mirror of
https://github.com/chylex/IntelliJ-IdeaVim.git
synced 2025-06-16 21:39:58 +02:00
Support incsearch highlighting for global command
Fixes VIM-2891
This commit is contained in:
parent
f552e43c5b
commit
f64c99c406
src
main/java/com/maddyhome/idea/vim
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
@ -34,6 +34,7 @@ import com.maddyhome.idea.vim.regexp.CharPointer;
|
|||||||
import com.maddyhome.idea.vim.regexp.RegExp;
|
import com.maddyhome.idea.vim.regexp.RegExp;
|
||||||
import com.maddyhome.idea.vim.ui.ExPanelBorder;
|
import com.maddyhome.idea.vim.ui.ExPanelBorder;
|
||||||
import com.maddyhome.idea.vim.vimscript.model.commands.Command;
|
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.model.commands.SubstituteCommand;
|
||||||
import com.maddyhome.idea.vim.vimscript.parser.VimscriptParser;
|
import com.maddyhome.idea.vim.vimscript.parser.VimscriptParser;
|
||||||
import org.jetbrains.annotations.Contract;
|
import org.jetbrains.annotations.Contract;
|
||||||
@ -270,11 +271,13 @@ public class ExEntryPanel extends JPanel implements VimCommandLine {
|
|||||||
try {
|
try {
|
||||||
final Editor editor = entry.getEditor();
|
final Editor editor = entry.getEditor();
|
||||||
|
|
||||||
|
final String labelText = label.getText(); // Either '/', '?' or ':'boolean searchCommand = false;
|
||||||
|
|
||||||
boolean searchCommand = false;
|
boolean searchCommand = false;
|
||||||
LineRange searchRange = null;
|
LineRange searchRange = null;
|
||||||
char separator = label.getText().charAt(0);
|
char separator = labelText.charAt(0);
|
||||||
String searchText = entry.getActualText();
|
String searchText = entry.getActualText();
|
||||||
if (label.getText().equals(":")) {
|
if (labelText.equals(":")) {
|
||||||
if (searchText.isEmpty()) return;
|
if (searchText.isEmpty()) return;
|
||||||
final Command command = getIncsearchCommand(searchText);
|
final Command command = getIncsearchCommand(searchText);
|
||||||
if (command == null) {
|
if (command == null) {
|
||||||
@ -287,14 +290,18 @@ public class ExEntryPanel extends JPanel implements VimCommandLine {
|
|||||||
separator = argument.charAt(0);
|
separator = argument.charAt(0);
|
||||||
searchText = argument.substring(1);
|
searchText = argument.substring(1);
|
||||||
}
|
}
|
||||||
if (searchText.length() == 0) {
|
if (!searchText.isEmpty()) {
|
||||||
// Reset back to the original search highlights after deleting a search from a substitution command.
|
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
|
// E.g. Highlight `whatever`, type `:%s/foo` + highlight `foo`, delete back to `:%s/` and reset highlights
|
||||||
// back to `whatever`
|
// back to `whatever`
|
||||||
VimPlugin.getSearch().resetIncsearchHighlights();
|
VimPlugin.getSearch().resetIncsearchHighlights();
|
||||||
|
resetCaretOffset(editor);
|
||||||
return;
|
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
|
// 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.
|
// obviously won't be a count.
|
||||||
int count1 = Math.max(1, KeyHandler.getInstance().getKeyHandlerState().getEditorCommandBuilder().getCount());
|
int count1 = Math.max(1, KeyHandler.getInstance().getKeyHandlerState().getEditorCommandBuilder().getCount());
|
||||||
|
|
||||||
final String labelText = label.getText();
|
|
||||||
if (labelText.equals("/") || labelText.equals("?") || searchCommand) {
|
if (labelText.equals("/") || labelText.equals("?") || searchCommand) {
|
||||||
final boolean forwards = !labelText.equals("?"); // :s, :g, :v are treated as forwards
|
final boolean forwards = !labelText.equals("?"); // :s, :g, :v are treated as forwards
|
||||||
final String pattern;
|
final String pattern;
|
||||||
@ -334,8 +340,8 @@ public class ExEntryPanel extends JPanel implements VimCommandLine {
|
|||||||
if (commandText == null) return null;
|
if (commandText == null) return null;
|
||||||
try {
|
try {
|
||||||
final Command exCommand = VimscriptParser.INSTANCE.parseCommand(commandText);
|
final Command exCommand = VimscriptParser.INSTANCE.parseCommand(commandText);
|
||||||
// TODO: Add global, vglobal, smagic and snomagic here when the commands are supported
|
// TODO: Add smagic and snomagic here if/when the commands are supported
|
||||||
if (exCommand instanceof SubstituteCommand) {
|
if (exCommand instanceof SubstituteCommand || exCommand instanceof GlobalCommand) {
|
||||||
return exCommand;
|
return exCommand;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,19 +39,19 @@ import com.maddyhome.idea.vim.vimscript.model.ExecutionResult
|
|||||||
*/
|
*/
|
||||||
@ExCommand(command = "g[lobal],v[global]")
|
@ExCommand(command = "g[lobal],v[global]")
|
||||||
internal data class GlobalCommand(val range: Range, val argument: String, val invert: Boolean) : Command.SingleExecution(range, argument) {
|
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 val argFlags = flags(RangeFlag.RANGE_OPTIONAL, ArgumentFlag.ARGUMENT_OPTIONAL, Access.SELF_SYNCHRONIZED)
|
||||||
|
|
||||||
override fun processCommand(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments): ExecutionResult {
|
override fun processCommand(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments): ExecutionResult {
|
||||||
var result: ExecutionResult = ExecutionResult.Success
|
var result: ExecutionResult = ExecutionResult.Success
|
||||||
editor.removeSecondaryCarets()
|
editor.removeSecondaryCarets()
|
||||||
val caret = editor.currentCaret()
|
val caret = editor.currentCaret()
|
||||||
|
val lineRange = getLineRange(editor, caret)
|
||||||
// For :g command the default range is %
|
|
||||||
val lineRange: LineRange = if (range.size() == 0) {
|
|
||||||
LineRange(0, editor.lineCount() - 1)
|
|
||||||
} else {
|
|
||||||
getLineRange(editor, caret)
|
|
||||||
}
|
|
||||||
if (!processGlobalCommand(editor, context, lineRange)) {
|
if (!processGlobalCommand(editor, context, lineRange)) {
|
||||||
result = ExecutionResult.Error
|
result = ExecutionResult.Error
|
||||||
}
|
}
|
||||||
|
@ -1657,7 +1657,7 @@ class SearchGroupTest : VimTestCase() {
|
|||||||
)
|
)
|
||||||
enterCommand("set hlsearch incsearch")
|
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>")
|
typeText(":", "%s/roc", "<BS><BS><BS>")
|
||||||
|
|
||||||
assertSearchHighlights(
|
assertSearchHighlights(
|
||||||
@ -1669,7 +1669,8 @@ class SearchGroupTest : VimTestCase() {
|
|||||||
""".trimMargin(),
|
""".trimMargin(),
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: Check caret position
|
// Make sure the caret is reset too
|
||||||
|
assertPosition(1, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -1683,7 +1684,7 @@ class SearchGroupTest : VimTestCase() {
|
|||||||
)
|
)
|
||||||
enterCommand("set hlsearch incsearch")
|
enterCommand("set hlsearch incsearch")
|
||||||
|
|
||||||
enterSearch("and")
|
enterSearch("and") // Moves the care to "and" on second line: (1, 10)
|
||||||
typeText(":", "%s/ass", "<Esc>")
|
typeText(":", "%s/ass", "<Esc>")
|
||||||
|
|
||||||
assertSearchHighlights(
|
assertSearchHighlights(
|
||||||
@ -1695,9 +1696,456 @@ class SearchGroupTest : VimTestCase() {
|
|||||||
""".trimMargin(),
|
""".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
|
@Test
|
||||||
fun `test incsearch updates selection when started in Visual mode`() {
|
fun `test incsearch updates selection when started in Visual mode`() {
|
||||||
doTest(
|
doTest(
|
||||||
|
@ -59,7 +59,7 @@ import com.maddyhome.idea.vim.api.setToggleOption
|
|||||||
import com.maddyhome.idea.vim.api.visualLineToBufferLine
|
import com.maddyhome.idea.vim.api.visualLineToBufferLine
|
||||||
import com.maddyhome.idea.vim.command.MappingMode
|
import com.maddyhome.idea.vim.command.MappingMode
|
||||||
import com.maddyhome.idea.vim.ex.ExException
|
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.EffectiveIjOptions
|
||||||
import com.maddyhome.idea.vim.group.GlobalIjOptions
|
import com.maddyhome.idea.vim.group.GlobalIjOptions
|
||||||
import com.maddyhome.idea.vim.group.IjOptions
|
import com.maddyhome.idea.vim.group.IjOptions
|
||||||
@ -658,14 +658,14 @@ abstract class VimTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun assertExOutput(expected: String) {
|
fun assertExOutput(expected: String) {
|
||||||
val actual = getInstance(fixture.editor).text
|
val actual = ExOutputModel.getInstance(fixture.editor).text
|
||||||
assertNotNull(actual, "No Ex output")
|
assertNotNull(actual, "No Ex output")
|
||||||
assertEquals(expected, actual)
|
assertEquals(expected, actual)
|
||||||
NeovimTesting.typeCommand("<esc>", testInfo, fixture.editor)
|
NeovimTesting.typeCommand("<esc>", testInfo, fixture.editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assertNoExOutput() {
|
fun assertNoExOutput() {
|
||||||
val actual = getInstance(fixture.editor).text
|
val actual = ExOutputModel.getInstance(fixture.editor).text
|
||||||
assertNull(actual, "Ex output not null")
|
assertNull(actual, "Ex output not null")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,11 +27,14 @@ import com.maddyhome.idea.vim.vimscript.model.ExecutionResult
|
|||||||
import com.maddyhome.idea.vim.vimscript.model.VimLContext
|
import com.maddyhome.idea.vim.vimscript.model.VimLContext
|
||||||
import java.util.*
|
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 vimContext: VimLContext
|
||||||
override lateinit var rangeInScript: TextRange
|
override lateinit var rangeInScript: TextRange
|
||||||
|
|
||||||
protected abstract val argFlags: CommandHandlerFlags
|
protected abstract val argFlags: CommandHandlerFlags
|
||||||
|
|
||||||
|
protected var defaultRange: String = "."
|
||||||
|
|
||||||
private var nextArgumentTokenOffset = 0
|
private var nextArgumentTokenOffset = 0
|
||||||
private val logger = vimLogger<Command>()
|
private val logger = vimLogger<Command>()
|
||||||
|
|
||||||
@ -54,12 +57,13 @@ public sealed class Command(protected var commandRange: Range, public val comman
|
|||||||
|
|
||||||
@Throws(ExException::class)
|
@Throws(ExException::class)
|
||||||
override fun execute(editor: VimEditor, context: ExecutionContext): ExecutionResult {
|
override fun execute(editor: VimEditor, context: ExecutionContext): ExecutionResult {
|
||||||
checkRanges(editor)
|
validate(editor)
|
||||||
checkArgument()
|
|
||||||
if (editor.nativeCarets().any { it.hasSelection() } && Flag.SAVE_VISUAL !in argFlags.flags) {
|
if (editor.nativeCarets().any { it.hasSelection() } && Flag.SAVE_VISUAL !in argFlags.flags) {
|
||||||
editor.removeSelection()
|
editor.removeSelection()
|
||||||
editor.removeSecondaryCarets()
|
editor.removeSecondaryCarets()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (argFlags.access == Access.WRITABLE && !editor.isDocumentWritable()) {
|
if (argFlags.access == Access.WRITABLE && !editor.isDocumentWritable()) {
|
||||||
logger.info("Trying to modify readonly document")
|
logger.info("Trying to modify readonly document")
|
||||||
return ExecutionResult.Error
|
return ExecutionResult.Error
|
||||||
@ -101,6 +105,11 @@ public sealed class Command(protected var commandRange: Range, public val comman
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun validate(editor: VimEditor) {
|
||||||
|
checkRanges(editor)
|
||||||
|
checkArgument()
|
||||||
|
}
|
||||||
|
|
||||||
private fun checkRanges(editor: VimEditor) {
|
private fun checkRanges(editor: VimEditor) {
|
||||||
if (RangeFlag.RANGE_FORBIDDEN == argFlags.rangeFlag && commandRange.size() != 0) {
|
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
|
// 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
|
// 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".
|
// 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) {
|
if (RangeFlag.RANGE_IS_COUNT == argFlags.rangeFlag) {
|
||||||
commandRange.defaultRange = "1"
|
commandRange.defaultRange = "1"
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
commandRange.defaultRange = defaultRange
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkArgument() {
|
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()
|
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
|
* 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
|
?: throw exExceptionMessage(Msg.e_invrange) // E16: Invalid range
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun getLine(editor: VimEditor): Int = getLine(editor, editor.currentCaret())
|
protected 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, caret: VimCaret): Int = commandRange.getLine(editor, caret)
|
||||||
|
|
||||||
public fun getLineRange(editor: VimEditor): LineRange = getLineRange(editor, editor.currentCaret())
|
protected 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, 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
|
* 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:
|
* The `{count}` argument must be a simple integer, with no trailing characters. This function will fail with "E488:
|
||||||
* Trailing characters" otherwise.
|
* Trailing characters" otherwise.
|
||||||
*/
|
*/
|
||||||
public fun getLineRangeWithCount(editor: VimEditor, caret: VimCaret): LineRange {
|
protected fun getLineRangeWithCount(editor: VimEditor, caret: VimCaret): LineRange {
|
||||||
val lineRange = commandRange.getLineRange(editor, caret)
|
val lineRange = getLineRange(editor, caret)
|
||||||
return getCountFromArgument()?.let { count ->
|
return getCountFromArgument()?.let { count ->
|
||||||
LineRange(lineRange.endLine, lineRange.endLine + count - 1)
|
LineRange(lineRange.endLine, lineRange.endLine + count - 1)
|
||||||
} ?: lineRange
|
} ?: lineRange
|
||||||
|
@ -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
|
// 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
|
// 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
|
// 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")
|
throw ExException("E474: Invalid argument")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user