1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2025-05-30 13:34:08 +02:00

Merge branch 'master' into VIM-510

This commit is contained in:
Alex Plate 2019-05-14 16:36:55 +03:00
commit bdc9b78ec8
No known key found for this signature in database
GPG Key ID: 0B97153C8FFEC09F
57 changed files with 3365 additions and 1089 deletions

View File

@ -3,3 +3,7 @@ root = true
[*.java] [*.java]
indent_size = 2 indent_size = 2
indent_style = space indent_style = space
[*.kt]
indent_size = 2
indent_style = space

View File

@ -66,6 +66,10 @@ Contributors:
* [gecko655](mailto:aqwsedrft1234@yahoo.co.jp) * [gecko655](mailto:aqwsedrft1234@yahoo.co.jp)
* [Daniele Megna](mailto:megna.dany@gmail.com) * [Daniele Megna](mailto:megna.dany@gmail.com)
* [Andrew Potter](mailto:apottere@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 If you are a contributor and your name is not listed here, feel free to
contact the maintainer. contact the maintainer.

View File

@ -38,6 +38,18 @@ To Be Released
* [VIM-607](https://youtrack.jetbrains.com/issue/VIM-607) Fix memory leaks * [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-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-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 0.51, 2019-02-12
---------------- ----------------

View File

@ -66,7 +66,7 @@ in the issue tracker.
### Copyright ### Copyright
1. Go to `Preferences | Appearance & Behavior | Scopes`, press "+" button, `local`. 1. Go to `Preferences | Appearance & Behavior | Scopes`, press "+" button, `Shared`.
Name: Copyright scope Name: Copyright scope
Pattern: `file[IdeaVIM.main]:com//*||file[IdeaVIM.test]:*/` Pattern: `file[IdeaVIM.main]:com//*||file[IdeaVIM.test]:*/`

191
index.txt
View File

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

View File

@ -4,6 +4,7 @@
<change-notes><![CDATA[ <change-notes><![CDATA[
<p>To be released:</p> <p>To be released:</p>
<ul> <ul>
<li>Support :command command</li>
<li>Support :shell command</li> <li>Support :shell command</li>
<li>Support :tabnext and :tabprevious commands</li> <li>Support :tabnext and :tabprevious commands</li>
<li>Commentary extension</li> <li>Commentary extension</li>
@ -67,6 +68,9 @@
<component> <component>
<implementation-class>com.maddyhome.idea.vim.VimPlugin</implementation-class> <implementation-class>com.maddyhome.idea.vim.VimPlugin</implementation-class>
</component> </component>
<component>
<implementation-class>com.maddyhome.idea.vim.VimLocalConfig</implementation-class>
</component>
</application-components> </application-components>
<extensionPoints> <extensionPoints>
@ -387,10 +391,7 @@
<action id="VimPlaybackLastRegister" class="com.maddyhome.idea.vim.action.macro.PlaybackLastRegisterAction" text="Playback Last Register"/> <action id="VimPlaybackLastRegister" class="com.maddyhome.idea.vim.action.macro.PlaybackLastRegisterAction" text="Playback Last Register"/>
<!-- Command Line --> <!-- 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="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 --> <!-- Other -->
<action id="VimLastSearchReplace" class="com.maddyhome.idea.vim.action.change.change.ChangeLastSearchReplaceAction" text="Repeat Last :s"/> <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"/> <action id="VimUndo" class="com.maddyhome.idea.vim.action.change.UndoAction" text="Undo"/>
<!-- Internal --> <!-- 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 --> <!-- Keys -->
<action id="VimShortcutKeyAction" class="com.maddyhome.idea.vim.action.VimShortcutKeyAction" text="Shortcuts"/> <action id="VimShortcutKeyAction" class="com.maddyhome.idea.vim.action.VimShortcutKeyAction" text="Shortcuts"/>

View File

@ -247,15 +247,15 @@ public class KeyHandler {
final CommandState commandState = CommandState.getInstance(editor); final CommandState commandState = CommandState.getInstance(editor);
commandState.stopMappingTimer(); commandState.stopMappingTimer();
final List<KeyStroke> mappingKeys = commandState.getMappingKeys();
final List<KeyStroke> fromKeys = new ArrayList<KeyStroke>(mappingKeys);
fromKeys.add(key);
final MappingMode mappingMode = commandState.getMappingMode(); final MappingMode mappingMode = commandState.getMappingMode();
if (MappingMode.NVO.contains(mappingMode) && (state != State.NEW_COMMAND || currentArg != Argument.Type.NONE)) { if (MappingMode.NVO.contains(mappingMode) && (state != State.NEW_COMMAND || currentArg != Argument.Type.NONE)) {
return false; 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 KeyMapping mapping = VimPlugin.getKey().getKeyMapping(mappingMode);
final MappingInfo currentMappingInfo = mapping.get(fromKeys); final MappingInfo currentMappingInfo = mapping.get(fromKeys);
final MappingInfo prevMappingInfo = mapping.get(mappingKeys); final MappingInfo prevMappingInfo = mapping.get(mappingKeys);

View File

@ -31,11 +31,11 @@ import javax.swing.*;
import java.awt.event.KeyEvent; import java.awt.event.KeyEvent;
import java.util.EnumSet; import java.util.EnumSet;
public class RegisterActions { class RegisterActions {
/** /**
* Register all the key/action mappings for the plugin. * Register all the key/action mappings for the plugin.
*/ */
public static void registerActions() { static void registerActions() {
registerVimCommandActions(); registerVimCommandActions();
registerInsertModeActions(); registerInsertModeActions();

View File

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

View File

@ -84,7 +84,7 @@ import java.util.concurrent.TimeUnit;
*/ */
@State( @State(
name = "VimSettings", name = "VimSettings",
storages = {@Storage(file = "$APP_CONFIG$/vim_settings.xml")}) storages = {@Storage("$APP_CONFIG$/vim_settings.xml")})
public class VimPlugin implements ApplicationComponent, PersistentStateComponent<Element> { public class VimPlugin implements ApplicationComponent, PersistentStateComponent<Element> {
private static final String IDEAVIM_COMPONENT_NAME = "VimPlugin"; private static final String IDEAVIM_COMPONENT_NAME = "VimPlugin";
private static final String IDEAVIM_PLUGIN_ID = "IdeaVIM"; 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_NOTIFICATION_ID = "ideavim";
public static final String IDEAVIM_STICKY_NOTIFICATION_ID = "ideavim-sticky"; public static final String IDEAVIM_STICKY_NOTIFICATION_ID = "ideavim-sticky";
public static final String IDEAVIM_NOTIFICATION_TITLE = "IdeaVim"; 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; private boolean error = false;
@ -106,6 +108,7 @@ public class VimPlugin implements ApplicationComponent, PersistentStateComponent
@NotNull private final MotionGroup motion; @NotNull private final MotionGroup motion;
@NotNull private final ChangeGroup change; @NotNull private final ChangeGroup change;
@NotNull private final CommandGroup command;
@NotNull private final MarkGroup mark; @NotNull private final MarkGroup mark;
@NotNull private final RegisterGroup register; @NotNull private final RegisterGroup register;
@NotNull private final FileGroup file; @NotNull private final FileGroup file;
@ -124,6 +127,7 @@ public class VimPlugin implements ApplicationComponent, PersistentStateComponent
public VimPlugin() { public VimPlugin() {
motion = new MotionGroup(); motion = new MotionGroup();
change = new ChangeGroup(); change = new ChangeGroup();
command = new CommandGroup();
mark = new MarkGroup(); mark = new MarkGroup();
register = new RegisterGroup(); register = new RegisterGroup();
file = new FileGroup(); file = new FileGroup();
@ -201,10 +205,6 @@ public class VimPlugin implements ApplicationComponent, PersistentStateComponent
state.setAttribute("enabled", Boolean.toString(enabled)); state.setAttribute("enabled", Boolean.toString(enabled));
element.addContent(state); element.addContent(state);
mark.saveData(element);
register.saveData(element);
search.saveData(element);
history.saveData(element);
key.saveData(element); key.saveData(element);
editor.saveData(element); editor.saveData(element);
@ -227,10 +227,13 @@ public class VimPlugin implements ApplicationComponent, PersistentStateComponent
previousKeyMap = state.getAttributeValue("keymap"); previousKeyMap = state.getAttributeValue("keymap");
} }
mark.readData(element); if (previousStateVersion > 0 && previousStateVersion < 5) {
register.readData(element); // Migrate settings from 4 to 5 version
search.readData(element); mark.readData(element);
history.readData(element); register.readData(element);
search.readData(element);
history.readData(element);
}
key.readData(element); key.readData(element);
editor.readData(element); editor.readData(element);
} }
@ -245,6 +248,9 @@ public class VimPlugin implements ApplicationComponent, PersistentStateComponent
return getInstance().change; return getInstance().change;
} }
@NotNull
public static CommandGroup getCommand() { return getInstance().command; }
@NotNull @NotNull
public static MarkGroup getMark() { public static MarkGroup getMark() {
return getInstance().mark; return getInstance().mark;
@ -359,7 +365,12 @@ public class VimPlugin implements ApplicationComponent, PersistentStateComponent
getInstance().error = true; getInstance().error = true;
} }
else if (!Options.getInstance().isSet("visualbell")) { 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;
}
} }
} }

View File

@ -42,11 +42,9 @@ import java.awt.event.KeyEvent;
public class VimTypedActionHandler implements TypedActionHandlerEx { public class VimTypedActionHandler implements TypedActionHandlerEx {
private static final Logger logger = Logger.getInstance(VimTypedActionHandler.class.getName()); private static final Logger logger = Logger.getInstance(VimTypedActionHandler.class.getName());
private final TypedActionHandler origHandler;
@NotNull private final KeyHandler handler; @NotNull private final KeyHandler handler;
public VimTypedActionHandler(TypedActionHandler origHandler) { VimTypedActionHandler(TypedActionHandler origHandler) {
this.origHandler = origHandler;
handler = KeyHandler.getInstance(); handler = KeyHandler.getInstance();
handler.setOriginalHandler(origHandler); handler.setOriginalHandler(origHandler);
} }
@ -57,7 +55,7 @@ public class VimTypedActionHandler implements TypedActionHandlerEx {
handler.beforeHandleKey(editor, KeyStroke.getKeyStroke(charTyped), context, plan); handler.beforeHandleKey(editor, KeyStroke.getKeyStroke(charTyped), context, plan);
} }
else { else {
TypedActionHandler originalHandler = KeyHandler.getInstance().getOriginalHandler(); TypedActionHandler originalHandler = handler.getOriginalHandler();
if (originalHandler instanceof TypedActionHandlerEx) { if (originalHandler instanceof TypedActionHandlerEx) {
((TypedActionHandlerEx)originalHandler).beforeExecute(editor, charTyped, context, plan); ((TypedActionHandlerEx)originalHandler).beforeExecute(editor, charTyped, context, plan);
} }
@ -76,6 +74,7 @@ public class VimTypedActionHandler implements TypedActionHandlerEx {
} }
else { else {
try (final VimListenerSuppressor ignored = SelectionVimListenerSuppressor.INSTANCE.lock()) { try (final VimListenerSuppressor ignored = SelectionVimListenerSuppressor.INSTANCE.lock()) {
TypedActionHandler origHandler = handler.getOriginalHandler();
origHandler.execute(editor, charTyped, context); origHandler.execute(editor, charTyped, context);
} }
} }

View File

@ -22,6 +22,7 @@ import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.ToggleAction; import com.intellij.openapi.actionSystem.ToggleAction;
import com.intellij.openapi.project.DumbAware; import com.intellij.openapi.project.DumbAware;
import com.maddyhome.idea.vim.VimPlugin; 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 * 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 * @param event The event that triggered the action
* @return true if the toggle is on, false if off * @return true if the toggle is on, false if off
*/ */
public boolean isSelected(AnActionEvent event) { public boolean isSelected(@NotNull AnActionEvent event) {
return VimPlugin.isEnabled(); return VimPlugin.isEnabled();
} }
@ -44,7 +45,7 @@ public class VimPluginToggleAction extends ToggleAction implements DumbAware {
* @param event The event that triggered the action * @param event The event that triggered the action
* @param b The new state - true is on, false is off * @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); VimPlugin.setEnabled(b);
} }
} }

View File

@ -27,6 +27,9 @@ import com.maddyhome.idea.vim.handler.EditorActionHandlerBase;
import org.jetbrains.annotations.NotNull; 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 class ProcessExEntryAction extends EditorAction {
public ProcessExEntryAction() { public ProcessExEntryAction() {

View File

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

View File

@ -54,7 +54,7 @@ public class WindowDownAction extends VimCommandAction {
@NotNull @NotNull
@Override @Override
public Set<List<KeyStroke>> getKeyStrokesSet() { 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 @NotNull

View File

@ -54,7 +54,7 @@ public class WindowLeftAction extends VimCommandAction {
@NotNull @NotNull
@Override @Override
public Set<List<KeyStroke>> getKeyStrokesSet() { 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 @NotNull

View File

@ -54,7 +54,7 @@ public class WindowRightAction extends VimCommandAction {
@NotNull @NotNull
@Override @Override
public Set<List<KeyStroke>> getKeyStrokesSet() { 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 @NotNull

View File

@ -54,7 +54,7 @@ public class WindowUpAction extends VimCommandAction {
@NotNull @NotNull
@Override @Override
public Set<List<KeyStroke>> getKeyStrokesSet() { 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 @NotNull

View File

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

View File

@ -40,6 +40,7 @@ import java.util.regex.Pattern;
* executes Ex commands entered by the user. * executes Ex commands entered by the user.
*/ */
public class CommandParser { public class CommandParser {
private static final int MAX_RECURSION = 100;
public static final int RES_EMPTY = 1; public static final int RES_EMPTY = 1;
public static final int RES_ERROR = 1; public static final int RES_ERROR = 1;
public static final int RES_READONLY = 1; public static final int RES_READONLY = 1;
@ -73,7 +74,10 @@ public class CommandParser {
new ActionListHandler(); new ActionListHandler();
new AsciiHandler(); new AsciiHandler();
new CmdFilterHandler(); new CmdFilterHandler();
new CmdHandler();
new CmdClearHandler();
new CopyTextHandler(); new CopyTextHandler();
new DelCmdHandler();
new DeleteLinesHandler(); new DeleteLinesHandler();
new DigraphHandler(); new DigraphHandler();
new DumpLineHandler(); new DumpLineHandler();
@ -165,14 +169,51 @@ public class CommandParser {
*/ */
public int processCommand(@NotNull Editor editor, @NotNull DataContext context, @NotNull String cmd, public int processCommand(@NotNull Editor editor, @NotNull DataContext context, @NotNull String cmd,
int count) throws ExException { 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 // Nothing entered
int result = 0; int result = 0;
if (cmd.length() == 0) { if (cmd.length() == 0) {
return result | RES_EMPTY; return result | RES_EMPTY;
} }
// Save the command history // Only save the command to the history if it is at the top of the stack.
VimPlugin.getHistory().addEntry(HistoryGroup.COMMAND, cmd); // 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 // Parse the command
final ExCommand command = parse(cmd); final ExCommand command = parse(cmd);
@ -192,7 +233,7 @@ public class CommandParser {
boolean ok = handler.process(editor, context, command, count); boolean ok = handler.process(editor, context, command, count);
if (ok && !handler.getArgFlags().getFlags().contains(CommandHandler.Flag.DONT_SAVE_LAST)) { if (ok && !handler.getArgFlags().getFlags().contains(CommandHandler.Flag.DONT_SAVE_LAST)) {
VimPlugin.getRegister().storeTextInternal(editor, new TextRange(-1, -1), cmd, 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)) { if (handler.getArgFlags().getFlags().contains(CommandHandler.Flag.DONT_REOPEN)) {

View File

@ -34,18 +34,20 @@ class ActionListHandler : CommandHandler(
flags(RangeFlag.RANGE_FORBIDDEN, ArgumentFlag.ARGUMENT_OPTIONAL, DONT_REOPEN) flags(RangeFlag.RANGE_FORBIDDEN, ArgumentFlag.ARGUMENT_OPTIONAL, DONT_REOPEN)
) { ) {
override fun execute(editor: Editor, context: DataContext, cmd: ExCommand): Boolean { 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 searchPattern = cmd.argument.trim().toLowerCase().split("*")
val actionManager = ActionManager.getInstance() val actionManager = ActionManager.getInstance()
val actions = actionManager.getActionIds("") val actions = actionManager.getActionIds("")
.filter { actionName -> searchPattern.all { it in actionName.toLowerCase() } } .sortedWith(String.CASE_INSENSITIVE_ORDER)
.sortedWith(String.CASE_INSENSITIVE_ORDER).joinToString(lineSeparator) { actionName -> .map { actionName ->
val shortcuts = actionManager.getAction(actionName).shortcutSet.shortcuts.joinToString(" ") { val shortcuts = actionManager.getAction(actionName).shortcutSet.shortcuts.joinToString(" ") {
if (it is KeyboardShortcut) StringHelper.toKeyNotation(it.firstKeyStroke) else it.toString() if (it is KeyboardShortcut) StringHelper.toKeyNotation(it.firstKeyStroke) else it.toString()
} }
if (shortcuts.isBlank()) actionName else "${actionName.padEnd(50)} $shortcuts" 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") ExOutputModel.getInstance(editor).output("--- Actions ---$lineSeparator$actions")

View File

@ -16,26 +16,22 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * 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.actionSystem.DataContext
import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actionSystem.EditorAction; import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.VimPlugin; import com.maddyhome.idea.vim.ex.CommandHandler
import com.maddyhome.idea.vim.command.Command; import com.maddyhome.idea.vim.ex.ExCommand
import com.maddyhome.idea.vim.handler.EditorActionHandlerBase; import com.maddyhome.idea.vim.ex.commands
import org.jetbrains.annotations.NotNull; import com.maddyhome.idea.vim.ex.flags
/** class CmdClearHandler : CommandHandler(
*/ commands("comc[lear]"),
public class BackspaceAction extends EditorAction { flags(RangeFlag.RANGE_FORBIDDEN, ArgumentFlag.ARGUMENT_FORBIDDEN)
public BackspaceAction() { ) {
super(new Handler()); override fun execute(editor: Editor, context: DataContext, cmd: ExCommand): Boolean {
} VimPlugin.getCommand().resetAliases()
return true
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));
} }
} }
}

View File

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

View File

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

View File

@ -1069,20 +1069,20 @@ public class ChangeGroup {
if (motion == null) { if (motion == null) {
return false; return false;
} }
EnumSet<CommandFlags> flags = motion.getFlags().clone();
if (!isChange && !motion.getFlags().contains(CommandFlags.FLAG_MOT_LINEWISE)) { if (!isChange && !motion.getFlags().contains(CommandFlags.FLAG_MOT_LINEWISE)) {
LogicalPosition start = editor.offsetToLogicalPosition(range.getStartOffset()); LogicalPosition start = editor.offsetToLogicalPosition(range.getStartOffset());
LogicalPosition end = editor.offsetToLogicalPosition(range.getEndOffset()); LogicalPosition end = editor.offsetToLogicalPosition(range.getEndOffset());
if (start.line != end.line) { if (start.line != end.line) {
if (!SearchHelper.anyNonWhitespace(editor, range.getStartOffset(), -1) && if (!SearchHelper.anyNonWhitespace(editor, range.getStartOffset(), -1) &&
!SearchHelper.anyNonWhitespace(editor, range.getEndOffset(), 1)) { !SearchHelper.anyNonWhitespace(editor, range.getEndOffset(), 1)) {
EnumSet<CommandFlags> flags = motion.getFlags();
flags.remove(CommandFlags.FLAG_MOT_EXCLUSIVE); flags.remove(CommandFlags.FLAG_MOT_EXCLUSIVE);
flags.remove(CommandFlags.FLAG_MOT_INCLUSIVE); flags.remove(CommandFlags.FLAG_MOT_INCLUSIVE);
flags.add(CommandFlags.FLAG_MOT_LINEWISE); 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; 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 Document document = editor.getDocument();
final int[] startOffsets = range.getStartOffsets(); final int[] startOffsets = range.getStartOffsets();
final int[] endOffsets = range.getEndOffsets(); final int[] endOffsets = range.getEndOffsets();

View File

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

View File

@ -28,7 +28,6 @@ import com.intellij.openapi.editor.event.*;
import com.intellij.openapi.editor.ex.EditorEx; import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.project.Project; import com.intellij.openapi.project.Project;
import com.maddyhome.idea.vim.EventFacade; import com.maddyhome.idea.vim.EventFacade;
import com.maddyhome.idea.vim.KeyHandler;
import com.maddyhome.idea.vim.VimPlugin; import com.maddyhome.idea.vim.VimPlugin;
import com.maddyhome.idea.vim.command.CommandState; import com.maddyhome.idea.vim.command.CommandState;
import com.maddyhome.idea.vim.helper.*; import com.maddyhome.idea.vim.helper.*;
@ -39,7 +38,6 @@ import org.jdom.Element;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*; import java.awt.*;
import java.util.List; import java.util.List;
@ -94,7 +92,7 @@ public class EditorGroup {
// Turn on insert mode if editor doesn't have any file // Turn on insert mode if editor doesn't have any file
if (!EditorData.isFileEditor(editor) && editor.getDocument().isWritable() && if (!EditorData.isFileEditor(editor) && editor.getDocument().isWritable() &&
!CommandState.inInsertMode(editor)) { !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().setBlockCursor(!CommandState.inInsertMode(editor));
editor.getSettings().setAnimatedScrolling(ANIMATED_SCROLLING_VIM_VALUE); editor.getSettings().setAnimatedScrolling(ANIMATED_SCROLLING_VIM_VALUE);

View File

@ -68,13 +68,12 @@ public class KeyGroup {
@NotNull private final Map<MappingMode, KeyMapping> keyMappings = new HashMap<>(); @NotNull private final Map<MappingMode, KeyMapping> keyMappings = new HashMap<>();
@Nullable private OperatorFunction operatorFunction = null; @Nullable private OperatorFunction operatorFunction = null;
public void registerRequiredShortcutKeys(@NotNull Editor editor) { void registerRequiredShortcutKeys(@NotNull Editor editor) {
final Set<KeyStroke> requiredKeys = VimPlugin.getKey().requiredShortcutKeys;
EventFacade.getInstance().registerCustomShortcutSet(VimShortcutKeyAction.getInstance(), 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()); EventFacade.getInstance().unregisterCustomShortcutSet(VimShortcutKeyAction.getInstance(), editor.getComponent());
} }

View File

@ -54,6 +54,7 @@ import com.maddyhome.idea.vim.listener.VimListenerManager;
import com.maddyhome.idea.vim.option.NumberOption; import com.maddyhome.idea.vim.option.NumberOption;
import com.maddyhome.idea.vim.option.Options; import com.maddyhome.idea.vim.option.Options;
import com.maddyhome.idea.vim.ui.ExEntryPanel; import com.maddyhome.idea.vim.ui.ExEntryPanel;
import kotlin.ranges.IntProgression;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; 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) { public int moveCaretHorizontal(@NotNull Editor editor, @NotNull Caret caret, int count, boolean allowPastEnd) {
int oldOffset = caret.getOffset(); 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) { if (offset == oldOffset) {
return -1; return -1;

View File

@ -90,7 +90,7 @@ public class ProcessGroup {
ExEntryPanel panel = ExEntryPanel.getInstance(); ExEntryPanel panel = ExEntryPanel.getInstance();
if (panel.isActive()) { if (panel.isActive()) {
UiHelper.requestFocus(panel); UiHelper.requestFocus(panel.getEntry());
panel.handleKey(stroke); panel.handleKey(stroke);
return true; return true;
@ -144,13 +144,11 @@ public class ProcessGroup {
return res; 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(); CommandState.getInstance(editor).popState();
KeyHandler.getInstance().reset(editor); KeyHandler.getInstance().reset(editor);
ExEntryPanel panel = ExEntryPanel.getInstance(); ExEntryPanel panel = ExEntryPanel.getInstance();
panel.deactivate(true); panel.deactivate(true);
return true;
} }
private void record(Editor editor, @NotNull String text) { private void record(Editor editor, @NotNull String text) {

View File

@ -49,7 +49,7 @@ public class DigraphSequence {
if (key.getKeyCode() == KeyEvent.VK_K && (key.getModifiers() & KeyEvent.CTRL_MASK) != 0) { if (key.getKeyCode() == KeyEvent.VK_K && (key.getModifiers() & KeyEvent.CTRL_MASK) != 0) {
logger.debug("found Ctrl-K"); logger.debug("found Ctrl-K");
digraphState = DIG_STATE_DIG_ONE; digraphState = DIG_STATE_DIG_ONE;
return DigraphResult.OK; return DigraphResult.OK_DIGRAPH;
} }
else if ((key.getKeyCode() == KeyEvent.VK_V || key.getKeyCode() == KeyEvent.VK_Q) && else if ((key.getKeyCode() == KeyEvent.VK_V || key.getKeyCode() == KeyEvent.VK_Q) &&
(key.getModifiers() & KeyEvent.CTRL_MASK) != 0) { (key.getModifiers() & KeyEvent.CTRL_MASK) != 0) {
@ -57,7 +57,7 @@ public class DigraphSequence {
digraphState = DIG_STATE_CODE_START; digraphState = DIG_STATE_CODE_START;
codeChars = new char[8]; codeChars = new char[8];
codeCnt = 0; codeCnt = 0;
return DigraphResult.OK; return DigraphResult.OK_LITERAL;
} }
else { else {
return new DigraphResult(key); return new DigraphResult(key);
@ -68,7 +68,7 @@ public class DigraphSequence {
digraphChar = key.getKeyChar(); digraphChar = key.getKeyChar();
digraphState = DIG_STATE_DIG_TWO; digraphState = DIG_STATE_DIG_TWO;
return DigraphResult.OK; return new DigraphResult(DigraphResult.RES_OK, digraphChar);
} }
else { else {
digraphState = DIG_STATE_START; digraphState = DIG_STATE_START;
@ -93,26 +93,26 @@ public class DigraphSequence {
digraphState = DIG_STATE_CODE_CHAR; digraphState = DIG_STATE_CODE_CHAR;
codeType = 8; codeType = 8;
logger.debug("Octal"); logger.debug("Octal");
return DigraphResult.OK; return DigraphResult.OK_LITERAL;
case 'x': case 'x':
case 'X': case 'X':
codeMax = 2; codeMax = 2;
digraphState = DIG_STATE_CODE_CHAR; digraphState = DIG_STATE_CODE_CHAR;
codeType = 16; codeType = 16;
logger.debug("hex2"); logger.debug("hex2");
return DigraphResult.OK; return DigraphResult.OK_LITERAL;
case 'u': case 'u':
codeMax = 4; codeMax = 4;
digraphState = DIG_STATE_CODE_CHAR; digraphState = DIG_STATE_CODE_CHAR;
codeType = 16; codeType = 16;
logger.debug("hex4"); logger.debug("hex4");
return DigraphResult.OK; return DigraphResult.OK_LITERAL;
case 'U': case 'U':
codeMax = 8; codeMax = 8;
digraphState = DIG_STATE_CODE_CHAR; digraphState = DIG_STATE_CODE_CHAR;
codeType = 16; codeType = 16;
logger.debug("hex8"); logger.debug("hex8");
return DigraphResult.OK; return DigraphResult.OK_LITERAL;
case '0': case '0':
case '1': case '1':
case '2': case '2':
@ -128,7 +128,7 @@ public class DigraphSequence {
codeType = 10; codeType = 10;
codeChars[codeCnt++] = key.getKeyChar(); codeChars[codeCnt++] = key.getKeyChar();
logger.debug("decimal"); logger.debug("decimal");
return DigraphResult.OK; return DigraphResult.OK_LITERAL;
default: default:
switch (key.getKeyCode()) { switch (key.getKeyCode()) {
case KeyEvent.VK_TAB: case KeyEvent.VK_TAB:
@ -177,7 +177,7 @@ public class DigraphSequence {
return new DigraphResult(code); return new DigraphResult(code);
} }
else { else {
return DigraphResult.OK; return DigraphResult.OK_LITERAL;
} }
} }
else if (codeCnt > 0) { else if (codeCnt > 0) {
@ -204,14 +204,21 @@ public class DigraphSequence {
public static final int RES_BAD = 1; public static final int RES_BAD = 1;
public static final int RES_DONE = 2; public static final int RES_DONE = 2;
public static final DigraphResult OK = new DigraphResult(RES_OK); static final DigraphResult OK_DIGRAPH = new DigraphResult(RES_OK, '?');
public static final DigraphResult BAD = new DigraphResult(RES_BAD); static final DigraphResult OK_LITERAL = new DigraphResult(RES_OK, '^');
static final DigraphResult BAD = new DigraphResult(RES_BAD);
DigraphResult(int result) { DigraphResult(int result) {
this.result = result; this.result = result;
stroke = null; stroke = null;
} }
DigraphResult(int result, char promptCharacter) {
this.result = result;
this.promptCharacter = promptCharacter;
stroke = null;
}
DigraphResult(@Nullable KeyStroke stroke) { DigraphResult(@Nullable KeyStroke stroke) {
result = RES_DONE; result = RES_DONE;
this.stroke = stroke; this.stroke = stroke;
@ -226,8 +233,13 @@ public class DigraphSequence {
return result; return result;
} }
public char getPromptCharacter() {
return promptCharacter;
}
private final int result; private final int result;
@Nullable private final KeyStroke stroke; @Nullable private final KeyStroke stroke;
private char promptCharacter;
} }
private int digraphState = DIG_STATE_START; private int digraphState = DIG_STATE_START;

View File

@ -18,7 +18,6 @@
package com.maddyhome.idea.vim.helper; package com.maddyhome.idea.vim.helper;
import com.google.common.collect.Lists;
import com.intellij.lang.CodeDocumentationAwareCommenter; import com.intellij.lang.CodeDocumentationAwareCommenter;
import com.intellij.lang.Commenter; import com.intellij.lang.Commenter;
import com.intellij.lang.Language; 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.Caret;
import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiComment; import com.intellij.psi.PsiComment;
import com.intellij.psi.PsiElement; import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile; import com.intellij.psi.PsiFile;
@ -42,6 +40,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Stack;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -115,13 +114,32 @@ public class SearchHelper {
int pos = caret.getOffset(); int pos = caret.getOffset();
int start = caret.getSelectionStart(); int start = caret.getSelectionStart();
int end = caret.getSelectionEnd(); int end = caret.getSelectionEnd();
if (start != end) {
pos = Math.min(start, end);
}
int loc = blockChars.indexOf(type); int loc = blockChars.indexOf(type);
char close = blockChars.charAt(loc + 1); 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); boolean initialPosIsInString = checkInString(chars, pos, true);
int bstart = -1; int bstart = -1;
@ -413,80 +431,192 @@ public class SearchHelper {
return -1; 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 @Nullable
public static TextRange findBlockTagRange(@NotNull Editor editor, @NotNull Caret caret, int count, boolean isOuter) { public static TextRange findBlockTagRange(@NotNull Editor editor, @NotNull Caret caret, int count, boolean isOuter) {
final int cursorOffset = caret.getOffset(); final int position = caret.getOffset();
int pos = cursorOffset;
int currentCount = count;
final CharSequence sequence = editor.getDocument().getCharsSequence(); 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) { while (true) {
final Pair<TextRange, String> closingTagResult = findClosingTag(sequence, pos); final Pair<TextRange, String> closingTag = findUnmatchedClosingTag(sequence, searchStartPosition, count);
if (closingTagResult == null) { if (closingTag == null) {
return null; return null;
} }
final TextRange closingTagTextRange = closingTagResult.getFirst(); final TextRange closingTagTextRange = closingTag.getFirst();
final String tagName = closingTagResult.getSecond(); final String tagName = closingTag.getSecond();
final TextRange openingTagTextRange = findOpeningTag(sequence, closingTagTextRange.getStartOffset(), tagName);
if (openingTagTextRange != null && openingTagTextRange.getStartOffset() <= cursorOffset && --currentCount == 0) { TextRange openingTag = findUnmatchedOpeningTag(sequence, closingTagTextRange.getStartOffset(), tagName);
if (isOuter) { if (openingTag == null) {
return new TextRange(openingTagTextRange.getStartOffset(), closingTagTextRange.getEndOffset()); 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 { if (openTags.isEmpty()) {
return new TextRange(openingTagTextRange.getEndOffset() + 1, closingTagTextRange.getStartOffset() - 1); 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 { else {
pos = closingTagTextRange.getEndOffset() + 1; openTags.push(match);
} }
} }
}
@Nullable if (openTags.isEmpty()) {
private static TextRange findOpeningTag(@NotNull CharSequence sequence, int position, @NotNull String tagName) { return null;
final String tagBeginning = "<" + tagName; } else {
final Pattern pattern = Pattern.compile(Pattern.quote(tagBeginning), Pattern.CASE_INSENSITIVE); return openTags.pop();
final Matcher matcher = pattern.matcher(sequence.subSequence(0, position));
final List<Integer> possibleBeginnings = Lists.newArrayList();
while (matcher.find()) {
possibleBeginnings.add(matcher.start());
} }
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;
} }

View File

@ -190,7 +190,7 @@ object VimListenerManager {
VimPlugin.getMotion() VimPlugin.getMotion()
val editor = event.editor val editor = event.editor
if (ExEntryPanel.getInstance().isActive) { if (ExEntryPanel.getInstance().isActive) {
ExEntryPanel.getInstance().deactivate(false) VimPlugin.getProcess().cancelExEntry(editor, ExEntryPanel.getInstance().entry.context)
} }
ExOutputModel.getInstance(editor).clear() ExOutputModel.getInstance(editor).clear()
@ -226,7 +226,7 @@ object VimListenerManager {
event.mouseEvent.button != MouseEvent.BUTTON3) { event.mouseEvent.button != MouseEvent.BUTTON3) {
VimPlugin.getMotion() VimPlugin.getMotion()
if (ExEntryPanel.getInstance().isActive) { if (ExEntryPanel.getInstance().isActive) {
ExEntryPanel.getInstance().deactivate(false) VimPlugin.getProcess().cancelExEntry(event.editor, ExEntryPanel.getInstance().entry.context)
} }
ExOutputModel.getInstance(event.editor).clear() ExOutputModel.getInstance(event.editor).clear()

View File

@ -307,6 +307,10 @@
* |CTRL-W_<Up>| {@link com.maddyhome.idea.vim.action.window.WindowUpAction} * |CTRL-W_<Up>| {@link com.maddyhome.idea.vim.action.window.WindowUpAction}
* |CTRL-W_<Left>| {@link com.maddyhome.idea.vim.action.window.WindowLeftAction} * |CTRL-W_<Left>| {@link com.maddyhome.idea.vim.action.window.WindowLeftAction}
* |CTRL-W_<Right>| {@link com.maddyhome.idea.vim.action.window.WindowRightAction} * |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 * 2.3. Square bracket commands
@ -594,7 +598,72 @@
* *
* 5. Command line editing * 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 * 6. Ex commands
* *
@ -621,6 +690,9 @@
* |:quitall| {@link com.maddyhome.idea.vim.ex.handler.ExitHandler} * |:quitall| {@link com.maddyhome.idea.vim.ex.handler.ExitHandler}
* |:wqall| {@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} * |: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. * The list of supported Ex commands is incomplete.

View File

@ -20,10 +20,7 @@ package com.maddyhome.idea.vim.ui;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import javax.swing.text.AttributeSet; import javax.swing.text.*;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.PlainDocument;
/** /**
* This document provides insert/overwrite mode * This document provides insert/overwrite mode
@ -32,7 +29,7 @@ public class ExDocument extends PlainDocument {
/** /**
* Toggles the insert/overwrite state * Toggles the insert/overwrite state
*/ */
public void toggleInsertReplace() { void toggleInsertReplace() {
overwrite = !overwrite; 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;
} }

View File

@ -81,13 +81,13 @@ public class ExEditorKit extends DefaultEditorKit {
} }
@Nullable @Nullable
public static KeyStroke convert(@NotNull ActionEvent event) { private static KeyStroke convert(@NotNull ActionEvent event) {
String cmd = event.getActionCommand(); String cmd = event.getActionCommand();
int mods = event.getModifiers(); int mods = event.getModifiers();
if (cmd != null && cmd.length() > 0) { if (cmd != null && cmd.length() > 0) {
char ch = cmd.charAt(0); char ch = cmd.charAt(0);
if (ch < ' ') { if (ch < ' ') {
if (mods == KeyEvent.CTRL_MASK) { if ((mods & KeyEvent.CTRL_MASK) != 0) {
return KeyStroke.getKeyStroke(KeyEvent.VK_A + ch - 1, mods); return KeyStroke.getKeyStroke(KeyEvent.VK_A + ch - 1, mods);
} }
} }
@ -99,25 +99,22 @@ public class ExEditorKit extends DefaultEditorKit {
return null; return null;
} }
public static final String DefaultExKey = "default-ex-key"; static final String CancelEntry = "cancel-entry";
public static final String CancelEntry = "cancel-entry"; static final String CompleteEntry = "complete-entry";
public static final String CompleteEntry = "complete-entry"; static final String EscapeChar = "escape";
public static final String EscapeChar = "escape"; static final String DeletePreviousChar = "delete-prev-char";
public static final String DeletePreviousChar = "delete-prev-char"; static final String DeletePreviousWord = "delete-prev-word";
public static final String DeletePreviousWord = "delete-prev-word"; static final String DeleteToCursor = "delete-to-cursor";
public static final String DeleteToCursor = "delete-to-cursor"; static final String DeleteFromCursor = "delete-from-cursor";
public static final String DeleteFromCursor = "delete-from-cursor"; static final String ToggleInsertReplace = "toggle-insert";
public static final String ToggleInsertReplace = "toggle-insert"; static final String InsertRegister = "insert-register";
public static final String InsertRegister = "insert-register"; static final String HistoryUp = "history-up";
public static final String InsertWord = "insert-word"; static final String HistoryDown = "history-down";
public static final String InsertWORD = "insert-WORD"; static final String HistoryUpFilter = "history-up-filter";
public static final String HistoryUp = "history-up"; static final String HistoryDownFilter = "history-down-filter";
public static final String HistoryDown = "history-down"; static final String StartDigraph = "start-digraph";
public static final String HistoryUpFilter = "history-up-filter";
public static final String HistoryDownFilter = "history-down-filter";
public 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.CancelEntryAction(),
new ExEditorKit.CompleteEntryAction(), new ExEditorKit.CompleteEntryAction(),
new ExEditorKit.EscapeCharAction(), 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 static class HistoryUpAction extends TextAction {
public HistoryUpAction() { HistoryUpAction() {
super(HistoryUp); super(HistoryUp);
} }
@ -172,7 +173,7 @@ public class ExEditorKit extends DefaultEditorKit {
} }
public static class HistoryDownAction extends TextAction { public static class HistoryDownAction extends TextAction {
public HistoryDownAction() { HistoryDownAction() {
super(HistoryDown); super(HistoryDown);
} }
@ -183,7 +184,7 @@ public class ExEditorKit extends DefaultEditorKit {
} }
public static class HistoryUpFilterAction extends TextAction { public static class HistoryUpFilterAction extends TextAction {
public HistoryUpFilterAction() { HistoryUpFilterAction() {
super(HistoryUpFilter); super(HistoryUpFilter);
} }
@ -194,7 +195,7 @@ public class ExEditorKit extends DefaultEditorKit {
} }
public static class HistoryDownFilterAction extends TextAction { public static class HistoryDownFilterAction extends TextAction {
public HistoryDownFilterAction() { HistoryDownFilterAction() {
super(HistoryDownFilter); super(HistoryDownFilter);
} }
@ -204,15 +205,15 @@ public class ExEditorKit extends DefaultEditorKit {
} }
} }
public static class InsertRegisterAction extends TextAction { public static class InsertRegisterAction extends TextAction implements MultiStepAction {
private static enum State { private enum State {
SKIP_CTRL_R, SKIP_CTRL_R,
WAIT_REGISTER, WAIT_REGISTER,
} }
@NotNull private State state = State.SKIP_CTRL_R; @NotNull private State state = State.SKIP_CTRL_R;
public InsertRegisterAction() { InsertRegisterAction() {
super(InsertRegister); super(InsertRegister);
} }
@ -223,11 +224,12 @@ public class ExEditorKit extends DefaultEditorKit {
switch (state) { switch (state) {
case SKIP_CTRL_R: case SKIP_CTRL_R:
state = State.WAIT_REGISTER; state = State.WAIT_REGISTER;
target.setCurrentAction(this); target.setCurrentAction(this, '\"');
break; break;
case WAIT_REGISTER: case WAIT_REGISTER:
state = State.SKIP_CTRL_R; state = State.SKIP_CTRL_R;
target.setCurrentAction(null); target.clearCurrentAction();
final char c = key.getKeyChar(); final char c = key.getKeyChar();
if (c != KeyEvent.CHAR_UNDEFINED) { if (c != KeyEvent.CHAR_UNDEFINED) {
final Register register = VimPlugin.getRegister().getRegister(c); final Register register = VimPlugin.getRegister().getRegister(c);
@ -235,20 +237,27 @@ public class ExEditorKit extends DefaultEditorKit {
final String oldText = target.getText(); final String oldText = target.getText();
final String text = register.getText(); final String text = register.getText();
if (oldText != null && text != null) { 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 if ((key.getModifiers() & KeyEvent.CTRL_MASK) != 0 && key.getKeyCode() == KeyEvent.VK_C) {
else { // Eat any unused keys, unless it's <C-C>, in which case forward on and cancel entry
target.handleKey(key); target.handleKey(key);
} }
} }
} }
} }
@Override
public void reset() {
state = State.SKIP_CTRL_R;
}
} }
public static class CompleteEntryAction extends TextAction { public static class CompleteEntryAction extends TextAction {
public CompleteEntryAction() { CompleteEntryAction() {
super(CompleteEntry); super(CompleteEntry);
} }
@ -256,27 +265,29 @@ public class ExEditorKit extends DefaultEditorKit {
logger.debug("complete entry"); logger.debug("complete entry");
KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0); KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0);
KeyHandler.getInstance().handleKey( // We send the <Enter> keystroke through the key handler rather than calling ProcessGroup#processExEntry directly.
ExEntryPanel.getInstance().getEntry().getEditor(), // We do this for a couple of reasons:
stroke, // * The C mode mapping for ProcessExEntryAction handles the actual entry, and most importantly, it does so as a
ExEntryPanel.getInstance().getEntry().getContext()); // 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 static class CancelEntryAction extends TextAction {
public CancelEntryAction() { CancelEntryAction() {
super(CancelEntry); super(CancelEntry);
} }
public void actionPerformed(ActionEvent e) { public void actionPerformed(ActionEvent e) {
VimPlugin.getProcess().cancelExEntry( ExTextField target = (ExTextField)getTextComponent(e);
ExEntryPanel.getInstance().getEntry().getEditor(), target.cancel();
ExEntryPanel.getInstance().getEntry().getContext());
} }
} }
public static class EscapeCharAction extends TextAction { public static class EscapeCharAction extends TextAction {
public EscapeCharAction() { EscapeCharAction() {
super(EscapeChar); super(EscapeChar);
} }
@ -287,7 +298,7 @@ public class ExEditorKit extends DefaultEditorKit {
} }
public static class DeletePreviousCharAction extends TextAction { public static class DeletePreviousCharAction extends TextAction {
public DeletePreviousCharAction() { DeletePreviousCharAction() {
super(DeletePreviousChar); super(DeletePreviousChar);
} }
@ -323,9 +334,7 @@ public class ExEditorKit extends DefaultEditorKit {
doc.remove(dot - delChars, delChars); doc.remove(dot - delChars, delChars);
} }
else { else {
VimPlugin.getProcess().cancelExEntry( target.cancel();
ExEntryPanel.getInstance().getEntry().getEditor(),
ExEntryPanel.getInstance().getEntry().getContext());
} }
} }
catch (BadLocationException bl) { catch (BadLocationException bl) {
@ -335,7 +344,7 @@ public class ExEditorKit extends DefaultEditorKit {
} }
public static class DeletePreviousWordAction extends TextAction { public static class DeletePreviousWordAction extends TextAction {
public DeletePreviousWordAction() { DeletePreviousWordAction() {
super(DeletePreviousWord); super(DeletePreviousWord);
} }
@ -362,7 +371,7 @@ public class ExEditorKit extends DefaultEditorKit {
} }
public static class DeleteToCursorAction extends TextAction { public static class DeleteToCursorAction extends TextAction {
public DeleteToCursorAction() { DeleteToCursorAction() {
super(DeleteToCursor); super(DeleteToCursor);
} }
@ -385,7 +394,7 @@ public class ExEditorKit extends DefaultEditorKit {
} }
public static class DeleteFromCursorAction extends TextAction { public static class DeleteFromCursorAction extends TextAction {
public DeleteFromCursorAction() { DeleteFromCursorAction() {
super(DeleteFromCursor); super(DeleteFromCursor);
} }
@ -408,7 +417,7 @@ public class ExEditorKit extends DefaultEditorKit {
} }
public static class ToggleInsertReplaceAction extends TextAction { public static class ToggleInsertReplaceAction extends TextAction {
public ToggleInsertReplaceAction() { ToggleInsertReplaceAction() {
super(ToggleInsertReplace); super(ToggleInsertReplace);
logger.debug("ToggleInsertReplaceAction()"); 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; @Nullable private DigraphSequence digraphSequence;
public StartDigraphAction() { StartDigraphAction() {
super(StartDigraph); super(StartDigraph);
} }
@ -437,14 +446,23 @@ public class ExEditorKit extends DefaultEditorKit {
if (key != null && digraphSequence != null) { if (key != null && digraphSequence != null) {
DigraphSequence.DigraphResult res = digraphSequence.processKey(key, target.getEditor()); DigraphSequence.DigraphResult res = digraphSequence.processKey(key, target.getEditor());
switch (res.getResult()) { switch (res.getResult()) {
case DigraphSequence.DigraphResult.RES_BAD: case DigraphSequence.DigraphResult.RES_OK:
target.setCurrentAction(null); target.setCurrentActionPromptCharacter(res.getPromptCharacter());
target.handleKey(key);
break; 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: case DigraphSequence.DigraphResult.RES_DONE:
final KeyStroke digraph = res.getStroke(); final KeyStroke digraph = res.getStroke();
digraphSequence = null; digraphSequence = null;
target.setCurrentAction(null); target.clearCurrentAction();
if (digraph != null) { if (digraph != null) {
target.handleKey(digraph); target.handleKey(digraph);
} }
@ -452,11 +470,16 @@ public class ExEditorKit extends DefaultEditorKit {
} }
} }
else if (key != null && DigraphSequence.isDigraphStart(key)) { else if (key != null && DigraphSequence.isDigraphStart(key)) {
target.setCurrentAction(this);
digraphSequence = new DigraphSequence(); 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; private static ExEditorKit instance;

View File

@ -79,6 +79,8 @@ public class ExEntryPanel extends JPanel implements LafManagerListener {
} }
}; };
new ExShortcutKeyAction(this).registerCustomShortcutSet();
LafManager.getInstance().addLafManagerListener(this); LafManager.getInstance().addLafManagerListener(this);
updateUI(); updateUI();
@ -93,6 +95,7 @@ public class ExEntryPanel extends JPanel implements LafManagerListener {
private void setFontForElements() { private void setFontForElements() {
final Font font = UiHelper.getEditorFont(); final Font font = UiHelper.getEditorFont();
label.setFont(font); 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 * @param count A holder for the ex entry count
*/ */
public void activate(@NotNull Editor editor, DataContext context, @NotNull String label, String initText, int 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.label.setText(label);
this.count = count; this.count = count;
setFontForElements(); setFontForElements();
entry.setDocument(entry.createDefaultModel()); entry.reset();
entry.setEditor(editor, context);
entry.setText(initText); entry.setText(initText);
entry.setType(label); entry.setType(label);
parent = editor.getContentComponent(); parent = editor.getContentComponent();
@ -139,7 +142,7 @@ public class ExEntryPanel extends JPanel implements LafManagerListener {
public void updateUI() { public void updateUI() {
super.updateUI(); super.updateUI();
setBorder(BorderFactory.createEtchedBorder()); setBorder(new ExPanelBorder());
// Can be null when called from base constructor // Can be null when called from base constructor
//noinspection ConstantConditions //noinspection ConstantConditions
@ -229,6 +232,8 @@ public class ExEntryPanel extends JPanel implements LafManagerListener {
logger.info("deactivate"); logger.info("deactivate");
if (!active) return; if (!active) return;
active = false; active = false;
entry.deactivate();
if (!ApplicationManager.getApplication().isUnitTestMode()) { if (!ApplicationManager.getApplication().isUnitTestMode()) {
if (refocusOwningEditor && parent != null) { if (refocusOwningEditor && parent != null) {
UiHelper.requestFocus(parent); UiHelper.requestFocus(parent);
@ -279,7 +284,7 @@ public class ExEntryPanel extends JPanel implements LafManagerListener {
@NotNull private final DocumentListener documentListener = new DocumentAdapter() { @NotNull private final DocumentListener documentListener = new DocumentAdapter() {
@Override @Override
protected void textChanged(DocumentEvent e) { protected void textChanged(@NotNull DocumentEvent e) {
final Editor editor = entry.getEditor(); final Editor editor = entry.getEditor();
final boolean forwards = !label.getText().equals("?"); final boolean forwards = !label.getText().equals("?");
if (incHighlighter != null) { if (incHighlighter != null) {
@ -300,6 +305,5 @@ public class ExEntryPanel extends JPanel implements LafManagerListener {
private boolean active; private boolean active;
private static ExEntryPanel instance; private static ExEntryPanel instance;
private static final Logger logger = Logger.getInstance(ExEntryPanel.class.getName()); private static final Logger logger = Logger.getInstance(ExEntryPanel.class.getName());
} }

View File

@ -24,20 +24,20 @@ import javax.swing.*;
import javax.swing.text.JTextComponent.KeyBinding; import javax.swing.text.JTextComponent.KeyBinding;
import java.awt.event.KeyEvent; import java.awt.event.KeyEvent;
/**
*
*/
public class ExKeyBindings { public class ExKeyBindings {
@NotNull @NotNull
public static KeyBinding[] getBindings() { static KeyBinding[] getBindings() {
return bindings; return bindings;
} }
// TODO - add the following keys: // TODO - add the following keys:
// Ctrl-\ Ctrl-N - abort // Ctrl-\ Ctrl-N - abort
static final KeyBinding[] bindings = new KeyBinding[]{ 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_ESCAPE, 0), ExEditorKit.EscapeChar),
new KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_OPEN_BRACKET, KeyEvent.CTRL_MASK), 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_C, KeyEvent.CTRL_MASK), ExEditorKit.CancelEntry),
new KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), ExEditorKit.CompleteEntry), 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_V, KeyEvent.CTRL_MASK), ExEditorKit.StartDigraph),
new KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_Q, 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_V, KeyEvent.META_MASK), ExEditorKit.pasteAction),
new KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, KeyEvent.SHIFT_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),
}; };
} }

View File

@ -113,7 +113,7 @@ public class ExOutputPanel extends JPanel implements LafManagerListener {
public void updateUI() { public void updateUI() {
super.updateUI(); super.updateUI();
setBorder(BorderFactory.createEtchedBorder()); setBorder(new ExPanelBorder());
// Can be null when called from base constructor // Can be null when called from base constructor
//noinspection ConstantConditions //noinspection ConstantConditions

View File

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

View File

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

View File

@ -21,48 +21,62 @@ package com.maddyhome.idea.vim.ui;
import com.intellij.openapi.actionSystem.DataContext; import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor; 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.project.Project;
import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.Disposer;
import com.intellij.util.ui.JBUI; import com.intellij.util.ui.JBUI;
import com.maddyhome.idea.vim.VimPlugin; import com.maddyhome.idea.vim.VimPlugin;
import com.maddyhome.idea.vim.group.HistoryGroup; import com.maddyhome.idea.vim.group.HistoryGroup;
import kotlin.text.StringsKt;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import javax.swing.*; import javax.swing.*;
import javax.swing.text.Document; import javax.swing.plaf.basic.BasicTextFieldUI;
import javax.swing.text.Keymap; import javax.swing.text.*;
import java.awt.*; import java.awt.*;
import java.awt.event.FocusEvent; import java.awt.event.*;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.util.Date; import java.util.Date;
import java.util.List; 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 * Provides a custom keymap for the text field. The keymap is the VIM Ex command keymapping
*/ */
public class ExTextField extends JTextField { public class ExTextField extends JTextField {
ExTextField() { ExTextField() {
addFocusListener(new FocusListener() { CommandLineCaret caret = new CommandLineCaret();
@Override caret.setBlinkRate(getCaret().getBlinkRate());
public void focusGained(FocusEvent e) { setCaret(caret);
setCaretPosition(getText().length()); setNormalModeCaret();
}
addCaretListener(e -> resetCaret());
addMouseListener(new MouseAdapter() {
@Override @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. void reset() {
// (I.e. DarculaTextFieldUI#getDefaultMargins, MacIntelliJTextFieldUI#getDefaultMargin, WinIntelliJTextFieldUI#getDefaultMargin) clearCurrentAction();
// This is an attempt to mitigate the gap in ExEntryPanel between the label (':', '/', '?') and the text field. setInsertMode();
// See VIM-1485 }
void deactivate() {
clearCurrentAction();
}
@Override @Override
public Insets getMargin() { public Insets getMargin() {
return JBUI.emptyInsets(); 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 // Called when the LAF is changed, but only if the control is visible
@Override @Override
public void updateUI() { public void updateUI() {
super.updateUI(); // 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
Font font = EditorColorsManager.getInstance().getGlobalScheme().getFont(EditorFontType.PLAIN); // directly next to the label
setFont(font); setUI(new BasicTextFieldUI());
invalidate();
setBorder(null); setBorder(null);
@ -98,7 +113,7 @@ public class ExTextField extends JTextField {
setKeymap(map); setKeymap(map);
} }
public void setType(@NotNull String type) { void setType(@NotNull String type) {
String hkey = null; String hkey = null;
switch (type.charAt(0)) { switch (type.charAt(0)) {
case '/': case '/':
@ -116,11 +131,11 @@ public class ExTextField extends JTextField {
} }
} }
public void saveLastEntry() { void saveLastEntry() {
lastEntry = getText(); lastEntry = getText();
} }
public void selectHistory(boolean isUp, boolean filter) { void selectHistory(boolean isUp, boolean filter) {
int dir = isUp ? -1 : 1; int dir = isUp ? -1 : 1;
if (histIndex + dir < 0 || histIndex + dir > history.size()) { if (histIndex + dir < 0 || histIndex + dir > history.size()) {
VimPlugin.indicateError(); 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() { public Editor getEditor() {
return editor; return editor;
@ -186,21 +222,25 @@ public class ExTextField extends JTextField {
c = Character.toChars(codePoint)[0]; 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.processKeyEvent(event);
super.setText(string); }
}
public void setText(String string) {
super.setText(string);
saveLastEntry();
} }
protected void processKeyEvent(KeyEvent e) { protected void processKeyEvent(KeyEvent e) {
@ -220,182 +260,270 @@ public class ExTextField extends JTextField {
return new ExDocument(); return new ExDocument();
} }
public void escape() { /**
* Cancels current action, if there is one. If not, cancels entry.
*/
void escape() {
if (currentAction != null) { if (currentAction != null) {
currentAction = null; clearCurrentAction();
} }
else { 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; 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 @Nullable
public Action getCurrentAction() { Action getCurrentAction() {
return currentAction; return currentAction;
} }
public void toggleInsertReplace() { private void setInsertMode() {
ExDocument doc = (ExDocument)getDocument();
if (doc.isOverwrite()) {
doc.toggleInsertReplace();
}
resetCaret();
}
void toggleInsertReplace() {
ExDocument doc = (ExDocument)getDocument(); ExDocument doc = (ExDocument)getDocument();
doc.toggleInsertReplace(); doc.toggleInsertReplace();
resetCaret();
/*
Caret caret;
int width;
if (doc.isOverwrite())
{
caret = blockCaret;
width = 8;
}
else
{
caret = origCaret;
width = 1;
}
setCaret(caret);
putClientProperty("caretWidth", new Integer(width));
*/
} }
/* private void resetCaret() {
private static class BlockCaret extends DefaultCaret if (getCaretPosition() == getText().length() || currentActionPromptCharacterOffset == getText().length() - 1) {
{ setNormalModeCaret();
public void paint(Graphics g) }
{ else {
if(!isVisible()) ExDocument doc = (ExDocument)getDocument();
return; if (doc.isOverwrite()) {
setReplaceModeCaret();
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
}
} }
else {
private boolean _contains(int i, int j, int k, int l) setInsertModeCaret();
{
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;
} }
}
}
// 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 Editor editor;
private DataContext context; private DataContext context;
private String lastEntry; private String lastEntry;
private List<HistoryGroup.HistoryEntry> history; private List<HistoryGroup.HistoryEntry> history;
private int histIndex = 0; private int histIndex = 0;
@Nullable private Action currentAction; @Nullable private ExEditorKit.MultiStepAction currentAction;
// TODO - support block cursor for overwrite mode private char currentActionPromptCharacter;
//private Caret origCaret; private int currentActionPromptCharacterOffset = -1;
//private Caret blockCaret;
private static final String vimExTextFieldDisposeKey = "vimExTextFieldDisposeKey";
private static final Logger logger = Logger.getInstance(ExTextField.class.getName()); 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);
}
}
} }

View File

@ -79,6 +79,9 @@ public abstract class VimTestCase extends UsefulTestCase {
KeyHandler.getInstance().fullReset(myFixture.getEditor()); KeyHandler.getInstance().fullReset(myFixture.getEditor());
Options.getInstance().resetAllOptions(); Options.getInstance().resetAllOptions();
VimPlugin.getKey().resetKeyMappings(); 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() { protected String getTestDataPath() {

View File

@ -158,41 +158,6 @@ public class MotionActionTest extends VimTestCase {
myFixture.checkResult(" 0:<caret>_ 1:a 2:b 3:c \n"); 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| // VIM-314 |d| |v_iB|
public void testDeleteInnerCurlyBraceBlock() { public void testDeleteInnerCurlyBraceBlock() {
typeTextInFile(parseKeys("di{"), typeTextInFile(parseKeys("di{"),
@ -226,30 +191,6 @@ public class MotionActionTest extends VimTestCase {
assertOffset(6); 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| // |d| |v_aw|
public void testDeleteOuterWord() { public void testDeleteOuterWord() {
typeTextInFile(parseKeys("daw"), typeTextInFile(parseKeys("daw"),
@ -449,350 +390,6 @@ public class MotionActionTest extends VimTestCase {
myFixture.checkResult("foo = ;\n"); 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 // VIM-1427
public void testDeleteOuterTagWithCount() { public void testDeleteOuterTagWithCount() {
typeTextInFile(parseKeys("d2at"),"<a><b><c><caret></c></b></a>"); typeTextInFile(parseKeys("d2at"),"<a><b><c><caret></c></b></a>");

View File

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

View File

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

View File

@ -16,26 +16,23 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * 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.maddyhome.idea.vim.command.CommandState
import com.intellij.openapi.editor.Editor; import com.maddyhome.idea.vim.helper.StringHelper.parseKeys
import com.intellij.openapi.editor.actionSystem.EditorAction; import org.jetbrains.plugins.ideavim.VimTestCase
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;
/** class InsertDeletePreviousWordActionTest : VimTestCase() {
*/ // VIM-1655
public class CancelExEntryAction extends EditorAction { fun `test deleted word is not yanked`() {
public CancelExEntryAction() { doTest(parseKeys("yiw", "3wea", "<C-W>", "<ESC>p"), """
super(new Handler()); A Discovery
}
private static class Handler extends EditorActionHandlerBase { I found <caret>it in a legendary land
protected boolean execute(@NotNull Editor editor, @NotNull DataContext context, @NotNull Command cmd) { """.trimIndent(), """
return VimPlugin.getProcess().cancelExEntry(editor, context); A Discovery
I found it in a i<caret>t land
""".trimIndent(), CommandState.Mode.COMMAND, CommandState.SubMode.NONE)
} }
} }
}

View File

@ -22,7 +22,6 @@ package org.jetbrains.plugins.ideavim.action.motion.leftright
import com.maddyhome.idea.vim.command.CommandState import com.maddyhome.idea.vim.command.CommandState
import com.maddyhome.idea.vim.helper.StringHelper.parseKeys import com.maddyhome.idea.vim.helper.StringHelper.parseKeys
import com.maddyhome.idea.vim.helper.VimBehaviourDiffers
import org.jetbrains.plugins.ideavim.VimTestCase import org.jetbrains.plugins.ideavim.VimTestCase
class MotionRightActionTest : VimTestCase() { class MotionRightActionTest : VimTestCase() {
@ -80,14 +79,6 @@ class MotionRightActionTest : VimTestCase() {
""".trimIndent(), CommandState.Mode.COMMAND, CommandState.SubMode.NONE) """.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`() { fun `test simple motion non-ascii`() {
doTest(parseKeys("l"), """ doTest(parseKeys("l"), """
A Discovery A Discovery
@ -99,21 +90,13 @@ class MotionRightActionTest : VimTestCase() {
""".trimIndent(), """ """.trimIndent(), """
A Discovery 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, all rocks and lavender and tufted grass,
where it was settled on some sodden sand where it was settled on some sodden sand
hard by the torrent of a mountain pass. hard by the torrent of a mountain pass.
""".trimIndent(), CommandState.Mode.COMMAND, CommandState.SubMode.NONE) """.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`() { fun `test simple motion emoji`() {
doTest(parseKeys("l"), """ doTest(parseKeys("l"), """
A Discovery A Discovery
@ -125,7 +108,25 @@ class MotionRightActionTest : VimTestCase() {
""".trimIndent(), """ """.trimIndent(), """
A Discovery 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, all rocks and lavender and tufted grass,
where it was settled on some sodden sand where it was settled on some sodden sand
hard by the torrent of a mountain pass. hard by the torrent of a mountain pass.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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