1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2025-07-26 17:59:02 +02:00

Add prompt when inserting digraphs and registers

Also resets state correctly when cancelling inserts
This commit is contained in:
Matt Ellis 2019-04-29 14:12:19 +01:00
parent 906d2a4168
commit 8601730dd8
No known key found for this signature in database
GPG Key ID: FA6025D54131324B
6 changed files with 202 additions and 56 deletions
src/com/maddyhome/idea/vim
test/org/jetbrains/plugins/ideavim/ex

View File

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

View File

@ -157,6 +157,10 @@ public class ExEditorKit extends DefaultEditorKit {
}
}
public interface MultiStepAction extends Action {
void reset();
}
public static class HistoryUpAction extends TextAction {
HistoryUpAction() {
super(HistoryUp);
@ -201,7 +205,7 @@ public class ExEditorKit extends DefaultEditorKit {
}
}
public static class InsertRegisterAction extends TextAction {
public static class InsertRegisterAction extends TextAction implements MultiStepAction {
private enum State {
SKIP_CTRL_R,
WAIT_REGISTER,
@ -220,11 +224,11 @@ public class ExEditorKit extends DefaultEditorKit {
switch (state) {
case SKIP_CTRL_R:
state = State.WAIT_REGISTER;
target.setCurrentAction(this);
target.setCurrentAction(this, '\"');
break;
case WAIT_REGISTER:
state = State.SKIP_CTRL_R;
target.setCurrentAction(null);
target.clearCurrentAction();
final char c = key.getKeyChar();
if (c != KeyEvent.CHAR_UNDEFINED) {
final Register register = VimPlugin.getRegister().getRegister(c);
@ -244,6 +248,11 @@ public class ExEditorKit extends DefaultEditorKit {
}
}
}
@Override
public void reset() {
state = State.SKIP_CTRL_R;
}
}
public static class CompleteEntryAction extends TextAction {
@ -268,9 +277,8 @@ public class ExEditorKit extends DefaultEditorKit {
}
public void actionPerformed(ActionEvent e) {
VimPlugin.getProcess().cancelExEntry(
ExEntryPanel.getInstance().getEntry().getEditor(),
ExEntryPanel.getInstance().getEntry().getContext());
ExTextField target = (ExTextField)getTextComponent(e);
target.cancel();
}
}
@ -322,9 +330,7 @@ public class ExEditorKit extends DefaultEditorKit {
doc.remove(dot - delChars, delChars);
}
else {
VimPlugin.getProcess().cancelExEntry(
ExEntryPanel.getInstance().getEntry().getEditor(),
ExEntryPanel.getInstance().getEntry().getContext());
target.cancel();
}
}
catch (BadLocationException bl) {
@ -423,7 +429,7 @@ public class ExEditorKit extends DefaultEditorKit {
}
}
public static class StartDigraphAction extends TextAction {
public static class StartDigraphAction extends TextAction implements MultiStepAction {
@Nullable private DigraphSequence digraphSequence;
StartDigraphAction() {
@ -436,14 +442,17 @@ public class ExEditorKit extends DefaultEditorKit {
if (key != null && digraphSequence != null) {
DigraphSequence.DigraphResult res = digraphSequence.processKey(key, target.getEditor());
switch (res.getResult()) {
case DigraphSequence.DigraphResult.RES_OK:
target.setCurrentActionPromptCharacter(res.getPromptCharacter());
break;
case DigraphSequence.DigraphResult.RES_BAD:
target.setCurrentAction(null);
target.clearCurrentAction();
target.handleKey(key);
break;
case DigraphSequence.DigraphResult.RES_DONE:
final KeyStroke digraph = res.getStroke();
digraphSequence = null;
target.setCurrentAction(null);
target.clearCurrentAction();
if (digraph != null) {
target.handleKey(digraph);
}
@ -451,11 +460,16 @@ public class ExEditorKit extends DefaultEditorKit {
}
}
else if (key != null && DigraphSequence.isDigraphStart(key)) {
target.setCurrentAction(this);
digraphSequence = new DigraphSequence();
digraphSequence.processKey(key, target.getEditor());
DigraphSequence.DigraphResult res = digraphSequence.processKey(key, target.getEditor());
target.setCurrentAction(this, res.getPromptCharacter());
}
}
@Override
public void reset() {
digraphSequence = null;
}
}
private static ExEditorKit instance;

View File

@ -111,10 +111,10 @@ public class ExEntryPanel extends JPanel implements LafManagerListener {
this.label.setText(label);
this.count = count;
setFontForElements();
entry.reset();
entry.setEditor(editor, context);
entry.setText(initText);
entry.setType(label);
entry.setInsertMode();
parent = editor.getContentComponent();
if (!ApplicationManager.getApplication().isUnitTestMode()) {
JRootPane root = SwingUtilities.getRootPane(parent);
@ -232,6 +232,8 @@ public class ExEntryPanel extends JPanel implements LafManagerListener {
logger.info("deactivate");
if (!active) return;
active = false;
entry.deactivate();
if (!ApplicationManager.getApplication().isUnitTestMode()) {
if (refocusOwningEditor && parent != null) {
UiHelper.requestFocus(parent);

View File

@ -33,8 +33,11 @@ public class ExKeyBindings {
// TODO - add the following keys:
// Ctrl-\ Ctrl-N - abort
static final KeyBinding[] bindings = new KeyBinding[]{
// Note that escape will cancel a pending insert digraph/register before cancelling
new KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), ExEditorKit.EscapeChar),
new KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_OPEN_BRACKET, KeyEvent.CTRL_MASK), ExEditorKit.EscapeChar),
// Cancel immediately cancels entry
new KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_C, KeyEvent.CTRL_MASK), ExEditorKit.CancelEntry),
new KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), ExEditorKit.CompleteEntry),

View File

@ -26,6 +26,7 @@ import com.intellij.openapi.util.Disposer;
import com.intellij.util.ui.JBUI;
import com.maddyhome.idea.vim.VimPlugin;
import com.maddyhome.idea.vim.group.HistoryGroup;
import kotlin.text.StringsKt;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
@ -33,13 +34,12 @@ import org.jetbrains.annotations.TestOnly;
import javax.swing.*;
import javax.swing.text.*;
import java.awt.*;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.KeyEvent;
import java.awt.event.*;
import java.util.Date;
import java.util.List;
import static java.lang.Math.max;
import static java.lang.Math.min;
/**
* Provides a custom keymap for the text field. The keymap is the VIM Ex command keymapping
@ -62,6 +62,15 @@ public class ExTextField extends JTextField {
});
}
void reset() {
clearCurrentAction();
setInsertMode();
}
void deactivate() {
clearCurrentAction();
}
// Minimize margins and insets. These get added to the default margins in the UI class that we can't override.
// (I.e. DarculaTextFieldUI#getDefaultMargins, MacIntelliJTextFieldUI#getDefaultMargin, WinIntelliJTextFieldUI#getDefaultMargin)
// This is an attempt to mitigate the gap in ExEntryPanel between the label (':', '/', '?') and the text field.
@ -231,17 +240,62 @@ public class ExTextField extends JTextField {
return new ExDocument();
}
public void escape() {
/**
* Cancels current action, if there is one. If not, cancels entry.
*/
void escape() {
if (currentAction != null) {
currentAction = null;
clearCurrentAction();
}
else {
VimPlugin.getProcess().cancelExEntry(editor, context);
cancel();
}
}
void setCurrentAction(@Nullable Action action) {
/**
* Cancels entry, including any current action.
*/
void cancel() {
clearCurrentAction();
VimPlugin.getProcess().cancelExEntry(editor, context);
}
void setCurrentAction(@NotNull ExEditorKit.MultiStepAction action, char pendingIndicator) {
this.currentAction = action;
setCurrentActionPromptCharacter(pendingIndicator);
}
void clearCurrentAction() {
if (currentAction != null) {
currentAction.reset();
}
currentAction = null;
clearCurrentActionPromptCharacter();
}
void setCurrentActionPromptCharacter(char promptCharacter) {
final String text = removePromptCharacter();
this.currentActionPromptCharacter = promptCharacter;
currentActionPromptCharacterOffset = currentActionPromptCharacterOffset == -1 ? getCaretPosition() : currentActionPromptCharacterOffset;
StringBuilder sb = new StringBuilder(text);
sb.insert(currentActionPromptCharacterOffset, currentActionPromptCharacter);
updateText(sb.toString());
setCaretPosition(currentActionPromptCharacterOffset);
}
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
@ -264,7 +318,7 @@ public class ExTextField extends JTextField {
}
private void resetCaret() {
if (getCaretPosition() == getText().length()) {
if (getCaretPosition() == getText().length() || currentActionPromptCharacterOffset == getText().length() - 1) {
setNormalModeCaret();
}
else {
@ -315,7 +369,6 @@ public class ExTextField extends JTextField {
}
void setMode(CaretMode mode, int blockPercentage) {
// Make sure damage gets updated for the old and new shape so the flashing works correctly
updateDamage();
this.mode = mode;
@ -403,7 +456,9 @@ public class ExTextField extends JTextField {
private String lastEntry;
private List<HistoryGroup.HistoryEntry> history;
private int histIndex = 0;
@Nullable private Action currentAction;
@Nullable private ExEditorKit.MultiStepAction currentAction;
private char currentActionPromptCharacter;
private int currentActionPromptCharacterOffset = -1;
private static final String vimExTextFieldDisposeKey = "vimExTextFieldDisposeKey";
private static final Logger logger = Logger.getInstance(ExTextField.class.getName());

View File

@ -32,7 +32,6 @@ class ExEntryTest: VimTestCase() {
deactivateExEntry()
// TODO: This has a different implementation, which is correct? What are the side effects?
assertFalse(options.isSet(Options.INCREMENTAL_SEARCH))
typeExInput(":set incsearch<C-C>")
assertFalse(options.isSet(Options.INCREMENTAL_SEARCH))
@ -65,19 +64,25 @@ class ExEntryTest: VimTestCase() {
}
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)
typeText("<Home>")
deactivateExEntry()
typeExInput(":set<Home>")
assertEquals("VER 25", exEntryPanel.entry.caretShape)
typeText("<Insert>")
deactivateExEntry()
typeExInput(":set<Home><Insert>")
assertEquals("HOR 20", exEntryPanel.entry.caretShape)
typeText("<Insert>")
deactivateExEntry()
typeExInput(":set<Home><Insert><Insert>")
assertEquals("VER 25", exEntryPanel.entry.caretShape)
}
@ -349,24 +354,64 @@ class ExEntryTest: VimTestCase() {
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)
}
// TODO: Test caret feedback
// Vim shows "?" as the char under the caret after <C-K>, then echoes the first char of the digraph
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 enter literal character by code`() {
fun `test insert literal character`() {
typeExInput(":<C-V>123<C-V>080")
assertExText("{P")
@ -394,21 +439,36 @@ class ExEntryTest: VimTestCase() {
typeExInput(":<C-Q>u00a9")
assertExText("©")
deactivateExEntry()
typeExInput(":set<Home><C-V>u00A9")
assertExText("©set")
assertExOffset(1)
}
fun `test escape cancels digraph`() {
typeExInput(":<C-K><Esc>OK")
assertIsActive()
assertExText("OK")
fun `test prompt while inserting literal character`() {
typeExInput(":<C-V>")
assertExText("^")
assertExOffset(0)
// TODO: The Vim docs states Esc exits command line context, but Vim actually cancels digraph context
// deactivateExEntry()
//
// typeExInput(":<C-K>O<Esc>K")
// assertTrue(exEntryPanel.isActive)
// assertEquals("K", exEntryPanel.text)
//
// deactivateExEntry()
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`() {
@ -491,7 +551,7 @@ class ExEntryTest: VimTestCase() {
private fun deactivateExEntry() {
// We don't need to reset text, that's handled by #active
if (exEntryPanel.isActive)
typeText("<Esc>")
typeText("<C-C>")
}
private fun assertExText(expected: String) {