1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2025-05-29 19:34:09 +02:00

Support incsearch highlighting for global command

Fixes VIM-2891
This commit is contained in:
Matt Ellis 2024-04-18 08:46:09 +01:00 committed by Alex Pláte
parent f552e43c5b
commit f64c99c406
6 changed files with 523 additions and 32 deletions
src
main/java/com/maddyhome/idea/vim
ui/ex
vimscript/model/commands
test/java/org/jetbrains/plugins/ideavim/group
testFixtures/kotlin/org/jetbrains/plugins/ideavim
vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands

View File

@ -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;
}
}

View File

@ -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
}

View File

@ -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(

View File

@ -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")
}

View File

@ -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

View File

@ -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")
}