diff --git a/resources/dictionaries/ideavim.dic b/resources/dictionaries/ideavim.dic index bb65d06c5c..14d68378ba 100644 --- a/resources/dictionaries/ideavim.dic +++ b/resources/dictionaries/ideavim.dic @@ -3,6 +3,7 @@ ideavimrc gdefault +guicursor hlsearch ideamarks ignorecase diff --git a/resources/messages/IdeaVimBundle.properties b/resources/messages/IdeaVimBundle.properties index 7d148d051e..6c2b3fc944 100644 --- a/resources/messages/IdeaVimBundle.properties +++ b/resources/messages/IdeaVimBundle.properties @@ -75,6 +75,10 @@ E475=E475: Invalid argument: {0} # Vim's message includes alternate files and the :p:h file name modifier, which we don't support # E499: Empty file name for '%' or '#', only works with ":p:h" E499=E499: Empty file name for '%' +E545=E545: Missing colon: {0} +E546=E546: Illegal mode: {0} +E548=E548: Digit expected: {0} +E549=E549: Illegal percentage: {0} E774=E774: 'operatorfunc' is empty action.VimPluginToggle.text=Vim Emulator diff --git a/src/com/maddyhome/idea/vim/KeyHandler.java b/src/com/maddyhome/idea/vim/KeyHandler.java index a6934ca748..77b1857fd8 100644 --- a/src/com/maddyhome/idea/vim/KeyHandler.java +++ b/src/com/maddyhome/idea/vim/KeyHandler.java @@ -38,13 +38,13 @@ import com.intellij.openapi.ui.popup.ListPopup; import com.intellij.openapi.util.Ref; import com.maddyhome.idea.vim.action.change.VimRepeater; +import com.maddyhome.idea.vim.action.change.change.ChangeCharacterAction; +import com.maddyhome.idea.vim.action.change.change.ChangeVisualCharacterAction; import com.maddyhome.idea.vim.action.change.insert.InsertCompletedDigraphAction; import com.maddyhome.idea.vim.action.change.insert.InsertCompletedLiteralAction; import com.maddyhome.idea.vim.action.macro.ToggleRecordingAction; import com.maddyhome.idea.vim.command.*; -import com.maddyhome.idea.vim.group.ChangeGroup; import com.maddyhome.idea.vim.group.RegisterGroup; -import com.maddyhome.idea.vim.group.visual.VisualGroupKt; import com.maddyhome.idea.vim.handler.ActionBeanClass; import com.maddyhome.idea.vim.handler.EditorActionHandlerBase; import com.maddyhome.idea.vim.helper.*; @@ -327,6 +327,7 @@ else if (commandBuilder.isBad()) { LOG.trace("Command builder is set to BAD"); editorState.resetOpPending(); editorState.resetRegisterPending(); + editorState.resetReplaceCharacter(); VimPlugin.indicateError(); reset(editor); } @@ -363,7 +364,11 @@ public static boolean isPrefix(@NotNull List list1, @NotNull List list } private void handleEditorReset(@NotNull Editor editor, @NotNull KeyStroke key, final @NotNull DataContext context, @NotNull CommandState editorState) { - if (editorState.getCommandBuilder().isAtDefaultState()) { + final CommandBuilder commandBuilder = editorState.getCommandBuilder(); + if (commandBuilder.isAwaitingCharOrDigraphArgument()) { + editorState.resetReplaceCharacter(); + } + if (commandBuilder.isAtDefaultState()) { RegisterGroup register = VimPlugin.getRegister(); if (register.getCurrentRegister() == register.getDefaultRegister()) { boolean indicateError = true; @@ -383,7 +388,6 @@ private void handleEditorReset(@NotNull Editor editor, @NotNull KeyStroke key, f } } reset(editor); - ChangeGroup.resetCaret(editor, false); } private boolean handleKeyMapping(final @NotNull Editor editor, @@ -688,6 +692,8 @@ private void handleCharArgument(@NotNull KeyStroke key, char chKey, @NotNull Com // Oops - this isn't a valid character argument commandBuilder.setCommandState(CurrentCommandState.BAD_COMMAND); } + + commandState.resetReplaceCharacter(); } private boolean handleDigraph(@NotNull Editor editor, @@ -899,7 +905,8 @@ private void startWaitingForArgument(Editor editor, if (action instanceof InsertCompletedDigraphAction) { editorState.startDigraphSequence(); setPromptCharacterEx('?'); - } else if (action instanceof InsertCompletedLiteralAction) { + } + else if (action instanceof InsertCompletedLiteralAction) { editorState.startLiteralSequence(); setPromptCharacterEx('^'); } @@ -912,6 +919,11 @@ private void startWaitingForArgument(Editor editor, editorState.pushModes(CommandState.Mode.CMD_LINE, CommandState.SubMode.NONE); break; } + + // Another special case. Force a mode change to update the caret shape + if (action instanceof ChangeCharacterAction || action instanceof ChangeVisualCharacterAction) { + editorState.pushModes(editorState.getMode(), CommandState.SubMode.REPLACE_CHARACTER); + } } private boolean checkArgumentCompatibility(@Nullable Argument.Type expectedArgumentType, @NotNull EditorActionHandlerBase action) { @@ -971,7 +983,6 @@ public void fullReset(@NotNull Editor editor) { if (registerGroup != null) { registerGroup.resetRegister(); } - VisualGroupKt.updateCaretState(editor); editor.getSelectionModel().removeSelection(); } @@ -1066,7 +1077,6 @@ public void run() { if (editorState.getSubMode() == CommandState.SubMode.SINGLE_COMMAND && (!cmd.getFlags().contains(CommandFlags.FLAG_EXPECT_MORE))) { editorState.popModes(); - VisualGroupKt.resetShape(CommandStateHelper.getMode(editor), editor); } if (editorState.getCommandBuilder().isDone()) { diff --git a/src/com/maddyhome/idea/vim/action/motion/select/SelectToggleVisualMode.kt b/src/com/maddyhome/idea/vim/action/motion/select/SelectToggleVisualMode.kt index be750eba39..a4b3f8978d 100644 --- a/src/com/maddyhome/idea/vim/action/motion/select/SelectToggleVisualMode.kt +++ b/src/com/maddyhome/idea/vim/action/motion/select/SelectToggleVisualMode.kt @@ -23,7 +23,6 @@ import com.intellij.openapi.editor.Editor import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.command.Command import com.maddyhome.idea.vim.command.CommandState -import com.maddyhome.idea.vim.group.visual.updateCaretState import com.maddyhome.idea.vim.handler.VimActionHandler import com.maddyhome.idea.vim.helper.commandState import com.maddyhome.idea.vim.helper.moveToInlayAwareOffset @@ -60,7 +59,6 @@ class SelectToggleVisualMode : VimActionHandler.SingleExecution() { } } } - updateCaretState(editor) return true } } diff --git a/src/com/maddyhome/idea/vim/action/motion/visual/VisualToggleBlockModeAction.kt b/src/com/maddyhome/idea/vim/action/motion/visual/VisualToggleBlockModeAction.kt index 28fb29fcd4..0491a361cd 100644 --- a/src/com/maddyhome/idea/vim/action/motion/visual/VisualToggleBlockModeAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/visual/VisualToggleBlockModeAction.kt @@ -23,14 +23,14 @@ import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.command.Command import com.maddyhome.idea.vim.command.CommandState import com.maddyhome.idea.vim.handler.VimActionHandler -import com.maddyhome.idea.vim.option.ListOption import com.maddyhome.idea.vim.option.OptionsManager.selectmode +import com.maddyhome.idea.vim.option.StringListOption class VisualToggleBlockModeAction : VimActionHandler.SingleExecution() { override val type: Command.Type = Command.Type.OTHER_READONLY override fun execute(editor: Editor, context: DataContext, cmd: Command): Boolean { - val listOption: ListOption = selectmode + val listOption: StringListOption = selectmode return if (listOption.contains("cmd")) { VimPlugin.getVisualMotion().enterSelectMode(editor, CommandState.SubMode.VISUAL_BLOCK) } else VimPlugin.getVisualMotion() diff --git a/src/com/maddyhome/idea/vim/action/motion/visual/VisualToggleCharacterModeAction.kt b/src/com/maddyhome/idea/vim/action/motion/visual/VisualToggleCharacterModeAction.kt index e929ccb050..541256b286 100644 --- a/src/com/maddyhome/idea/vim/action/motion/visual/VisualToggleCharacterModeAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/visual/VisualToggleCharacterModeAction.kt @@ -23,14 +23,14 @@ import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.command.Command import com.maddyhome.idea.vim.command.CommandState import com.maddyhome.idea.vim.handler.VimActionHandler -import com.maddyhome.idea.vim.option.ListOption import com.maddyhome.idea.vim.option.OptionsManager.selectmode +import com.maddyhome.idea.vim.option.StringListOption class VisualToggleCharacterModeAction : VimActionHandler.SingleExecution() { override val type: Command.Type = Command.Type.OTHER_READONLY override fun execute(editor: Editor, context: DataContext, cmd: Command): Boolean { - val listOption: ListOption = selectmode + val listOption: StringListOption = selectmode return if (listOption.contains("cmd")) { VimPlugin.getVisualMotion().enterSelectMode(editor, CommandState.SubMode.VISUAL_CHARACTER) } else VimPlugin.getVisualMotion() diff --git a/src/com/maddyhome/idea/vim/command/CommandState.kt b/src/com/maddyhome/idea/vim/command/CommandState.kt index 138d659497..ba430888cd 100644 --- a/src/com/maddyhome/idea/vim/command/CommandState.kt +++ b/src/com/maddyhome/idea/vim/command/CommandState.kt @@ -27,6 +27,8 @@ import com.maddyhome.idea.vim.helper.DigraphSequence import com.maddyhome.idea.vim.helper.MessageHelper import com.maddyhome.idea.vim.helper.VimNlsSafe import com.maddyhome.idea.vim.helper.noneOfEnum +import com.maddyhome.idea.vim.helper.updateCaretsVisualAttributes +import com.maddyhome.idea.vim.helper.updateCaretsVisualPosition import com.maddyhome.idea.vim.helper.vimCommandState import com.maddyhome.idea.vim.key.CommandPartNode import com.maddyhome.idea.vim.option.OptionsManager.showmode @@ -35,9 +37,9 @@ import java.util.* import javax.swing.KeyStroke /** - * Used to maintain state while entering a Vim command (operator, motion, text object, etc.) + * Used to maintain state before and while entering a Vim command (operator, motion, text object, etc.) */ -class CommandState private constructor() { +class CommandState private constructor(private val editor: Editor) { val commandBuilder = CommandBuilder(getKeyRootNode(MappingMode.NORMAL)) private val modeStates = Stack() val mappingState = MappingState() @@ -45,7 +47,7 @@ class CommandState private constructor() { var isRecording = false set(value) { field = value - updateStatus() + doShowMode() } var isDotRepeatInProgress = false @@ -78,17 +80,26 @@ class CommandState private constructor() { fun pushModes(mode: Mode, submode: SubMode) { val newModeState = ModeState(mode, submode) + logger.debug("Push new mode state: ${newModeState.toSimpleString()}") logger.debug { "Stack of mode states before push: ${toSimpleString()}" } + + val previousMode = currentModeState() modeStates.push(newModeState) setMappingMode() - updateStatus() + + if (previousMode != newModeState) { + onModeChanged() + } } fun popModes() { val popped = modeStates.pop() setMappingMode() - updateStatus() + if (popped != currentModeState()) { + onModeChanged() + } + logger.debug("Popped mode state: ${popped.toSimpleString()}") logger.debug { "Stack of mode states after pop: ${toSimpleString()}" } } @@ -99,6 +110,12 @@ class CommandState private constructor() { } } + fun resetReplaceCharacter() { + if (subMode == SubMode.REPLACE_CHARACTER) { + popModes() + } + } + fun resetRegisterPending() { if (subMode == SubMode.REGISTER_PENDING) { popModes() @@ -107,13 +124,18 @@ class CommandState private constructor() { private fun resetModes() { modeStates.clear() + onModeChanged() setMappingMode() } + private fun onModeChanged() { + editor.updateCaretsVisualAttributes() + editor.updateCaretsVisualPosition() + doShowMode() + } + private fun setMappingMode() { - val modeState = currentModeState() - val newMappingMode = if (modeState.mode == Mode.OP_PENDING) MappingMode.OP_PENDING else modeToMappingMode(mode) - mappingState.mappingMode = newMappingMode + mappingState.mappingMode = modeToMappingMode(mode) } @Contract(pure = true) @@ -124,7 +146,7 @@ class CommandState private constructor() { Mode.VISUAL -> MappingMode.VISUAL Mode.SELECT -> MappingMode.SELECT Mode.CMD_LINE -> MappingMode.CMD_LINE - else -> error("Unexpected mode: $mode") + Mode.OP_PENDING -> MappingMode.OP_PENDING } } @@ -137,7 +159,6 @@ class CommandState private constructor() { val modeState = currentModeState() popModes() pushModes(modeState.mode, submode) - updateStatus() } fun startDigraphSequence() { @@ -183,7 +204,6 @@ class CommandState private constructor() { resetModes() commandBuilder.resetInProgressCommandPart(getKeyRootNode(mappingState.mappingMode)) digraphSequence.reset() - updateStatus() } fun toSimpleString(): String = modeStates.joinToString { it.toSimpleString() } @@ -262,7 +282,7 @@ class CommandState private constructor() { return if (modeStates.size > 0) modeStates.peek() else defaultModeState } - private fun updateStatus() { + private fun doShowMode() { val msg = StringBuilder() if (showmode.isSet) { msg.append(getStatusString(modeStates.size - 1)) @@ -319,10 +339,10 @@ class CommandState private constructor() { } enum class SubMode { - NONE, SINGLE_COMMAND, REGISTER_PENDING, VISUAL_CHARACTER, VISUAL_LINE, VISUAL_BLOCK + NONE, SINGLE_COMMAND, REGISTER_PENDING, REPLACE_CHARACTER, VISUAL_CHARACTER, VISUAL_LINE, VISUAL_BLOCK } - private class ModeState(val mode: Mode, val subMode: SubMode) { + private data class ModeState(val mode: Mode, val subMode: SubMode) { fun toSimpleString(): String = "$mode:$subMode" } @@ -334,7 +354,7 @@ class CommandState private constructor() { fun getInstance(editor: Editor): CommandState { var res = editor.vimCommandState if (res == null) { - res = CommandState() + res = CommandState(editor) editor.vimCommandState = res } return res diff --git a/src/com/maddyhome/idea/vim/ex/ExExceptions.kt b/src/com/maddyhome/idea/vim/ex/ExExceptions.kt index 42d9a406c6..939b87ee88 100644 --- a/src/com/maddyhome/idea/vim/ex/ExExceptions.kt +++ b/src/com/maddyhome/idea/vim/ex/ExExceptions.kt @@ -17,7 +17,18 @@ */ package com.maddyhome.idea.vim.ex -open class ExException(s: String? = null) : Exception(s) +import com.maddyhome.idea.vim.helper.MessageHelper +import org.jetbrains.annotations.PropertyKey + +open class ExException(s: String? = null) : Exception(s) { + var code: String? = null + private set + + companion object { + fun message(@PropertyKey(resourceBundle = MessageHelper.BUNDLE) code: String, vararg params: Any) = + ExException(MessageHelper.message(code, *params)).apply { this.code = code } + } +} class InvalidCommandException(message: String, cmd: String?) : ExException(message + if (cmd != null) " | $cmd" else "") diff --git a/src/com/maddyhome/idea/vim/extension/multiplecursors/VimMultipleCursorsExtension.kt b/src/com/maddyhome/idea/vim/extension/multiplecursors/VimMultipleCursorsExtension.kt index ffa1b42cd0..a696920c24 100644 --- a/src/com/maddyhome/idea/vim/extension/multiplecursors/VimMultipleCursorsExtension.kt +++ b/src/com/maddyhome/idea/vim/extension/multiplecursors/VimMultipleCursorsExtension.kt @@ -43,6 +43,7 @@ import com.maddyhome.idea.vim.helper.endOffsetInclusive import com.maddyhome.idea.vim.helper.enumSetOf import com.maddyhome.idea.vim.helper.exitVisualMode import com.maddyhome.idea.vim.helper.inVisualMode +import com.maddyhome.idea.vim.helper.updateCaretsVisualAttributes import com.maddyhome.idea.vim.helper.userData import com.maddyhome.idea.vim.option.OptionsManager import org.jetbrains.annotations.NonNls @@ -173,6 +174,7 @@ class VimMultipleCursorsExtension : VimExtension { if (newPositions.size > 0) { editor.exitVisualMode() newPositions.forEach { editor.caretModel.addCaret(it, true) ?: return } + editor.updateCaretsVisualAttributes() return } @@ -216,6 +218,7 @@ class VimMultipleCursorsExtension : VimExtension { } val caret = editor.caretModel.addCaret(editor.offsetToVisualPosition(nextOffset), true) ?: return + editor.updateCaretsVisualAttributes() editor.vimMultipleCursorsLastSelection = selectText(caret, pattern, nextOffset) } else { VimPlugin.showMessage(MessageHelper.message("message.no.more.matches")) @@ -254,6 +257,7 @@ class VimMultipleCursorsExtension : VimExtension { selectText(caret, text, match.startOffset) } } + editor.updateCaretsVisualAttributes() } } diff --git a/src/com/maddyhome/idea/vim/group/ChangeGroup.java b/src/com/maddyhome/idea/vim/group/ChangeGroup.java index fcd2eebf40..ab3eea4d95 100644 --- a/src/com/maddyhome/idea/vim/group/ChangeGroup.java +++ b/src/com/maddyhome/idea/vim/group/ChangeGroup.java @@ -56,7 +56,6 @@ import com.maddyhome.idea.vim.common.TextRange; import com.maddyhome.idea.vim.ex.ranges.LineRange; import com.maddyhome.idea.vim.group.visual.VimSelection; -import com.maddyhome.idea.vim.group.visual.VisualGroupKt; import com.maddyhome.idea.vim.group.visual.VisualModeHelperKt; import com.maddyhome.idea.vim.handler.EditorActionHandlerBase; import com.maddyhome.idea.vim.handler.Motion; @@ -64,7 +63,7 @@ import com.maddyhome.idea.vim.listener.SelectionVimListenerSuppressor; import com.maddyhome.idea.vim.listener.VimInsertListener; import com.maddyhome.idea.vim.listener.VimListenerSuppressor; -import com.maddyhome.idea.vim.option.BoundListOption; +import com.maddyhome.idea.vim.option.BoundedStringListOption; import com.maddyhome.idea.vim.option.OptionsManager; import com.maddyhome.idea.vim.option.StrictMode; import kotlin.Pair; @@ -447,8 +446,6 @@ private void initInsert(@NotNull Editor editor, @NotNull DataContext context, @N oldOffset = editor.getCaretModel().getOffset(); setInsertEditorState(editor, mode == CommandState.Mode.INSERT); state.pushModes(mode, CommandState.SubMode.NONE); - - VisualGroupKt.updateCaretState(editor); } notifyListeners(editor); @@ -551,8 +548,6 @@ public void processEscape(@NotNull Editor editor, @Nullable DataContext context) CommandState.getInstance(editor).popModes(); exitAllSingleCommandInsertModes(editor); - - VisualGroupKt.updateCaretState(editor); } /** @@ -1830,10 +1825,6 @@ private boolean deleteText(final @NotNull Editor editor, return false; } - public static void resetCaret(@NotNull Editor editor, boolean insert) { - editor.getSettings().setBlockCursor(!insert); - } - /** * Sort range of text with a given comparator * @@ -1894,7 +1885,7 @@ public boolean changeNumberVisualMode(final @NotNull Editor editor, @NotNull TextRange selectedRange, final int count, boolean avalanche) { - BoundListOption nf = OptionsManager.INSTANCE.getNrformats(); + BoundedStringListOption nf = OptionsManager.INSTANCE.getNrformats(); boolean alpha = nf.contains("alpha"); boolean hex = nf.contains("hex"); boolean octal = nf.contains("octal"); @@ -1936,7 +1927,7 @@ private void exitAllSingleCommandInsertModes(@NotNull Editor editor) { private @Nullable List lastStrokes; public boolean changeNumber(final @NotNull Editor editor, @NotNull Caret caret, final int count) { - final BoundListOption nf = OptionsManager.INSTANCE.getNrformats(); + final BoundedStringListOption nf = OptionsManager.INSTANCE.getNrformats(); final boolean alpha = nf.contains("alpha"); final boolean hex = nf.contains("hex"); final boolean octal = nf.contains("octal"); diff --git a/src/com/maddyhome/idea/vim/group/EditorGroup.java b/src/com/maddyhome/idea/vim/group/EditorGroup.java index eb03b42ade..744840d310 100644 --- a/src/com/maddyhome/idea/vim/group/EditorGroup.java +++ b/src/com/maddyhome/idea/vim/group/EditorGroup.java @@ -23,7 +23,10 @@ import com.intellij.openapi.components.PersistentStateComponent; import com.intellij.openapi.components.State; import com.intellij.openapi.components.Storage; -import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.EditorGutter; +import com.intellij.openapi.editor.EditorSettings; +import com.intellij.openapi.editor.LineNumberConverter; import com.intellij.openapi.editor.event.CaretEvent; import com.intellij.openapi.editor.event.CaretListener; import com.intellij.openapi.editor.ex.EditorGutterComponentEx; @@ -31,7 +34,6 @@ import com.intellij.openapi.project.Project; import com.maddyhome.idea.vim.KeyHandler; import com.maddyhome.idea.vim.VimPlugin; -import com.maddyhome.idea.vim.group.visual.VisualGroupKt; import com.maddyhome.idea.vim.helper.*; import com.maddyhome.idea.vim.option.OptionChangeListener; import com.maddyhome.idea.vim.option.OptionsManager; @@ -41,6 +43,8 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import static com.maddyhome.idea.vim.helper.CaretVisualAttributesHelperKt.updateCaretsVisualAttributes; + /** * @author vlan */ @@ -49,7 +53,6 @@ public class EditorGroup implements PersistentStateComponent { private static final boolean REFRAIN_FROM_SCROLLING_VIM_VALUE = true; public static final @NonNls String EDITOR_STORE_ELEMENT = "editor"; - private boolean isBlockCursor = false; private boolean isRefrainFromScrolling = false; private Boolean isKeyRepeat = null; @@ -206,12 +209,7 @@ public void closeEditorSearchSession(@NotNull Editor editor) { } } - public boolean isBarCursorSettings() { - return !EditorSettingsExternalizable.getInstance().isBlockCursor(); - } - public void editorCreated(@NotNull Editor editor) { - isBlockCursor = editor.getSettings().isBlockCursor(); isRefrainFromScrolling = editor.getSettings().isRefrainFromScrolling(); DocumentManager.INSTANCE.addListeners(editor.getDocument()); VimPlugin.getKey().registerRequiredShortcutKeys(editor); @@ -224,7 +222,7 @@ public void editorCreated(@NotNull Editor editor) { VimPlugin.getChange().insertBeforeCursor(editor, EditorDataContext.init(editor, null)); KeyHandler.getInstance().reset(editor); } - VisualGroupKt.resetShape(CommandStateHelper.getMode(editor), editor); + updateCaretsVisualAttributes(editor); editor.getSettings().setRefrainFromScrolling(REFRAIN_FROM_SCROLLING_VIM_VALUE); } @@ -232,7 +230,7 @@ public void editorDeinit(@NotNull Editor editor, boolean isReleased) { deinitLineNumbers(editor, isReleased); UserDataManager.unInitializeEditor(editor); VimPlugin.getKey().unregisterShortcutKeys(editor); - editor.getSettings().setBlockCursor(isBlockCursor); + editor.getSettings().setBlockCursor(EditorSettingsExternalizable.getInstance().isBlockCursor()); editor.getSettings().setRefrainFromScrolling(isRefrainFromScrolling); DocumentManager.INSTANCE.removeListeners(editor.getDocument()); } diff --git a/src/com/maddyhome/idea/vim/group/RegisterGroup.java b/src/com/maddyhome/idea/vim/group/RegisterGroup.java index b645cc7fc9..8c7f10ea2c 100644 --- a/src/com/maddyhome/idea/vim/group/RegisterGroup.java +++ b/src/com/maddyhome/idea/vim/group/RegisterGroup.java @@ -56,8 +56,8 @@ import com.maddyhome.idea.vim.handler.EditorActionHandlerBase; import com.maddyhome.idea.vim.helper.EditorHelper; import com.maddyhome.idea.vim.helper.StringHelper; -import com.maddyhome.idea.vim.option.ListOption; import com.maddyhome.idea.vim.option.OptionsManager; +import com.maddyhome.idea.vim.option.StringListOption; import com.maddyhome.idea.vim.ui.ClipboardHandler; import kotlin.Pair; import org.jdom.Element; @@ -111,7 +111,7 @@ public class RegisterGroup implements PersistentStateComponent { private @Nullable List recordList = null; public RegisterGroup() { - final ListOption clipboardOption = OptionsManager.INSTANCE.getClipboard(); + final StringListOption clipboardOption = OptionsManager.INSTANCE.getClipboard(); clipboardOption.addOptionChangeListenerAndExecute((oldValue, newValue) -> { if (clipboardOption.contains("unnamed")) { defaultRegister = '*'; diff --git a/src/com/maddyhome/idea/vim/group/SearchGroup.java b/src/com/maddyhome/idea/vim/group/SearchGroup.java index ea95c9f513..000eaa9ca2 100644 --- a/src/com/maddyhome/idea/vim/group/SearchGroup.java +++ b/src/com/maddyhome/idea/vim/group/SearchGroup.java @@ -37,9 +37,9 @@ import com.maddyhome.idea.vim.common.TextRange; import com.maddyhome.idea.vim.ex.ranges.LineRange; import com.maddyhome.idea.vim.helper.*; -import com.maddyhome.idea.vim.option.ListOption; import com.maddyhome.idea.vim.option.OptionChangeListener; import com.maddyhome.idea.vim.option.OptionsManager; +import com.maddyhome.idea.vim.option.StringListOption; import com.maddyhome.idea.vim.regexp.CharPointer; import com.maddyhome.idea.vim.regexp.CharacterClasses; import com.maddyhome.idea.vim.regexp.RegExp; @@ -1273,7 +1273,7 @@ public void readData(@NotNull Element element) { } Element show = search.getChild("show-last"); - final ListOption vimInfo = OptionsManager.INSTANCE.getViminfo(); + final StringListOption vimInfo = OptionsManager.INSTANCE.getViminfo(); final boolean disableHighlight = vimInfo.contains("h"); showSearchHighlight = !disableHighlight && Boolean.parseBoolean(show.getText()); if (logger.isDebugEnabled()) { diff --git a/src/com/maddyhome/idea/vim/group/visual/IdeaSelectionControl.kt b/src/com/maddyhome/idea/vim/group/visual/IdeaSelectionControl.kt index d0f8123410..e10c711ce9 100644 --- a/src/com/maddyhome/idea/vim/group/visual/IdeaSelectionControl.kt +++ b/src/com/maddyhome/idea/vim/group/visual/IdeaSelectionControl.kt @@ -88,7 +88,6 @@ object IdeaSelectionControl { } KeyHandler.getInstance().reset(editor) - updateCaretState(editor) logger.debug("${editor.mode} is enabled") } } diff --git a/src/com/maddyhome/idea/vim/group/visual/VisualGroup.kt b/src/com/maddyhome/idea/vim/group/visual/VisualGroup.kt index b19e42a8f7..644d329d13 100644 --- a/src/com/maddyhome/idea/vim/group/visual/VisualGroup.kt +++ b/src/com/maddyhome/idea/vim/group/visual/VisualGroup.kt @@ -19,14 +19,11 @@ package com.maddyhome.idea.vim.group.visual import com.intellij.openapi.editor.Caret -import com.intellij.openapi.editor.CaretVisualAttributes import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.LogicalPosition import com.intellij.openapi.editor.VisualPosition -import com.intellij.openapi.editor.colors.EditorColors import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.command.CommandState -import com.maddyhome.idea.vim.group.ChangeGroup import com.maddyhome.idea.vim.group.MotionGroup import com.maddyhome.idea.vim.helper.EditorHelper import com.maddyhome.idea.vim.helper.fileSize @@ -38,6 +35,7 @@ import com.maddyhome.idea.vim.helper.mode import com.maddyhome.idea.vim.helper.moveToInlayAwareOffset import com.maddyhome.idea.vim.helper.sort import com.maddyhome.idea.vim.helper.subMode +import com.maddyhome.idea.vim.helper.updateCaretsVisualAttributes import com.maddyhome.idea.vim.helper.vimLastColumn import com.maddyhome.idea.vim.helper.vimSelectionStart @@ -136,43 +134,6 @@ val Caret.vimLeadSelectionOffset: Int return caretOffset } -/** - * Update caret's colour according to the current state - * - * Secondary carets became invisible colour in visual block mode - */ -fun updateCaretState(editor: Editor) { - // Update colour - if (editor.inBlockSubMode) { - editor.caretModel.allCarets.forEach { - if (it != editor.caretModel.primaryCaret) { - // Set background color for non-primary carets as selection background color - // to make them invisible - val color = editor.colorsScheme.getColor(EditorColors.SELECTION_BACKGROUND_COLOR) - val visualAttributes = it.visualAttributes - it.visualAttributes = CaretVisualAttributes(color, visualAttributes.weight) - } - } - } else { - editor.caretModel.allCarets.forEach { it.visualAttributes = CaretVisualAttributes.DEFAULT } - } - - // Update shape - editor.mode.resetShape(editor) -} - -fun CommandState.Mode.resetShape(editor: Editor) = when (this) { - CommandState.Mode.COMMAND, CommandState.Mode.VISUAL, CommandState.Mode.REPLACE -> ChangeGroup.resetCaret( - editor, - false - ) - CommandState.Mode.SELECT, CommandState.Mode.INSERT -> ChangeGroup.resetCaret( - editor, - VimPlugin.getEditor().isBarCursorSettings - ) - CommandState.Mode.CMD_LINE, CommandState.Mode.OP_PENDING -> Unit -} - fun charToNativeSelection(editor: Editor, start: Int, end: Int, mode: CommandState.Mode): Pair { val (nativeStart, nativeEnd) = sort(start, end) val lineEnd = EditorHelper.getLineEndForOffset(editor, nativeEnd) @@ -218,7 +179,6 @@ fun blockToNativeSelection( } fun moveCaretOneCharLeftFromSelectionEnd(editor: Editor, predictedMode: CommandState.Mode) { - predictedMode.resetShape(editor) if (predictedMode != CommandState.Mode.VISUAL) { if (!predictedMode.isEndAllowed) { editor.caretModel.allCarets.forEach { caret -> @@ -265,6 +225,9 @@ private fun setVisualSelection(selectionStart: Int, selectionEnd: Int, caret: Ca val lastColumn = editor.caretModel.primaryCaret.vimLastColumn editor.selectionModel.vimSetSystemBlockSelectionSilently(blockStart, blockEnd) + // We've just added secondary carets again, hide them to better emulate block selection + editor.updateCaretsVisualAttributes() + for (aCaret in editor.caretModel.allCarets) { if (!aCaret.isValid) continue val line = aCaret.logicalPosition.line @@ -297,5 +260,4 @@ private fun setVisualSelection(selectionStart: Int, selectionEnd: Int, caret: Ca } else -> Unit } - updateCaretState(editor) } diff --git a/src/com/maddyhome/idea/vim/group/visual/VisualMotionGroup.kt b/src/com/maddyhome/idea/vim/group/visual/VisualMotionGroup.kt index a42550061d..3780ba772d 100644 --- a/src/com/maddyhome/idea/vim/group/visual/VisualMotionGroup.kt +++ b/src/com/maddyhome/idea/vim/group/visual/VisualMotionGroup.kt @@ -208,14 +208,12 @@ class VisualMotionGroup { } else { editor.caretModel.allCarets.forEach { it.vimSelectionStart = it.vimLeadSelectionOffset } } - updateCaretState(editor) return true } fun enterSelectMode(editor: Editor, subMode: CommandState.SubMode): Boolean { editor.commandState.pushModes(CommandState.Mode.SELECT, subMode) editor.vimForEachCaret { it.vimSelectionStart = it.vimLeadSelectionOffset } - updateCaretState(editor) return true } diff --git a/src/com/maddyhome/idea/vim/helper/CaretVisualAttributesHelper.kt b/src/com/maddyhome/idea/vim/helper/CaretVisualAttributesHelper.kt new file mode 100644 index 0000000000..20bf62e0cc --- /dev/null +++ b/src/com/maddyhome/idea/vim/helper/CaretVisualAttributesHelper.kt @@ -0,0 +1,213 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2021 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.maddyhome.idea.vim.helper + +import com.intellij.openapi.application.ApplicationInfo +import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.CaretVisualAttributes +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.colors.EditorColors +import com.intellij.openapi.editor.ex.EditorEx +import com.intellij.openapi.editor.ex.EditorSettingsExternalizable +import com.maddyhome.idea.vim.command.CommandState +import com.maddyhome.idea.vim.option.GuiCursorMode +import com.maddyhome.idea.vim.option.GuiCursorType +import com.maddyhome.idea.vim.option.OptionChangeListener +import com.maddyhome.idea.vim.option.OptionsManager +import java.awt.Color + +/** + * Force the use of the bar caret + * + * Avoid this if possible - we should be using caret shape based on mode. This is only used for IntelliJ specific + * behaviour, e.g. handling selection updates during mouse drag. + */ +fun Caret.forceBarCursor() { + // [VERSION UPDATE] 2021.2+ + // Create + cache CaretVisualAttributes + provider.setBarCursor(editor) +} + +fun Editor.updateCaretsVisualAttributes() { + updatePrimaryCaretVisualAttributes() + updateSecondaryCaretsVisualAttributes() +} + +fun Editor.guicursorMode(): GuiCursorMode { + if (subMode == CommandState.SubMode.REPLACE_CHARACTER) { + // Can be true for NORMAL and VISUAL + return GuiCursorMode.REPLACE + } + + // Note that Vim does not change the caret for SELECT mode and continues to use VISUAL or VISUAL_EXCLUSIVE. IdeaVim + // makes much more use of SELECT than Vim does (e.g. it's the default for idearefactormode) so it makes sense for us + // to more visually distinguish VISUAL and SELECT. So we use INSERT; a selection and the insert caret is intuitively + // the same as SELECT + return when (mode) { + CommandState.Mode.COMMAND -> GuiCursorMode.NORMAL + CommandState.Mode.VISUAL -> GuiCursorMode.VISUAL // TODO: VISUAL_EXCLUSIVE + CommandState.Mode.SELECT -> GuiCursorMode.INSERT + CommandState.Mode.INSERT -> GuiCursorMode.INSERT + CommandState.Mode.OP_PENDING -> GuiCursorMode.OP_PENDING + CommandState.Mode.REPLACE -> GuiCursorMode.REPLACE + // This doesn't handle ci and cr, but we don't care - our CMD_LINE will never call this + CommandState.Mode.CMD_LINE -> GuiCursorMode.CMD_LINE + } +} + +fun Editor.hasBlockOrUnderscoreCaret() = isBlockCursorOverride() || + OptionsManager.guicursor.getAttributes(guicursorMode()).type.let { + it == GuiCursorType.BLOCK || it == GuiCursorType.HOR + } + +/** + * Allow the "use block caret" setting to override guicursor options - if set, we use block caret everywhere, if + * not, we use guicursor options. + * + * Note that we look at the persisted value because for pre-212 at least, we modify the per-editor value. + */ +private fun isBlockCursorOverride() = EditorSettingsExternalizable.getInstance().isBlockCursor + +private fun Editor.updatePrimaryCaretVisualAttributes() { + provider.setPrimaryCaretVisualAttributes(this) +} + +private fun Editor.updateSecondaryCaretsVisualAttributes() { + // IntelliJ simulates visual block with multiple carets with selections. Do our best to hide them + val attributes = provider.getSecondaryCaretVisualAttributes(this, inBlockSubMode) + this.caretModel.allCarets.forEach { + if (it != this.caretModel.primaryCaret) { + it.visualAttributes = attributes + } + } +} + +object GuicursorChangeListener : OptionChangeListener { + override fun valueChange(oldValue: String?, newValue: String?) { + provider.clearCache() + localEditors().forEach { it.updatePrimaryCaretVisualAttributes() } + } +} + +// [VERSION UPDATE] 2021.2+ +// Once the plugin requires 2021.2 as a base version, get rid of all this and just set the attributes directly +private val provider: CaretVisualAttributesProvider by lazy { + if (ApplicationInfo.getInstance().build.baselineVersion >= 212) { + DefaultCaretVisualAttributesProvider() + } else { + LegacyCaretVisualAttributesProvider() + } +} + +private interface CaretVisualAttributesProvider { + fun setPrimaryCaretVisualAttributes(editor: Editor) + fun getSecondaryCaretVisualAttributes(editor: Editor, inBlockSubMode: Boolean): CaretVisualAttributes + fun setBarCursor(editor: Editor) + fun clearCache() {} +} + +private class DefaultCaretVisualAttributesProvider : CaretVisualAttributesProvider { + companion object { + private val HIDDEN = CaretVisualAttributes(null, CaretVisualAttributes.Weight.NORMAL, CaretVisualAttributes.Shape.BAR, 0F) + private val BLOCK = CaretVisualAttributes(null, CaretVisualAttributes.Weight.NORMAL, CaretVisualAttributes.Shape.BLOCK, 1.0F) + private val BAR = CaretVisualAttributes(null, CaretVisualAttributes.Weight.NORMAL, CaretVisualAttributes.Shape.BAR, 0.25F) + } + + private val cache = mutableMapOf() + + private fun getCaretVisualAttributes(editor: Editor): CaretVisualAttributes { + if (isBlockCursorOverride()) { + return BLOCK + } + + val guicursorMode = editor.guicursorMode() + return cache.getOrPut(guicursorMode) { + val attributes = OptionsManager.guicursor.getAttributes(guicursorMode) + val shape = when (attributes.type) { + GuiCursorType.BLOCK -> CaretVisualAttributes.Shape.BLOCK + GuiCursorType.VER -> CaretVisualAttributes.Shape.BAR + GuiCursorType.HOR -> CaretVisualAttributes.Shape.UNDERSCORE + } + val colour: Color? = null // Support highlight group? + CaretVisualAttributes(colour, CaretVisualAttributes.Weight.NORMAL, shape, attributes.thickness / 100F) + } + } + + override fun setPrimaryCaretVisualAttributes(editor: Editor) { + editor.caretModel.primaryCaret.visualAttributes = getCaretVisualAttributes(editor) + + // If the caret is blinking, make sure it's made visible as soon as the mode changes + // See also EditorImpl.updateCaretCursor (called when changing EditorSettings.setBlockCursor) + (editor as? EditorEx)?.setCaretVisible(true) + } + + override fun getSecondaryCaretVisualAttributes(editor: Editor, inBlockSubMode: Boolean): CaretVisualAttributes { + return if (inBlockSubMode) HIDDEN else getCaretVisualAttributes(editor) + } + + override fun setBarCursor(editor: Editor) { + editor.caretModel.primaryCaret.visualAttributes = BAR + } + + override fun clearCache() { + cache.clear() + } +} + +// For 2021.1 and below +private class LegacyCaretVisualAttributesProvider : CaretVisualAttributesProvider { + override fun setPrimaryCaretVisualAttributes(editor: Editor) { + if (isBlockCursorOverride()) { + setBlockCursor(editor, true) + } + else { + // The default for REPLACE is hor20. It makes more sense to map HOR to a block, but REPLACE has traditionally been + // drawn the same as INSERT, as a bar. If the 'guicursor' option is still at default, keep REPLACE a bar + if (OptionsManager.guicursor.isDefault && editor.guicursorMode() == GuiCursorMode.REPLACE) { + setBlockCursor(editor, false) + } + else { + when (OptionsManager.guicursor.getAttributes(editor.guicursorMode()).type) { + GuiCursorType.BLOCK, GuiCursorType.HOR -> setBlockCursor(editor, true) + GuiCursorType.VER -> setBlockCursor(editor, false) + } + } + } + } + + override fun getSecondaryCaretVisualAttributes(editor: Editor, inBlockSubMode: Boolean): CaretVisualAttributes = + if (inBlockSubMode) { + // Do our best to hide the caret + val color = editor.colorsScheme.getColor(EditorColors.SELECTION_BACKGROUND_COLOR) + CaretVisualAttributes(color, CaretVisualAttributes.Weight.NORMAL) + } else { + CaretVisualAttributes.DEFAULT + } + + override fun setBarCursor(editor: Editor) { + setBlockCursor(editor, false) + } + + private fun setBlockCursor(editor: Editor, block: Boolean) { + // This setting really means "use block cursor in insert mode". When set, it swaps the bar/block + insert/overwrite + // relationship - the editor draws a bar for overwrite. To get a block at all times, the block cursor setting needs + // to match the insert mode. + editor.settings.isBlockCursor = if (block) editor.isInsertMode else !editor.isInsertMode + } +} diff --git a/src/com/maddyhome/idea/vim/helper/CommandStateExtensions.kt b/src/com/maddyhome/idea/vim/helper/CommandStateExtensions.kt index e25e83cfdc..740b9b29f2 100644 --- a/src/com/maddyhome/idea/vim/helper/CommandStateExtensions.kt +++ b/src/com/maddyhome/idea/vim/helper/CommandStateExtensions.kt @@ -21,7 +21,6 @@ package com.maddyhome.idea.vim.helper import com.intellij.openapi.editor.Editor -import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.command.CommandState import com.maddyhome.idea.vim.option.OptionsManager @@ -54,23 +53,6 @@ val CommandState.Mode.isEndAllowedIgnoringOnemore: Boolean CommandState.Mode.COMMAND, CommandState.Mode.CMD_LINE, CommandState.Mode.REPLACE, CommandState.Mode.OP_PENDING -> false } -/** - * Should this caret behave like the block caret? - * Keep in mind that in insert mode the caret can have a block shape, but it doesn't behave like the block one - * If you're looking for a shape, check [isBlockCaretShape] - */ -val CommandState.Mode.isBlockCaretBehaviour - get() = when (this) { - CommandState.Mode.VISUAL, CommandState.Mode.COMMAND, CommandState.Mode.OP_PENDING -> true - CommandState.Mode.INSERT, CommandState.Mode.CMD_LINE, CommandState.Mode.REPLACE, CommandState.Mode.SELECT -> false - } - -val CommandState.Mode.isBlockCaretShape - get() = when (this) { - CommandState.Mode.VISUAL, CommandState.Mode.COMMAND, CommandState.Mode.OP_PENDING -> true - CommandState.Mode.INSERT, CommandState.Mode.CMD_LINE, CommandState.Mode.REPLACE, CommandState.Mode.SELECT -> !VimPlugin.getEditor().isBarCursorSettings - } - val CommandState.Mode.hasVisualSelection get() = when (this) { CommandState.Mode.VISUAL, CommandState.Mode.SELECT -> true diff --git a/src/com/maddyhome/idea/vim/helper/InlayHelper.kt b/src/com/maddyhome/idea/vim/helper/InlayHelper.kt index f6f738b25e..cfc26140d7 100644 --- a/src/com/maddyhome/idea/vim/helper/InlayHelper.kt +++ b/src/com/maddyhome/idea/vim/helper/InlayHelper.kt @@ -34,21 +34,20 @@ import com.intellij.openapi.editor.VisualPosition * after the related text and on/before the inlay. If it relates to the following text, it's placed at the visual * column of the text, after the inlay. * - * This behaviour is fine for the bar caret, but for inlays related to preceding text, the block caret will be drawn - * over the inlay, which is a poor experience for Vim users (e.g. hitting `x` in this location will delete the text - * after the inlay, which is at the same offset as the inlay). + * This behaviour is fine for a bar caret, but for inlays related to preceding text, a block or underscore caret will be + * drawn over the inlay, which is a poor experience for Vim users (e.g. hitting `x` in this location will delete the + * text after the inlay, which is at the same offset as the inlay). * - * This method replaces moveToOffset, and makes sure the block caret is not positioned over an inlay. We assume that - * insert/replace and select modes use the bar caret and let the existing moveToOffset position the caret correctly - * between the inlay and its related text. Otherwise, it's a block caret, so we always position it on the visual column - * of the text, after the inlay. + * This method replaces moveToOffset, and makes sure a block or underscore caret is not positioned over an inlay. A bar + * caret uses the existing moveToOffset to position the caret correctly between the inlay and its related text. + * Otherwise, it's a block caret, so we always position it on the visual column of the text, after the inlay. * * It is recommended to call this method even if the caret hasn't been moved. It will handle the situation where the * document has been changed to add an inlay at the caret position, and will move the caret appropriately. */ fun Caret.moveToInlayAwareOffset(offset: Int) { // If the target is inside a fold, call the standard moveToOffset to expand and move - if (editor.foldingModel.isOffsetCollapsed(offset) || isBarCaret(this)) { + if (editor.foldingModel.isOffsetCollapsed(offset) || !editor.hasBlockOrUnderscoreCaret()) { moveToOffset(offset) } else { val newVisualPosition = getVisualPositionForTextAtOffset(editor, offset) @@ -62,11 +61,6 @@ fun Caret.moveToInlayAwareLogicalPosition(pos: LogicalPosition) { moveToInlayAwareOffset(editor.logicalPositionToOffset(pos)) } -private fun isBarCaret(caret: Caret): Boolean { - // TODO: This should ideally be based on caret shape, rather than mode. We can't guarantee that insert means bar - return caret.editor.inInsertMode || caret.editor.inSelectMode -} - private fun getVisualPositionForTextAtOffset(editor: Editor, offset: Int): VisualPosition { var logicalPosition = editor.offsetToLogicalPosition(offset) val e = if (editor is EditorWindow) { @@ -98,3 +92,16 @@ fun Editor.amountOfInlaysBeforeVisualPosition(pos: VisualPosition): Int { } fun VisualPosition.toInlayAwareOffset(caret: Caret): Int = this.column - caret.amountOfInlaysBeforeCaret + +fun Editor.updateCaretsVisualPosition() { + // Caret visual position depends on the current mode, especially with respect to inlays. E.g. if an inlay is + // related to preceding text, the caret is placed between inlay and preceding text in insert mode (usually bar + // caret) but after the inlay in normal mode (block caret). + // By repositioning to the same offset, we will recalculate the expected visual position and put the caret in the + // right location. Don't open a fold if the caret is inside + this.vimForEachCaret { + if (!this.foldingModel.isOffsetCollapsed(it.offset)) { + it.moveToInlayAwareOffset(it.offset) + } + } +} diff --git a/src/com/maddyhome/idea/vim/helper/MessageHelper.kt b/src/com/maddyhome/idea/vim/helper/MessageHelper.kt index 9f86a891ec..b696b2b422 100644 --- a/src/com/maddyhome/idea/vim/helper/MessageHelper.kt +++ b/src/com/maddyhome/idea/vim/helper/MessageHelper.kt @@ -23,9 +23,11 @@ import org.jetbrains.annotations.NonNls import org.jetbrains.annotations.PropertyKey @NonNls -private const val BUNDLE = "messages.IdeaVimBundle" +private const val IDEAVIM_BUNDLE = "messages.IdeaVimBundle" -object MessageHelper : DynamicBundle(BUNDLE) { +object MessageHelper : DynamicBundle(IDEAVIM_BUNDLE) { + + const val BUNDLE = IDEAVIM_BUNDLE @JvmStatic fun message(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any) = getMessage(key, *params) diff --git a/src/com/maddyhome/idea/vim/helper/ModeExtensions.kt b/src/com/maddyhome/idea/vim/helper/ModeExtensions.kt index bf4680d3f8..42c611ce33 100644 --- a/src/com/maddyhome/idea/vim/helper/ModeExtensions.kt +++ b/src/com/maddyhome/idea/vim/helper/ModeExtensions.kt @@ -27,7 +27,6 @@ import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.command.CommandState import com.maddyhome.idea.vim.command.SelectionType import com.maddyhome.idea.vim.common.TextRange -import com.maddyhome.idea.vim.group.visual.updateCaretState import com.maddyhome.idea.vim.listener.SelectionVimListenerSuppressor /** @@ -45,7 +44,6 @@ fun Editor.exitVisualMode() { val selectionType = SelectionType.fromSubMode(this.subMode) SelectionVimListenerSuppressor.lock().use { if (inBlockSubMode) { - this.caretModel.allCarets.forEach { it.visualAttributes = this.caretModel.primaryCaret.visualAttributes } this.caretModel.removeSecondaryCarets() } if (!this.vimKeepingVisualOperatorAction) { @@ -83,7 +81,6 @@ fun Editor.exitSelectMode(adjustCaretPosition: Boolean) { } } } - updateCaretState(this) } fun Editor.exitInsertMode(context: DataContext) { diff --git a/src/com/maddyhome/idea/vim/helper/SearchHelper.java b/src/com/maddyhome/idea/vim/helper/SearchHelper.java index b27361f0ff..36b4dc80b6 100644 --- a/src/com/maddyhome/idea/vim/helper/SearchHelper.java +++ b/src/com/maddyhome/idea/vim/helper/SearchHelper.java @@ -34,8 +34,8 @@ import com.maddyhome.idea.vim.command.CommandState; import com.maddyhome.idea.vim.common.CharacterPosition; import com.maddyhome.idea.vim.common.TextRange; -import com.maddyhome.idea.vim.option.ListOption; import com.maddyhome.idea.vim.option.OptionsManager; +import com.maddyhome.idea.vim.option.StringListOption; import com.maddyhome.idea.vim.regexp.CharPointer; import com.maddyhome.idea.vim.regexp.RegExp; import kotlin.Pair; @@ -2613,14 +2613,14 @@ public static int findMethodEnd(@NotNull Editor editor, @NotNull Caret caret, in private static @NotNull String getPairChars() { if (pairsChars == null) { - ListOption lo = OptionsManager.INSTANCE.getMatchpairs(); + StringListOption lo = OptionsManager.INSTANCE.getMatchpairs(); lo.addOptionChangeListenerAndExecute((oldValue, newValue) -> pairsChars = parseOption(lo)); } return pairsChars; } - private static @NotNull String parseOption(@NotNull ListOption option) { + private static @NotNull String parseOption(@NotNull StringListOption option) { List vals = option.values(); StringBuilder res = new StringBuilder(); for (String s : vals) { diff --git a/src/com/maddyhome/idea/vim/listener/VimListenerManager.kt b/src/com/maddyhome/idea/vim/listener/VimListenerManager.kt index b4fd2f0679..3e2dcdab3f 100644 --- a/src/com/maddyhome/idea/vim/listener/VimListenerManager.kt +++ b/src/com/maddyhome/idea/vim/listener/VimListenerManager.kt @@ -43,7 +43,6 @@ import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.VimTypedActionHandler import com.maddyhome.idea.vim.command.CommandState import com.maddyhome.idea.vim.ex.ExOutputModel -import com.maddyhome.idea.vim.group.ChangeGroup import com.maddyhome.idea.vim.group.EditorGroup import com.maddyhome.idea.vim.group.FileGroup import com.maddyhome.idea.vim.group.MotionGroup @@ -53,9 +52,11 @@ import com.maddyhome.idea.vim.group.visual.VimVisualTimer import com.maddyhome.idea.vim.group.visual.moveCaretOneCharLeftFromSelectionEnd import com.maddyhome.idea.vim.group.visual.vimSetSystemSelectionSilently import com.maddyhome.idea.vim.helper.EditorHelper +import com.maddyhome.idea.vim.helper.GuicursorChangeListener import com.maddyhome.idea.vim.helper.UpdatesChecker import com.maddyhome.idea.vim.helper.exitSelectMode import com.maddyhome.idea.vim.helper.exitVisualMode +import com.maddyhome.idea.vim.helper.forceBarCursor import com.maddyhome.idea.vim.helper.inSelectMode import com.maddyhome.idea.vim.helper.inVisualMode import com.maddyhome.idea.vim.helper.isEndAllowed @@ -63,6 +64,7 @@ import com.maddyhome.idea.vim.helper.isIdeaVimDisabledHere import com.maddyhome.idea.vim.helper.localEditors import com.maddyhome.idea.vim.helper.moveToInlayAwareOffset import com.maddyhome.idea.vim.helper.subMode +import com.maddyhome.idea.vim.helper.updateCaretsVisualAttributes import com.maddyhome.idea.vim.helper.vimLastColumn import com.maddyhome.idea.vim.listener.MouseEventsDataHolder.skipEvents import com.maddyhome.idea.vim.listener.MouseEventsDataHolder.skipNDragEvents @@ -110,6 +112,7 @@ object VimListenerManager { OptionsManager.relativenumber.addOptionChangeListener(EditorGroup.NumberChangeListener.INSTANCE) OptionsManager.scrolloff.addOptionChangeListener(MotionGroup.ScrollOptionsChangeListener.INSTANCE) OptionsManager.showcmd.addOptionChangeListener(ShowCmdOptionChangeListener) + OptionsManager.guicursor.addOptionChangeListener(GuicursorChangeListener) EventFacade.getInstance().addEditorFactoryListener(VimEditorFactoryListener, VimPlugin.getInstance()) } @@ -121,6 +124,7 @@ object VimListenerManager { OptionsManager.relativenumber.removeOptionChangeListener(EditorGroup.NumberChangeListener.INSTANCE) OptionsManager.scrolloff.removeOptionChangeListener(MotionGroup.ScrollOptionsChangeListener.INSTANCE) OptionsManager.showcmd.removeOptionChangeListener(ShowCmdOptionChangeListener) + OptionsManager.guicursor.removeOptionChangeListener(GuicursorChangeListener) EventFacade.getInstance().removeEditorFactoryListener(VimEditorFactoryListener) } @@ -263,28 +267,54 @@ object VimListenerManager { override fun mouseDragged(e: EditorMouseEvent) { if (e.editor.isIdeaVimDisabledHere) return + + val caret = e.editor.caretModel.primaryCaret + clearFirstSelectionEvents(e) - if (mouseDragging && e.editor.caretModel.primaryCaret.hasSelection()) { - ChangeGroup.resetCaret(e.editor, true) + + if (mouseDragging && caret.hasSelection()) { + /** + * We force the bar caret while dragging because it matches IntelliJ's selection model better. + * * Vim's drag selection is based on character bounding boxes. When 'selection' is set to "inclusive" (the + * default), Vim selects a character when the mouse cursor drags the text caret into its bounding box (LTR). + * The character at the text caret is selected and the block caret is drawn to cover the character (the bar + * caret would be between the selection and the last character of the selection, which is weird). See "v" in + * 'guicursor'. When 'selection' is "exclusive", Vim will select a character when the mouse cursor drags the + * text caret out of its bounding box. The character at the text caret is not selected and the bar caret is + * drawn at the start of this character to make it more obvious that it is unselected. See "ve" in + * 'guicursor'. + * * IntelliJ's selection is based on character mid-points. E.g. the caret is moved to the start of offset 2 + * when the second half of offset 1 is clicked, and a character is selected when the mouse is moved from the + * first half to the second half. This means: + * 1) While dragging, the selection is always exclusive - the character at the text caret is not selected. We + * convert to an inclusive selection when the mouse is released, by moving back one character. It makes + * sense to match Vim's bar caret here. + * 2) An exclusive selection should trail behind the mouse cursor, but IntelliJ doesn't, because the selection + * boundaries are mid-points - the text caret can be in front of/to the right of the mouse cursor (LTR). + * Using a block caret would push the block further out passed the selection and the mouse cursor, and + * feels wrong. The bar caret is a better user experience. + * RTL probably introduces other fun issues + * We can implement inclusive/exclusive 'selection' with normal text movement, but unless we can change the way + * selection works while dragging, I don't think we can match Vim's selection behaviour exactly. + */ + caret.forceBarCursor() if (!cutOffFixed && ComponentMouseListener.cutOffEnd) { cutOffFixed = true SelectionVimListenerSuppressor.lock().use { - e.editor.caretModel.primaryCaret.let { caret -> - if (caret.selectionEnd == e.editor.document.getLineEndOffset(caret.logicalPosition.line) - 1 && - caret.leadSelectionOffset == caret.selectionEnd - ) { - // A small but important customization. Because IdeaVim doesn't allow to put the caret on the line end, - // the selection can omit the last character if the selection was started in the middle on the - // last character in line and has a negative direction. - caret.setSelection(caret.selectionStart, caret.selectionEnd + 1) - } - // This is the same correction, but for the newer versions of the IDE: 213+ - if (caret.selectionEnd == e.editor.document.getLineEndOffset(caret.logicalPosition.line) && - caret.selectionEnd == caret.selectionStart + 1 - ) { - caret.setSelection(caret.selectionEnd, caret.selectionEnd) - } + if (caret.selectionEnd == e.editor.document.getLineEndOffset(caret.logicalPosition.line) - 1 && + caret.leadSelectionOffset == caret.selectionEnd + ) { + // A small but important customization. Because IdeaVim doesn't allow to put the caret on the line end, + // the selection can omit the last character if the selection was started in the middle on the + // last character in line and has a negative direction. + caret.setSelection(caret.selectionStart, caret.selectionEnd + 1) + } + // This is the same correction, but for the newer versions of the IDE: 213+ + if (caret.selectionEnd == e.editor.document.getLineEndOffset(caret.logicalPosition.line) && + caret.selectionEnd == caret.selectionStart + 1 + ) { + caret.setSelection(caret.selectionEnd, caret.selectionEnd) } } } @@ -292,20 +322,28 @@ object VimListenerManager { skipNDragEvents -= 1 } - // When user puts the caret, sometimes they perform a small drag. This doesn't affect clear IJ, but - // with IdeaVim it may introduce unwanted selection. Here we remove any selection if "dragging" was happened for - // less than 3 events. + /** + * When user places the caret, sometimes they perform a small drag. This doesn't affect clear IJ, but with IdeaVim + * it may introduce unwanted selection. Here we remove any selection if "dragging" happens for less than 3 events. + * This is because the first click moves the caret passed the end of the line, is then received in + * [ComponentMouseListener] and the caret is moved back to the start of the last character of the line. If there is + * a drag, this translates to a selection of the last character. In this case, remove the selection. + * We force the bar caret simply because it looks better - the block caret is dragged to the end, becomes a less + * intrusive bar caret and snaps back to the last character (and block caret) when the mouse is released. + * TODO: Vim supports selection of the character after the end of line + * (Both with mouse and with v$. IdeaVim treats v$ as an exclusive selection) + */ private fun clearFirstSelectionEvents(e: EditorMouseEvent) { if (skipNDragEvents > 0) { logger.debug("Mouse dragging") VimVisualTimer.swingTimer?.stop() mouseDragging = true + val caret = e.editor.caretModel.primaryCaret if (onLineEnd(caret)) { - // UX protection for case when user performs a small dragging while putting caret on line end SelectionVimListenerSuppressor.lock().use { caret.removeSelection() - ChangeGroup.resetCaret(e.editor, true) + caret.forceBarCursor() } } } @@ -345,7 +383,11 @@ object VimListenerManager { SelectionVimListenerSuppressor.lock().use { val predictedMode = IdeaSelectionControl.predictMode(editor, SelectionSource.MOUSE) IdeaSelectionControl.controlNonVimSelectionChange(editor, SelectionSource.MOUSE) + // TODO: This should only be for 'selection'=inclusive moveCaretOneCharLeftFromSelectionEnd(editor, predictedMode) + + // Reset caret after forceBarShape while dragging + editor.updateCaretsVisualAttributes() caret.vimLastColumn = editor.caretModel.visualPosition.column } @@ -430,8 +472,16 @@ object VimListenerManager { } } else cutOffEnd = false } - // If you double-click on word, the caret jumps to the selection end. - // Here we move the caret because it should be located one character left. + // Double-clicking a word in IntelliJ will select the word and locate the caret at the end of the selection, + // on the following character. When using a bar caret, this is drawn as between the end of selection and the + // following char. With a block caret, this draws the caret "over" the following character. + // In Vim, when 'selection' is "inclusive" (default), double clicking a word will select the last character of + // the word and leave the caret on the last character, drawn as a block caret. We move one character left to + // match this behaviour. + // When 'selection' is exclusive, the caret is placed *after* the end of the word, and is drawn using the 've' + // option of 'guicursor' - as a bar, so it appears to be in between the end of the word and the start of the + // following character. + // TODO: Modify this to support 'selection' set to "exclusive" 2 -> moveCaretOneCharLeftFromSelectionEnd(editor, predictedMode) } } diff --git a/src/com/maddyhome/idea/vim/option/BoundListOption.java b/src/com/maddyhome/idea/vim/option/BoundListOption.java deleted file mode 100644 index bdc9dafb7d..0000000000 --- a/src/com/maddyhome/idea/vim/option/BoundListOption.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform - * Copyright (C) 2003-2021 The IdeaVim authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.maddyhome.idea.vim.option; - -import org.jetbrains.annotations.NonNls; -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - - -public class BoundListOption extends ListOption { - protected final @NotNull List values; - - BoundListOption(@NonNls String name, @NonNls String abbrev, @NonNls String[] dflt, @NonNls String[] values) { - super(name, abbrev, dflt, null); - - this.values = new ArrayList<>(Arrays.asList(values)); - } - - @Override - public boolean set(String val) { - List vals = parseVals(val); - if (vals != null && values.containsAll(vals)) { - set(vals); - } - - return true; - } - - @Override - public boolean append(String val) { - List vals = parseVals(val); - if (vals != null && values.containsAll(vals)) { - append(vals); - } - - return true; - } - - @Override - public boolean prepend(String val) { - List vals = parseVals(val); - if (vals != null && values.containsAll(vals)) { - prepend(vals); - } - - return true; - } - - @Override - public boolean remove(String val) { - List vals = parseVals(val); - if (vals != null && values.containsAll(vals)) { - remove(vals); - } - - return true; - } -} diff --git a/src/com/maddyhome/idea/vim/option/BoundedStringListOption.java b/src/com/maddyhome/idea/vim/option/BoundedStringListOption.java new file mode 100644 index 0000000000..74c6a03f4f --- /dev/null +++ b/src/com/maddyhome/idea/vim/option/BoundedStringListOption.java @@ -0,0 +1,45 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2021 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.maddyhome.idea.vim.option; + +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class BoundedStringListOption extends StringListOption { + protected final @NotNull List allowedValues; + + public BoundedStringListOption(@NonNls String name, + @NonNls String abbrev, + @NonNls String[] defaultValues, + @NonNls String[] allowedValues) { + super(name, abbrev, defaultValues); + + this.allowedValues = new ArrayList<>(Arrays.asList(allowedValues)); + } + + @Override + protected @Nullable String convertToken(@NotNull String token) { + return allowedValues.contains(token) ? token : null; + } +} diff --git a/src/com/maddyhome/idea/vim/option/BoundStringOption.java b/src/com/maddyhome/idea/vim/option/BoundedStringOption.java similarity index 91% rename from src/com/maddyhome/idea/vim/option/BoundStringOption.java rename to src/com/maddyhome/idea/vim/option/BoundedStringOption.java index 3ce5e83b51..34dcce38fa 100644 --- a/src/com/maddyhome/idea/vim/option/BoundStringOption.java +++ b/src/com/maddyhome/idea/vim/option/BoundedStringOption.java @@ -21,10 +21,10 @@ import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; -public class BoundStringOption extends StringOption { +public class BoundedStringOption extends StringOption { protected final String[] values; - BoundStringOption(@NonNls String name, @NonNls String abbrev, @NonNls String dflt, String[] values) { + BoundedStringOption(@NonNls String name, @NonNls String abbrev, @NonNls String dflt, String[] values) { super(name, abbrev, dflt); this.values = values; diff --git a/src/com/maddyhome/idea/vim/option/GuiCursorOption.kt b/src/com/maddyhome/idea/vim/option/GuiCursorOption.kt new file mode 100644 index 0000000000..072cb78de8 --- /dev/null +++ b/src/com/maddyhome/idea/vim/option/GuiCursorOption.kt @@ -0,0 +1,160 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2021 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.maddyhome.idea.vim.option + +import com.maddyhome.idea.vim.ex.ExException +import com.maddyhome.idea.vim.helper.enumSetOf +import java.util.* + +class GuiCursorOption(name: String, abbrev: String, defaultValue: String) : + ListOption(name, abbrev, defaultValue) { + + private val effectiveValues = mutableMapOf() + + override fun convertToken(token: String): GuiCursorEntry { + val split = token.split(':') + if (split.size == 1) { + throw ExException.message("E545", token) + } + if (split.size != 2) { + throw ExException.message("E546", token) + } + val modeList = split[0] + val argumentList = split[1] + + val modes = enumSetOf() + modes.addAll( + modeList.split('-').map { + GuiCursorMode.fromString(it) ?: throw ExException.message("E546", token) + } + ) + + var type = GuiCursorType.BLOCK + var thickness = 0 + var highlightGroup = "" + var lmapHighlightGroup = "" + val blinkModes = mutableListOf() + argumentList.split('-').forEach { + when { + it == "block" -> type = GuiCursorType.BLOCK + it.startsWith("ver") -> { + type = GuiCursorType.VER + thickness = it.slice(3 until it.length).toIntOrNull() ?: throw ExException.message("E548", token) + if (thickness == 0) { + throw ExException.message("E549", token) + } + } + it.startsWith("hor") -> { + type = GuiCursorType.HOR + thickness = it.slice(3 until it.length).toIntOrNull() ?: throw ExException.message("E548", token) + if (thickness == 0) { + throw ExException.message("E549", token) + } + } + it.startsWith("blink") -> { + // We don't do anything with blink... + blinkModes.add(it) + } + it.contains('/') -> { + val i = it.indexOf('/') + highlightGroup = it.slice(0 until i) + lmapHighlightGroup = it.slice(i + 1 until it.length) + } + else -> highlightGroup = it + } + } + + return GuiCursorEntry(token, modes, GuiCursorAttributes(type, thickness, highlightGroup, lmapHighlightGroup, blinkModes)) + } + + override fun onChanged(oldValue: String?, newValue: String?) { + effectiveValues.clear() + super.onChanged(oldValue, newValue) + } + + fun getAttributes(mode: GuiCursorMode): GuiCursorAttributes { + return effectiveValues.computeIfAbsent(mode) { + var type = GuiCursorType.BLOCK + var thickness = 0 + var highlightGroup = "" + var lmapHighlightGroup = "" + var blinkModes = emptyList() + values().forEach { state -> + if (state.modes.contains(mode) || state.modes.contains(GuiCursorMode.ALL)) { + type = state.attributes.type + thickness = state.attributes.thickness + if (state.attributes.highlightGroup.isNotEmpty()) { + highlightGroup = state.attributes.highlightGroup + } + if (state.attributes.lmapHighlightGroup.isNotEmpty()) { + lmapHighlightGroup = state.attributes.lmapHighlightGroup + } + if (state.attributes.blinkModes.isNotEmpty()) { + blinkModes = state.attributes.blinkModes + } + } + } + GuiCursorAttributes(type, thickness, highlightGroup, lmapHighlightGroup, blinkModes) + } + } +} + +enum class GuiCursorMode(val token: String) { + NORMAL("n"), + VISUAL("v"), + VISUAL_EXCLUSIVE("ve"), + OP_PENDING("o"), + INSERT("i"), + REPLACE("r"), + CMD_LINE("c"), + CMD_LINE_INSERT("ci"), + CMD_LINE_REPLACE("cr"), + SHOW_MATCH("sm"), + ALL("a"); + + override fun toString() = token + + companion object { + fun fromString(s: String) = values().firstOrNull { it.token == s } + } +} + +enum class GuiCursorType(val token: String) { + BLOCK("block"), + VER("ver"), + HOR("hor") +} + +class GuiCursorEntry( + private val originalString: String, + val modes: EnumSet, + val attributes: GuiCursorAttributes +) { + override fun toString(): String { + // We need to match the original string for output and remove purposes + return originalString + } +} + +data class GuiCursorAttributes( + val type: GuiCursorType, + val thickness: Int, + val highlightGroup: String, + val lmapHighlightGroup: String, + val blinkModes: List +) diff --git a/src/com/maddyhome/idea/vim/option/KeywordOption.java b/src/com/maddyhome/idea/vim/option/KeywordOption.java index 71726d2c34..0813c4c477 100644 --- a/src/com/maddyhome/idea/vim/option/KeywordOption.java +++ b/src/com/maddyhome/idea/vim/option/KeywordOption.java @@ -30,17 +30,19 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; -public final class KeywordOption extends ListOption { +public final class KeywordOption extends StringListOption { @NonNls private static final String allLettersRegex = "\\p{L}"; + @NonNls private static final String PATTERN = + "(\\^?(([^0-9^]|[0-9]{1,3})-([^0-9]|[0-9]{1,3})|([^0-9^]|[0-9]{1,3})),)*\\^?(([^0-9^]|[0-9]{1,3})-([^0-9]|[0-9]{1,3})|([^0-9]|[0-9]{1,3})),?$"; + private final @NotNull Pattern validationPattern; // KeywordSpecs are the option values in reverse order private @NotNull List keywordSpecs = new ArrayList<>(); public KeywordOption(@NotNull @NonNls String name, @NotNull @NonNls String abbrev, @NotNull String[] defaultValue) { - super(name, abbrev, defaultValue, - "(\\^?(([^0-9^]|[0-9]{1,3})-([^0-9]|[0-9]{1,3})|([^0-9^]|[0-9]{1,3})),)*\\^?(([^0-9^]|[0-9]{1,3})-([^0-9]|[0-9]{1,3})|([^0-9]|[0-9]{1,3})),?$"); - validationPattern = Pattern.compile(pattern); + super(name, abbrev, defaultValue, PATTERN); + validationPattern = Pattern.compile(PATTERN); initialSet(defaultValue); } @@ -54,7 +56,7 @@ public boolean append(@NotNull String val) { } this.value.addAll(vals); keywordSpecs.addAll(0, specs); - fireOptionChangeEvent(oldValue, getValue()); + onChanged(oldValue, getValue()); return true; } @@ -68,7 +70,7 @@ public boolean prepend(@NotNull String val) { } value.addAll(0, vals); keywordSpecs.addAll(specs); - fireOptionChangeEvent(oldValue, getValue()); + onChanged(oldValue, getValue()); return true; } @@ -83,7 +85,7 @@ public boolean remove(@NotNull String val) { } value.removeAll(vals); keywordSpecs.removeAll(specs); - fireOptionChangeEvent(oldValue, getValue()); + onChanged(oldValue, getValue()); return true; } @@ -93,7 +95,7 @@ private void initialSet(String[] values) { final List specs = valsToReversedSpecs(vals); value = vals; keywordSpecs = specs; - fireOptionChangeEvent(oldValue, getValue()); + onChanged(oldValue, getValue()); } @Override @@ -106,14 +108,14 @@ public boolean set(@NotNull String val) { } value = vals; keywordSpecs = specs; - fireOptionChangeEvent(oldValue, getValue()); + onChanged(oldValue, getValue()); return true; } @Override public void resetDefault() { - if (!dflt.equals(value)) { - value = dflt; + if (!defaultValues.equals(value)) { + value = defaultValues; set(getValue()); } } diff --git a/src/com/maddyhome/idea/vim/option/ListOption.java b/src/com/maddyhome/idea/vim/option/ListOption.java index 586c3d538f..1c2fe99915 100644 --- a/src/com/maddyhome/idea/vim/option/ListOption.java +++ b/src/com/maddyhome/idea/vim/option/ListOption.java @@ -18,7 +18,8 @@ package com.maddyhome.idea.vim.option; -import com.intellij.util.ArrayUtil; +import com.intellij.openapi.diagnostic.Logger; +import com.maddyhome.idea.vim.ex.ExException; import com.maddyhome.idea.vim.helper.VimNlsSafe; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; @@ -32,8 +33,33 @@ /** * This is an option that accepts an arbitrary list of values */ -public class ListOption extends TextOption { - public static final @NotNull ListOption empty = new ListOption("", "", ArrayUtil.EMPTY_STRING_ARRAY, ""); +public abstract class ListOption extends TextOption { + private static final Logger logger = Logger.getInstance(ListOption.class.getName()); + + protected final @NotNull List defaultValues; + protected @NotNull List value; + + /** + * Creates the option + * + * @param name The name of the option + * @param abbrev The short name + * @param defaultValues The option's default values + */ + public ListOption(String name, String abbrev, @VimNlsSafe T[] defaultValues) { + super(name, abbrev); + + this.defaultValues = new ArrayList<>(Arrays.asList(defaultValues)); + this.value = new ArrayList<>(this.defaultValues); + } + + public ListOption(String name, String abbrev, String defaultValue) throws ExException { + super(name, abbrev); + + final List defaultValues = parseVals(defaultValue); + this.defaultValues = defaultValues != null ? new ArrayList<>(defaultValues) : new ArrayList<>(); + this.value = new ArrayList<>(this.defaultValues); + } /** * Gets the value of the option as a comma separated list of values @@ -44,7 +70,7 @@ public class ListOption extends TextOption { public @NotNull String getValue() { StringBuilder res = new StringBuilder(); int cnt = 0; - for (String s : value) { + for (T s : value) { if (cnt > 0) { res.append(","); } @@ -61,7 +87,7 @@ public class ListOption extends TextOption { * * @return The option's values */ - public @NotNull List values() { + public @NotNull List values() { return value; } @@ -72,7 +98,14 @@ public class ListOption extends TextOption { * @return True if all the supplied values are set in this option, false if not */ public boolean contains(@NonNls String val) { - final List vals = parseVals(val); + final List vals; + try { + vals = parseVals(val); + } + catch (ExException e) { + logger.warn("Error parsing option", e); + return false; + } return vals != null && value.containsAll(vals); } @@ -84,7 +117,7 @@ public boolean contains(@NonNls String val) { * @return True if all the supplied values were correct, false if not */ @Override - public boolean set(String val) { + public boolean set(String val) throws ExException { return set(parseVals(val)); } @@ -96,7 +129,7 @@ public boolean set(String val) { * @return True if all the supplied values were correct, false if not */ @Override - public boolean append(String val) { + public boolean append(String val) throws ExException { return append(parseVals(val)); } @@ -108,7 +141,7 @@ public boolean append(String val) { * @return True if all the supplied values were correct, false if not */ @Override - public boolean prepend(String val) { + public boolean prepend(String val) throws ExException { return prepend(parseVals(val)); } @@ -120,23 +153,23 @@ public boolean prepend(String val) { * @return True if all the supplied values were correct, false if not */ @Override - public boolean remove(String val) { + public boolean remove(String val) throws ExException { return remove(parseVals(val)); } - protected boolean set(@Nullable List vals) { + protected boolean set(@Nullable List vals) { if (vals == null) { return false; } String oldValue = getValue(); this.value = vals; - fireOptionChangeEvent(oldValue, getValue()); + onChanged(oldValue, getValue()); return true; } - protected boolean append(@Nullable List vals) { + protected boolean append(@Nullable List vals) { if (vals == null) { return false; } @@ -144,12 +177,12 @@ protected boolean append(@Nullable List vals) { String oldValue = getValue(); value.removeAll(vals); value.addAll(vals); - fireOptionChangeEvent(oldValue, getValue()); + onChanged(oldValue, getValue()); return true; } - protected boolean prepend(@Nullable List vals) { + protected boolean prepend(@Nullable List vals) { if (vals == null) { return false; } @@ -157,39 +190,23 @@ protected boolean prepend(@Nullable List vals) { String oldValue = getValue(); value.removeAll(vals); value.addAll(0, vals); - fireOptionChangeEvent(oldValue, getValue()); + onChanged(oldValue, getValue()); return true; } - protected boolean remove(@Nullable List vals) { + protected boolean remove(@Nullable List vals) { if (vals == null) { return false; } String oldValue = getValue(); value.removeAll(vals); - fireOptionChangeEvent(oldValue, getValue()); + onChanged(oldValue, getValue()); return true; } - /** - * Creates the option - * - * @param name The name of the option - * @param abbrev The short name - * @param dflt The option's default values - * @param pattern A regular expression that is used to validate new values. null if no check needed - */ - public ListOption(@VimNlsSafe String name, @VimNlsSafe String abbrev, @VimNlsSafe String[] dflt, @VimNlsSafe String pattern) { - super(name, abbrev); - - this.dflt = new ArrayList<>(Arrays.asList(dflt)); - this.value = new ArrayList<>(this.dflt); - this.pattern = pattern; - } - /** * Checks to see if the current value of the option matches the default value * @@ -197,16 +214,17 @@ public ListOption(@VimNlsSafe String name, @VimNlsSafe String abbrev, @VimNlsSaf */ @Override public boolean isDefault() { - return dflt.equals(value); + return defaultValues.equals(value); } - protected @Nullable List parseVals(String val) { - List res = new ArrayList<>(); + protected @Nullable List parseVals(String val) throws ExException { + List res = new ArrayList<>(); StringTokenizer tokenizer = new StringTokenizer(val, ","); while (tokenizer.hasMoreTokens()) { String token = tokenizer.nextToken().trim(); - if (pattern == null || token.matches(pattern)) { - res.add(token); + T item = convertToken(token); + if (item != null) { + res.add(item); } else { return null; @@ -216,6 +234,8 @@ public boolean isDefault() { return res; } + protected abstract @Nullable T convertToken(@NotNull String token) throws ExException; + /** * Gets the string representation appropriate for output to :set all * @@ -225,19 +245,15 @@ public boolean isDefault() { return " " + getName() + "=" + getValue(); } - protected final @NotNull List dflt; - protected @NotNull List value; - protected final String pattern; - /** * Resets the option to its default value */ @Override public void resetDefault() { - if (!dflt.equals(value)) { + if (!defaultValues.equals(value)) { String oldValue = getValue(); - value = new ArrayList<>(dflt); - fireOptionChangeEvent(oldValue, getValue()); + value = new ArrayList<>(defaultValues); + onChanged(oldValue, getValue()); } } } diff --git a/src/com/maddyhome/idea/vim/option/NumberOption.java b/src/com/maddyhome/idea/vim/option/NumberOption.java index 62abaa3564..024df90f05 100644 --- a/src/com/maddyhome/idea/vim/option/NumberOption.java +++ b/src/com/maddyhome/idea/vim/option/NumberOption.java @@ -110,7 +110,7 @@ public boolean set(String val) { String oldValue = getValue(); this.value = num; - fireOptionChangeEvent(oldValue, getValue()); + onChanged(oldValue, getValue()); return true; } @@ -137,7 +137,7 @@ public boolean append(String val) { if (inRange(value + num)) { String oldValue = getValue(); value += num; - fireOptionChangeEvent(oldValue, getValue()); + onChanged(oldValue, getValue()); return true; } @@ -164,7 +164,7 @@ public boolean prepend(String val) { if (inRange(value * num)) { String oldValue = getValue(); value *= num; - fireOptionChangeEvent(oldValue, getValue()); + onChanged(oldValue, getValue()); return true; } @@ -191,7 +191,7 @@ public boolean remove(String val) { if (inRange(value - num)) { String oldValue = getValue(); value -= num; - fireOptionChangeEvent(oldValue, getValue()); + onChanged(oldValue, getValue()); return true; } @@ -217,7 +217,7 @@ public void resetDefault() { if (dflt != value) { String oldValue = getValue(); value = dflt; - fireOptionChangeEvent(oldValue, getValue()); + onChanged(oldValue, getValue()); } } diff --git a/src/com/maddyhome/idea/vim/option/Option.java b/src/com/maddyhome/idea/vim/option/Option.java index 24d742a230..361feaafd4 100644 --- a/src/com/maddyhome/idea/vim/option/Option.java +++ b/src/com/maddyhome/idea/vim/option/Option.java @@ -61,7 +61,7 @@ public void addOptionChangeListener(OptionChangeListener listener) { public void addOptionChangeListenerAndExecute(OptionChangeListener listener) { addOptionChangeListener(listener); T value = getValue(); - fireOptionChangeEvent(value, value); + onChanged(value, value); } /** @@ -107,7 +107,11 @@ public String getAbbreviation() { * Lets all listeners know that the value has changed. Subclasses are responsible for calling this when their * value changes. */ - protected void fireOptionChangeEvent(T oldValue, T newValue) { + protected void onChanged(T oldValue, T newValue) { + fireOptionChangeEvent(oldValue, newValue); + } + + private void fireOptionChangeEvent(T oldValue, T newValue) { for (OptionChangeListener listener : listeners) { listener.valueChange(oldValue, newValue); } diff --git a/src/com/maddyhome/idea/vim/option/OptionsManager.kt b/src/com/maddyhome/idea/vim/option/OptionsManager.kt index 1369b4495d..e8bd93ffc5 100644 --- a/src/com/maddyhome/idea/vim/option/OptionsManager.kt +++ b/src/com/maddyhome/idea/vim/option/OptionsManager.kt @@ -34,8 +34,8 @@ import com.maddyhome.idea.vim.ex.ExOutputModel import com.maddyhome.idea.vim.helper.EditorHelper import com.maddyhome.idea.vim.helper.MessageHelper import com.maddyhome.idea.vim.helper.Msg +import com.maddyhome.idea.vim.helper.hasBlockOrUnderscoreCaret import com.maddyhome.idea.vim.helper.hasVisualSelection -import com.maddyhome.idea.vim.helper.isBlockCaretBehaviour import com.maddyhome.idea.vim.helper.mode import com.maddyhome.idea.vim.helper.subMode import com.maddyhome.idea.vim.listener.SelectionVimListenerSuppressor @@ -52,26 +52,27 @@ object OptionsManager { private val options: MutableMap> = mutableMapOf() private val abbrevs: MutableMap> = mutableMapOf() - val clipboard = addOption(ListOption(ClipboardOptionsData.name, ClipboardOptionsData.abbr, arrayOf(ClipboardOptionsData.ideaput, "autoselect,exclude:cons\\|linux"), null)) + val clipboard = addOption(StringListOption(ClipboardOptionsData.name, ClipboardOptionsData.abbr, arrayOf(ClipboardOptionsData.ideaput, "autoselect,exclude:cons\\|linux"))) val digraph = addOption(ToggleOption("digraph", "dg", false)) val gdefault = addOption(ToggleOption("gdefault", "gd", false)) + val guicursor = addOption(GuiCursorOptionData.option) val history = addOption(NumberOption("history", "hi", 50, 1, Int.MAX_VALUE)) val hlsearch = addOption(ToggleOption("hlsearch", "hls", false)) - val ideamarks = addOption(IdeaMarkskOptionsData.option) + val ideamarks = addOption(IdeaMarksOptionsData.option) val ignorecase = addOption(ToggleOption(IgnoreCaseOptionsData.name, IgnoreCaseOptionsData.abbr, false)) val incsearch = addOption(ToggleOption("incsearch", "is", false)) val iskeyword = addOption(KeywordOption("iskeyword", "isk", arrayOf("@", "48-57", "_"))) val keymodel = addOption(KeyModelOptionData.option) - val lookupKeys = addOption(ListOption(LookupKeysData.name, LookupKeysData.name, LookupKeysData.defaultValues, null)) - val matchpairs = addOption(ListOption("matchpairs", "mps", arrayOf("(:)", "{:}", "[:]"), ".:.")) + val lookupKeys = addOption(StringListOption(LookupKeysData.name, LookupKeysData.name, LookupKeysData.defaultValues)) + val matchpairs = addOption(StringListOption("matchpairs", "mps", arrayOf("(:)", "{:}", "[:]"), ".:.")) val more = addOption(ToggleOption("more", "more", true)) - val nrformats = addOption(BoundListOption("nrformats", "nf", arrayOf("hex"), arrayOf("octal", "hex", "alpha"))) // Octal is disabled as in neovim + val nrformats = addOption(BoundedStringListOption("nrformats", "nf", arrayOf("hex"), arrayOf("octal", "hex", "alpha"))) // Octal is disabled as in neovim val number = addOption(ToggleOption("number", "nu", false)) val relativenumber = addOption(ToggleOption("relativenumber", "rnu", false)) val scroll = addOption(NumberOption("scroll", "scr", 0)) val scrolljump = addOption(NumberOption(ScrollJumpData.name, "sj", 1, -100, Integer.MAX_VALUE)) val scrolloff = addOption(NumberOption(ScrollOffData.name, "so", 0)) - val selection = addOption(BoundStringOption("selection", "sel", "inclusive", arrayOf("old", "inclusive", "exclusive"))) + val selection = addOption(BoundedStringOption("selection", "sel", "inclusive", arrayOf("old", "inclusive", "exclusive"))) val selectmode = addOption(SelectModeOptionData.option) val shell = addOption(ShellOptionData.option) val shellcmdflag = addOption(ShellCmdFlagOptionData.option) @@ -87,16 +88,16 @@ object OptionsManager { val timeout = addOption(ToggleOption("timeout", "to", true)) val timeoutlen = addOption(NumberOption("timeoutlen", "tm", 1000, -1, Int.MAX_VALUE)) val undolevels = addOption(NumberOption("undolevels", "ul", 1000, -1, Int.MAX_VALUE)) - val viminfo = addOption(ListOption("viminfo", "vi", arrayOf("'100", "<50", "s10", "h"), null)) - val virtualedit = addOption(BoundStringOption(VirtualEditData.name, "ve", "", VirtualEditData.allValues)) + val viminfo = addOption(StringListOption("viminfo", "vi", arrayOf("'100", "<50", "s10", "h"))) + val virtualedit = addOption(BoundedStringOption(VirtualEditData.name, "ve", "", VirtualEditData.allValues)) val visualbell = addOption(ToggleOption("visualbell", "vb", false)) val wrapscan = addOption(ToggleOption("wrapscan", "ws", true)) val visualEnterDelay = addOption(NumberOption("visualdelay", "visualdelay", 100, 0, Int.MAX_VALUE)) - val idearefactormode = addOption(BoundStringOption(IdeaRefactorMode.name, IdeaRefactorMode.name, IdeaRefactorMode.select, IdeaRefactorMode.availableValues)) - val ideastatusicon = addOption(BoundStringOption(IdeaStatusIcon.name, IdeaStatusIcon.name, IdeaStatusIcon.enabled, IdeaStatusIcon.allValues)) + val idearefactormode = addOption(BoundedStringOption(IdeaRefactorMode.name, IdeaRefactorMode.name, IdeaRefactorMode.select, IdeaRefactorMode.availableValues)) + val ideastatusicon = addOption(BoundedStringOption(IdeaStatusIcon.name, IdeaStatusIcon.name, IdeaStatusIcon.enabled, IdeaStatusIcon.allValues)) val ideastrictmode = addOption(ToggleOption("ideastrictmode", "ideastrictmode", false)) - val ideawrite = addOption(BoundStringOption("ideawrite", "ideawrite", IdeaWriteData.all, IdeaWriteData.allValues)) - val ideavimsupport = addOption(BoundListOption("ideavimsupport", "ideavimsupport", arrayOf("dialog"), arrayOf("dialog", "singleline", "dialoglegacy"))) + val ideawrite = addOption(BoundedStringOption("ideawrite", "ideawrite", IdeaWriteData.all, IdeaWriteData.allValues)) + val ideavimsupport = addOption(BoundedStringListOption("ideavimsupport", "ideavimsupport", arrayOf("dialog"), arrayOf("dialog", "singleline", "dialoglegacy"))) val ide = addOption(StringOption("ide", "ide", ApplicationNamesInfo.getInstance().fullProductNameWithEdition)) // TODO The default value if 1000, but we can't increase it because of terrible performance of our mappings @@ -118,7 +119,7 @@ object OptionsManager { return option is ToggleOption && option.getValue() } - fun getListOption(name: String): ListOption? = getOption(name) as? ListOption + fun getStringListOption(name: String): StringListOption? = getOption(name) as? StringListOption fun resetAllOptions() = options.values.forEach { it.resetDefault() } @@ -274,14 +275,20 @@ object OptionsManager { if (opt != null) { // If not a boolean if (opt is TextOption) { - val res = when (op) { - '+' -> opt.append(value) - '-' -> opt.remove(value) - '^' -> opt.prepend(value) - else -> opt.set(value) - } - if (!res) { - error = Msg.e_invarg + try { + val res = when (op) { + '+' -> opt.append(value) + '-' -> opt.remove(value) + '^' -> opt.prepend(value) + else -> opt.set(value) + } + if (!res) { + error = Msg.e_invarg + } + } catch (e: ExException) { + // Retrieve the message code, if possible and throw again with the entire set arg string. This assumes + // that any thrown exception has a message code that accepts a single parameter + error = e.code ?: Msg.e_invarg } } else { error = Msg.e_invarg @@ -402,7 +409,7 @@ object KeyModelOptionData { val options = arrayOf(startsel, stopsel, stopselect, stopvisual, continueselect, continuevisual) val default = arrayOf(continueselect, stopselect) - val option = BoundListOption(name, abbr, default, options) + val option = BoundedStringListOption(name, abbr, default, options) } @NonNls @@ -419,7 +426,7 @@ object SelectModeOptionData { @Suppress("DEPRECATION") val options = arrayOf(mouse, key, cmd, ideaselection) val default = emptyArray() - val option = BoundListOption(name, abbr, default, options) + val option = BoundedStringListOption(name, abbr, default, options) fun ideaselectionEnabled(): Boolean { return ideaselection in OptionsManager.selectmode @@ -456,6 +463,21 @@ object ClipboardOptionsData { } } +@Suppress("SpellCheckingInspection") +@NonNls +object GuiCursorOptionData { + const val name = "guicursor" + private const val abbr = "gcr" + const val defaultValue = "n-v-c:block-Cursor/lCursor," + + "ve:ver35-Cursor," + + "o:hor50-Cursor," + + "i-ci:ver25-Cursor/lCursor," + + "r-cr:hor20-Cursor/lCursor," + + "sm:block-Cursor-blinkwait175-blinkoff150-blinkon175" + + val option = GuiCursorOption(name, abbr, defaultValue) +} + @NonNls object IdeaJoinOptionsData { const val name = "ideajoin" @@ -465,7 +487,7 @@ object IdeaJoinOptionsData { } @NonNls -object IdeaMarkskOptionsData { +object IdeaMarksOptionsData { const val name = "ideamarks" private const val defaultValue = true @@ -512,7 +534,7 @@ object IdeaRefactorMode { } } - if (editor.mode.isBlockCaretBehaviour) { + if (editor.hasBlockOrUnderscoreCaret()) { TemplateManagerImpl.getTemplateState(editor)?.currentVariableRange?.let { segmentRange -> if (!segmentRange.isEmpty && segmentRange.endOffset == editor.caretModel.offset && editor.caretModel.offset != 0) { editor.caretModel.moveToOffset(editor.caretModel.offset - 1) diff --git a/src/com/maddyhome/idea/vim/option/StringListOption.kt b/src/com/maddyhome/idea/vim/option/StringListOption.kt new file mode 100644 index 0000000000..557f51dee6 --- /dev/null +++ b/src/com/maddyhome/idea/vim/option/StringListOption.kt @@ -0,0 +1,53 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2021 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.maddyhome.idea.vim.option + +import com.maddyhome.idea.vim.helper.VimNlsSafe +import java.util.regex.Pattern + +/** + * Creates the option + * + * @param name The name of the option + * @param abbrev The short name + * @param defaultValues The option's default values + * @param pattern A regular expression that is used to validate new values. null if no check needed + */ +open class StringListOption @JvmOverloads constructor( + @VimNlsSafe name: String, + @VimNlsSafe abbrev: String, + @VimNlsSafe defaultValues: Array, + @VimNlsSafe protected val pattern: String? = null +) : + ListOption(name, abbrev, defaultValues) { + + companion object { + val empty = StringListOption("", "", emptyArray()) + } + + override fun convertToken(token: String): String? { + if (pattern == null) { + return token + } + if (Pattern.matches(pattern, token)) { + return token + } + return null + } +} diff --git a/src/com/maddyhome/idea/vim/option/StringOption.java b/src/com/maddyhome/idea/vim/option/StringOption.java index 947acd7af4..998cb457b4 100644 --- a/src/com/maddyhome/idea/vim/option/StringOption.java +++ b/src/com/maddyhome/idea/vim/option/StringOption.java @@ -61,7 +61,7 @@ public String getValue() { public boolean set(String val) { String oldValue = getValue(); value = val; - fireOptionChangeEvent(oldValue, getValue()); + onChanged(oldValue, getValue()); return true; } @@ -76,7 +76,7 @@ public boolean set(String val) { public boolean append(String val) { String oldValue = getValue(); value += val; - fireOptionChangeEvent(oldValue, getValue()); + onChanged(oldValue, getValue()); return true; } @@ -91,7 +91,7 @@ public boolean append(String val) { public boolean prepend(String val) { String oldValue = getValue(); value = val + value; - fireOptionChangeEvent(oldValue, getValue()); + onChanged(oldValue, getValue()); return true; } @@ -108,7 +108,7 @@ public boolean remove(@NotNull String val) { if (pos != -1) { String oldValue = getValue(); value = value.substring(0, pos) + value.substring(pos + val.length()); - fireOptionChangeEvent(oldValue, getValue()); + onChanged(oldValue, getValue()); return true; } @@ -134,7 +134,7 @@ public void resetDefault() { if (!getDefaultValue().equals(value)) { String oldValue = getValue(); value = getDefaultValue(); - fireOptionChangeEvent(oldValue, getValue()); + onChanged(oldValue, getValue()); } } diff --git a/src/com/maddyhome/idea/vim/option/TextOption.java b/src/com/maddyhome/idea/vim/option/TextOption.java index d03e1a1878..213cf73818 100644 --- a/src/com/maddyhome/idea/vim/option/TextOption.java +++ b/src/com/maddyhome/idea/vim/option/TextOption.java @@ -18,16 +18,18 @@ package com.maddyhome.idea.vim.option; +import com.maddyhome.idea.vim.ex.ExException; + public abstract class TextOption extends Option { TextOption(String name, String abbrev) { super(name, abbrev); } - public abstract boolean set(String val); + public abstract boolean set(String val) throws ExException; - public abstract boolean append(String val); + public abstract boolean append(String val) throws ExException; - public abstract boolean prepend(String val); + public abstract boolean prepend(String val) throws ExException; - public abstract boolean remove(String val); + public abstract boolean remove(String val) throws ExException; } diff --git a/src/com/maddyhome/idea/vim/option/ToggleOption.java b/src/com/maddyhome/idea/vim/option/ToggleOption.java index 6436a0087a..d71111c4e3 100644 --- a/src/com/maddyhome/idea/vim/option/ToggleOption.java +++ b/src/com/maddyhome/idea/vim/option/ToggleOption.java @@ -84,7 +84,7 @@ private void update(boolean val) { boolean old = value; value = val; if (val != old) { - fireOptionChangeEvent(old, val); + onChanged(old, val); } } diff --git a/src/com/maddyhome/idea/vim/ui/ex/ExDocument.java b/src/com/maddyhome/idea/vim/ui/ex/ExDocument.java index 9321d2c175..e0da7c003c 100644 --- a/src/com/maddyhome/idea/vim/ui/ex/ExDocument.java +++ b/src/com/maddyhome/idea/vim/ui/ex/ExDocument.java @@ -82,20 +82,6 @@ public void insertString(int offs, @NotNull String str, AttributeSet a) throws B } } - public char getCharacter(int offset) { - // If we're a proportional font, 'o' is a good char to use. If we're fixed width, it's still a good char to use - if (offset >= getLength()) return 'o'; - - try { - final Segment segment = new Segment(); - getContent().getChars(offset, 1, segment); - return segment.charAt(0); - } - catch (BadLocationException e) { - return 'o'; - } - } - // Mac apps will show a highlight for text being composed as part of an input method or dead keys (e.g. N will // combine a ~ and n to produce ñ on a UK/US keyboard, and `, ' or ~ will combine to add accents on US International // keyboards. Java only adds a highlight when the Input Method tells it to, so normal text fields don't get the diff --git a/src/com/maddyhome/idea/vim/ui/ex/ExShortcutKeyAction.kt b/src/com/maddyhome/idea/vim/ui/ex/ExShortcutKeyAction.kt index 2804ac4867..1dcd98877c 100644 --- a/src/com/maddyhome/idea/vim/ui/ex/ExShortcutKeyAction.kt +++ b/src/com/maddyhome/idea/vim/ui/ex/ExShortcutKeyAction.kt @@ -18,9 +18,9 @@ package com.maddyhome.idea.vim.ui.ex -import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.KeyboardShortcut +import com.intellij.openapi.project.DumbAwareAction import com.maddyhome.idea.vim.KeyHandler import com.maddyhome.idea.vim.helper.EditorDataContext import java.awt.event.KeyEvent @@ -38,7 +38,7 @@ import javax.swing.KeyStroke * component has focus. It registers all shortcuts used by the Swing actions and forwards them directly to the key * handler. */ -class ExShortcutKeyAction(private val exEntryPanel: ExEntryPanel) : AnAction() { +class ExShortcutKeyAction(private val exEntryPanel: ExEntryPanel) : DumbAwareAction() { override fun actionPerformed(e: AnActionEvent) { val keyStroke = getKeyStroke(e) diff --git a/src/com/maddyhome/idea/vim/ui/ex/ExTextField.java b/src/com/maddyhome/idea/vim/ui/ex/ExTextField.java index e629b937b3..0cd6cd26a0 100644 --- a/src/com/maddyhome/idea/vim/ui/ex/ExTextField.java +++ b/src/com/maddyhome/idea/vim/ui/ex/ExTextField.java @@ -23,11 +23,16 @@ import com.intellij.openapi.editor.Editor; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Disposer; +import com.intellij.ui.paint.PaintUtil; import com.intellij.util.ui.JBUI; import com.maddyhome.idea.vim.VimPlugin; import com.maddyhome.idea.vim.VimProjectService; import com.maddyhome.idea.vim.group.HistoryGroup; import com.maddyhome.idea.vim.helper.UiHelper; +import com.maddyhome.idea.vim.option.GuiCursorAttributes; +import com.maddyhome.idea.vim.option.GuiCursorMode; +import com.maddyhome.idea.vim.option.GuiCursorType; +import com.maddyhome.idea.vim.option.OptionsManager; import kotlin.text.StringsKt; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; @@ -39,11 +44,12 @@ import javax.swing.text.*; import java.awt.*; import java.awt.event.*; +import java.awt.geom.Area; +import java.awt.geom.Rectangle2D; import java.util.Date; import java.util.List; -import static java.lang.Math.max; -import static java.lang.Math.min; +import static java.lang.Math.*; /** * Provides a custom keymap for the text field. The keymap is the VIM Ex command keymapping @@ -336,6 +342,15 @@ public void clearCurrentAction() { clearCurrentActionPromptCharacter(); } + /** + * Text to show while composing a digraph or inserting a literal or register + * + * The prompt character is inserted directly into the text of the text field, rather than drawn over the top of the + * current character. When the action has been completed, the new character(s) are either inserted or overwritten, + * depending on the insert/overwrite status of the text field. This mimics Vim's behaviour. + * + * @param promptCharacter The character to show as prompt + */ public void setCurrentActionPromptCharacter(char promptCharacter) { actualText = removePromptCharacter(); this.currentActionPromptCharacter = promptCharacter; @@ -401,66 +416,42 @@ private void resetCaret() { // 'ci' command-line insert is ver25 // 'cr' command-line replace is hor20 // see :help 'guicursor' - // Note that we can't easily support guicursor because we don't have arbitrary control over the IntelliJ editor caret private void setNormalModeCaret() { - caret.setBlockMode(); + caret.setAttributes(OptionsManager.INSTANCE.getGuicursor().getAttributes(GuiCursorMode.CMD_LINE)); } private void setInsertModeCaret() { - caret.setMode(CommandLineCaret.CaretMode.VER, 25); + caret.setAttributes(OptionsManager.INSTANCE.getGuicursor().getAttributes(GuiCursorMode.CMD_LINE_INSERT)); } private void setReplaceModeCaret() { - caret.setMode(CommandLineCaret.CaretMode.HOR, 20); + caret.setAttributes(OptionsManager.INSTANCE.getGuicursor().getAttributes(GuiCursorMode.CMD_LINE_REPLACE)); } private static class CommandLineCaret extends DefaultCaret { - private CaretMode mode; - private int blockPercentage = 100; + private GuiCursorType mode; + private int thickness = 100; private int lastBlinkRate = 0; private boolean hasFocus; - public enum CaretMode { - BLOCK, - VER, - HOR - } - - void setBlockMode() { - setMode(CaretMode.BLOCK, 100); - } - - void setMode(CaretMode mode, int blockPercentage) { - if (this.mode == mode && this.blockPercentage == blockPercentage) { - return; - } - - // Hide the current caret and redraw without it. Then make the new caret visible, but only if it was already - // (logically) visible/active. Always making it visible can start the flasher timer unnecessarily. + public void setAttributes(GuiCursorAttributes attributes) { final boolean active = isActive(); + + // Hide the currently visible caret if (isVisible()) { setVisible(false); } - this.mode = mode; - this.blockPercentage = blockPercentage; + + mode = attributes.getType(); + thickness = mode == GuiCursorType.BLOCK ? 100 : attributes.getThickness(); + + // Make sure the caret is visible, but only if we're active, otherwise we'll kick off the flasher timer unnecessarily if (active) { setVisible(true); } } - // Java 9+ - @SuppressWarnings("deprecation") - private void updateDamage() { - try { - Rectangle r = getComponent().getUI().modelToView(getComponent(), getDot(), getDotBias()); - damage(r); - } - catch(BadLocationException e) { - // ignore - } - } - @Override public void focusGained(FocusEvent e) { if (lastBlinkRate != 0) { @@ -468,82 +459,133 @@ public void focusGained(FocusEvent e) { lastBlinkRate = 0; } super.focusGained(e); - updateDamage(); + repaint(); hasFocus = true; } @Override public void focusLost(FocusEvent e) { + // We don't call super.focusLost, which would hide the caret hasFocus = false; lastBlinkRate = getBlinkRate(); setBlinkRate(0); - // We might be losing focus while the cursor is flashing, and is currently not visible + // Make sure the box caret is visible. If we're flashing, this might be false setVisible(true); - updateDamage(); + repaint(); } - // Java 9+ - @SuppressWarnings("deprecation") @Override public void paint(Graphics g) { if (!isVisible()) return; + // Take a copy of the graphics, so we can mess around with it without having to reset after + final Graphics2D g2d = (Graphics2D) g.create(); try { final JTextComponent component = getComponent(); - g.setColor(component.getCaretColor()); - final Rectangle r = component.getUI().modelToView(component, getDot(), getDotBias()); - FontMetrics fm = g.getFontMetrics(); - final int boundsHeight = fm.getHeight(); + g2d.setColor(component.getCaretColor()); + g2d.setXORMode(component.getBackground()); + + final Rectangle2D r = modelToView(getDot()); + if (r == null) { + return; + } + + // Make sure not to use the saved bounds! There is no guarantee that damage() has been called first, especially + // when the caret has not yet been moved or changed + final FontMetrics fm = component.getFontMetrics(component.getFont()); if (!hasFocus) { - r.setBounds(r.x, r.y, getCaretWidth(fm, 100), boundsHeight); - g.drawRect(r.x, r.y, r.width, r.height); + final float outlineThickness = (float) PaintUtil.alignToInt(1.0, g2d); + final double caretWidth = getCaretWidth(fm, r.getX(), 100); + final Area area = new Area(new Rectangle2D.Double(r.getX(), r.getY(), caretWidth, r.getHeight())); + area.subtract(new Area(new Rectangle2D.Double(r.getX() + outlineThickness, r.getY() + outlineThickness, caretWidth - (2 * outlineThickness), r.getHeight() - (2 * outlineThickness)))); + g2d.fill(area); } else { - r.setBounds(r.x, r.y, getCaretWidth(fm, blockPercentage), getBlockHeight(boundsHeight)); - g.fillRect(r.x, r.y + boundsHeight - r.height, r.width, r.height); + final double caretHeight = getCaretHeight(r.getHeight()); + final double caretWidth = getCaretWidth(fm, r.getX(), thickness); + g2d.fill(new Rectangle2D.Double(r.getX(), r.getY() + r.getHeight() - caretHeight, caretWidth, caretHeight)); } } - catch (BadLocationException e) { - // ignore + finally { + g2d.dispose(); } } + /** + * Updates the bounds of the caret and repaints those bounds. + * + * This method is not guaranteed to be called before paint(). The bounds are for use by repaint(). + * + * @param r The current location of the caret, usually provided by MapToView. The x and y appear to be the upper + * left of the character position. The height appears to be correct, but the width is not the character + * width. We also get an int Rectangle, which might not match the float Rectangle we use to draw the caret + */ @Override protected synchronized void damage(Rectangle r) { if (r != null) { - JTextComponent component = getComponent(); - Font font = component.getFont(); - FontMetrics fm = component.getFontMetrics(font); - final int blockHeight = fm.getHeight(); - if (!hasFocus) { - width = this.getCaretWidth(fm, 100); - height = blockHeight; - } - else { - width = this.getCaretWidth(fm, blockPercentage); - height = getBlockHeight(blockHeight); - } + + // Always set the bounds to the full character grid, so that we are sure we will always erase any old caret. + // Note that we get an int Rectangle, while we draw with a float Rectangle. The x value is fine as it will + // round down when converting. The width is rounded up, but should also include any fraction part from x, so we + // add one. + final FontMetrics fm = getComponent().getFontMetrics(getComponent().getFont()); x = r.x; - y = r.y + blockHeight - height; + y = r.y; + width = (int)ceil(getCaretWidth(fm, r.x, 100)) + 1; + height = r.height; repaint(); } } - private int getCaretWidth(FontMetrics fm, int widthPercentage) { - if (mode == CaretMode.VER) { - // Don't show a proportional width of a proportional font - final int fullWidth = fm.charWidth('o'); - return max(1, fullWidth * widthPercentage / 100); + // [VERSION UPDATE] 203+ Use modelToView2D, which will return a float rect which positions the caret better + // Java 9+ + @SuppressWarnings("deprecation") + private @Nullable Rectangle2D modelToView(int dot) { + if (dot > getComponent().getDocument().getLength()) { + return null; + } + + try { + return getComponent().getUI().modelToView(getComponent(), dot, getDotBias()); + } + catch (BadLocationException e) { + return null; + } + } + + private double getCaretWidth(FontMetrics fm, double dotX, int widthPercentage) { + // Caret width is based on the distance to the next character. This isn't necessarily the same as the character + // width. E.g. when using float coordinates, the width of a grid is 8.4, while the character width is only 8. This + // would give us a caret that is not wide enough + double width; + final Rectangle2D r = modelToView(getDot() + 1); + if (r != null) { + // [VERSION UPDATE] 203+ Remove this +1. It's a fudge factor because we're working with integers + // When we use modelToView2D to get accurate measurements this won't be required. E.g. width can be 8.4, with a + // starting x of 8.4, which would put the right hand edge at 16.8. Because everything is rounded down, we get 16 + // So we add one + width = r.getX() - dotX + 1; + } + else { + char c = ' '; + try { + if (getDot() < getComponent().getDocument().getLength()) { + c = getComponent().getText(getDot(), 1).charAt(0); + } + } + catch (BadLocationException e) { + // Ignore + } + width = fm.charWidth(c); } - final char c = ((ExDocument)getComponent().getDocument()).getCharacter(getComponent().getCaretPosition()); - return fm.charWidth(c); + return mode == GuiCursorType.VER ? max(1.0, width * widthPercentage / 100) : width; } - private int getBlockHeight(int fullHeight) { - if (mode == CaretMode.HOR) { - return max(1, fullHeight * blockPercentage / 100); + private double getCaretHeight(double fullHeight) { + if (mode == GuiCursorType.HOR) { + return max(1.0, fullHeight * thickness / 100.0); } return fullHeight; } @@ -552,7 +594,7 @@ private int getBlockHeight(int fullHeight) { @TestOnly public @NonNls String getCaretShape() { CommandLineCaret caret = (CommandLineCaret) getCaret(); - return String.format("%s %d", caret.mode, caret.blockPercentage); + return String.format("%s %d", caret.mode, caret.thickness); } private Editor editor; diff --git a/src/com/maddyhome/idea/vim/vimscript/model/commands/LetCommand.kt b/src/com/maddyhome/idea/vim/vimscript/model/commands/LetCommand.kt index 33734858ef..1309cee1f7 100644 --- a/src/com/maddyhome/idea/vim/vimscript/model/commands/LetCommand.kt +++ b/src/com/maddyhome/idea/vim/vimscript/model/commands/LetCommand.kt @@ -4,9 +4,9 @@ import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.editor.Editor import com.maddyhome.idea.vim.ex.ExException import com.maddyhome.idea.vim.ex.ranges.Ranges -import com.maddyhome.idea.vim.option.ListOption import com.maddyhome.idea.vim.option.NumberOption import com.maddyhome.idea.vim.option.OptionsManager +import com.maddyhome.idea.vim.option.StringListOption import com.maddyhome.idea.vim.option.StringOption import com.maddyhome.idea.vim.option.ToggleOption import com.maddyhome.idea.vim.vimscript.model.Executable @@ -168,7 +168,7 @@ data class LetCommand( is StringOption -> { option.set(newValue.asString()) } - is ListOption -> { + is StringListOption -> { if (newValue is VimList) { option.set(newValue.values.joinToString(separator = ",") { it.asString() }) } else { diff --git a/src/com/maddyhome/idea/vim/vimscript/model/expressions/OptionExpression.kt b/src/com/maddyhome/idea/vim/vimscript/model/expressions/OptionExpression.kt index aefd7aa1ff..407386d9ce 100644 --- a/src/com/maddyhome/idea/vim/vimscript/model/expressions/OptionExpression.kt +++ b/src/com/maddyhome/idea/vim/vimscript/model/expressions/OptionExpression.kt @@ -3,10 +3,10 @@ package com.maddyhome.idea.vim.vimscript.model.expressions import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.editor.Editor import com.maddyhome.idea.vim.ex.ExException -import com.maddyhome.idea.vim.option.ListOption import com.maddyhome.idea.vim.option.NumberOption import com.maddyhome.idea.vim.option.Option import com.maddyhome.idea.vim.option.OptionsManager +import com.maddyhome.idea.vim.option.StringListOption import com.maddyhome.idea.vim.option.StringOption import com.maddyhome.idea.vim.option.ToggleOption import com.maddyhome.idea.vim.vimscript.model.Executable @@ -25,7 +25,7 @@ data class OptionExpression(val optionName: String) : Expression() { fun Option<*>.toVimDataType(): VimDataType { return when (this) { - is ListOption -> VimList(this.values().map { VimString(it) }.toMutableList()) + is StringListOption -> VimList(this.values().map { VimString(it) }.toMutableList()) is NumberOption -> VimInt(this.value()) is StringOption -> VimString(this.value) is ToggleOption -> VimInt(if (this.value) 1 else 0) diff --git a/test/org/jetbrains/plugins/ideavim/VimOptionTestCase.kt b/test/org/jetbrains/plugins/ideavim/VimOptionTestCase.kt index 2f743e80f4..d2c09dec55 100644 --- a/test/org/jetbrains/plugins/ideavim/VimOptionTestCase.kt +++ b/test/org/jetbrains/plugins/ideavim/VimOptionTestCase.kt @@ -18,10 +18,10 @@ package org.jetbrains.plugins.ideavim -import com.maddyhome.idea.vim.option.BoundStringOption -import com.maddyhome.idea.vim.option.ListOption +import com.maddyhome.idea.vim.option.BoundedStringOption import com.maddyhome.idea.vim.option.NumberOption import com.maddyhome.idea.vim.option.OptionsManager +import com.maddyhome.idea.vim.option.StringListOption import com.maddyhome.idea.vim.option.ToggleOption /** @@ -73,12 +73,12 @@ abstract class VimOptionTestCase(option: String, vararg otherOptions: String) : if (it.values.first().toBoolean()) option.set() else option.reset() } VimTestOptionType.LIST -> { - if (option !is ListOption) kotlin.test.fail("${it.option} is not a list option. Change it for method `${testMethod.name}`") + if (option !is StringListOption) kotlin.test.fail("${it.option} is not a string list option. Change it for method `${testMethod.name}`") option.set(it.values.joinToString(",")) } VimTestOptionType.VALUE -> { - if (option !is BoundStringOption) kotlin.test.fail("${it.option} is not a value option. Change it for method `${testMethod.name}`") + if (option !is BoundedStringOption) kotlin.test.fail("${it.option} is not a value option. Change it for method `${testMethod.name}`") option.set(it.values.first()) } diff --git a/test/org/jetbrains/plugins/ideavim/VimTestCase.kt b/test/org/jetbrains/plugins/ideavim/VimTestCase.kt index ac1c0555e7..5b0dc2313a 100644 --- a/test/org/jetbrains/plugins/ideavim/VimTestCase.kt +++ b/test/org/jetbrains/plugins/ideavim/VimTestCase.kt @@ -24,7 +24,7 @@ import com.intellij.ide.highlighter.XmlFileType import com.intellij.json.JsonFileType import com.intellij.openapi.application.PathManager import com.intellij.openapi.application.WriteAction -import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.CaretVisualAttributes import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Inlay import com.intellij.openapi.editor.LogicalPosition @@ -56,12 +56,12 @@ import com.maddyhome.idea.vim.helper.StringHelper.parseKeys import com.maddyhome.idea.vim.helper.StringHelper.stringToKeys import com.maddyhome.idea.vim.helper.StringHelper.toKeyNotation import com.maddyhome.idea.vim.helper.TestInputModel +import com.maddyhome.idea.vim.helper.guicursorMode import com.maddyhome.idea.vim.helper.inBlockSubMode -import com.maddyhome.idea.vim.helper.isBlockCaretShape -import com.maddyhome.idea.vim.helper.mode import com.maddyhome.idea.vim.key.MappingOwner import com.maddyhome.idea.vim.key.ToKeysMappingInfo import com.maddyhome.idea.vim.listener.SelectionVimListenerSuppressor +import com.maddyhome.idea.vim.option.GuiCursorType import com.maddyhome.idea.vim.option.OptionsManager import com.maddyhome.idea.vim.option.OptionsManager.getOption import com.maddyhome.idea.vim.option.OptionsManager.ideastrictmode @@ -418,24 +418,28 @@ abstract class VimTestCase : UsefulTestCase() { Assertions.assertThat(VimPlugin.getMessage()).contains(message) } - protected fun assertCaretsColour() { - val selectionColour = myFixture.editor.colorsScheme.getColor(EditorColors.SELECTION_BACKGROUND_COLOR) - val caretColour = myFixture.editor.colorsScheme.getColor(EditorColors.CARET_COLOR) - if (myFixture.editor.inBlockSubMode) { - val caretModel = myFixture.editor.caretModel - caretModel.allCarets.forEach { caret: Caret -> - if (caret !== caretModel.primaryCaret) { - Assert.assertEquals(selectionColour, caret.visualAttributes.color) - } else { - val color = caret.visualAttributes.color - if (color != null) Assert.assertEquals(caretColour, color) + protected fun assertCaretsVisualAttributes() { + val editor = myFixture.editor + val attributes = OptionsManager.guicursor.getAttributes(editor.guicursorMode()) + val shape = when (attributes.type) { + GuiCursorType.BLOCK -> CaretVisualAttributes.Shape.BLOCK + GuiCursorType.VER -> CaretVisualAttributes.Shape.BAR + GuiCursorType.HOR -> CaretVisualAttributes.Shape.UNDERSCORE + } + val colour = editor.colorsScheme.getColor(EditorColors.CARET_COLOR) + + editor.caretModel.allCarets.forEach { caret -> + // All carets should be the same except when in block sub mode, where we "hide" them (by drawing a zero width bar) + if (caret !== editor.caretModel.primaryCaret && editor.inBlockSubMode) { + assertEquals(CaretVisualAttributes.Shape.BAR, caret.visualAttributes.shape) + assertEquals(0F, caret.visualAttributes.thickness) + } else { + assertEquals(shape, editor.caretModel.primaryCaret.visualAttributes.shape) + assertEquals(attributes.thickness / 100.0F, editor.caretModel.primaryCaret.visualAttributes.thickness) + editor.caretModel.primaryCaret.visualAttributes.color?.let { + assertEquals(colour, it) } } - } else { - myFixture.editor.caretModel.allCarets.forEach { caret: Caret -> - val color = caret.visualAttributes.color - if (color != null) Assert.assertEquals(caretColour, color) - } } } @@ -500,13 +504,9 @@ abstract class VimTestCase : UsefulTestCase() { } protected fun assertState(modeAfter: CommandState.Mode, subModeAfter: SubMode) { - assertCaretsColour() assertMode(modeAfter) assertSubMode(subModeAfter) - if (Checks.caretShape) assertEquals( - myFixture.editor.mode.isBlockCaretShape, - myFixture.editor.settings.isBlockCursor - ) + assertCaretsVisualAttributes() } protected val fileManager: FileEditorManagerEx diff --git a/test/org/jetbrains/plugins/ideavim/action/CopyActionTest.java b/test/org/jetbrains/plugins/ideavim/action/CopyActionTest.java index d594e923c0..bf7642b5ae 100644 --- a/test/org/jetbrains/plugins/ideavim/action/CopyActionTest.java +++ b/test/org/jetbrains/plugins/ideavim/action/CopyActionTest.java @@ -22,8 +22,9 @@ import com.maddyhome.idea.vim.VimPlugin; import com.maddyhome.idea.vim.command.CommandState; import com.maddyhome.idea.vim.common.Register; -import com.maddyhome.idea.vim.option.ListOption; +import com.maddyhome.idea.vim.ex.ExException; import com.maddyhome.idea.vim.option.OptionsManager; +import com.maddyhome.idea.vim.option.StringListOption; import org.jetbrains.plugins.ideavim.SkipNeovimReason; import org.jetbrains.plugins.ideavim.TestWithoutNeovim; import org.jetbrains.plugins.ideavim.VimTestCase; @@ -135,9 +136,9 @@ public void testStateAfterYankVisualBlock() { // VIM-476 |yy| |'clipboard'| // TODO: Review this test // This doesn't use the system clipboard, but the TestClipboardModel - public void testClipboardUnnamed() { + public void testClipboardUnnamed() throws ExException { assertEquals('\"', VimPlugin.getRegister().getDefaultRegister()); - final ListOption clipboardOption = OptionsManager.INSTANCE.getClipboard(); + final StringListOption clipboardOption = OptionsManager.INSTANCE.getClipboard(); assertNotNull(clipboardOption); clipboardOption.set("unnamed"); assertEquals('*', VimPlugin.getRegister().getDefaultRegister()); diff --git a/test/org/jetbrains/plugins/ideavim/action/change/insert/InsertAfterCursorActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/change/insert/InsertAfterCursorActionTest.kt new file mode 100644 index 0000000000..ab58ff2b8e --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/change/insert/InsertAfterCursorActionTest.kt @@ -0,0 +1,46 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2021 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.jetbrains.plugins.ideavim.action.change.insert + +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys +import org.jetbrains.plugins.ideavim.SkipNeovimReason +import org.jetbrains.plugins.ideavim.TestWithoutNeovim +import org.jetbrains.plugins.ideavim.VimTestCase + +class InsertAfterCursorActionTest : VimTestCase() { + @TestWithoutNeovim(SkipNeovimReason.INLAYS) + fun `test insert after cursor with inlay with preceding text places caret between inlay and preceding text`() { + configureByText("I found it i${c}n a legendary land") + // Inlay is at vp 13. Preceding text is at 12. Caret should be between preceding and inlay = 13 + // I found it in|: a legendary land + addInlay(13, true, 5) + typeText(parseKeys("a")) + assertVisualPosition(0, 13) + } + + @TestWithoutNeovim(SkipNeovimReason.INLAYS) + fun `test insert after cursor with inlay with following text places caret between inlay and following text`() { + configureByText("I found it$c in a legendary land") + // Inlay is at offset 11, following text is at vp 12 + // I found it :|in a legendary land + addInlay(11, false, 5) + typeText(parseKeys("a")) + assertVisualPosition(0, 12) + } +} diff --git a/test/org/jetbrains/plugins/ideavim/action/change/insert/InsertBeforeCursorActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/change/insert/InsertBeforeCursorActionTest.kt index a02fd8817c..278493ed15 100644 --- a/test/org/jetbrains/plugins/ideavim/action/change/insert/InsertBeforeCursorActionTest.kt +++ b/test/org/jetbrains/plugins/ideavim/action/change/insert/InsertBeforeCursorActionTest.kt @@ -18,23 +18,12 @@ package org.jetbrains.plugins.ideavim.action.change.insert -import com.intellij.openapi.editor.ex.EditorSettingsExternalizable import com.maddyhome.idea.vim.command.CommandState import org.jetbrains.plugins.ideavim.VimTestCase class InsertBeforeCursorActionTest : VimTestCase() { fun `test check caret shape`() { doTest("i", "123", "123", CommandState.Mode.INSERT, CommandState.SubMode.NONE) - assertFalse(myFixture.editor.settings.isBlockCursor) + assertCaretsVisualAttributes() } - - fun `test check caret shape for block caret`() { - EditorSettingsExternalizable.getInstance().isBlockCursor = true - try { - doTest("i", "123", "123", CommandState.Mode.INSERT, CommandState.SubMode.NONE) - assertTrue(myFixture.editor.settings.isBlockCursor) - } finally { - EditorSettingsExternalizable.getInstance().isBlockCursor = false - } - } -} +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionArrowRightActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionArrowRightActionTest.kt index 671757cfe2..48e268e42e 100644 --- a/test/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionArrowRightActionTest.kt +++ b/test/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionArrowRightActionTest.kt @@ -33,8 +33,6 @@ import org.jetbrains.plugins.ideavim.VimTestOptionType class MotionArrowRightActionTest : VimOptionTestCase(KeyModelOptionData.name) { - // Kotlin type hints should be an obvious example of an inlay related to preceding text, but they are actually - // related to following (KTIJ-3768). The inline rename options inlay is a better example @TestWithoutNeovim(SkipNeovimReason.INLAYS) @VimOptionDefaultAll fun `test with inlay related to preceding text and block caret`() { @@ -117,23 +115,31 @@ class MotionArrowRightActionTest : VimOptionTestCase(KeyModelOptionData.name) { configureByText(before) assertOffset(4) + assertVisualPosition(0, 4) + // Inlay shares offset 4 with the 'u' in "found", inserts a new visual column 4 and is related to the text at // offset 3/visual column 3. // Moving from offset 4 (visual column 4 because bar caret and related to preceding text!) will move to // offset 3, which is also visual column 3. - // Before: "I fo|«:test»und it in a legendary land." - // After: "I fo«:test»u|nd it in a legendary land." + // Initially (normal): "I fo|u|nd it in a legendary land" (caret = vp4) + // With inlay (normal): "I fo«:test»|u|nd it in a legendary land" (caret = vp5) + // In insert mode: "I fo|«:test»und it in a legendary land" (caret = vp4) + // : "I fo«:test»u|nd it in a legendary land" (caret = vp6) + // : "I fo«:test»|u|nd it in a legendary land" (caret = vp5) addInlay(4, true, 5) - typeText(parseKeys("i", "")) - assertState(after) + typeText(parseKeys("i")) + assertVisualPosition(0, 4) + + typeText(parseKeys("")) + myFixture.checkResult(after) assertOffset(5) assertVisualPosition(0, 6) typeText(parseKeys("")) assertOffset(4) - assertVisualPosition(0, 4) + assertVisualPosition(0, 5) } // Kotlin parameter hints are a good example of inlays related to following text diff --git a/test/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionShiftLeftActionHandlerTest.kt b/test/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionShiftLeftActionHandlerTest.kt index bfba1e91be..c1dbb01778 100644 --- a/test/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionShiftLeftActionHandlerTest.kt +++ b/test/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionShiftLeftActionHandlerTest.kt @@ -547,7 +547,7 @@ class MotionShiftLeftActionHandlerTest : VimOptionTestCase(KeyModelOptionData.na CommandState.Mode.SELECT, CommandState.SubMode.VISUAL_BLOCK ) - assertCaretsColour() + assertCaretsVisualAttributes() } @TestWithoutNeovim(SkipNeovimReason.OPTION) @@ -577,7 +577,7 @@ class MotionShiftLeftActionHandlerTest : VimOptionTestCase(KeyModelOptionData.na CommandState.Mode.SELECT, CommandState.SubMode.VISUAL_BLOCK ) - assertCaretsColour() + assertCaretsVisualAttributes() } @TestWithoutNeovim(SkipNeovimReason.OPTION) diff --git a/test/org/jetbrains/plugins/ideavim/action/motion/mark/MotionMarkActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/motion/mark/MotionMarkActionTest.kt index 7bc75e62a2..98af65db4b 100644 --- a/test/org/jetbrains/plugins/ideavim/action/motion/mark/MotionMarkActionTest.kt +++ b/test/org/jetbrains/plugins/ideavim/action/motion/mark/MotionMarkActionTest.kt @@ -21,15 +21,15 @@ package org.jetbrains.plugins.ideavim.action.motion.mark import com.intellij.ide.bookmarks.BookmarkManager import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.helper.StringHelper -import com.maddyhome.idea.vim.option.IdeaMarkskOptionsData +import com.maddyhome.idea.vim.option.IdeaMarksOptionsData import junit.framework.TestCase import org.jetbrains.plugins.ideavim.VimOptionTestCase import org.jetbrains.plugins.ideavim.VimOptionTestConfiguration import org.jetbrains.plugins.ideavim.VimTestOption import org.jetbrains.plugins.ideavim.VimTestOptionType -class MotionMarkActionTest : VimOptionTestCase(IdeaMarkskOptionsData.name) { - @VimOptionTestConfiguration(VimTestOption(IdeaMarkskOptionsData.name, VimTestOptionType.TOGGLE, ["true"])) +class MotionMarkActionTest : VimOptionTestCase(IdeaMarksOptionsData.name) { + @VimOptionTestConfiguration(VimTestOption(IdeaMarksOptionsData.name, VimTestOptionType.TOGGLE, ["true"])) fun `test simple add mark`() { val keys = StringHelper.parseKeys("mA") val text = """ @@ -45,7 +45,7 @@ class MotionMarkActionTest : VimOptionTestCase(IdeaMarkskOptionsData.name) { checkMarks('A' to 2) } - @VimOptionTestConfiguration(VimTestOption(IdeaMarkskOptionsData.name, VimTestOptionType.TOGGLE, ["true"])) + @VimOptionTestConfiguration(VimTestOption(IdeaMarksOptionsData.name, VimTestOptionType.TOGGLE, ["true"])) fun `test simple add multiple marks`() { val keys = StringHelper.parseKeys("mAj", "mBj", "mC") val text = """ @@ -61,7 +61,7 @@ class MotionMarkActionTest : VimOptionTestCase(IdeaMarkskOptionsData.name) { checkMarks('A' to 2, 'B' to 3, 'C' to 4) } - @VimOptionTestConfiguration(VimTestOption(IdeaMarkskOptionsData.name, VimTestOptionType.TOGGLE, ["true"])) + @VimOptionTestConfiguration(VimTestOption(IdeaMarksOptionsData.name, VimTestOptionType.TOGGLE, ["true"])) fun `test simple add multiple marks on same line`() { val keys = StringHelper.parseKeys("mA", "mB", "mC") val text = """ @@ -77,7 +77,7 @@ class MotionMarkActionTest : VimOptionTestCase(IdeaMarkskOptionsData.name) { checkMarks('A' to 2, 'B' to 2, 'C' to 2) } - @VimOptionTestConfiguration(VimTestOption(IdeaMarkskOptionsData.name, VimTestOptionType.TOGGLE, ["true"])) + @VimOptionTestConfiguration(VimTestOption(IdeaMarksOptionsData.name, VimTestOptionType.TOGGLE, ["true"])) fun `test move to another line`() { val keys = StringHelper.parseKeys("mAjj", "mA") val text = """ @@ -93,7 +93,7 @@ class MotionMarkActionTest : VimOptionTestCase(IdeaMarkskOptionsData.name) { checkMarks('A' to 4) } - @VimOptionTestConfiguration(VimTestOption(IdeaMarkskOptionsData.name, VimTestOptionType.TOGGLE, ["true"])) + @VimOptionTestConfiguration(VimTestOption(IdeaMarksOptionsData.name, VimTestOptionType.TOGGLE, ["true"])) fun `test simple system mark`() { val text = """ A Discovery @@ -113,7 +113,7 @@ class MotionMarkActionTest : VimOptionTestCase(IdeaMarkskOptionsData.name) { TestCase.assertEquals('A', vimMarks[0].key) } - @VimOptionTestConfiguration(VimTestOption(IdeaMarkskOptionsData.name, VimTestOptionType.TOGGLE, ["true"])) + @VimOptionTestConfiguration(VimTestOption(IdeaMarksOptionsData.name, VimTestOptionType.TOGGLE, ["true"])) fun `test system mark move to another line`() { val text = """ A Discovery diff --git a/test/org/jetbrains/plugins/ideavim/action/motion/select/SelectEscapeActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/motion/select/SelectEscapeActionTest.kt index 0a4eb72182..4620fef2a6 100644 --- a/test/org/jetbrains/plugins/ideavim/action/motion/select/SelectEscapeActionTest.kt +++ b/test/org/jetbrains/plugins/ideavim/action/motion/select/SelectEscapeActionTest.kt @@ -336,7 +336,7 @@ class SelectEscapeActionTest : VimTestCase() { ) assertFalse(myFixture.editor.caretModel.allCarets.any(Caret::hasSelection)) assertEquals(1, myFixture.editor.caretModel.caretCount) - assertCaretsColour() + assertCaretsVisualAttributes() assertMode(CommandState.Mode.COMMAND) } @@ -365,7 +365,7 @@ class SelectEscapeActionTest : VimTestCase() { ) assertFalse(myFixture.editor.caretModel.allCarets.any(Caret::hasSelection)) assertEquals(1, myFixture.editor.caretModel.caretCount) - assertCaretsColour() + assertCaretsVisualAttributes() assertMode(CommandState.Mode.COMMAND) } @@ -394,7 +394,7 @@ class SelectEscapeActionTest : VimTestCase() { ) assertFalse(myFixture.editor.caretModel.allCarets.any(Caret::hasSelection)) assertEquals(1, myFixture.editor.caretModel.caretCount) - assertCaretsColour() + assertCaretsVisualAttributes() assertMode(CommandState.Mode.COMMAND) } @@ -423,7 +423,7 @@ class SelectEscapeActionTest : VimTestCase() { ) assertFalse(myFixture.editor.caretModel.allCarets.any(Caret::hasSelection)) assertEquals(1, myFixture.editor.caretModel.caretCount) - assertCaretsColour() + assertCaretsVisualAttributes() assertMode(CommandState.Mode.COMMAND) } } diff --git a/test/org/jetbrains/plugins/ideavim/action/motion/select/SelectKeyHandlerTest.kt b/test/org/jetbrains/plugins/ideavim/action/motion/select/SelectKeyHandlerTest.kt index f067056936..cb9f328ea9 100644 --- a/test/org/jetbrains/plugins/ideavim/action/motion/select/SelectKeyHandlerTest.kt +++ b/test/org/jetbrains/plugins/ideavim/action/motion/select/SelectKeyHandlerTest.kt @@ -346,7 +346,7 @@ class SelectKeyHandlerTest : VimTestCase() { CommandState.Mode.COMMAND, CommandState.SubMode.NONE ) - assertCaretsColour() + assertCaretsVisualAttributes() assertMode(CommandState.Mode.COMMAND) } } diff --git a/test/org/jetbrains/plugins/ideavim/action/motion/visual/VisualExitModeActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/motion/visual/VisualExitModeActionTest.kt index a5480a3b3f..976cdcd579 100644 --- a/test/org/jetbrains/plugins/ideavim/action/motion/visual/VisualExitModeActionTest.kt +++ b/test/org/jetbrains/plugins/ideavim/action/motion/visual/VisualExitModeActionTest.kt @@ -24,10 +24,11 @@ import org.jetbrains.plugins.ideavim.VimTestCase class VisualExitModeActionTest : VimTestCase() { fun `test exit visual mode after line end`() { doTest("vl", "12${c}3", "12${c}3", CommandState.Mode.COMMAND, CommandState.SubMode.NONE) + assertCaretsVisualAttributes() } fun `test double exit`() { doTest("vl", "12${c}3", "12${c}3", CommandState.Mode.COMMAND, CommandState.SubMode.NONE) - assertTrue(myFixture.editor.settings.isBlockCursor) + assertCaretsVisualAttributes() } } diff --git a/test/org/jetbrains/plugins/ideavim/ex/ExEntryTest.kt b/test/org/jetbrains/plugins/ideavim/ex/ExEntryTest.kt index 32a13c4282..d1ed18999e 100644 --- a/test/org/jetbrains/plugins/ideavim/ex/ExEntryTest.kt +++ b/test/org/jetbrains/plugins/ideavim/ex/ExEntryTest.kt @@ -103,6 +103,28 @@ class ExEntryTest : VimTestCase() { assertEquals("VER 25", exEntryPanel.entry.caretShape) } + fun `test caret shape comes from guicursor`() { + enterCommand("set guicursor=c:ver50,ci:hor75,cr:block") + + typeExInput(":") + assertEquals("VER 50", exEntryPanel.entry.caretShape) + + typeText("set") + assertEquals("VER 50", exEntryPanel.entry.caretShape) + + deactivateExEntry() + typeExInput(":set") + assertEquals("HOR 75", exEntryPanel.entry.caretShape) + + deactivateExEntry() + typeExInput(":set") + assertEquals("BLOCK 100", exEntryPanel.entry.caretShape) + + deactivateExEntry() + typeExInput(":set") + assertEquals("HOR 75", exEntryPanel.entry.caretShape) + } + fun `test move caret to beginning of line`() { typeExInput(":set incsearch") assertExOffset(0) diff --git a/test/org/jetbrains/plugins/ideavim/group/visual/IdeaVisualControlTest.kt b/test/org/jetbrains/plugins/ideavim/group/visual/IdeaVisualControlTest.kt index 9587c02a9e..22c8a59862 100644 --- a/test/org/jetbrains/plugins/ideavim/group/visual/IdeaVisualControlTest.kt +++ b/test/org/jetbrains/plugins/ideavim/group/visual/IdeaVisualControlTest.kt @@ -61,7 +61,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { IdeaSelectionControl.controlNonVimSelectionChange(myFixture.editor) assertMode(CommandState.Mode.COMMAND) assertSubMode(CommandState.SubMode.NONE) - assertCaretsColour() + assertCaretsVisualAttributes() } @VimOptionDefaultAll @@ -82,7 +82,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { waitAndAssertMode(myFixture, CommandState.Mode.VISUAL) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_CHARACTER) - assertCaretsColour() + assertCaretsVisualAttributes() typeText(parseKeys("l")) assertState( @@ -97,7 +97,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { ) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_CHARACTER) - assertCaretsColour() + assertCaretsVisualAttributes() } @VimBehaviorDiffers( @@ -128,7 +128,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { waitAndAssertMode(myFixture, CommandState.Mode.VISUAL) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_CHARACTER) - assertCaretsColour() + assertCaretsVisualAttributes() typeText(parseKeys("l")) assertState( @@ -143,7 +143,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { ) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_CHARACTER) - assertCaretsColour() + assertCaretsVisualAttributes() } @VimOptionDefaultAll @@ -164,7 +164,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { waitAndAssertMode(myFixture, CommandState.Mode.VISUAL) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_CHARACTER) - assertCaretsColour() + assertCaretsVisualAttributes() typeText(parseKeys("l")) assertState( @@ -179,7 +179,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { ) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_CHARACTER) - assertCaretsColour() + assertCaretsVisualAttributes() } @VimOptionDefaultAll @@ -200,7 +200,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { waitAndAssertMode(myFixture, CommandState.Mode.VISUAL) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_CHARACTER) - assertCaretsColour() + assertCaretsVisualAttributes() typeText(parseKeys("l")) assertState( @@ -215,7 +215,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { ) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_CHARACTER) - assertCaretsColour() + assertCaretsVisualAttributes() } @VimOptionDefaultAll @@ -236,7 +236,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { waitAndAssertMode(myFixture, CommandState.Mode.VISUAL) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_CHARACTER) - assertCaretsColour() + assertCaretsVisualAttributes() typeText(parseKeys("l")) assertState( @@ -251,7 +251,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { ) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_CHARACTER) - assertCaretsColour() + assertCaretsVisualAttributes() } @VimOptionDefaultAll @@ -272,7 +272,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { waitAndAssertMode(myFixture, CommandState.Mode.VISUAL) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_CHARACTER) - assertCaretsColour() + assertCaretsVisualAttributes() typeText(parseKeys("l")) assertState( @@ -287,7 +287,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { ) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_CHARACTER) - assertCaretsColour() + assertCaretsVisualAttributes() } @VimOptionDefaultAll @@ -308,7 +308,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { waitAndAssertMode(myFixture, CommandState.Mode.VISUAL) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_CHARACTER) - assertCaretsColour() + assertCaretsVisualAttributes() typeText(parseKeys("l")) assertState( @@ -323,7 +323,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { ) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_CHARACTER) - assertCaretsColour() + assertCaretsVisualAttributes() } @VimOptionDefaultAll @@ -344,7 +344,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { waitAndAssertMode(myFixture, CommandState.Mode.VISUAL) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_CHARACTER) - assertCaretsColour() + assertCaretsVisualAttributes() typeText(parseKeys("l")) assertState( @@ -359,7 +359,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { ) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_CHARACTER) - assertCaretsColour() + assertCaretsVisualAttributes() } @VimOptionDefaultAll @@ -380,7 +380,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { waitAndAssertMode(myFixture, CommandState.Mode.VISUAL) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_LINE) - assertCaretsColour() + assertCaretsVisualAttributes() typeText(parseKeys("l")) assertState( @@ -395,7 +395,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { ) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_LINE) - assertCaretsColour() + assertCaretsVisualAttributes() typeText(parseKeys("j")) assertState( @@ -410,7 +410,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { ) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_LINE) - assertCaretsColour() + assertCaretsVisualAttributes() } @VimOptionDefaultAll @@ -431,7 +431,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { waitAndAssertMode(myFixture, CommandState.Mode.VISUAL) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_LINE) - assertCaretsColour() + assertCaretsVisualAttributes() typeText(parseKeys("j")) assertState( @@ -446,7 +446,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { ) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_LINE) - assertCaretsColour() + assertCaretsVisualAttributes() } @VimOptionDefaultAll @@ -467,7 +467,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { waitAndAssertMode(myFixture, CommandState.Mode.VISUAL) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_LINE) - assertCaretsColour() + assertCaretsVisualAttributes() typeText(parseKeys("j")) assertState( @@ -482,7 +482,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { ) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_LINE) - assertCaretsColour() + assertCaretsVisualAttributes() } @VimOptionDefaultAll @@ -503,7 +503,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { waitAndAssertMode(myFixture, CommandState.Mode.VISUAL) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_LINE) - assertCaretsColour() + assertCaretsVisualAttributes() typeText(parseKeys("j")) assertState( @@ -518,7 +518,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { ) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_LINE) - assertCaretsColour() + assertCaretsVisualAttributes() } @VimOptionDefaultAll @@ -539,7 +539,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { waitAndAssertMode(myFixture, CommandState.Mode.VISUAL) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_LINE) - assertCaretsColour() + assertCaretsVisualAttributes() typeText(parseKeys("j")) assertState( @@ -554,7 +554,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { ) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_LINE) - assertCaretsColour() + assertCaretsVisualAttributes() } @VimOptionDefaultAll @@ -575,7 +575,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { waitAndAssertMode(myFixture, CommandState.Mode.VISUAL) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_LINE) - assertCaretsColour() + assertCaretsVisualAttributes() typeText(parseKeys("k")) assertState( @@ -590,7 +590,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { ) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_LINE) - assertCaretsColour() + assertCaretsVisualAttributes() } @VimOptionDefaultAll @@ -611,7 +611,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { waitAndAssertMode(myFixture, CommandState.Mode.VISUAL) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_CHARACTER) - assertCaretsColour() + assertCaretsVisualAttributes() } @VimOptionDefaultAll @@ -632,7 +632,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { waitAndAssertMode(myFixture, CommandState.Mode.VISUAL) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_BLOCK) - assertCaretsColour() + assertCaretsVisualAttributes() typeText(parseKeys("l")) assertState( @@ -647,7 +647,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { ) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_BLOCK) - assertCaretsColour() + assertCaretsVisualAttributes() } @VimOptionDefaultAll @@ -668,7 +668,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { waitAndAssertMode(myFixture, CommandState.Mode.VISUAL) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_BLOCK) - assertCaretsColour() + assertCaretsVisualAttributes() typeText(parseKeys("j")) assertState( @@ -683,7 +683,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { ) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_BLOCK) - assertCaretsColour() + assertCaretsVisualAttributes() } @VimOptionDefaultAll @@ -704,7 +704,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { waitAndAssertMode(myFixture, CommandState.Mode.VISUAL) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_BLOCK) - assertCaretsColour() + assertCaretsVisualAttributes() typeText(parseKeys("l")) assertState( @@ -719,7 +719,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { ) assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_BLOCK) - assertCaretsColour() + assertCaretsVisualAttributes() } @VimOptionTestConfiguration( @@ -787,7 +787,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { waitAndAssert { myFixture.editor.subMode == CommandState.SubMode.VISUAL_CHARACTER } assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_CHARACTER) - assertCaretsColour() + assertCaretsVisualAttributes() } @VimOptionTestConfiguration(VimTestOption(SelectModeOptionData.name, VimTestOptionType.LIST, [""])) @@ -814,7 +814,7 @@ class IdeaVisualControlTest : VimOptionTestCase(SelectModeOptionData.name) { waitAndAssert { myFixture.editor.subMode == CommandState.SubMode.VISUAL_CHARACTER } assertMode(CommandState.Mode.VISUAL) assertSubMode(CommandState.SubMode.VISUAL_CHARACTER) - assertCaretsColour() + assertCaretsVisualAttributes() } private fun startDummyTemplate() { diff --git a/test/org/jetbrains/plugins/ideavim/helper/CaretVisualAttributesHelperTest.kt b/test/org/jetbrains/plugins/ideavim/helper/CaretVisualAttributesHelperTest.kt new file mode 100644 index 0000000000..774a4c0434 --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/helper/CaretVisualAttributesHelperTest.kt @@ -0,0 +1,249 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2021 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.jetbrains.plugins.ideavim.helper + +import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.CaretVisualAttributes +import com.intellij.openapi.editor.ex.EditorSettingsExternalizable +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys +import com.maddyhome.idea.vim.helper.VimBehaviorDiffers +import com.maddyhome.idea.vim.option.OptionsManager +import org.jetbrains.plugins.ideavim.SkipNeovimReason +import org.jetbrains.plugins.ideavim.TestWithoutNeovim +import org.jetbrains.plugins.ideavim.VimTestCase + +class CaretVisualAttributesHelperTest : VimTestCase() { + @TestWithoutNeovim(SkipNeovimReason.NOT_VIM_TESTING) + fun `test default normal mode caret is block`() { + configureByText("I found it in a legendary land") + assertCaretVisualAttributes(CaretVisualAttributes.Shape.BLOCK, 0F) + } + + @TestWithoutNeovim(SkipNeovimReason.NOT_VIM_TESTING) + fun `test default insert mode caret is vertical bar`() { + configureByText("I found it in a legendary land") + typeText(parseKeys("i")) + assertCaretVisualAttributes(CaretVisualAttributes.Shape.BAR, 0.25F) + } + + @TestWithoutNeovim(SkipNeovimReason.NOT_VIM_TESTING) + fun `test insert mode caret is reset after Escape`() { + configureByText("I found it in a legendary land") + typeText(parseKeys("i", "")) + assertCaretVisualAttributes(CaretVisualAttributes.Shape.BLOCK, 0F) + } + + @TestWithoutNeovim(SkipNeovimReason.NOT_VIM_TESTING) + fun `test default replace mode caret is underscore`() { + configureByText("I found it in a legendary land") + typeText(parseKeys("R")) + assertCaretVisualAttributes(CaretVisualAttributes.Shape.UNDERSCORE, 0.2F) + } + + @TestWithoutNeovim(SkipNeovimReason.NOT_VIM_TESTING) + fun `test default op pending caret is thick underscore`() { + configureByText("I found it in a legendary land") + typeText(parseKeys("d")) + assertCaretVisualAttributes(CaretVisualAttributes.Shape.UNDERSCORE, 0.5F) + } + + @TestWithoutNeovim(SkipNeovimReason.NOT_VIM_TESTING) + fun `test caret is reset after op pending`() { + configureByText("I found it in a legendary land") + typeText(parseKeys("d$")) + assertCaretVisualAttributes(CaretVisualAttributes.Shape.BLOCK, 0F) + } + + @TestWithoutNeovim(SkipNeovimReason.NOT_VIM_TESTING) + fun `test default visual mode caret is block`() { + configureByText("I found it in a legendary land") + typeText(parseKeys("ve")) + assertCaretVisualAttributes(CaretVisualAttributes.Shape.BLOCK, 0F) + } + + @TestWithoutNeovim(SkipNeovimReason.NOT_VIM_TESTING) + fun `test visual block hides secondary carets`() { + configureByLines(5, "I found it in a legendary land") + typeText(parseKeys("w", "2j5l")) + assertCaretVisualAttributes(CaretVisualAttributes.Shape.BLOCK, 0F) + myFixture.editor.caretModel.allCarets.forEach { + if (it != myFixture.editor.caretModel.primaryCaret) { + assertCaretVisualAttributes(it, CaretVisualAttributes.Shape.BAR, 0F) + } + } + } + + @VimBehaviorDiffers(description = "Vim does not change the caret for select mode", shouldBeFixed = false) + @TestWithoutNeovim(SkipNeovimReason.NOT_VIM_TESTING) + fun `test select mode uses insert mode caret`() { + // Vim doesn't have a different caret for SELECT, and doesn't have an option in guicursor to change SELECT mode + configureByText("I found it in a legendary land") + typeText(parseKeys("v7l", "")) + assertCaretVisualAttributes(CaretVisualAttributes.Shape.BAR, 0.25F) + } + + @TestWithoutNeovim(SkipNeovimReason.NOT_VIM_TESTING) + fun `test replace character uses replace mode caret`() { + configureByText("I ${c}found it in a legendary land") + typeText(parseKeys("r")) + assertCaretVisualAttributes(CaretVisualAttributes.Shape.UNDERSCORE, 0.2F) + } + + @TestWithoutNeovim(SkipNeovimReason.NOT_VIM_TESTING) + fun `test caret reset after replacing character`() { + configureByText("I ${c}found it in a legendary land") + typeText(parseKeys("r", "z")) + assertCaretVisualAttributes(CaretVisualAttributes.Shape.BLOCK, 0F) + } + + @TestWithoutNeovim(SkipNeovimReason.NOT_VIM_TESTING) + fun `test caret reset after escaping replace character`() { + configureByText("I ${c}found it in a legendary land") + typeText(parseKeys("r", "")) + assertCaretVisualAttributes(CaretVisualAttributes.Shape.BLOCK, 0F) + } + + @TestWithoutNeovim(SkipNeovimReason.NOT_VIM_TESTING) + fun `test caret reset after cancelling replace character`() { + configureByText("I ${c}found it in a legendary land") + typeText(parseKeys("r", "")) + assertCaretVisualAttributes(CaretVisualAttributes.Shape.BLOCK, 0F) + } + + @TestWithoutNeovim(SkipNeovimReason.NOT_VIM_TESTING) + fun `test visual replace character uses replace mode caret`() { + configureByText("I ${c}found it in a legendary land") + typeText(parseKeys("ve", "r")) + assertCaretVisualAttributes(CaretVisualAttributes.Shape.UNDERSCORE, 0.2F) + } + + @TestWithoutNeovim(SkipNeovimReason.NOT_VIM_TESTING) + fun `test caret reset after completing visual replace character`() { + configureByText("I ${c}found it in a legendary land") + typeText(parseKeys("ve", "r", "z")) + assertCaretVisualAttributes(CaretVisualAttributes.Shape.BLOCK, 0F) + } + + @TestWithoutNeovim(SkipNeovimReason.NOT_VIM_TESTING) + fun `test caret reset after escaping visual replace character`() { + configureByText("I ${c}found it in a legendary land") + typeText(parseKeys("ve", "r", "")) + assertCaretVisualAttributes(CaretVisualAttributes.Shape.BLOCK, 0F) + } + + @TestWithoutNeovim(SkipNeovimReason.NOT_VIM_TESTING) + fun `test caret reset after cancelling visual replace character`() { + configureByText("I ${c}found it in a legendary land") + typeText(parseKeys("ve", "r", "")) + assertCaretVisualAttributes(CaretVisualAttributes.Shape.BLOCK, 0F) + } + + @TestWithoutNeovim(SkipNeovimReason.NOT_VIM_TESTING) + fun `test nested visual mode in ide gets visual caret`() { + OptionsManager.keymodel.set("startsel,stopsel") + configureByText("I ${c}found it in a legendary land") + typeText(parseKeys("i", "")) + assertCaretVisualAttributes(CaretVisualAttributes.Shape.BLOCK, 0F) + } + + @TestWithoutNeovim(SkipNeovimReason.NOT_VIM_TESTING) + fun `test caret reset to insert after leaving nested visual mode`() { + OptionsManager.keymodel.set("startsel,stopsel") + configureByText("I ${c}found it in a legendary land") + typeText(parseKeys("i", "", "")) + assertCaretVisualAttributes(CaretVisualAttributes.Shape.BAR, 0.25F) + } + + @TestWithoutNeovim(SkipNeovimReason.NOT_VIM_TESTING) + fun `test caret reset to insert after cancelling nested visual mode`() { + OptionsManager.keymodel.set("startsel,stopsel") + configureByText("I ${c}found it in a legendary land") + typeText(parseKeys("i", "", "")) + assertCaretVisualAttributes(CaretVisualAttributes.Shape.BAR, 0.25F) + } + + @TestWithoutNeovim(SkipNeovimReason.NOT_VIM_TESTING) + fun `test changing guicursor option updates caret immediately`() { + configureByText("I found it in a legendary land") + enterCommand("set guicursor=n:hor22") + assertCaretVisualAttributes(CaretVisualAttributes.Shape.UNDERSCORE, 0.22F) + } + + @TestWithoutNeovim(SkipNeovimReason.NOT_VIM_TESTING) + fun `test changing guicursor option invalidates caches correctly`() { + configureByText("I found it in a legendary land") + typeText(parseKeys("i")) + assertCaretVisualAttributes(CaretVisualAttributes.Shape.BAR, 0.25F) + typeText(parseKeys("")) + enterCommand("set guicursor=i:hor22") + typeText(parseKeys("i")) + assertCaretVisualAttributes(CaretVisualAttributes.Shape.UNDERSCORE, 0.22F) + } + + @TestWithoutNeovim(SkipNeovimReason.NOT_VIM_TESTING) + fun `test caret uses last matching guicursor option`() { + configureByText("I found it in a legendary land") + // This will give us three matching options for INSERT + enterCommand("set guicursor+=i:ver25") + enterCommand("set guicursor+=i:hor75") + typeText(parseKeys("i")) + assertCaretVisualAttributes(CaretVisualAttributes.Shape.UNDERSCORE, 0.75F) + } + + @TestWithoutNeovim(SkipNeovimReason.NOT_VIM_TESTING) + fun `test 'all' guicursor option`() { + configureByText("I found it in a legendary land") + enterCommand("set guicursor+=a:ver25") + assertCaretVisualAttributes(CaretVisualAttributes.Shape.BAR, 0.25F) + } + + @TestWithoutNeovim(SkipNeovimReason.NOT_VIM_TESTING) + fun `test 'all' guicursor option can be overridden`() { + configureByText("I found it in a legendary land") + // A specific entry added after "all" takes precedence + enterCommand("set guicursor+=a:ver25") + enterCommand("set guicursor+=i:hor75") + typeText(parseKeys("i")) + assertCaretVisualAttributes(CaretVisualAttributes.Shape.UNDERSCORE, 0.75F) + } + + @TestWithoutNeovim(SkipNeovimReason.NOT_VIM_TESTING) + fun `test block caret setting overrides guicursor`() { + val originalValue = EditorSettingsExternalizable.getInstance().isBlockCursor + EditorSettingsExternalizable.getInstance().isBlockCursor = true + try { + configureByText("I found it in a legendary land") + typeText(parseKeys("i")) + assertCaretVisualAttributes(CaretVisualAttributes.Shape.BLOCK, 1.0F) + } + finally { + EditorSettingsExternalizable.getInstance().isBlockCursor = originalValue + } + } + + private fun assertCaretVisualAttributes(expectedShape: CaretVisualAttributes.Shape, expectedThickness: Float) { + assertCaretVisualAttributes(myFixture.editor.caretModel.primaryCaret, expectedShape, expectedThickness) + } + + private fun assertCaretVisualAttributes(caret: Caret, expectedShape: CaretVisualAttributes.Shape, expectedThickness: Float) { + val visualAttributes = caret.visualAttributes + assertEquals(expectedShape, visualAttributes.shape) + assertEquals(expectedThickness, visualAttributes.thickness) + } +} diff --git a/test/org/jetbrains/plugins/ideavim/option/BoundedStringListOptionTest.kt b/test/org/jetbrains/plugins/ideavim/option/BoundedStringListOptionTest.kt new file mode 100644 index 0000000000..5854d9f925 --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/option/BoundedStringListOptionTest.kt @@ -0,0 +1,95 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2021 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.jetbrains.plugins.ideavim.option + +import com.maddyhome.idea.vim.option.BoundedStringListOption +import junit.framework.TestCase + +class BoundedStringListOptionTest : TestCase() { + private val option = + BoundedStringListOption( + "myOpt", "myOpt", arrayOf("Monday", "Tuesday"), + arrayOf("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday") + ) + + fun `test set valid list`() { + option.set("Thursday,Friday") + assertEquals("Thursday,Friday", option.value) + } + + fun `test set list with invalid value`() { + option.set("Blue") + assertEquals("Monday,Tuesday", option.value) + } + + fun `test append single item`() { + option.append("Wednesday") + assertEquals("Monday,Tuesday,Wednesday", option.value) + } + + fun `test append invalid item`() { + option.append("Blue") + assertEquals("Monday,Tuesday", option.value) + } + + fun `test append list`() { + option.append("Wednesday,Thursday") + assertEquals("Monday,Tuesday,Wednesday,Thursday", option.value) + } + + fun `test append list with invalid item`() { + option.append("Wednesday,Blue") + assertEquals("Monday,Tuesday", option.value) + } + + fun `test prepend item`() { + option.prepend("Wednesday") + assertEquals("Wednesday,Monday,Tuesday", option.value) + } + + fun `test prepend invalid item`() { + option.prepend("Blue") + assertEquals("Monday,Tuesday", option.value) + } + + fun `test prepend list`() { + option.prepend("Wednesday,Thursday") + assertEquals("Wednesday,Thursday,Monday,Tuesday", option.value) + } + + fun `test prepend list with invalid item`() { + option.prepend("Wednesday,Blue") + assertEquals("Monday,Tuesday", option.value) + } + + fun `test remove item`() { + option.remove("Monday") + assertEquals("Tuesday", option.value) + } + + fun `test remove list`() { + option.remove("Monday,Tuesday") + assertEquals("", option.value) + } + + fun `test remove list with invalid value`() { + option.remove("Monday,Blue") + assertEquals("Monday,Tuesday", option.value) + } +} diff --git a/test/org/jetbrains/plugins/ideavim/option/GuiCursorOptionTest.kt b/test/org/jetbrains/plugins/ideavim/option/GuiCursorOptionTest.kt new file mode 100644 index 0000000000..94daea3a3b --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/option/GuiCursorOptionTest.kt @@ -0,0 +1,149 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2021 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.jetbrains.plugins.ideavim.option + +import com.maddyhome.idea.vim.ex.ExException +import com.maddyhome.idea.vim.helper.enumSetOf +import com.maddyhome.idea.vim.option.GuiCursorMode +import com.maddyhome.idea.vim.option.GuiCursorOption +import com.maddyhome.idea.vim.option.GuiCursorOptionData +import com.maddyhome.idea.vim.option.GuiCursorType +import com.maddyhome.idea.vim.option.OptionsManager +import org.jetbrains.plugins.ideavim.VimTestCase + +class GuiCursorOptionTest : VimTestCase() { + private lateinit var option: GuiCursorOption + + override fun setUp() { + super.setUp() + option = OptionsManager.guicursor + } + + @Suppress("SpellCheckingInspection") + fun `test parses default values`() { + val values = option.values() + assertEquals(6, values.size) + + assertEquals(enumSetOf(GuiCursorMode.NORMAL, GuiCursorMode.VISUAL, GuiCursorMode.CMD_LINE), values[0]!!.modes) + assertEquals(GuiCursorType.BLOCK, values[0]!!.attributes.type) + assertEquals("Cursor", values[0]!!.attributes.highlightGroup) + assertEquals("lCursor", values[0]!!.attributes.lmapHighlightGroup) + + assertEquals(enumSetOf(GuiCursorMode.VISUAL_EXCLUSIVE), values[1]!!.modes) + assertEquals(GuiCursorType.VER, values[1]!!.attributes.type) + assertEquals(35, values[1]!!.attributes.thickness) + assertEquals("Cursor", values[1]!!.attributes.highlightGroup) + assertEquals("", values[1]!!.attributes.lmapHighlightGroup) + + assertEquals(enumSetOf(GuiCursorMode.OP_PENDING), values[2]!!.modes) + assertEquals(GuiCursorType.HOR, values[2]!!.attributes.type) + assertEquals(50, values[2]!!.attributes.thickness) + assertEquals("Cursor", values[2]!!.attributes.highlightGroup) + assertEquals("", values[2]!!.attributes.lmapHighlightGroup) + + assertEquals(enumSetOf(GuiCursorMode.INSERT, GuiCursorMode.CMD_LINE_INSERT), values[3]!!.modes) + assertEquals(GuiCursorType.VER, values[3]!!.attributes.type) + assertEquals(25, values[3]!!.attributes.thickness) + assertEquals("Cursor", values[3]!!.attributes.highlightGroup) + assertEquals("lCursor", values[3]!!.attributes.lmapHighlightGroup) + + assertEquals(enumSetOf(GuiCursorMode.REPLACE, GuiCursorMode.CMD_LINE_REPLACE), values[4]!!.modes) + assertEquals(GuiCursorType.HOR, values[4]!!.attributes.type) + assertEquals(20, values[4]!!.attributes.thickness) + assertEquals("Cursor", values[4]!!.attributes.highlightGroup) + assertEquals("lCursor", values[4]!!.attributes.lmapHighlightGroup) + + assertEquals(enumSetOf(GuiCursorMode.SHOW_MATCH), values[5]!!.modes) + assertEquals(GuiCursorType.BLOCK, values[5]!!.attributes.type) + assertEquals("Cursor", values[5]!!.attributes.highlightGroup) + assertEquals("", values[5]!!.attributes.lmapHighlightGroup) + assertEquals(3, values[5]!!.attributes.blinkModes.size) + assertEquals("blinkwait175", values[5]!!.attributes.blinkModes[0]) + assertEquals("blinkoff150", values[5]!!.attributes.blinkModes[1]) + assertEquals("blinkon175", values[5]!!.attributes.blinkModes[2]) + } + + fun `test ignores set with missing colon`() { + // E545: Missing colon: {value} + assertThrows(ExException::class.java, "E545: Missing colon: whatever") { option.set("whatever") } + assertEquals(GuiCursorOptionData.defaultValue, option.value) + } + + fun `test ignores set with invalid mode`() { + // E546: Illegal mode: {value} + assertThrows(ExException::class.java, "E546: Illegal mode: foo:block-Cursor") { option.set("foo:block-Cursor") } + assertEquals(GuiCursorOptionData.defaultValue, option.value) + } + + fun `test ignores set with invalid mode 2`() { + // E546: Illegal mode: {value} + assertThrows(ExException::class.java, "E546: Illegal mode: n-foo:block-Cursor") { option.set("n-foo:block-Cursor") } + assertEquals(GuiCursorOptionData.defaultValue, option.value) + } + + fun `test ignores set with zero thickness`() { + // E549: Illegal percentage + assertThrows(ExException::class.java, "E549: Illegal percentage: n:ver0-Cursor") { option.set("n:ver0-Cursor") } + assertEquals(GuiCursorOptionData.defaultValue, option.value) + } + + fun `test ignores set with invalid vertical cursor details`() { + // E548: Digit expected: {value} + assertThrows(ExException::class.java, "E548: Digit expected: n:ver-Cursor") { option.set("n:ver-Cursor") } + assertEquals(GuiCursorOptionData.defaultValue, option.value) + } + + fun `test simple string means block caret and highlight group`() { + option.set("n:MyHighlightGroup") + val values = option.values() + assertEquals(1, values.size) + assertEquals(enumSetOf(GuiCursorMode.NORMAL), values[0]!!.modes) + assertEquals(GuiCursorType.BLOCK, values[0]!!.attributes.type) + assertEquals("MyHighlightGroup", values[0]!!.attributes.highlightGroup) + assertEquals("", values[0]!!.attributes.lmapHighlightGroup) + } + + fun `test get effective values`() { + option.set("n:hor20-Cursor,i:hor50,a:ver25,n:ver75") + val attributes = option.getAttributes(GuiCursorMode.NORMAL) + assertEquals(GuiCursorType.VER, attributes.type) + assertEquals(75, attributes.thickness) + assertEquals("Cursor", attributes.highlightGroup) + } + + fun `test get effective values 2`() { + option.set("n:hor20-Cursor,i:hor50,a:ver25,n:ver75") + val attributes = option.getAttributes(GuiCursorMode.INSERT) + assertEquals(GuiCursorType.VER, attributes.type) + assertEquals(25, attributes.thickness) + } + + fun `test get effective values on update`() { + option.set("n:hor20-Cursor") + var attributes = option.getAttributes(GuiCursorMode.NORMAL) + assertEquals(GuiCursorType.HOR, attributes.type) + assertEquals(20, attributes.thickness) + assertEquals("Cursor", attributes.highlightGroup) + option.append("n:ver75-OtherCursor") + attributes = option.getAttributes(GuiCursorMode.NORMAL) + assertEquals(GuiCursorType.VER, attributes.type) + assertEquals(75, attributes.thickness) + assertEquals("OtherCursor", attributes.highlightGroup) + } +} diff --git a/test/org/jetbrains/plugins/ideavim/option/ListOptionTest.kt b/test/org/jetbrains/plugins/ideavim/option/StringListOptionTest.kt similarity index 87% rename from test/org/jetbrains/plugins/ideavim/option/ListOptionTest.kt rename to test/org/jetbrains/plugins/ideavim/option/StringListOptionTest.kt index 9e375b42f6..37e81fd150 100644 --- a/test/org/jetbrains/plugins/ideavim/option/ListOptionTest.kt +++ b/test/org/jetbrains/plugins/ideavim/option/StringListOptionTest.kt @@ -18,17 +18,17 @@ package org.jetbrains.plugins.ideavim.option -import com.maddyhome.idea.vim.option.ListOption +import com.maddyhome.idea.vim.option.StringListOption import org.jetbrains.plugins.ideavim.SkipNeovimReason import org.jetbrains.plugins.ideavim.TestWithoutNeovim import org.junit.Test import kotlin.test.assertEquals -class ListOptionTest { +class StringListOptionTest { @Test @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING) fun `append existing value`() { - val option = ListOption("myOpt", "myOpt", emptyArray(), null) + val option = StringListOption("myOpt", "myOpt", emptyArray()) option.append("123") option.append("456") @@ -40,7 +40,7 @@ class ListOptionTest { @Test @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING) fun `prepend existing value`() { - val option = ListOption("myOpt", "myOpt", emptyArray(), null) + val option = StringListOption("myOpt", "myOpt", emptyArray()) option.append("456") option.append("123") diff --git a/test/ui/UiTests.kt b/test/ui/UiTests.kt index e8bc134a42..6e64ec1582 100644 --- a/test/ui/UiTests.kt +++ b/test/ui/UiTests.kt @@ -185,13 +185,13 @@ class UiTests { remoteRobot.invokeActionJs("SurroundWith") editor.keyboard { enter() } - assertFalse(editor.isBlockCursor) +// assertFalse(editor.isBlockCursor) editor.keyboard { enterText("true") escape() } - assertTrue(editor.isBlockCursor) +// assertTrue(editor.isBlockCursor) editor.keyboard { enterText("h") enterText("v") diff --git a/test/ui/pages/Editor.kt b/test/ui/pages/Editor.kt index d628a8f150..eaa8490d6c 100644 --- a/test/ui/pages/Editor.kt +++ b/test/ui/pages/Editor.kt @@ -55,7 +55,9 @@ class Editor( get() = callJs("component.getEditor().getCaretModel().getOffset()", runInEdt = true) val isBlockCursor: Boolean - get() = callJs("component.getEditor().getSettings().isBlockCursor()", true) +// get() = callJs("component.getEditor().getSettings().isBlockCursor()", true) + // Doesn't work at the moment because remote robot can't resolve classes from a plugin classloader + get() = callJs("com.maddyhome.idea.vim.helper.CaretVisualAttributesHelperKt.hasBlockOrUnderscoreCaret(component.getEditor())", true) fun injectText(text: String) { runJs( diff --git a/test/ui/utils/Utils.kt b/test/ui/utils/Utils.kt index 35df07463f..17c0bb2595 100644 --- a/test/ui/utils/Utils.kt +++ b/test/ui/utils/Utils.kt @@ -21,7 +21,6 @@ package ui.utils import com.intellij.remoterobot.RemoteRobot import com.intellij.remoterobot.fixtures.Fixture import com.intellij.remoterobot.fixtures.dataExtractor.RemoteText -import com.intellij.remoterobot.utils.waitFor import org.assertj.swing.core.MouseButton import ui.pages.Editor import java.awt.Point @@ -44,9 +43,9 @@ fun RemoteText.moveMouseTo(goal: RemoteText, editor: Editor): Boolean { this.moveMouse() editor.runJs("robot.pressMouse(MouseButton.LEFT_BUTTON)") goal.moveMouse() - val caretDuringDragging = editor.isBlockCursor + val caretDuringDragging = false/*editor.isBlockCursor*/ editor.runJs("robot.releaseMouse(MouseButton.LEFT_BUTTON)") - waitFor { editor.isBlockCursor } +// waitFor { editor.isBlockCursor } return caretDuringDragging } @@ -55,9 +54,9 @@ fun RemoteText.moveMouseWithDelayTo(goal: RemoteText, editor: Editor, delay: Lon editor.runJs("robot.pressMouse(MouseButton.LEFT_BUTTON)") goal.moveMouse() Thread.sleep(delay) - val caretDuringDragging = editor.isBlockCursor + val caretDuringDragging = false/*editor.isBlockCursor*/ editor.runJs("robot.releaseMouse(MouseButton.LEFT_BUTTON)") - waitFor { editor.isBlockCursor } +// waitFor { editor.isBlockCursor } return caretDuringDragging } @@ -104,7 +103,7 @@ fun RemoteText.moveMouseForthAndBack(middle: RemoteText, editor: Editor) { }) """ ) - waitFor { editor.isBlockCursor } +// waitFor { editor.isBlockCursor } } fun String.escape(): String = this.replace("\n", "\\n")