diff --git a/src/main/java/com/maddyhome/idea/vim/ui/ex/ExEntryPanel.java b/src/main/java/com/maddyhome/idea/vim/ui/ex/ExEntryPanel.java index 3a24c0fce..45c4ad08b 100644 --- a/src/main/java/com/maddyhome/idea/vim/ui/ex/ExEntryPanel.java +++ b/src/main/java/com/maddyhome/idea/vim/ui/ex/ExEntryPanel.java @@ -308,6 +308,9 @@ public class ExEntryPanel extends JPanel implements VimCommandLine { protected void textChanged(@NotNull DocumentEvent e) { try { final Editor editor = entry.getEditor(); + if (editor == null) { + return; + } final String labelText = label.getText(); // Either '/', '?' or ':'boolean searchCommand = false; @@ -351,8 +354,8 @@ public class ExEntryPanel extends JPanel implements VimCommandLine { if (labelText.equals("/") || labelText.equals("?") || searchCommand) { final boolean forwards = !labelText.equals("?"); // :s, :g, :v are treated as forwards - int pattenEnd = injector.getSearchGroup().findEndOfPattern(searchText, separator, 0); - final String pattern = searchText.substring(0, pattenEnd); + int patternEnd = injector.getSearchGroup().findEndOfPattern(searchText, separator, 0); + final String pattern = searchText.substring(0, patternEnd); VimPlugin.getEditor().closeEditorSearchSession(editor); final int matchOffset = diff --git a/src/test/java/org/jetbrains/plugins/ideavim/group/search/IncsearchTests.kt b/src/test/java/org/jetbrains/plugins/ideavim/group/search/IncsearchTests.kt index a628154b6..e45ceb62a 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/group/search/IncsearchTests.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/group/search/IncsearchTests.kt @@ -563,7 +563,67 @@ class IncsearchTests : VimTestCase() { } @Test - fun `test incsearch updates selection when started in Visual mode`() { + fun `test incsearch updates Visual selection`() { + doTest( + listOf("ve", "/dolor"), + """ + |Lorem ipsum dolor sit amet, + |consectetur adipiscing elit + |Sed in orci mauris. + |Cras id tellus in ex imperdiet egestas. + """.trimMargin(), + """ + |${s}Lorem ipsum ${c}d${se}olor sit amet, + |consectetur adipiscing elit + |Sed in orci mauris. + |Cras id tellus in ex imperdiet egestas. + """.trimMargin(), + Mode.CMD_LINE(Mode.VISUAL(SelectionType.CHARACTER_WISE)) + ) { + enterCommand("set hlsearch incsearch") + } + assertSearchHighlights("dolor", + """ + |Lorem ipsum ‷dolor‴ sit amet, + |consectetur adipiscing elit + |Sed in orci mauris. + |Cras id tellus in ex imperdiet egestas. + """.trimMargin() + ) + } + + @Test + fun `test incsearch updates empty Visual selection`() { + doTest( + "v/ipsum", + """ + |Lorem ipsum dolor sit amet, + |consectetur adipiscing elit + |Sed in orci mauris. + |Cras id tellus in ex imperdiet egestas. + """.trimMargin(), + """ + |${s}Lorem ${c}i${se}psum dolor sit amet, + |consectetur adipiscing elit + |Sed in orci mauris. + |Cras id tellus in ex imperdiet egestas. + """.trimMargin(), + Mode.CMD_LINE(Mode.VISUAL(SelectionType.CHARACTER_WISE)) + ) { + enterCommand("set hlsearch incsearch") + } + assertSearchHighlights("ipsum", + """ + |Lorem ‷ipsum‴ dolor sit amet, + |consectetur adipiscing elit + |Sed in orci mauris. + |Cras id tellus in ex imperdiet egestas. + """.trimMargin() + ) + } + + @Test + fun `test incsearch updates exclusive Visual selection`() { doTest( listOf("ve", "/dolor"), """ @@ -580,8 +640,48 @@ class IncsearchTests : VimTestCase() { """.trimMargin(), Mode.CMD_LINE(Mode.VISUAL(SelectionType.CHARACTER_WISE)) ) { + enterCommand("set selection=exclusive") enterCommand("set hlsearch incsearch") } + assertSearchHighlights("dolor", + """ + |Lorem ipsum ‷dolor‴ sit amet, + |consectetur adipiscing elit + |Sed in orci mauris. + |Cras id tellus in ex imperdiet egestas. + """.trimMargin() + ) + } + + @Test + fun `test incsearch updates empty exclusive Visual selection`() { + doTest( + "v/ipsum", + """ + |Lorem ipsum dolor sit amet, + |consectetur adipiscing elit + |Sed in orci mauris. + |Cras id tellus in ex imperdiet egestas. + """.trimMargin(), + """ + |${s}Lorem ${c}${se}ipsum dolor sit amet, + |consectetur adipiscing elit + |Sed in orci mauris. + |Cras id tellus in ex imperdiet egestas. + """.trimMargin(), + Mode.CMD_LINE(Mode.VISUAL(SelectionType.CHARACTER_WISE)) + ) { + enterCommand("set selection=exclusive") + enterCommand("set hlsearch incsearch") + } + assertSearchHighlights("ipsum", + """ + |Lorem ‷ipsum‴ dolor sit amet, + |consectetur adipiscing elit + |Sed in orci mauris. + |Cras id tellus in ex imperdiet egestas. + """.trimMargin() + ) } @Test diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimCaret.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimCaret.kt index 2874a642a..999577a74 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimCaret.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimCaret.kt @@ -20,7 +20,7 @@ import com.maddyhome.idea.vim.helper.exitVisualMode import com.maddyhome.idea.vim.register.Register import com.maddyhome.idea.vim.state.mode.SelectionType import com.maddyhome.idea.vim.state.mode.inBlockSelection -import com.maddyhome.idea.vim.state.mode.inCommandLineMode +import com.maddyhome.idea.vim.state.mode.inCommandLineModeWithVisual import com.maddyhome.idea.vim.state.mode.inSelectMode import com.maddyhome.idea.vim.state.mode.inVisualMode import javax.swing.KeyStroke @@ -98,27 +98,26 @@ per-caret marks. // Make sure to always reposition the caret, even if the offset hasn't changed. We might need to reposition due to // changes in surrounding text, especially with inline inlays. val oldOffset = this.offset - var caretAfterMove = moveToInlayAwareOffset(offset) + var updatedCaret = moveToInlayAwareOffset(offset) // Similarly, always make sure the caret is positioned within the view. Adding or removing text could move the caret // position relative to the view, without changing offset. if (this == editor.primaryCaret()) { injector.scroll.scrollCaretIntoView(editor) } - caretAfterMove = if (editor.inVisualMode || editor.inSelectMode) { + + // If we're in Visual or Select mode, update the selection. We also need to handle Command-line mode, with Visual + // pending, e.g. `v/foo` or `v:<C-U>normal 3j` + updatedCaret = if (editor.inVisualMode || editor.inSelectMode || editor.inCommandLineModeWithVisual) { // Another inconsistency with immutable caret. This method should be called on the new caret instance. - caretAfterMove.vimMoveSelectionToCaret(this.vimSelectionStart) - editor.findLastVersionOfCaret(caretAfterMove) ?: caretAfterMove - } else if (editor.inCommandLineMode && caretAfterMove.hasSelection()) { - // If we're updating the caret in Command-line mode, it's most likely due to incsearch - caretAfterMove.setSelection(caretAfterMove.selectionStart, offset) - editor.findLastVersionOfCaret(caretAfterMove) ?: caretAfterMove + updatedCaret.vimMoveSelectionToCaret(this.vimSelectionStart) + editor.findLastVersionOfCaret(updatedCaret) ?: updatedCaret } else { editor.exitVisualMode() - caretAfterMove + updatedCaret } injector.motion.onAppCodeMovement(editor, this, offset, oldOffset) - return caretAfterMove + return updatedCaret } fun moveToOffsetNative(offset: Int) diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/group/visual/EngineVisualGroup.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/group/visual/EngineVisualGroup.kt index a7dccc907..fb5c738a4 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/group/visual/EngineVisualGroup.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/group/visual/EngineVisualGroup.kt @@ -20,6 +20,7 @@ import com.maddyhome.idea.vim.helper.RWLockLabel import com.maddyhome.idea.vim.state.mode.Mode import com.maddyhome.idea.vim.state.mode.SelectionType import com.maddyhome.idea.vim.state.mode.inBlockSelection +import com.maddyhome.idea.vim.state.mode.inCommandLineModeWithVisual import com.maddyhome.idea.vim.state.mode.inSelectMode import com.maddyhome.idea.vim.state.mode.inVisualMode import com.maddyhome.idea.vim.state.mode.selectionType @@ -122,7 +123,7 @@ fun vimMoveBlockSelectionToOffset(editor: VimEditor, offset: Int) { * @see vimMoveBlockSelectionToOffset for blockwise selection */ fun VimCaret.vimMoveSelectionToCaret(vimSelectionStart: Int = this.vimSelectionStart) { - if (!editor.inVisualMode && !editor.inSelectMode) error("Attempt to extent selection in non-visual mode") + if (!editor.inVisualMode && !editor.inSelectMode && !editor.inCommandLineModeWithVisual) error("Attempt to extent selection in non-visual mode") if (editor.inBlockSelection) error("Move caret with [vimMoveBlockSelectionToOffset]") val startOffsetMark = vimSelectionStart diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/mode/Mode.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/mode/Mode.kt index 444fc1051..f647abf34 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/mode/Mode.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/mode/Mode.kt @@ -156,6 +156,13 @@ sealed interface Mode { "CMD_LINE mode can be active only in NORMAL, OP_PENDING, VISUAL or INSERT modes, not ${returnTo.javaClass.simpleName}" } } + + /** + * Returns true if Visual mode is pending, by starting Command-line with a Visual selection + * + * For example, `v/foo` or `v:<C-U>normal 3j` + */ + val isVisualPending = returnTo is VISUAL } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/mode/editorExtensions.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/mode/editorExtensions.kt index 8389b4f33..538e7704f 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/mode/editorExtensions.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/mode/editorExtensions.kt @@ -26,6 +26,9 @@ val VimEditor.inSelectMode: Boolean val VimEditor.inCommandLineMode: Boolean get() = this.mode is Mode.CMD_LINE +val VimEditor.inCommandLineModeWithVisual: Boolean + get() = (this.mode as? Mode.CMD_LINE)?.isVisualPending == true + val VimEditor.singleModeActive: Boolean get() = this.mode.isSingleModeActive