From d68323f997736aa40070829eb4386ff2ac15002b Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Tue, 23 Feb 2021 15:16:57 +0000 Subject: [PATCH 1/8] [VIM-2238] Correctly place cursor at mid line of short files --- .../screen/MotionMiddleScreenLineAction.kt | 5 + .../idea/vim/helper/EditorHelper.java | 7 +- .../jetbrains/plugins/ideavim/VimTestCase.kt | 28 +++- .../MotionMiddleScreenLineActionTest.kt | 123 ++++++++++++++++++ 4 files changed, 157 insertions(+), 6 deletions(-) create mode 100644 test/org/jetbrains/plugins/ideavim/action/motion/screen/MotionMiddleScreenLineActionTest.kt diff --git a/src/com/maddyhome/idea/vim/action/motion/screen/MotionMiddleScreenLineAction.kt b/src/com/maddyhome/idea/vim/action/motion/screen/MotionMiddleScreenLineAction.kt index 2b6dfb87fb..cebd91e132 100644 --- a/src/com/maddyhome/idea/vim/action/motion/screen/MotionMiddleScreenLineAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/screen/MotionMiddleScreenLineAction.kt @@ -28,6 +28,11 @@ import com.maddyhome.idea.vim.handler.MotionActionHandler import com.maddyhome.idea.vim.helper.enumSetOf import java.util.* +/* + *M* +M To Middle line of window, on the first non-blank + character |linewise|. See also 'startofline' option. + */ class MotionMiddleScreenLineAction : MotionActionHandler.ForEachCaret() { override val flags: EnumSet = enumSetOf(CommandFlags.FLAG_SAVE_JUMP) diff --git a/src/com/maddyhome/idea/vim/helper/EditorHelper.java b/src/com/maddyhome/idea/vim/helper/EditorHelper.java index 995b8bc8a1..67e3840826 100644 --- a/src/com/maddyhome/idea/vim/helper/EditorHelper.java +++ b/src/com/maddyhome/idea/vim/helper/EditorHelper.java @@ -38,6 +38,7 @@ import java.util.List; import static java.lang.Integer.max; +import static java.lang.Integer.min; /** * This is a set of helper methods for working with editors. All line and column values are zero based. @@ -72,8 +73,12 @@ public static int getVisualLineAtTopOfScreen(final @NotNull Editor editor) { } public static int getVisualLineAtMiddleOfScreen(final @NotNull Editor editor) { + // The editor will return line numbers of virtual space if the text doesn't reach the end of the visible area + // (either because it's too short, or it's been scrolled up) + final int lastLineBaseline = editor.logicalPositionToXY(new LogicalPosition(getLineCount(editor), 0)).y; final Rectangle visibleArea = getVisibleArea(editor); - return editor.yToVisualLine(visibleArea.y + (visibleArea.height / 2)); + final int height = min(lastLineBaseline - visibleArea.y, visibleArea.height); + return editor.yToVisualLine(visibleArea.y + (height / 2)); } public static int getVisualLineAtBottomOfScreen(final @NotNull Editor editor) { diff --git a/test/org/jetbrains/plugins/ideavim/VimTestCase.kt b/test/org/jetbrains/plugins/ideavim/VimTestCase.kt index 872fc6ba63..d2de5bb259 100644 --- a/test/org/jetbrains/plugins/ideavim/VimTestCase.kt +++ b/test/org/jetbrains/plugins/ideavim/VimTestCase.kt @@ -29,6 +29,7 @@ import com.intellij.openapi.editor.Inlay import com.intellij.openapi.editor.LogicalPosition import com.intellij.openapi.editor.VisualPosition import com.intellij.openapi.editor.colors.EditorColors +import com.intellij.openapi.editor.ex.EditorEx import com.intellij.openapi.editor.ex.util.EditorUtil import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx import com.intellij.openapi.fileTypes.FileType @@ -151,6 +152,12 @@ abstract class VimTestCase : UsefulTestCase() { EditorTestUtil.setEditorVisibleSize(myFixture.editor, width, height) } + protected fun setEditorVirtualSpace() { + // Enable virtual space at the bottom of the file and force a layout to pick up the changes + myFixture.editor.settings.isAdditionalPageAtBottom = true + (myFixture.editor as EditorEx).scrollPane.viewport.doLayout() + } + 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) @@ -460,12 +467,23 @@ abstract class VimTestCase : UsefulTestCase() { protected val fileManager: FileEditorManagerEx get() = FileEditorManagerEx.getInstanceEx(myFixture.project) + // Specify width in columns, not pixels, just like we do for visible screen size. The default text char width differs + // per platform (e.g. Windows is 7, Mac is 8) so we can't guarantee correct positioning for tests if we use hard coded + // pixel widths 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)!! + val widthInPixels = EditorUtil.getPlainSpaceWidth(myFixture.editor) * widthInColumns + return EditorTestUtil.addInlay(myFixture.editor, offset, relatesToPrecedingText, widthInPixels) + } + + // As for inline inlays, height is specified as a multiplier of line height, as we can't guarantee the same line + // height on all platforms, so can't guarantee correct positioning for tests if we use pixels. This currently limits + // us to integer multiples of line heights. I don't think this will cause any issues, but we can change this to a + // float if necessary. We'd still be working scaled to the line height, so fractional values should still work. + protected fun addBlockInlay(offset: Int, showAbove: Boolean, heightInRows: Int): Inlay<*> { + val widthInColumns = 10 // Arbitrary width. We don't care. + val widthInPixels = EditorUtil.getPlainSpaceWidth(myFixture.editor) * widthInColumns + val heightInPixels = myFixture.editor.lineHeight * heightInRows + return EditorTestUtil.addBlockInlay(myFixture.editor, offset, false, showAbove, widthInPixels, heightInPixels) } // Disable or enable checks for the particular test diff --git a/test/org/jetbrains/plugins/ideavim/action/motion/screen/MotionMiddleScreenLineActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/motion/screen/MotionMiddleScreenLineActionTest.kt new file mode 100644 index 0000000000..de96b18af5 --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/motion/screen/MotionMiddleScreenLineActionTest.kt @@ -0,0 +1,123 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2021 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.motion.screen + +import com.maddyhome.idea.vim.helper.EditorHelper +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys +import org.jetbrains.plugins.ideavim.VimTestCase + +class MotionMiddleScreenLineActionTest : VimTestCase() { + fun `test move caret to middle line of full screen with odd number of lines`() { + assertEquals(35, screenHeight) + configureByLines(50, " I found it in a legendary land") + setPositionAndScroll(0, 0) + typeText(parseKeys("M")) + assertPosition(17, 4) + } + + fun `test move caret to middle line of full screen with even number of lines`() { + configureByLines(50, " I found it in a legendary land") + setEditorVisibleSize(screenWidth, 34) + setPositionAndScroll(0, 0) + typeText(parseKeys("M")) + assertPosition(17, 4) + } + + fun `test move caret to middle line of scrolled down screen`() { + assertEquals(35, screenHeight) + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(50, 50) + typeText(parseKeys("M")) + assertPosition(67, 4) + } + + fun `test move caret to middle line when file is shorter than screen`() { + assertEquals(35, screenHeight) + configureByLines(20, " I found it in a legendary land") + setPositionAndScroll(0, 0) + typeText(parseKeys("M")) + assertPosition(10, 4) + } + + fun `test move caret to middle line when file is shorter than screen 2`() { + assertEquals(35, screenHeight) + configureByLines(21, " I found it in a legendary land") + setPositionAndScroll(0, 0) + typeText(parseKeys("M")) + assertPosition(11, 4) + } + + fun `test move caret to middle line when file is shorter than screen 3`() { + configureByLines(20, " I found it in a legendary land") + setEditorVisibleSize(screenWidth, 34) + setPositionAndScroll(0, 0) + typeText(parseKeys("M")) + assertPosition(10, 4) + } + + fun `test move caret to middle line when file is shorter than screen 4`() { + configureByLines(21, " I found it in a legendary land") + setEditorVisibleSize(screenWidth, 34) + setPositionAndScroll(0, 0) + typeText(parseKeys("M")) + assertPosition(11, 4) + } + + fun `test move caret to middle line of visible lines with virtual space enabled`() { + configureByLines(30, " I found it in a legendary land") + setEditorVirtualSpace() + setPositionAndScroll(20, 20) + typeText(parseKeys("M")) + assertPosition(25, 4) + } + + fun `test move caret to middle line of screen with block inlays above`() { + // Move the caret to the line that is closest to the middle of the screen, rather than the numerically middle line + configureByLines(50, " I found it in a legendary land") + addBlockInlay(EditorHelper.getOffset(myFixture.editor, 5, 5), true, 5) + typeText(parseKeys("M")) + assertPosition(12, 4) + } + + fun `test move caret to middle line of screen with block inlays below`() { + // Move the caret to the line that is closest to the middle of the screen, rather than the numerically middle line + configureByLines(50, " I found it in a legendary land") + addBlockInlay(EditorHelper.getOffset(myFixture.editor, 25, 5), true, 5) + typeText(parseKeys("M")) + assertPosition(17, 4) + } + + fun `test move caret to middle line of screen with block inlays above and below`() { + // Move the caret to the line that is closest to the middle of the screen, rather than the numerically middle line + configureByLines(50, " I found it in a legendary land") + addBlockInlay(EditorHelper.getOffset(myFixture.editor, 5, 5), true, 5) + addBlockInlay(EditorHelper.getOffset(myFixture.editor, 25, 5), true, 5) + typeText(parseKeys("M")) + assertPosition(12, 4) + } + + fun `test move caret to middle line of screen with block inlays and a file shorter than the screen`() { + assertEquals(35, screenHeight) + configureByLines(21, " I found it in a legendary land") + addBlockInlay(EditorHelper.getOffset(myFixture.editor, 5, 5), true, 5) + setPositionAndScroll(0, 0) + typeText(parseKeys("M")) + assertPosition(8, 4) + } +} \ No newline at end of file From c2bd1a8c9a837b9a7e996c2f9e112cb0e9889ae8 Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Wed, 24 Feb 2021 23:12:21 +0000 Subject: [PATCH 2/8] Add tests and introduce 'startofline' option Update behaviour of H, L and M to handle 'scrolloff' correctly, operator pending mode and 'startofline' caret placement. Also implemented 'startofline' support for delete motion action. --- resources/META-INF/includes/VimActions.xml | 6 +- .../screen/MotionFirstScreenLineAction.kt | 29 +- .../screen/MotionLastScreenLineAction.kt | 25 +- .../screen/MotionMiddleScreenLineAction.kt | 2 +- .../idea/vim/ex/handler/RepeatHandler.kt | 2 +- .../maddyhome/idea/vim/ex/ranges/Ranges.kt | 4 +- .../maddyhome/idea/vim/group/ChangeGroup.java | 5 +- .../maddyhome/idea/vim/group/MotionGroup.java | 107 ++++--- .../vim/group/visual/VisualOperationChange.kt | 2 +- .../idea/vim/helper/EditorHelper.java | 8 +- .../idea/vim/option/OptionsManager.kt | 1 + src/com/maddyhome/idea/vim/package-info.java | 2 + .../jetbrains/plugins/ideavim/VimTestCase.kt | 42 ++- .../change/delete/DeleteMotionActionTest.kt | 27 +- .../delete/DeleteVisualLinesEndActionTest.kt | 52 +++- .../screen/MotionFirstScreenLineActionTest.kt | 179 +++++++++++ .../screen/MotionLastScreenLineActionTest.kt | 293 ++++++++++++++++++ .../MotionMiddleScreenLineActionTest.kt | 39 ++- 18 files changed, 730 insertions(+), 95 deletions(-) create mode 100644 test/org/jetbrains/plugins/ideavim/action/motion/screen/MotionFirstScreenLineActionTest.kt create mode 100644 test/org/jetbrains/plugins/ideavim/action/motion/screen/MotionLastScreenLineActionTest.kt diff --git a/resources/META-INF/includes/VimActions.xml b/resources/META-INF/includes/VimActions.xml index 2d121b363c..75df307032 100644 --- a/resources/META-INF/includes/VimActions.xml +++ b/resources/META-INF/includes/VimActions.xml @@ -126,8 +126,10 @@ - - + + + + diff --git a/src/com/maddyhome/idea/vim/action/motion/screen/MotionFirstScreenLineAction.kt b/src/com/maddyhome/idea/vim/action/motion/screen/MotionFirstScreenLineAction.kt index 80dc7e9a5b..cdeb1bacac 100644 --- a/src/com/maddyhome/idea/vim/action/motion/screen/MotionFirstScreenLineAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/screen/MotionFirstScreenLineAction.kt @@ -22,13 +22,24 @@ import com.intellij.openapi.editor.Caret import com.intellij.openapi.editor.Editor import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.command.Argument +import com.maddyhome.idea.vim.command.Command import com.maddyhome.idea.vim.command.CommandFlags import com.maddyhome.idea.vim.command.MotionType import com.maddyhome.idea.vim.handler.MotionActionHandler import com.maddyhome.idea.vim.helper.enumSetOf import java.util.* -class MotionFirstScreenLineAction : MotionActionHandler.ForEachCaret() { +/* + *H* +H To line [count] from top (Home) of window (default: + first line on the window) on the first non-blank + character |linewise|. See also 'startofline' option. + Cursor is adjusted for 'scrolloff' option, unless an + operator is pending, in which case the text may + scroll. E.g. "yH" yanks from the first visible line + until the cursor line (inclusive). + */ +abstract class MotionFirstScreenLineActionBase(private val operatorPending: Boolean) : MotionActionHandler.ForEachCaret() { override val flags: EnumSet = enumSetOf(CommandFlags.FLAG_SAVE_JUMP) override val motionType: MotionType = MotionType.LINE_WISE @@ -41,6 +52,20 @@ class MotionFirstScreenLineAction : MotionActionHandler.ForEachCaret() { rawCount: Int, argument: Argument? ): Int { - return VimPlugin.getMotion().moveCaretToFirstScreenLine(editor, count) + + // Only apply scrolloff for NX motions. For op pending, use the actual first line and apply scrolloff after. + // E.g. yH will yank from first visible line to current line, but it also moves the caret to the first visible line. + // This is inside scrolloff, so Vim scrolls + return VimPlugin.getMotion().moveCaretToFirstScreenLine(editor, caret, count, !operatorPending) + } + + override fun postMove(editor: Editor, caret: Caret, context: DataContext, cmd: Command) { + if (operatorPending) { + // Convert current caret line from a 0-based logical line to a 1-based logical line + VimPlugin.getMotion().scrollLineToFirstScreenLine(editor, caret.logicalPosition.line + 1, false) + } } } + +class MotionFirstScreenLineAction : MotionFirstScreenLineActionBase(false) +class MotionOpPendingFirstScreenLineAction : MotionFirstScreenLineActionBase(true) diff --git a/src/com/maddyhome/idea/vim/action/motion/screen/MotionLastScreenLineAction.kt b/src/com/maddyhome/idea/vim/action/motion/screen/MotionLastScreenLineAction.kt index 9013281784..23adee0d22 100644 --- a/src/com/maddyhome/idea/vim/action/motion/screen/MotionLastScreenLineAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/screen/MotionLastScreenLineAction.kt @@ -22,13 +22,24 @@ import com.intellij.openapi.editor.Caret import com.intellij.openapi.editor.Editor import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.command.Argument +import com.maddyhome.idea.vim.command.Command import com.maddyhome.idea.vim.command.CommandFlags import com.maddyhome.idea.vim.command.MotionType import com.maddyhome.idea.vim.handler.MotionActionHandler import com.maddyhome.idea.vim.helper.enumSetOf import java.util.* -class MotionLastScreenLineAction : MotionActionHandler.ForEachCaret() { +/* + *L* +L To line [count] from bottom of window (default: Last + line on the window) on the first non-blank character + |linewise|. See also 'startofline' option. + Cursor is adjusted for 'scrolloff' option, unless an + operator is pending, in which case the text may + scroll. E.g. "yL" yanks from the cursor to the last + visible line. + */ +abstract class MotionLastScreenLineActionBase(private val operatorPending: Boolean) : MotionActionHandler.ForEachCaret() { override val flags: EnumSet = enumSetOf(CommandFlags.FLAG_SAVE_JUMP) override val motionType: MotionType = MotionType.LINE_WISE @@ -41,6 +52,16 @@ class MotionLastScreenLineAction : MotionActionHandler.ForEachCaret() { rawCount: Int, argument: Argument? ): Int { - return VimPlugin.getMotion().moveCaretToLastScreenLine(editor, count) + return VimPlugin.getMotion().moveCaretToLastScreenLine(editor, caret, count, !operatorPending) + } + + override fun postMove(editor: Editor, caret: Caret, context: DataContext, cmd: Command) { + if (operatorPending) { + // Convert current caret line from a 0-based logical line to a 1-based logical line + VimPlugin.getMotion().scrollLineToFirstScreenLine(editor, caret.logicalPosition.line + 1, false) + } } } + +class MotionLastScreenLineAction : MotionLastScreenLineActionBase(false) +class MotionOpPendingLastScreenLineAction : MotionLastScreenLineActionBase(true) diff --git a/src/com/maddyhome/idea/vim/action/motion/screen/MotionMiddleScreenLineAction.kt b/src/com/maddyhome/idea/vim/action/motion/screen/MotionMiddleScreenLineAction.kt index cebd91e132..a16f5db39f 100644 --- a/src/com/maddyhome/idea/vim/action/motion/screen/MotionMiddleScreenLineAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/screen/MotionMiddleScreenLineAction.kt @@ -46,6 +46,6 @@ class MotionMiddleScreenLineAction : MotionActionHandler.ForEachCaret() { rawCount: Int, argument: Argument? ): Int { - return VimPlugin.getMotion().moveCaretToMiddleScreenLine(editor) + return VimPlugin.getMotion().moveCaretToMiddleScreenLine(editor, caret) } } diff --git a/src/com/maddyhome/idea/vim/ex/handler/RepeatHandler.kt b/src/com/maddyhome/idea/vim/ex/handler/RepeatHandler.kt index 8bb8db101e..fbe4a2f9f7 100644 --- a/src/com/maddyhome/idea/vim/ex/handler/RepeatHandler.kt +++ b/src/com/maddyhome/idea/vim/ex/handler/RepeatHandler.kt @@ -47,7 +47,7 @@ class RepeatHandler : CommandHandler.ForEachCaret() { MotionGroup.moveCaret( editor, caret, - VimPlugin.getMotion().moveCaretToLine(editor, line, editor.caretModel.primaryCaret) + VimPlugin.getMotion().moveCaretToLineWithSameColumn(editor, line, editor.caretModel.primaryCaret) ) if (arg == ':') { diff --git a/src/com/maddyhome/idea/vim/ex/ranges/Ranges.kt b/src/com/maddyhome/idea/vim/ex/ranges/Ranges.kt index fff37f9169..f950529d04 100644 --- a/src/com/maddyhome/idea/vim/ex/ranges/Ranges.kt +++ b/src/com/maddyhome/idea/vim/ex/ranges/Ranges.kt @@ -153,7 +153,7 @@ class Ranges { if (range.isMove) { MotionGroup.moveCaret( editor, editor.caretModel.primaryCaret, - VimPlugin.getMotion().moveCaretToLine(editor, endLine, editor.caretModel.primaryCaret) + VimPlugin.getMotion().moveCaretToLineWithSameColumn(editor, endLine, editor.caretModel.primaryCaret) ) } // Did that last range represent the start of the file? @@ -177,7 +177,7 @@ class Ranges { if (range.isMove) MotionGroup.moveCaret( editor, caret, - VimPlugin.getMotion().moveCaretToLine(editor, endLine, editor.caretModel.primaryCaret) + VimPlugin.getMotion().moveCaretToLineWithSameColumn(editor, endLine, editor.caretModel.primaryCaret) ) lastZero = endLine < 0 ++count diff --git a/src/com/maddyhome/idea/vim/group/ChangeGroup.java b/src/com/maddyhome/idea/vim/group/ChangeGroup.java index 626bfd00f1..11a4ef5846 100644 --- a/src/com/maddyhome/idea/vim/group/ChangeGroup.java +++ b/src/com/maddyhome/idea/vim/group/ChangeGroup.java @@ -1140,6 +1140,9 @@ public boolean deleteRange(@NotNull Editor editor, @Nullable SelectionType type, boolean isChange) { + // Update the last column before we delete, or we might be retrieving the data for a line that no longer exists + UserDataManager.setVimLastColumn(caret, InlayHelperKt.getInlayAwareVisualColumn(caret)); + boolean removeLastNewLine = removeLastNewLine(editor, range, type); final boolean res = deleteText(editor, range, type); if (removeLastNewLine) { @@ -1150,7 +1153,7 @@ public boolean deleteRange(@NotNull Editor editor, if (res) { int pos = EditorHelper.normalizeOffset(editor, range.getStartOffset(), isChange); if (type == SelectionType.LINE_WISE) { - pos = VimPlugin.getMotion().moveCaretToLineStart(editor, editor.offsetToLogicalPosition(pos).line); + pos = VimPlugin.getMotion().moveCaretToLineWithStartOfLineOption(editor, editor.offsetToLogicalPosition(pos).line, caret); } MotionGroup.moveCaret(editor, caret, pos); } diff --git a/src/com/maddyhome/idea/vim/group/MotionGroup.java b/src/com/maddyhome/idea/vim/group/MotionGroup.java index b7b195e173..4eef560b57 100755 --- a/src/com/maddyhome/idea/vim/group/MotionGroup.java +++ b/src/com/maddyhome/idea/vim/group/MotionGroup.java @@ -58,6 +58,8 @@ import static com.maddyhome.idea.vim.group.ChangeGroup.*; import static com.maddyhome.idea.vim.helper.EditorHelper.*; +import static java.lang.Math.max; +import static java.lang.Math.min; /** * This handles all motion related commands and marks @@ -151,7 +153,7 @@ else if (cmd.getAction() instanceof TextObjectActionHandler) { if (cmd.isLinewiseMotion()) { if (caret.getLogicalPosition().line != getLineCount(editor) - 1) { start = getLineStartForOffset(editor, start); - end = Math.min(getLineEndForOffset(editor, end) + 1, EditorHelperRt.getFileSize(editor)); + end = min(getLineEndForOffset(editor, end) + 1, EditorHelperRt.getFileSize(editor)); } else { start = getLineStartForOffset(editor, start); @@ -268,7 +270,7 @@ private static int getScrollScreenTargetCaretVisualLine(final @NotNull Editor ed } public int moveCaretToNthCharacter(@NotNull Editor editor, int count) { - return Math.max(0, Math.min(count, EditorHelperRt.getFileSize(editor) - 1)); + return max(0, min(count, EditorHelperRt.getFileSize(editor) - 1)); } private static int getScrollOption(int rawCount) { @@ -611,7 +613,7 @@ 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); + final int column = max(0, caretVisualPosition.column - scrollOffset); scrollColumnToLeftOfScreen(editor, caretVisualPosition.line, column); return true; } @@ -641,13 +643,15 @@ private static void scrollCaretIntoViewVertically(@NotNull Editor editor, final // Ironically, after figuring out how Vim's algorithm works (although not *why*) and reimplementing, it looks likely // that this needs to be replaced as a more or less dumb line for line rewrite. + // This algorithm is based on screen height, so get the non-normalised line number at the bottom of the screen, even + // if the text ends sooner final int topLine = getVisualLineAtTopOfScreen(editor); - final int bottomLine = getVisualLineAtBottomOfScreen(editor); + final int bottomLine = getNonNormalizedVisualLineAtBottomOfScreen(editor); // 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, bottomLine - scrollOffset); + final int bottomBound = max(topBound, 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 @@ -701,22 +705,22 @@ private static void scrollCaretIntoViewVertically(@NotNull Editor editor, final } else if (caretLine < topBound) { // Scrolling up, put the cursor at the top of the window (minus scrolloff) // Initial approximation in move.c:update_topline (including same calculation for halfHeight) - if (topLine + scrollOffset - caretLine >= Math.max(2, (height / 2) - 1)) { + if (topLine + scrollOffset - caretLine >= max(2, (height / 2) - 1)) { scrollVisualLineToMiddleOfScreen(editor, caretLine); } else { // 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); + final int scrollJumpTopLine = max(0, (caretLine < topLine) ? caretLine - scrollJump + 1 : topLine - scrollJump); + final int scrollOffsetTopLine = max(0, caretLine - scrollOffset); + final int newTopLine = min(scrollOffsetTopLine, scrollJumpTopLine); // Used is set to the line height of caretLine (1 or how many lines soft wraps take up), and then incremented by // the line heights of the lines above and below caretLine (up to scrolloff or end of file). // Our implementation ignores soft wrap line heights. Folds already have a line height of 1. final int usedAbove = caretLine - newTopLine; - final int usedBelow = Math.min(scrollOffset, getVisualLineCount(editor) - caretLine); + final int usedBelow = min(scrollOffset, getVisualLineCount(editor) - caretLine); final int used = 1 + usedAbove + usedBelow; if (used > height) { scrollVisualLineToMiddleOfScreen(editor, caretLine); @@ -738,7 +742,7 @@ else if (caretLine > bottomBound) { // 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 extra = max(scrollOffset, scrollJump - 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 @@ -748,8 +752,8 @@ else if (caretLine > bottomBound) { // 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); + final int usedBelow = min(getVisualLineCount(editor) - caretLine, usedAbove - 1); + final int used = 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. @@ -773,10 +777,10 @@ private static int getScrollJump(@NotNull Editor editor, int height) { if (scrollJump) { final int scrollJumpSize = OptionsManager.INSTANCE.getScrolljump().value(); if (scrollJumpSize < 0) { - return (int) (height * (Math.min(100, -scrollJumpSize) / 100.0)); + return (int) (height * (min(100, -scrollJumpSize) / 100.0)); } else { - return Math.max(1, scrollJumpSize); + return max(1, scrollJumpSize); } } return 1; @@ -808,7 +812,7 @@ private static void scrollCaretIntoViewHorizontally(@NotNull Editor editor, diff = sidescroll; } if (offsetLeft < 0) { - scrollColumnToLeftOfScreen(editor, position.line, Math.max(0, currentVisualLeftColumn - diff)); + scrollColumnToLeftOfScreen(editor, position.line, max(0, currentVisualLeftColumn - diff)); } else { scrollColumnToRightOfScreen(editor, position.line, normalizeVisualColumn(editor, position.line, currentVisualRightColumn + diff, false)); @@ -817,16 +821,16 @@ private static void scrollCaretIntoViewHorizontally(@NotNull Editor editor, } } - public int moveCaretToFirstScreenLine(@NotNull Editor editor, int count) { - return moveCaretToScreenLocation(editor, ScreenLocation.TOP, count); + public int moveCaretToFirstScreenLine(@NotNull Editor editor, @NotNull Caret caret, int count, boolean normalizeToScreen) { + return moveCaretToScreenLocation(editor, caret, ScreenLocation.TOP, count - 1, normalizeToScreen); } - public int moveCaretToLastScreenLine(@NotNull Editor editor, int count) { - return moveCaretToScreenLocation(editor, ScreenLocation.BOTTOM, count); + public int moveCaretToLastScreenLine(@NotNull Editor editor, @NotNull Caret caret, int count, boolean normalizeToScreen) { + return moveCaretToScreenLocation(editor, caret, ScreenLocation.BOTTOM, count - 1, normalizeToScreen); } - public int moveCaretToMiddleScreenLine(@NotNull Editor editor) { - return moveCaretToScreenLocation(editor, ScreenLocation.MIDDLE, 0); + public int moveCaretToMiddleScreenLine(@NotNull Editor editor, @NotNull Caret caret) { + return moveCaretToScreenLocation(editor, caret, ScreenLocation.MIDDLE, 0, false); } public boolean scrollLine(@NotNull Editor editor, int lines) { @@ -947,7 +951,7 @@ public int moveCaretToMiddleColumn(@NotNull Editor editor, @NotNull Caret caret) final int width = getApproximateScreenWidth(editor) / 2; final int len = getLineLength(editor); - return moveCaretToColumn(editor, caret, Math.max(0, Math.min(len - 1, width)), false); + return moveCaretToColumn(editor, caret, max(0, min(len - 1, width)), false); } public int moveCaretToColumn(@NotNull Editor editor, @NotNull Caret caret, int count, boolean allowEnd) { @@ -1036,7 +1040,7 @@ public int moveCaretToLineScreenEnd(@NotNull Editor editor, @NotNull Caret caret public int moveCaretHorizontalWrap(@NotNull Editor editor, @NotNull Caret caret, int count) { // FIX - allows cursor over newlines int oldOffset = caret.getOffset(); - int offset = Math.min(Math.max(0, caret.getOffset() + count), EditorHelperRt.getFileSize(editor)); + int offset = min(max(0, caret.getOffset() + count), EditorHelperRt.getFileSize(editor)); if (offset == oldOffset) { return -1; } @@ -1104,7 +1108,7 @@ else if (pages < 0) { return false; } - public int moveCaretToLine(@NotNull Editor editor, int logicalLine, @NotNull Caret caret) { + public int moveCaretToLineWithSameColumn(@NotNull Editor editor, int logicalLine, @NotNull Caret caret) { int col = UserDataManager.getVimLastColumn(caret); int line = logicalLine; if (logicalLine < 0) { @@ -1121,6 +1125,15 @@ else if (logicalLine >= getLineCount(editor)) { return editor.logicalPositionToOffset(newPos); } + public int moveCaretToLineWithStartOfLineOption(@NotNull Editor editor, int logicalLine, @NotNull Caret caret) { + if (OptionsManager.INSTANCE.getStartofline().isSet()) { + return moveCaretToLineStartSkipLeading(editor, logicalLine); + } + else { + return moveCaretToLineWithSameColumn(editor, logicalLine, caret); + } + } + public boolean scrollScreen(final @NotNull Editor editor, int rawCount, boolean down) { final CaretModel caretModel = editor.getCaretModel(); final int currentLogicalLine = caretModel.getLogicalPosition().line; @@ -1161,7 +1174,7 @@ public boolean scrollScreen(final @NotNull Editor editor, int rawCount, boolean final int visualTop = getVisualLineAtTopOfScreen(editor) + scrollOffset; final int visualBottom = getVisualLineAtBottomOfScreen(editor) - scrollOffset; - targetCaretVisualLine = Math.max(visualTop, Math.min(visualBottom, targetCaretVisualLine)); + targetCaretVisualLine = max(visualTop, min(visualBottom, targetCaretVisualLine)); } int logicalLine = visualLineToLogicalLine(editor, targetCaretVisualLine); @@ -1341,47 +1354,55 @@ public int selectNextSearch(@NotNull Editor editor, int count, boolean forwards) if (range == null) return -1; final int adj = VimPlugin.getVisualMotion().getSelectionAdj(); if (!CommandStateHelper.inVisualMode(editor)) { - final int startOffset = forwards ? range.getStartOffset() : Math.max(range.getEndOffset() - adj, 0); + final int startOffset = forwards ? range.getStartOffset() : max(range.getEndOffset() - adj, 0); MotionGroup.moveCaret(editor, caret, startOffset); VimPlugin.getVisualMotion().enterVisualMode(editor, CommandState.SubMode.VISUAL_CHARACTER); } - return forwards ? Math.max(range.getEndOffset() - adj, 0) : range.getStartOffset(); + return forwards ? max(range.getEndOffset() - adj, 0) : range.getStartOffset(); } private int lastFTCmd = 0; private char lastFTChar; - // [count] is a visual line offset, which means it's 1 based. The value is ignored for ScreenLocation.MIDDLE + // visualLineOffset is a zero based offset to subtract from the direction of travel, where zero is the same as a count + // of 1. I.e. 1L = L, which is an offset of zero. 2L is an offset of 1 extra line + // When normalizeToScreen is true, the offset is bounded to the current screen dimensions, and scrolloff is applied. + // When false, the offset is used directly, and scrolloff is not applied. This is used for op pending motions + // (scrolloff is applied after) private int moveCaretToScreenLocation(@NotNull Editor editor, + @NotNull Caret caret, @NotNull ScreenLocation screenLocation, - int visualLineOffset) { - final int scrollOffset = getNormalizedScrollOffset(editor); + int visualLineOffset, + boolean normalizeToScreen) { - int topVisualLine = getVisualLineAtTopOfScreen(editor); - int bottomVisualLine = getVisualLineAtBottomOfScreen(editor); + final int scrollOffset = normalizeToScreen ? getNormalizedScrollOffset(editor) : 0; + + final int maxVisualLine = getVisualLineCount(editor); + + final int topVisualLine = getVisualLineAtTopOfScreen(editor); + final int topScrollOff = topVisualLine > 0 ? scrollOffset : 0; + + final int bottomVisualLine = getVisualLineAtBottomOfScreen(editor); + final int bottomScrollOff = bottomVisualLine < (maxVisualLine - 1) ? scrollOffset : 0; - // Don't apply scrolloff if we're at the top or bottom of the file - int offsetTopVisualLine = topVisualLine > 0 ? topVisualLine + scrollOffset : topVisualLine; - int offsetBottomVisualLine = - bottomVisualLine < getVisualLineCount(editor) ? bottomVisualLine - scrollOffset : bottomVisualLine; + final int topMaxVisualLine = normalizeToScreen ? bottomVisualLine - bottomScrollOff : maxVisualLine; + final int bottomMinVisualLine = normalizeToScreen ? topVisualLine + topScrollOff : 0; - // [count]H/[count]L moves caret to that screen line, bounded by top/bottom scroll offsets int targetVisualLine = 0; switch (screenLocation) { case TOP: - targetVisualLine = Math.max(offsetTopVisualLine, topVisualLine + visualLineOffset - 1); - targetVisualLine = Math.min(targetVisualLine, offsetBottomVisualLine); + targetVisualLine = min(topVisualLine + max(topScrollOff, visualLineOffset), topMaxVisualLine); break; case MIDDLE: targetVisualLine = getVisualLineAtMiddleOfScreen(editor); break; case BOTTOM: - targetVisualLine = Math.min(offsetBottomVisualLine, bottomVisualLine - visualLineOffset + 1); - targetVisualLine = Math.max(targetVisualLine, offsetTopVisualLine); + targetVisualLine = max(bottomVisualLine - max(bottomScrollOff, visualLineOffset), bottomMinVisualLine); break; } - return moveCaretToLineStartSkipLeading(editor, visualLineToLogicalLine(editor, targetVisualLine)); + final int targetLogicalLine = visualLineToLogicalLine(editor, targetVisualLine); + return moveCaretToLineWithStartOfLineOption(editor, targetLogicalLine, caret); } public int moveCaretToLineEndOffset(@NotNull Editor editor, diff --git a/src/com/maddyhome/idea/vim/group/visual/VisualOperationChange.kt b/src/com/maddyhome/idea/vim/group/visual/VisualOperationChange.kt index 42184b84ea..4d66172069 100644 --- a/src/com/maddyhome/idea/vim/group/visual/VisualOperationChange.kt +++ b/src/com/maddyhome/idea/vim/group/visual/VisualOperationChange.kt @@ -91,7 +91,7 @@ object VisualOperation { val endLine = (sp.line + linesDiff).coerceAtMost(editor.document.lineCount - 1) return when (type) { - SelectionType.LINE_WISE -> VimPlugin.getMotion().moveCaretToLine(editor, endLine, caret) + SelectionType.LINE_WISE -> VimPlugin.getMotion().moveCaretToLineWithSameColumn(editor, endLine, caret) SelectionType.CHARACTER_WISE -> when { lines > 1 -> VimPlugin.getMotion() .moveCaretToLineStart(editor, endLine) + min(EditorHelper.getLineLength(editor, endLine), chars) diff --git a/src/com/maddyhome/idea/vim/helper/EditorHelper.java b/src/com/maddyhome/idea/vim/helper/EditorHelper.java index 67e3840826..be035423ca 100644 --- a/src/com/maddyhome/idea/vim/helper/EditorHelper.java +++ b/src/com/maddyhome/idea/vim/helper/EditorHelper.java @@ -81,12 +81,18 @@ public static int getVisualLineAtMiddleOfScreen(final @NotNull Editor editor) { return editor.yToVisualLine(visibleArea.y + (height / 2)); } - public static int getVisualLineAtBottomOfScreen(final @NotNull Editor editor) { + public static int getNonNormalizedVisualLineAtBottomOfScreen(final @NotNull Editor editor) { + // The editor will return line numbers of virtual space if the text doesn't reach the end of the visible area + // (either because it's too short, or it's been scrolled up) final Rectangle visibleArea = getVisibleArea(editor); return getFullVisualLine(editor, visibleArea.y + visibleArea.height, visibleArea.y, visibleArea.y + visibleArea.height); } + public static int getVisualLineAtBottomOfScreen(final @NotNull Editor editor) { + return normalizeVisualLine(editor, getNonNormalizedVisualLineAtBottomOfScreen(editor)); + } + /** * Gets the number of characters on the current line. This will be different than the number of visual * characters if there are "real" tabs in the line. diff --git a/src/com/maddyhome/idea/vim/option/OptionsManager.kt b/src/com/maddyhome/idea/vim/option/OptionsManager.kt index fde1d32404..65fc4a45dc 100644 --- a/src/com/maddyhome/idea/vim/option/OptionsManager.kt +++ b/src/com/maddyhome/idea/vim/option/OptionsManager.kt @@ -75,6 +75,7 @@ object OptionsManager { val sidescroll = addOption(NumberOption("sidescroll", "ss", 0)) val sidescrolloff = addOption(NumberOption("sidescrolloff", "siso", 0)) val smartcase = addOption(ToggleOption(SmartCaseOptionsData.name, SmartCaseOptionsData.abbr, false)) + val startofline = addOption(ToggleOption("startofline", "sol", true)) val ideajoin = addOption(IdeaJoinOptionsData.option) val timeout = addOption(ToggleOption("timeout", "to", true)) val timeoutlen = addOption(NumberOption("timeoutlen", "tm", 1000, -1, Int.MAX_VALUE)) diff --git a/src/com/maddyhome/idea/vim/package-info.java b/src/com/maddyhome/idea/vim/package-info.java index 02e63d6f66..e97cc6b9b5 100644 --- a/src/com/maddyhome/idea/vim/package-info.java +++ b/src/com/maddyhome/idea/vim/package-info.java @@ -164,10 +164,12 @@ * |F| {@link com.maddyhome.idea.vim.action.motion.leftright.MotionLeftMatchCharAction} * |G| {@link com.maddyhome.idea.vim.action.motion.updown.MotionGotoLineLastAction} * |H| {@link com.maddyhome.idea.vim.action.motion.screen.MotionFirstScreenLineAction} + * |H| {@link com.maddyhome.idea.vim.action.motion.screen.MotionOpPendingFirstScreenLineAction} * |I| {@link com.maddyhome.idea.vim.action.change.insert.InsertBeforeFirstNonBlankAction} * |J| {@link com.maddyhome.idea.vim.action.change.delete.DeleteJoinLinesSpacesAction} * |K| {@link com.maddyhome.idea.vim.action.editor.VimQuickJavaDoc} * |L| {@link com.maddyhome.idea.vim.action.motion.screen.MotionLastScreenLineAction} + * |L| {@link com.maddyhome.idea.vim.action.motion.screen.MotionOpPendingLastScreenLineAction} * |M| {@link com.maddyhome.idea.vim.action.motion.screen.MotionMiddleScreenLineAction} * |N| {@link com.maddyhome.idea.vim.action.motion.search.SearchAgainPreviousAction} * |O| {@link com.maddyhome.idea.vim.action.change.insert.InsertNewLineAboveAction} diff --git a/test/org/jetbrains/plugins/ideavim/VimTestCase.kt b/test/org/jetbrains/plugins/ideavim/VimTestCase.kt index d2de5bb259..8711b0d5b0 100644 --- a/test/org/jetbrains/plugins/ideavim/VimTestCase.kt +++ b/test/org/jetbrains/plugins/ideavim/VimTestCase.kt @@ -185,9 +185,10 @@ abstract class VimTestCase : UsefulTestCase() { protected fun configureByLines(lineCount: Int, line: String) { val stringBuilder = StringBuilder() - repeat(lineCount) { + repeat(lineCount - 1) { stringBuilder.appendln(line) } + stringBuilder.append(line) configureByText(stringBuilder.toString()) } @@ -211,27 +212,21 @@ abstract class VimTestCase : UsefulTestCase() { OptionsManager.scrolloff.set(0) OptionsManager.scrolljump.set(1) - // Convert to visual lines to handle any collapsed folds - val scrollToVisualLine = EditorHelper.logicalLineToVisualLine(myFixture.editor, scrollToLogicalLine) - val bottomVisualLine = scrollToVisualLine + EditorHelper.getApproximateScreenHeight(myFixture.editor) - 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, scrollToVisualLine) - val boundsBottom = EditorHelper.visualLineToLogicalLine(myFixture.editor, bottomVisualLine) - Assert.assertTrue( - "Caret line $caretLogicalLine not inside legal screen bounds ($boundsTop - $boundsBottom)", - caretLogicalLine in boundsTop..boundsBottom - ) - typeText(parseKeys("${scrollToLogicalLine + 1}z", "${caretLogicalLine + 1}G", "${caretLogicalColumn + 1}|")) OptionsManager.scrolljump.set(scrolljump) OptionsManager.scrolloff.set(scrolloff) - // Make sure we're where we want to be - assertVisibleArea(scrollToLogicalLine, bottomLogicalLine) + // Make sure we're where we want to be. If there are block inlays, we can't easily assert the bottom line because + // we'd have to duplicate the scrolling logic here. Asserting top when we know height is good enough + assertTopLogicalLine(scrollToLogicalLine) assertPosition(caretLogicalLine, caretLogicalColumn) + + // Belt and braces. Let's make sure that the caret is fully onscreen + val bottomLogicalLine = EditorHelper.visualLineToLogicalLine(myFixture.editor, + EditorHelper.getVisualLineAtBottomOfScreen(myFixture.editor)) + assertTrue(bottomLogicalLine >= caretLogicalLine) + assertTrue(caretLogicalLine >= scrollToLogicalLine) } protected fun typeText(keys: List): Editor { @@ -292,12 +287,21 @@ 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) { + assertTopLogicalLine(topLogicalLine) + assertBottomLogicalLine(bottomLogicalLine) + } + + fun assertTopLogicalLine(topLogicalLine: Int) { val actualVisualTop = EditorHelper.getVisualLineAtTopOfScreen(myFixture.editor) val actualLogicalTop = EditorHelper.visualLineToLogicalLine(myFixture.editor, actualVisualTop) + + Assert.assertEquals("Top logical lines don't match", topLogicalLine, actualLogicalTop) + } + + fun assertBottomLogicalLine(bottomLogicalLine: Int) { 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) } @@ -315,6 +319,10 @@ abstract class VimTestCase : UsefulTestCase() { Assert.assertEquals(expected, actual) } + fun assertLineCount(expected: Int) { + assertEquals(expected, EditorHelper.getLineCount(myFixture.editor)) + } + fun putMapping(modes: Set, from: String, to: String, recursive: Boolean) { VimPlugin.getKey().putKeyMapping(modes, parseKeys(from), MappingOwner.IdeaVim, parseKeys(to), recursive) } diff --git a/test/org/jetbrains/plugins/ideavim/action/change/delete/DeleteMotionActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/change/delete/DeleteMotionActionTest.kt index 13e50f360e..05321b4200 100644 --- a/test/org/jetbrains/plugins/ideavim/action/change/delete/DeleteMotionActionTest.kt +++ b/test/org/jetbrains/plugins/ideavim/action/change/delete/DeleteMotionActionTest.kt @@ -24,16 +24,11 @@ import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.command.CommandState 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 class DeleteMotionActionTest : VimTestCase() { - @VimBehaviorDiffers( - originalVimAfter = """ - def xxx(): - ${c}expression one - """ - ) fun `test delete last line`() { typeTextInFile( parseKeys("dd"), @@ -46,11 +41,29 @@ class DeleteMotionActionTest : VimTestCase() { myFixture.checkResult( """ def xxx(): - ${c} expression one + ${c}expression one """.trimIndent() ) } + fun `test delete last line with nostartofline`() { + OptionsManager.startofline.reset() + typeTextInFile( + parseKeys("dd"), + """ + |def xxx(): + | expression one + | expression${c} two + """.trimMargin() + ) + myFixture.checkResult( + """ + |def xxx(): + | expression${c} one + """.trimMargin() + ) + } + @VimBehaviorDiffers(originalVimAfter = " expression two\n") fun `test delete last line stored with new line`() { typeTextInFile( diff --git a/test/org/jetbrains/plugins/ideavim/action/change/delete/DeleteVisualLinesEndActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/change/delete/DeleteVisualLinesEndActionTest.kt index a66990f938..018544e3e5 100644 --- a/test/org/jetbrains/plugins/ideavim/action/change/delete/DeleteVisualLinesEndActionTest.kt +++ b/test/org/jetbrains/plugins/ideavim/action/change/delete/DeleteVisualLinesEndActionTest.kt @@ -22,7 +22,7 @@ package org.jetbrains.plugins.ideavim.action.change.delete import com.maddyhome.idea.vim.command.CommandState import com.maddyhome.idea.vim.helper.StringHelper.parseKeys -import com.maddyhome.idea.vim.helper.VimBehaviorDiffers +import com.maddyhome.idea.vim.option.OptionsManager import com.maddyhome.idea.vim.option.VirtualEditData import org.jetbrains.plugins.ideavim.SkipNeovimReason import org.jetbrains.plugins.ideavim.TestWithoutNeovim @@ -111,17 +111,30 @@ class DeleteVisualLinesEndActionTest : VimOptionTestCase(VirtualEditData.name) { ) } - @VimBehaviorDiffers( - originalVimAfter = """ + @VimOptionDefaultAll + fun `test simple deletion with indent`() { + val keys = listOf("v", "D") + val before = """ + A Discovery + + I ${c}found 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}all rocks and lavender and tufted grass, where it was settled on some sodden sand hard by the torrent of a mountain pass. - """ - ) + """.trimIndent() + doTest(keys, before, after, CommandState.Mode.COMMAND, CommandState.SubMode.NONE) + } + @VimOptionDefaultAll - fun `test simple deletion with indent`() { + fun `test simple deletion with indent and nostartofline`() { + OptionsManager.startofline.reset() val keys = listOf("v", "D") val before = """ A Discovery @@ -134,7 +147,7 @@ class DeleteVisualLinesEndActionTest : VimOptionTestCase(VirtualEditData.name) { val after = """ A Discovery - ${c} all rocks and lavender and tufted grass, + ${c} all rocks and lavender and tufted grass, where it was settled on some sodden sand hard by the torrent of a mountain pass. """.trimIndent() @@ -329,17 +342,30 @@ class DeleteVisualLinesEndActionTest : VimOptionTestCase(VirtualEditData.name) { doTest(keys, before, after, CommandState.Mode.COMMAND, CommandState.SubMode.NONE) } - @VimBehaviorDiffers( - originalVimAfter = """ + @VimOptionDefaultAll + fun `test line deletion with indent`() { + val keys = listOf("V", "D") + val before = """ + A Discovery + + I ${c}found 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}all rocks and lavender and tufted grass, where it was settled on some sodden sand hard by the torrent of a mountain pass. - """ - ) + """.trimIndent() + doTest(keys, before, after, CommandState.Mode.COMMAND, CommandState.SubMode.NONE) + } + @VimOptionDefaultAll - fun `test line deletion with indent`() { + fun `test line deletion with indent and nostartofline`() { + OptionsManager.startofline.reset() val keys = listOf("V", "D") val before = """ A Discovery @@ -352,7 +378,7 @@ class DeleteVisualLinesEndActionTest : VimOptionTestCase(VirtualEditData.name) { val after = """ A Discovery - ${c} all rocks and lavender and tufted grass, + ${c} all rocks and lavender and tufted grass, where it was settled on some sodden sand hard by the torrent of a mountain pass. """.trimIndent() diff --git a/test/org/jetbrains/plugins/ideavim/action/motion/screen/MotionFirstScreenLineActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/motion/screen/MotionFirstScreenLineActionTest.kt new file mode 100644 index 0000000000..4b9b7f798b --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/motion/screen/MotionFirstScreenLineActionTest.kt @@ -0,0 +1,179 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2021 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.motion.screen + +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 + +class MotionFirstScreenLineActionTest : VimTestCase() { + fun `test move caret to first line of screen`() { + configureByLines(50, " I found it in a legendary land") + setPositionAndScroll(0, 20) + typeText(parseKeys("H")) + assertPosition(0, 4) + } + + fun `test move caret to first line of screen further down file`() { + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(40, 60) + typeText(parseKeys("H")) + assertPosition(40, 4) + } + + fun `test move caret to count line from top of screen`() { + configureByLines(50, " I found it in a legendary land") + setPositionAndScroll(0, 20) + typeText(parseKeys("10H")) + assertPosition(9, 4) + } + + fun `test move caret to count line from top of screen further down file`() { + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(40, 60) + typeText(parseKeys("10H")) + assertPosition(49, 4) + } + + fun `test move caret to too large count line from top of screen`() { + assertEquals(35, screenHeight) + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(40, 60) + typeText(parseKeys("100H")) + assertPosition(74, 4) + } + + fun `test move caret ignores scrolloff when top of screen is top of file`() { + OptionsManager.scrolloff.set(10) + configureByLines(50, " I found it in a legendary land") + setPositionAndScroll(0, 20) + typeText(parseKeys("H")) + assertPosition(0, 4) + } + + fun `test move caret applies scrolloff when top of screen is not top of file`() { + OptionsManager.scrolloff.set(10) + configureByLines(50, " I found it in a legendary land") + setPositionAndScroll(1, 20) + typeText(parseKeys("H")) + assertPosition(11, 4) + } + + fun `test move caret applies scrolloff when top of screen is not top of file 2`() { + OptionsManager.scrolloff.set(10) + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(20, 40) + typeText(parseKeys("H")) + assertPosition(30, 4) + } + + fun `test move caret to first screen line with count and scrolloff at top of file`() { + OptionsManager.scrolloff.set(10) + configureByLines(50, " I found it in a legendary land") + setPositionAndScroll(0, 20) + typeText(parseKeys("5H")) + assertPosition(4, 4) + } + + fun `test move caret to first screen line with count and scrolloff not at top of file`() { + OptionsManager.scrolloff.set(10) + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(20, 40) + typeText(parseKeys("5H")) + assertPosition(30, 4) + } + + fun `test operator pending acts to first screen line` () { + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(20, 40, 10) + typeText(parseKeys("dH")) + assertPosition(20, 4) + assertLineCount(79) + } + + fun `test operator pending acts on count line from top of screen`() { + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(20, 40, 10) + typeText(parseKeys("d5H")) + assertPosition(24, 4) + } + + fun `test operator pending acts to first screen line with nostartofline` () { + OptionsManager.startofline.reset() + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(20, 40, 10) + typeText(parseKeys("dH")) + assertPosition(20, 10) + } + + fun `test operator pending acts on count line from top of screen with nostartofline`() { + OptionsManager.startofline.reset() + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(20, 40, 10) + typeText(parseKeys("d5H")) + assertPosition(24, 10) + } + + fun `test operator pending acts to first screen line and then scrolls scrolloff`() { + OptionsManager.scrolloff.set(10) + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(20, 40) + typeText(parseKeys("dH")) + assertPosition(20, 4) + assertVisibleArea(10, 44) + } + + fun `test move caret to same column with nostartofline`() { + OptionsManager.startofline.reset() + configureByLines(50, " I found it in a legendary land") + setPositionAndScroll(0, 20, 10) + typeText(parseKeys("H")) + assertPosition(0, 10) + } + + fun `test move caret to end of shorter line with nostartofline`() { + OptionsManager.startofline.reset() + configureByLines(70, " I found it in a legendary land") + setPositionAndScroll(10, 30, 10) + typeText(parseKeys("A", " extra text", "")) + typeText(parseKeys("H")) + assertPosition(10, 33) + } + + fun `test move caret to first line of screen with inlays`() { + // We're not scrolling, so inlays don't affect anything. Just place the caret on the first visible line + configureByLines(50, " I found it in a legendary land") + addBlockInlay(EditorHelper.getOffset(myFixture.editor, 5, 5), true, 10) + setPositionAndScroll(0, 20, 10) + typeText(parseKeys("H")) + assertPosition(0, 4) + } + + fun `test keep caret on screen when count is greater than visible lines plus inlays`() { + assertEquals(35, screenHeight) + configureByLines(50, " I found it in a legendary land") + addBlockInlay(EditorHelper.getOffset(myFixture.editor, 5, 5), true, 10) + setPositionAndScroll(0, 20, 10) + // Should move to the 34th visible line. We have space for 35 lines, but we're using some of that for inlays + typeText(parseKeys("34H")) + assertPosition(24, 4) + } +} + diff --git a/test/org/jetbrains/plugins/ideavim/action/motion/screen/MotionLastScreenLineActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/motion/screen/MotionLastScreenLineActionTest.kt new file mode 100644 index 0000000000..e54865ea1f --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/motion/screen/MotionLastScreenLineActionTest.kt @@ -0,0 +1,293 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2021 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.motion.screen + +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 + +class MotionLastScreenLineActionTest : VimTestCase() { + fun `test move caret to last line of screen`() { + configureByLines(50, " I found it in a legendary land") + setPositionAndScroll(0, 20) + typeText(parseKeys("L")) + assertPosition(34, 4) + } + + fun `test move caret to last line when last line of file is less than screen`() { + assertEquals(35, screenHeight) + configureByLines(20, " I found it in a legendary land") + typeText(parseKeys("L")) + assertPosition(19, 4) + } + + fun `test move caret to last line of screen when bottom of file is scrolled up`() { + assertEquals(35, screenHeight) + configureByLines(38, " I found it in a legendary land") + setPositionAndScroll(3, 5) + typeText(parseKeys("L")) + assertPosition(37, 4) + assertTopLogicalLine(3) + } + + fun `test move caret to last line of screen when bottom of file is scrolled up with virtual space`() { + assertEquals(35, screenHeight) + configureByLines(38, " I found it in a legendary land") + setEditorVirtualSpace() + setPositionAndScroll(15, 20) + typeText(parseKeys("L")) + assertPosition(37, 4) + assertTopLogicalLine(15) + } + + fun `test move caret to count line from bottom of screen`() { + configureByLines(50, " I found it in a legendary land") + setPositionAndScroll(0, 20) + typeText(parseKeys("10L")) + assertPosition(25, 4) + } + + fun `test move caret to too large count line from bottom of screen`() { + configureByLines(50, " I found it in a legendary land") + setPositionAndScroll(0, 20) + typeText(parseKeys("100L")) + assertPosition(0, 4) + } + + fun `test move caret to too large count line from bottom of screen 2`() { + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(20, 40) + typeText(parseKeys("100L")) + assertPosition(20, 4) + } + + fun `test move caret ignores scrolloff when bottom of screen is bottom of file`() { + assertEquals(35, screenHeight) + OptionsManager.scrolloff.set(10) + configureByLines(35, " I found it in a legendary land") + typeText(parseKeys("L")) + assertPosition(34, 4) + } + + fun `test move caret applies scrolloff when bottom of screen is not bottom of file`() { + assertEquals(35, screenHeight) + OptionsManager.scrolloff.set(10) + configureByLines(50, " I found it in a legendary land") + typeText(parseKeys("L")) + assertPosition(24, 4) + } + + fun `test move caret to last screen line with count and scrolloff at bottom of file`() { + assertEquals(35, screenHeight) + OptionsManager.scrolloff.set(10) + configureByLines(35, " I found it in a legendary land") + typeText(parseKeys("5L")) + assertPosition(30, 4) + } + + fun `test move caret to last screen line with count and scrolloff not at bottom of file`() { + assertEquals(35, screenHeight) + OptionsManager.scrolloff.set(10) + configureByLines(50, " I found it in a legendary land") + typeText(parseKeys("5L")) + assertPosition(24, 4) + } + + fun `test move caret ignores scrolloff with large count at top of file`() { + assertEquals(35, screenHeight) + OptionsManager.scrolloff.set(10) + configureByLines(50, " I found it in a legendary land") + setPositionAndScroll(0, 20) + typeText(parseKeys("100L")) + assertPosition(0, 4) + assertTopLogicalLine(0) + } + + fun `test move caret applies scrolloff with large count when not at top of file`() { + assertEquals(35, screenHeight) + OptionsManager.scrolloff.set(10) + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(20, 40) + typeText(parseKeys("100L")) + assertPosition(30, 4) + assertTopLogicalLine(20) + } + + fun `test operator pending acts to last screen line`() { + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(20, 40, 10) + typeText(parseKeys("dL")) + assertPosition(40, 4) + assertLineCount(85) + } + + fun `test operator pending acts to last screen line with scrolloff`() { + // Current caret location is the start of the operator range and doesn't get moved to the end, so there is no + // scrolling, and scrolloff does not apply + OptionsManager.scrolloff.set(10) + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(20, 40, 10) + typeText(parseKeys("dL")) + assertTopLogicalLine(20) + assertPosition(40, 4) + assertLineCount(85) + } + + fun `test operator pending acts on count line from bottom of screen`() { + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(20, 40, 10) + typeText(parseKeys("d5L")) + assertPosition(40, 4) + assertLineCount(89) + } + + fun `test operator pending acts on large count line from bottom of screen`() { + // Operator range is from current line to bottom of screen minus count. + // 35 high screen, 100 high file. Top line is 20, caret is 40, bottom is 54. d25L will delete from 40 to 54-25=29. + // Range gets reversed, so we delete :29-40d. Caret stays at 40. + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(20, 40, 10) + typeText(parseKeys("d25L")) + assertPosition(30, 4) + assertLineCount(89) + } + + fun `test operator pending acts on count line from bottom of screen with scrolloff`() { + // Current caret location is the start of the operator range and doesn't get moved to the end, so there is no + // scrolling, and scrolloff does not apply + OptionsManager.scrolloff.set(10) + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(20, 40, 10) + typeText(parseKeys("d5L")) + assertTopLogicalLine(20) + assertPosition(40, 4) + assertLineCount(89) + } + + fun `test operator pending acts on large count from bottom of screen with scrolloff`() { + // Current caret location is the start of the operator range and doesn't get moved to the end, so there is no + // scrolling, and scrolloff does not apply + OptionsManager.scrolloff.set(10) + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(20, 40, 10) + typeText(parseKeys("d35L")) + assertTopLogicalLine(10) + assertPosition(20, 4) + assertLineCount(79) + } + + fun `test operator pending acts on large count line from bottom of screen with scrolloff and without virtual space`() { + // When using a large count, the range is effectively reversed, and the current caret location becomes the end of + // the range, and is moved, so scrolloff can apply + // 50 high file. Top line 20, caret at 40. d35L will delete from current line to 35 lines up from the bottom of the + // screen. There are only 10 actual lines below, so this will delete from current line up to current line - 10 = 30 + OptionsManager.scrolloff.set(10) + configureByLines(50, " I found it in a legendary land") + setPositionAndScroll(20, 40, 10) + typeText(parseKeys("d35L")) + assertTopLogicalLine(0) + assertPosition(15, 4) + assertLineCount(24) + } + + fun `test operator pending acts on large count line from bottom of screen with scrolloff and virtual space`() { + // When using a large count, the range is effectively reversed, and the current caret location becomes the end of + // the range, and is moved, so scrolloff can apply + // 50 high file. Top line 20, caret at 40. d35L will delete from current line to 35 lines up from the bottom of the + // screen. There are only 10 actual lines below, so this will delete from current line up to current line - 10 = 30 + OptionsManager.scrolloff.set(10) + configureByLines(50, " I found it in a legendary land") + setEditorVirtualSpace() + setPositionAndScroll(20, 40, 10) + typeText(parseKeys("d35L")) + assertTopLogicalLine(5) + assertPosition(15, 4) + assertLineCount(24) + } + + fun `test operator pending acts to last screen line with nostartofline`() { + OptionsManager.startofline.reset() + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(20, 40, 10) + typeText(parseKeys("dL")) + assertPosition(40, 10) + assertLineCount(85) + } + + fun `test operator pending acts on count line from bottom of screen with nostartofline`() { + OptionsManager.startofline.reset() + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(20, 40, 10) + typeText(parseKeys("d5L")) + assertPosition(40, 10) + assertLineCount(89) + } + + fun `test move caret to same column with nostartofline`() { + OptionsManager.startofline.reset() + configureByLines(50, " I found it in a legendary land") + setPositionAndScroll(10, 30, 10) + typeText(parseKeys("L")) + assertPosition(44, 10) + } + + fun `test move caret to end of shorter line with nostartofline`() { + OptionsManager.startofline.reset() + configureByLines(50, " I found it in a legendary land") + setPositionAndScroll(10, 30, 10) + typeText(parseKeys("A", " extra text", "")) + typeText(parseKeys("L")) + assertPosition(44, 33) + } + + fun `test move caret to last line of screen with inlays`() { + // 35 high, with an inlay that is 10 rows high. Bottom line will be 25 (1 based) + configureByLines(50, " I found it in a legendary land") + addBlockInlay(EditorHelper.getOffset(myFixture.editor, 20, 5), true, 10) + setPositionAndScroll(0, 10, 10) + typeText(parseKeys("L")) + assertPosition(24, 4) + assertBottomLogicalLine(24) + } + + fun `test move caret to last line of screen with inlays and scrolloff`() { + // 35 high, with an inlay that is 10 rows high. Bottom line will be 25 (1 based), scrolloff of 10 puts caret at 15 + OptionsManager.scrolloff.set(10) + configureByLines(50, " I found it in a legendary land") + addBlockInlay(EditorHelper.getOffset(myFixture.editor, 20, 5), true, 10) + setPositionAndScroll(0, 10, 10) + typeText(parseKeys("L")) + assertPosition(14, 4) + assertBottomLogicalLine(24) + } + + fun `test keep caret on screen when count is greater than visible lines plus inlays`() { + // Screen is 35 high. Top line is 21 (1 based), inlay starts at 26, is 10 rows high and bottom line is 45. + // Caret is at line 31. 35L should go to 34 lines above bottom line (L == 1L), which would be 11, which would be off + // screen. Screen doesn't scroll, caret remains on screen at existing top line - 21 + assertEquals(35, screenHeight) + configureByLines(100, " I found it in a legendary land") + addBlockInlay(EditorHelper.getOffset(myFixture.editor, 25, 5), true, 10) + setPositionAndScroll(20, 30, 10) + typeText(parseKeys("35L")) + assertPosition(20, 4) + assertBottomLogicalLine(44) + } +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/action/motion/screen/MotionMiddleScreenLineActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/motion/screen/MotionMiddleScreenLineActionTest.kt index de96b18af5..c3961f31ca 100644 --- a/test/org/jetbrains/plugins/ideavim/action/motion/screen/MotionMiddleScreenLineActionTest.kt +++ b/test/org/jetbrains/plugins/ideavim/action/motion/screen/MotionMiddleScreenLineActionTest.kt @@ -20,6 +20,7 @@ package org.jetbrains.plugins.ideavim.action.motion.screen 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 class MotionMiddleScreenLineActionTest : VimTestCase() { @@ -60,7 +61,7 @@ class MotionMiddleScreenLineActionTest : VimTestCase() { configureByLines(21, " I found it in a legendary land") setPositionAndScroll(0, 0) typeText(parseKeys("M")) - assertPosition(11, 4) + assertPosition(10, 4) } fun `test move caret to middle line when file is shorter than screen 3`() { @@ -76,7 +77,7 @@ class MotionMiddleScreenLineActionTest : VimTestCase() { setEditorVisibleSize(screenWidth, 34) setPositionAndScroll(0, 0) typeText(parseKeys("M")) - assertPosition(11, 4) + assertPosition(10, 4) } fun `test move caret to middle line of visible lines with virtual space enabled`() { @@ -87,6 +88,40 @@ class MotionMiddleScreenLineActionTest : VimTestCase() { assertPosition(25, 4) } + fun `test move caret to same column with nostartofline`() { + OptionsManager.startofline.reset() + configureByLines(50, " I found it in a legendary land") + setPositionAndScroll(0, 0, 10) + typeText(parseKeys("M")) + assertPosition(17, 10) + } + + fun `test move caret to end of shorter line with nostartofline`() { + OptionsManager.startofline.reset() + configureByLines(70, " I found it in a legendary land") + setPositionAndScroll(0, 0, 10) + typeText(parseKeys("A", " extra text", "")) + typeText(parseKeys("M")) + assertPosition(17, 33) + } + + fun `test operator pending acts to middle line`() { + configureByLines(20, " I found it in a legendary land") + setPositionAndScroll(0, 4, 10) + typeText(parseKeys("dM")) + assertPosition(4, 4) + assertLineCount(13) + } + + fun `test operator pending acts to middle line with nostartofline`() { + OptionsManager.startofline.reset() + configureByLines(20, " I found it in a legendary land") + setPositionAndScroll(0, 4, 10) + typeText(parseKeys("dM")) + assertPosition(4, 10) + assertLineCount(13) + } + fun `test move caret to middle line of screen with block inlays above`() { // Move the caret to the line that is closest to the middle of the screen, rather than the numerically middle line configureByLines(50, " I found it in a legendary land") From 283e9612ea7f511ebaa6c4e1bb51e276e45edf17 Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Wed, 24 Feb 2021 23:19:29 +0000 Subject: [PATCH 3/8] Add 'startofline' support to goto line action --- .../maddyhome/idea/vim/ex/CommandParser.kt | 10 +- .../idea/vim/ex/handler/GotoLineHandler.kt | 3 +- .../plugins/ideavim/ex/RangeTest.java | 12 ++ .../ideavim/ex/handler/GotoLineHandlerTest.kt | 196 ++++++++++++++++++ 4 files changed, 215 insertions(+), 6 deletions(-) create mode 100644 test/org/jetbrains/plugins/ideavim/ex/handler/GotoLineHandlerTest.kt diff --git a/src/com/maddyhome/idea/vim/ex/CommandParser.kt b/src/com/maddyhome/idea/vim/ex/CommandParser.kt index 05198fede8..bbcfdaae39 100644 --- a/src/com/maddyhome/idea/vim/ex/CommandParser.kt +++ b/src/com/maddyhome/idea/vim/ex/CommandParser.kt @@ -208,7 +208,7 @@ object CommandParser { val argument = StringBuilder() // The command's argument(s) var location: StringBuffer? = null // The current range text var offsetSign = 1 // Sign of current range offset - var offsetNumber = 0 // The value of the current range offset + var offsetNumber = -1 // The value of the current range offset var offsetTotal = 0 // The sum of all the current range offsets var move = false // , vs. ; separated ranges (true=; false=,) var patternType = 0.toChar() // ? or / @@ -251,7 +251,7 @@ object CommandParser { State.RANGE -> { location = StringBuffer() offsetTotal = 0 - offsetNumber = 0 + offsetNumber = -1 move = false if (ch in '0'..'9') { state = State.RANGE_LINE @@ -401,7 +401,7 @@ object CommandParser { } State.RANGE_OFFSET -> { // Figure out the sign of the offset and reset the offset value - offsetNumber = 0 + offsetNumber = -1 if (ch == '+') { offsetSign = 1 } else if (ch == '-') { @@ -418,7 +418,7 @@ object CommandParser { } State.RANGE_OFFSET_DONE -> { // No number implies a one - if (offsetNumber == 0) { + if (offsetNumber == -1) { offsetNumber = 1 } // Update offset total for this range @@ -433,7 +433,7 @@ object CommandParser { } State.RANGE_OFFSET_NUM -> // Update the value of the current offset if (ch in '0'..'9') { - offsetNumber = offsetNumber * 10 + (ch - '0') + offsetNumber = (if (offsetNumber == -1) 0 else offsetNumber) * 10 + (ch - '0') state = State.RANGE_OFFSET_MAYBE_DONE reprocess = false } else if (ch == '+' || ch == '-') { diff --git a/src/com/maddyhome/idea/vim/ex/handler/GotoLineHandler.kt b/src/com/maddyhome/idea/vim/ex/handler/GotoLineHandler.kt index efeba667b7..ee56e169d7 100644 --- a/src/com/maddyhome/idea/vim/ex/handler/GotoLineHandler.kt +++ b/src/com/maddyhome/idea/vim/ex/handler/GotoLineHandler.kt @@ -53,7 +53,8 @@ class GotoLineHandler : CommandHandler.ForEachCaret() { val line = min(cmd.getLine(editor, caret), EditorHelper.getLineCount(editor) - 1) if (line >= 0) { - MotionGroup.moveCaret(editor, caret, VimPlugin.getMotion().moveCaretToLineStartSkipLeading(editor, line)) + val offset = VimPlugin.getMotion().moveCaretToLineWithStartOfLineOption(editor, line, caret) + MotionGroup.moveCaret(editor, caret, offset) return true } diff --git a/test/org/jetbrains/plugins/ideavim/ex/RangeTest.java b/test/org/jetbrains/plugins/ideavim/ex/RangeTest.java index f1b83b1898..165a322e47 100644 --- a/test/org/jetbrains/plugins/ideavim/ex/RangeTest.java +++ b/test/org/jetbrains/plugins/ideavim/ex/RangeTest.java @@ -66,6 +66,12 @@ public void testOffsetWithNoNumber() { myFixture.checkResult("1\n2\n3\n5\n"); } + public void testOffsetWithZero() { + myFixture.configureByText("a.txt", "1\n2\n3\n4\n5\n"); + typeText(commandToKeys(".+0d")); + myFixture.checkResult("1\n2\n4\n5\n"); + } + public void testTwoOffsetsWithSameSign() { myFixture.configureByText("a.txt", "1\n2\n3\n4\n5\n"); typeText(commandToKeys(".+1+1d")); @@ -78,6 +84,12 @@ public void testTwoOffsetsWithDifferentSign() { myFixture.checkResult("1\n2\n4\n5\n"); } + public void testMultipleZeroOffsets() { + myFixture.configureByText("a.txt", "1\n2\n3\n4\n5\n"); + typeText(commandToKeys(".+0-0d")); + myFixture.checkResult("1\n3\n4\n5\n"); + } + public void testSearchForward() { myFixture.configureByText("a.txt", "c\na\nb\nc\nd\ne\n"); typeText(commandToKeys("/c/d")); diff --git a/test/org/jetbrains/plugins/ideavim/ex/handler/GotoLineHandlerTest.kt b/test/org/jetbrains/plugins/ideavim/ex/handler/GotoLineHandlerTest.kt new file mode 100644 index 0000000000..2f18201c1b --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/ex/handler/GotoLineHandlerTest.kt @@ -0,0 +1,196 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2021 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.ex.handler + +import com.maddyhome.idea.vim.option.OptionsManager +import org.jetbrains.plugins.ideavim.VimTestCase + +class GotoLineHandlerTest : VimTestCase() { + fun `test goto explicit line`() { + val before = """ + A Discovery + + I found it in a legendary land + all rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the ${c}torrent of a mountain pass. + """.trimIndent() + configureByText(before) + enterCommand("3") + val after = """ + A Discovery + + ${c}I found 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() + myFixture.checkResult(after) + } + + fun `test goto positive relative line`() { + val before = """ + A Discovery + + I found it ${c}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) + enterCommand("+2") + val after = """ + A Discovery + + I found it in a legendary land + all rocks and lavender and tufted grass, + ${c}where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent() + myFixture.checkResult(after) + } + + fun `test goto negative relative line`() { + val before = """ + A Discovery + + I found it in a legendary land + all rocks and lavender and tufted grass, + where it ${c}was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent() + configureByText(before) + enterCommand("-2") + val after = """ + A Discovery + + ${c}I found 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() + myFixture.checkResult(after) + } + + fun `test goto line moves to first non-blank char`() { + val before = """ + A Discovery + + I found it in a legendary land + all rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the ${c}torrent of a mountain pass. + """.trimIndent() + configureByText(before) + enterCommand("3") + val after = """ + A Discovery + + ${c}I found 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() + myFixture.checkResult(after) + } + + fun `test goto zero relative line moves to first non-blank char on current line`() { + val before = """ + A Discovery + + I found it ${c}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) + enterCommand("+0") + val after = """ + A Discovery + + ${c}I found 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() + myFixture.checkResult(after) + } + + fun `test goto line moves to same column with nostartofline option`() { + OptionsManager.startofline.reset() + val before = """ + A Discovery + + I found it in a legendary land + all rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the ${c}torrent of a mountain pass. + """.trimIndent() + configureByText(before) + enterCommand("3") + val after = """ + 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() + myFixture.checkResult(after) + } + + fun `test goto zero relative line with nostartofline option does not move caret`() { + OptionsManager.startofline.reset() + val before = """ + A Discovery + + I found it ${c}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) + enterCommand("+0") + val after = """ + A Discovery + + I found it ${c}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() + myFixture.checkResult(after) + } + + fun `test goto line with scrolloff`() { + OptionsManager.scrolloff.set(10) + configureByLines(100, " I found it in a legendary land") + enterCommand("30") + assertPosition(29, 4) + assertTopLogicalLine(5) + } + + fun `test goto relative line with scrolloff`() { + OptionsManager.scrolloff.set(10) + configureByLines(100, " I found it in a legendary land") + enterCommand("+30") + assertPosition(30, 4) + assertTopLogicalLine(6) + } +} \ No newline at end of file From f6c4d407a0f158c8b5a9c29483bc90198a872362 Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Thu, 25 Feb 2021 00:36:23 +0000 Subject: [PATCH 4/8] Add 'startofline' support to G, gg and i_ --- .../updown/MotionGotoLineFirstAction.kt | 7 +- .../motion/updown/MotionGotoLineLastAction.kt | 8 +- .../maddyhome/idea/vim/group/MotionGroup.java | 11 -- .../ideavim/action/MultipleCaretsTest.java | 2 +- .../updown/MotionGotoLineFirstActionTest.kt | 164 ++++++++++++++++++ .../MotionGotoLineFirstInsertActionTest.kt | 72 ++++++++ .../updown/MotionGotoLineLastActionTest.kt | 117 +++++++++++++ 7 files changed, 366 insertions(+), 15 deletions(-) create mode 100644 test/org/jetbrains/plugins/ideavim/action/motion/updown/MotionGotoLineFirstActionTest.kt create mode 100644 test/org/jetbrains/plugins/ideavim/action/motion/updown/MotionGotoLineFirstInsertActionTest.kt diff --git a/src/com/maddyhome/idea/vim/action/motion/updown/MotionGotoLineFirstAction.kt b/src/com/maddyhome/idea/vim/action/motion/updown/MotionGotoLineFirstAction.kt index a5225c8217..c058153b51 100644 --- a/src/com/maddyhome/idea/vim/action/motion/updown/MotionGotoLineFirstAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/updown/MotionGotoLineFirstAction.kt @@ -26,6 +26,7 @@ import com.maddyhome.idea.vim.command.Argument import com.maddyhome.idea.vim.command.CommandFlags import com.maddyhome.idea.vim.command.MotionType import com.maddyhome.idea.vim.handler.MotionActionHandler +import com.maddyhome.idea.vim.helper.EditorHelper import com.maddyhome.idea.vim.helper.enumSetOf import java.util.* @@ -42,7 +43,8 @@ class MotionGotoLineFirstAction : MotionActionHandler.ForEachCaret() { rawCount: Int, argument: Argument? ): Int { - return VimPlugin.getMotion().moveCaretGotoLineFirst(editor, count - 1) + val line = EditorHelper.normalizeLine(editor, count - 1) + return VimPlugin.getMotion().moveCaretToLineWithStartOfLineOption(editor, line, caret) } } @@ -59,6 +61,7 @@ class MotionGotoLineFirstInsertAction : MotionActionHandler.ForEachCaret() { rawCount: Int, argument: Argument? ): Int { - return VimPlugin.getMotion().moveCaretGotoLineFirst(editor, count - 1) + val line = EditorHelper.normalizeLine(editor, count - 1) + return VimPlugin.getMotion().moveCaretToLineStart(editor, line) } } diff --git a/src/com/maddyhome/idea/vim/action/motion/updown/MotionGotoLineLastAction.kt b/src/com/maddyhome/idea/vim/action/motion/updown/MotionGotoLineLastAction.kt index 87ae5c3758..d8ef1a7668 100644 --- a/src/com/maddyhome/idea/vim/action/motion/updown/MotionGotoLineLastAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/updown/MotionGotoLineLastAction.kt @@ -25,6 +25,7 @@ import com.maddyhome.idea.vim.command.Argument import com.maddyhome.idea.vim.command.CommandFlags import com.maddyhome.idea.vim.command.MotionType import com.maddyhome.idea.vim.handler.MotionActionHandler +import com.maddyhome.idea.vim.helper.EditorHelper import com.maddyhome.idea.vim.helper.enumSetOf import java.util.* @@ -41,6 +42,11 @@ class MotionGotoLineLastAction : MotionActionHandler.ForEachCaret() { rawCount: Int, argument: Argument? ): Int { - return VimPlugin.getMotion().moveCaretGotoLineLast(editor, rawCount) + val line = EditorHelper.normalizeLine(editor, if (rawCount == 0) { + EditorHelper.getLineCount(editor) - 1 + } else { + rawCount - 1 + }) + return VimPlugin.getMotion().moveCaretToLineWithStartOfLineOption(editor, line, caret) } } diff --git a/src/com/maddyhome/idea/vim/group/MotionGroup.java b/src/com/maddyhome/idea/vim/group/MotionGroup.java index 4eef560b57..6e70eeb978 100755 --- a/src/com/maddyhome/idea/vim/group/MotionGroup.java +++ b/src/com/maddyhome/idea/vim/group/MotionGroup.java @@ -1208,10 +1208,6 @@ public int moveCaretToLineEnd(@NotNull Editor editor, int line, boolean allowPas return normalizeOffset(editor, line, getLineEndOffset(editor, line, allowPastEnd), allowPastEnd); } - public int moveCaretGotoLineFirst(@NotNull Editor editor, int line) { - return moveCaretToLineStartSkipLeading(editor, line); - } - // Scrolls current or [count] line to given screen location // In Vim, [count] refers to a file line, so it's a one-based logical line private void scrollLineToScreenLocation(@NotNull Editor editor, @@ -1310,13 +1306,6 @@ public int moveCaretToLinePercent(@NotNull Editor editor, int count) { return moveCaretToLineStartSkipLeading(editor, normalizeLine(editor, (getLineCount(editor) * count + 99) / 100 - 1)); } - public int moveCaretGotoLineLast(@NotNull Editor editor, int rawCount) { - final int line = - rawCount == 0 ? normalizeLine(editor, getLineCount(editor) - 1) : rawCount - 1; - - return moveCaretToLineStartSkipLeading(editor, line); - } - public int moveCaretGotoLineLastEnd(@NotNull Editor editor, int rawCount, int line, boolean pastEnd) { return moveCaretToLineEnd(editor, rawCount == 0 ? normalizeLine(editor, getLineCount(editor) - 1) diff --git a/test/org/jetbrains/plugins/ideavim/action/MultipleCaretsTest.java b/test/org/jetbrains/plugins/ideavim/action/MultipleCaretsTest.java index 122c37583c..f2e29c0f65 100644 --- a/test/org/jetbrains/plugins/ideavim/action/MultipleCaretsTest.java +++ b/test/org/jetbrains/plugins/ideavim/action/MultipleCaretsTest.java @@ -1133,7 +1133,7 @@ public void testMotionGoToLineFirst() { "dsfadsf fg dsfg sdfjgkfdgl jsdf" + "nflgj sd\n dflgj dfdsfg\n dfsgj sdfklgj"); myFixture - .checkResult(" sdf" + "dsfadsf fg dsfg sdfjgkfdgl jsdf" + "nflgj sd\n dflgj dfdsfg\n dfsgj sdfklgj"); + .checkResult(" sdf" + "dsfadsf fg dsfg sdfjgkfdgl jsdf" + "nflgj sd\n dflgj dfdsfg\n dfsgj sdfklgj"); } public void testMotionGotoLineLastEnd() { diff --git a/test/org/jetbrains/plugins/ideavim/action/motion/updown/MotionGotoLineFirstActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/motion/updown/MotionGotoLineFirstActionTest.kt new file mode 100644 index 0000000000..eae729bf1b --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/motion/updown/MotionGotoLineFirstActionTest.kt @@ -0,0 +1,164 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2021 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.motion.updown + +import com.maddyhome.idea.vim.command.CommandState +import com.maddyhome.idea.vim.option.OptionsManager +import org.jetbrains.plugins.ideavim.VimTestCase + +class MotionGotoLineFirstActionTest : VimTestCase() { + fun `test simple motion`() { + doTest( + "gg", + """ + A Discovery + + I found it in a legendary land + all ${c}rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + """ + ${c}A Discovery + + I found 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(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE + ) + } + + fun `test motion with count`() { + doTest( + "5gg", + """ + A ${c}Discovery + + I found 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(), + """ + A Discovery + + I found it in a legendary land + all rocks and lavender and tufted grass, + ${c}where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE + ) + } + + fun `test motion with large count`() { + doTest( + "100gg", + """ + A ${c}Discovery + + I found 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(), + """ + A Discovery + + I found it in a legendary land + all rocks and lavender and tufted grass, + where it was settled on some sodden sand + ${c}hard by the torrent of a mountain pass. + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE + ) + } + + fun `test motion with zero count`() { + doTest( + "0gg", + """ + A Discovery + + I found it in a legendary land + all ${c}rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + """ + ${c}A Discovery + + I found 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(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE + ) + } + + fun `test moves caret to first non-blank char`() { + doTest( + "gg", + """ + | A Discovery + | + | I found it in a legendary land + | all ${c}rocks and lavender and tufted grass, + | where it was settled on some sodden sand + | hard by the torrent of a mountain pass. + """.trimMargin(), + """ + | ${c}A Discovery + | + | I found 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. + """.trimMargin(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE + ) + } + + fun `test moves caret to same column with nostartofline`() { + OptionsManager.startofline.reset() + doTest( + "gg", + """ + | A Discovery + | + | I found it in a legendary land + | all ${c}rocks and lavender and tufted grass, + | where it was settled on some sodden sand + | hard by the torrent of a mountain pass. + """.trimMargin(), + """ + | A Di${c}scovery + | + | I found 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. + """.trimMargin(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE + ) + } +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/action/motion/updown/MotionGotoLineFirstInsertActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/motion/updown/MotionGotoLineFirstInsertActionTest.kt new file mode 100644 index 0000000000..3a2f5a8885 --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/motion/updown/MotionGotoLineFirstInsertActionTest.kt @@ -0,0 +1,72 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2021 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.motion.updown + +import com.maddyhome.idea.vim.command.CommandState +import com.maddyhome.idea.vim.option.OptionsManager +import org.jetbrains.plugins.ideavim.VimTestCase + +class MotionGotoLineFirstInsertActionTest : VimTestCase() { + fun `test simple motion`() { + doTest( + listOf("i", "", ""), + """ + | A Discovery + | + | I found it in a legendary land + | all ${c}rocks and lavender and tufted grass, + | where it was settled on some sodden sand + | hard by the torrent of a mountain pass. + """.trimMargin(), + """ + |${c} A Discovery + | + | I found 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. + """.trimMargin(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE + ) + } + + fun `test simple motion ignores nostartofline`() { + OptionsManager.startofline.reset() + doTest( + listOf("i", "", ""), + """ + | A Discovery + | + | I found it in a legendary land + | all ${c}rocks and lavender and tufted grass, + | where it was settled on some sodden sand + | hard by the torrent of a mountain pass. + """.trimMargin(), + """ + |${c} A Discovery + | + | I found 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. + """.trimMargin(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE + ) + } +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/action/motion/updown/MotionGotoLineLastActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/motion/updown/MotionGotoLineLastActionTest.kt index 55198473f9..11acb19bac 100644 --- a/test/org/jetbrains/plugins/ideavim/action/motion/updown/MotionGotoLineLastActionTest.kt +++ b/test/org/jetbrains/plugins/ideavim/action/motion/updown/MotionGotoLineLastActionTest.kt @@ -19,6 +19,7 @@ package org.jetbrains.plugins.ideavim.action.motion.updown import com.maddyhome.idea.vim.command.CommandState +import com.maddyhome.idea.vim.option.OptionsManager import org.jetbrains.plugins.ideavim.VimTestCase class MotionGotoLineLastActionTest : VimTestCase() { @@ -45,6 +46,122 @@ class MotionGotoLineLastActionTest : VimTestCase() { ) } + fun `test motion with count`() { + doTest( + "5G", + """ + A ${c}Discovery + + I found 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(), + """ + A Discovery + + I found it in a legendary land + all rocks and lavender and tufted grass, + ${c}where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE + ) + } + + fun `test motion with large count`() { + doTest( + "100G", + """ + A ${c}Discovery + + I found 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(), + """ + A Discovery + + I found it in a legendary land + all rocks and lavender and tufted grass, + where it was settled on some sodden sand + ${c}hard by the torrent of a mountain pass. + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE + ) + } + + fun `test motion with zero count`() { + doTest( + "0G", + """ + A ${c}Discovery + + I found 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(), + """ + A Discovery + + I found it in a legendary land + all rocks and lavender and tufted grass, + where it was settled on some sodden sand + ${c}hard by the torrent of a mountain pass. + """.trimIndent(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE + ) + } + + fun `test moves caret to first non-blank char`() { + doTest( + "G", + """ + | A Discovery + | + | I found it in a legendary land + | all ${c}rocks and lavender and tufted grass, + | where it was settled on some sodden sand + | hard by the torrent of a mountain pass. + """.trimMargin(), + """ + | A Discovery + | + | I found it in a legendary land + | all rocks and lavender and tufted grass, + | where it was settled on some sodden sand + | ${c}hard by the torrent of a mountain pass. + """.trimMargin(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE + ) + } + + fun `test moves caret to same column with nostartofline`() { + OptionsManager.startofline.reset() + doTest( + "G", + """ + | A Discovery + | + | I found it in a legendary land + | all ${c}rocks and lavender and tufted grass, + | where it was settled on some sodden sand + | hard by the torrent of a mountain pass. + """.trimMargin(), + """ + | A Discovery + | + | I found it in a legendary land + | all rocks and lavender and tufted grass, + | where it was settled on some sodden sand + | hard${c} by the torrent of a mountain pass. + """.trimMargin(), + CommandState.Mode.COMMAND, CommandState.SubMode.NONE + ) + } + fun `test with last empty line`() { doTest( "G", From cdb89be0efa1b47d18590ac85b931fbbbb524063 Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Thu, 25 Feb 2021 01:14:09 +0000 Subject: [PATCH 5/8] Add 'startofline' support for scrolling actions --- .../scroll/MotionScrollHalfPageDownAction.kt | 2 +- .../scroll/MotionScrollHalfPageUpAction.kt | 2 +- .../scroll/MotionScrollPageDownAction.kt | 4 ++-- .../motion/scroll/MotionScrollPageUpAction.kt | 4 ++-- .../motion/updown/MotionShiftDownAction.kt | 2 +- .../motion/updown/MotionShiftUpAction.kt | 2 +- .../maddyhome/idea/vim/group/MotionGroup.java | 15 +++++++------- .../scroll/ScrollHalfPageDownActionTest.kt | 14 +++++++++---- .../scroll/ScrollHalfPageUpActionTest.kt | 14 +++++++++---- .../action/scroll/ScrollPageDownActionTest.kt | 17 ++++++++++++++++ .../action/scroll/ScrollPageUpActionTest.kt | 20 +++++++++++++++++-- 11 files changed, 71 insertions(+), 25 deletions(-) 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 12d3b539e3..9d43c3178b 100644 --- a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollHalfPageDownAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollHalfPageDownAction.kt @@ -32,6 +32,6 @@ class MotionScrollHalfPageDownAction : VimActionHandler.SingleExecution() { 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) + return VimPlugin.getMotion().scrollScreen(editor, editor.caretModel.primaryCaret, cmd.rawCount, true) } } diff --git a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollHalfPageUpAction.kt b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollHalfPageUpAction.kt index f7a7efe765..04abfdbcd4 100644 --- a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollHalfPageUpAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollHalfPageUpAction.kt @@ -32,6 +32,6 @@ class MotionScrollHalfPageUpAction : VimActionHandler.SingleExecution() { 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, false) + return VimPlugin.getMotion().scrollScreen(editor, editor.caretModel.primaryCaret, cmd.rawCount, false) } } 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 bf97d0c29d..628ff7a1f9 100644 --- a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollPageDownAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollPageDownAction.kt @@ -39,7 +39,7 @@ class MotionScrollPageDownAction : VimActionHandler.SingleExecution() { override val flags: EnumSet = enumSetOf(FLAG_IGNORE_SCROLL_JUMP) override fun execute(editor: Editor, context: DataContext, cmd: Command): Boolean { - return VimPlugin.getMotion().scrollFullPage(editor, cmd.count) + return VimPlugin.getMotion().scrollFullPage(editor, editor.caretModel.primaryCaret, cmd.count) } } @@ -54,6 +54,6 @@ class MotionScrollPageDownInsertModeAction : VimActionHandler.SingleExecution(), override val flags: EnumSet = enumSetOf(FLAG_IGNORE_SCROLL_JUMP, FLAG_CLEAR_STROKES) override fun execute(editor: Editor, context: DataContext, cmd: Command): Boolean { - return VimPlugin.getMotion().scrollFullPage(editor, cmd.count) + return VimPlugin.getMotion().scrollFullPage(editor, editor.caretModel.primaryCaret, 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 66f0b6cbe3..703ea5445f 100644 --- a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollPageUpAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollPageUpAction.kt @@ -39,7 +39,7 @@ class MotionScrollPageUpAction : VimActionHandler.SingleExecution() { override val flags: EnumSet = enumSetOf(FLAG_IGNORE_SCROLL_JUMP) override fun execute(editor: Editor, context: DataContext, cmd: Command): Boolean { - return VimPlugin.getMotion().scrollFullPage(editor, -cmd.count) + return VimPlugin.getMotion().scrollFullPage(editor, editor.caretModel.primaryCaret, -cmd.count) } } @@ -54,6 +54,6 @@ class MotionScrollPageUpInsertModeAction : VimActionHandler.SingleExecution(), C override val flags: EnumSet = enumSetOf(FLAG_IGNORE_SCROLL_JUMP, FLAG_CLEAR_STROKES) override fun execute(editor: Editor, context: DataContext, cmd: Command): Boolean { - return VimPlugin.getMotion().scrollFullPage(editor, -cmd.count) + return VimPlugin.getMotion().scrollFullPage(editor, editor.caretModel.primaryCaret, -cmd.count) } } diff --git a/src/com/maddyhome/idea/vim/action/motion/updown/MotionShiftDownAction.kt b/src/com/maddyhome/idea/vim/action/motion/updown/MotionShiftDownAction.kt index 2e9d1348b3..d945394153 100644 --- a/src/com/maddyhome/idea/vim/action/motion/updown/MotionShiftDownAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/updown/MotionShiftDownAction.kt @@ -46,6 +46,6 @@ class MotionShiftDownAction : ShiftedArrowKeyHandler() { } override fun motionWithoutKeyModel(editor: Editor, context: DataContext, cmd: Command) { - VimPlugin.getMotion().scrollFullPage(editor, cmd.count) + VimPlugin.getMotion().scrollFullPage(editor, editor.caretModel.primaryCaret, cmd.count) } } diff --git a/src/com/maddyhome/idea/vim/action/motion/updown/MotionShiftUpAction.kt b/src/com/maddyhome/idea/vim/action/motion/updown/MotionShiftUpAction.kt index 3ae8688831..1a61c38ce2 100644 --- a/src/com/maddyhome/idea/vim/action/motion/updown/MotionShiftUpAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/updown/MotionShiftUpAction.kt @@ -46,6 +46,6 @@ class MotionShiftUpAction : ShiftedArrowKeyHandler() { } override fun motionWithoutKeyModel(editor: Editor, context: DataContext, cmd: Command) { - VimPlugin.getMotion().scrollFullPage(editor, -cmd.count) + VimPlugin.getMotion().scrollFullPage(editor, editor.caretModel.primaryCaret, -cmd.count) } } diff --git a/src/com/maddyhome/idea/vim/group/MotionGroup.java b/src/com/maddyhome/idea/vim/group/MotionGroup.java index 6e70eeb978..42f16a8028 100755 --- a/src/com/maddyhome/idea/vim/group/MotionGroup.java +++ b/src/com/maddyhome/idea/vim/group/MotionGroup.java @@ -1075,7 +1075,7 @@ public int getOffsetOfHorizontalMotion(@NotNull Editor editor, @NotNull Caret ca } } - public boolean scrollFullPage(@NotNull Editor editor, int pages) { + public boolean scrollFullPage(@NotNull Editor editor, @NotNull Caret caret, int pages) { int caretVisualLine = EditorHelper.scrollFullPage(editor, pages); if (caretVisualLine != -1) { final int scrollOffset = getNormalizedScrollOffset(editor); @@ -1099,9 +1099,10 @@ else if (pages < 0) { } } - int offset = - moveCaretToLineStartSkipLeading(editor, visualLineToLogicalLine(editor, caretVisualLine)); - moveCaret(editor, editor.getCaretModel().getPrimaryCaret(), offset); + int offset = moveCaretToLineWithStartOfLineOption(editor, + visualLineToLogicalLine(editor, caretVisualLine), + caret); + moveCaret(editor, caret, offset); return success; } @@ -1134,7 +1135,7 @@ public int moveCaretToLineWithStartOfLineOption(@NotNull Editor editor, int logi } } - public boolean scrollScreen(final @NotNull Editor editor, int rawCount, boolean down) { + public boolean scrollScreen(final @NotNull Editor editor, final @NotNull Caret caret, int rawCount, boolean down) { final CaretModel caretModel = editor.getCaretModel(); final int currentLogicalLine = caretModel.getLogicalPosition().line; @@ -1178,8 +1179,8 @@ public boolean scrollScreen(final @NotNull Editor editor, int rawCount, boolean } int logicalLine = visualLineToLogicalLine(editor, targetCaretVisualLine); - int caretOffset = moveCaretToLineStartSkipLeading(editor, logicalLine); - moveCaret(editor, caretModel.getPrimaryCaret(), caretOffset); + int caretOffset = moveCaretToLineWithStartOfLineOption(editor, logicalLine, caret); + moveCaret(editor, caret, caretOffset); return true; } diff --git a/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollHalfPageDownActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollHalfPageDownActionTest.kt index c0036fb80b..141135f434 100644 --- a/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollHalfPageDownActionTest.kt +++ b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollHalfPageDownActionTest.kt @@ -20,9 +20,7 @@ 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 /* @@ -83,7 +81,7 @@ class ScrollHalfPageDownActionTest : VimTestCase() { configureByPages(5) setPositionAndScroll(100, 110) typeText(parseKeys("10")) - Assert.assertEquals(OptionsManager.scroll.value(), 10) + assertEquals(OptionsManager.scroll.value(), 10) } fun `test scroll downwards uses scroll option`() { @@ -103,7 +101,6 @@ class ScrollHalfPageDownActionTest : VimTestCase() { 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) @@ -111,4 +108,13 @@ class ScrollHalfPageDownActionTest : VimTestCase() { assertPosition(42, 4) assertVisibleArea(37, 71) } + + fun `test scroll downwards keeps same column with nostartofline`() { + OptionsManager.startofline.reset() + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(20, 25, 14) + typeText(parseKeys("")) + assertPosition(42, 14) + assertVisibleArea(37, 71) + } } diff --git a/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollHalfPageUpActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollHalfPageUpActionTest.kt index b6172a9f7f..e7259971a2 100644 --- a/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollHalfPageUpActionTest.kt +++ b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollHalfPageUpActionTest.kt @@ -20,9 +20,7 @@ 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 /* @@ -75,7 +73,7 @@ class ScrollHalfPageUpActionTest : VimTestCase() { configureByPages(5) setPositionAndScroll(50, 53) typeText(parseKeys("10")) - Assert.assertEquals(OptionsManager.scroll.value(), 10) + assertEquals(OptionsManager.scroll.value(), 10) } fun `test scroll upwards uses scroll option`() { @@ -95,7 +93,6 @@ class ScrollHalfPageUpActionTest : VimTestCase() { 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) @@ -103,4 +100,13 @@ class ScrollHalfPageUpActionTest : VimTestCase() { assertPosition(43, 4) assertVisibleArea(33, 67) } + + fun `test scroll upwards keeps same column with nostartofline`() { + OptionsManager.startofline.reset() + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(50, 60, 14) + typeText(parseKeys("")) + assertPosition(43, 14) + assertVisibleArea(33, 67) + } } diff --git a/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollPageDownActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollPageDownActionTest.kt index e016783ff0..7a58f4a992 100644 --- a/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollPageDownActionTest.kt +++ b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollPageDownActionTest.kt @@ -166,4 +166,21 @@ class ScrollPageDownActionTest : VimTestCase() { typeText(parseKeys("")) assertTrue(VimPlugin.isError()) } + + fun `test scroll page down puts cursor on first non-blank column`() { + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(20, 25, 14) + typeText(parseKeys("")) + assertPosition(53, 4) + assertVisibleArea(53, 87) + } + + fun `test scroll page down keeps same column with nostartofline`() { + OptionsManager.startofline.reset() + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(20, 25, 14) + typeText(parseKeys("")) + assertPosition(53, 14) + assertVisibleArea(53, 87) + } } diff --git a/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollPageUpActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollPageUpActionTest.kt index 715079a870..9586e9d8e3 100644 --- a/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollPageUpActionTest.kt +++ b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollPageUpActionTest.kt @@ -21,7 +21,6 @@ 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 /* @@ -155,7 +154,7 @@ class ScrollPageUpActionTest : VimTestCase() { configureByPages(5) setPositionAndScroll(0, 25) typeText(parseKeys("")) - Assert.assertTrue(VimPlugin.isError()) + assertTrue(VimPlugin.isError()) } fun `test scroll page up on second page moves cursor to previous top`() { @@ -165,4 +164,21 @@ class ScrollPageUpActionTest : VimTestCase() { assertPosition(11, 0) assertVisibleArea(0, 34) } + + fun `test scroll page up puts cursor on first non-blank column`() { + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(50, 60, 14) + typeText(parseKeys("")) + assertPosition(51, 4) + assertVisibleArea(17, 51) + } + + fun `test scroll page up keeps same column with nostartofline`() { + OptionsManager.startofline.reset() + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(50, 60, 14) + typeText(parseKeys("")) + assertPosition(51, 14) + assertVisibleArea(17, 51) + } } From fcf0cddbf41ac88dc977563814daffe971de329d Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Thu, 25 Feb 2021 10:44:59 +0000 Subject: [PATCH 6/8] Add 'startofline' support for shift operations --- .../maddyhome/idea/vim/group/ChangeGroup.java | 5 +- .../action/change/shift/ShiftLeftTest.kt | 65 ++++++++++++++++++- .../action/change/shift/ShiftRightTest.kt | 44 ++++++++++++- 3 files changed, 111 insertions(+), 3 deletions(-) diff --git a/src/com/maddyhome/idea/vim/group/ChangeGroup.java b/src/com/maddyhome/idea/vim/group/ChangeGroup.java index 11a4ef5846..69dbf2bab3 100644 --- a/src/com/maddyhome/idea/vim/group/ChangeGroup.java +++ b/src/com/maddyhome/idea/vim/group/ChangeGroup.java @@ -1652,6 +1652,9 @@ public void indentRange(@NotNull Editor editor, logger.debug("count=" + count); } + // Update the last column before we indent, or we might be retrieving the data for a line that no longer exists + UserDataManager.setVimLastColumn(caret, InlayHelperKt.getInlayAwareVisualColumn(caret)); + IndentConfig indentConfig = IndentConfig.create(editor, context); final int sline = editor.offsetToLogicalPosition(range.getStartOffset()).line; @@ -1713,7 +1716,7 @@ public void indentRange(@NotNull Editor editor, if (!CommandStateHelper.inInsertMode(editor)) { if (!range.isMultiple()) { - MotionGroup.moveCaret(editor, caret, VimPlugin.getMotion().moveCaretToLineStartSkipLeading(editor, sline)); + MotionGroup.moveCaret(editor, caret, VimPlugin.getMotion().moveCaretToLineWithStartOfLineOption(editor, sline, caret)); } else { MotionGroup.moveCaret(editor, caret, range.getStartOffset()); diff --git a/test/org/jetbrains/plugins/ideavim/action/change/shift/ShiftLeftTest.kt b/test/org/jetbrains/plugins/ideavim/action/change/shift/ShiftLeftTest.kt index 6b8eb578e0..f75b988e92 100644 --- a/test/org/jetbrains/plugins/ideavim/action/change/shift/ShiftLeftTest.kt +++ b/test/org/jetbrains/plugins/ideavim/action/change/shift/ShiftLeftTest.kt @@ -19,6 +19,7 @@ package org.jetbrains.plugins.ideavim.action.change.shift import com.maddyhome.idea.vim.helper.StringHelper +import com.maddyhome.idea.vim.option.OptionsManager import org.jetbrains.plugins.ideavim.VimTestCase class ShiftLeftTest : VimTestCase() { @@ -36,7 +37,7 @@ class ShiftLeftTest : VimTestCase() { """ A Discovery - I found it in a legendary land + ${c}I found 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. @@ -44,6 +45,68 @@ class ShiftLeftTest : VimTestCase() { ) } + fun `test shift left positions caret at first non-blank char`() { + val file = """ + |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. + """.trimMargin() + typeTextInFile(StringHelper.parseKeys("<<"), file) + myFixture.checkResult(""" + |A Discovery + + | ${c}I found 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. + """.trimMargin()) + } + + fun `test shift left does not move caret with nostartofline`() { + OptionsManager.startofline.reset() + val file = """ + |A Discovery + | + | I found it in a ${c}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. + """.trimMargin() + typeTextInFile(StringHelper.parseKeys("<<"), file) + myFixture.checkResult(""" + |A Discovery + + | I found it in a lege${c}ndary land + | all rocks and lavender and tufted grass, + | where it was settled on some sodden sand + | hard by the torrent of a mountain pass. + """.trimMargin()) + } + + fun `test shift left positions caret at end of line with nostartofline`() { + OptionsManager.startofline.reset() + val file = """ + |A Discovery + | + | I found it in a legendary la${c}nd + | all rocks and lavender and tufted grass, + | where it was settled on some sodden sand + | hard by the torrent of a mountain pass. + """.trimMargin() + typeTextInFile(StringHelper.parseKeys("<<"), file) + myFixture.checkResult(""" + |A Discovery + + | I found it in a legendary lan${c}d + | all rocks and lavender and tufted grass, + | where it was settled on some sodden sand + | hard by the torrent of a mountain pass. + """.trimMargin()) + } + fun `test shift ctrl-D`() { val file = """ A Discovery diff --git a/test/org/jetbrains/plugins/ideavim/action/change/shift/ShiftRightTest.kt b/test/org/jetbrains/plugins/ideavim/action/change/shift/ShiftRightTest.kt index 9514962135..63f44d37ac 100644 --- a/test/org/jetbrains/plugins/ideavim/action/change/shift/ShiftRightTest.kt +++ b/test/org/jetbrains/plugins/ideavim/action/change/shift/ShiftRightTest.kt @@ -19,6 +19,7 @@ package org.jetbrains.plugins.ideavim.action.change.shift import com.maddyhome.idea.vim.helper.StringHelper +import com.maddyhome.idea.vim.option.OptionsManager import org.jetbrains.plugins.ideavim.VimTestCase class ShiftRightTest : VimTestCase() { @@ -36,7 +37,7 @@ class ShiftRightTest : VimTestCase() { """ A Discovery - I found it in a legendary land + ${c}I found 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. @@ -118,6 +119,47 @@ class ShiftRightTest : VimTestCase() { myFixture.checkResult("foo foo\nfoo bar\nfoo baz\n") } + fun `test shift right positions caret at first non-blank char`() { + val file = """ + |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. + """.trimMargin() + typeTextInFile(StringHelper.parseKeys(">>"), file) + myFixture.checkResult(""" + |A Discovery + + | ${c}I found 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. + """.trimMargin()) + } + + fun `test shift right does not move caret with nostartofline`() { + OptionsManager.startofline.reset() + val file = """ + |A Discovery + | + | I found it in a ${c}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. + """.trimMargin() + typeTextInFile(StringHelper.parseKeys(">>"), file) + myFixture.checkResult(""" + |A Discovery + + | I found it i${c}n 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. + """.trimMargin()) + } + fun `test shift ctrl-t`() { val file = """ A Discovery From e5bfa3795afe705237d3937c94ca0f2010abedfd Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Thu, 25 Feb 2021 13:28:35 +0000 Subject: [PATCH 7/8] Add 'startofline' to count percent motion --- .../updown/MotionPercentOrMatchAction.kt | 2 +- .../maddyhome/idea/vim/group/MotionGroup.java | 9 ++--- .../updown/MotionPercentOrMatchActionTest.kt | 35 +++++++++++++++++++ 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/src/com/maddyhome/idea/vim/action/motion/updown/MotionPercentOrMatchAction.kt b/src/com/maddyhome/idea/vim/action/motion/updown/MotionPercentOrMatchAction.kt index c5443ed7d1..84802de271 100644 --- a/src/com/maddyhome/idea/vim/action/motion/updown/MotionPercentOrMatchAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/updown/MotionPercentOrMatchAction.kt @@ -43,7 +43,7 @@ class MotionPercentOrMatchAction : MotionActionHandler.ForEachCaret() { return if (rawCount == 0) { VimPlugin.getMotion().moveCaretToMatchingPair(editor, caret) } else { - VimPlugin.getMotion().moveCaretToLinePercent(editor, count) + VimPlugin.getMotion().moveCaretToLinePercent(editor, caret, count) } } diff --git a/src/com/maddyhome/idea/vim/group/MotionGroup.java b/src/com/maddyhome/idea/vim/group/MotionGroup.java index 42f16a8028..a8552b0fe1 100755 --- a/src/com/maddyhome/idea/vim/group/MotionGroup.java +++ b/src/com/maddyhome/idea/vim/group/MotionGroup.java @@ -29,6 +29,7 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileManager; import com.intellij.openapi.vfs.VirtualFileSystem; +import com.intellij.util.MathUtil; import com.maddyhome.idea.vim.KeyHandler; import com.maddyhome.idea.vim.VimPlugin; import com.maddyhome.idea.vim.command.*; @@ -1301,10 +1302,10 @@ public int moveCaretVertical(@NotNull Editor editor, @NotNull Caret caret, int c } } - public int moveCaretToLinePercent(@NotNull Editor editor, int count) { - if (count > 100) count = 100; - - return moveCaretToLineStartSkipLeading(editor, normalizeLine(editor, (getLineCount(editor) * count + 99) / 100 - 1)); + public int moveCaretToLinePercent(@NotNull Editor editor, @NotNull Caret caret, int count) { + return moveCaretToLineWithStartOfLineOption(editor, + normalizeLine(editor, (getLineCount(editor) * MathUtil.clamp(count, 0, 100) + 99) /100 - 1), + caret); } public int moveCaretGotoLineLastEnd(@NotNull Editor editor, int rawCount, int line, boolean pastEnd) { diff --git a/test/org/jetbrains/plugins/ideavim/action/motion/updown/MotionPercentOrMatchActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/motion/updown/MotionPercentOrMatchActionTest.kt index 1628e724c6..4265119b49 100644 --- a/test/org/jetbrains/plugins/ideavim/action/motion/updown/MotionPercentOrMatchActionTest.kt +++ b/test/org/jetbrains/plugins/ideavim/action/motion/updown/MotionPercentOrMatchActionTest.kt @@ -20,6 +20,7 @@ package org.jetbrains.plugins.ideavim.action.motion.updown import com.maddyhome.idea.vim.command.CommandState import com.maddyhome.idea.vim.helper.StringHelper.parseKeys +import com.maddyhome.idea.vim.option.OptionsManager import org.jetbrains.plugins.ideavim.VimTestCase /** @@ -231,4 +232,38 @@ class MotionPercentOrMatchActionTest : VimTestCase() { fun `test deleting with percent motion`() { doTest("d%", "$c(foo bar)", c, CommandState.Mode.COMMAND, CommandState.SubMode.NONE) } + + fun `test count percent moves to line as percentage of file height`() { + configureByLines(100, " I found it in a legendary land") + typeText(parseKeys("25%")) + assertPosition(24, 4) + } + + fun `test count percent moves to line as percentage of file height 2`() { + configureByLines(50, " I found it in a legendary land") + typeText(parseKeys("25%")) + assertPosition(12, 4) + } + + fun `test count percent moves to line as percentage of file height 3`() { + configureByLines(17, " I found it in a legendary land") + typeText(parseKeys("25%")) + assertPosition(4, 4) + } + + fun `test count percent keeps same column with nostartline`() { + OptionsManager.startofline.reset() + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(0, 0, 14) + typeText(parseKeys("25%")) + assertPosition(24, 14) + } + + fun `test count percent handles shorter line with nostartline`() { + OptionsManager.startofline.reset() + configureByLines(100, " I found it in a legendary land") + typeText(parseKeys("A", " extra text", "")) + typeText(parseKeys("25%")) + assertPosition(24, 33) + } } From 84bae341a052c566d9f673b931e444dedcc7332d Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Thu, 25 Feb 2021 14:12:02 +0000 Subject: [PATCH 8/8] Update after running ktlintFormatter --- .../motion/updown/MotionGotoLineLastAction.kt | 13 ++++++++----- .../jetbrains/plugins/ideavim/VimTestCase.kt | 6 ++++-- .../action/change/shift/ShiftLeftTest.kt | 18 ++++++++++++------ .../action/change/shift/ShiftRightTest.kt | 12 ++++++++---- .../screen/MotionFirstScreenLineActionTest.kt | 5 ++--- .../screen/MotionLastScreenLineActionTest.kt | 2 +- .../screen/MotionMiddleScreenLineActionTest.kt | 2 +- .../updown/MotionGotoLineFirstActionTest.kt | 2 +- .../MotionGotoLineFirstInsertActionTest.kt | 6 +++--- .../updown/MotionGotoLineLastActionTest.kt | 2 +- .../ideavim/ex/handler/GotoLineHandlerTest.kt | 2 +- test/ui/UiTests.kt | 2 +- 12 files changed, 43 insertions(+), 29 deletions(-) diff --git a/src/com/maddyhome/idea/vim/action/motion/updown/MotionGotoLineLastAction.kt b/src/com/maddyhome/idea/vim/action/motion/updown/MotionGotoLineLastAction.kt index d8ef1a7668..2dbcd6dc76 100644 --- a/src/com/maddyhome/idea/vim/action/motion/updown/MotionGotoLineLastAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/updown/MotionGotoLineLastAction.kt @@ -42,11 +42,14 @@ class MotionGotoLineLastAction : MotionActionHandler.ForEachCaret() { rawCount: Int, argument: Argument? ): Int { - val line = EditorHelper.normalizeLine(editor, if (rawCount == 0) { - EditorHelper.getLineCount(editor) - 1 - } else { - rawCount - 1 - }) + val line = EditorHelper.normalizeLine( + editor, + if (rawCount == 0) { + EditorHelper.getLineCount(editor) - 1 + } else { + rawCount - 1 + } + ) return VimPlugin.getMotion().moveCaretToLineWithStartOfLineOption(editor, line, caret) } } diff --git a/test/org/jetbrains/plugins/ideavim/VimTestCase.kt b/test/org/jetbrains/plugins/ideavim/VimTestCase.kt index 8711b0d5b0..1a880e4a8b 100644 --- a/test/org/jetbrains/plugins/ideavim/VimTestCase.kt +++ b/test/org/jetbrains/plugins/ideavim/VimTestCase.kt @@ -223,8 +223,10 @@ abstract class VimTestCase : UsefulTestCase() { assertPosition(caretLogicalLine, caretLogicalColumn) // Belt and braces. Let's make sure that the caret is fully onscreen - val bottomLogicalLine = EditorHelper.visualLineToLogicalLine(myFixture.editor, - EditorHelper.getVisualLineAtBottomOfScreen(myFixture.editor)) + val bottomLogicalLine = EditorHelper.visualLineToLogicalLine( + myFixture.editor, + EditorHelper.getVisualLineAtBottomOfScreen(myFixture.editor) + ) assertTrue(bottomLogicalLine >= caretLogicalLine) assertTrue(caretLogicalLine >= scrollToLogicalLine) } diff --git a/test/org/jetbrains/plugins/ideavim/action/change/shift/ShiftLeftTest.kt b/test/org/jetbrains/plugins/ideavim/action/change/shift/ShiftLeftTest.kt index f75b988e92..faa05959da 100644 --- a/test/org/jetbrains/plugins/ideavim/action/change/shift/ShiftLeftTest.kt +++ b/test/org/jetbrains/plugins/ideavim/action/change/shift/ShiftLeftTest.kt @@ -55,14 +55,16 @@ class ShiftLeftTest : VimTestCase() { | hard by the torrent of a mountain pass. """.trimMargin() typeTextInFile(StringHelper.parseKeys("<<"), file) - myFixture.checkResult(""" + myFixture.checkResult( + """ |A Discovery | ${c}I found 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. - """.trimMargin()) + """.trimMargin() + ) } fun `test shift left does not move caret with nostartofline`() { @@ -76,14 +78,16 @@ class ShiftLeftTest : VimTestCase() { | hard by the torrent of a mountain pass. """.trimMargin() typeTextInFile(StringHelper.parseKeys("<<"), file) - myFixture.checkResult(""" + myFixture.checkResult( + """ |A Discovery | I found it in a lege${c}ndary land | all rocks and lavender and tufted grass, | where it was settled on some sodden sand | hard by the torrent of a mountain pass. - """.trimMargin()) + """.trimMargin() + ) } fun `test shift left positions caret at end of line with nostartofline`() { @@ -97,14 +101,16 @@ class ShiftLeftTest : VimTestCase() { | hard by the torrent of a mountain pass. """.trimMargin() typeTextInFile(StringHelper.parseKeys("<<"), file) - myFixture.checkResult(""" + myFixture.checkResult( + """ |A Discovery | I found it in a legendary lan${c}d | all rocks and lavender and tufted grass, | where it was settled on some sodden sand | hard by the torrent of a mountain pass. - """.trimMargin()) + """.trimMargin() + ) } fun `test shift ctrl-D`() { diff --git a/test/org/jetbrains/plugins/ideavim/action/change/shift/ShiftRightTest.kt b/test/org/jetbrains/plugins/ideavim/action/change/shift/ShiftRightTest.kt index 63f44d37ac..6f7d333ebd 100644 --- a/test/org/jetbrains/plugins/ideavim/action/change/shift/ShiftRightTest.kt +++ b/test/org/jetbrains/plugins/ideavim/action/change/shift/ShiftRightTest.kt @@ -129,14 +129,16 @@ class ShiftRightTest : VimTestCase() { | hard by the torrent of a mountain pass. """.trimMargin() typeTextInFile(StringHelper.parseKeys(">>"), file) - myFixture.checkResult(""" + myFixture.checkResult( + """ |A Discovery | ${c}I found 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. - """.trimMargin()) + """.trimMargin() + ) } fun `test shift right does not move caret with nostartofline`() { @@ -150,14 +152,16 @@ class ShiftRightTest : VimTestCase() { | hard by the torrent of a mountain pass. """.trimMargin() typeTextInFile(StringHelper.parseKeys(">>"), file) - myFixture.checkResult(""" + myFixture.checkResult( + """ |A Discovery | I found it i${c}n 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. - """.trimMargin()) + """.trimMargin() + ) } fun `test shift ctrl-t`() { diff --git a/test/org/jetbrains/plugins/ideavim/action/motion/screen/MotionFirstScreenLineActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/motion/screen/MotionFirstScreenLineActionTest.kt index 4b9b7f798b..dabf54e3eb 100644 --- a/test/org/jetbrains/plugins/ideavim/action/motion/screen/MotionFirstScreenLineActionTest.kt +++ b/test/org/jetbrains/plugins/ideavim/action/motion/screen/MotionFirstScreenLineActionTest.kt @@ -100,7 +100,7 @@ class MotionFirstScreenLineActionTest : VimTestCase() { assertPosition(30, 4) } - fun `test operator pending acts to first screen line` () { + fun `test operator pending acts to first screen line`() { configureByLines(100, " I found it in a legendary land") setPositionAndScroll(20, 40, 10) typeText(parseKeys("dH")) @@ -115,7 +115,7 @@ class MotionFirstScreenLineActionTest : VimTestCase() { assertPosition(24, 4) } - fun `test operator pending acts to first screen line with nostartofline` () { + fun `test operator pending acts to first screen line with nostartofline`() { OptionsManager.startofline.reset() configureByLines(100, " I found it in a legendary land") setPositionAndScroll(20, 40, 10) @@ -176,4 +176,3 @@ class MotionFirstScreenLineActionTest : VimTestCase() { assertPosition(24, 4) } } - diff --git a/test/org/jetbrains/plugins/ideavim/action/motion/screen/MotionLastScreenLineActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/motion/screen/MotionLastScreenLineActionTest.kt index e54865ea1f..e38316859c 100644 --- a/test/org/jetbrains/plugins/ideavim/action/motion/screen/MotionLastScreenLineActionTest.kt +++ b/test/org/jetbrains/plugins/ideavim/action/motion/screen/MotionLastScreenLineActionTest.kt @@ -290,4 +290,4 @@ class MotionLastScreenLineActionTest : VimTestCase() { assertPosition(20, 4) assertBottomLogicalLine(44) } -} \ No newline at end of file +} diff --git a/test/org/jetbrains/plugins/ideavim/action/motion/screen/MotionMiddleScreenLineActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/motion/screen/MotionMiddleScreenLineActionTest.kt index c3961f31ca..45c28924d0 100644 --- a/test/org/jetbrains/plugins/ideavim/action/motion/screen/MotionMiddleScreenLineActionTest.kt +++ b/test/org/jetbrains/plugins/ideavim/action/motion/screen/MotionMiddleScreenLineActionTest.kt @@ -155,4 +155,4 @@ class MotionMiddleScreenLineActionTest : VimTestCase() { typeText(parseKeys("M")) assertPosition(8, 4) } -} \ No newline at end of file +} diff --git a/test/org/jetbrains/plugins/ideavim/action/motion/updown/MotionGotoLineFirstActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/motion/updown/MotionGotoLineFirstActionTest.kt index eae729bf1b..d451be0296 100644 --- a/test/org/jetbrains/plugins/ideavim/action/motion/updown/MotionGotoLineFirstActionTest.kt +++ b/test/org/jetbrains/plugins/ideavim/action/motion/updown/MotionGotoLineFirstActionTest.kt @@ -161,4 +161,4 @@ class MotionGotoLineFirstActionTest : VimTestCase() { CommandState.Mode.COMMAND, CommandState.SubMode.NONE ) } -} \ No newline at end of file +} diff --git a/test/org/jetbrains/plugins/ideavim/action/motion/updown/MotionGotoLineFirstInsertActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/motion/updown/MotionGotoLineFirstInsertActionTest.kt index 3a2f5a8885..a78496472a 100644 --- a/test/org/jetbrains/plugins/ideavim/action/motion/updown/MotionGotoLineFirstInsertActionTest.kt +++ b/test/org/jetbrains/plugins/ideavim/action/motion/updown/MotionGotoLineFirstInsertActionTest.kt @@ -35,7 +35,7 @@ class MotionGotoLineFirstInsertActionTest : VimTestCase() { | hard by the torrent of a mountain pass. """.trimMargin(), """ - |${c} A Discovery + |$c A Discovery | | I found it in a legendary land | all rocks and lavender and tufted grass, @@ -59,7 +59,7 @@ class MotionGotoLineFirstInsertActionTest : VimTestCase() { | hard by the torrent of a mountain pass. """.trimMargin(), """ - |${c} A Discovery + |$c A Discovery | | I found it in a legendary land | all rocks and lavender and tufted grass, @@ -69,4 +69,4 @@ class MotionGotoLineFirstInsertActionTest : VimTestCase() { CommandState.Mode.COMMAND, CommandState.SubMode.NONE ) } -} \ No newline at end of file +} diff --git a/test/org/jetbrains/plugins/ideavim/action/motion/updown/MotionGotoLineLastActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/motion/updown/MotionGotoLineLastActionTest.kt index 11acb19bac..fad814875d 100644 --- a/test/org/jetbrains/plugins/ideavim/action/motion/updown/MotionGotoLineLastActionTest.kt +++ b/test/org/jetbrains/plugins/ideavim/action/motion/updown/MotionGotoLineLastActionTest.kt @@ -156,7 +156,7 @@ class MotionGotoLineLastActionTest : VimTestCase() { | I found it in a legendary land | all rocks and lavender and tufted grass, | where it was settled on some sodden sand - | hard${c} by the torrent of a mountain pass. + | hard$c by the torrent of a mountain pass. """.trimMargin(), CommandState.Mode.COMMAND, CommandState.SubMode.NONE ) diff --git a/test/org/jetbrains/plugins/ideavim/ex/handler/GotoLineHandlerTest.kt b/test/org/jetbrains/plugins/ideavim/ex/handler/GotoLineHandlerTest.kt index 2f18201c1b..2e9a48e28f 100644 --- a/test/org/jetbrains/plugins/ideavim/ex/handler/GotoLineHandlerTest.kt +++ b/test/org/jetbrains/plugins/ideavim/ex/handler/GotoLineHandlerTest.kt @@ -193,4 +193,4 @@ class GotoLineHandlerTest : VimTestCase() { assertPosition(30, 4) assertTopLogicalLine(6) } -} \ No newline at end of file +} diff --git a/test/ui/UiTests.kt b/test/ui/UiTests.kt index a1296760d6..db3a179fd6 100644 --- a/test/ui/UiTests.kt +++ b/test/ui/UiTests.kt @@ -25,7 +25,6 @@ import com.intellij.remoterobot.stepsProcessing.step import com.intellij.remoterobot.utils.keyboard import com.intellij.remoterobot.utils.waitFor import org.assertj.swing.core.MouseButton -import ui.utils.JavaExampleSteps import org.junit.Test import ui.pages.Editor import ui.pages.actionMenu @@ -35,6 +34,7 @@ import ui.pages.editor import ui.pages.gutter import ui.pages.idea import ui.pages.welcomeFrame +import ui.utils.JavaExampleSteps import ui.utils.StepsLogger import ui.utils.doubleClickOnRight import ui.utils.moveMouseForthAndBack