// Copyright 2012 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.collide.client.editor.input;
import com.google.collide.client.editor.search.SearchModel;
import com.google.collide.client.editor.selection.LocalCursorController;
import com.google.collide.client.editor.selection.SelectionModel;
import com.google.collide.client.editor.selection.SelectionModel.MoveAction;
import com.google.collide.client.util.Elements;
import com.google.collide.client.util.input.CharCodeWithModifiers;
import com.google.collide.client.util.input.KeyCodeMap;
import com.google.collide.client.util.input.ModifierKeys;
import com.google.collide.shared.document.Document;
import com.google.collide.shared.document.Line;
import com.google.collide.shared.document.LineInfo;
import com.google.collide.shared.document.Position;
import com.google.collide.shared.document.util.LineUtils;
import com.google.collide.shared.document.util.PositionUtils;
import com.google.collide.shared.util.ScopeMatcher;
import com.google.collide.shared.util.StringUtils;
import com.google.collide.shared.util.TextUtils;
import com.google.common.base.Preconditions;
import org.waveprotocol.wave.client.common.util.JsoIntMap;
import org.waveprotocol.wave.client.common.util.SignalEvent;
import org.waveprotocol.wave.client.common.util.SignalEvent.MoveUnit;
import elemental.css.CSSStyleDeclaration;
import elemental.html.Element;
/**
* Basic Vi(m) keybinding support. This is limited to single file operations.
* This includes the main Vim modes for Command, Visual, Insert, and single-line
* search/command entry.
*/
public class VimScheme extends InputScheme {
private static final JsoIntMap<MoveAction> CHAR_MOVEMENT_KEYS_MAPPING = JsoIntMap.create();
private static final JsoIntMap<MoveAction> LINE_MOVEMENT_KEYS_MAPPING = JsoIntMap.create();
/**
* Command mode mirrors Vim's command mode for performing commands from single
* keypresses.
*/
InputMode commandMode;
/**
* Insert mode can only insert text or escape back to command mode.
*/
InputMode insertMode;
/**
* Command capture mode gets switched to from command mode when a ":" is typed
* until the enter key is pressed, and the resulting text between : and
* <enter> is used as the argument to {@link #doColonCommand(StringBuilder)}.
*/
InputMode commandCaptureMode;
/**
* Search capture mode gets switched to from command mode when a "/" is typed
* and will highlight whatever text is typed in the current document until
* escape or enter are pressed.
*/
InputMode searchCaptureMode;
Element statusHeader;
boolean inVisualMode;
MoveUnit visualMoveUnit;
/** Any numbers typed before a command shortcut. */
private StringBuilder numericPrefixText = new StringBuilder();
private StringBuilder command;
private StringBuilder searchTerm;
private String clipboard;
private boolean isLineCopy;
private static enum Modes {
COMMAND, INSERT, COMMAND_CAPTURE, SEARCH_CAPTURE
}
// The order of characters here needs to match
private static final String OPENING_GROUPS = "{[(";
private static final String CLOSING_GROUPS = "}])";
static {
CHAR_MOVEMENT_KEYS_MAPPING.put(
CharCodeWithModifiers.computeKeyDigest(ModifierKeys.NONE, 'h'), MoveAction.LEFT);
CHAR_MOVEMENT_KEYS_MAPPING.put(
CharCodeWithModifiers.computeKeyDigest(ModifierKeys.NONE, 'j'), MoveAction.DOWN);
CHAR_MOVEMENT_KEYS_MAPPING.put(
CharCodeWithModifiers.computeKeyDigest(ModifierKeys.NONE, 'k'), MoveAction.UP);
CHAR_MOVEMENT_KEYS_MAPPING.put(
CharCodeWithModifiers.computeKeyDigest(ModifierKeys.NONE, 'l'), MoveAction.RIGHT);
LINE_MOVEMENT_KEYS_MAPPING.put(
CharCodeWithModifiers.computeKeyDigest(ModifierKeys.NONE, 'h'), MoveAction.LINE_START);
LINE_MOVEMENT_KEYS_MAPPING.put(
CharCodeWithModifiers.computeKeyDigest(ModifierKeys.NONE, 'j'), MoveAction.DOWN);
LINE_MOVEMENT_KEYS_MAPPING.put(
CharCodeWithModifiers.computeKeyDigest(ModifierKeys.NONE, 'k'), MoveAction.UP);
LINE_MOVEMENT_KEYS_MAPPING.put(
CharCodeWithModifiers.computeKeyDigest(ModifierKeys.NONE, 'l'), MoveAction.LINE_END);
}
/**
* TODO: Refactor shortcut system to separate activation key
* definition (the "Shortcut") from the actual shortcut function (the
* "Action") as some shortcuts have duplicate actions.
*/
public VimScheme(InputController inputController) {
super(inputController);
commandMode = new InputMode() {
@Override
public void setup() {
setStatus("-- COMMAND --");
inVisualMode = false;
numericPrefixText.setLength(0);
LocalCursorController cursor = getInputController().getEditor().getCursorController();
cursor.setBlockMode(true);
cursor.setColor("blue");
getInputController().getEditor().getSelection().deselect();
}
@Override
public void teardown() {
getInputController().getEditor().getCursorController().resetCursorView();
}
@Override
public boolean onDefaultInput(SignalEvent signal, char character) {
// navigate here
int letter = KeyCodeMap.getKeyFromEvent(signal);
int modifiers = ModifierKeys.computeModifiers(signal);
modifiers = modifiers & ~ModifierKeys.SHIFT;
int strippedKeyDigest = CharCodeWithModifiers.computeKeyDigest(modifiers, letter);
JsoIntMap<MoveAction> movementKeysMapping = getMovementKeysMapping();
if (movementKeysMapping.containsKey(strippedKeyDigest)) {
MoveAction action = movementKeysMapping.get(strippedKeyDigest);
getScheme().getInputController().getSelection().move(action, inVisualMode);
return true;
}
if (tryAddNumericPrefix((char) letter)) {
return true;
}
numericPrefixText.setLength(0);
return false;
}
/**
* Paste events. These can come in from native Command+V or forced via
* holding a local clipboard/buffer of any text copied within the editor.
*/
@Override
public boolean onDefaultPaste(SignalEvent signal, String text) {
handlePaste(text, true);
return true;
}
};
addMode(Modes.COMMAND, commandMode);
/*
* Command+Alt+V - Switch from Vim to Default Scheme
*/
commandMode.addShortcut(new EventShortcut(ModifierKeys.ACTION | ModifierKeys.ALT, 'v') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
getInputController().setActiveInputScheme(getInputController().nativeScheme);
return true;
}
});
/*
* ESC, ACTION+[ - reset state in command mode
*/
commandMode.addShortcut(new EventShortcut(ModifierKeys.NONE, KeyCodeMap.ESC) {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
switchMode(Modes.COMMAND);
return true;
}
});
commandMode.addShortcut(new EventShortcut(ModifierKeys.ACTION, '[') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
switchMode(Modes.COMMAND);
return true;
}
});
/*
* TODO: extract common visual mode switching code.
*/
/*
* v - Switch to visual mode (character)
*/
commandMode.addShortcut(new EventShortcut(ModifierKeys.NONE, 'v') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
setStatus("-- VISUAL (char) --");
inVisualMode = true;
visualMoveUnit = MoveUnit.CHARACTER;
// select the character the cursor is over now
SelectionModel selectionModel = getInputController().getEditor().getSelection();
LineInfo cursorLineInfo =
new LineInfo(selectionModel.getCursorLine(), selectionModel.getCursorLineNumber());
int cursorColumn = selectionModel.getCursorColumn();
selectionModel.setSelection(cursorLineInfo, cursorColumn, cursorLineInfo, cursorColumn + 1);
return true;
}
});
/*
* V - Switch to visual mode (line)
*/
/*
* TODO: Doesn't exactly match vim's visual-line mode, force
* selections of entire lines.
*/
commandMode.addShortcut(new EventShortcut(ModifierKeys.NONE, 'V') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
setStatus("-- VISUAL (line) --");
inVisualMode = true;
visualMoveUnit = MoveUnit.LINE;
// move cursor to beginning of current line, select to column 0 of next
// line
SelectionModel selectionModel = getInputController().getEditor().getSelection();
LineInfo cursorLineInfo =
new LineInfo(selectionModel.getCursorLine(), selectionModel.getCursorLineNumber());
LineInfo nextLineInfo =
new LineInfo(cursorLineInfo.line().getNextLine(), cursorLineInfo.number() + 1);
selectionModel.setSelection(cursorLineInfo, 0, nextLineInfo, 0);
return true;
}
});
/*
* i - Switch to insert mode
*/
commandMode.addShortcut(new EventShortcut(ModifierKeys.NONE, 'i') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
switchMode(Modes.INSERT);
return true;
}
});
/*
* A - Jump to end of line, enter insert mode.
*/
commandMode.addShortcut(new EventShortcut(ModifierKeys.NONE, 'A') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
SelectionModel selectionModel = getInputController().getEditor().getSelection();
LineInfo cursorLineInfo =
new LineInfo(selectionModel.getCursorLine(), selectionModel.getCursorLineNumber());
int lastColumn = LineUtils.getLastCursorColumn(cursorLineInfo.line());
selectionModel.setCursorPosition(cursorLineInfo, lastColumn);
switchMode(Modes.INSERT);
return true;
}
});
/*
* O - Insert line above, enter insert mode.
*/
commandMode.addShortcut(new EventShortcut(ModifierKeys.NONE, 'O') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
SelectionModel selectionModel = getInputController().getEditor().getSelection();
Document document = getInputController().getEditor().getDocument();
Line cursorLine = selectionModel.getCursorLine();
int cursorLineNumber = selectionModel.getCursorLineNumber();
document.insertText(cursorLine, 0, "\n");
selectionModel.setCursorPosition(new LineInfo(cursorLine, cursorLineNumber), 0);
switchMode(Modes.INSERT);
return true;
}
});
/*
* o - Insert line below, enter insert mode.
*/
commandMode.addShortcut(new EventShortcut(ModifierKeys.NONE, 'o') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
SelectionModel selectionModel = getInputController().getEditor().getSelection();
Document document = getInputController().getEditor().getDocument();
Line cursorLine = selectionModel.getCursorLine();
int cursorLineNumber = selectionModel.getCursorLineNumber();
document.insertText(cursorLine, LineUtils.getLastCursorColumn(cursorLine), "\n");
selectionModel.setCursorPosition(new LineInfo(cursorLine.getNextLine(),
cursorLineNumber + 1), 0);
switchMode(Modes.INSERT);
return true;
}
});
/*
* : - Switch to colon capture mode for commands.
*/
commandMode.addShortcut(new EventShortcut(ModifierKeys.NONE, ':') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
switchMode(Modes.COMMAND_CAPTURE);
return true;
}
});
/*
* "/" - Switch to search mode.
*/
commandMode.addShortcut(new EventShortcut(ModifierKeys.NONE, '/') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
switchMode(Modes.SEARCH_CAPTURE);
return true;
}
});
commandMode.addShortcut(new EventShortcut(ModifierKeys.NONE, '*') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
SelectionModel selectionModel = getInputController().getEditor().getSelection();
String word =
TextUtils.getWordAtColumn(selectionModel.getCursorLine().getText(),
selectionModel.getCursorColumn());
if (word == null) {
return true;
}
switchMode(Modes.SEARCH_CAPTURE);
searchTerm.append(word);
doPartialSearch();
drawSearchTerm();
return true;
}
});
/*
* Movement
*/
/*
* ^,0 - Move to first character in line.
*/
commandMode.addShortcut(new EventShortcut(ModifierKeys.NONE, '^') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
SelectionModel selectionModel = getInputController().getEditor().getSelection();
LineInfo cursorLineInfo =
new LineInfo(selectionModel.getCursorLine(), selectionModel.getCursorLineNumber());
selectionModel.setCursorPosition(cursorLineInfo, 0);
return true;
}
});
commandMode.addShortcut(new EventShortcut(ModifierKeys.NONE, '0') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
if (tryAddNumericPrefix('0')) {
return true;
}
SelectionModel selectionModel = getInputController().getEditor().getSelection();
LineInfo cursorLineInfo =
new LineInfo(selectionModel.getCursorLine(), selectionModel.getCursorLineNumber());
selectionModel.setCursorPosition(cursorLineInfo, 0);
return true;
}
});
/*
* $ - Move to end of line.
*/
commandMode.addShortcut(new EventShortcut(ModifierKeys.NONE, '$') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
SelectionModel selectionModel = getInputController().getEditor().getSelection();
Line cursorLine = selectionModel.getCursorLine();
LineInfo cursorLineInfo = new LineInfo(cursorLine, selectionModel.getCursorLineNumber());
selectionModel.setCursorPosition(cursorLineInfo, LineUtils.getLastCursorColumn(cursorLine));
return true;
}
});
/*
* w - move the cursor to the first character of the next word.
*/
commandMode.addShortcut(new EventShortcut(ModifierKeys.NONE, 'w') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
SelectionModel selectionModel = getInputController().getSelection();
LineInfo cursorLineInfo =
new LineInfo(selectionModel.getCursorLine(), selectionModel.getCursorLineNumber());
String text = selectionModel.getCursorLine().getText();
int column = selectionModel.getCursorColumn();
column = TextUtils.moveByWord(text, column, true, false);
if (column == -1) {
Line cursorLine = cursorLineInfo.line().getNextLine();
if (cursorLine != null) {
cursorLineInfo = new LineInfo(cursorLine, cursorLineInfo.number() + 1);
column = 0;
} else {
column = LineUtils.getLastCursorColumn(cursorLine); // at last character
// in document
}
}
selectionModel.setCursorPosition(cursorLineInfo, column);
return true;
}
});
/*
* b - move the cursor to the first character of the previous word.
*/
commandMode.addShortcut(new EventShortcut(ModifierKeys.NONE, 'b') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
SelectionModel selectionModel = getInputController().getSelection();
LineInfo cursorLineInfo =
new LineInfo(selectionModel.getCursorLine(), selectionModel.getCursorLineNumber());
String text = selectionModel.getCursorLine().getText();
int column = selectionModel.getCursorColumn();
column = TextUtils.moveByWord(text, column, false, false);
if (column == -1) {
Line cursorLine = cursorLineInfo.line().getPreviousLine();
if (cursorLine != null) {
cursorLineInfo = new LineInfo(cursorLine, cursorLineInfo.number() - 1);
column = LineUtils.getLastCursorColumn(cursorLine);
} else {
column = 0; // at first character in document
}
}
selectionModel.setCursorPosition(cursorLineInfo, column);
return true;
}
});
/*
* e - move the cursor to the last character of the next word.
*/
commandMode.addShortcut(new EventShortcut(ModifierKeys.NONE, 'e') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
SelectionModel selectionModel = getInputController().getSelection();
LineInfo cursorLineInfo =
new LineInfo(selectionModel.getCursorLine(), selectionModel.getCursorLineNumber());
String text = selectionModel.getCursorLine().getText();
int column = selectionModel.getCursorColumn();
column = TextUtils.moveByWord(text, column, true, true);
if (column == -1) {
Line cursorLine = cursorLineInfo.line().getNextLine();
if (cursorLine != null) {
cursorLineInfo = new LineInfo(cursorLine, cursorLineInfo.number() + 1);
column = 0;
} else {
// at the last character in the document
column = LineUtils.getLastCursorColumn(cursorLine);
}
}
selectionModel.setCursorPosition(cursorLineInfo, column);
return true;
}
});
/*
* % - jump to the next matching {}, [] or () character if the cursor is
* over one of the two.
*/
commandMode.addShortcut(new EventShortcut(ModifierKeys.NONE, '%') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
final SelectionModel selectionModel = getInputController().getSelection();
Document document = getInputController().getEditor().getDocument();
LineInfo cursorLineInfo =
new LineInfo(selectionModel.getCursorLine(), selectionModel.getCursorLineNumber());
String text = selectionModel.getCursorLine().getText();
final char cursorChar = text.charAt(selectionModel.getCursorColumn());
final char searchChar;
final boolean searchingForward = OPENING_GROUPS.indexOf(cursorChar) >= 0;
final Position searchingTo;
if (searchingForward) {
searchChar = CLOSING_GROUPS.charAt(OPENING_GROUPS.indexOf(cursorChar));
searchingTo =
new Position(new LineInfo(document.getLastLine(), document.getLastLineNumber()),
document.getLastLine().length());
} else if (CLOSING_GROUPS.indexOf(cursorChar) >= 0) {
searchChar = OPENING_GROUPS.charAt(CLOSING_GROUPS.indexOf(cursorChar));
searchingTo = new Position(new LineInfo(document.getFirstLine(), 0), 0);
} else {
return true; // not on a valid starting character
}
Position startingPosition = new Position(cursorLineInfo, selectionModel.getCursorColumn()
+ (searchingForward ? 0 : 1));
PositionUtils.visit(new LineUtils.LineVisitor() {
// keep a stack to match the correct corresponding bracket
ScopeMatcher scopeMatcher = new ScopeMatcher(searchingForward, cursorChar, searchChar);
@Override
public boolean accept(Line line, int lineNumber, int beginColumn, int endColumn) {
int column;
String text = line.getText().substring(beginColumn, endColumn);
column = scopeMatcher.searchNextLine(text);
if (column >= 0) {
selectionModel
.setCursorPosition(new LineInfo(line, lineNumber), column + beginColumn);
return false;
}
return true;
}
}, startingPosition, searchingTo);
return true;
}
});
/*
* } - next paragraph.
*/
commandMode.addShortcut(new EventShortcut(ModifierKeys.NONE, '}') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
SelectionModel selectionModel = getInputController().getSelection();
LineInfo cursorLineInfo =
new LineInfo(selectionModel.getCursorLine(), selectionModel.getCursorLineNumber());
int lineNumber = cursorLineInfo.number();
boolean skippingEmptyLines = true;
Line line;
for (line = cursorLineInfo.line(); line.getNextLine() != null; line = line.getNextLine(),
lineNumber++) {
String text = line.getText();
text = text.substring(0, text.length() - (text.endsWith("\n") ? 1 : 0));
boolean isEmptyLine = text.trim().length() > 0;
if (skippingEmptyLines) {
// check if this line is empty
if (isEmptyLine) {
skippingEmptyLines = false; // non-empty line
}
} else {
// check if this line is not empty
if (!isEmptyLine) {
break;
}
}
}
selectionModel.setCursorPosition(new LineInfo(line, lineNumber), 0);
return true;
}
});
/*
* TODO: merge both paragraph searching blocks together.
*/
/*
* { - previous paragraph.
*/
commandMode.addShortcut(new EventShortcut(ModifierKeys.NONE, '{') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
SelectionModel selectionModel = getInputController().getSelection();
LineInfo cursorLineInfo =
new LineInfo(selectionModel.getCursorLine(), selectionModel.getCursorLineNumber());
int lineNumber = cursorLineInfo.number();
boolean skippingEmptyLines = true;
Line line;
for (line = cursorLineInfo.line(); line.getPreviousLine() != null; line =
line.getPreviousLine(), lineNumber--) {
String text = line.getText();
text = text.substring(0, text.length() - (text.endsWith("\n") ? 1 : 0));
if (skippingEmptyLines) {
// check if this line is empty
if (text.trim().length() > 0) {
skippingEmptyLines = false; // non-empty line
}
} else {
// check if this line is not empty
if (text.trim().length() > 0) {
// not empty, continue
} else {
break;
}
}
}
selectionModel.setCursorPosition(new LineInfo(line, lineNumber), 0);
return true;
}
});
/*
* Cmd+u - page up.
*/
commandMode.addShortcut(new EventShortcut(ModifierKeys.ACTION, 'u') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
getInputController().getSelection().move(MoveAction.PAGE_UP, inVisualMode);
return true;
}
});
/*
* Cmd+d - page down.
*/
commandMode.addShortcut(new EventShortcut(ModifierKeys.ACTION, 'd') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
getInputController().getSelection().move(MoveAction.PAGE_DOWN, inVisualMode);
return true;
}
});
/*
* Ngg - move cursor to line N, or first line by default.
*/
commandMode.addShortcut(new StreamShortcut("gg") {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
moveCursorToLine(getPrefixValue(), true);
return true;
}
});
/*
* NG - move cursor to line N, or last line by default.
*/
commandMode.addShortcut(new EventShortcut(ModifierKeys.NONE, 'G') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
moveCursorToLine(getPrefixValue(), false);
return true;
}
});
/*
* Text manipulation
*/
/*
* x - Delete one character to right of cursor, or the current selection.
*/
commandMode.addShortcut(new EventShortcut(ModifierKeys.NONE, 'x') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
scheme.getInputController().deleteCharacter(true);
return true;
}
});
/*
* X - Delete one character to left of cursor.
*/
commandMode.addShortcut(new EventShortcut(ModifierKeys.NONE, 'X') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
scheme.getInputController().deleteCharacter(false);
return true;
}
});
/*
* p - Paste after cursor.
*/
commandMode.addShortcut(new EventShortcut(ModifierKeys.NONE, 'p') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
if (clipboard != null && clipboard.length() > 0) {
handlePaste(clipboard, true);
}
return true;
}
});
/*
* P - Paste before cursor.
*/
commandMode.addShortcut(new EventShortcut(ModifierKeys.NONE, 'P') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
if (clipboard != null && clipboard.length() > 0) {
handlePaste(clipboard, false);
}
return true;
}
});
/*
* Nyy - Copy N lines. If there is already a selection, copy that instead.
*/
commandMode.addShortcut(new StreamShortcut("yy") {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
SelectionModel selectionModel = getInputController().getEditor().getSelection();
if (selectionModel.hasSelection()) {
isLineCopy = (visualMoveUnit == MoveUnit.LINE);
} else {
int numLines = getPrefixValue();
if (numLines <= 0) {
numLines = 1;
}
selectNextNLines(numLines);
isLineCopy = false;
}
Preconditions.checkState(selectionModel.hasSelection());
getInputController().prepareForCopy();
Position[] selectionRange = selectionModel.getSelectionRange(true);
clipboard =
LineUtils.getText(selectionRange[0].getLine(), selectionRange[0].getColumn(),
selectionRange[1].getLine(), selectionRange[1].getColumn());
selectionModel.deselect();
switchMode(Modes.COMMAND);
return false;
}
});
/*
* Ndd - Cut N lines.
*/
commandMode.addShortcut(new StreamShortcut("dd") {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
int numLines = getPrefixValue();
if (numLines <= 0) {
numLines = 1;
}
SelectionModel selectionModel = getInputController().getEditor().getSelection();
selectNextNLines(numLines);
Preconditions.checkState(selectionModel.hasSelection());
getInputController().prepareForCopy();
Position[] selectionRange = selectionModel.getSelectionRange(true);
clipboard =
LineUtils.getText(selectionRange[0].getLine(), selectionRange[0].getColumn(),
selectionRange[1].getLine(), selectionRange[1].getColumn());
selectionModel.deleteSelection(getInputController().getEditorDocumentMutator());
return false;
}
});
/*
* >> - indent line.
*/
commandMode.addShortcut(new StreamShortcut(">>") {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
scheme.getInputController().indentSelection();
return true;
}
});
/*
* << - dedent line.
*/
commandMode.addShortcut(new StreamShortcut("<<") {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
scheme.getInputController().dedentSelection();
return true;
}
});
/*
* u - Undo.
*/
commandMode.addShortcut(new EventShortcut(ModifierKeys.NONE, 'u') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
scheme.getInputController().getEditor().undo();
return true;
}
});
/*
* ACTION+r - Redo.
*/
commandMode.addShortcut(new EventShortcut(ModifierKeys.ACTION, 'r') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
scheme.getInputController().getEditor().redo();
return true;
}
});
/**
* n - next search match
*/
commandMode.addShortcut(new EventShortcut(ModifierKeys.NONE, 'n') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
doSearch(true);
return true;
}
});
/**
* N - previous search match
*/
commandMode.addShortcut(new EventShortcut(ModifierKeys.NONE, 'N') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
doSearch(false);
return true;
}
});
insertMode = new InputMode() {
@Override
public void setup() {
setStatus("-- INSERT --");
}
@Override
public void teardown() {
}
@Override
public boolean onDefaultInput(SignalEvent signal, char character) {
int letter = KeyCodeMap.getKeyFromEvent(signal);
int modifiers = ModifierKeys.computeModifiers(signal);
modifiers = modifiers & ~ModifierKeys.SHIFT;
int strippedKeyDigest = CharCodeWithModifiers.computeKeyDigest(modifiers, letter);
JsoIntMap<MoveAction> movementKeysMapping = DefaultScheme.MOVEMENT_KEYS_MAPPING;
if (movementKeysMapping.containsKey(strippedKeyDigest)) {
MoveAction action = movementKeysMapping.get(strippedKeyDigest);
getScheme().getInputController().getSelection().move(action, false);
return true;
}
InputController input = getInputController();
SelectionModel selection = input.getSelection();
int column = selection.getCursorColumn();
if (!signal.getCommandKey() && KeyCodeMap.isPrintable(letter)) {
String text = Character.toString((char) letter);
// insert a single character
input.getEditorDocumentMutator().insertText(selection.getCursorLine(),
selection.getCursorLineNumber(), column, text);
return true;
}
return false; // let it fall through
}
@Override
public boolean onDefaultPaste(SignalEvent signal, String text) {
return false;
}
};
addMode(Modes.INSERT, insertMode);
/*
* ESC, ACTION+[ - Switch to command mode.
*/
insertMode.addShortcut(new EventShortcut(ModifierKeys.NONE, KeyCodeMap.ESC) {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
switchMode(Modes.COMMAND);
return true;
}
});
insertMode.addShortcut(new EventShortcut(ModifierKeys.ACTION, '[') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
switchMode(Modes.COMMAND);
return true;
}
});
/*
* BACKSPACE - Native behavior.
*/
insertMode.addShortcut(new EventShortcut(ModifierKeys.NONE, KeyCodeMap.BACKSPACE) {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
getInputController().deleteCharacter(false);
return true;
}
});
/*
* DELETE - Native behavior.
*/
insertMode.addShortcut(new EventShortcut(ModifierKeys.NONE, KeyCodeMap.DELETE) {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
getInputController().deleteCharacter(true);
return true;
}
});
/*
* TAB - Insert a tab.
*/
insertMode.addShortcut(new EventShortcut(ModifierKeys.NONE, KeyCodeMap.TAB) {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
getInputController().handleTab();
return true;
}
});
commandCaptureMode = new InputMode() {
@Override
public void setup() {
command = new StringBuilder();
setStatus("-- COMMAND CAPTURE --");
}
@Override
public void teardown() {
}
@Override
public boolean onDefaultInput(SignalEvent signal, char character) {
int letter = KeyCodeMap.getKeyFromEvent(signal);
if (KeyCodeMap.isPrintable(letter)) {
command.append(Character.toString((char) letter));
drawCommandTerm();
}
return false;
}
@Override
public boolean onDefaultPaste(SignalEvent signal, String text) {
return false;
}
};
addMode(Modes.COMMAND_CAPTURE, commandCaptureMode);
/*
* ESC, ACTION+[ - Exit on escape back to command mode.
*/
commandCaptureMode.addShortcut(new EventShortcut(ModifierKeys.NONE, KeyCodeMap.ESC) {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
switchMode(Modes.COMMAND);
return true;
}
});
commandCaptureMode.addShortcut(new EventShortcut(ModifierKeys.ACTION, '[') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
switchMode(Modes.COMMAND);
return true;
}
});
/*
* ENTER - Do the command, then exit to command mode.
*/
commandCaptureMode.addShortcut(new EventShortcut(ModifierKeys.NONE, KeyCodeMap.ENTER) {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
doColonCommand(command);
switchMode(Modes.COMMAND);
return true;
}
});
searchCaptureMode = new InputMode() {
@Override
public void setup() {
searchTerm = new StringBuilder();
setStatus("-- SEARCH --");
}
@Override
public void teardown() {
}
@Override
public boolean onDefaultInput(SignalEvent signal, char character) {
int letter = KeyCodeMap.getKeyFromEvent(signal);
if (KeyCodeMap.isPrintable(letter)) {
searchTerm.append(Character.toString((char) letter));
doPartialSearch();
drawSearchTerm();
}
return false;
}
@Override
public boolean onDefaultPaste(SignalEvent signal, String text) {
return false;
}
};
addMode(Modes.SEARCH_CAPTURE, searchCaptureMode);
/*
* ESC, ACTION+[ - Exit on escape back to command mode, and clear any
* highlights.
*/
searchCaptureMode.addShortcut(new EventShortcut(ModifierKeys.NONE, KeyCodeMap.ESC) {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
scheme.getInputController().getEditor().getSearchModel().setQuery("");
switchMode(Modes.COMMAND);
return true;
}
});
searchCaptureMode.addShortcut(new EventShortcut(ModifierKeys.ACTION, '[') {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
scheme.getInputController().getEditor().getSearchModel().setQuery("");
switchMode(Modes.COMMAND);
return true;
}
});
/*
* ENTER - Do the command, then exit to command mode.
*/
searchCaptureMode.addShortcut(new EventShortcut(ModifierKeys.NONE, KeyCodeMap.ENTER) {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
/*
* TODO: There is a bug when switching modes that erases the
* current selection, when we get to actually working on vim support I
* should track this down.
*/
switchMode(Modes.COMMAND);
return true;
}
});
/*
* BACKSPACE - Remove the last character from the searchTerm.
*/
searchCaptureMode.addShortcut(new EventShortcut(ModifierKeys.NONE, KeyCodeMap.BACKSPACE) {
@Override
public boolean event(InputScheme scheme, SignalEvent event) {
if (searchTerm.length() > 0) {
searchTerm.deleteCharAt(searchTerm.length() - 1);
doPartialSearch();
drawSearchTerm();
}
return true;
}
});
}
private JsoIntMap<MoveAction> getMovementKeysMapping() {
if (!inVisualMode) {
return DefaultScheme.MOVEMENT_KEYS_MAPPING;
}
if (visualMoveUnit == MoveUnit.LINE) {
return LINE_MOVEMENT_KEYS_MAPPING;
} else {
return CHAR_MOVEMENT_KEYS_MAPPING;
}
}
private void setStatus(String text) {
statusHeader.setTextContent(text);
}
private void drawSearchTerm() {
setStatus("Search: '" + searchTerm + "'");
}
private void drawCommandTerm() {
setStatus("Command: '" + command + "'");
}
private void doColonCommand(StringBuilder command) {
if (command.equals("q")) {
}
}
/**
* Display search matches for the currently entered searchTerm, but don't move
* the cursor.
*/
private void doPartialSearch() {
getInputController().getEditor().getSearchModel().setQuery(searchTerm.toString());
}
private void doSearch(boolean searchNext) {
SearchModel model = getInputController().getEditor().getSearchModel();
if (StringUtils.isNullOrEmpty(model.getQuery())) {
return;
}
if (searchNext) {
model.getMatchManager().selectNextMatch();
} else {
model.getMatchManager().selectPreviousMatch();
}
}
private void selectNextNLines(int numLines) {
SelectionModel selectionModel = getInputController().getEditor().getSelection();
Document document = getInputController().getEditor().getDocument();
Line cursorLine = selectionModel.getCursorLine();
int cursorLineNumber = selectionModel.getCursorLineNumber();
LineInfo cursorLineInfo = new LineInfo(cursorLine, cursorLineNumber);
LineInfo endLineInfo;
if (cursorLineNumber + numLines > document.getLastLineNumber()) {
endLineInfo = new LineInfo(document.getLastLine(), document.getLastLineNumber());
} else {
endLineInfo =
cursorLine.getDocument().getLineFinder()
.findLine(cursorLineInfo, cursorLineNumber + numLines);
}
selectionModel.setSelection(cursorLineInfo, 0, endLineInfo, 0);
}
/**
* Jump to lineNumber (1-based). If requested number is invalid, either go to
* the first line or the last line, depending upon defaultToFirstLine.
*/
private void moveCursorToLine(int lineNumber, boolean defaultToFirstLine) {
Document document = getInputController().getEditor().getDocument();
SelectionModel selectionModel = getInputController().getEditor().getSelection();
LineInfo targetLineInfo;
if (lineNumber > document.getLastLineNumber() + 1 || lineNumber <= 0) {
if (defaultToFirstLine) {
targetLineInfo = new LineInfo(document.getFirstLine(), 0);
} else {
targetLineInfo = new LineInfo(document.getLastLine(), document.getLastLineNumber());
}
} else {
Line cursorLine = selectionModel.getCursorLine();
int cursorLineNumber = selectionModel.getCursorLineNumber();
LineInfo cursorLineInfo = new LineInfo(cursorLine, cursorLineNumber);
targetLineInfo =
cursorLine.getDocument().getLineFinder().findLine(cursorLineInfo, lineNumber - 1);
}
selectionModel.setCursorPosition(targetLineInfo, 0);
}
/**
* Return the prefix value, or -1 if there is no valid prefix.
*/
private int getPrefixValue() {
if (numericPrefixText.length() == 0) {
return -1;
}
return Integer.parseInt(numericPrefixText.toString());
}
/**
* Try to append character to the current numeric prefix if it is a number and
* not a leading 0.
*
* @param character
* @return True if the character was added.
*/
private boolean tryAddNumericPrefix(char character) {
// if this is a number, keep track of it to do Ndd-type commands
if (('0' < character && character <= '9') || (character == '0'
&& numericPrefixText.length() > 0)) {
numericPrefixText.append(character);
return true;
}
return false;
}
private void switchMode(Modes newMode) {
super.switchMode(newMode.ordinal());
}
private void addMode(Modes modeId, InputMode mode) {
super.addMode(modeId.ordinal(), mode);
}
private void handlePaste(String text, boolean isPasteAfter) {
SelectionModel selection = getInputController().getSelection();
int lineNumber = selection.getCursorLineNumber();
Line line = selection.getCursorLine();
if (isLineCopy) {
// multi-line paste, insert on new line above or below
if (!isPasteAfter) {
// insert text before the cursor line
getInputController().getEditorDocumentMutator().insertText(line, lineNumber, 0, text);
} else {
// insert at end of current line (before \n)
text = "\n" + text.substring(0, text.length() - (text.endsWith("\n") ? 1 : 0));
getInputController().getEditorDocumentMutator().insertText(line, lineNumber,
LineUtils.getLastCursorColumn(line), text);
}
} else {
// not a full-line paste, act normally
getInputController().getEditorDocumentMutator().insertText(line, lineNumber,
selection.getCursorColumn(), text);
}
}
@Override
public void setup() {
/*
* TODO: create some sort of mode status display area for
* current mode and stream input.
*
* TODO: Long term move this display area into the awesome bar.
*/
statusHeader = Elements.createDivElement();
statusHeader.getStyle().setPosition(CSSStyleDeclaration.Position.ABSOLUTE);
statusHeader.getStyle().setBottom(0, CSSStyleDeclaration.Unit.PX);
statusHeader.getStyle().setLeft(50, CSSStyleDeclaration.Unit.PCT);
statusHeader.getStyle().setFontWeight(CSSStyleDeclaration.FontWeight.BOLD);
Elements.getBody().appendChild(statusHeader);
switchMode(Modes.COMMAND);
}
@Override
public void teardown() {
super.teardown();
statusHeader.removeFromParent();
}
/**
* Undo any shortcut-specific state (prefix values, etc).
*/
@Override
public void handleShortcutCalled() {
numericPrefixText.setLength(0);
}
}