From f64c99c4063d711bb931e74d2c85475eb8f63721 Mon Sep 17 00:00:00 2001 From: Matt Ellis <m.t.ellis@gmail.com> Date: Thu, 18 Apr 2024 08:46:09 +0100 Subject: [PATCH] Support incsearch highlighting for global command Fixes VIM-2891 --- .../idea/vim/ui/ex/ExEntryPanel.java | 22 +- .../vimscript/model/commands/GlobalCommand.kt | 14 +- .../plugins/ideavim/group/SearchGroupTest.kt | 456 +++++++++++++++++- .../jetbrains/plugins/ideavim/VimTestCase.kt | 6 +- .../vim/vimscript/model/commands/Command.kt | 55 ++- .../vimscript/model/commands/FileCommand.kt | 2 +- 6 files changed, 523 insertions(+), 32 deletions(-) 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 5f9f9083b..94e8512fe 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 @@ -34,6 +34,7 @@ import com.maddyhome.idea.vim.regexp.CharPointer; import com.maddyhome.idea.vim.regexp.RegExp; import com.maddyhome.idea.vim.ui.ExPanelBorder; import com.maddyhome.idea.vim.vimscript.model.commands.Command; +import com.maddyhome.idea.vim.vimscript.model.commands.GlobalCommand; import com.maddyhome.idea.vim.vimscript.model.commands.SubstituteCommand; import com.maddyhome.idea.vim.vimscript.parser.VimscriptParser; import org.jetbrains.annotations.Contract; @@ -270,11 +271,13 @@ public class ExEntryPanel extends JPanel implements VimCommandLine { try { final Editor editor = entry.getEditor(); + final String labelText = label.getText(); // Either '/', '?' or ':'boolean searchCommand = false; + boolean searchCommand = false; LineRange searchRange = null; - char separator = label.getText().charAt(0); + char separator = labelText.charAt(0); String searchText = entry.getActualText(); - if (label.getText().equals(":")) { + if (labelText.equals(":")) { if (searchText.isEmpty()) return; final Command command = getIncsearchCommand(searchText); if (command == null) { @@ -287,14 +290,18 @@ public class ExEntryPanel extends JPanel implements VimCommandLine { separator = argument.charAt(0); searchText = argument.substring(1); } - if (searchText.length() == 0) { - // Reset back to the original search highlights after deleting a search from a substitution command. + if (!searchText.isEmpty()) { + searchRange = command.getLineRangeSafe(new IjVimEditor(editor)); + } + if (searchText.isEmpty() || searchRange == null) { + // Reset back to the original search highlights after deleting a search from a substitution command.Or if + // there is no search range (because the user entered an invalid range, e.g. mark not set). // E.g. Highlight `whatever`, type `:%s/foo` + highlight `foo`, delete back to `:%s/` and reset highlights // back to `whatever` VimPlugin.getSearch().resetIncsearchHighlights(); + resetCaretOffset(editor); return; } - searchRange = command.getLineRange(new IjVimEditor(editor)); } // If we're showing highlights for the search command `/`, then the command builder will have a count already @@ -302,7 +309,6 @@ public class ExEntryPanel extends JPanel implements VimCommandLine { // obviously won't be a count. int count1 = Math.max(1, KeyHandler.getInstance().getKeyHandlerState().getEditorCommandBuilder().getCount()); - final String labelText = label.getText(); if (labelText.equals("/") || labelText.equals("?") || searchCommand) { final boolean forwards = !labelText.equals("?"); // :s, :g, :v are treated as forwards final String pattern; @@ -334,8 +340,8 @@ public class ExEntryPanel extends JPanel implements VimCommandLine { if (commandText == null) return null; try { final Command exCommand = VimscriptParser.INSTANCE.parseCommand(commandText); - // TODO: Add global, vglobal, smagic and snomagic here when the commands are supported - if (exCommand instanceof SubstituteCommand) { + // TODO: Add smagic and snomagic here if/when the commands are supported + if (exCommand instanceof SubstituteCommand || exCommand instanceof GlobalCommand) { return exCommand; } } diff --git a/src/main/java/com/maddyhome/idea/vim/vimscript/model/commands/GlobalCommand.kt b/src/main/java/com/maddyhome/idea/vim/vimscript/model/commands/GlobalCommand.kt index 42bf81f54..e52768e7d 100644 --- a/src/main/java/com/maddyhome/idea/vim/vimscript/model/commands/GlobalCommand.kt +++ b/src/main/java/com/maddyhome/idea/vim/vimscript/model/commands/GlobalCommand.kt @@ -39,19 +39,19 @@ import com.maddyhome.idea.vim.vimscript.model.ExecutionResult */ @ExCommand(command = "g[lobal],v[global]") internal data class GlobalCommand(val range: Range, val argument: String, val invert: Boolean) : Command.SingleExecution(range, argument) { + + init { + // Most commands have a default range of the current line ("."). Global has a default range of the whole file + defaultRange = "%" + } + override val argFlags = flags(RangeFlag.RANGE_OPTIONAL, ArgumentFlag.ARGUMENT_OPTIONAL, Access.SELF_SYNCHRONIZED) override fun processCommand(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments): ExecutionResult { var result: ExecutionResult = ExecutionResult.Success editor.removeSecondaryCarets() val caret = editor.currentCaret() - - // For :g command the default range is % - val lineRange: LineRange = if (range.size() == 0) { - LineRange(0, editor.lineCount() - 1) - } else { - getLineRange(editor, caret) - } + val lineRange = getLineRange(editor, caret) if (!processGlobalCommand(editor, context, lineRange)) { result = ExecutionResult.Error } diff --git a/src/test/java/org/jetbrains/plugins/ideavim/group/SearchGroupTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/group/SearchGroupTest.kt index c8143fada..df48680a3 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/group/SearchGroupTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/group/SearchGroupTest.kt @@ -1657,7 +1657,7 @@ class SearchGroupTest : VimTestCase() { ) enterCommand("set hlsearch incsearch") - enterSearch("and") + enterSearch("and") // Moves the caret to "and" on the second line: (1, 10) typeText(":", "%s/roc", "<BS><BS><BS>") assertSearchHighlights( @@ -1669,7 +1669,8 @@ class SearchGroupTest : VimTestCase() { """.trimMargin(), ) - // TODO: Check caret position + // Make sure the caret is reset too + assertPosition(1, 10) } @Test @@ -1683,7 +1684,7 @@ class SearchGroupTest : VimTestCase() { ) enterCommand("set hlsearch incsearch") - enterSearch("and") + enterSearch("and") // Moves the care to "and" on second line: (1, 10) typeText(":", "%s/ass", "<Esc>") assertSearchHighlights( @@ -1695,9 +1696,456 @@ class SearchGroupTest : VimTestCase() { """.trimMargin(), ) - // TODO: Check caret position + // Make sure the caret is reset too + assertPosition(1, 10) } + // global + @Test + fun `test incsearch highlights for global command with range`() { + configureByText( + """I found it in a legendary land + |${c}all rocks and lavender and tufted grass, + |where it was settled on some sodden sand + |hard by the torrent of a mountain pass. + """.trimMargin(), + ) + enterCommand("set hlsearch incsearch") + + typeText(":", "%g/and") + + assertSearchHighlights( + "and", + """I found it in a legendary l‷and‴ + |all rocks «and» lavender «and» tufted grass, + |where it was settled on some sodden s«and» + |hard by the torrent of a mountain pass. + """.trimMargin(), + ) + } + + @Test + fun `test incsearch highlights for global command in whole file with default range`() { + configureByText( + """I found it in a legendary land + |${c}all rocks and lavender and tufted grass, + |where it was settled on some sodden sand + |hard by the torrent of a mountain pass. + """.trimMargin(), + ) + enterCommand("set hlsearch incsearch") + + typeText(":", "g/and") + + assertSearchHighlights( + "and", + """I found it in a legendary l‷and‴ + |all rocks «and» lavender «and» tufted grass, + |where it was settled on some sodden s«and» + |hard by the torrent of a mountain pass. + """.trimMargin(), + ) + } + + @Test + fun `test incsearch highlights for global-bang command`() { + configureByText( + """I found it in a legendary land + |${c}all rocks and lavender and tufted grass, + |where it was settled on some sodden sand + |hard by the torrent of a mountain pass. + """.trimMargin(), + ) + enterCommand("set hlsearch incsearch") + + typeText(":", "%g!/and") + + assertSearchHighlights( + "and", + """I found it in a legendary l‷and‴ + |all rocks «and» lavender «and» tufted grass, + |where it was settled on some sodden s«and» + |hard by the torrent of a mountain pass. + """.trimMargin(), + ) + } + + @Test + fun `test incsearch only highlights for global command after valid argument`() { + configureByText( + """I found it in a legendary land + |${c}all rocks and lavender and tufted grass, + |where it was settled on some sodden sand + |hard by the torrent of a mountain pass. + """.trimMargin(), + ) + enterCommand("set hlsearch incsearch") + + // E.g. don't remove highlights when trying to type :goto + enterSearch("and") + typeText(":g") + + assertSearchHighlights( + "and", + """I found it in a legendary l«and» + |all rocks «and» lavender «and» tufted grass, + |where it was settled on some sodden s«and» + |hard by the torrent of a mountain pass. + """.trimMargin(), + ) + } + + @Test + fun `test incsearch highlights for global command only highlights in range`() { + configureByText( + """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 and rush of a mountain pass. + """.trimMargin(), + ) + enterCommand("set hlsearch incsearch") + + typeText(":", "2,3g/and") + + assertSearchHighlights( + "and", + """I found it in a legendary land + |all rocks ‷and‴ lavender «and» tufted grass, + |where it was settled on some sodden s«and» + |hard by the torrent and rush of a mountain pass. + """.trimMargin(), + ) + } + + @Test + fun `test incsearch for global command starts at beginning of range not caret position`() { + configureByText( + """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. + |${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. + |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(), + ) + enterCommand("set hlsearch incsearch") + + typeText(":", "2,8g/and") + + assertSearchHighlights( + "and", + """I found it in a legendary land + |all rocks ‷and‴ lavender «and» tufted grass, + |where it was settled on some sodden s«and» + |hard by the torrent of a mountain pass. + |I found it in a legendary l«and» + |all rocks «and» lavender «and» tufted grass, + |where it was settled on some sodden s«and» + |hard by the torrent of a mountain pass. + |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(), + ) + } + + @Test + fun `test incsearch highlights for global command clears highlights on backspace`() { + configureByText( + """I found it in a legendary land + |${c}all rocks and lavender and tufted grass, + |where it was settled on some sodden sand + |hard by the torrent of a mountain pass. + """.trimMargin(), + ) + enterCommand("set hlsearch incsearch") + + typeText(":", "g/and", "<BS><BS><BS>") + + assertSearchHighlights( + "and", + """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(), + ) + } + + @Test + fun `test incsearch highlights for global command resets highlights on backspace`() { + configureByText( + """I found it in a legendary land + |${c}all rocks and lavender and tufted grass, + |where it was settled on some sodden sand + |hard by the torrent of a mountain pass. + """.trimMargin(), + ) + enterCommand("set hlsearch incsearch") + + enterSearch("and") // Moves the caret to "and" on the second line: (1, 10) + typeText(":", "g/roc", "<BS><BS><BS>") + + assertSearchHighlights( + "and", + """I found it in a legendary l«and» + |all rocks «and» lavender «and» tufted grass, + |where it was settled on some sodden s«and» + |hard by the torrent of a mountain pass. + """.trimMargin(), + ) + + // Make sure the caret is reset too + assertPosition(1, 10) + } + + @Test + fun `test cancelling incsearch highlights for global command shows previous highlights`() { + configureByText( + """I found it in a legendary land + |${c}all rocks and lavender and tufted grass, + |where it was settled on some sodden sand + |hard by the torrent of a mountain pass. + """.trimMargin(), + ) + enterCommand("set hlsearch incsearch") + + enterSearch("and") // Moves the caret to "and" on the second line (1, 10) + typeText(":", "g/ass", "<Esc>") + + assertSearchHighlights( + "and", + """I found it in a legendary l«and» + |all rocks «and» lavender «and» tufted grass, + |where it was settled on some sodden s«and» + |hard by the torrent of a mountain pass. + """.trimMargin(), + ) + + // Make sure the caret is reset too + assertPosition(1, 10) + } + + // vglobal + @Test + fun `test incsearch highlights for vglobal command with range`() { + configureByText( + """I found it in a legendary land + |${c}all rocks and lavender and tufted grass, + |where it was settled on some sodden sand + |hard by the torrent of a mountain pass. + """.trimMargin(), + ) + enterCommand("set hlsearch incsearch") + + typeText(":", "%v/and") + + assertSearchHighlights( + "and", + """I found it in a legendary l‷and‴ + |all rocks «and» lavender «and» tufted grass, + |where it was settled on some sodden s«and» + |hard by the torrent of a mountain pass. + """.trimMargin(), + ) + } + + @Test + fun `test incsearch highlights for vglobal command in whole file with default range`() { + configureByText( + """I found it in a legendary land + |${c}all rocks and lavender and tufted grass, + |where it was settled on some sodden sand + |hard by the torrent of a mountain pass. + """.trimMargin(), + ) + enterCommand("set hlsearch incsearch") + + typeText(":", "v/and") + + assertSearchHighlights( + "and", + """I found it in a legendary l‷and‴ + |all rocks «and» lavender «and» tufted grass, + |where it was settled on some sodden s«and» + |hard by the torrent of a mountain pass. + """.trimMargin(), + ) + } + + @Test + fun `test incsearch only highlights for vglobal command after valid argument`() { + configureByText( + """I found it in a legendary land + |${c}all rocks and lavender and tufted grass, + |where it was settled on some sodden sand + |hard by the torrent of a mountain pass. + """.trimMargin(), + ) + enterCommand("set hlsearch incsearch") + + // E.g. don't remove highlights when trying to type :vmap + enterSearch("and") + typeText(":v") + + assertSearchHighlights( + "and", + """I found it in a legendary l«and» + |all rocks «and» lavender «and» tufted grass, + |where it was settled on some sodden s«and» + |hard by the torrent of a mountain pass. + """.trimMargin(), + ) + } + + @Test + fun `test incsearch highlights for vglobal command only highlights in range`() { + configureByText( + """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 and rush of a mountain pass. + """.trimMargin(), + ) + enterCommand("set hlsearch incsearch") + + typeText(":", "2,3v/and") + + assertSearchHighlights( + "and", + """I found it in a legendary land + |all rocks ‷and‴ lavender «and» tufted grass, + |where it was settled on some sodden s«and» + |hard by the torrent and rush of a mountain pass. + """.trimMargin(), + ) + } + + @Test + fun `test incsearch for vglobal command starts at beginning of range not caret position`() { + configureByText( + """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. + |${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. + |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(), + ) + enterCommand("set hlsearch incsearch") + + typeText(":", "2,8v/and") + + assertSearchHighlights( + "and", + """I found it in a legendary land + |all rocks ‷and‴ lavender «and» tufted grass, + |where it was settled on some sodden s«and» + |hard by the torrent of a mountain pass. + |I found it in a legendary l«and» + |all rocks «and» lavender «and» tufted grass, + |where it was settled on some sodden s«and» + |hard by the torrent of a mountain pass. + |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(), + ) + } + + @Test + fun `test incsearch highlights for vglobal command clears highlights on backspace`() { + configureByText( + """I found it in a legendary land + |${c}all rocks and lavender and tufted grass, + |where it was settled on some sodden sand + |hard by the torrent of a mountain pass. + """.trimMargin(), + ) + enterCommand("set hlsearch incsearch") + + typeText(":", "v/and", "<BS><BS><BS>") + + assertSearchHighlights( + "and", + """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(), + ) + } + + @Test + fun `test incsearch highlights for vglobal command resets highlights on backspace`() { + configureByText( + """I found it in a legendary land + |${c}all rocks and lavender and tufted grass, + |where it was settled on some sodden sand + |hard by the torrent of a mountain pass. + """.trimMargin(), + ) + enterCommand("set hlsearch incsearch") + + enterSearch("and") // Moves the caret to "and" on the second line: (1, 10) + typeText(":", "v/roc", "<BS><BS><BS>") + + assertSearchHighlights( + "and", + """I found it in a legendary l«and» + |all rocks «and» lavender «and» tufted grass, + |where it was settled on some sodden s«and» + |hard by the torrent of a mountain pass. + """.trimMargin(), + ) + + // Make sure the caret is reset too + assertPosition(1, 10) + } + + @Test + fun `test cancelling incsearch highlights for vglobal command shows previous highlights`() { + configureByText( + """I found it in a legendary land + |${c}all rocks and lavender and tufted grass, + |where it was settled on some sodden sand + |hard by the torrent of a mountain pass. + """.trimMargin(), + ) + enterCommand("set hlsearch incsearch") + + enterSearch("and") // Moves the caret to "and" on the second line: (1, 10) + typeText(":", "v/ass", "<Esc>") + + assertSearchHighlights( + "and", + """I found it in a legendary l«and» + |all rocks «and» lavender «and» tufted grass, + |where it was settled on some sodden s«and» + |hard by the torrent of a mountain pass. + """.trimMargin(), + ) + + // Make sure the caret is reset too + assertPosition(1, 10) + } + + @Test fun `test incsearch updates selection when started in Visual mode`() { doTest( diff --git a/src/testFixtures/kotlin/org/jetbrains/plugins/ideavim/VimTestCase.kt b/src/testFixtures/kotlin/org/jetbrains/plugins/ideavim/VimTestCase.kt index 50102ba53..75a5a506b 100644 --- a/src/testFixtures/kotlin/org/jetbrains/plugins/ideavim/VimTestCase.kt +++ b/src/testFixtures/kotlin/org/jetbrains/plugins/ideavim/VimTestCase.kt @@ -59,7 +59,7 @@ import com.maddyhome.idea.vim.api.setToggleOption import com.maddyhome.idea.vim.api.visualLineToBufferLine import com.maddyhome.idea.vim.command.MappingMode import com.maddyhome.idea.vim.ex.ExException -import com.maddyhome.idea.vim.ex.ExOutputModel.Companion.getInstance +import com.maddyhome.idea.vim.ex.ExOutputModel import com.maddyhome.idea.vim.group.EffectiveIjOptions import com.maddyhome.idea.vim.group.GlobalIjOptions import com.maddyhome.idea.vim.group.IjOptions @@ -658,14 +658,14 @@ abstract class VimTestCase { } fun assertExOutput(expected: String) { - val actual = getInstance(fixture.editor).text + val actual = ExOutputModel.getInstance(fixture.editor).text assertNotNull(actual, "No Ex output") assertEquals(expected, actual) NeovimTesting.typeCommand("<esc>", testInfo, fixture.editor) } fun assertNoExOutput() { - val actual = getInstance(fixture.editor).text + val actual = ExOutputModel.getInstance(fixture.editor).text assertNull(actual, "Ex output not null") } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/Command.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/Command.kt index 27f390427..5ae0926d6 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/Command.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/Command.kt @@ -27,11 +27,14 @@ import com.maddyhome.idea.vim.vimscript.model.ExecutionResult import com.maddyhome.idea.vim.vimscript.model.VimLContext import java.util.* -public sealed class Command(protected var commandRange: Range, public val commandArgument: String) : Executable { +public sealed class Command(private val commandRange: Range, public val commandArgument: String) : Executable { override lateinit var vimContext: VimLContext override lateinit var rangeInScript: TextRange protected abstract val argFlags: CommandHandlerFlags + + protected var defaultRange: String = "." + private var nextArgumentTokenOffset = 0 private val logger = vimLogger<Command>() @@ -54,12 +57,13 @@ public sealed class Command(protected var commandRange: Range, public val comman @Throws(ExException::class) override fun execute(editor: VimEditor, context: ExecutionContext): ExecutionResult { - checkRanges(editor) - checkArgument() + validate(editor) + if (editor.nativeCarets().any { it.hasSelection() } && Flag.SAVE_VISUAL !in argFlags.flags) { editor.removeSelection() editor.removeSecondaryCarets() } + if (argFlags.access == Access.WRITABLE && !editor.isDocumentWritable()) { logger.info("Trying to modify readonly document") return ExecutionResult.Error @@ -101,6 +105,11 @@ public sealed class Command(protected var commandRange: Range, public val comman return result } + private fun validate(editor: VimEditor) { + checkRanges(editor) + checkArgument() + } + private fun checkRanges(editor: VimEditor) { if (RangeFlag.RANGE_FORBIDDEN == argFlags.rangeFlag && commandRange.size() != 0) { // Some commands (e.g. `:file`) throw "E474: Invalid argument" instead, while e.g. `:3ascii` throws E481 @@ -115,9 +124,16 @@ public sealed class Command(protected var commandRange: Range, public val comman // If a range isn't specified, the default range for most commands is the current line ("."). If the command is // expecting a count instead of a range, the default would be a count of 1, represented as the range "1". + // GlobalCommand is the only other command that has a different default. We could introduce another RangeFlag + // (and maybe make them an enum set so it can be optional and whole-file-range), or set it + // TODO: This is initialisation, not validation + // It would be nice to do this in the constructor, but argFlags is abstract, so we can't access it if (RangeFlag.RANGE_IS_COUNT == argFlags.rangeFlag) { commandRange.defaultRange = "1" } + else { + commandRange.defaultRange = defaultRange + } } private fun checkArgument() { @@ -218,6 +234,8 @@ public sealed class Command(protected var commandRange: Range, public val comman private fun getNextArgumentToken() = commandArgument.substring(nextArgumentTokenOffset).trimStart() + protected fun isRangeSpecified(): Boolean = commandRange.size() > 0 + /** * Return the last line of the range as a count, one-based */ @@ -276,11 +294,30 @@ public sealed class Command(protected var commandRange: Range, public val comman ?: throw exExceptionMessage(Msg.e_invrange) // E16: Invalid range } - public fun getLine(editor: VimEditor): Int = getLine(editor, editor.currentCaret()) - public fun getLine(editor: VimEditor, caret: VimCaret): Int = commandRange.getLine(editor, caret) + protected fun getLine(editor: VimEditor): Int = getLine(editor, editor.currentCaret()) + protected fun getLine(editor: VimEditor, caret: VimCaret): Int = commandRange.getLine(editor, caret) - public fun getLineRange(editor: VimEditor): LineRange = getLineRange(editor, editor.currentCaret()) - public fun getLineRange(editor: VimEditor, caret: VimCaret): LineRange = commandRange.getLineRange(editor, caret) + protected fun getLineRange(editor: VimEditor): LineRange = getLineRange(editor, editor.currentCaret()) + protected fun getLineRange(editor: VimEditor, caret: VimCaret): LineRange = commandRange.getLineRange(editor, caret) + + /** + * Accessor method purely for incsearch + * + * Ensures that the range and argument have been correctly initialised and validated, specifically that the default + * range has been set. Any validation errors are swallowed and ignored. + * + * It would be cleaner to move incsearch handling into the search Command instances, which could access this data + * safely. + */ + public fun getLineRangeSafe(editor: VimEditor): LineRange? { + try { + validate(editor) + } + catch (t: Throwable) { + return null + } + return getLineRange(editor) + } /** * Get the line range using the optional count argument @@ -291,8 +328,8 @@ public sealed class Command(protected var commandRange: Range, public val comman * The `{count}` argument must be a simple integer, with no trailing characters. This function will fail with "E488: * Trailing characters" otherwise. */ - public fun getLineRangeWithCount(editor: VimEditor, caret: VimCaret): LineRange { - val lineRange = commandRange.getLineRange(editor, caret) + protected fun getLineRangeWithCount(editor: VimEditor, caret: VimCaret): LineRange { + val lineRange = getLineRange(editor, caret) return getCountFromArgument()?.let { count -> LineRange(lineRange.endLine, lineRange.endLine + count - 1) } ?: lineRange diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/FileCommand.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/FileCommand.kt index b377e02b2..8b34868c7 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/FileCommand.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/FileCommand.kt @@ -28,7 +28,7 @@ public data class FileCommand(val range: Range, val argument: String) : Command. // TODO: Support the `:file {name}` argument to set the name of the current file // Note that `:file` doesn't really support a range or count. But `:0file` is support to remove the current file // name. We don't support either of these features, but by accepting a range/count, we can report the right error - if (commandRange.size() != 0) { + if (isRangeSpecified()) { throw ExException("E474: Invalid argument") }