diff --git a/.editorconfig b/.editorconfig index 11a2a3b48..f5bb74985 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,3 +3,7 @@ root = true [*.java] indent_size = 2 indent_style = space + +[*.kt] +indent_size = 2 +indent_style = space diff --git a/AUTHORS.md b/AUTHORS.md index 8e89cb989..80e331559 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -66,6 +66,10 @@ Contributors: * [gecko655](mailto:aqwsedrft1234@yahoo.co.jp) * [Daniele Megna](mailto:megna.dany@gmail.com) * [Andrew Potter](mailto:apottere@gmail.com) +* [Romain Gautier](mailto:romain.gautier@nimamoh.net) +* [Elliot Courant](mailto:elliot.courant@wheniwork.com) +* [Simon Rainer](mailto:simon.rainer@fau.de) +* [Michael Ziwisky](mailto:mziwisky@instructure.com) If you are a contributor and your name is not listed here, feel free to contact the maintainer. diff --git a/CHANGES.md b/CHANGES.md index df20e630c..caddd64f5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -38,6 +38,18 @@ To Be Released * [VIM-607](https://youtrack.jetbrains.com/issue/VIM-607) Fix memory leaks * [VIM-1546](https://youtrack.jetbrains.com/issue/VIM-1546) Storing TAB key as input * [VIM-1231](https://youtrack.jetbrains.com/issue/VIM-1231) Get indent from PsiFile +* [VIM-1633](https://youtrack.jetbrains.com/issue/VIM-1633) Fixed sequential text object commands in visual mode +* [VIM-1105](https://youtrack.jetbrains.com/issue/VIM-1105) Added the `:command` command +* [VIM-1090](https://youtrack.jetbrains.com/issue/VIM-1090) Fixed tag motion with duplicate tags +* [VIM-1644](https://youtrack.jetbrains.com/issue/VIM-1644) Fixed repeat with visual mode +* Fixed invoking IDE actions instead of command line actions with same shortcuts +* [VIM-1550](https://youtrack.jetbrains.com/issue/VIM-1550) Fixed leaving command line mode on backspace +* Fix insert position of `<C-R>` in ex commands +* Command line editing caret shape and insert digraph/register feedback +* [VIM-1419](https://youtrack.jetbrains.com/issue/VIM-1419), [VIM-1493](https://youtrack.jetbrains.com/issue/VIM-1493) Correctly set focus when handling cmode mapping +* Fix incorrect handling of subsequent key strokes after ex command line loses focus +* [VIM-1240](https://youtrack.jetbrains.com/issue/VIM-1240) Improve UI of ex command line and output panel +* [VIM-1485](https://youtrack.jetbrains.com/issue/VIM-1485) Remove incorrect gap between ex command line label and text 0.51, 2019-02-12 ---------------- diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9074ae57e..7e3f291ae 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -66,7 +66,7 @@ in the issue tracker. ### Copyright -1. Go to `Preferences | Appearance & Behavior | Scopes`, press "+" button, `local`. +1. Go to `Preferences | Appearance & Behavior | Scopes`, press "+" button, `Shared`. Name: Copyright scope Pattern: `file[IdeaVIM.main]:com//*||file[IdeaVIM.test]:*/` diff --git a/index.txt b/index.txt deleted file mode 100644 index af7097062..000000000 --- a/index.txt +++ /dev/null @@ -1,191 +0,0 @@ -*index.txt* - - - IDEAVIM REFERENCE MANUAL based on Vim Reference Manual - - *index* -This file contains a list of commands that are covered with tests, for each -mode, with a tag and a short description. The lists are sorted on ASCII value. - -Tip: When looking for certain functionality, use a search command. E.g., -to look for deleting something, use: "/delete". - -1. Insert mode |insert-index| -2. Normal mode |normal-index| - 2.1. Text objects |objects| - 2.3. Square bracket commands |[| -3. Visual mode |visual-index| -5. EX commands |ex-cmd-index| - -============================================================================== -1. Insert mode *insert-index* - -tag char action in Insert mode ~ ------------------------------------------------------------------------ -|i_CTRL-K| CTRL-K {char1} {char2} - enter digraph -|i_CTRL-O| CTRL-O execute a single command and return to insert - mode -|i_CTRL-R| CTRL-R {0-9a-z"%#*:=} - insert the contents of a register -|i_CTRL-W| CTRL-W delete word before the cursor - -============================================================================== -2. Normal mode *normal-index* - -CHAR any non-blank character -WORD a sequence of non-blank characters -N a number entered before the command -{motion} a cursor movement command -Nmove the text that is moved over with a {motion} -SECTION a section that possibly starts with '}' instead of '{' - -note: 1 = cursor movement command; 2 = can be undone/redone - -tag char note action in Normal mode ~ ------------------------------------------------------------------------------- -|quote| "{a-zA-Z0-9.%#:-"} use register {a-zA-Z0-9.%#:-"} for next - delete, yank or put (uppercase to append) - ({.%#:} only work with put) -|%| % 1 find the next (curly/square) bracket on - this line and go to its match, or go to - matching comment bracket, or go to matching - preprocessor directive. -|/| /{pattern}<CR> 1 search forward for the Nth occurrence of - {pattern} -|count| 0 1 cursor to the first char of the line -|count| 1 prepend to command to give a count -|count| 2 " -|count| 3 " -|count| 4 " -|count| 5 " -|count| 6 " -|count| 7 " -|count| 8 " -|count| 9 " -|F| F{char} 1 cursor to the Nth occurrence of {char} to - the left -|O| O 2 begin a new line above the cursor and - insert text, repeat N times -|P| ["x]P 2 put the text [from buffer x] before the - cursor N times -|T| T{char} 1 cursor till after Nth occurrence of {char} - to the left -|Y| ["x]Y yank N lines [into buffer x]; synonym for - "yy" -|c| ["x]c{motion} 2 delete Nmove text [into buffer x] and start - insert -|cc| ["x]cc 2 delete N lines [into buffer x] and start -|d| ["x]d{motion} 2 delete Nmove text [into buffer x] -|f| f{char} 1 cursor to Nth occurrence of {char} to the - right -|i| i 2 insert text before the cursor N times -|p| ["x]p 2 put the text [from register x] after the - cursor N times -|q| q{0-9a-zA-Z"} record typed characters into named register - {0-9a-zA-Z"} (uppercase to append) -|q| q (while recording) stops recording -|t| t{char} 1 cursor till before Nth occurrence of {char} - to the right -|y| ["x]y{motion} yank Nmove text [into buffer x] -|yy| ["x]yy yank N lines [into buffer x] -|~| ~ 2 'tildeop' off: switch case of N characters - under cursor and move the cursor N - characters to the right - -============================================================================== -2.1 Text objects *objects* - -These can be used after an operator or in Visual mode to select an object. - -tag command action in op-pending and Visual mode ~ ------------------------------------------------------------------------------- -|v_aquote| a" double quoted string -|v_a'| a' single quoted string -|v_a(| a( same as ab -|v_a)| a) same as ab -|v_a<| a< "a <>" from '<' to the matching '>' -|v_a>| a> same as a< -|v_aB| aB "a Block" from "[{" to "]}" (with brackets) -|v_aW| aW "a WORD" (with white space) -|v_a[| a[ "a []" from '[' to the matching ']' -|v_a]| a] same as a[ -|v_a`| a` string in backticks -|v_ab| ab "a block" from "[(" to "])" (with braces) -|v_ap| ap "a paragraph" (with white space) -|v_as| as "a sentence" (with white space) -|v_aw| aw "a word" (with white space) -|v_a{| a{ same as aB -|v_a}| a} same as aB -|v_iquote| i" double quoted string without the quotes -|v_i'| i' single quoted string without the quotes -|v_i(| i( same as ib -|v_i)| i) same as ib -|v_i<| i< "inner <>" from '<' to the matching '>' -|v_i>| i> same as i< -|v_iB| iB "inner Block" from "[{" and "]}" -|v_iW| iW "inner WORD" -|v_i[| i[ "inner []" from '[' to the matching ']' -|v_i]| i] same as i[ -|v_i`| i` string in backticks without the backticks -|v_ib| ib "inner block" from "[(" to "])" -|v_ip| ip "inner paragraph" -|v_is| is "inner sentence" -|v_iw| iw "inner word" -|v_i{| i{ same as iB -|v_i}| i} same as iB - -============================================================================== -2.3 Square bracket commands *[* *]* - -tag char note action in Normal mode ~ ------------------------------------------------------------------------------- -|[(| [( 1 cursor N times back to unmatched '(' -|[{| [{ 1 cursor N times back to unmatched '{' -|])| ]) 1 cursor N times forward to unmatched ')' -|]}| ]} 1 cursor N times forward to unmatched '}' - -============================================================================== -2.4 Commands starting with 'g' *g* - -tag char note action in Normal mode ~ ------------------------------------------------------------------------------- -|gg| gg 1 cursor to line N, default first line -|gi| gi 2 like "i", but first move to the |'^| mark - -============================================================================== -3. Visual mode *visual-index* - -Most commands in Visual mode are the same as in Normal mode. The ones listed -here are those that are different. - -tag command note action in Visual mode ~ ------------------------------------------------------------------------------- -|v_y| y yank the highlighted area - -============================================================================== -4. Command-line editing *ex-edit-index* - -Get to the command-line with the ':', '!', '/' or '?' commands. -Normal characters are inserted at the current cursor position. -"Completion" below refers to context-sensitive completion. It will complete -file names, tags, commands etc. as appropriate. - -tag command action in Command-line editing mode ~ ------------------------------------------------------------------------------- -|c_CTRL-R| CTRL-R {0-9a-z"%#*:= CTRL-F CTRL-P CTRL-W CTRL-A} - insert the contents of a register or object - under the cursor as if typed - -============================================================================== -5. EX commands *ex-cmd-index* - -This is a brief but complete listing of all the ":" commands, without -mentioning any arguments. The optional part of the command name is inside []. -The commands are sorted on the non-optional part of their name. - -tag command action ~ ------------------------------------------------------------------------------- -|:display| :di[splay] display registers -|:registers| :reg[isters] display the contents of registers -|:substitute| :s[ubstitute] find and replace text \ No newline at end of file diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml index b85019bb0..1290590a2 100644 --- a/resources/META-INF/plugin.xml +++ b/resources/META-INF/plugin.xml @@ -4,6 +4,7 @@ <change-notes><![CDATA[ <p>To be released:</p> <ul> + <li>Support :command command</li> <li>Support :shell command</li> <li>Support :tabnext and :tabprevious commands</li> <li>Commentary extension</li> @@ -67,6 +68,9 @@ <component> <implementation-class>com.maddyhome.idea.vim.VimPlugin</implementation-class> </component> + <component> + <implementation-class>com.maddyhome.idea.vim.VimLocalConfig</implementation-class> + </component> </application-components> <extensionPoints> @@ -387,10 +391,7 @@ <action id="VimPlaybackLastRegister" class="com.maddyhome.idea.vim.action.macro.PlaybackLastRegisterAction" text="Playback Last Register"/> <!-- Command Line --> - <action id="VimExBackspace" class="com.maddyhome.idea.vim.action.ex.BackspaceAction" text="Backspace"/> <action id="VimProcessExEntry" class="com.maddyhome.idea.vim.action.ex.ProcessExEntryAction" text="Process Ex Entry"/> - <action id="VimProcessExKey" class="com.maddyhome.idea.vim.action.ex.ProcessExKeyAction" text="Process Ex Key"/> - <action id="VimCancelExEntry" class="com.maddyhome.idea.vim.action.ex.CancelExEntryAction" text="Cancel Ex Entry"/> <!-- Other --> <action id="VimLastSearchReplace" class="com.maddyhome.idea.vim.action.change.change.ChangeLastSearchReplaceAction" text="Repeat Last :s"/> @@ -403,7 +404,7 @@ <action id="VimUndo" class="com.maddyhome.idea.vim.action.change.UndoAction" text="Undo"/> <!-- Internal --> - <action id="VimInternalAddInlays" class="com.maddyhome.idea.vim.action.internal.AddInlaysAction" text="Vim (internal) add test inlays" internal="true"/> + <action id="VimInternalAddInlays" class="com.maddyhome.idea.vim.action.internal.AddInlaysAction" text="Add test inlays | IdeaVim internal" internal="true"/> <!-- Keys --> <action id="VimShortcutKeyAction" class="com.maddyhome.idea.vim.action.VimShortcutKeyAction" text="Shortcuts"/> diff --git a/src/com/maddyhome/idea/vim/KeyHandler.java b/src/com/maddyhome/idea/vim/KeyHandler.java index 53d48cf1b..22ae5104a 100644 --- a/src/com/maddyhome/idea/vim/KeyHandler.java +++ b/src/com/maddyhome/idea/vim/KeyHandler.java @@ -247,15 +247,15 @@ public class KeyHandler { final CommandState commandState = CommandState.getInstance(editor); commandState.stopMappingTimer(); - final List<KeyStroke> mappingKeys = commandState.getMappingKeys(); - final List<KeyStroke> fromKeys = new ArrayList<KeyStroke>(mappingKeys); - fromKeys.add(key); - final MappingMode mappingMode = commandState.getMappingMode(); if (MappingMode.NVO.contains(mappingMode) && (state != State.NEW_COMMAND || currentArg != Argument.Type.NONE)) { return false; } + final List<KeyStroke> mappingKeys = commandState.getMappingKeys(); + final List<KeyStroke> fromKeys = new ArrayList<KeyStroke>(mappingKeys); + fromKeys.add(key); + final KeyMapping mapping = VimPlugin.getKey().getKeyMapping(mappingMode); final MappingInfo currentMappingInfo = mapping.get(fromKeys); final MappingInfo prevMappingInfo = mapping.get(mappingKeys); diff --git a/src/com/maddyhome/idea/vim/RegisterActions.java b/src/com/maddyhome/idea/vim/RegisterActions.java index 4bcb23614..3c391cbec 100644 --- a/src/com/maddyhome/idea/vim/RegisterActions.java +++ b/src/com/maddyhome/idea/vim/RegisterActions.java @@ -31,11 +31,11 @@ import javax.swing.*; import java.awt.event.KeyEvent; import java.util.EnumSet; -public class RegisterActions { +class RegisterActions { /** * Register all the key/action mappings for the plugin. */ - public static void registerActions() { + static void registerActions() { registerVimCommandActions(); registerInsertModeActions(); diff --git a/src/com/maddyhome/idea/vim/VimLocalConfig.kt b/src/com/maddyhome/idea/vim/VimLocalConfig.kt new file mode 100644 index 000000000..581684e19 --- /dev/null +++ b/src/com/maddyhome/idea/vim/VimLocalConfig.kt @@ -0,0 +1,38 @@ +package com.maddyhome.idea.vim + +import com.intellij.configurationStore.APP_CONFIG +import com.intellij.openapi.components.PersistentStateComponent +import com.intellij.openapi.components.RoamingType +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.maddyhome.idea.vim.VimPlugin.STATE_VERSION +import org.jdom.Element + +/** + * @author Alex Plate + */ + +@State(name = "VimLocalSettings", + storages = [Storage("$APP_CONFIG$/vim_local_settings.xml", roamingType = RoamingType.DISABLED)]) +class VimLocalConfig : PersistentStateComponent<Element> { + override fun getState(): Element { + val element = Element("ideavim-local") + + val state = Element("state") + state.setAttribute("version", Integer.toString(STATE_VERSION)) + element.addContent(state) + + VimPlugin.getMark().saveData(element) + VimPlugin.getRegister().saveData(element) + VimPlugin.getSearch().saveData(element) + VimPlugin.getHistory().saveData(element) + return element + } + + override fun loadState(state: Element) { + VimPlugin.getMark().readData(state) + VimPlugin.getRegister().readData(state) + VimPlugin.getSearch().readData(state) + VimPlugin.getHistory().readData(state) + } +} \ No newline at end of file diff --git a/src/com/maddyhome/idea/vim/VimPlugin.java b/src/com/maddyhome/idea/vim/VimPlugin.java index a02ff6d4e..be2532b9b 100644 --- a/src/com/maddyhome/idea/vim/VimPlugin.java +++ b/src/com/maddyhome/idea/vim/VimPlugin.java @@ -84,7 +84,7 @@ import java.util.concurrent.TimeUnit; */ @State( name = "VimSettings", - storages = {@Storage(file = "$APP_CONFIG$/vim_settings.xml")}) + storages = {@Storage("$APP_CONFIG$/vim_settings.xml")}) public class VimPlugin implements ApplicationComponent, PersistentStateComponent<Element> { private static final String IDEAVIM_COMPONENT_NAME = "VimPlugin"; private static final String IDEAVIM_PLUGIN_ID = "IdeaVIM"; @@ -92,7 +92,9 @@ public class VimPlugin implements ApplicationComponent, PersistentStateComponent public static final String IDEAVIM_NOTIFICATION_ID = "ideavim"; public static final String IDEAVIM_STICKY_NOTIFICATION_ID = "ideavim-sticky"; public static final String IDEAVIM_NOTIFICATION_TITLE = "IdeaVim"; - public static final int STATE_VERSION = 4; + public static final int STATE_VERSION = 5; + + private static long lastBeepTimeMillis; private boolean error = false; @@ -106,6 +108,7 @@ public class VimPlugin implements ApplicationComponent, PersistentStateComponent @NotNull private final MotionGroup motion; @NotNull private final ChangeGroup change; + @NotNull private final CommandGroup command; @NotNull private final MarkGroup mark; @NotNull private final RegisterGroup register; @NotNull private final FileGroup file; @@ -124,6 +127,7 @@ public class VimPlugin implements ApplicationComponent, PersistentStateComponent public VimPlugin() { motion = new MotionGroup(); change = new ChangeGroup(); + command = new CommandGroup(); mark = new MarkGroup(); register = new RegisterGroup(); file = new FileGroup(); @@ -201,10 +205,6 @@ public class VimPlugin implements ApplicationComponent, PersistentStateComponent state.setAttribute("enabled", Boolean.toString(enabled)); element.addContent(state); - mark.saveData(element); - register.saveData(element); - search.saveData(element); - history.saveData(element); key.saveData(element); editor.saveData(element); @@ -227,10 +227,13 @@ public class VimPlugin implements ApplicationComponent, PersistentStateComponent previousKeyMap = state.getAttributeValue("keymap"); } - mark.readData(element); - register.readData(element); - search.readData(element); - history.readData(element); + if (previousStateVersion > 0 && previousStateVersion < 5) { + // Migrate settings from 4 to 5 version + mark.readData(element); + register.readData(element); + search.readData(element); + history.readData(element); + } key.readData(element); editor.readData(element); } @@ -245,6 +248,9 @@ public class VimPlugin implements ApplicationComponent, PersistentStateComponent return getInstance().change; } + @NotNull + public static CommandGroup getCommand() { return getInstance().command; } + @NotNull public static MarkGroup getMark() { return getInstance().mark; @@ -359,7 +365,12 @@ public class VimPlugin implements ApplicationComponent, PersistentStateComponent getInstance().error = true; } else if (!Options.getInstance().isSet("visualbell")) { - Toolkit.getDefaultToolkit().beep(); + // Vim only allows a beep once every half second - :help 'visualbell' + final long currentTimeMillis = System.currentTimeMillis(); + if (currentTimeMillis - lastBeepTimeMillis > 500) { + Toolkit.getDefaultToolkit().beep(); + lastBeepTimeMillis = currentTimeMillis; + } } } diff --git a/src/com/maddyhome/idea/vim/VimTypedActionHandler.java b/src/com/maddyhome/idea/vim/VimTypedActionHandler.java index 487bf6fe7..c7b11a6be 100644 --- a/src/com/maddyhome/idea/vim/VimTypedActionHandler.java +++ b/src/com/maddyhome/idea/vim/VimTypedActionHandler.java @@ -42,11 +42,9 @@ import java.awt.event.KeyEvent; public class VimTypedActionHandler implements TypedActionHandlerEx { private static final Logger logger = Logger.getInstance(VimTypedActionHandler.class.getName()); - private final TypedActionHandler origHandler; @NotNull private final KeyHandler handler; - public VimTypedActionHandler(TypedActionHandler origHandler) { - this.origHandler = origHandler; + VimTypedActionHandler(TypedActionHandler origHandler) { handler = KeyHandler.getInstance(); handler.setOriginalHandler(origHandler); } @@ -57,7 +55,7 @@ public class VimTypedActionHandler implements TypedActionHandlerEx { handler.beforeHandleKey(editor, KeyStroke.getKeyStroke(charTyped), context, plan); } else { - TypedActionHandler originalHandler = KeyHandler.getInstance().getOriginalHandler(); + TypedActionHandler originalHandler = handler.getOriginalHandler(); if (originalHandler instanceof TypedActionHandlerEx) { ((TypedActionHandlerEx)originalHandler).beforeExecute(editor, charTyped, context, plan); } @@ -76,6 +74,7 @@ public class VimTypedActionHandler implements TypedActionHandlerEx { } else { try (final VimListenerSuppressor ignored = SelectionVimListenerSuppressor.INSTANCE.lock()) { + TypedActionHandler origHandler = handler.getOriginalHandler(); origHandler.execute(editor, charTyped, context); } } diff --git a/src/com/maddyhome/idea/vim/action/VimPluginToggleAction.java b/src/com/maddyhome/idea/vim/action/VimPluginToggleAction.java index c72afb1cb..8f1d4b469 100644 --- a/src/com/maddyhome/idea/vim/action/VimPluginToggleAction.java +++ b/src/com/maddyhome/idea/vim/action/VimPluginToggleAction.java @@ -22,6 +22,7 @@ import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.ToggleAction; import com.intellij.openapi.project.DumbAware; import com.maddyhome.idea.vim.VimPlugin; +import org.jetbrains.annotations.NotNull; /** * This class is used to handle the Vim Plugin enabled/disabled toggle. This is most likely used as a menu option @@ -34,7 +35,7 @@ public class VimPluginToggleAction extends ToggleAction implements DumbAware { * @param event The event that triggered the action * @return true if the toggle is on, false if off */ - public boolean isSelected(AnActionEvent event) { + public boolean isSelected(@NotNull AnActionEvent event) { return VimPlugin.isEnabled(); } @@ -44,7 +45,7 @@ public class VimPluginToggleAction extends ToggleAction implements DumbAware { * @param event The event that triggered the action * @param b The new state - true is on, false is off */ - public void setSelected(AnActionEvent event, boolean b) { + public void setSelected(@NotNull AnActionEvent event, boolean b) { VimPlugin.setEnabled(b); } } diff --git a/src/com/maddyhome/idea/vim/action/ex/ProcessExEntryAction.java b/src/com/maddyhome/idea/vim/action/ex/ProcessExEntryAction.java index 9edb19add..205569796 100644 --- a/src/com/maddyhome/idea/vim/action/ex/ProcessExEntryAction.java +++ b/src/com/maddyhome/idea/vim/action/ex/ProcessExEntryAction.java @@ -27,6 +27,9 @@ import com.maddyhome.idea.vim.handler.EditorActionHandlerBase; import org.jetbrains.annotations.NotNull; /** + * Called by KeyHandler to process the contents of the ex entry panel + * <p> + * The mapping for this action means that the ex command is executed as a write action */ public class ProcessExEntryAction extends EditorAction { public ProcessExEntryAction() { diff --git a/src/com/maddyhome/idea/vim/action/ex/ProcessExKeyAction.java b/src/com/maddyhome/idea/vim/action/ex/ProcessExKeyAction.java deleted file mode 100644 index a32844fdb..000000000 --- a/src/com/maddyhome/idea/vim/action/ex/ProcessExKeyAction.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform - * Copyright (C) 2003-2019 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 <http://www.gnu.org/licenses/>. - */ - -package com.maddyhome.idea.vim.action.ex; - -import com.intellij.openapi.actionSystem.DataContext; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.editor.actionSystem.EditorAction; -import com.maddyhome.idea.vim.VimPlugin; -import com.maddyhome.idea.vim.command.Command; -import com.maddyhome.idea.vim.handler.EditorActionHandlerBase; -import org.jetbrains.annotations.NotNull; - -/** - */ -public class ProcessExKeyAction extends EditorAction { - public ProcessExKeyAction() { - super(new Handler()); - } - - private static class Handler extends EditorActionHandlerBase { - protected boolean execute(@NotNull Editor editor, @NotNull DataContext context, @NotNull Command cmd) { - return VimPlugin.getProcess().processExKey(editor, cmd.getKeys().get(0)); - } - } -} diff --git a/src/com/maddyhome/idea/vim/action/window/WindowDownAction.java b/src/com/maddyhome/idea/vim/action/window/WindowDownAction.java index 6b78c3146..16604e632 100644 --- a/src/com/maddyhome/idea/vim/action/window/WindowDownAction.java +++ b/src/com/maddyhome/idea/vim/action/window/WindowDownAction.java @@ -54,7 +54,7 @@ public class WindowDownAction extends VimCommandAction { @NotNull @Override public Set<List<KeyStroke>> getKeyStrokesSet() { - return parseKeysSet("<C-W>j", "<C-W><Down>"); + return parseKeysSet("<C-W>j", "<C-W><C-J>", "<C-W><Down>"); } @NotNull diff --git a/src/com/maddyhome/idea/vim/action/window/WindowLeftAction.java b/src/com/maddyhome/idea/vim/action/window/WindowLeftAction.java index 5cc13dd65..e7baf5eba 100644 --- a/src/com/maddyhome/idea/vim/action/window/WindowLeftAction.java +++ b/src/com/maddyhome/idea/vim/action/window/WindowLeftAction.java @@ -54,7 +54,7 @@ public class WindowLeftAction extends VimCommandAction { @NotNull @Override public Set<List<KeyStroke>> getKeyStrokesSet() { - return parseKeysSet("<C-W>h", "<C-W><Left>"); + return parseKeysSet("<C-W>h", "<C-W><C-H>", "<C-W><Left>"); } @NotNull diff --git a/src/com/maddyhome/idea/vim/action/window/WindowRightAction.java b/src/com/maddyhome/idea/vim/action/window/WindowRightAction.java index 0ffe4dc9b..8dbd1bbcb 100644 --- a/src/com/maddyhome/idea/vim/action/window/WindowRightAction.java +++ b/src/com/maddyhome/idea/vim/action/window/WindowRightAction.java @@ -54,7 +54,7 @@ public class WindowRightAction extends VimCommandAction { @NotNull @Override public Set<List<KeyStroke>> getKeyStrokesSet() { - return parseKeysSet("<C-W>l", "<C-W><Right>"); + return parseKeysSet("<C-W>l", "<C-W><C-L>", "<C-W><Right>"); } @NotNull diff --git a/src/com/maddyhome/idea/vim/action/window/WindowUpAction.java b/src/com/maddyhome/idea/vim/action/window/WindowUpAction.java index bbed5cdb1..b4f2b81b5 100644 --- a/src/com/maddyhome/idea/vim/action/window/WindowUpAction.java +++ b/src/com/maddyhome/idea/vim/action/window/WindowUpAction.java @@ -54,7 +54,7 @@ public class WindowUpAction extends VimCommandAction { @NotNull @Override public Set<List<KeyStroke>> getKeyStrokesSet() { - return parseKeysSet("<C-W>k", "<C-W><Up>"); + return parseKeysSet("<C-W>k", "<C-W><C-K>", "<C-W><Up>"); } @NotNull diff --git a/src/com/maddyhome/idea/vim/common/Alias.kt b/src/com/maddyhome/idea/vim/common/Alias.kt new file mode 100644 index 000000000..873a9cf12 --- /dev/null +++ b/src/com/maddyhome/idea/vim/common/Alias.kt @@ -0,0 +1,75 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2019 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 <http://www.gnu.org/licenses/>. + */ + +package com.maddyhome.idea.vim.common + +import com.maddyhome.idea.vim.VimPlugin + +/** + * @author Elliot Courant + */ +class Alias( + private val minimumNumberOfArguments: Int, + private val maximumNumberOfArguments: Int, + val name: String, + val command: String +) { + val numberOfArguments = + when { + this.minimumNumberOfArguments == 0 && this.maximumNumberOfArguments == 0 -> "0" // No arguments + this.minimumNumberOfArguments == 0 && this.maximumNumberOfArguments == -1 -> "*" // Any number of arguments + this.minimumNumberOfArguments == 0 && this.maximumNumberOfArguments == 1 -> "?" // Zero or one argument + this.minimumNumberOfArguments == 1 && this.maximumNumberOfArguments == -1 -> "+" // One or more arguments + else -> this.minimumNumberOfArguments.toString() // Specified number of arguments + } + + private companion object { + const val LessThan = "<lt>" + const val Count = "<count>" + const val Arguments = "<args>" + const val QuotedArguments = "<q-args>" + } + + fun getCommand(input: String, count: Int): String { + if (this.maximumNumberOfArguments == 0 && this.maximumNumberOfArguments == 0) { + return this.command + } + var compiledCommand = this.command + val cleanedInput = input.trim().removePrefix(name).trim() + if (minimumNumberOfArguments > 0 && cleanedInput.isEmpty()) { + VimPlugin.showMessage("E471: Argument required") + VimPlugin.indicateError() + return "" + } + for (symbol in arrayOf(Count, Arguments, QuotedArguments)) { + compiledCommand = compiledCommand.replace(symbol, when (symbol) { + Count -> arrayOf(count.toString()) + Arguments -> arrayOf(cleanedInput) + QuotedArguments -> arrayOf("'$cleanedInput'") + else -> emptyArray() + }.joinToString(", ")) + } + + // We want to escape <lt> after we've dropped in all of our args, if they are + // using <lt> its because they are escaping something that we don't want to handle + // yet. + compiledCommand = compiledCommand.replace(LessThan, "<") + + return compiledCommand + } +} \ No newline at end of file diff --git a/src/com/maddyhome/idea/vim/ex/CommandParser.java b/src/com/maddyhome/idea/vim/ex/CommandParser.java index 6b768f5b6..98eae6da4 100644 --- a/src/com/maddyhome/idea/vim/ex/CommandParser.java +++ b/src/com/maddyhome/idea/vim/ex/CommandParser.java @@ -40,6 +40,7 @@ import java.util.regex.Pattern; * executes Ex commands entered by the user. */ public class CommandParser { + private static final int MAX_RECURSION = 100; public static final int RES_EMPTY = 1; public static final int RES_ERROR = 1; public static final int RES_READONLY = 1; @@ -73,7 +74,10 @@ public class CommandParser { new ActionListHandler(); new AsciiHandler(); new CmdFilterHandler(); + new CmdHandler(); + new CmdClearHandler(); new CopyTextHandler(); + new DelCmdHandler(); new DeleteLinesHandler(); new DigraphHandler(); new DumpLineHandler(); @@ -165,14 +169,51 @@ public class CommandParser { */ public int processCommand(@NotNull Editor editor, @NotNull DataContext context, @NotNull String cmd, int count) throws ExException { + return processCommand(editor, context, cmd, count, MAX_RECURSION); + } + + /** + * Parse and execute an Ex command entered by the user + * + * @param editor The editor to run the command in + * @param context The data context + * @param cmd The text entered by the user + * @param count The count entered before the colon + * @param aliasCountdown A countdown for the depth of alias recursion that is allowed + * @return A bitwise collection of flags, if any, from the result of running the command. + * @throws ExException if any part of the command is invalid or unknown + */ + private int processCommand(@NotNull Editor editor, @NotNull DataContext context, @NotNull String cmd, + int count, int aliasCountdown) throws ExException { // Nothing entered int result = 0; if (cmd.length() == 0) { return result | RES_EMPTY; } - // Save the command history - VimPlugin.getHistory().addEntry(HistoryGroup.COMMAND, cmd); + // Only save the command to the history if it is at the top of the stack. + // We don't want to save the aliases that will be executed, only the actual + // user input. + if (aliasCountdown == MAX_RECURSION) { + // Save the command history + VimPlugin.getHistory().addEntry(HistoryGroup.COMMAND, cmd); + } + + // If there is a command alias for the entered text, then process the alias and return that + // instead of the original command. + if (VimPlugin.getCommand().isAlias(cmd)) { + if (aliasCountdown > 0) { + String commandAlias = VimPlugin.getCommand().getAliasCommand(cmd, count); + if (commandAlias.isEmpty()) { + return result |= RES_ERROR; + } + return processCommand(editor, context, commandAlias, count, aliasCountdown - 1); + } else { + VimPlugin.showMessage("Recursion detected, maximum alias depth reached."); + VimPlugin.indicateError(); + return result |= RES_ERROR; + } + } // Parse the command final ExCommand command = parse(cmd); @@ -192,7 +233,7 @@ public class CommandParser { boolean ok = handler.process(editor, context, command, count); if (ok && !handler.getArgFlags().getFlags().contains(CommandHandler.Flag.DONT_SAVE_LAST)) { VimPlugin.getRegister().storeTextInternal(editor, new TextRange(-1, -1), cmd, - SelectionType.CHARACTER_WISE, ':', false); + SelectionType.CHARACTER_WISE, ':', false); } if (handler.getArgFlags().getFlags().contains(CommandHandler.Flag.DONT_REOPEN)) { diff --git a/src/com/maddyhome/idea/vim/ex/handler/ActionListHandler.kt b/src/com/maddyhome/idea/vim/ex/handler/ActionListHandler.kt index 966fef695..1566255a7 100644 --- a/src/com/maddyhome/idea/vim/ex/handler/ActionListHandler.kt +++ b/src/com/maddyhome/idea/vim/ex/handler/ActionListHandler.kt @@ -34,18 +34,20 @@ class ActionListHandler : CommandHandler( flags(RangeFlag.RANGE_FORBIDDEN, ArgumentFlag.ARGUMENT_OPTIONAL, DONT_REOPEN) ) { override fun execute(editor: Editor, context: DataContext, cmd: ExCommand): Boolean { - val lineSeparator = System.lineSeparator() + val lineSeparator = "\n" val searchPattern = cmd.argument.trim().toLowerCase().split("*") val actionManager = ActionManager.getInstance() val actions = actionManager.getActionIds("") - .filter { actionName -> searchPattern.all { it in actionName.toLowerCase() } } - .sortedWith(String.CASE_INSENSITIVE_ORDER).joinToString(lineSeparator) { actionName -> + .sortedWith(String.CASE_INSENSITIVE_ORDER) + .map { actionName -> val shortcuts = actionManager.getAction(actionName).shortcutSet.shortcuts.joinToString(" ") { if (it is KeyboardShortcut) StringHelper.toKeyNotation(it.firstKeyStroke) else it.toString() } if (shortcuts.isBlank()) actionName else "${actionName.padEnd(50)} $shortcuts" } + .filter { line -> searchPattern.all { it in line.toLowerCase() } } + .joinToString(lineSeparator) ExOutputModel.getInstance(editor).output("--- Actions ---$lineSeparator$actions") diff --git a/src/com/maddyhome/idea/vim/action/ex/BackspaceAction.java b/src/com/maddyhome/idea/vim/ex/handler/CmdClearHandler.kt similarity index 50% rename from src/com/maddyhome/idea/vim/action/ex/BackspaceAction.java rename to src/com/maddyhome/idea/vim/ex/handler/CmdClearHandler.kt index 55e93abbb..a462e85c6 100644 --- a/src/com/maddyhome/idea/vim/action/ex/BackspaceAction.java +++ b/src/com/maddyhome/idea/vim/ex/handler/CmdClearHandler.kt @@ -16,26 +16,22 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package com.maddyhome.idea.vim.action.ex; +package com.maddyhome.idea.vim.ex.handler -import com.intellij.openapi.actionSystem.DataContext; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.editor.actionSystem.EditorAction; -import com.maddyhome.idea.vim.VimPlugin; -import com.maddyhome.idea.vim.command.Command; -import com.maddyhome.idea.vim.handler.EditorActionHandlerBase; -import org.jetbrains.annotations.NotNull; +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.editor.Editor +import com.maddyhome.idea.vim.VimPlugin +import com.maddyhome.idea.vim.ex.CommandHandler +import com.maddyhome.idea.vim.ex.ExCommand +import com.maddyhome.idea.vim.ex.commands +import com.maddyhome.idea.vim.ex.flags -/** - */ -public class BackspaceAction extends EditorAction { - public BackspaceAction() { - super(new Handler()); - } - - private static class Handler extends EditorActionHandlerBase { - protected boolean execute(@NotNull Editor editor, @NotNull DataContext context, @NotNull Command cmd) { - return VimPlugin.getProcess().processExKey(editor, cmd.getKeys().get(0)); +class CmdClearHandler : CommandHandler( + commands("comc[lear]"), + flags(RangeFlag.RANGE_FORBIDDEN, ArgumentFlag.ARGUMENT_FORBIDDEN) +) { + override fun execute(editor: Editor, context: DataContext, cmd: ExCommand): Boolean { + VimPlugin.getCommand().resetAliases() + return true } - } -} +} \ No newline at end of file diff --git a/src/com/maddyhome/idea/vim/ex/handler/CmdHandler.kt b/src/com/maddyhome/idea/vim/ex/handler/CmdHandler.kt new file mode 100644 index 000000000..a7db24988 --- /dev/null +++ b/src/com/maddyhome/idea/vim/ex/handler/CmdHandler.kt @@ -0,0 +1,177 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2019 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 <http://www.gnu.org/licenses/>. + */ + +package com.maddyhome.idea.vim.ex.handler + +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.editor.Editor +import com.maddyhome.idea.vim.VimPlugin +import com.maddyhome.idea.vim.common.Alias +import com.maddyhome.idea.vim.ex.* +import com.maddyhome.idea.vim.ex.vimscript.VimScriptCommandHandler +import com.maddyhome.idea.vim.group.CommandGroup.Companion.BLACKLISTED_ALIASES + +/** + * @author Elliot Courant + */ +class CmdHandler : CommandHandler( + commands("com[mand]"), + flags(RangeFlag.RANGE_FORBIDDEN, ArgumentFlag.ARGUMENT_OPTIONAL, Flag.DONT_REOPEN) +), VimScriptCommandHandler { + // Static definitions needed for aliases. + private companion object { + const val overridePrefix = "!" + const val argsPrefix = "-nargs" + + const val anyNumberOfArguments = "*" + const val zeroOrOneArguments = "?" + const val moreThanZeroArguments = "+" + + const val errorInvalidNumberOfArguments = "E176: Invalid number of arguments" + const val errorCannotStartWithLowercase = "E183: User defined commands must start with an uppercase letter" + const val errorReservedName = "E841: Reserved name, cannot be used for user defined command" + const val errorCommandAlreadyExists = "E174: Command already exists: add ! to replace it" + } + + override fun execute(cmd: ExCommand) { + this.addAlias(cmd, null) + } + + override fun execute(editor: Editor, context: DataContext, cmd: ExCommand): Boolean { + if (cmd.argument.trim().isEmpty()) { + return this.listAlias(editor, "") + } + return this.addAlias(cmd, editor) + } + + private fun listAlias(editor: Editor, filter: String): Boolean { + val lineSeparator = "\n" + val allAliases = VimPlugin.getCommand().listAliases() + val aliases = allAliases.filter { + (filter.isEmpty() || it.key.startsWith(filter)) + }.map { + "${it.key.padEnd(12)}${it.value.numberOfArguments.padEnd(11)}${it.value.command}" + }.sortedWith(String.CASE_INSENSITIVE_ORDER).joinToString(lineSeparator) + ExOutputModel.getInstance(editor).output("Name Args Definition$lineSeparator$aliases") + return true + } + + private fun addAlias(cmd: ExCommand, editor: Editor?): Boolean { + var argument = cmd.argument.trim() + + // Handle overwriting of aliases + val overrideAlias = argument.startsWith(overridePrefix) + if (overrideAlias) { + argument = argument.removePrefix(overridePrefix).trim() + } + + // Handle alias arguments + val hasArguments = argument.startsWith(argsPrefix) + var minNumberOfArgs = 0 + var maxNumberOfArgs = 0 + if (hasArguments) { + // Extract the -nargs that's part of this execution, it's possible that -nargs is + // in the actual alias being created, and we don't want to parse that one. + val trimmedInput = argument.takeWhile { it != ' ' } + val pattern = Regex("(?>-nargs=((|[-])\\d+|[?]|[+]|[*]))").find(trimmedInput) ?: run { + VimPlugin.showMessage(errorInvalidNumberOfArguments) + return false + } + val nargForTrim = pattern.groupValues[0] + val argumentValue = pattern.groups[1]!!.value + val argNum = argumentValue.toIntOrNull() + if (argNum == null) { // If the argument number is null then it is not a number. + // Make sure the argument value is a valid symbol that we can handle. + when (argumentValue) { + anyNumberOfArguments -> { + minNumberOfArgs = 0 + maxNumberOfArgs = -1 + } + zeroOrOneArguments -> maxNumberOfArgs = 1 + moreThanZeroArguments -> { + minNumberOfArgs = 1 + maxNumberOfArgs = -1 + } + else -> { + // Technically this should never be reached, but is here just in case + // I missed something, since the regex limits the value to be ? + * or + // a valid number, its not possible (as far as I know) to have another value + // that regex would accept that is not valid. + VimPlugin.showMessage(errorInvalidNumberOfArguments) + return false + } + } + } else { + // Not sure why this isn't documented, but if you try to create a command in vim + // with an explicit number of arguments greater than 1 it returns this error. + if (argNum > 1 || argNum < 0) { + VimPlugin.showMessage(errorInvalidNumberOfArguments) + return false + } + minNumberOfArgs = argNum + maxNumberOfArgs = argNum + } + argument = argument.removePrefix(nargForTrim).trim() + } + + // We want to trim off any "!" at the beginning of the arguments. + // This will also remove any extra spaces. + argument = argument.trim() + + // We want to get the first character sequence in the arguments. + // eg. command! Wq wq + // We want to extract the Wq only, and then just use the rest of + // the argument as the alias result. + val alias = argument.split(" ")[0] + argument = argument.removePrefix(alias).trim() + + // User-aliases need to begin with an uppercase character. + if (!alias[0].isUpperCase()) { + VimPlugin.showMessage(errorCannotStartWithLowercase) + return false + } + + if (alias in BLACKLISTED_ALIASES) { + VimPlugin.showMessage(errorReservedName) + return false + } + + if (argument.isEmpty()) { + if (editor == null) { + // If there is no editor then we can't list aliases, just return false. + // No message should be shown either, since there is no editor. + return false + } + return this.listAlias(editor, alias) + } + + // If we are not over-writing existing aliases, and an alias with the same command + // already exists then we want to do nothing. + if (!overrideAlias && VimPlugin.getCommand().hasAlias(alias)) { + VimPlugin.showMessage(errorCommandAlreadyExists) + return false + } + + // Store the alias and the command. We don't need to parse the argument + // at this time, if the syntax is wrong an error will be returned when + // the alias is executed. + VimPlugin.getCommand().setAlias(alias, Alias(minNumberOfArgs, maxNumberOfArgs, alias, argument)) + + return true + } +} \ No newline at end of file diff --git a/src/com/maddyhome/idea/vim/ex/handler/DelCmdHandler.kt b/src/com/maddyhome/idea/vim/ex/handler/DelCmdHandler.kt new file mode 100644 index 000000000..be9c87c58 --- /dev/null +++ b/src/com/maddyhome/idea/vim/ex/handler/DelCmdHandler.kt @@ -0,0 +1,42 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2019 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 <http://www.gnu.org/licenses/>. + */ + +package com.maddyhome.idea.vim.ex.handler + +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.editor.Editor +import com.maddyhome.idea.vim.VimPlugin +import com.maddyhome.idea.vim.ex.CommandHandler +import com.maddyhome.idea.vim.ex.ExCommand +import com.maddyhome.idea.vim.ex.commands +import com.maddyhome.idea.vim.ex.flags + +class DelCmdHandler : CommandHandler( + commands("delc[ommand]"), + flags(RangeFlag.RANGE_FORBIDDEN, ArgumentFlag.ARGUMENT_REQUIRED) +) { + override fun execute(editor: Editor, context: DataContext, cmd: ExCommand): Boolean { + if (!VimPlugin.getCommand().hasAlias(cmd.argument)) { + VimPlugin.showMessage("E184: No such user-defined command: ${cmd.argument}") + return false + } + + VimPlugin.getCommand().removeAlias(cmd.argument) + return true + } +} \ No newline at end of file diff --git a/src/com/maddyhome/idea/vim/group/ChangeGroup.java b/src/com/maddyhome/idea/vim/group/ChangeGroup.java index bb0dfa683..e19260bc9 100644 --- a/src/com/maddyhome/idea/vim/group/ChangeGroup.java +++ b/src/com/maddyhome/idea/vim/group/ChangeGroup.java @@ -1069,20 +1069,20 @@ public class ChangeGroup { if (motion == null) { return false; } + EnumSet<CommandFlags> flags = motion.getFlags().clone(); if (!isChange && !motion.getFlags().contains(CommandFlags.FLAG_MOT_LINEWISE)) { LogicalPosition start = editor.offsetToLogicalPosition(range.getStartOffset()); LogicalPosition end = editor.offsetToLogicalPosition(range.getEndOffset()); if (start.line != end.line) { if (!SearchHelper.anyNonWhitespace(editor, range.getStartOffset(), -1) && !SearchHelper.anyNonWhitespace(editor, range.getEndOffset(), 1)) { - EnumSet<CommandFlags> flags = motion.getFlags(); flags.remove(CommandFlags.FLAG_MOT_EXCLUSIVE); flags.remove(CommandFlags.FLAG_MOT_INCLUSIVE); flags.add(CommandFlags.FLAG_MOT_LINEWISE); } } } - return deleteRange(editor, caret, range, SelectionType.fromCommandFlags(motion.getFlags()), isChange); + return deleteRange(editor, caret, range, SelectionType.fromCommandFlags(flags), isChange); } /** @@ -1678,7 +1678,7 @@ public class ChangeGroup { return false; } - if (type == null || VimPlugin.getRegister().storeText(editor, range, type, true)) { + if (type == null || CommandState.inInsertMode(editor) || VimPlugin.getRegister().storeText(editor, range, type, true)) { final Document document = editor.getDocument(); final int[] startOffsets = range.getStartOffsets(); final int[] endOffsets = range.getEndOffsets(); diff --git a/src/com/maddyhome/idea/vim/group/CommandGroup.kt b/src/com/maddyhome/idea/vim/group/CommandGroup.kt new file mode 100644 index 000000000..6fcaa2cc6 --- /dev/null +++ b/src/com/maddyhome/idea/vim/group/CommandGroup.kt @@ -0,0 +1,84 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2019 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 <http://www.gnu.org/licenses/>. + */ + +package com.maddyhome.idea.vim.group + +import com.maddyhome.idea.vim.common.Alias + +/** + * @author Elliot Courant + */ +class CommandGroup { + companion object { + val BLACKLISTED_ALIASES = arrayOf("X", "Next", "Print") + private const val overridePrefix = "!" + } + private var aliases = HashMap<String, Alias>() + + fun isAlias(command: String): Boolean { + val name = this.getAliasName(command) + // If the first letter is not uppercase then it cannot be an alias + // and reject immediately. + if (!name[0].isUpperCase()) { + return false + } + + // If the input is blacklisted, then it is not an alias. + if (name in BLACKLISTED_ALIASES) { + return false + } + + return this.hasAlias(name) + } + + fun hasAlias(name: String): Boolean { + return name in this.aliases + } + + fun getAlias(name: String): Alias { + return this.aliases[name]!! + } + + fun getAliasCommand(command: String, count: Int): String { + return this.getAlias(this.getAliasName(command)).getCommand(command, count) + } + + fun setAlias(name: String, alias: Alias) { + this.aliases[name] = alias + } + + fun removeAlias(name: String) { + this.aliases.remove(name) + } + + fun listAliases(): Set<Map.Entry<String, Alias>> { + return this.aliases.entries + } + + fun resetAliases() { + this.aliases.clear() + } + + private fun getAliasName(command: String): String { + val items = command.split(" ") + if (items.count() > 1) { + return items[0].removeSuffix(overridePrefix) + } + return command.removeSuffix(overridePrefix) + } +} \ No newline at end of file diff --git a/src/com/maddyhome/idea/vim/group/EditorGroup.java b/src/com/maddyhome/idea/vim/group/EditorGroup.java index 65e0581e3..4c9fdad7e 100644 --- a/src/com/maddyhome/idea/vim/group/EditorGroup.java +++ b/src/com/maddyhome/idea/vim/group/EditorGroup.java @@ -28,7 +28,6 @@ import com.intellij.openapi.editor.event.*; import com.intellij.openapi.editor.ex.EditorEx; import com.intellij.openapi.project.Project; import com.maddyhome.idea.vim.EventFacade; -import com.maddyhome.idea.vim.KeyHandler; import com.maddyhome.idea.vim.VimPlugin; import com.maddyhome.idea.vim.command.CommandState; import com.maddyhome.idea.vim.helper.*; @@ -39,7 +38,6 @@ import org.jdom.Element; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import javax.swing.*; import java.awt.*; import java.util.List; @@ -94,7 +92,7 @@ public class EditorGroup { // Turn on insert mode if editor doesn't have any file if (!EditorData.isFileEditor(editor) && editor.getDocument().isWritable() && !CommandState.inInsertMode(editor)) { - KeyHandler.getInstance().handleKey(editor, KeyStroke.getKeyStroke('i'), new EditorDataContext(editor)); + VimPlugin.getChange().insertBeforeCursor(editor, new EditorDataContext(editor)); } editor.getSettings().setBlockCursor(!CommandState.inInsertMode(editor)); editor.getSettings().setAnimatedScrolling(ANIMATED_SCROLLING_VIM_VALUE); diff --git a/src/com/maddyhome/idea/vim/group/KeyGroup.java b/src/com/maddyhome/idea/vim/group/KeyGroup.java index 051d48421..5adb5d422 100644 --- a/src/com/maddyhome/idea/vim/group/KeyGroup.java +++ b/src/com/maddyhome/idea/vim/group/KeyGroup.java @@ -68,13 +68,12 @@ public class KeyGroup { @NotNull private final Map<MappingMode, KeyMapping> keyMappings = new HashMap<>(); @Nullable private OperatorFunction operatorFunction = null; - public void registerRequiredShortcutKeys(@NotNull Editor editor) { - final Set<KeyStroke> requiredKeys = VimPlugin.getKey().requiredShortcutKeys; + void registerRequiredShortcutKeys(@NotNull Editor editor) { EventFacade.getInstance().registerCustomShortcutSet(VimShortcutKeyAction.getInstance(), - toShortcutSet(requiredKeys), editor.getComponent()); + toShortcutSet(requiredShortcutKeys), editor.getComponent()); } - public void unregisterShortcutKeys(@NotNull Editor editor) { + void unregisterShortcutKeys(@NotNull Editor editor) { EventFacade.getInstance().unregisterCustomShortcutSet(VimShortcutKeyAction.getInstance(), editor.getComponent()); } diff --git a/src/com/maddyhome/idea/vim/group/MotionGroup.java b/src/com/maddyhome/idea/vim/group/MotionGroup.java index da9f7f6d2..bdfbc59b2 100755 --- a/src/com/maddyhome/idea/vim/group/MotionGroup.java +++ b/src/com/maddyhome/idea/vim/group/MotionGroup.java @@ -54,6 +54,7 @@ import com.maddyhome.idea.vim.listener.VimListenerManager; import com.maddyhome.idea.vim.option.NumberOption; import com.maddyhome.idea.vim.option.Options; import com.maddyhome.idea.vim.ui.ExEntryPanel; +import kotlin.ranges.IntProgression; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -1055,7 +1056,21 @@ public class MotionGroup { public int moveCaretHorizontal(@NotNull Editor editor, @NotNull Caret caret, int count, boolean allowPastEnd) { int oldOffset = caret.getOffset(); - int offset = EditorHelper.normalizeOffset(editor, caret.getLogicalPosition().line, oldOffset + count, allowPastEnd); + int diff = 0; + String text = editor.getDocument().getText(); + int sign = (int)Math.signum(count); + for (Integer pointer : new IntProgression(0, count - sign, sign)) { + int textPointer = oldOffset + pointer; + if (textPointer < text.length() && textPointer >= 0) { + // Actual char size can differ from 1 if unicode characters are used (like 🐔) + diff += Character.charCount(text.codePointAt(textPointer)); + } + else { + diff += 1; + } + } + int offset = + EditorHelper.normalizeOffset(editor, caret.getLogicalPosition().line, oldOffset + (sign * diff), allowPastEnd); if (offset == oldOffset) { return -1; diff --git a/src/com/maddyhome/idea/vim/group/ProcessGroup.java b/src/com/maddyhome/idea/vim/group/ProcessGroup.java index 92e104fc3..00f45373c 100644 --- a/src/com/maddyhome/idea/vim/group/ProcessGroup.java +++ b/src/com/maddyhome/idea/vim/group/ProcessGroup.java @@ -90,7 +90,7 @@ public class ProcessGroup { ExEntryPanel panel = ExEntryPanel.getInstance(); if (panel.isActive()) { - UiHelper.requestFocus(panel); + UiHelper.requestFocus(panel.getEntry()); panel.handleKey(stroke); return true; @@ -144,13 +144,11 @@ public class ProcessGroup { return res; } - public boolean cancelExEntry(@NotNull final Editor editor, @NotNull final DataContext context) { + public void cancelExEntry(@NotNull final Editor editor, @NotNull final DataContext context) { CommandState.getInstance(editor).popState(); KeyHandler.getInstance().reset(editor); ExEntryPanel panel = ExEntryPanel.getInstance(); panel.deactivate(true); - - return true; } private void record(Editor editor, @NotNull String text) { diff --git a/src/com/maddyhome/idea/vim/helper/DigraphSequence.java b/src/com/maddyhome/idea/vim/helper/DigraphSequence.java index 077a1cbed..378a33591 100644 --- a/src/com/maddyhome/idea/vim/helper/DigraphSequence.java +++ b/src/com/maddyhome/idea/vim/helper/DigraphSequence.java @@ -49,7 +49,7 @@ public class DigraphSequence { if (key.getKeyCode() == KeyEvent.VK_K && (key.getModifiers() & KeyEvent.CTRL_MASK) != 0) { logger.debug("found Ctrl-K"); digraphState = DIG_STATE_DIG_ONE; - return DigraphResult.OK; + return DigraphResult.OK_DIGRAPH; } else if ((key.getKeyCode() == KeyEvent.VK_V || key.getKeyCode() == KeyEvent.VK_Q) && (key.getModifiers() & KeyEvent.CTRL_MASK) != 0) { @@ -57,7 +57,7 @@ public class DigraphSequence { digraphState = DIG_STATE_CODE_START; codeChars = new char[8]; codeCnt = 0; - return DigraphResult.OK; + return DigraphResult.OK_LITERAL; } else { return new DigraphResult(key); @@ -68,7 +68,7 @@ public class DigraphSequence { digraphChar = key.getKeyChar(); digraphState = DIG_STATE_DIG_TWO; - return DigraphResult.OK; + return new DigraphResult(DigraphResult.RES_OK, digraphChar); } else { digraphState = DIG_STATE_START; @@ -93,26 +93,26 @@ public class DigraphSequence { digraphState = DIG_STATE_CODE_CHAR; codeType = 8; logger.debug("Octal"); - return DigraphResult.OK; + return DigraphResult.OK_LITERAL; case 'x': case 'X': codeMax = 2; digraphState = DIG_STATE_CODE_CHAR; codeType = 16; logger.debug("hex2"); - return DigraphResult.OK; + return DigraphResult.OK_LITERAL; case 'u': codeMax = 4; digraphState = DIG_STATE_CODE_CHAR; codeType = 16; logger.debug("hex4"); - return DigraphResult.OK; + return DigraphResult.OK_LITERAL; case 'U': codeMax = 8; digraphState = DIG_STATE_CODE_CHAR; codeType = 16; logger.debug("hex8"); - return DigraphResult.OK; + return DigraphResult.OK_LITERAL; case '0': case '1': case '2': @@ -128,7 +128,7 @@ public class DigraphSequence { codeType = 10; codeChars[codeCnt++] = key.getKeyChar(); logger.debug("decimal"); - return DigraphResult.OK; + return DigraphResult.OK_LITERAL; default: switch (key.getKeyCode()) { case KeyEvent.VK_TAB: @@ -177,7 +177,7 @@ public class DigraphSequence { return new DigraphResult(code); } else { - return DigraphResult.OK; + return DigraphResult.OK_LITERAL; } } else if (codeCnt > 0) { @@ -204,14 +204,21 @@ public class DigraphSequence { public static final int RES_BAD = 1; public static final int RES_DONE = 2; - public static final DigraphResult OK = new DigraphResult(RES_OK); - public static final DigraphResult BAD = new DigraphResult(RES_BAD); + static final DigraphResult OK_DIGRAPH = new DigraphResult(RES_OK, '?'); + static final DigraphResult OK_LITERAL = new DigraphResult(RES_OK, '^'); + static final DigraphResult BAD = new DigraphResult(RES_BAD); DigraphResult(int result) { this.result = result; stroke = null; } + DigraphResult(int result, char promptCharacter) { + this.result = result; + this.promptCharacter = promptCharacter; + stroke = null; + } + DigraphResult(@Nullable KeyStroke stroke) { result = RES_DONE; this.stroke = stroke; @@ -226,8 +233,13 @@ public class DigraphSequence { return result; } + public char getPromptCharacter() { + return promptCharacter; + } + private final int result; @Nullable private final KeyStroke stroke; + private char promptCharacter; } private int digraphState = DIG_STATE_START; diff --git a/src/com/maddyhome/idea/vim/helper/SearchHelper.java b/src/com/maddyhome/idea/vim/helper/SearchHelper.java index 2ba37061f..d8be3a659 100644 --- a/src/com/maddyhome/idea/vim/helper/SearchHelper.java +++ b/src/com/maddyhome/idea/vim/helper/SearchHelper.java @@ -18,7 +18,6 @@ package com.maddyhome.idea.vim.helper; -import com.google.common.collect.Lists; import com.intellij.lang.CodeDocumentationAwareCommenter; import com.intellij.lang.Commenter; import com.intellij.lang.Language; @@ -27,7 +26,6 @@ import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Caret; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.util.Pair; -import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.PsiComment; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; @@ -42,6 +40,7 @@ import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; +import java.util.Stack; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -115,13 +114,32 @@ public class SearchHelper { int pos = caret.getOffset(); int start = caret.getSelectionStart(); int end = caret.getSelectionEnd(); - if (start != end) { - pos = Math.min(start, end); - } int loc = blockChars.indexOf(type); char close = blockChars.charAt(loc + 1); + boolean rangeSelection = end - start > 1; + if (rangeSelection && start == 0) // early return not only for optimization + return null; // but also not to break the interval semantic on this edge case (see below) + + /* In case of successive inner selection. We want to break out of + * the block delimiter of the current inner selection. + * In other terms, for the rest of the algorithm, a previous inner selection of a block + * if equivalent to an outer one. */ + if (!isOuter + && (start - 1) >= 0 && type == chars.charAt(start - 1) + && end < chars.length() && close == chars.charAt(end)) { + start = start - 1; + pos = start; + rangeSelection = true; + } + + /* when one char is selected, we want to find the enclosing block of (start,end] + * although when a range of characters is selected, we want the enclosing block of [start, end] + * shifting the position allow to express which kind of interval we work on */ + if (rangeSelection) + pos = Math.max(0, start - 1); + boolean initialPosIsInString = checkInString(chars, pos, true); int bstart = -1; @@ -413,80 +431,192 @@ public class SearchHelper { return -1; } + /** returns new position which ignore whitespaces at beginning of the line*/ + private static int ignoreWhitespaceAtLineStart(CharSequence seq, int lineStart, int pos) { + if (seq.subSequence(lineStart, pos).chars().allMatch(Character::isWhitespace)) { + while (pos < seq.length() && seq.charAt(pos) != '\n' && Character.isWhitespace(seq.charAt(pos))) { + pos++; + } + } + return pos; + } + + @Nullable public static TextRange findBlockTagRange(@NotNull Editor editor, @NotNull Caret caret, int count, boolean isOuter) { - final int cursorOffset = caret.getOffset(); - int pos = cursorOffset; - int currentCount = count; + final int position = caret.getOffset(); final CharSequence sequence = editor.getDocument().getCharsSequence(); + + final int selectionStart = caret.getSelectionStart(); + final int selectionEnd = caret.getSelectionEnd(); + + final boolean isRangeSelection = selectionEnd - selectionStart > 1; + + int searchStartPosition; + if (!isRangeSelection) { + final int line = caret.getLogicalPosition().line; + final int lineBegin = editor.getDocument().getLineStartOffset(line); + searchStartPosition = ignoreWhitespaceAtLineStart(sequence, lineBegin, position); + } else { + searchStartPosition = selectionEnd; + } + + if (isInHTMLTag(sequence, searchStartPosition, false)) { + // caret is inside opening tag. Move to closing '>'. + while (searchStartPosition < sequence.length() && sequence.charAt(searchStartPosition) != '>') { + searchStartPosition ++; + } + } + else if (isInHTMLTag(sequence, searchStartPosition, true)) { + // caret is inside closing tag. Move to starting '<'. + while (searchStartPosition > 0 && sequence.charAt(searchStartPosition) != '<') { + searchStartPosition --; + } + } + while (true) { - final Pair<TextRange, String> closingTagResult = findClosingTag(sequence, pos); - if (closingTagResult == null) { + final Pair<TextRange, String> closingTag = findUnmatchedClosingTag(sequence, searchStartPosition, count); + if (closingTag == null) { return null; } - final TextRange closingTagTextRange = closingTagResult.getFirst(); - final String tagName = closingTagResult.getSecond(); - final TextRange openingTagTextRange = findOpeningTag(sequence, closingTagTextRange.getStartOffset(), tagName); - if (openingTagTextRange != null && openingTagTextRange.getStartOffset() <= cursorOffset && --currentCount == 0) { - if (isOuter) { - return new TextRange(openingTagTextRange.getStartOffset(), closingTagTextRange.getEndOffset()); + final TextRange closingTagTextRange = closingTag.getFirst(); + final String tagName = closingTag.getSecond(); + + TextRange openingTag = findUnmatchedOpeningTag(sequence, closingTagTextRange.getStartOffset(), tagName); + if (openingTag == null) { + return null; + } + + if (isRangeSelection && openingTag.getEndOffset() - 1 >= selectionStart) { + // If there was already some text selected and the new selection would not extend further, we try again + searchStartPosition = closingTagTextRange.getEndOffset(); + count = 1; + continue; + } + + int selectionEndWithoutNewline = selectionEnd; + while (selectionEndWithoutNewline < sequence.length() && sequence.charAt(selectionEndWithoutNewline) == '\n') { + selectionEndWithoutNewline ++; + } + + if (closingTagTextRange.getStartOffset() == selectionEndWithoutNewline && openingTag.getEndOffset() == selectionStart) { + // Special case: if the inner tag is already selected we should like isOuter is active + // Note that we need to ignore newlines, because their selection is lost between multiple "it" invocations + isOuter = true; + } else + if (openingTag.getEndOffset() == closingTagTextRange.getStartOffset() && selectionStart == openingTag.getEndOffset()) { + // Special case: for an empty tag pair (e.g. <a></a>) the whole tag is selected if the caret is in the middle. + isOuter = true; + } + + if (isOuter) { + return new TextRange(openingTag.getStartOffset(), closingTagTextRange.getEndOffset() - 1); + } else { + return new TextRange(openingTag.getEndOffset(), Math.max(closingTagTextRange.getStartOffset() - 1, openingTag.getEndOffset())); + } + } + } + + /** + * Returns true if there is a html at the given position. Ignores tags with a trailing slash like <aaa/>. + */ + private static boolean isInHTMLTag(@NotNull final CharSequence sequence, final int position, final boolean isEndtag) { + int openingBracket = -1; + for (int i = position; i >= 0 && i < sequence.length(); i--) { + if (sequence.charAt(i) == '<') { + openingBracket = i; + break; + } + if (sequence.charAt(i) == '>' && i != position) { + return false; + } + } + + if (openingBracket == -1) { + return false; + } + + boolean hasSlashAfterOpening = openingBracket + 1 < sequence.length() && sequence.charAt(openingBracket + 1) == '/'; + if ((isEndtag && !hasSlashAfterOpening) || (!isEndtag && hasSlashAfterOpening)) { + return false; + } + + int closingBracket = -1; + for (int i = openingBracket; i < sequence.length(); i++) { + if (sequence.charAt(i) == '>') { + closingBracket = i; + break; + } + } + + return closingBracket != -1 && sequence.charAt(closingBracket - 1) != '/'; + } + + @Nullable + private static Pair<TextRange,String> findUnmatchedClosingTag(@NotNull final CharSequence sequence, final int position, int count) { + // The tag name may contain any characters except slashes, whitespace and '>' + final String tagNamePattern = "([^/\\s>]+)"; + // An opening tag consists of '<' followed by a tag name, optionally some additional text after whitespace and a '>' + final String openingTagPattern = String.format("<%s(?:\\s[^>]*)?>", tagNamePattern); + final String closingTagPattern = String.format("</%s>", tagNamePattern); + final Pattern tagPattern = Pattern.compile(String.format("(?:%s)|(?:%s)", openingTagPattern, closingTagPattern)); + final Matcher matcher = tagPattern.matcher(sequence.subSequence(position, sequence.length())); + + final Stack<String> openTags = new Stack<>(); + + while (matcher.find()) { + boolean isClosingTag = matcher.group(1) == null; + if (isClosingTag) { + final String tagName = matcher.group(2); + // Ignore unmatched open tags. Either the file is malformed or it might be a tag like <br> that does not need to be closed. + while (!openTags.isEmpty() && !openTags.peek().equalsIgnoreCase(tagName)) { + openTags.pop(); } - else { - return new TextRange(openingTagTextRange.getEndOffset() + 1, closingTagTextRange.getStartOffset() - 1); + if (openTags.isEmpty()) { + if (count <= 1) { + return Pair.create(new TextRange(position + matcher.start(), position + matcher.end()), tagName); + } else { + count--; + } + } else { + openTags.pop(); + } + } else { + final String tagName = matcher.group(1); + openTags.push(tagName); + } + } + return null; + } + + @Nullable + private static TextRange findUnmatchedOpeningTag(@NotNull CharSequence sequence, int position, @NotNull String tagName) { + final String quotedTagName = Pattern.quote(tagName); + final String patternString = "(</%s>)" // match closing tags + + "|(<%s" // or opening tags starting with tagName + + "(\\s([^>]*" // After at least one whitespace there might be additional text in the tag. E.g. <html lang="en"> + + "[^/])?)?>)"; // Slash is not allowed as last character (this would be a self closing tag). + final Pattern tagPattern = Pattern.compile(String.format(patternString, quotedTagName, quotedTagName), Pattern.CASE_INSENSITIVE); + final Matcher matcher = tagPattern.matcher(sequence.subSequence(0, position+1)); + final Stack<TextRange> openTags = new Stack<>(); + + while (matcher.find()) { + final TextRange match = new TextRange(matcher.start(), matcher.end()); + if (sequence.charAt(matcher.start() + 1) == '/') { + if (!openTags.isEmpty()) { + openTags.pop(); } } else { - pos = closingTagTextRange.getEndOffset() + 1; + openTags.push(match); } } - } - @Nullable - private static TextRange findOpeningTag(@NotNull CharSequence sequence, int position, @NotNull String tagName) { - final String tagBeginning = "<" + tagName; - final Pattern pattern = Pattern.compile(Pattern.quote(tagBeginning), Pattern.CASE_INSENSITIVE); - final Matcher matcher = pattern.matcher(sequence.subSequence(0, position)); - final List<Integer> possibleBeginnings = Lists.newArrayList(); - while (matcher.find()) { - possibleBeginnings.add(matcher.start()); + if (openTags.isEmpty()) { + return null; + } else { + return openTags.pop(); } - final List<Integer> reversedBeginnings = Lists.reverse(possibleBeginnings); - for (int openingTagPos : reversedBeginnings) { - final int openingTagEndPos = openingTagPos + tagBeginning.length(); - final int closeBracketPos = StringUtil.indexOf(sequence, '>', openingTagEndPos); - if (closeBracketPos > 0 && (closeBracketPos == openingTagEndPos || sequence.charAt(openingTagEndPos) == ' ')) { - return new TextRange(openingTagPos, closeBracketPos); - } - } - return null; - } - - @Nullable - private static Pair<TextRange, String> findClosingTag(@NotNull CharSequence sequence, int pos) { - int closeBracketPos = pos; - int openBracketPos; - while (closeBracketPos < sequence.length()) { - closeBracketPos = StringUtil.indexOf(sequence, '>', closeBracketPos); - if (closeBracketPos < 0) { - return null; - } - openBracketPos = closeBracketPos - 1; - while (openBracketPos >= 0) { - openBracketPos = StringUtil.lastIndexOf(sequence, '<', 0, openBracketPos); - if (openBracketPos >= 0 && - openBracketPos + 1 < sequence.length() && - sequence.charAt(openBracketPos + 1) == '/') { - final String tagName = String.valueOf(sequence.subSequence(openBracketPos + "</".length(), closeBracketPos)); - if (tagName.length() > 0 && tagName.charAt(0) != ' ') { - TextRange textRange = new TextRange(openBracketPos, closeBracketPos); - return Pair.create(textRange, tagName); - } - } - openBracketPos--; - } - closeBracketPos++; - } - return null; } diff --git a/src/com/maddyhome/idea/vim/listener/ListenerManager.kt b/src/com/maddyhome/idea/vim/listener/ListenerManager.kt index c1546b576..b351b40e1 100644 --- a/src/com/maddyhome/idea/vim/listener/ListenerManager.kt +++ b/src/com/maddyhome/idea/vim/listener/ListenerManager.kt @@ -190,7 +190,7 @@ object VimListenerManager { VimPlugin.getMotion() val editor = event.editor if (ExEntryPanel.getInstance().isActive) { - ExEntryPanel.getInstance().deactivate(false) + VimPlugin.getProcess().cancelExEntry(editor, ExEntryPanel.getInstance().entry.context) } ExOutputModel.getInstance(editor).clear() @@ -226,7 +226,7 @@ object VimListenerManager { event.mouseEvent.button != MouseEvent.BUTTON3) { VimPlugin.getMotion() if (ExEntryPanel.getInstance().isActive) { - ExEntryPanel.getInstance().deactivate(false) + VimPlugin.getProcess().cancelExEntry(event.editor, ExEntryPanel.getInstance().entry.context) } ExOutputModel.getInstance(event.editor).clear() diff --git a/src/com/maddyhome/idea/vim/package-info.java b/src/com/maddyhome/idea/vim/package-info.java index 5532b64f9..965bec805 100644 --- a/src/com/maddyhome/idea/vim/package-info.java +++ b/src/com/maddyhome/idea/vim/package-info.java @@ -307,6 +307,10 @@ * |CTRL-W_<Up>| {@link com.maddyhome.idea.vim.action.window.WindowUpAction} * |CTRL-W_<Left>| {@link com.maddyhome.idea.vim.action.window.WindowLeftAction} * |CTRL-W_<Right>| {@link com.maddyhome.idea.vim.action.window.WindowRightAction} + * |CTRL-W_CTRL-H| {@link com.maddyhome.idea.vim.action.window.WindowLeftAction} + * |CTRL-W_CTRL-J| {@link com.maddyhome.idea.vim.action.window.WindowDownAction} + * |CTRL-W_CTRL-K| {@link com.maddyhome.idea.vim.action.window.WindowUpAction} + * |CTRL-W_CTRL-L| {@link com.maddyhome.idea.vim.action.window.WindowRightAction} * * * 2.3. Square bracket commands @@ -594,7 +598,72 @@ * * 5. Command line editing * - * There is no up-to-date list of supported command line editing commands. + * tag action + * ------------------------------------------------------------------------------------------------------------------- + * + * |c_CTRL-A| TODO + * |c_CTRL-B| {@link javax.swing.text.DefaultEditorKit#beginLineAction} + * |c_CTRL-C| {@link com.maddyhome.idea.vim.ui.ExEditorKit.CancelEntryAction} + * |c_CTRL-D| TODO + * |c_CTRL-E| {@link javax.swing.text.DefaultEditorKit#endLineAction} + * |c_CTRL-G| TODO + * |c_CTRL-H| {@link com.maddyhome.idea.vim.ui.ExEditorKit.DeletePreviousCharAction} + * |c_CTRL-I| TODO + * |c_CTRL-J| {@link com.maddyhome.idea.vim.ui.ExEditorKit.CompleteEntryAction} + * |c_CTRL-K| {@link com.maddyhome.idea.vim.ui.ExEditorKit.StartDigraphAction} + * |c_CTRL-L| TODO + * |c_CTRL-M| {@link com.maddyhome.idea.vim.ui.ExEditorKit.CompleteEntryAction} + * |c_CTRL-N| TODO + * |c_CTRL-P| TODO + * |c_CTRL-Q| {@link com.maddyhome.idea.vim.ui.ExEditorKit.StartDigraphAction} + * |c_CTRL-R| {@link com.maddyhome.idea.vim.ui.ExEditorKit.InsertRegisterAction} + * |c_CTRL-R_CTRL-A| TODO + * |c_CTRL-R_CTRL-F| TODO + * |c_CTRL-R_CTRL-L| TODO + * |c_CTRL-R_CTRL-O| TODO + * |c_CTRL-R_CTRL-P| TODO + * |c_CTRL-R_CTRL-R| TODO + * |c_CTRL-R_CTRL-W| TODO + * |c_CTRL-T| TODO + * |c_CTRL-U| {@link com.maddyhome.idea.vim.ui.ExEditorKit.DeleteToCursorAction} + * |c_CTRL-V| {@link com.maddyhome.idea.vim.ui.ExEditorKit.StartDigraphAction} + * |c_CTRL-W| {@link com.maddyhome.idea.vim.ui.ExEditorKit.DeletePreviousWordAction} + * |c_CTRL-Y| TODO + * |c_CTRL-\_e| TODO + * |c_CTRL-\_CTRL-G| TODO + * |c_CTRL-\_CTRL-N| TODO + * |c_CTRL-_| not applicable + * |c_CTRL-^| not applicable + * |c_CTRL-]| TODO + * |c_CTRL-[| {@link com.maddyhome.idea.vim.ui.ExEditorKit.EscapeCharAction} + * |c_<BS>| {@link com.maddyhome.idea.vim.ui.ExEditorKit.DeletePreviousCharAction} + * |c_<CR>| {@link com.maddyhome.idea.vim.ui.ExEditorKit.CompleteEntryAction} + * |c_<C-Left>| {@link javax.swing.text.DefaultEditorKit#previousWordAction} + * |c_<C-Right>| {@link javax.swing.text.DefaultEditorKit#nextWordAction} + * |c_<Del>| {@link javax.swing.text.DefaultEditorKit#deleteNextCharAction} + * |c_<Down>| {@link com.maddyhome.idea.vim.ui.ExEditorKit.HistoryDownFilterAction} + * |c_<End>| {@link javax.swing.text.DefaultEditorKit#endLineAction} + * |c_<Esc>| {@link com.maddyhome.idea.vim.ui.ExEditorKit.EscapeCharAction} + * |c_<Home>| {@link javax.swing.text.DefaultEditorKit#beginLineAction} + * |c_<Insert>| {@link com.maddyhome.idea.vim.ui.ExEditorKit.ToggleInsertReplaceAction} + * |c_<Left>| {@link javax.swing.text.DefaultEditorKit#backwardAction} + * |c_<LeftMouse>| not applicable + * |c_<MiddleMouse>| TODO + * |c_<NL>| {@link com.maddyhome.idea.vim.ui.ExEditorKit.CompleteEntryAction} + * |c_<PageUp>| {@link com.maddyhome.idea.vim.ui.ExEditorKit.HistoryUpAction} + * |c_<PageDown>| {@link com.maddyhome.idea.vim.ui.ExEditorKit.HistoryDownAction} + * |c_<Right>| {@link javax.swing.text.DefaultEditorKit#forwardAction} + * |c_<S-Down>| {@link com.maddyhome.idea.vim.ui.ExEditorKit.HistoryDownAction} + * |c_<S-Left>| {@link javax.swing.text.DefaultEditorKit#previousWordAction} + * |c_<S-Right>| {@link javax.swing.text.DefaultEditorKit#nextWordAction} + * |c_<S-Tab>| TODO + * |c_<S-Up>| {@link com.maddyhome.idea.vim.ui.ExEditorKit.HistoryUpAction} + * |c_<Tab>| TODO + * |c_<Up>| {@link com.maddyhome.idea.vim.ui.ExEditorKit.HistoryUpFilterAction} + * |c_digraph| {char1} <BS> {char2} {@link com.maddyhome.idea.vim.ui.ExEditorKit.StartDigraphAction} + * |c_wildchar| TODO + * |'cedit'| TODO + * * * 6. Ex commands * @@ -621,6 +690,9 @@ * |:quitall| {@link com.maddyhome.idea.vim.ex.handler.ExitHandler} * |:wqall| {@link com.maddyhome.idea.vim.ex.handler.ExitHandler} * |:xall| {@link com.maddyhome.idea.vim.ex.handler.ExitHandler} + * |:command| {@link com.maddyhome.idea.vim.ex.handler.CmdHandler} [To Be Released] + * |:delcommand| {@link com.maddyhome.idea.vim.ex.handler.DelCmdHandler} [To Be Released] + * |:comclear| {@link com.maddyhome.idea.vim.ex.handler.CmdClearHandler} [To Be Released] * ... * * The list of supported Ex commands is incomplete. diff --git a/src/com/maddyhome/idea/vim/ui/ExDocument.java b/src/com/maddyhome/idea/vim/ui/ExDocument.java index 73b33bc6f..b908810da 100644 --- a/src/com/maddyhome/idea/vim/ui/ExDocument.java +++ b/src/com/maddyhome/idea/vim/ui/ExDocument.java @@ -20,10 +20,7 @@ package com.maddyhome.idea.vim.ui; import org.jetbrains.annotations.NotNull; -import javax.swing.text.AttributeSet; -import javax.swing.text.BadLocationException; -import javax.swing.text.Document; -import javax.swing.text.PlainDocument; +import javax.swing.text.*; /** * This document provides insert/overwrite mode @@ -32,7 +29,7 @@ public class ExDocument extends PlainDocument { /** * Toggles the insert/overwrite state */ - public void toggleInsertReplace() { + void toggleInsertReplace() { overwrite = !overwrite; } @@ -72,5 +69,19 @@ public class ExDocument extends PlainDocument { } } - protected boolean overwrite = false; + 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'; + } + } + + private boolean overwrite = false; } diff --git a/src/com/maddyhome/idea/vim/ui/ExEditorKit.java b/src/com/maddyhome/idea/vim/ui/ExEditorKit.java index 049aa3c70..ed691b42a 100644 --- a/src/com/maddyhome/idea/vim/ui/ExEditorKit.java +++ b/src/com/maddyhome/idea/vim/ui/ExEditorKit.java @@ -81,13 +81,13 @@ public class ExEditorKit extends DefaultEditorKit { } @Nullable - public static KeyStroke convert(@NotNull ActionEvent event) { + private static KeyStroke convert(@NotNull ActionEvent event) { String cmd = event.getActionCommand(); int mods = event.getModifiers(); if (cmd != null && cmd.length() > 0) { char ch = cmd.charAt(0); if (ch < ' ') { - if (mods == KeyEvent.CTRL_MASK) { + if ((mods & KeyEvent.CTRL_MASK) != 0) { return KeyStroke.getKeyStroke(KeyEvent.VK_A + ch - 1, mods); } } @@ -99,25 +99,22 @@ public class ExEditorKit extends DefaultEditorKit { return null; } - public static final String DefaultExKey = "default-ex-key"; - public static final String CancelEntry = "cancel-entry"; - public static final String CompleteEntry = "complete-entry"; - public static final String EscapeChar = "escape"; - public static final String DeletePreviousChar = "delete-prev-char"; - public static final String DeletePreviousWord = "delete-prev-word"; - public static final String DeleteToCursor = "delete-to-cursor"; - public static final String DeleteFromCursor = "delete-from-cursor"; - public static final String ToggleInsertReplace = "toggle-insert"; - public static final String InsertRegister = "insert-register"; - public static final String InsertWord = "insert-word"; - public static final String InsertWORD = "insert-WORD"; - public static final String HistoryUp = "history-up"; - public static final String HistoryDown = "history-down"; - public static final String HistoryUpFilter = "history-up-filter"; - public static final String HistoryDownFilter = "history-down-filter"; - public static final String StartDigraph = "start-digraph"; + static final String CancelEntry = "cancel-entry"; + static final String CompleteEntry = "complete-entry"; + static final String EscapeChar = "escape"; + static final String DeletePreviousChar = "delete-prev-char"; + static final String DeletePreviousWord = "delete-prev-word"; + static final String DeleteToCursor = "delete-to-cursor"; + static final String DeleteFromCursor = "delete-from-cursor"; + static final String ToggleInsertReplace = "toggle-insert"; + static final String InsertRegister = "insert-register"; + static final String HistoryUp = "history-up"; + static final String HistoryDown = "history-down"; + static final String HistoryUpFilter = "history-up-filter"; + static final String HistoryDownFilter = "history-down-filter"; + static final String StartDigraph = "start-digraph"; - @NotNull protected final Action[] exActions = new Action[]{ + @NotNull private final Action[] exActions = new Action[]{ new ExEditorKit.CancelEntryAction(), new ExEditorKit.CompleteEntryAction(), new ExEditorKit.EscapeCharAction(), @@ -160,8 +157,12 @@ public class ExEditorKit extends DefaultEditorKit { } } + public interface MultiStepAction extends Action { + void reset(); + } + public static class HistoryUpAction extends TextAction { - public HistoryUpAction() { + HistoryUpAction() { super(HistoryUp); } @@ -172,7 +173,7 @@ public class ExEditorKit extends DefaultEditorKit { } public static class HistoryDownAction extends TextAction { - public HistoryDownAction() { + HistoryDownAction() { super(HistoryDown); } @@ -183,7 +184,7 @@ public class ExEditorKit extends DefaultEditorKit { } public static class HistoryUpFilterAction extends TextAction { - public HistoryUpFilterAction() { + HistoryUpFilterAction() { super(HistoryUpFilter); } @@ -194,7 +195,7 @@ public class ExEditorKit extends DefaultEditorKit { } public static class HistoryDownFilterAction extends TextAction { - public HistoryDownFilterAction() { + HistoryDownFilterAction() { super(HistoryDownFilter); } @@ -204,15 +205,15 @@ public class ExEditorKit extends DefaultEditorKit { } } - public static class InsertRegisterAction extends TextAction { - private static enum State { + public static class InsertRegisterAction extends TextAction implements MultiStepAction { + private enum State { SKIP_CTRL_R, WAIT_REGISTER, } @NotNull private State state = State.SKIP_CTRL_R; - public InsertRegisterAction() { + InsertRegisterAction() { super(InsertRegister); } @@ -223,11 +224,12 @@ public class ExEditorKit extends DefaultEditorKit { switch (state) { case SKIP_CTRL_R: state = State.WAIT_REGISTER; - target.setCurrentAction(this); + target.setCurrentAction(this, '\"'); break; + case WAIT_REGISTER: state = State.SKIP_CTRL_R; - target.setCurrentAction(null); + target.clearCurrentAction(); final char c = key.getKeyChar(); if (c != KeyEvent.CHAR_UNDEFINED) { final Register register = VimPlugin.getRegister().getRegister(c); @@ -235,20 +237,27 @@ public class ExEditorKit extends DefaultEditorKit { final String oldText = target.getText(); final String text = register.getText(); if (oldText != null && text != null) { - target.setText(oldText + text); + final int offset = target.getCaretPosition(); + target.setText(oldText.substring(0, offset) + text + oldText.substring(offset)); + target.setCaretPosition(offset + text.length()); } } - } - else { + } else if ((key.getModifiers() & KeyEvent.CTRL_MASK) != 0 && key.getKeyCode() == KeyEvent.VK_C) { + // Eat any unused keys, unless it's <C-C>, in which case forward on and cancel entry target.handleKey(key); } } } } + + @Override + public void reset() { + state = State.SKIP_CTRL_R; + } } public static class CompleteEntryAction extends TextAction { - public CompleteEntryAction() { + CompleteEntryAction() { super(CompleteEntry); } @@ -256,27 +265,29 @@ public class ExEditorKit extends DefaultEditorKit { logger.debug("complete entry"); KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0); - KeyHandler.getInstance().handleKey( - ExEntryPanel.getInstance().getEntry().getEditor(), - stroke, - ExEntryPanel.getInstance().getEntry().getContext()); + // We send the <Enter> keystroke through the key handler rather than calling ProcessGroup#processExEntry directly. + // We do this for a couple of reasons: + // * The C mode mapping for ProcessExEntryAction handles the actual entry, and most importantly, it does so as a + // write action + // * The key handler routines get the chance to clean up and reset state + final ExTextField entry = ExEntryPanel.getInstance().getEntry(); + KeyHandler.getInstance().handleKey(entry.getEditor(), stroke, entry.getContext()); } } public static class CancelEntryAction extends TextAction { - public CancelEntryAction() { + CancelEntryAction() { super(CancelEntry); } public void actionPerformed(ActionEvent e) { - VimPlugin.getProcess().cancelExEntry( - ExEntryPanel.getInstance().getEntry().getEditor(), - ExEntryPanel.getInstance().getEntry().getContext()); + ExTextField target = (ExTextField)getTextComponent(e); + target.cancel(); } } public static class EscapeCharAction extends TextAction { - public EscapeCharAction() { + EscapeCharAction() { super(EscapeChar); } @@ -287,7 +298,7 @@ public class ExEditorKit extends DefaultEditorKit { } public static class DeletePreviousCharAction extends TextAction { - public DeletePreviousCharAction() { + DeletePreviousCharAction() { super(DeletePreviousChar); } @@ -323,9 +334,7 @@ public class ExEditorKit extends DefaultEditorKit { doc.remove(dot - delChars, delChars); } else { - VimPlugin.getProcess().cancelExEntry( - ExEntryPanel.getInstance().getEntry().getEditor(), - ExEntryPanel.getInstance().getEntry().getContext()); + target.cancel(); } } catch (BadLocationException bl) { @@ -335,7 +344,7 @@ public class ExEditorKit extends DefaultEditorKit { } public static class DeletePreviousWordAction extends TextAction { - public DeletePreviousWordAction() { + DeletePreviousWordAction() { super(DeletePreviousWord); } @@ -362,7 +371,7 @@ public class ExEditorKit extends DefaultEditorKit { } public static class DeleteToCursorAction extends TextAction { - public DeleteToCursorAction() { + DeleteToCursorAction() { super(DeleteToCursor); } @@ -385,7 +394,7 @@ public class ExEditorKit extends DefaultEditorKit { } public static class DeleteFromCursorAction extends TextAction { - public DeleteFromCursorAction() { + DeleteFromCursorAction() { super(DeleteFromCursor); } @@ -408,7 +417,7 @@ public class ExEditorKit extends DefaultEditorKit { } public static class ToggleInsertReplaceAction extends TextAction { - public ToggleInsertReplaceAction() { + ToggleInsertReplaceAction() { super(ToggleInsertReplace); logger.debug("ToggleInsertReplaceAction()"); @@ -424,10 +433,10 @@ public class ExEditorKit extends DefaultEditorKit { } } - public static class StartDigraphAction extends TextAction { + public static class StartDigraphAction extends TextAction implements MultiStepAction { @Nullable private DigraphSequence digraphSequence; - public StartDigraphAction() { + StartDigraphAction() { super(StartDigraph); } @@ -437,14 +446,23 @@ public class ExEditorKit extends DefaultEditorKit { if (key != null && digraphSequence != null) { DigraphSequence.DigraphResult res = digraphSequence.processKey(key, target.getEditor()); switch (res.getResult()) { - case DigraphSequence.DigraphResult.RES_BAD: - target.setCurrentAction(null); - target.handleKey(key); + case DigraphSequence.DigraphResult.RES_OK: + target.setCurrentActionPromptCharacter(res.getPromptCharacter()); break; + + case DigraphSequence.DigraphResult.RES_BAD: + target.clearCurrentAction(); + // Eat the character, unless it's <C-C>, in which case, forward on and cancel entry. Note that at some point + // we should support input of control characters + if ((key.getModifiers() & KeyEvent.CTRL_MASK) != 0 && key.getKeyCode() == KeyEvent.VK_C) { + target.handleKey(key); + } + break; + case DigraphSequence.DigraphResult.RES_DONE: final KeyStroke digraph = res.getStroke(); digraphSequence = null; - target.setCurrentAction(null); + target.clearCurrentAction(); if (digraph != null) { target.handleKey(digraph); } @@ -452,11 +470,16 @@ public class ExEditorKit extends DefaultEditorKit { } } else if (key != null && DigraphSequence.isDigraphStart(key)) { - target.setCurrentAction(this); digraphSequence = new DigraphSequence(); - digraphSequence.processKey(key, target.getEditor()); + DigraphSequence.DigraphResult res = digraphSequence.processKey(key, target.getEditor()); + target.setCurrentAction(this, res.getPromptCharacter()); } } + + @Override + public void reset() { + digraphSequence = null; + } } private static ExEditorKit instance; diff --git a/src/com/maddyhome/idea/vim/ui/ExEntryPanel.java b/src/com/maddyhome/idea/vim/ui/ExEntryPanel.java index 06102ee2e..c7e45daa3 100644 --- a/src/com/maddyhome/idea/vim/ui/ExEntryPanel.java +++ b/src/com/maddyhome/idea/vim/ui/ExEntryPanel.java @@ -79,6 +79,8 @@ public class ExEntryPanel extends JPanel implements LafManagerListener { } }; + new ExShortcutKeyAction(this).registerCustomShortcutSet(); + LafManager.getInstance().addLafManagerListener(this); updateUI(); @@ -93,6 +95,7 @@ public class ExEntryPanel extends JPanel implements LafManagerListener { private void setFontForElements() { final Font font = UiHelper.getEditorFont(); label.setFont(font); + entry.setFont(font); } /** @@ -105,11 +108,11 @@ public class ExEntryPanel extends JPanel implements LafManagerListener { * @param count A holder for the ex entry count */ public void activate(@NotNull Editor editor, DataContext context, @NotNull String label, String initText, int count) { - entry.setEditor(editor, context); this.label.setText(label); this.count = count; setFontForElements(); - entry.setDocument(entry.createDefaultModel()); + entry.reset(); + entry.setEditor(editor, context); entry.setText(initText); entry.setType(label); parent = editor.getContentComponent(); @@ -139,7 +142,7 @@ public class ExEntryPanel extends JPanel implements LafManagerListener { public void updateUI() { super.updateUI(); - setBorder(BorderFactory.createEtchedBorder()); + setBorder(new ExPanelBorder()); // Can be null when called from base constructor //noinspection ConstantConditions @@ -229,6 +232,8 @@ public class ExEntryPanel extends JPanel implements LafManagerListener { logger.info("deactivate"); if (!active) return; active = false; + entry.deactivate(); + if (!ApplicationManager.getApplication().isUnitTestMode()) { if (refocusOwningEditor && parent != null) { UiHelper.requestFocus(parent); @@ -279,7 +284,7 @@ public class ExEntryPanel extends JPanel implements LafManagerListener { @NotNull private final DocumentListener documentListener = new DocumentAdapter() { @Override - protected void textChanged(DocumentEvent e) { + protected void textChanged(@NotNull DocumentEvent e) { final Editor editor = entry.getEditor(); final boolean forwards = !label.getText().equals("?"); if (incHighlighter != null) { @@ -300,6 +305,5 @@ public class ExEntryPanel extends JPanel implements LafManagerListener { private boolean active; private static ExEntryPanel instance; - private static final Logger logger = Logger.getInstance(ExEntryPanel.class.getName()); } diff --git a/src/com/maddyhome/idea/vim/ui/ExKeyBindings.java b/src/com/maddyhome/idea/vim/ui/ExKeyBindings.java index 47f41c122..efd9697b9 100644 --- a/src/com/maddyhome/idea/vim/ui/ExKeyBindings.java +++ b/src/com/maddyhome/idea/vim/ui/ExKeyBindings.java @@ -24,20 +24,20 @@ import javax.swing.*; import javax.swing.text.JTextComponent.KeyBinding; import java.awt.event.KeyEvent; -/** - * - */ public class ExKeyBindings { @NotNull - public static KeyBinding[] getBindings() { + static KeyBinding[] getBindings() { return bindings; } // TODO - add the following keys: // Ctrl-\ Ctrl-N - abort static final KeyBinding[] bindings = new KeyBinding[]{ + // Note that escape will cancel a pending insert digraph/register before cancelling new KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), ExEditorKit.EscapeChar), new KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_OPEN_BRACKET, KeyEvent.CTRL_MASK), ExEditorKit.EscapeChar), + + // Cancel immediately cancels entry new KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_C, KeyEvent.CTRL_MASK), ExEditorKit.CancelEntry), new KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), ExEditorKit.CompleteEntry), @@ -82,9 +82,10 @@ public class ExKeyBindings { new KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_V, KeyEvent.CTRL_MASK), ExEditorKit.StartDigraph), new KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_Q, KeyEvent.CTRL_MASK), ExEditorKit.StartDigraph), + new KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_R, KeyEvent.CTRL_MASK), ExEditorKit.InsertRegister), + + // These appear to be non-Vim shortcuts new KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_V, KeyEvent.META_MASK), ExEditorKit.pasteAction), new KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, KeyEvent.SHIFT_MASK), ExEditorKit.pasteAction), - - new KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_R, KeyEvent.CTRL_MASK), ExEditorKit.InsertRegister), }; } diff --git a/src/com/maddyhome/idea/vim/ui/ExOutputPanel.java b/src/com/maddyhome/idea/vim/ui/ExOutputPanel.java index 374467f31..4ffc999e3 100644 --- a/src/com/maddyhome/idea/vim/ui/ExOutputPanel.java +++ b/src/com/maddyhome/idea/vim/ui/ExOutputPanel.java @@ -113,7 +113,7 @@ public class ExOutputPanel extends JPanel implements LafManagerListener { public void updateUI() { super.updateUI(); - setBorder(BorderFactory.createEtchedBorder()); + setBorder(new ExPanelBorder()); // Can be null when called from base constructor //noinspection ConstantConditions diff --git a/src/com/maddyhome/idea/vim/ui/ExPanelBorder.kt b/src/com/maddyhome/idea/vim/ui/ExPanelBorder.kt new file mode 100644 index 000000000..20711450f --- /dev/null +++ b/src/com/maddyhome/idea/vim/ui/ExPanelBorder.kt @@ -0,0 +1,22 @@ +package com.maddyhome.idea.vim.ui + +import com.intellij.ui.JBColor +import com.intellij.ui.SideBorder +import com.intellij.util.ui.JBInsets + +import java.awt.* + +class ExPanelBorder internal constructor() : SideBorder(JBColor.border(), SideBorder.TOP) { + + override fun getBorderInsets(component: Component?): Insets { + return JBInsets(getThickness() + 2, 0, 2, 2) + } + + override fun getBorderInsets(component: Component?, insets: Insets): Insets { + insets.top = getThickness() + 2 + insets.left = 0 + insets.bottom = 2 + insets.right = 2 + return insets + } +} diff --git a/src/com/maddyhome/idea/vim/ui/ExShortcutKeyAction.kt b/src/com/maddyhome/idea/vim/ui/ExShortcutKeyAction.kt new file mode 100644 index 000000000..f0e402211 --- /dev/null +++ b/src/com/maddyhome/idea/vim/ui/ExShortcutKeyAction.kt @@ -0,0 +1,50 @@ +package com.maddyhome.idea.vim.ui + +import com.intellij.openapi.actionSystem.* +import com.maddyhome.idea.vim.VimPlugin +import java.awt.event.KeyEvent +import javax.swing.KeyStroke + +/** + * An IntelliJ action to forward shortcuts to the ex entry component + * <p> + * Key events are processed by the IDE action system before they are dispatched to the actual component, which means + * they take precedence over the keyboard shortcuts registered with the ex component as Swing actions. This can cause + * clashes such as <C-R> invoking the Run action instead of the Paste Register ex action, or <BS> being handled by the + * editor rather than allowing us to cancel the ex entry. + * <p> + * This class is an IDE action that is registered with the ex entry component, so is available only when the ex entry + * 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() { + + override fun actionPerformed(e: AnActionEvent) { + val keyStroke = getKeyStroke(e) + if (keyStroke != null) { + VimPlugin.getProcess().processExKey(exEntryPanel.entry.editor, keyStroke) + } + } + + override fun update(e: AnActionEvent) { + e.presentation.isEnabled = exEntryPanel.isActive + } + + private fun getKeyStroke(e: AnActionEvent): KeyStroke? { + val inputEvent = e.inputEvent + if (inputEvent is KeyEvent) { + return KeyStroke.getKeyStrokeForEvent(inputEvent) + } + return null + } + + fun registerCustomShortcutSet() { + + val shortcuts = ExKeyBindings.bindings.map { + KeyboardShortcut(it.key, null) + }.toTypedArray() + + registerCustomShortcutSet({ shortcuts }, exEntryPanel) + } +} + diff --git a/src/com/maddyhome/idea/vim/ui/ExTextField.java b/src/com/maddyhome/idea/vim/ui/ExTextField.java index 2182fe271..fe554c68a 100644 --- a/src/com/maddyhome/idea/vim/ui/ExTextField.java +++ b/src/com/maddyhome/idea/vim/ui/ExTextField.java @@ -21,48 +21,62 @@ package com.maddyhome.idea.vim.ui; import com.intellij.openapi.actionSystem.DataContext; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.editor.colors.EditorColorsManager; -import com.intellij.openapi.editor.colors.EditorFontType; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Disposer; import com.intellij.util.ui.JBUI; import com.maddyhome.idea.vim.VimPlugin; import com.maddyhome.idea.vim.group.HistoryGroup; +import kotlin.text.StringsKt; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; import javax.swing.*; -import javax.swing.text.Document; -import javax.swing.text.Keymap; +import javax.swing.plaf.basic.BasicTextFieldUI; +import javax.swing.text.*; import java.awt.*; -import java.awt.event.FocusEvent; -import java.awt.event.FocusListener; -import java.awt.event.KeyEvent; +import java.awt.event.*; import java.util.Date; import java.util.List; +import static java.lang.Math.max; +import static java.lang.Math.min; + /** * Provides a custom keymap for the text field. The keymap is the VIM Ex command keymapping */ public class ExTextField extends JTextField { ExTextField() { - addFocusListener(new FocusListener() { - @Override - public void focusGained(FocusEvent e) { - setCaretPosition(getText().length()); - } + CommandLineCaret caret = new CommandLineCaret(); + caret.setBlinkRate(getCaret().getBlinkRate()); + setCaret(caret); + setNormalModeCaret(); + addCaretListener(e -> resetCaret()); + addMouseListener(new MouseAdapter() { @Override - public void focusLost(FocusEvent e) { + public void mouseClicked(MouseEvent e) { + // If we're in the middle of an action (e.g. entering a register to paste, or inserting a digraph), cancel it if + // the mouse is clicked anywhere. Vim's behaviour is to use the mouse click as an event, which can lead to + // something like : !%!C, which I don't believe is documented, or useful + if (currentAction != null) { + clearCurrentAction(); + } + super.mouseClicked(e); } }); } - // Minimize margins and insets. These get added to the default margins in the UI class that we can't override. - // (I.e. DarculaTextFieldUI#getDefaultMargins, MacIntelliJTextFieldUI#getDefaultMargin, WinIntelliJTextFieldUI#getDefaultMargin) - // This is an attempt to mitigate the gap in ExEntryPanel between the label (':', '/', '?') and the text field. - // See VIM-1485 + void reset() { + clearCurrentAction(); + setInsertMode(); + } + + void deactivate() { + clearCurrentAction(); + } + @Override public Insets getMargin() { return JBUI.emptyInsets(); @@ -76,10 +90,11 @@ public class ExTextField extends JTextField { // Called when the LAF is changed, but only if the control is visible @Override public void updateUI() { - super.updateUI(); - - Font font = EditorColorsManager.getInstance().getGlobalScheme().getFont(EditorFontType.PLAIN); - setFont(font); + // Override the default look and feel specific UI so we can have a completely borderless and margin-less text field. + // (See TextFieldWithPopupHandlerUI#getDefaultMargins and derived classes). This allows us to draw the text field + // directly next to the label + setUI(new BasicTextFieldUI()); + invalidate(); setBorder(null); @@ -98,7 +113,7 @@ public class ExTextField extends JTextField { setKeymap(map); } - public void setType(@NotNull String type) { + void setType(@NotNull String type) { String hkey = null; switch (type.charAt(0)) { case '/': @@ -116,11 +131,11 @@ public class ExTextField extends JTextField { } } - public void saveLastEntry() { + void saveLastEntry() { lastEntry = getText(); } - public void selectHistory(boolean isUp, boolean filter) { + void selectHistory(boolean isUp, boolean filter) { int dir = isUp ? -1 : 1; if (histIndex + dir < 0 || histIndex + dir > history.size()) { VimPlugin.indicateError(); @@ -164,7 +179,28 @@ public class ExTextField extends JTextField { } } - private static final String vimExTextFieldDisposeKey = "vimExTextFieldDisposeKey"; + private void updateText(String string) { + super.setText(string); + } + + public void setText(String string) { + super.setText(string); + + saveLastEntry(); + } + + void setEditor(Editor editor, DataContext context) { + this.editor = editor; + this.context = context; + String disposeKey = vimExTextFieldDisposeKey + editor.hashCode(); + Project project = editor.getProject(); + if (Disposer.get(disposeKey) == null && project != null) { + Disposer.register(project, () -> { + this.editor = null; + this.context = null; + }, disposeKey); + } + } public Editor getEditor() { return editor; @@ -186,21 +222,25 @@ public class ExTextField extends JTextField { c = Character.toChars(codePoint)[0]; } } - KeyEvent event = new KeyEvent(this, keyChar != KeyEvent.CHAR_UNDEFINED ? KeyEvent.KEY_TYPED : - (stroke.isOnKeyRelease() ? KeyEvent.KEY_RELEASED : KeyEvent.KEY_PRESSED), - (new Date()).getTime(), modifiers, keyCode, c); - super.processKeyEvent(event); - } + // Make sure the current action sees any subsequent keystrokes, and they're not processed by Swing's action system. + // Note that this will only handle simple characters and any control characters that are already registered against + // ExShortcutKeyAction - any other control characters will can be "stolen" by other IDE actions. + // If we need to capture ANY subsequent keystroke (e.g. for ^V<Tab>, or to stop the Swing standard <C-A> going to + // start of line), we should replace ExShortcutAction with a dispatcher registered with IdeEventQueue#addDispatcher. + // This gets called for ALL events, before the IDE starts to process key events for the action system. We can add a + // dispatcher that checks that the plugin is enabled, checks that the component with the focus is ExTextField, + // dispatch to ExEntryPanel#handleKey and if it's processed, mark the event as consumed. + if (currentAction != null) { + currentAction.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "" + c, modifiers)); + } + else { + KeyEvent event = new KeyEvent(this, keyChar != KeyEvent.CHAR_UNDEFINED ? KeyEvent.KEY_TYPED : + (stroke.isOnKeyRelease() ? KeyEvent.KEY_RELEASED : KeyEvent.KEY_PRESSED), + (new Date()).getTime(), modifiers, keyCode, c); - public void updateText(String string) { - super.setText(string); - } - - public void setText(String string) { - super.setText(string); - - saveLastEntry(); + super.processKeyEvent(event); + } } protected void processKeyEvent(KeyEvent e) { @@ -220,182 +260,270 @@ public class ExTextField extends JTextField { return new ExDocument(); } - public void escape() { + /** + * Cancels current action, if there is one. If not, cancels entry. + */ + void escape() { if (currentAction != null) { - currentAction = null; + clearCurrentAction(); } else { - VimPlugin.getProcess().cancelExEntry(editor, context); + cancel(); } } - public void setCurrentAction(@Nullable Action action) { + /** + * Cancels entry, including any current action. + */ + void cancel() { + clearCurrentAction(); + VimPlugin.getProcess().cancelExEntry(editor, context); + } + + void setCurrentAction(@NotNull ExEditorKit.MultiStepAction action, char pendingIndicator) { this.currentAction = action; + setCurrentActionPromptCharacter(pendingIndicator); + } + + void clearCurrentAction() { + if (currentAction != null) { + currentAction.reset(); + } + currentAction = null; + clearCurrentActionPromptCharacter(); + } + + void setCurrentActionPromptCharacter(char promptCharacter) { + final String text = removePromptCharacter(); + this.currentActionPromptCharacter = promptCharacter; + currentActionPromptCharacterOffset = currentActionPromptCharacterOffset == -1 ? getCaretPosition() : currentActionPromptCharacterOffset; + StringBuilder sb = new StringBuilder(text); + sb.insert(currentActionPromptCharacterOffset, currentActionPromptCharacter); + updateText(sb.toString()); + setCaretPosition(currentActionPromptCharacterOffset); + } + + private void clearCurrentActionPromptCharacter() { + final int offset = getCaretPosition(); + final String text = removePromptCharacter(); + updateText(text); + setCaretPosition(min(offset, text.length())); + currentActionPromptCharacter = '\0'; + currentActionPromptCharacterOffset = -1; + } + + private String removePromptCharacter() { + return currentActionPromptCharacterOffset == -1 + ? getText() + : StringsKt.removeRange(getText(), currentActionPromptCharacterOffset, currentActionPromptCharacterOffset + 1).toString(); } @Nullable - public Action getCurrentAction() { + Action getCurrentAction() { return currentAction; } - public void toggleInsertReplace() { + private void setInsertMode() { + ExDocument doc = (ExDocument)getDocument(); + if (doc.isOverwrite()) { + doc.toggleInsertReplace(); + } + resetCaret(); + } + + void toggleInsertReplace() { ExDocument doc = (ExDocument)getDocument(); doc.toggleInsertReplace(); - - /* - Caret caret; - int width; - if (doc.isOverwrite()) - { - caret = blockCaret; - width = 8; - } - else - { - caret = origCaret; - width = 1; - } - - setCaret(caret); - putClientProperty("caretWidth", new Integer(width)); - */ + resetCaret(); } - /* - private static class BlockCaret extends DefaultCaret - { - public void paint(Graphics g) - { - if(!isVisible()) - return; - - try - { - Rectangle rectangle; - TextUI textui = getComponent().getUI(); - rectangle = textui.modelToView(getComponent(), getDot(), Position.Bias.Forward); - if (rectangle == null || rectangle.width == 0 && rectangle.height == 0) - { - return; - } - if (width > 0 && height > 0 && !_contains(rectangle.x, rectangle.y, rectangle.width, rectangle.height)) - { - Rectangle rectangle1 = g.getClipBounds(); - if (rectangle1 != null && !rectangle1.contains(this)) - { - repaint(); - } - damage(rectangle); - } - g.setColor(getComponent().getCaretColor()); - int i = 8; - //rectangle.x -= i >> 1; - g.fillRect(rectangle.x, rectangle.y, i, rectangle.height - 1); - Document document = getComponent().getDocument(); - if (document instanceof AbstractDocument) - { - Element element = ((AbstractDocument)document).getBidiRootElement(); - if (element != null && element.getElementCount() > 1) - { - int[] flagXPoints = new int[3]; - int[] flagYPoints = new int[3]; - flagXPoints[0] = rectangle.x + i; - flagYPoints[0] = rectangle.y; - flagXPoints[1] = flagXPoints[0]; - flagYPoints[1] = flagYPoints[0] + 4; - flagXPoints[2] = flagXPoints[0] + 4; - flagYPoints[2] = flagYPoints[0]; - g.fillPolygon(flagXPoints, flagYPoints, 3); - } - } - } - catch (BadLocationException badlocationexception) - { - // ignore - } + private void resetCaret() { + if (getCaretPosition() == getText().length() || currentActionPromptCharacterOffset == getText().length() - 1) { + setNormalModeCaret(); + } + else { + ExDocument doc = (ExDocument)getDocument(); + if (doc.isOverwrite()) { + setReplaceModeCaret(); } - - private boolean _contains(int i, int j, int k, int l) - { - int i1 = width; - int j1 = height; - if ((i1 | j1 | k | l) < 0) - { - return false; - } - int k1 = x; - int l1 = y; - if (i < k1 || j < l1) - { - return false; - } - if (k > 0) - { - i1 += k1; - k += i; - if (k <= i) - { - if (i1 >= k1 || k > i1) - { - return false; - } - } - else if (i1 >= k1 && k > i1) - { - return false; - } - } - else if (k1 + i1 < i) - { - return false; - } - if (l > 0) - { - j1 += l1; - l += j; - if (l <= j) - { - if (j1 >= l1 || l > j1) - { - return false; - } - } - else if (j1 >= l1 && l > j1) - { - return false; - } - } - else if (l1 + j1 < j) - { - return false; - } - return true; + else { + setInsertModeCaret(); } + } + } + + // The default cursor shapes for command line are: + // 'c' command-line normal is block + // '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() { + CommandLineCaret caret = (CommandLineCaret) getCaret(); + caret.setBlockMode(); + } + + private void setInsertModeCaret() { + CommandLineCaret caret = (CommandLineCaret) getCaret(); + caret.setMode(CommandLineCaret.CaretMode.VER, 25); + } + + private void setReplaceModeCaret() { + CommandLineCaret caret = (CommandLineCaret) getCaret(); + caret.setMode(CommandLineCaret.CaretMode.HOR, 20); + } + + private static class CommandLineCaret extends DefaultCaret { + + private CaretMode mode; + private int blockPercentage = 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. + final boolean active = isActive(); + if (isVisible()) { + setVisible(false); + } + this.mode = mode; + this.blockPercentage = blockPercentage; + if (active) { + setVisible(true); + } + } + + 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) { + setBlinkRate(lastBlinkRate); + lastBlinkRate = 0; + } + super.focusGained(e); + updateDamage(); + hasFocus = true; + } + + @Override + public void focusLost(FocusEvent e) { + hasFocus = false; + lastBlinkRate = getBlinkRate(); + setBlinkRate(0); + // We might be losing focus while the cursor is flashing, and is currently not visible + setVisible(true); + updateDamage(); + } + + @Override + public void paint(Graphics g) { + if (!isVisible()) return; + + try { + final JTextComponent component = getComponent(); + final Color color = g.getColor(); + + g.setColor(component.getBackground()); + g.setXORMode(component.getCaretColor()); + + // We have to use the deprecated version because we still support 1.8 + final Rectangle r = component.getUI().modelToView(component, getDot(), getDotBias()); + FontMetrics fm = g.getFontMetrics(); + final int boundsHeight = fm.getHeight(); + if (!hasFocus) { + r.setBounds(r.x, r.y, getCaretWidth(fm, 100), boundsHeight); + g.drawRect(r.x, r.y, r.width, r.height); + } + 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); + } + g.setPaintMode(); + g.setColor(color); + } + catch (BadLocationException e) { + // ignore + } + } + + 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); + } + x = r.x; + y = r.y + blockHeight - 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); + } + + final char c = ((ExDocument)getComponent().getDocument()).getCharacter(getComponent().getCaretPosition()); + return fm.charWidth(c); + } + + private int getBlockHeight(int fullHeight) { + if (mode == CaretMode.HOR) { + return max(1, fullHeight * blockPercentage / 100); + } + return fullHeight; + } + } + + @TestOnly + public String getCaretShape() { + CommandLineCaret caret = (CommandLineCaret) getCaret(); + return String.format("%s %d", caret.mode, caret.blockPercentage); } - */ private Editor editor; private DataContext context; private String lastEntry; private List<HistoryGroup.HistoryEntry> history; private int histIndex = 0; - @Nullable private Action currentAction; - // TODO - support block cursor for overwrite mode - //private Caret origCaret; - //private Caret blockCaret; + @Nullable private ExEditorKit.MultiStepAction currentAction; + private char currentActionPromptCharacter; + private int currentActionPromptCharacterOffset = -1; + private static final String vimExTextFieldDisposeKey = "vimExTextFieldDisposeKey"; private static final Logger logger = Logger.getInstance(ExTextField.class.getName()); - - void setEditor(Editor editor, DataContext context) { - this.editor = editor; - this.context = context; - String disposeKey = vimExTextFieldDisposeKey + editor.hashCode(); - Project project = editor.getProject(); - if (Disposer.get(disposeKey) == null && project != null) { - Disposer.register(project, () -> { - this.editor = null; - this.context = null; - }, disposeKey); - } - } } diff --git a/test/org/jetbrains/plugins/ideavim/VimTestCase.java b/test/org/jetbrains/plugins/ideavim/VimTestCase.java index c9a536591..bc1fd403c 100644 --- a/test/org/jetbrains/plugins/ideavim/VimTestCase.java +++ b/test/org/jetbrains/plugins/ideavim/VimTestCase.java @@ -79,6 +79,9 @@ public abstract class VimTestCase extends UsefulTestCase { KeyHandler.getInstance().fullReset(myFixture.getEditor()); Options.getInstance().resetAllOptions(); VimPlugin.getKey().resetKeyMappings(); + + // Make sure the entry text field gets a bounds, or we won't be able to work out caret location + ExEntryPanel.getInstance().getEntry().setBounds(0,0, 100, 25); } protected String getTestDataPath() { diff --git a/test/org/jetbrains/plugins/ideavim/action/MotionActionTest.java b/test/org/jetbrains/plugins/ideavim/action/MotionActionTest.java index 7be86e5e3..b2a1bffc7 100644 --- a/test/org/jetbrains/plugins/ideavim/action/MotionActionTest.java +++ b/test/org/jetbrains/plugins/ideavim/action/MotionActionTest.java @@ -158,41 +158,6 @@ public class MotionActionTest extends VimTestCase { myFixture.checkResult(" 0:<caret>_ 1:a 2:b 3:c \n"); } - // VIM-326 |d| |v_ib| - public void testDeleteInnerBlock() { - typeTextInFile(parseKeys("di)"), - "foo(\"b<caret>ar\")\n"); - myFixture.checkResult("foo()\n"); - } - - // VIM-326 |d| |v_ib| - public void testDeleteInnerBlockCaretBeforeString() { - typeTextInFile(parseKeys("di)"), - "foo(<caret>\"bar\")\n"); - myFixture.checkResult("foo()\n"); - } - - // VIM-326 |c| |v_ib| - public void testChangeInnerBlockCaretBeforeString() { - typeTextInFile(parseKeys("ci)"), - "foo(<caret>\"bar\")\n"); - myFixture.checkResult("foo()\n"); - } - - // VIM-392 |c| |v_ib| - public void testChangeInnerBlockCaretBeforeBlock() { - typeTextInFile(parseKeys("ci)"), - "foo<caret>(bar)\n"); - myFixture.checkResult("foo()\n"); - assertOffset(4); - } - - // |v_ib| - public void testInnerBlockCrashWhenNoDelimiterFound() { - typeTextInFile(parseKeys("di)"), "(x\n"); - myFixture.checkResult("(x\n"); - } - // VIM-314 |d| |v_iB| public void testDeleteInnerCurlyBraceBlock() { typeTextInFile(parseKeys("di{"), @@ -226,30 +191,6 @@ public class MotionActionTest extends VimTestCase { assertOffset(6); } - // VIM-275 |d| |v_ib| - public void testDeleteInnerParensBlockBeforeOpen() { - typeTextInFile(parseKeys("di)"), - "foo<caret>(bar)\n"); - myFixture.checkResult("foo()\n"); - assertOffset(4); - } - - // |d| |v_ib| - public void testDeleteInnerParensBlockBeforeClose() { - typeTextInFile(parseKeys("di)"), - "foo(bar<caret>)\n"); - myFixture.checkResult("foo()\n"); - } - - // |d| |v_ab| - public void testDeleteOuterBlock() { - typeTextInFile(parseKeys("da)"), - "foo(b<caret>ar, baz);\n"); - myFixture.checkResult("foo;\n"); - } - - - // |d| |v_aw| public void testDeleteOuterWord() { typeTextInFile(parseKeys("daw"), @@ -449,350 +390,6 @@ public class MotionActionTest extends VimTestCase { myFixture.checkResult("foo = ;\n"); } - - //|d| |v_it| - public void testDeleteInnerTagBlockCaretInHtml() { - typeTextInFile(parseKeys("dit"), "<template <caret>name=\"hello\">\n" + - " <button>Click Me</button>\n" + - " <p>You've pressed the button {{counter}} times.</p>\n" + - "</template>\n"); - myFixture.checkResult("<template name=\"hello\"></template>\n"); - } - - //|d| |v_it| - public void testDeleteInnerTagBlockCaretInHtmlUnclosedTag() { - typeTextInFile(parseKeys("dit"), "<template <caret>name=\"hello\">\n" + - " <button>Click Me</button>\n" + - " <br>\n" + - " <p>You've pressed the button {{counter}} times.</p>\n" + - "</template>\n"); - myFixture.checkResult("<template name=\"hello\"></template>\n"); - } - - public void testDeleteInnerTagBlockCaretEdgeTag() { - typeTextInFile(parseKeys("dit"), "<template name=\"hello\"<caret>>\n" + - " <button>Click Me</button>\n" + - " <br>\n" + - " <p>You've pressed the button {{counter}} times.</p>\n" + - "</template>\n"); - myFixture.checkResult("<template name=\"hello\"></template>\n"); - } - - //|d| |v_it| - public void testDeleteInnerTagBlockBefore() { - typeTextInFile(parseKeys("dit"), "abc<caret>de<tag>fg</tag>hi"); - myFixture.checkResult("abcde<tag>fg</tag>hi"); - } - - //|d| |v_it| - public void testDeleteInnerTagBlockInOpen() { - typeTextInFile(parseKeys("dit"), "abcde<ta<caret>g>fg</tag>hi"); - myFixture.checkResult("abcde<tag></tag>hi"); - } - - //|d| |v_it| - public void testDeleteInnerTagBlockInOpenEndOfLine() { - typeTextInFile(parseKeys("dit"), "abcde<ta<caret>g>fg</tag>"); - myFixture.checkResult("abcde<tag></tag>"); - } - - //|d| |v_it| - public void testDeleteInnerTagBlockInOpenStartOfLine() { - typeTextInFile(parseKeys("dit"), "<ta<caret>g>fg</tag>hi"); - myFixture.checkResult("<tag></tag>hi"); - } - - //|d| |v_it| - public void testDeleteInnerTagBlockInOpenWithArgs() { - typeTextInFile(parseKeys("dit"), "abcde<ta<caret>g name = \"name\">fg</tag>hi"); - myFixture.checkResult("abcde<tag name = \"name\"></tag>hi"); - } - - //|d| |v_it| - public void testDeleteInnerTagBlockBetween() { - typeTextInFile(parseKeys("dit"), "abcde<tag>f<caret>g</tag>hi"); - myFixture.checkResult("abcde<tag></tag>hi"); - } - - //|d| |v_it| - public void testDeleteInnerTagBlockBetweenTagWithRegex() { - typeTextInFile(parseKeys("dit"), "abcde<[abc]*>af<caret>gbc</[abc]*>hi"); - myFixture.checkResult("abcde<[abc]*></[abc]*>hi"); - } - - //|d| |v_it| - public void testDeleteInnerTagBlockBetweenCamelCase() { - typeTextInFile(parseKeys("dit"), "abcde<tAg>f<caret>g</tag>hi"); - myFixture.checkResult("abcde<tAg></tag>hi"); - } - - //|d| |v_it| - public void testDeleteInnerTagBlockBetweenCaps() { - typeTextInFile(parseKeys("dit"), "abcde<tag>f<caret>g</TAG>hi"); - myFixture.checkResult("abcde<tag></TAG>hi"); - } - - //|d| |v_it| - public void testDeleteInnerTagBlockBetweenWithSpaceBeforeTag() { - typeTextInFile(parseKeys("dit"), "abcde< tag>f<caret>g</ tag>hi"); - myFixture.checkResult("abcde< tag>fg</ tag>hi"); - } - - //|d| |v_it| - public void testDeleteInnerTagBlockBetweenWithSpaceAfterTag() { - typeTextInFile(parseKeys("dit"), "abcde<tag >f<caret>g</tag>hi"); - myFixture.checkResult("abcde<tag ></tag>hi"); - } - - //|d| |v_it| - public void testDeleteInnerTagBlockBetweenWithArgs() { - typeTextInFile(parseKeys("dit"), "abcde<tag name = \"name\">f<caret>g</tag>hi"); - myFixture.checkResult("abcde<tag name = \"name\"></tag>hi"); - } - - //|d| |v_it| - public void testDeleteInnerTagBlockInClose() { - typeTextInFile(parseKeys("dit"), "abcde<tag>fg</ta<caret>g>hi"); - myFixture.checkResult("abcde<tag></tag>hi"); - } - - //|d| |v_it| - public void testDeleteInnerTagBlockAfter() { - typeTextInFile(parseKeys("dit"), "abcde<tag>fg</tag>h<caret>i"); - myFixture.checkResult("abcde<tag>fg</tag>hi"); - } - - //|d| |v_it| - public void testDeleteInnerTagBlockInAlone() { - typeTextInFile(parseKeys("dit"), "abcde<ta<caret>g>fghi"); - myFixture.checkResult("abcde<tag>fghi"); - } - - //|d| |v_it| - public void testDeleteInnerTagBlockWithoutTags() { - typeTextInFile(parseKeys("dit"), "abc<caret>de"); - myFixture.checkResult("abcde"); - } - - //|d| |v_it| - public void testDeleteInnerTagBlockBeforeWithoutOpenTag() { - typeTextInFile(parseKeys("dit"), "abc<caret>defg</tag>hi"); - myFixture.checkResult("abcdefg</tag>hi"); - } - - //|d| |v_it| - public void testDeleteInnerTagBlockInCloseWithoutOpenTag() { - typeTextInFile(parseKeys("dit"), "abcdefg</ta<caret>g>hi"); - myFixture.checkResult("abcdefg</tag>hi"); - } - - //|d| |v_it| - public void testDeleteInnerTagBlockAfterWithoutOpenTag() { - typeTextInFile(parseKeys("dit"), "abcdefg</tag>h<caret>i"); - myFixture.checkResult("abcdefg</tag>hi"); - } - - //|d| |v_it| - public void testDeleteInnerTagBlockBeforeWithoutCloseTag() { - typeTextInFile(parseKeys("dit"), "abc<caret>defg<tag>hi"); - myFixture.checkResult("abcdefg<tag>hi"); - } - - //|d| |v_it| - public void testDeleteInnerTagBlockInOpenWithoutCloseTag() { - typeTextInFile(parseKeys("dit"), "abcdefg<ta<caret>g>hi"); - myFixture.checkResult("abcdefg<tag>hi"); - } - - //|d| |v_it| - public void testDeleteInnerTagBlockAfterWithoutCloseTag() { - typeTextInFile(parseKeys("dit"), "abcdefg<tag>h<caret>i"); - myFixture.checkResult("abcdefg<tag>hi"); - } - - //|d| |v_it| - public void testDeleteInnerTagBlockBeforeWrongOrder() { - typeTextInFile(parseKeys("dit"), "abc<caret>de</tag>fg<tag>hi"); - myFixture.checkResult("abcde</tag>fg<tag>hi"); - } - - //|d| |v_it| - public void testDeleteInnerTagBlockInOpenWrongOrder() { - typeTextInFile(parseKeys("dit"), "abcde</ta<caret>g>fg<tag>hi"); - myFixture.checkResult("abcde</tag>fg<tag>hi"); - } - - //|d| |v_it| - public void testDeleteInnerTagBlockBetweenWrongOrder() { - typeTextInFile(parseKeys("dit"), "abcde</tag>f<caret>g<tag>hi"); - myFixture.checkResult("abcde</tag>fg<tag>hi"); - } - - //|d| |v_it| - public void testDeleteInnerTagBlockInCloseWrongOrder() { - typeTextInFile(parseKeys("dit"), "abcde</tag>fg<ta<caret>g>hi"); - myFixture.checkResult("abcde</tag>fg<tag>hi"); - } - - //|d| |v_it| - public void testDeleteInnerTagBlockTwoTagsWrongOrder() { - typeTextInFile(parseKeys("dit"), "<foo><html>t<caret>ext</foo></html>"); - myFixture.checkResult("<foo></foo></html>"); - } - - //|d| |v_it| - public void testDeleteInnerTagBlockTwoTagsWrongOrderInClosingTag() { - typeTextInFile(parseKeys("dit"), "<foo><html>text</foo></htm<caret>l>"); - myFixture.checkResult("<foo><html></html>"); - } - - //|d| |v_it| - public void testDeleteInnerTagBlockAfterWrongOrder() { - typeTextInFile(parseKeys("dit"), "abcde</tag>fg<tag>h<caret>i"); - myFixture.checkResult("abcde</tag>fg<tag>hi"); - } - - //|d| |v_it| - public void testDeleteInnerTagBlockBracketInside() { - typeTextInFile(parseKeys("dit"), "abcde<tag>f<caret><>g</tag>hi"); - myFixture.checkResult("abcde<tag></tag>hi"); - } - - //|d| |v_it| - public void testDeleteInnerTagBlockBracketInsideString() { - typeTextInFile(parseKeys("dit"), "abcde<tag>f<caret>\"<>\"g</tag>hi"); - myFixture.checkResult("abcde<tag></tag>hi"); - } - - //|d| |v_at| - public void testDeleteOuterTagBlockBefore() { - typeTextInFile(parseKeys("dat"), "abc<caret>de<tag>fg</tag>hi"); - myFixture.checkResult("abcde<tag>fg</tag>hi"); - } - - //|d| |v_at| - public void testDeleteOuterTagBlockInOpen() { - typeTextInFile(parseKeys("dat"), "abcde<ta<caret>g>fg</tag>hi"); - myFixture.checkResult("abcdehi"); - } - - //|d| |v_at| - public void testDeleteOuterTagBlockInOpenWithArgs() { - typeTextInFile(parseKeys("dat"), "abcde<ta<caret>g name = \"name\">fg</tag>hi"); - myFixture.checkResult("abcdehi"); - } - - //|d| |v_at| - public void testDeleteOuterTagBlockBetween() { - typeTextInFile(parseKeys("dat"), "abcde<tag>f<caret>g</tag>hi"); - myFixture.checkResult("abcdehi"); - } - - //|d| |v_at| - public void testDeleteOuterTagBlockBetweenWithArgs() { - typeTextInFile(parseKeys("dat"), "abcde<tag name = \"name\">f<caret>g</tag>hi"); - myFixture.checkResult("abcdehi"); - } - - //|d| |v_at| - public void testDeleteOuterTagBlockInClose() { - typeTextInFile(parseKeys("dat"), "abcde<tag>fg</ta<caret>g>hi"); - myFixture.checkResult("abcdehi"); - } - - //|d| |v_at| - public void testDeleteOuterTagBlockAfter() { - typeTextInFile(parseKeys("dat"), "abcde<tag>fg</tag>h<caret>i"); - myFixture.checkResult("abcde<tag>fg</tag>hi"); - } - - //|d| |v_at| - public void testDeleteOuterTagBlockInAlone() { - typeTextInFile(parseKeys("dat"), "abcde<ta<caret>g>fghi"); - myFixture.checkResult("abcde<tag>fghi"); - } - - //|d| |v_at| - public void testDeleteOuterTagBlockWithoutTags() { - typeTextInFile(parseKeys("dat"), "abc<caret>de"); - myFixture.checkResult("abcde"); - } - - //|d| |v_at| - public void testDeleteOuterTagBlockBeforeWithoutOpenTag() { - typeTextInFile(parseKeys("dat"), "abc<caret>defg</tag>hi"); - myFixture.checkResult("abcdefg</tag>hi"); - } - - //|d| |v_at| - public void testDeleteOuterTagBlockInCloseWithoutOpenTag() { - typeTextInFile(parseKeys("dat"), "abcdefg</ta<caret>g>hi"); - myFixture.checkResult("abcdefg</tag>hi"); - } - - //|d| |v_at| - public void testDeleteOuterTagBlockAfterWithoutOpenTag() { - typeTextInFile(parseKeys("dat"), "abcdefg</tag>h<caret>i"); - myFixture.checkResult("abcdefg</tag>hi"); - } - - //|d| |v_at| - public void testDeleteOuterTagBlockBeforeWithoutCloseTag() { - typeTextInFile(parseKeys("dat"), "abc<caret>defg<tag>hi"); - myFixture.checkResult("abcdefg<tag>hi"); - } - - //|d| |v_at| - public void testDeleteOuterTagBlockInOpenWithoutCloseTag() { - typeTextInFile(parseKeys("dat"), "abcdefg<ta<caret>g>hi"); - myFixture.checkResult("abcdefg<tag>hi"); - } - - //|d| |v_at| - public void testDeleteOuterTagBlockAfterWithoutCloseTag() { - typeTextInFile(parseKeys("dat"), "abcdefg<tag>h<caret>i"); - myFixture.checkResult("abcdefg<tag>hi"); - } - - //|d| |v_at| - public void testDeleteOuterTagBlockBeforeWrongOrder() { - typeTextInFile(parseKeys("dat"), "abc<caret>de</tag>fg<tag>hi"); - myFixture.checkResult("abcde</tag>fg<tag>hi"); - } - - //|d| |v_at| - public void testDeleteOuterTagBlockInOpenWrongOrder() { - typeTextInFile(parseKeys("dat"), "abcde</ta<caret>g>fg<tag>hi"); - myFixture.checkResult("abcde</tag>fg<tag>hi"); - } - - //|d| |v_at| - public void testDeleteOuterTagBlockBetweenWrongOrder() { - typeTextInFile(parseKeys("dat"), "abcde</tag>f<caret>g<tag>hi"); - myFixture.checkResult("abcde</tag>fg<tag>hi"); - } - - //|d| |v_at| - public void testDeleteOuterTagBlockInCloseWrongOrder() { - typeTextInFile(parseKeys("dat"), "abcde</tag>fg<ta<caret>g>hi"); - myFixture.checkResult("abcde</tag>fg<tag>hi"); - } - - //|d| |v_at| - public void testDeleteOuterTagBlockAfterWrongOrder() { - typeTextInFile(parseKeys("dat"), "abcde</tag>fg<tag>h<caret>i"); - myFixture.checkResult("abcde</tag>fg<tag>hi"); - } - - // |v_it| - public void testFileStartsWithSlash() { - configureByText("/*hello\n" + - "<caret>foo\n" + - "bar>baz\n"); - typeText(parseKeys("vit")); - assertPluginError(true); - } - // VIM-1427 public void testDeleteOuterTagWithCount() { typeTextInFile(parseKeys("d2at"),"<a><b><c><caret></c></b></a>"); diff --git a/test/org/jetbrains/plugins/ideavim/action/RepeatActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/RepeatActionTest.kt new file mode 100644 index 000000000..d07a8c30d --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/RepeatActionTest.kt @@ -0,0 +1,73 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2019 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 <http://www.gnu.org/licenses/>. + */ + +package org.jetbrains.plugins.ideavim.action + +import org.jetbrains.plugins.ideavim.VimTestCase +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys +import org.junit.Test + +class RepeatActionTest : VimTestCase() { + + @Test + fun testSimpleRepeatLastCommand() { + configureByText("foo foo") + typeText(parseKeys("cw", "bar", "<Esc>", "w", ".")) + myFixture.checkResult("bar bar") + } + + @Test + fun testRepeatChangeToCharInNextLine() { + configureByText("The first line.\n" + + "This is the second line.\n" + + "Third line here, with a comma.\n" + + "Last line.") + typeText(parseKeys("j", "ct.", "Change the line to point", "<Esc>", "j0", ".")) + myFixture.checkResult("The first line.\n" + + "Change the line to point.\n" + + "Change the line to point.\n" + + "Last line.") + } + + // VIM-1644 + @Test + fun testRepeatChangeInVisualMode() { + configureByText("foobar foobar") + typeText(parseKeys("<C-V>llc", "fu", "<Esc>", "w", ".")) + myFixture.checkResult("fubar fubar") + } + + // VIM-1644 + @Test + fun testRepeatChangeInVisualModeMultiline() { + configureByText( + "There is a red house.\n" + + "Another red house there.\n" + + "They have red windows.\n" + + "Good." + ) + typeText(parseKeys("www", "<C-V>ec", "blue", "<Esc>", "j0w.", "j0ww.")) + myFixture.checkResult( + "There is a blue house.\n" + + "Another blue house there.\n" + + "They have blue windows.\n" + + "Good." + ) + } + +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/action/change/insert/InsertDeleteInsertedTextActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/change/insert/InsertDeleteInsertedTextActionTest.kt new file mode 100644 index 000000000..9a44baca7 --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/change/insert/InsertDeleteInsertedTextActionTest.kt @@ -0,0 +1,52 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2019 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 <http://www.gnu.org/licenses/>. + */ + +package org.jetbrains.plugins.ideavim.action.change.insert + +import com.maddyhome.idea.vim.command.CommandState +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys +import org.jetbrains.plugins.ideavim.VimTestCase + +class InsertDeleteInsertedTextActionTest : VimTestCase() { + // VIM-1655 + fun `test deleted text is not yanked`() { + doTest(parseKeys("yiw", "ea", "Hello", "<C-U>", "<ESC>p"), """ + A Discovery + + I found <caret>it in a legendary land + """.trimIndent(), """ + A Discovery + + I found iti<caret>t in a legendary land + """.trimIndent(), CommandState.Mode.COMMAND, CommandState.SubMode.NONE) + } + + // VIM-1655 + // VimBehaviourDiffers. Inserted text is not deleted after <C-U> + fun `test deleted text is not yanked after replace`() { + doTest(parseKeys("yiw", "eR", "Hello", "<C-U>", "<ESC>p"), """ + A Discovery + + I found <caret>it in a legendary land + """.trimIndent(), """ + A Discovery + + I found ii<caret>ta legendary land + """.trimIndent(), CommandState.Mode.COMMAND, CommandState.SubMode.NONE) + } +} \ No newline at end of file diff --git a/src/com/maddyhome/idea/vim/action/ex/CancelExEntryAction.java b/test/org/jetbrains/plugins/ideavim/action/change/insert/InsertDeletePreviousWordActionTest.kt similarity index 50% rename from src/com/maddyhome/idea/vim/action/ex/CancelExEntryAction.java rename to test/org/jetbrains/plugins/ideavim/action/change/insert/InsertDeletePreviousWordActionTest.kt index 4257cdfa5..2b041e02b 100644 --- a/src/com/maddyhome/idea/vim/action/ex/CancelExEntryAction.java +++ b/test/org/jetbrains/plugins/ideavim/action/change/insert/InsertDeletePreviousWordActionTest.kt @@ -16,26 +16,23 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package com.maddyhome.idea.vim.action.ex; +package org.jetbrains.plugins.ideavim.action.change.insert -import com.intellij.openapi.actionSystem.DataContext; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.editor.actionSystem.EditorAction; -import com.maddyhome.idea.vim.VimPlugin; -import com.maddyhome.idea.vim.command.Command; -import com.maddyhome.idea.vim.handler.EditorActionHandlerBase; -import org.jetbrains.annotations.NotNull; +import com.maddyhome.idea.vim.command.CommandState +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys +import org.jetbrains.plugins.ideavim.VimTestCase -/** - */ -public class CancelExEntryAction extends EditorAction { - public CancelExEntryAction() { - super(new Handler()); - } +class InsertDeletePreviousWordActionTest : VimTestCase() { + // VIM-1655 + fun `test deleted word is not yanked`() { + doTest(parseKeys("yiw", "3wea", "<C-W>", "<ESC>p"), """ + A Discovery - private static class Handler extends EditorActionHandlerBase { - protected boolean execute(@NotNull Editor editor, @NotNull DataContext context, @NotNull Command cmd) { - return VimPlugin.getProcess().cancelExEntry(editor, context); + I found <caret>it in a legendary land + """.trimIndent(), """ + A Discovery + + I found it in a i<caret>t land + """.trimIndent(), CommandState.Mode.COMMAND, CommandState.SubMode.NONE) } - } -} +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionRightActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionRightActionTest.kt index 656f0ebc0..eb0113592 100644 --- a/test/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionRightActionTest.kt +++ b/test/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionRightActionTest.kt @@ -22,7 +22,6 @@ package org.jetbrains.plugins.ideavim.action.motion.leftright import com.maddyhome.idea.vim.command.CommandState import com.maddyhome.idea.vim.helper.StringHelper.parseKeys -import com.maddyhome.idea.vim.helper.VimBehaviourDiffers import org.jetbrains.plugins.ideavim.VimTestCase class MotionRightActionTest : VimTestCase() { @@ -80,14 +79,6 @@ class MotionRightActionTest : VimTestCase() { """.trimIndent(), CommandState.Mode.COMMAND, CommandState.SubMode.NONE) } - @VimBehaviourDiffers(""" - A Discovery - - I found it in a legendar𝛁<caret> land - all rocks and lavender and tufted grass, - where it was settled on some sodden sand - hard by the torrent of a mountain pass. - """) fun `test simple motion non-ascii`() { doTest(parseKeys("l"), """ A Discovery @@ -99,21 +90,13 @@ class MotionRightActionTest : VimTestCase() { """.trimIndent(), """ A Discovery - I found it in a legendar<caret>𝛁 land + I found it in a legendar𝛁<caret> land all rocks and lavender and tufted grass, where it was settled on some sodden sand hard by the torrent of a mountain pass. """.trimIndent(), CommandState.Mode.COMMAND, CommandState.SubMode.NONE) } - @VimBehaviourDiffers(""" - A Discovery - - I found it in a legendar🐔<caret> land - all rocks and lavender and tufted grass, - where it was settled on some sodden sand - hard by the torrent of a mountain pass. - """) fun `test simple motion emoji`() { doTest(parseKeys("l"), """ A Discovery @@ -125,7 +108,25 @@ class MotionRightActionTest : VimTestCase() { """.trimIndent(), """ A Discovery - I found it in a legendar<caret>🐔 land + I found it in a legendar🐔<caret> land + all rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), CommandState.Mode.COMMAND, CommandState.SubMode.NONE) + } + + fun `test simple motion czech`() { + doTest(parseKeys("l"), """ + A Discovery + + I found it in a legendar<caret>ž land + all rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), """ + A Discovery + + I found it in a legendarž<caret> land all rocks and lavender and tufted grass, where it was settled on some sodden sand hard by the torrent of a mountain pass. diff --git a/test/org/jetbrains/plugins/ideavim/action/motion/object/MotionInnerBlockParenActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/motion/object/MotionInnerBlockParenActionTest.kt new file mode 100644 index 000000000..41fb35a6c --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/motion/object/MotionInnerBlockParenActionTest.kt @@ -0,0 +1,129 @@ +package org.jetbrains.plugins.ideavim.action.motion.`object` + +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys +import org.jetbrains.plugins.ideavim.VimTestCase + +class MotionInnerBlockParenActionTest : VimTestCase() { + // VIM-1633 |v_i)| + fun `test single letter with single parentheses`() { + configureByText("(<caret>a)") + typeText(parseKeys("vi)")) + assertSelection("a") + } + + fun `test single letter with double parentheses`() { + configureByText("((<caret>a))") + typeText(parseKeys("vi)")) + assertSelection("(a)") + } + + fun `test multiline outside parentheses`() { + configureByText("""(outer + |<caret>(inner))""".trimMargin()) + typeText(parseKeys("vi)")) + assertSelection("inner") + } + + fun `test multiline in parentheses`() { + configureByText("""(outer + |(inner<caret>))""".trimMargin()) + typeText(parseKeys("vi)")) + assertSelection("inner") + } + + fun `test multiline inside of outer parentheses`() { + configureByText("""(outer + |<caret> (inner))""".trimMargin()) + typeText(parseKeys("vi)")) + assertSelection("""outer + | (inner)""".trimMargin()) + } + + fun `test double motion`() { + configureByText("""(outer + |<caret>(inner))""".trimMargin()) + typeText(parseKeys("vi)i)")) + assertSelection("""outer + |(inner)""".trimMargin()) + } + + fun `test motion with count`() { + configureByText("""(outer + |<caret>(inner))""".trimMargin()) + typeText(parseKeys("v2i)")) + assertSelection("""outer + |(inner)""".trimMargin()) + } + + fun `test text object after motion`() { + configureByText("""(outer + |<caret>(inner))""".trimMargin()) + typeText(parseKeys("vlli)")) + assertSelection("""outer + |(inner)""".trimMargin()) + } + + fun `test text object after motion outside parentheses`() { + configureByText("""(outer + |(inner<caret>))""".trimMargin()) + typeText(parseKeys("vlli)")) + assertSelection("inner") + } + + fun `test text object after motion inside parentheses`() { + configureByText("""(outer + |(<caret>inner))""".trimMargin()) + typeText(parseKeys("vllli)")) + assertSelection("inner") + } + + // VIM-326 |d| |v_ib| + fun testDeleteInnerBlock() { + typeTextInFile(parseKeys("di)"), + "foo(\"b<caret>ar\")\n") + myFixture.checkResult("foo()\n") + } + + // VIM-326 |d| |v_ib| + fun testDeleteInnerBlockCaretBeforeString() { + typeTextInFile(parseKeys("di)"), + "foo(<caret>\"bar\")\n") + myFixture.checkResult("foo()\n") + } + + // VIM-326 |c| |v_ib| + fun testChangeInnerBlockCaretBeforeString() { + typeTextInFile(parseKeys("ci)"), + "foo(<caret>\"bar\")\n") + myFixture.checkResult("foo()\n") + } + + // VIM-392 |c| |v_ib| + fun testChangeInnerBlockCaretBeforeBlock() { + typeTextInFile(parseKeys("ci)"), + "foo<caret>(bar)\n") + myFixture.checkResult("foo()\n") + assertOffset(4) + } + + // |v_ib| + fun testInnerBlockCrashWhenNoDelimiterFound() { + typeTextInFile(parseKeys("di)"), "(x\n") + myFixture.checkResult("(x\n") + } + + // VIM-275 |d| |v_ib| + fun testDeleteInnerParensBlockBeforeOpen() { + typeTextInFile(parseKeys("di)"), + "foo<caret>(bar)\n") + myFixture.checkResult("foo()\n") + assertOffset(4) + } + + // |d| |v_ib| + fun testDeleteInnerParensBlockBeforeClose() { + typeTextInFile(parseKeys("di)"), + "foo(bar<caret>)\n") + myFixture.checkResult("foo()\n") + } +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/action/motion/object/MotionInnerBlockTagActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/motion/object/MotionInnerBlockTagActionTest.kt new file mode 100644 index 000000000..b3bc9f0eb --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/motion/object/MotionInnerBlockTagActionTest.kt @@ -0,0 +1,374 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2019 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 <http://www.gnu.org/licenses/>. + */ + +package org.jetbrains.plugins.ideavim.action.motion.`object` + +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys +import org.jetbrains.plugins.ideavim.VimTestCase + +class MotionInnerBlockTagActionTest : VimTestCase() { + + //|d| |v_it| + fun testDeleteInnerTagBlockCaretInHtml() { + typeTextInFile(parseKeys("dit"), "<template <caret>name=\"hello\">\n" + + " <button>Click Me</button>\n" + + " <p>You've pressed the button {{counter}} times.</p>\n" + + "</template>\n") + myFixture.checkResult("<template name=\"hello\"></template>\n") + } + + //|d| |v_it| + fun testDeleteInnerTagBlockCaretInHtmlUnclosedTag() { + typeTextInFile(parseKeys("dit"), "<template <caret>name=\"hello\">\n" + + " <button>Click Me</button>\n" + + " <br>\n" + + " <p>You've pressed the button {{counter}} times.</p>\n" + + "</template>\n") + myFixture.checkResult("<template name=\"hello\"></template>\n") + } + + fun testDeleteInnerTagBlockCaretEdgeTag() { + typeTextInFile(parseKeys("dit"), "<template name=\"hello\"<caret>>\n" + + " <button>Click Me</button>\n" + + " <br>\n" + + " <p>You've pressed the button {{counter}} times.</p>\n" + + "</template>\n") + myFixture.checkResult("<template name=\"hello\"></template>\n") + } + + //|d| |v_it| + fun testDeleteInnerTagBlockBefore() { + typeTextInFile(parseKeys("dit"), "abc<caret>de<tag>fg</tag>hi") + myFixture.checkResult("abcde<tag>fg</tag>hi") + } + + //|d| |v_it| + fun testDeleteInnerTagBlockInOpen() { + typeTextInFile(parseKeys("dit"), "abcde<ta<caret>g>fg</tag>hi") + myFixture.checkResult("abcde<tag></tag>hi") + } + + //|d| |v_it| + fun testDeleteInnerTagBlockInOpenEndOfLine() { + typeTextInFile(parseKeys("dit"), "abcde<ta<caret>g>fg</tag>") + myFixture.checkResult("abcde<tag></tag>") + } + + //|d| |v_it| + fun testDeleteInnerTagBlockInOpenStartOfLine() { + typeTextInFile(parseKeys("dit"), "<ta<caret>g>fg</tag>hi") + myFixture.checkResult("<tag></tag>hi") + } + + //|d| |v_it| + fun testDeleteInnerTagBlockInOpenWithArgs() { + typeTextInFile(parseKeys("dit"), "abcde<ta<caret>g name = \"name\">fg</tag>hi") + myFixture.checkResult("abcde<tag name = \"name\"></tag>hi") + } + + //|d| |v_it| + fun testDeleteInnerTagBlockBetween() { + typeTextInFile(parseKeys("dit"), "abcde<tag>f<caret>g</tag>hi") + myFixture.checkResult("abcde<tag></tag>hi") + } + + //|d| |v_it| + fun testDeleteInnerTagBlockBetweenTagWithRegex() { + typeTextInFile(parseKeys("dit"), "abcde<[abc]*>af<caret>gbc</[abc]*>hi") + myFixture.checkResult("abcde<[abc]*></[abc]*>hi") + } + + //|d| |v_it| + fun testDeleteInnerTagBlockBetweenCamelCase() { + typeTextInFile(parseKeys("dit"), "abcde<tAg>f<caret>g</tag>hi") + myFixture.checkResult("abcde<tAg></tag>hi") + } + + //|d| |v_it| + fun testDeleteInnerTagBlockBetweenCaps() { + typeTextInFile(parseKeys("dit"), "abcde<tag>f<caret>g</TAG>hi") + myFixture.checkResult("abcde<tag></TAG>hi") + } + + //|d| |v_it| + fun testDeleteInnerTagBlockBetweenWithSpaceBeforeTag() { + typeTextInFile(parseKeys("dit"), "abcde< tag>f<caret>g</ tag>hi") + myFixture.checkResult("abcde< tag>fg</ tag>hi") + } + + //|d| |v_it| + fun testDeleteInnerTagBlockBetweenWithSpaceAfterTag() { + typeTextInFile(parseKeys("dit"), "abcde<tag >f<caret>g</tag>hi") + myFixture.checkResult("abcde<tag ></tag>hi") + } + + //|d| |v_it| + fun testDeleteInnerTagBlockBetweenWithArgs() { + typeTextInFile(parseKeys("dit"), "abcde<tag name = \"name\">f<caret>g</tag>hi") + myFixture.checkResult("abcde<tag name = \"name\"></tag>hi") + } + + //|d| |v_it| + fun testDeleteInnerTagBlockInClose() { + typeTextInFile(parseKeys("dit"), "abcde<tag>fg</ta<caret>g>hi") + myFixture.checkResult("abcde<tag></tag>hi") + } + + //|d| |v_it| + fun testDeleteInnerTagBlockAfter() { + typeTextInFile(parseKeys("dit"), "abcde<tag>fg</tag>h<caret>i") + myFixture.checkResult("abcde<tag>fg</tag>hi") + } + + //|d| |v_it| + fun testDeleteInnerTagBlockInAlone() { + typeTextInFile(parseKeys("dit"), "abcde<ta<caret>g>fghi") + myFixture.checkResult("abcde<tag>fghi") + } + + //|d| |v_it| + fun testDeleteInnerTagBlockWithoutTags() { + typeTextInFile(parseKeys("dit"), "abc<caret>de") + myFixture.checkResult("abcde") + } + + //|d| |v_it| + fun testDeleteInnerTagBlockBeforeWithoutOpenTag() { + typeTextInFile(parseKeys("dit"), "abc<caret>defg</tag>hi") + myFixture.checkResult("abcdefg</tag>hi") + } + + //|d| |v_it| + fun testDeleteInnerTagBlockInCloseWithoutOpenTag() { + typeTextInFile(parseKeys("dit"), "abcdefg</ta<caret>g>hi") + myFixture.checkResult("abcdefg</tag>hi") + } + + //|d| |v_it| + fun testDeleteInnerTagBlockAfterWithoutOpenTag() { + typeTextInFile(parseKeys("dit"), "abcdefg</tag>h<caret>i") + myFixture.checkResult("abcdefg</tag>hi") + } + + //|d| |v_it| + fun testDeleteInnerTagBlockBeforeWithoutCloseTag() { + typeTextInFile(parseKeys("dit"), "abc<caret>defg<tag>hi") + myFixture.checkResult("abcdefg<tag>hi") + } + + //|d| |v_it| + fun testDeleteInnerTagBlockInOpenWithoutCloseTag() { + typeTextInFile(parseKeys("dit"), "abcdefg<ta<caret>g>hi") + myFixture.checkResult("abcdefg<tag>hi") + } + + //|d| |v_it| + fun testDeleteInnerTagBlockAfterWithoutCloseTag() { + typeTextInFile(parseKeys("dit"), "abcdefg<tag>h<caret>i") + myFixture.checkResult("abcdefg<tag>hi") + } + + //|d| |v_it| + fun testDeleteInnerTagBlockBeforeWrongOrder() { + typeTextInFile(parseKeys("dit"), "abc<caret>de</tag>fg<tag>hi") + myFixture.checkResult("abcde</tag>fg<tag>hi") + } + + //|d| |v_it| + fun testDeleteInnerTagBlockInOpenWrongOrder() { + typeTextInFile(parseKeys("dit"), "abcde</ta<caret>g>fg<tag>hi") + myFixture.checkResult("abcde</tag>fg<tag>hi") + } + + //|d| |v_it| + fun testDeleteInnerTagBlockBetweenWrongOrder() { + typeTextInFile(parseKeys("dit"), "abcde</tag>f<caret>g<tag>hi") + myFixture.checkResult("abcde</tag>fg<tag>hi") + } + + //|d| |v_it| + fun testDeleteInnerTagBlockInCloseWrongOrder() { + typeTextInFile(parseKeys("dit"), "abcde</tag>fg<ta<caret>g>hi") + myFixture.checkResult("abcde</tag>fg<tag>hi") + } + + //|d| |v_it| + fun testDeleteInnerTagBlockTwoTagsWrongOrder() { + typeTextInFile(parseKeys("dit"), "<foo><html>t<caret>ext</foo></html>") + myFixture.checkResult("<foo></foo></html>") + } + + //|d| |v_it| + fun testDeleteInnerTagBlockTwoTagsWrongOrderInClosingTag() { + typeTextInFile(parseKeys("dit"), "<foo><html>text</foo></htm<caret>l>") + myFixture.checkResult("<foo><html></html>") + } + + //|d| |v_it| + fun testDeleteInnerTagBlockAfterWrongOrder() { + typeTextInFile(parseKeys("dit"), "abcde</tag>fg<tag>h<caret>i") + myFixture.checkResult("abcde</tag>fg<tag>hi") + } + + //|d| |v_it| + fun testDeleteInnerTagBlockBracketInside() { + typeTextInFile(parseKeys("dit"), "abcde<tag>f<caret><>g</tag>hi") + myFixture.checkResult("abcde<tag></tag>hi") + } + + //|d| |v_it| + fun testDeleteInnerTagBlockBracketInsideString() { + typeTextInFile(parseKeys("dit"), "abcde<tag>f<caret>\"<>\"g</tag>hi") + myFixture.checkResult("abcde<tag></tag>hi") + } + + //|d| |v_it| + fun testDeleteInnerTagIsCaseInsensitive() { + typeTextInFile(parseKeys("dit"), "<a> <as<caret>df> </A>") + myFixture.checkResult("<a></A>") + } + + //|d| |v_it| + fun testDeleteInnerTagSlashesInAttribute() { + typeTextInFile(parseKeys("dit"), "<a href=\"http://isitchristmas.com\" class=\"button\">Bing <caret>Bing bing</a>") + myFixture.checkResult("<a href=\"http://isitchristmas.com\" class=\"button\"></a>") + } + + // VIM-1090 |d| |v_it| + // Adapted from vim source file "test_textobjects.vim" + fun testDeleteInnerTagDuplicateTags() { + typeTextInFile(parseKeys("dit"), "<b>as<caret>d<i>as<b />df</i>asdf</b>") + myFixture.checkResult("<b></b>") + } + + // |v_it| + fun testFileStartsWithSlash() { + configureByText("/*hello\n" + + "<caret>foo\n" + + "bar>baz\n") + typeText(parseKeys("vit")) + assertPluginError(true) + } + + // |v_it| + fun testSelectInnerTagEmptyTag() { + configureByText("<a><caret></a>") + typeText(parseKeys("vit")) + assertSelection("<a></a>") + } + + fun `test single character`() { + // The whole tag block is also selected if there is only a single character inside + configureByText("<a><caret>a</a>") + typeText(parseKeys("vit")) + assertSelection("<a>a</a>") + } + + fun `test single character inside tag`() { + configureByText("<a<caret>></a>") + typeText(parseKeys("vit")) + assertSelection("<") + } + + // VIM-1633 |v_it| + fun testNestedInTagSelection() { + configureByText("<t>Outer\n" + + " <t><caret>Inner</t>\n" + + "</t>\n") + typeText(parseKeys("vit")) + assertSelection("Inner") + } + + fun `test nested tag double motion`() { + configureByText("<o>Outer\n" + + " <caret> <t></t>\n" + + "</o>\n") + typeText(parseKeys("vitit")) + assertSelection("<t></t>") + } + + fun `test in inner tag double motion`() { + configureByText("<o><t><caret></t>\n</o>") + typeText(parseKeys("vitit")) + assertSelection("<o><t></t>\n</o>") + } + + fun `test nested tags between tags`() { + configureByText("<t>Outer\n" + + " <t>Inner</t> <caret> <t>Inner</t>\n" + + "</t>\n") + typeText(parseKeys("vit")) + assertSelection("Outer\n" + " <t>Inner</t> <t>Inner</t>") + } + + fun `test nested tags number motion`() { + configureByText("<t>Outer\n" + + " <t><caret>Inner</t>\n" + + "</t>\n") + typeText(parseKeys("v2it")) + assertSelection("Outer\n" + " <t>Inner</t>") + } + + fun `test nested tags double motion`() { + configureByText("<o>Outer\n" + + " <t><caret>Inner</t>\n" + + "</o>\n") + typeText(parseKeys("vitit")) + assertSelection("<t>Inner</t>") + } + + fun `test nested tags triple motion`() { + configureByText("<t>Outer\n" + + " <t><caret>Inner</t>\n" + + "</t>\n") + typeText(parseKeys("vititit")) + assertSelection("Outer\n" + " <t>Inner</t>") + } + + fun `test nested tags in closing tag`() { + configureByText("<t>Outer\n" + + " <t>Inner</t>\n" + + "</<caret>t>\n") + typeText(parseKeys("vit")) + assertSelection("Outer\n" + " <t>Inner</t>") + } + + fun `test nested tags in opening tag`() { + configureByText("<<caret>t>Outer\n" + + " <t>Inner</t>\n" + + "</t>\n") + typeText(parseKeys("vit")) + assertSelection("Outer\n" + " <t>Inner</t>") + } + + fun `test nested tags ouside tag`() { + configureByText("<caret><t>Outer\n" + + " <t>Inner</t>\n" + + "</t>\n") + typeText(parseKeys("vit")) + assertSelection("Outer\n" + " <t>Inner</t>") + } + + fun `test skip whitespace at start of line`() { + configureByText("<o>Outer\n" + + " <caret> <t></t>\n" + + "</o>\n") + typeText(parseKeys("vit")) + assertSelection("<") + } +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/action/motion/object/MotionOuterBlockParenActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/motion/object/MotionOuterBlockParenActionTest.kt new file mode 100644 index 000000000..0393cfabb --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/motion/object/MotionOuterBlockParenActionTest.kt @@ -0,0 +1,79 @@ +package org.jetbrains.plugins.ideavim.action.motion.`object` + +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys +import org.jetbrains.plugins.ideavim.VimTestCase + +class MotionOuterBlockParenActionTest : VimTestCase() { + // VIM-1633 |v_a)| + fun `test single letter with single parentheses`() { + configureByText("(<caret>a)") + typeText(parseKeys("va)")) + assertSelection("(a)") + } + + fun `test single letter with double parentheses`() { + configureByText("((<caret>a))") + typeText(parseKeys("va)")) + assertSelection("(a)") + } + + fun `test multiline outside parentheses`() { + configureByText("""(outer + |<caret>(inner))""".trimMargin()) + typeText(parseKeys("va)")) + assertSelection("(inner)") + } + + fun `test multiline in parentheses`() { + configureByText("""(outer + |(inner<caret>))""".trimMargin()) + typeText(parseKeys("va)")) + assertSelection("(inner)") + } + + fun `test multiline inside of outer parentheses`() { + configureByText("""(outer + |<caret> (inner))""".trimMargin()) + typeText(parseKeys("va)")) + assertSelection("""(outer + | (inner))""".trimMargin()) + } + + fun `test double motion`() { + configureByText("""(outer + |<caret>(inner))""".trimMargin()) + typeText(parseKeys("va)a)")) + assertSelection("""(outer + |(inner))""".trimMargin()) + } + + fun `test motion with count`() { + configureByText("""(outer + |<caret>(inner))""".trimMargin()) + typeText(parseKeys("v2a)")) + assertSelection("""(outer + |(inner))""".trimMargin()) + } + + fun `test text object after motion`() { + configureByText("""(outer + |<caret>(inner))""".trimMargin()) + typeText(parseKeys("vlla)")) + assertSelection("""(outer + |(inner))""".trimMargin()) + } + + fun `test text object after motion outside parentheses`() { + configureByText("""(outer + |(inner<caret>))""".trimMargin()) + typeText(parseKeys("vlla)")) + assertSelection("(inner)") + } + + // |d| |v_ab| + fun testDeleteOuterBlock() { + typeTextInFile(parseKeys("da)"), + "foo(b<caret>ar, baz);\n") + myFixture.checkResult("foo;\n") + } +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/action/motion/object/MotionOuterBlockTagActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/motion/object/MotionOuterBlockTagActionTest.kt new file mode 100644 index 000000000..8701beb06 --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/motion/object/MotionOuterBlockTagActionTest.kt @@ -0,0 +1,254 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2019 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 <http://www.gnu.org/licenses/>. + */ + +package org.jetbrains.plugins.ideavim.action.motion.`object` + +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys +import org.jetbrains.plugins.ideavim.VimTestCase + +class MotionOuterBlockTagActionTest : VimTestCase() { + + //|d| |v_at| + fun testDeleteOuterTagBlockBefore() { + typeTextInFile(parseKeys("dat"), "abc<caret>de<tag>fg</tag>hi") + myFixture.checkResult("abcde<tag>fg</tag>hi") + } + + //|d| |v_at| + fun testDeleteOuterTagBlockInOpen() { + typeTextInFile(parseKeys("dat"), "abcde<ta<caret>g>fg</tag>hi") + myFixture.checkResult("abcdehi") + } + + //|d| |v_at| + fun testDeleteOuterTagBlockInOpenWithArgs() { + typeTextInFile(parseKeys("dat"), "abcde<ta<caret>g name = \"name\">fg</tag>hi") + myFixture.checkResult("abcdehi") + } + + //|d| |v_at| + fun testDeleteOuterTagBlockBetween() { + typeTextInFile(parseKeys("dat"), "abcde<tag>f<caret>g</tag>hi") + myFixture.checkResult("abcdehi") + } + + //|d| |v_at| + fun testDeleteOuterTagBlockBetweenWithArgs() { + typeTextInFile(parseKeys("dat"), "abcde<tag name = \"name\">f<caret>g</tag>hi") + myFixture.checkResult("abcdehi") + } + + //|d| |v_at| + fun testDeleteOuterTagBlockInClose() { + typeTextInFile(parseKeys("dat"), "abcde<tag>fg</ta<caret>g>hi") + myFixture.checkResult("abcdehi") + } + + //|d| |v_at| + fun testDeleteOuterTagBlockAfter() { + typeTextInFile(parseKeys("dat"), "abcde<tag>fg</tag>h<caret>i") + myFixture.checkResult("abcde<tag>fg</tag>hi") + } + + //|d| |v_at| + fun testDeleteOuterTagBlockInAlone() { + typeTextInFile(parseKeys("dat"), "abcde<ta<caret>g>fghi") + myFixture.checkResult("abcde<tag>fghi") + } + + //|d| |v_at| + fun testDeleteOuterTagBlockWithoutTags() { + typeTextInFile(parseKeys("dat"), "abc<caret>de") + myFixture.checkResult("abcde") + } + + //|d| |v_at| + fun testDeleteOuterTagBlockBeforeWithoutOpenTag() { + typeTextInFile(parseKeys("dat"), "abc<caret>defg</tag>hi") + myFixture.checkResult("abcdefg</tag>hi") + } + + //|d| |v_at| + fun testDeleteOuterTagBlockInCloseWithoutOpenTag() { + typeTextInFile(parseKeys("dat"), "abcdefg</ta<caret>g>hi") + myFixture.checkResult("abcdefg</tag>hi") + } + + //|d| |v_at| + fun testDeleteOuterTagBlockAfterWithoutOpenTag() { + typeTextInFile(parseKeys("dat"), "abcdefg</tag>h<caret>i") + myFixture.checkResult("abcdefg</tag>hi") + } + + //|d| |v_at| + fun testDeleteOuterTagBlockBeforeWithoutCloseTag() { + typeTextInFile(parseKeys("dat"), "abc<caret>defg<tag>hi") + myFixture.checkResult("abcdefg<tag>hi") + } + + //|d| |v_at| + fun testDeleteOuterTagBlockInOpenWithoutCloseTag() { + typeTextInFile(parseKeys("dat"), "abcdefg<ta<caret>g>hi") + myFixture.checkResult("abcdefg<tag>hi") + } + + //|d| |v_at| + fun testDeleteOuterTagBlockAfterWithoutCloseTag() { + typeTextInFile(parseKeys("dat"), "abcdefg<tag>h<caret>i") + myFixture.checkResult("abcdefg<tag>hi") + } + + //|d| |v_at| + fun testDeleteOuterTagBlockBeforeWrongOrder() { + typeTextInFile(parseKeys("dat"), "abc<caret>de</tag>fg<tag>hi") + myFixture.checkResult("abcde</tag>fg<tag>hi") + } + + //|d| |v_at| + fun testDeleteOuterTagBlockInOpenWrongOrder() { + typeTextInFile(parseKeys("dat"), "abcde</ta<caret>g>fg<tag>hi") + myFixture.checkResult("abcde</tag>fg<tag>hi") + } + + //|d| |v_at| + fun testDeleteOuterTagBlockBetweenWrongOrder() { + typeTextInFile(parseKeys("dat"), "abcde</tag>f<caret>g<tag>hi") + myFixture.checkResult("abcde</tag>fg<tag>hi") + } + + //|d| |v_at| + fun testDeleteOuterTagBlockInCloseWrongOrder() { + typeTextInFile(parseKeys("dat"), "abcde</tag>fg<ta<caret>g>hi") + myFixture.checkResult("abcde</tag>fg<tag>hi") + } + + //|d| |v_at| + fun testDeleteOuterTagBlockAfterWrongOrder() { + typeTextInFile(parseKeys("dat"), "abcde</tag>fg<tag>h<caret>i") + myFixture.checkResult("abcde</tag>fg<tag>hi") + } + + //|d| |v_it| + fun testDeleteInnerTagAngleBrackets() { + typeTextInFile(parseKeys("dit"), "<div <caret>hello=\"d > hsj < akl\"></div>") + myFixture.checkResult("<div hello=\"d ></div>") + } + + // VIM-1090 |d| |v_at| + fun testDeleteOuterTagDuplicateTags() { + typeTextInFile(parseKeys("dat"), "<a><a></a></a<caret>>") + myFixture.checkResult("") + } + + // |v_it| |v_at| + fun testTagSelectionSkipsWhitespaceAtStartOfLine() { + // Also skip tabs + configureByText("<o>Outer\n" + + " <caret> \t <t>Inner</t>\n" + + "</o>\n") + typeText(parseKeys("vat")) + assertSelection("<t>Inner</t>") + } + + fun `test skip new line`() { + // Newline must not be skipped + configureByText("<caret>\n" + " <t>asdf</t>") + typeText(parseKeys("vat")) + assertSelection(null) + } + + fun `test whitespace skip`() { + // Whitespace is only skipped if there is nothing else at the start of the line + configureByText("<o>Outer\n" + + "a <caret> <t>Inner</t>\n" + + "</o>\n") + typeText(parseKeys("vat")) + assertSelection("<o>Outer\n" + + "a <t>Inner</t>\n" + + "</o>") + } + + // |v_at| + fun testNestedTagSelection() { + configureByText("<t>Outer\n" + + " <t><caret>Inner</t>\n" + + "</t>\n") + typeText(parseKeys("vat")) + assertSelection("<t>Inner</t>") + } + + fun `test nested tags between tags`() { + configureByText("<t>Outer\n" + + " <t>Inner</t> <caret> <t>Inner</t>\n" + + "</t>\n") + typeText(parseKeys("vat")) + assertSelection("<t>Outer\n" + + " <t>Inner</t> <t>Inner</t>\n" + + "</t>") + } + + fun `test nested tags double motion`() { + configureByText("<t>Outer\n" + + " <t><caret>Inner</t>\n" + + "</t>\n") + typeText(parseKeys("vatat")) + assertSelection("<t>Outer\n" + + " <t>Inner</t>\n" + + "</t>") + } + + fun `test nested tags number motion`() { + configureByText("<t>Outer\n" + + " <t><caret>Inner</t>\n" + + "</t>\n") + typeText(parseKeys("v2at")) + assertSelection("<t>Outer\n" + + " <t>Inner</t>\n" + + "</t>") + } + + fun `test nested tags on outer`() { + configureByText("<t>Outer\n" + + " <t>Inner</t>\n" + + "</<caret>t>\n") + typeText(parseKeys("vat")) + assertSelection("<t>Outer\n" + + " <t>Inner</t>\n" + + "</t>") + } + + fun `test nested tags on outer start`() { + configureByText("<<caret>t>Outer\n" + + " <t>Inner</t>\n" + + "</t>\n") + typeText(parseKeys("vat")) + assertSelection("<t>Outer\n" + + " <t>Inner</t>\n" + + "</t>") + } + + fun `test nested tags outside outer`() { + configureByText("<caret><t>Outer\n" + + " <t>Inner</t>\n" + + "</t>\n") + typeText(parseKeys("vat")) + assertSelection("<t>Outer\n" + + " <t>Inner</t>\n" + + "</t>") + } +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/ex/ActionListCommandTest.java b/test/org/jetbrains/plugins/ideavim/ex/ActionListCommandTest.java new file mode 100644 index 000000000..54122733e --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/ex/ActionListCommandTest.java @@ -0,0 +1,80 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2019 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 <http://www.gnu.org/licenses/>. + */ + +package org.jetbrains.plugins.ideavim.ex; + +import com.intellij.openapi.actionSystem.ActionManager; +import com.maddyhome.idea.vim.ex.ExOutputModel; +import org.jetbrains.plugins.ideavim.VimTestCase; + +/** + * @author Naoto Ikeno + */ +public class ActionListCommandTest extends VimTestCase { + public void testListAllActions() { + configureByText("\n"); + typeText(commandToKeys("actionlist")); + + String output = ExOutputModel.getInstance(myFixture.getEditor()).getText(); + assertNotNull(output); + + // Header line + String[] displayedLines = output.split("\n"); + assertEquals("--- Actions ---", displayedLines[0]); + + // Action lines + int displayedActionNum = displayedLines.length - 1; + String[] actionIds = ActionManager.getInstance().getActionIds(""); + assertEquals(displayedActionNum, actionIds.length); + } + + public void testSearchByActionName() { + configureByText("\n"); + typeText(commandToKeys("actionlist quickimpl")); + + String[] displayedLines = parseActionListOutput(); + for (int i = 0; i < displayedLines.length; i++) { + String line = displayedLines[i]; + if (i == 0) { + assertEquals("--- Actions ---", line); + }else { + assertTrue(line.toLowerCase().contains("quickimpl")); + } + } + } + + public void testSearchByAssignedShortcutKey() { + configureByText("\n"); + typeText(commandToKeys("actionlist <M-S-")); + + String[] displayedLines = parseActionListOutput(); + for (int i = 0; i < displayedLines.length; i++) { + String line = displayedLines[i]; + if (i == 0) { + assertEquals("--- Actions ---", line); + }else { + assertTrue(line.toLowerCase().contains("<m-s-")); + } + } + } + + private String[] parseActionListOutput() { + String output = ExOutputModel.getInstance(myFixture.getEditor()).getText(); + return output == null ? new String[]{} : output.split("\n"); + } +} diff --git a/test/org/jetbrains/plugins/ideavim/ex/ExEntryTest.kt b/test/org/jetbrains/plugins/ideavim/ex/ExEntryTest.kt new file mode 100644 index 000000000..83f5c1988 --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/ex/ExEntryTest.kt @@ -0,0 +1,578 @@ +package org.jetbrains.plugins.ideavim.ex + +import com.maddyhome.idea.vim.VimPlugin +import com.maddyhome.idea.vim.helper.StringHelper +import com.maddyhome.idea.vim.option.Options +import com.maddyhome.idea.vim.ui.ExDocument +import com.maddyhome.idea.vim.ui.ExEntryPanel +import org.jetbrains.plugins.ideavim.VimTestCase +import java.awt.event.KeyEvent +import javax.swing.KeyStroke + +class ExEntryTest: VimTestCase() { + override fun setUp() { + super.setUp() + configureByText("\n") + } + + fun `test cancel entry`() { + val options = Options.getInstance() + + assertFalse(options.isSet(Options.INCREMENTAL_SEARCH)) + typeExInput(":set incsearch<Esc>") + assertFalse(options.isSet(Options.INCREMENTAL_SEARCH)) + assertIsDeactivated() + + deactivateExEntry() + + assertFalse(options.isSet(Options.INCREMENTAL_SEARCH)) + typeExInput(":set incsearch<C-[>") + assertFalse(options.isSet(Options.INCREMENTAL_SEARCH)) + assertIsDeactivated() + + deactivateExEntry() + + assertFalse(options.isSet(Options.INCREMENTAL_SEARCH)) + typeExInput(":set incsearch<C-C>") + assertFalse(options.isSet(Options.INCREMENTAL_SEARCH)) + assertIsDeactivated() + } + + fun `test complete entry`() { + val options = Options.getInstance() + + assertFalse(options.isSet(Options.INCREMENTAL_SEARCH)) + typeExInput(":set incsearch<Enter>") + assertTrue(options.isSet(Options.INCREMENTAL_SEARCH)) + assertIsDeactivated() + + deactivateExEntry() + options.resetAllOptions() + + assertFalse(options.isSet(Options.INCREMENTAL_SEARCH)) + typeExInput(":set incsearch<C-J>") + assertTrue(options.isSet(Options.INCREMENTAL_SEARCH)) + assertIsDeactivated() + + deactivateExEntry() + options.resetAllOptions() + + assertFalse(Options.getInstance().isSet(Options.INCREMENTAL_SEARCH)) + typeExInput(":set incsearch<C-M>") + assertTrue(Options.getInstance().isSet(Options.INCREMENTAL_SEARCH)) + assertIsDeactivated() + } + + fun `test caret shape`() { + // Show block at end of input (normal) + // Show vertical bar in insert mode + // Show horizontal bar in replace mode + typeExInput(":") + assertEquals("BLOCK 100", exEntryPanel.entry.caretShape) + + typeText("set") + assertEquals("BLOCK 100", exEntryPanel.entry.caretShape) + + deactivateExEntry() + typeExInput(":set<Home>") + assertEquals("VER 25", exEntryPanel.entry.caretShape) + + deactivateExEntry() + typeExInput(":set<Home><Insert>") + assertEquals("HOR 20", exEntryPanel.entry.caretShape) + + deactivateExEntry() + typeExInput(":set<Home><Insert><Insert>") + assertEquals("VER 25", exEntryPanel.entry.caretShape) + } + + fun `test move caret to beginning of line`() { + typeExInput(":set incsearch<C-B>") + assertExOffset(0) + + deactivateExEntry() + + typeExInput(":set incsearch<Home>") + assertExOffset(0) + } + + fun `test move caret to end of line`() { + typeExInput(":set incsearch<C-B>") + assertExOffset(0) + + typeText("<C-E>") + assertExOffset(13) + + deactivateExEntry() + typeExInput(":set incsearch<C-B>") + assertExOffset(0) + + typeText("<End>") + assertExOffset(13) + } + + fun `test delete character in front of caret`() { + typeExInput(":set incsearch<BS>") + assertExText("set incsearc") + + typeText("<C-H>") + assertExText("set incsear") + } + + fun `test delete character in front of caret cancels entry`() { + typeExInput(":<BS>") + assertIsDeactivated() + + deactivateExEntry() + + typeExInput(":set<BS><BS><BS><BS>") + assertIsDeactivated() + + deactivateExEntry() + + typeExInput(":<C-H>") + assertIsDeactivated() + + deactivateExEntry() + + // TODO: Vim behaviour is to NOT deactivate if there is still text + typeExInput(":set<C-B>") + assertExOffset(0) + typeText("<BS>") + assertIsDeactivated() + } + + fun `test delete character under caret`() { + typeExInput(":set<Left>") + typeText("<Del>") + assertExText("se") + } + + fun `test delete word before caret`() { + typeExInput(":set incsearch<C-W>") + assertExText("set ") + + deactivateExEntry() + + typeExInput(":set incsearch<Left><Left><Left>") + typeText("<C-W>") + assertExText("set rch") + } + + fun `test delete to start of line`() { + typeExInput(":set incsearch<C-U>") + assertExText("") + + deactivateExEntry() + + typeExInput(":set incsearch<Left><Left><Left><C-U>") + assertExText("rch") + } + + fun `test command history`() { + typeExInput(":set digraph<CR>") + typeExInput(":digraph<CR>") + typeExInput(":set incsearch<CR>") + + typeExInput(":<Up>") + assertExText("set incsearch") + typeText("<Up>") + assertExText("digraph") + typeText("<Up>") + assertExText("set digraph") + + deactivateExEntry() + + // TODO: Vim behaviour reorders the history even when cancelling history +// typeExInput(":<Up>") +// assertExText("set digraph") +// typeText("<Up>") +// assertExText("set incsearch") + + typeExInput(":<S-Up>") + assertExText("set incsearch") + typeText("<Up>") + assertExText("digraph") + typeText("<Up>") + assertExText("set digraph") + + deactivateExEntry() + + typeExInput(":<PageUp>") + assertExText("set incsearch") + typeText("<PageUp>") + assertExText("digraph") + typeText("<PageUp>") + assertExText("set digraph") + } + + fun `test matching command history`() { + typeExInput(":set digraph<CR>") + typeExInput(":digraph<CR>") + typeExInput(":set incsearch<CR>") + + typeExInput(":set<Up>") + assertExText("set incsearch") + typeText("<Up>") + assertExText("set digraph") + + deactivateExEntry() + + typeExInput(":set<S-Up>") + assertExText("set incsearch") + typeText("<S-Up>") + assertExText("digraph") + typeText("<S-Up>") + assertExText("set digraph") + + deactivateExEntry() + + typeExInput(":set<PageUp>") + assertExText("set incsearch") + typeText("<PageUp>") + assertExText("digraph") + typeText("<PageUp>") + assertExText("set digraph") + } + + fun `test search history`() { + typeExInput("/something cool<CR>") + typeExInput("/not cool<CR>") + typeExInput("/so cool<CR>") + + typeExInput("/<Up>") + assertExText("so cool") + typeText("<Up>") + assertExText("not cool") + typeText("<Up>") + assertExText("something cool") + + deactivateExEntry() + + typeExInput("/<S-Up>") + assertExText("so cool") + typeText("<S-Up>") + assertExText("not cool") + typeText("<S-Up>") + assertExText("something cool") + + deactivateExEntry() + + typeExInput("/<PageUp>") + assertExText("so cool") + typeText("<PageUp>") + assertExText("not cool") + typeText("<PageUp>") + assertExText("something cool") + } + + fun `test matching search history`() { + typeExInput("/something cool<CR>") + typeExInput("/not cool<CR>") + typeExInput("/so cool<CR>") + + typeExInput("/so<Up>") + assertExText("so cool") + typeText("<Up>") + assertExText("something cool") + + deactivateExEntry() + + // TODO: Vim behaviour reorders the history even when cancelling history +// typeExInput(":<Up>") +// assertEquals("set digraph", exEntryPanel.text) +// typeText("<Up>") +// assertEquals("set incsearch", exEntryPanel.text) + + typeExInput("/so<S-Up>") + assertExText("so cool") + typeText("<S-Up>") + assertExText("not cool") + typeText("<S-Up>") + assertExText("something cool") + + deactivateExEntry() + + typeExInput("/so<PageUp>") + assertExText("so cool") + typeText("<PageUp>") + assertExText("not cool") + typeText("<PageUp>") + assertExText("something cool") + } + + fun `test toggle insert replace`() { + val exDocument = exEntryPanel.entry.document as ExDocument + assertFalse(exDocument.isOverwrite) + typeExInput(":set<C-B>digraph") + assertExText("digraphset") + + deactivateExEntry() + + typeExInput(":set<C-B><Insert>digraph") + assertTrue(exDocument.isOverwrite) + assertExText("digraph") + + typeText("<Insert><C-B>set ") + assertFalse(exDocument.isOverwrite) + assertExText("set digraph") + } + + fun `test move caret one WORD left`() { + typeExInput(":set incsearch<S-Left>") + assertExOffset(4) + typeText("<S-Left>") + assertExOffset(0) + + deactivateExEntry() + + typeExInput(":set incsearch<C-Left>") + assertExOffset(4) + typeText("<C-Left>") + assertExOffset(0) + } + + fun `test move caret one WORD right`() { + typeExInput(":set incsearch") + caret.dot = 0 + typeText("<S-Right>") + // TODO: Vim moves caret to "set| ", while we move it to "set |" + assertExOffset(4) + + typeText("<S-Right>") + assertExOffset(13) + + caret.dot = 0 + typeText("<C-Right>") + // TODO: Vim moves caret to "set| ", while we move it to "set |" + assertExOffset(4) + + typeText("<C-Right>") + assertExOffset(13) + } + + fun `test insert digraph`() { + typeExInput(":<C-K>OK") + assertExText("✓") + assertExOffset(1) + + deactivateExEntry() + + typeExInput(":set<Home><C-K>OK") + assertExText("✓set") + assertExOffset(1) + + deactivateExEntry() + + typeExInput(":set<Home><Insert><C-K>OK") + assertExText("✓et") + assertExOffset(1) + } + + fun `test prompt while inserting digraph`() { + typeExInput(":<C-K>") + assertExText("?") + assertExOffset(0) + + deactivateExEntry() + + typeExInput(":<C-K>O") + assertExText("O") + assertExOffset(0) + + deactivateExEntry() + + typeExInput(":set<Home><C-K>") + assertExText("?set") + assertExOffset(0) + + deactivateExEntry() + + typeExInput(":set<Home><C-K>O") + assertExText("Oset") + assertExOffset(0) + } + + fun `test escape cancels digraph`() { + typeExInput(":<C-K><Esc>OK") + assertIsActive() + assertExText("OK") + + deactivateExEntry() + + // Note that the docs state that hitting escape stops digraph entry and cancels command line mode. In practice, + // this isn't true - digraph entry is stopped, but command line mode continues + typeExInput(":<C-K>O<Esc>K") + assertIsActive() + assertEquals("K", exEntryPanel.text) + + deactivateExEntry() + } + + // TODO: Test inserting control characters, if/when supported + + fun `test insert literal character`() { + typeExInput(":<C-V>123<C-V>080") + assertExText("{P") + + deactivateExEntry() + + typeExInput(":<C-V>o123") + assertExText("S") + + deactivateExEntry() + + typeExInput(":<C-V>u00A9") + assertExText("©") + + deactivateExEntry() + + typeExInput(":<C-Q>123<C-Q>080") + assertExText("{P") + + deactivateExEntry() + + typeExInput(":<C-Q>o123") + assertExText("S") + + deactivateExEntry() + + typeExInput(":<C-Q>u00a9") + assertExText("©") + + deactivateExEntry() + + typeExInput(":set<Home><C-V>u00A9") + assertExText("©set") + assertExOffset(1) + } + + fun `test prompt while inserting literal character`() { + typeExInput(":<C-V>") + assertExText("^") + assertExOffset(0) + + deactivateExEntry() + + typeExInput(":<C-V>o") + assertExText("^") + assertExOffset(0) + + typeText("1") + assertExText("^") + assertExOffset(0) + + typeText("2") + assertExText("^") + assertExOffset(0) + + typeText("3") + assertExText("S") + assertExOffset(1) + } + + fun `test insert register`() { + VimPlugin.getRegister().setKeys('c', StringHelper.parseKeys("hello world")) + VimPlugin.getRegister().setKeys('5', StringHelper.parseKeys("greetings programs")) + + typeExInput(":<C-R>c") + assertExText("hello world") + + deactivateExEntry() + + typeExInput(":<C-R>5") + assertExText("greetings programs") + + deactivateExEntry() + + typeExInput(":set<Home><C-R>c") + assertExText("hello worldset") + assertExOffset(11) // Just before 'set' + + // TODO: Test caret feedback + // Vim shows " after hitting <C-R> + } + + fun `test insert multi-line register`() { + // parseKeys parses <CR> in a way that Register#getText doesn't like + val keys = mutableListOf<KeyStroke>() + keys.addAll(StringHelper.parseKeys("hello")) + keys.add(KeyStroke.getKeyStroke('\n')) + keys.addAll(StringHelper.parseKeys("world")) + VimPlugin.getRegister().setKeys('c', keys) + + typeExInput(":<C-R>c") + assertExText("hello world") + } + + // TODO: Test other special registers, if/when supported + // E.g. '.' '%' '#', etc. + + fun `test insert last command`() { + typeExInput(":set incsearch<CR>") + typeExInput(":<C-R>:") + assertExText("set incsearch") + } + + fun `test insert last search command`() { + typeExInput("/hello<CR>") + typeExInput(":<C-R>/") + assertExText("hello") + } + + private fun typeExInput(text: String) { + assertTrue("Ex command must start with ':', '/' or '?'", + text.startsWith(":") || text.startsWith('/') || text.startsWith('?')) + + val keys = mutableListOf<KeyStroke>() + StringHelper.parseKeys(text).forEach { + // <Left> doesn't work correctly in tests. The DefaultEditorKit.NextVisualPositionAction action is correctly + // called, but fails to move the caret correctly because the text component has never been painted + if (it.keyCode == KeyEvent.VK_LEFT && it.modifiers == 0) { + if (keys.count() > 0) { + typeText(keys) + keys.clear() + } + + exEntryPanel.entry.caret.dot-- + } + else { + keys.add(it) + } + } + if (keys.count() > 0) + typeText(keys) + } + + private fun typeText(text: String) { + typeText(StringHelper.parseKeys(text)) + } + + private fun deactivateExEntry() { + // We don't need to reset text, that's handled by #active + if (exEntryPanel.isActive) + typeText("<C-C>") + } + + private fun assertExText(expected: String) { + assertEquals(expected, exEntryPanel.text) + } + + private fun assertIsActive() { + assertTrue(exEntryPanel.isActive) + } + + private fun assertIsDeactivated() { + assertFalse(exEntryPanel.isActive) + } + + private fun assertExOffset(expected: Int) { + assertEquals(expected, caret.dot) + } + + private val exEntryPanel + get() = ExEntryPanel.getInstance() + + private val caret + get() = exEntryPanel.entry.caret +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/ex/handler/CmdClearHandlerTest.kt b/test/org/jetbrains/plugins/ideavim/ex/handler/CmdClearHandlerTest.kt new file mode 100644 index 000000000..590ea1d15 --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/ex/handler/CmdClearHandlerTest.kt @@ -0,0 +1,56 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2019 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 <http://www.gnu.org/licenses/>. + */ + +package org.jetbrains.plugins.ideavim.ex.handler +import com.maddyhome.idea.vim.VimPlugin +import org.jetbrains.plugins.ideavim.VimFileEditorTestCase + +/** + * @author Elliot Courant + */ +class CmdClearHandlerTest : VimFileEditorTestCase() { + fun `test clear aliases`() { + VimPlugin.getCommand().resetAliases() + configureByText("\n") + typeText(commandToKeys("command")) + assertPluginError(false) + assertExOutput("Name Args Definition\n") // There should not be any aliases. + + typeText(commandToKeys("command Vs vs")) + assertPluginError(false) + typeText(commandToKeys("command Wq wq")) + assertPluginError(false) + typeText(commandToKeys("command WQ wq")) + assertPluginError(false) + typeText(commandToKeys("command")) + assertPluginError(false) + // The added alias should be listed + assertExOutput("""Name Args Definition + |Vs 0 vs + |Wq 0 wq + |WQ 0 wq + """.trimMargin()) + + // Delete all of the aliases and then list aliases again. + typeText(commandToKeys("comclear")) + assertPluginError(false) + typeText(commandToKeys("command")) + assertPluginError(false) + assertExOutput("Name Args Definition\n") // There should not be any aliases. + } +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/ex/handler/CmdHandlerTest.kt b/test/org/jetbrains/plugins/ideavim/ex/handler/CmdHandlerTest.kt new file mode 100644 index 000000000..ebde4240f --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/ex/handler/CmdHandlerTest.kt @@ -0,0 +1,207 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2019 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 <http://www.gnu.org/licenses/>. + */ + +package org.jetbrains.plugins.ideavim.ex.handler + +import com.maddyhome.idea.vim.VimPlugin +import org.jetbrains.plugins.ideavim.VimFileEditorTestCase +import org.jetbrains.plugins.ideavim.VimTestCase + +/** + * @author Elliot Courant + */ +class CmdHandlerTest : VimFileEditorTestCase() { + fun `test recursive`() { + VimPlugin.getCommand().resetAliases() + configureByText("\n") + typeText(VimTestCase.commandToKeys("command Recur1 Recur2")) + assertPluginError(false) + typeText(VimTestCase.commandToKeys("command Recur2 Recur1")) + assertPluginError(false) + typeText(VimTestCase.commandToKeys("Recur1")) + assertPluginError(true) // Recursive command should error. + } + + fun `test list aliases`() { + VimPlugin.getCommand().resetAliases() + configureByText("\n") + typeText(VimTestCase.commandToKeys("command")) + assertPluginError(false) + assertExOutput("Name Args Definition\n") // There should not be any aliases. + + typeText(VimTestCase.commandToKeys("command Vs vs")) + assertPluginError(false) + typeText(VimTestCase.commandToKeys("command Wq wq")) + assertPluginError(false) + typeText(VimTestCase.commandToKeys("command WQ wq")) + assertPluginError(false) + typeText(VimTestCase.commandToKeys("command-nargs=* Test1 echo")) + assertPluginError(false) + typeText(VimTestCase.commandToKeys("command-nargs=? Test2 echo")) + assertPluginError(false) + typeText(VimTestCase.commandToKeys("command-nargs=+ Test3 echo")) + assertPluginError(false) + typeText(VimTestCase.commandToKeys("command-nargs=1 Test4 echo")) + assertPluginError(false) + typeText(VimTestCase.commandToKeys("command")) + assertPluginError(false) + // The added alias should be listed + assertExOutput("""Name Args Definition + |Test1 * echo + |Test2 ? echo + |Test3 + echo + |Test4 1 echo + |Vs 0 vs + |Wq 0 wq + |WQ 0 wq + """.trimMargin()) + + typeText(VimTestCase.commandToKeys("command W")) + assertPluginError(false) + // The filtered aliases should be listed + assertExOutput("""Name Args Definition + |Wq 0 wq + |WQ 0 wq + """.trimMargin()) + } + + fun `test bad alias`() { + VimPlugin.getCommand().resetAliases() + configureByText("\n") + typeText(VimTestCase.commandToKeys("Vs")) + assertPluginError(true) + typeText(VimTestCase.commandToKeys("command Vs vs")) + assertPluginError(false) + typeText(VimTestCase.commandToKeys("Vs")) + assertPluginError(false) + } + + fun `test lowercase should fail`() { + VimPlugin.getCommand().resetAliases() + configureByText("\n") + typeText(VimTestCase.commandToKeys("command lowercase vs")) + assertPluginError(true) + } + + fun `test blacklisted alias should fail`() { + VimPlugin.getCommand().resetAliases() + configureByText("\n") + typeText(VimTestCase.commandToKeys("command X vs")) + assertPluginError(true) + typeText(VimTestCase.commandToKeys("command Next vs")) + assertPluginError(true) + typeText(VimTestCase.commandToKeys("command Print vs")) + assertPluginError(true) + } + + fun `test add an existing alias and overwrite`() { + VimPlugin.getCommand().resetAliases() + configureByText("\n") + typeText(VimTestCase.commandToKeys("command Existing1 vs")) + assertPluginError(false) + typeText(VimTestCase.commandToKeys("command Existing1 wq")) + assertPluginError(true) + typeText(VimTestCase.commandToKeys("command! Existing1 wq")) + assertPluginError(false) + } + + fun `test add command with arguments`() { + VimPlugin.getCommand().resetAliases() + configureByText("\n") + typeText(VimTestCase.commandToKeys("command -nargs=* Error echo <args>")) + assertPluginError(false) + } + + fun `test add command with arguments short`() { + VimPlugin.getCommand().resetAliases() + configureByText("\n") + typeText(VimTestCase.commandToKeys("command-nargs=* Error echo <args>")) + assertPluginError(false) + } + + fun `test add command with arguments even shorter`() { + VimPlugin.getCommand().resetAliases() + configureByText("\n") + typeText(VimTestCase.commandToKeys("com-nargs=* Error echo <args>")) + assertPluginError(false) + } + + fun `test add command with various arguments`() { + VimPlugin.getCommand().resetAliases() + configureByText("\n") + typeText(VimTestCase.commandToKeys("command! -nargs=0 Error echo <args>")) + assertPluginError(false) + typeText(VimTestCase.commandToKeys("command! -nargs=1 Error echo <args>")) + assertPluginError(false) + typeText(VimTestCase.commandToKeys("command! -nargs=* Error echo <args>")) + assertPluginError(false) + typeText(VimTestCase.commandToKeys("command! -nargs=? Error echo <args>")) + assertPluginError(false) + typeText(VimTestCase.commandToKeys("command! -nargs=+ Error echo <args>")) + assertPluginError(false) + } + + fun `test add command with invalid arguments`() { + VimPlugin.getCommand().resetAliases() + configureByText("\n") + typeText(VimTestCase.commandToKeys("command! -nargs= Error echo <args>")) + assertPluginError(true) + typeText(VimTestCase.commandToKeys("command! -nargs=-1 Error echo <args>")) + assertPluginError(true) + typeText(VimTestCase.commandToKeys("command! -nargs=# Error echo <args>")) + assertPluginError(true) + } + + fun `test run command with arguments`() { + VimPlugin.getCommand().resetAliases() + configureByText("\n") + typeText(VimTestCase.commandToKeys("let test = \"Hello!\"")) + assertPluginError(false) + typeText(VimTestCase.commandToKeys("command! -nargs=1 Error echo <args>")) + assertPluginError(false) + typeText(VimTestCase.commandToKeys("Error test")) + assertPluginError(false) + assertExOutput("Hello!\n") + + typeText(VimTestCase.commandToKeys("command! -nargs=1 Error echo <q-args>")) + assertPluginError(false) + typeText(VimTestCase.commandToKeys("Error test message")) + assertPluginError(false) + assertExOutput("test message\n") + } + + fun `test run command that creates another command`() { + VimPlugin.getCommand().resetAliases() + configureByText("\n") + typeText(VimTestCase.commandToKeys("command! -nargs=1 CreateCommand command -nargs=1 <args> <lt>q-args>")) + assertPluginError(false) + typeText(VimTestCase.commandToKeys("CreateCommand Show echo")) + assertPluginError(false) + typeText(VimTestCase.commandToKeys("Show test")) + assertPluginError(false) + assertExOutput("test\n") + } + + fun `test run command missing required argument`() { + VimPlugin.getCommand().resetAliases() + configureByText("\n") + typeText(VimTestCase.commandToKeys("command! -nargs=1 Error echo <q-args>")) + assertPluginError(false) + typeText(VimTestCase.commandToKeys("Error")) + } +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/ex/handler/DelCmdHandlerTest.kt b/test/org/jetbrains/plugins/ideavim/ex/handler/DelCmdHandlerTest.kt new file mode 100644 index 000000000..74a7a613b --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/ex/handler/DelCmdHandlerTest.kt @@ -0,0 +1,75 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2019 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 <http://www.gnu.org/licenses/>. + */ + +package org.jetbrains.plugins.ideavim.ex.handler +import com.maddyhome.idea.vim.VimPlugin +import org.jetbrains.plugins.ideavim.VimFileEditorTestCase +import org.jetbrains.plugins.ideavim.VimTestCase + +/** + * @author Elliot Courant + */ +class DelCmdHandlerTest : VimFileEditorTestCase() { + fun `test remove alias`() { + VimPlugin.getCommand().resetAliases() + configureByText("\n") + typeText(VimTestCase.commandToKeys("command")) + assertPluginError(false) + assertExOutput("Name Args Definition\n") // There should not be any aliases. + + typeText(VimTestCase.commandToKeys("command Vs vs")) + assertPluginError(false) + typeText(VimTestCase.commandToKeys("command Wq wq")) + assertPluginError(false) + typeText(VimTestCase.commandToKeys("command WQ wq")) + assertPluginError(false) + typeText(VimTestCase.commandToKeys("command")) + assertPluginError(false) + // The added alias should be listed + assertExOutput("""Name Args Definition + |Vs 0 vs + |Wq 0 wq + |WQ 0 wq + """.trimMargin()) + + typeText(VimTestCase.commandToKeys("command W")) + assertPluginError(false) + // The filtered aliases should be listed + assertExOutput("""Name Args Definition + |Wq 0 wq + |WQ 0 wq + """.trimMargin()) + + // Delete one of the aliases and then list all aliases again. + typeText(VimTestCase.commandToKeys("delcommand Wq")) + assertPluginError(false) + typeText(VimTestCase.commandToKeys("command")) + assertPluginError(false) + assertExOutput("""Name Args Definition + |Vs 0 vs + |WQ 0 wq + """.trimMargin()) + } + + fun `test remove non-existant alias`() { + VimPlugin.getCommand().resetAliases() + configureByText("\n") + typeText(VimTestCase.commandToKeys("delcommand VS")) + assertPluginError(true) + } +} \ No newline at end of file