codemirror
Version:
Full-featured in-browser code editor
1,089 lines (1,041 loc) • 232 kB
JavaScript
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../lib/codemirror"), require("../addon/search/searchcursor"), require("../addon/dialog/dialog"), require("../addon/edit/matchbrackets.js"));
else if (typeof define == "function" && define.amd) // AMD
define(["../lib/codemirror", "../addon/search/searchcursor", "../addon/dialog/dialog", "../addon/edit/matchbrackets"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
'use strict';
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
/**
* Supported keybindings:
* Too many to list. Refer to defaultKeymap below.
*
* Supported Ex commands:
* Refer to defaultExCommandMap below.
*
* Registers: unnamed, -, ., :, /, _, a-z, A-Z, 0-9
* (Does not respect the special case for number registers when delete
* operator is made with these commands: %, (, ), , /, ?, n, N, {, } )
* TODO: Implement the remaining registers.
*
* Marks: a-z, A-Z, and 0-9
* TODO: Implement the remaining special marks. They have more complex
* behavior.
*
* Events:
* 'vim-mode-change' - raised on the editor anytime the current mode changes,
* Event object: {mode: "visual", subMode: "linewise"}
*
* Code structure:
* 1. Default keymap
* 2. Variable declarations and short basic helpers
* 3. Instance (External API) implementation
* 4. Internal state tracking objects (input state, counter) implementation
* and instantiation
* 5. Key handler (the main command dispatcher) implementation
* 6. Motion, operator, and action implementations
* 7. Helper functions for the key handler, motions, operators, and actions
* 8. Set up Vim to work as a keymap for CodeMirror.
* 9. Ex command implementations.
*/
function initVim$1(CodeMirror) {
var Pos = CodeMirror.Pos;
function transformCursor(cm, range) {
var vim = cm.state.vim;
if (!vim || vim.insertMode) return range.head;
var head = vim.sel.head;
if (!head) return range.head;
if (vim.visualBlock) {
if (range.head.line != head.line) {
return;
}
}
if (range.from() == range.anchor && !range.empty()) {
if (range.head.line == head.line && range.head.ch != head.ch)
return new Pos(range.head.line, range.head.ch - 1);
}
return range.head;
}
var defaultKeymap = [
// Key to key mapping. This goes first to make it possible to override
// existing mappings.
{ keys: '<Left>', type: 'keyToKey', toKeys: 'h' },
{ keys: '<Right>', type: 'keyToKey', toKeys: 'l' },
{ keys: '<Up>', type: 'keyToKey', toKeys: 'k' },
{ keys: '<Down>', type: 'keyToKey', toKeys: 'j' },
{ keys: 'g<Up>', type: 'keyToKey', toKeys: 'gk' },
{ keys: 'g<Down>', type: 'keyToKey', toKeys: 'gj' },
{ keys: '<Space>', type: 'keyToKey', toKeys: 'l' },
{ keys: '<BS>', type: 'keyToKey', toKeys: 'h', context: 'normal'},
{ keys: '<Del>', type: 'keyToKey', toKeys: 'x', context: 'normal'},
{ keys: '<C-Space>', type: 'keyToKey', toKeys: 'W' },
{ keys: '<C-BS>', type: 'keyToKey', toKeys: 'B', context: 'normal' },
{ keys: '<S-Space>', type: 'keyToKey', toKeys: 'w' },
{ keys: '<S-BS>', type: 'keyToKey', toKeys: 'b', context: 'normal' },
{ keys: '<C-n>', type: 'keyToKey', toKeys: 'j' },
{ keys: '<C-p>', type: 'keyToKey', toKeys: 'k' },
{ keys: '<C-[>', type: 'keyToKey', toKeys: '<Esc>' },
{ keys: '<C-c>', type: 'keyToKey', toKeys: '<Esc>' },
{ keys: '<C-[>', type: 'keyToKey', toKeys: '<Esc>', context: 'insert' },
{ keys: '<C-c>', type: 'keyToKey', toKeys: '<Esc>', context: 'insert' },
{ keys: '<C-Esc>', type: 'keyToKey', toKeys: '<Esc>' }, // ipad keyboard sends C-Esc instead of C-[
{ keys: '<C-Esc>', type: 'keyToKey', toKeys: '<Esc>', context: 'insert' },
{ keys: 's', type: 'keyToKey', toKeys: 'cl', context: 'normal' },
{ keys: 's', type: 'keyToKey', toKeys: 'c', context: 'visual'},
{ keys: 'S', type: 'keyToKey', toKeys: 'cc', context: 'normal' },
{ keys: 'S', type: 'keyToKey', toKeys: 'VdO', context: 'visual' },
{ keys: '<Home>', type: 'keyToKey', toKeys: '0' },
{ keys: '<End>', type: 'keyToKey', toKeys: '$' },
{ keys: '<PageUp>', type: 'keyToKey', toKeys: '<C-b>' },
{ keys: '<PageDown>', type: 'keyToKey', toKeys: '<C-f>' },
{ keys: '<CR>', type: 'keyToKey', toKeys: 'j^', context: 'normal' },
{ keys: '<Ins>', type: 'keyToKey', toKeys: 'i', context: 'normal'},
{ keys: '<Ins>', type: 'action', action: 'toggleOverwrite', context: 'insert' },
// Motions
{ keys: 'H', type: 'motion', motion: 'moveToTopLine', motionArgs: { linewise: true, toJumplist: true }},
{ keys: 'M', type: 'motion', motion: 'moveToMiddleLine', motionArgs: { linewise: true, toJumplist: true }},
{ keys: 'L', type: 'motion', motion: 'moveToBottomLine', motionArgs: { linewise: true, toJumplist: true }},
{ keys: 'h', type: 'motion', motion: 'moveByCharacters', motionArgs: { forward: false }},
{ keys: 'l', type: 'motion', motion: 'moveByCharacters', motionArgs: { forward: true }},
{ keys: 'j', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, linewise: true }},
{ keys: 'k', type: 'motion', motion: 'moveByLines', motionArgs: { forward: false, linewise: true }},
{ keys: 'gj', type: 'motion', motion: 'moveByDisplayLines', motionArgs: { forward: true }},
{ keys: 'gk', type: 'motion', motion: 'moveByDisplayLines', motionArgs: { forward: false }},
{ keys: 'w', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: false }},
{ keys: 'W', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: false, bigWord: true }},
{ keys: 'e', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: true, inclusive: true }},
{ keys: 'E', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: true, bigWord: true, inclusive: true }},
{ keys: 'b', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false }},
{ keys: 'B', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false, bigWord: true }},
{ keys: 'ge', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: true, inclusive: true }},
{ keys: 'gE', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: true, bigWord: true, inclusive: true }},
{ keys: '{', type: 'motion', motion: 'moveByParagraph', motionArgs: { forward: false, toJumplist: true }},
{ keys: '}', type: 'motion', motion: 'moveByParagraph', motionArgs: { forward: true, toJumplist: true }},
{ keys: '(', type: 'motion', motion: 'moveBySentence', motionArgs: { forward: false }},
{ keys: ')', type: 'motion', motion: 'moveBySentence', motionArgs: { forward: true }},
{ keys: '<C-f>', type: 'motion', motion: 'moveByPage', motionArgs: { forward: true }},
{ keys: '<C-b>', type: 'motion', motion: 'moveByPage', motionArgs: { forward: false }},
{ keys: '<C-d>', type: 'motion', motion: 'moveByScroll', motionArgs: { forward: true, explicitRepeat: true }},
{ keys: '<C-u>', type: 'motion', motion: 'moveByScroll', motionArgs: { forward: false, explicitRepeat: true }},
{ keys: 'gg', type: 'motion', motion: 'moveToLineOrEdgeOfDocument', motionArgs: { forward: false, explicitRepeat: true, linewise: true, toJumplist: true }},
{ keys: 'G', type: 'motion', motion: 'moveToLineOrEdgeOfDocument', motionArgs: { forward: true, explicitRepeat: true, linewise: true, toJumplist: true }},
{keys: "g$", type: "motion", motion: "moveToEndOfDisplayLine"},
{keys: "g^", type: "motion", motion: "moveToStartOfDisplayLine"},
{keys: "g0", type: "motion", motion: "moveToStartOfDisplayLine"},
{ keys: '0', type: 'motion', motion: 'moveToStartOfLine' },
{ keys: '^', type: 'motion', motion: 'moveToFirstNonWhiteSpaceCharacter' },
{ keys: '+', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, toFirstChar:true }},
{ keys: '-', type: 'motion', motion: 'moveByLines', motionArgs: { forward: false, toFirstChar:true }},
{ keys: '_', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, toFirstChar:true, repeatOffset:-1 }},
{ keys: '$', type: 'motion', motion: 'moveToEol', motionArgs: { inclusive: true }},
{ keys: '%', type: 'motion', motion: 'moveToMatchedSymbol', motionArgs: { inclusive: true, toJumplist: true }},
{ keys: 'f<character>', type: 'motion', motion: 'moveToCharacter', motionArgs: { forward: true , inclusive: true }},
{ keys: 'F<character>', type: 'motion', motion: 'moveToCharacter', motionArgs: { forward: false }},
{ keys: 't<character>', type: 'motion', motion: 'moveTillCharacter', motionArgs: { forward: true, inclusive: true }},
{ keys: 'T<character>', type: 'motion', motion: 'moveTillCharacter', motionArgs: { forward: false }},
{ keys: ';', type: 'motion', motion: 'repeatLastCharacterSearch', motionArgs: { forward: true }},
{ keys: ',', type: 'motion', motion: 'repeatLastCharacterSearch', motionArgs: { forward: false }},
{ keys: '\'<character>', type: 'motion', motion: 'goToMark', motionArgs: {toJumplist: true, linewise: true}},
{ keys: '`<character>', type: 'motion', motion: 'goToMark', motionArgs: {toJumplist: true}},
{ keys: ']`', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true } },
{ keys: '[`', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false } },
{ keys: ']\'', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true, linewise: true } },
{ keys: '[\'', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false, linewise: true } },
// the next two aren't motions but must come before more general motion declarations
{ keys: ']p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: true, isEdit: true, matchIndent: true}},
{ keys: '[p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: false, isEdit: true, matchIndent: true}},
{ keys: ']<character>', type: 'motion', motion: 'moveToSymbol', motionArgs: { forward: true, toJumplist: true}},
{ keys: '[<character>', type: 'motion', motion: 'moveToSymbol', motionArgs: { forward: false, toJumplist: true}},
{ keys: '|', type: 'motion', motion: 'moveToColumn'},
{ keys: 'o', type: 'motion', motion: 'moveToOtherHighlightedEnd', context:'visual'},
{ keys: 'O', type: 'motion', motion: 'moveToOtherHighlightedEnd', motionArgs: {sameLine: true}, context:'visual'},
// Operators
{ keys: 'd', type: 'operator', operator: 'delete' },
{ keys: 'y', type: 'operator', operator: 'yank' },
{ keys: 'c', type: 'operator', operator: 'change' },
{ keys: '=', type: 'operator', operator: 'indentAuto' },
{ keys: '>', type: 'operator', operator: 'indent', operatorArgs: { indentRight: true }},
{ keys: '<', type: 'operator', operator: 'indent', operatorArgs: { indentRight: false }},
{ keys: 'g~', type: 'operator', operator: 'changeCase' },
{ keys: 'gu', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: true}, isEdit: true },
{ keys: 'gU', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: false}, isEdit: true },
{ keys: 'n', type: 'motion', motion: 'findNext', motionArgs: { forward: true, toJumplist: true }},
{ keys: 'N', type: 'motion', motion: 'findNext', motionArgs: { forward: false, toJumplist: true }},
{ keys: 'gn', type: 'motion', motion: 'findAndSelectNextInclusive', motionArgs: { forward: true }},
{ keys: 'gN', type: 'motion', motion: 'findAndSelectNextInclusive', motionArgs: { forward: false }},
// Operator-Motion dual commands
{ keys: 'x', type: 'operatorMotion', operator: 'delete', motion: 'moveByCharacters', motionArgs: { forward: true }, operatorMotionArgs: { visualLine: false }},
{ keys: 'X', type: 'operatorMotion', operator: 'delete', motion: 'moveByCharacters', motionArgs: { forward: false }, operatorMotionArgs: { visualLine: true }},
{ keys: 'D', type: 'operatorMotion', operator: 'delete', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'},
{ keys: 'D', type: 'operator', operator: 'delete', operatorArgs: { linewise: true }, context: 'visual'},
{ keys: 'Y', type: 'operatorMotion', operator: 'yank', motion: 'expandToLine', motionArgs: { linewise: true }, context: 'normal'},
{ keys: 'Y', type: 'operator', operator: 'yank', operatorArgs: { linewise: true }, context: 'visual'},
{ keys: 'C', type: 'operatorMotion', operator: 'change', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'},
{ keys: 'C', type: 'operator', operator: 'change', operatorArgs: { linewise: true }, context: 'visual'},
{ keys: '~', type: 'operatorMotion', operator: 'changeCase', motion: 'moveByCharacters', motionArgs: { forward: true }, operatorArgs: { shouldMoveCursor: true }, context: 'normal'},
{ keys: '~', type: 'operator', operator: 'changeCase', context: 'visual'},
{ keys: '<C-u>', type: 'operatorMotion', operator: 'delete', motion: 'moveToStartOfLine', context: 'insert' },
{ keys: '<C-w>', type: 'operatorMotion', operator: 'delete', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false }, context: 'insert' },
//ignore C-w in normal mode
{ keys: '<C-w>', type: 'idle', context: 'normal' },
// Actions
{ keys: '<C-i>', type: 'action', action: 'jumpListWalk', actionArgs: { forward: true }},
{ keys: '<C-o>', type: 'action', action: 'jumpListWalk', actionArgs: { forward: false }},
{ keys: '<C-e>', type: 'action', action: 'scroll', actionArgs: { forward: true, linewise: true }},
{ keys: '<C-y>', type: 'action', action: 'scroll', actionArgs: { forward: false, linewise: true }},
{ keys: 'a', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'charAfter' }, context: 'normal' },
{ keys: 'A', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'eol' }, context: 'normal' },
{ keys: 'A', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'endOfSelectedArea' }, context: 'visual' },
{ keys: 'i', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'inplace' }, context: 'normal' },
{ keys: 'gi', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'lastEdit' }, context: 'normal' },
{ keys: 'I', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'firstNonBlank'}, context: 'normal' },
{ keys: 'gI', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'bol'}, context: 'normal' },
{ keys: 'I', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'startOfSelectedArea' }, context: 'visual' },
{ keys: 'o', type: 'action', action: 'newLineAndEnterInsertMode', isEdit: true, interlaceInsertRepeat: true, actionArgs: { after: true }, context: 'normal' },
{ keys: 'O', type: 'action', action: 'newLineAndEnterInsertMode', isEdit: true, interlaceInsertRepeat: true, actionArgs: { after: false }, context: 'normal' },
{ keys: 'v', type: 'action', action: 'toggleVisualMode' },
{ keys: 'V', type: 'action', action: 'toggleVisualMode', actionArgs: { linewise: true }},
{ keys: '<C-v>', type: 'action', action: 'toggleVisualMode', actionArgs: { blockwise: true }},
{ keys: '<C-q>', type: 'action', action: 'toggleVisualMode', actionArgs: { blockwise: true }},
{ keys: 'gv', type: 'action', action: 'reselectLastSelection' },
{ keys: 'J', type: 'action', action: 'joinLines', isEdit: true },
{ keys: 'gJ', type: 'action', action: 'joinLines', actionArgs: { keepSpaces: true }, isEdit: true },
{ keys: 'p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: true, isEdit: true }},
{ keys: 'P', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: false, isEdit: true }},
{ keys: 'r<character>', type: 'action', action: 'replace', isEdit: true },
{ keys: '@<character>', type: 'action', action: 'replayMacro' },
{ keys: 'q<character>', type: 'action', action: 'enterMacroRecordMode' },
// Handle Replace-mode as a special case of insert mode.
{ keys: 'R', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { replace: true }, context: 'normal'},
{ keys: 'R', type: 'operator', operator: 'change', operatorArgs: { linewise: true, fullLine: true }, context: 'visual', exitVisualBlock: true},
{ keys: 'u', type: 'action', action: 'undo', context: 'normal' },
{ keys: 'u', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: true}, context: 'visual', isEdit: true },
{ keys: 'U', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: false}, context: 'visual', isEdit: true },
{ keys: '<C-r>', type: 'action', action: 'redo' },
{ keys: 'm<character>', type: 'action', action: 'setMark' },
{ keys: '"<character>', type: 'action', action: 'setRegister' },
{ keys: 'zz', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'center' }},
{ keys: 'z.', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'center' }, motion: 'moveToFirstNonWhiteSpaceCharacter' },
{ keys: 'zt', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'top' }},
{ keys: 'z<CR>', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'top' }, motion: 'moveToFirstNonWhiteSpaceCharacter' },
{ keys: 'zb', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'bottom' }},
{ keys: 'z-', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'bottom' }, motion: 'moveToFirstNonWhiteSpaceCharacter' },
{ keys: '.', type: 'action', action: 'repeatLastEdit' },
{ keys: '<C-a>', type: 'action', action: 'incrementNumberToken', isEdit: true, actionArgs: {increase: true, backtrack: false}},
{ keys: '<C-x>', type: 'action', action: 'incrementNumberToken', isEdit: true, actionArgs: {increase: false, backtrack: false}},
{ keys: '<C-t>', type: 'action', action: 'indent', actionArgs: { indentRight: true }, context: 'insert' },
{ keys: '<C-d>', type: 'action', action: 'indent', actionArgs: { indentRight: false }, context: 'insert' },
// Text object motions
{ keys: 'a<character>', type: 'motion', motion: 'textObjectManipulation' },
{ keys: 'i<character>', type: 'motion', motion: 'textObjectManipulation', motionArgs: { textObjectInner: true }},
// Search
{ keys: '/', type: 'search', searchArgs: { forward: true, querySrc: 'prompt', toJumplist: true }},
{ keys: '?', type: 'search', searchArgs: { forward: false, querySrc: 'prompt', toJumplist: true }},
{ keys: '*', type: 'search', searchArgs: { forward: true, querySrc: 'wordUnderCursor', wholeWordOnly: true, toJumplist: true }},
{ keys: '#', type: 'search', searchArgs: { forward: false, querySrc: 'wordUnderCursor', wholeWordOnly: true, toJumplist: true }},
{ keys: 'g*', type: 'search', searchArgs: { forward: true, querySrc: 'wordUnderCursor', toJumplist: true }},
{ keys: 'g#', type: 'search', searchArgs: { forward: false, querySrc: 'wordUnderCursor', toJumplist: true }},
// Ex command
{ keys: ':', type: 'ex' }
];
var defaultKeymapLength = defaultKeymap.length;
/**
* Ex commands
* Care must be taken when adding to the default Ex command map. For any
* pair of commands that have a shared prefix, at least one of their
* shortNames must not match the prefix of the other command.
*/
var defaultExCommandMap = [
{ name: 'colorscheme', shortName: 'colo' },
{ name: 'map' },
{ name: 'imap', shortName: 'im' },
{ name: 'nmap', shortName: 'nm' },
{ name: 'vmap', shortName: 'vm' },
{ name: 'unmap' },
{ name: 'write', shortName: 'w' },
{ name: 'undo', shortName: 'u' },
{ name: 'redo', shortName: 'red' },
{ name: 'set', shortName: 'se' },
{ name: 'setlocal', shortName: 'setl' },
{ name: 'setglobal', shortName: 'setg' },
{ name: 'sort', shortName: 'sor' },
{ name: 'substitute', shortName: 's', possiblyAsync: true },
{ name: 'nohlsearch', shortName: 'noh' },
{ name: 'yank', shortName: 'y' },
{ name: 'delmarks', shortName: 'delm' },
{ name: 'registers', shortName: 'reg', excludeFromCommandHistory: true },
{ name: 'vglobal', shortName: 'v' },
{ name: 'global', shortName: 'g' }
];
function enterVimMode(cm) {
cm.setOption('disableInput', true);
cm.setOption('showCursorWhenSelecting', false);
CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"});
cm.on('cursorActivity', onCursorActivity);
maybeInitVimState(cm);
CodeMirror.on(cm.getInputField(), 'paste', getOnPasteFn(cm));
}
function leaveVimMode(cm) {
cm.setOption('disableInput', false);
cm.off('cursorActivity', onCursorActivity);
CodeMirror.off(cm.getInputField(), 'paste', getOnPasteFn(cm));
cm.state.vim = null;
if (highlightTimeout) clearTimeout(highlightTimeout);
}
function detachVimMap(cm, next) {
if (this == CodeMirror.keyMap.vim) {
cm.options.$customCursor = null;
CodeMirror.rmClass(cm.getWrapperElement(), "cm-fat-cursor");
}
if (!next || next.attach != attachVimMap)
leaveVimMode(cm);
}
function attachVimMap(cm, prev) {
if (this == CodeMirror.keyMap.vim) {
if (cm.curOp) cm.curOp.selectionChanged = true;
cm.options.$customCursor = transformCursor;
CodeMirror.addClass(cm.getWrapperElement(), "cm-fat-cursor");
}
if (!prev || prev.attach != attachVimMap)
enterVimMode(cm);
}
// Deprecated, simply setting the keymap works again.
CodeMirror.defineOption('vimMode', false, function(cm, val, prev) {
if (val && cm.getOption("keyMap") != "vim")
cm.setOption("keyMap", "vim");
else if (!val && prev != CodeMirror.Init && /^vim/.test(cm.getOption("keyMap")))
cm.setOption("keyMap", "default");
});
function cmKey(key, cm) {
if (!cm) { return undefined; }
if (this[key]) { return this[key]; }
var vimKey = cmKeyToVimKey(key);
if (!vimKey) {
return false;
}
var cmd = vimApi.findKey(cm, vimKey);
if (typeof cmd == 'function') {
CodeMirror.signal(cm, 'vim-keypress', vimKey);
}
return cmd;
}
var modifiers = {Shift:'S',Ctrl:'C',Alt:'A',Cmd:'D',Mod:'A',CapsLock:''};
var specialKeys = {Enter:'CR',Backspace:'BS',Delete:'Del',Insert:'Ins'};
function cmKeyToVimKey(key) {
if (key.charAt(0) == '\'') {
// Keypress character binding of format "'a'"
return key.charAt(1);
}
var pieces = key.split(/-(?!$)/);
var lastPiece = pieces[pieces.length - 1];
if (pieces.length == 1 && pieces[0].length == 1) {
// No-modifier bindings use literal character bindings above. Skip.
return false;
} else if (pieces.length == 2 && pieces[0] == 'Shift' && lastPiece.length == 1) {
// Ignore Shift+char bindings as they should be handled by literal character.
return false;
}
var hasCharacter = false;
for (var i = 0; i < pieces.length; i++) {
var piece = pieces[i];
if (piece in modifiers) { pieces[i] = modifiers[piece]; }
else { hasCharacter = true; }
if (piece in specialKeys) { pieces[i] = specialKeys[piece]; }
}
if (!hasCharacter) {
// Vim does not support modifier only keys.
return false;
}
// TODO: Current bindings expect the character to be lower case, but
// it looks like vim key notation uses upper case.
if (isUpperCase(lastPiece)) {
pieces[pieces.length - 1] = lastPiece.toLowerCase();
}
return '<' + pieces.join('-') + '>';
}
function getOnPasteFn(cm) {
var vim = cm.state.vim;
if (!vim.onPasteFn) {
vim.onPasteFn = function() {
if (!vim.insertMode) {
cm.setCursor(offsetCursor(cm.getCursor(), 0, 1));
actions.enterInsertMode(cm, {}, vim);
}
};
}
return vim.onPasteFn;
}
var numberRegex = /[\d]/;
var wordCharTest = [CodeMirror.isWordChar, function(ch) {
return ch && !CodeMirror.isWordChar(ch) && !/\s/.test(ch);
}], bigWordCharTest = [function(ch) {
return /\S/.test(ch);
}];
function makeKeyRange(start, size) {
var keys = [];
for (var i = start; i < start + size; i++) {
keys.push(String.fromCharCode(i));
}
return keys;
}
var upperCaseAlphabet = makeKeyRange(65, 26);
var lowerCaseAlphabet = makeKeyRange(97, 26);
var numbers = makeKeyRange(48, 10);
var validMarks = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['<', '>']);
var validRegisters = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['-', '"', '.', ':', '_', '/']);
var upperCaseChars;
try { upperCaseChars = new RegExp("^[\\p{Lu}]$", "u"); }
catch (_) { upperCaseChars = /^[A-Z]$/; }
function isLine(cm, line) {
return line >= cm.firstLine() && line <= cm.lastLine();
}
function isLowerCase(k) {
return (/^[a-z]$/).test(k);
}
function isMatchableSymbol(k) {
return '()[]{}'.indexOf(k) != -1;
}
function isNumber(k) {
return numberRegex.test(k);
}
function isUpperCase(k) {
return upperCaseChars.test(k);
}
function isWhiteSpaceString(k) {
return (/^\s*$/).test(k);
}
function isEndOfSentenceSymbol(k) {
return '.?!'.indexOf(k) != -1;
}
function inArray(val, arr) {
for (var i = 0; i < arr.length; i++) {
if (arr[i] == val) {
return true;
}
}
return false;
}
var options = {};
function defineOption(name, defaultValue, type, aliases, callback) {
if (defaultValue === undefined && !callback) {
throw Error('defaultValue is required unless callback is provided');
}
if (!type) { type = 'string'; }
options[name] = {
type: type,
defaultValue: defaultValue,
callback: callback
};
if (aliases) {
for (var i = 0; i < aliases.length; i++) {
options[aliases[i]] = options[name];
}
}
if (defaultValue) {
setOption(name, defaultValue);
}
}
function setOption(name, value, cm, cfg) {
var option = options[name];
cfg = cfg || {};
var scope = cfg.scope;
if (!option) {
return new Error('Unknown option: ' + name);
}
if (option.type == 'boolean') {
if (value && value !== true) {
return new Error('Invalid argument: ' + name + '=' + value);
} else if (value !== false) {
// Boolean options are set to true if value is not defined.
value = true;
}
}
if (option.callback) {
if (scope !== 'local') {
option.callback(value, undefined);
}
if (scope !== 'global' && cm) {
option.callback(value, cm);
}
} else {
if (scope !== 'local') {
option.value = option.type == 'boolean' ? !!value : value;
}
if (scope !== 'global' && cm) {
cm.state.vim.options[name] = {value: value};
}
}
}
function getOption(name, cm, cfg) {
var option = options[name];
cfg = cfg || {};
var scope = cfg.scope;
if (!option) {
return new Error('Unknown option: ' + name);
}
if (option.callback) {
var local = cm && option.callback(undefined, cm);
if (scope !== 'global' && local !== undefined) {
return local;
}
if (scope !== 'local') {
return option.callback();
}
return;
} else {
var local = (scope !== 'global') && (cm && cm.state.vim.options[name]);
return (local || (scope !== 'local') && option || {}).value;
}
}
defineOption('filetype', undefined, 'string', ['ft'], function(name, cm) {
// Option is local. Do nothing for global.
if (cm === undefined) {
return;
}
// The 'filetype' option proxies to the CodeMirror 'mode' option.
if (name === undefined) {
var mode = cm.getOption('mode');
return mode == 'null' ? '' : mode;
} else {
var mode = name == '' ? 'null' : name;
cm.setOption('mode', mode);
}
});
var createCircularJumpList = function() {
var size = 100;
var pointer = -1;
var head = 0;
var tail = 0;
var buffer = new Array(size);
function add(cm, oldCur, newCur) {
var current = pointer % size;
var curMark = buffer[current];
function useNextSlot(cursor) {
var next = ++pointer % size;
var trashMark = buffer[next];
if (trashMark) {
trashMark.clear();
}
buffer[next] = cm.setBookmark(cursor);
}
if (curMark) {
var markPos = curMark.find();
// avoid recording redundant cursor position
if (markPos && !cursorEqual(markPos, oldCur)) {
useNextSlot(oldCur);
}
} else {
useNextSlot(oldCur);
}
useNextSlot(newCur);
head = pointer;
tail = pointer - size + 1;
if (tail < 0) {
tail = 0;
}
}
function move(cm, offset) {
pointer += offset;
if (pointer > head) {
pointer = head;
} else if (pointer < tail) {
pointer = tail;
}
var mark = buffer[(size + pointer) % size];
// skip marks that are temporarily removed from text buffer
if (mark && !mark.find()) {
var inc = offset > 0 ? 1 : -1;
var newCur;
var oldCur = cm.getCursor();
do {
pointer += inc;
mark = buffer[(size + pointer) % size];
// skip marks that are the same as current position
if (mark &&
(newCur = mark.find()) &&
!cursorEqual(oldCur, newCur)) {
break;
}
} while (pointer < head && pointer > tail);
}
return mark;
}
function find(cm, offset) {
var oldPointer = pointer;
var mark = move(cm, offset);
pointer = oldPointer;
return mark && mark.find();
}
return {
cachedCursor: undefined, //used for # and * jumps
add: add,
find: find,
move: move
};
};
// Returns an object to track the changes associated insert mode. It
// clones the object that is passed in, or creates an empty object one if
// none is provided.
var createInsertModeChanges = function(c) {
if (c) {
// Copy construction
return {
changes: c.changes,
expectCursorActivityForChange: c.expectCursorActivityForChange
};
}
return {
// Change list
changes: [],
// Set to true on change, false on cursorActivity.
expectCursorActivityForChange: false
};
};
function MacroModeState() {
this.latestRegister = undefined;
this.isPlaying = false;
this.isRecording = false;
this.replaySearchQueries = [];
this.onRecordingDone = undefined;
this.lastInsertModeChanges = createInsertModeChanges();
}
MacroModeState.prototype = {
exitMacroRecordMode: function() {
var macroModeState = vimGlobalState.macroModeState;
if (macroModeState.onRecordingDone) {
macroModeState.onRecordingDone(); // close dialog
}
macroModeState.onRecordingDone = undefined;
macroModeState.isRecording = false;
},
enterMacroRecordMode: function(cm, registerName) {
var register =
vimGlobalState.registerController.getRegister(registerName);
if (register) {
register.clear();
this.latestRegister = registerName;
if (cm.openDialog) {
var template = dom('span', {class: 'cm-vim-message'}, 'recording @' + registerName);
this.onRecordingDone = cm.openDialog(template, null, {bottom:true});
}
this.isRecording = true;
}
}
};
function maybeInitVimState(cm) {
if (!cm.state.vim) {
// Store instance state in the CodeMirror object.
cm.state.vim = {
inputState: new InputState(),
// Vim's input state that triggered the last edit, used to repeat
// motions and operators with '.'.
lastEditInputState: undefined,
// Vim's action command before the last edit, used to repeat actions
// with '.' and insert mode repeat.
lastEditActionCommand: undefined,
// When using jk for navigation, if you move from a longer line to a
// shorter line, the cursor may clip to the end of the shorter line.
// If j is pressed again and cursor goes to the next line, the
// cursor should go back to its horizontal position on the longer
// line if it can. This is to keep track of the horizontal position.
lastHPos: -1,
// Doing the same with screen-position for gj/gk
lastHSPos: -1,
// The last motion command run. Cleared if a non-motion command gets
// executed in between.
lastMotion: null,
marks: {},
insertMode: false,
// Repeat count for changes made in insert mode, triggered by key
// sequences like 3,i. Only exists when insertMode is true.
insertModeRepeat: undefined,
visualMode: false,
// If we are in visual line mode. No effect if visualMode is false.
visualLine: false,
visualBlock: false,
lastSelection: null,
lastPastedText: null,
sel: {},
// Buffer-local/window-local values of vim options.
options: {}
};
}
return cm.state.vim;
}
var vimGlobalState;
function resetVimGlobalState() {
vimGlobalState = {
// The current search query.
searchQuery: null,
// Whether we are searching backwards.
searchIsReversed: false,
// Replace part of the last substituted pattern
lastSubstituteReplacePart: undefined,
jumpList: createCircularJumpList(),
macroModeState: new MacroModeState,
// Recording latest f, t, F or T motion command.
lastCharacterSearch: {increment:0, forward:true, selectedCharacter:''},
registerController: new RegisterController({}),
// search history buffer
searchHistoryController: new HistoryController(),
// ex Command history buffer
exCommandHistoryController : new HistoryController()
};
for (var optionName in options) {
var option = options[optionName];
option.value = option.defaultValue;
}
}
var lastInsertModeKeyTimer;
var vimApi = {
enterVimMode: enterVimMode,
buildKeyMap: function() {
// TODO: Convert keymap into dictionary format for fast lookup.
},
// Testing hook, though it might be useful to expose the register
// controller anyway.
getRegisterController: function() {
return vimGlobalState.registerController;
},
// Testing hook.
resetVimGlobalState_: resetVimGlobalState,
// Testing hook.
getVimGlobalState_: function() {
return vimGlobalState;
},
// Testing hook.
maybeInitVimState_: maybeInitVimState,
suppressErrorLogging: false,
InsertModeKey: InsertModeKey,
map: function(lhs, rhs, ctx) {
// Add user defined key bindings.
exCommandDispatcher.map(lhs, rhs, ctx);
},
unmap: function(lhs, ctx) {
return exCommandDispatcher.unmap(lhs, ctx);
},
// Non-recursive map function.
// NOTE: This will not create mappings to key maps that aren't present
// in the default key map. See TODO at bottom of function.
noremap: function(lhs, rhs, ctx) {
function toCtxArray(ctx) {
return ctx ? [ctx] : ['normal', 'insert', 'visual'];
}
var ctxsToMap = toCtxArray(ctx);
// Look through all actual defaults to find a map candidate.
var actualLength = defaultKeymap.length, origLength = defaultKeymapLength;
for (var i = actualLength - origLength;
i < actualLength && ctxsToMap.length;
i++) {
var mapping = defaultKeymap[i];
// Omit mappings that operate in the wrong context(s) and those of invalid type.
if (mapping.keys == rhs &&
(!ctx || !mapping.context || mapping.context === ctx) &&
mapping.type.substr(0, 2) !== 'ex' &&
mapping.type.substr(0, 3) !== 'key') {
// Make a shallow copy of the original keymap entry.
var newMapping = {};
for (var key in mapping) {
newMapping[key] = mapping[key];
}
// Modify it point to the new mapping with the proper context.
newMapping.keys = lhs;
if (ctx && !newMapping.context) {
newMapping.context = ctx;
}
// Add it to the keymap with a higher priority than the original.
this._mapCommand(newMapping);
// Record the mapped contexts as complete.
var mappedCtxs = toCtxArray(mapping.context);
ctxsToMap = ctxsToMap.filter(function(el) { return mappedCtxs.indexOf(el) === -1; });
}
}
// TODO: Create non-recursive keyToKey mappings for the unmapped contexts once those exist.
},
// Remove all user-defined mappings for the provided context.
mapclear: function(ctx) {
// Partition the existing keymap into user-defined and true defaults.
var actualLength = defaultKeymap.length,
origLength = defaultKeymapLength;
var userKeymap = defaultKeymap.slice(0, actualLength - origLength);
defaultKeymap = defaultKeymap.slice(actualLength - origLength);
if (ctx) {
// If a specific context is being cleared, we need to keep mappings
// from all other contexts.
for (var i = userKeymap.length - 1; i >= 0; i--) {
var mapping = userKeymap[i];
if (ctx !== mapping.context) {
if (mapping.context) {
this._mapCommand(mapping);
} else {
// `mapping` applies to all contexts so create keymap copies
// for each context except the one being cleared.
var contexts = ['normal', 'insert', 'visual'];
for (var j in contexts) {
if (contexts[j] !== ctx) {
var newMapping = {};
for (var key in mapping) {
newMapping[key] = mapping[key];
}
newMapping.context = contexts[j];
this._mapCommand(newMapping);
}
}
}
}
}
}
},
// TODO: Expose setOption and getOption as instance methods. Need to decide how to namespace
// them, or somehow make them work with the existing CodeMirror setOption/getOption API.
setOption: setOption,
getOption: getOption,
defineOption: defineOption,
defineEx: function(name, prefix, func){
if (!prefix) {
prefix = name;
} else if (name.indexOf(prefix) !== 0) {
throw new Error('(Vim.defineEx) "'+prefix+'" is not a prefix of "'+name+'", command not registered');
}
exCommands[name]=func;
exCommandDispatcher.commandMap_[prefix]={name:name, shortName:prefix, type:'api'};
},
handleKey: function (cm, key, origin) {
var command = this.findKey(cm, key, origin);
if (typeof command === 'function') {
return command();
}
},
multiSelectHandleKey: multiSelectHandleKey,
/**
* This is the outermost function called by CodeMirror, after keys have
* been mapped to their Vim equivalents.
*
* Finds a command based on the key (and cached keys if there is a
* multi-key sequence). Returns `undefined` if no key is matched, a noop
* function if a partial match is found (multi-key), and a function to
* execute the bound command if a a key is matched. The function always
* returns true.
*/
findKey: function(cm, key, origin) {
var vim = maybeInitVimState(cm);
function handleMacroRecording() {
var macroModeState = vimGlobalState.macroModeState;
if (macroModeState.isRecording) {
if (key == 'q') {
macroModeState.exitMacroRecordMode();
clearInputState(cm);
return true;
}
if (origin != 'mapping') {
logKey(macroModeState, key);
}
}
}
function handleEsc() {
if (key == '<Esc>') {
if (vim.visualMode) {
// Get back to normal mode.
exitVisualMode(cm);
} else if (vim.insertMode) {
// Get back to normal mode.
exitInsertMode(cm);
} else {
// We're already in normal mode. Let '<Esc>' be handled normally.
return;
}
clearInputState(cm);
return true;
}
}
function doKeyToKey(keys) {
// TODO: prevent infinite recursion.
var match;
while (keys) {
// Pull off one command key, which is either a single character
// or a special sequence wrapped in '<' and '>', e.g. '<Space>'.
match = (/<\w+-.+?>|<\w+>|./).exec(keys);
key = match[0];
keys = keys.substring(match.index + key.length);
vimApi.handleKey(cm, key, 'mapping');
}
}
function handleKeyInsertMode() {
if (handleEsc()) { return true; }
var keys = vim.inputState.keyBuffer = vim.inputState.keyBuffer + key;
var keysAreChars = key.length == 1;
var match = commandDispatcher.matchCommand(keys, defaultKeymap, vim.inputState, 'insert');
// Need to check all key substrings in insert mode.
while (keys.length > 1 && match.type != 'full') {
var keys = vim.inputState.keyBuffer = keys.slice(1);
var thisMatch = commandDispatcher.matchCommand(keys, defaultKeymap, vim.inputState, 'insert');
if (thisMatch.type != 'none') { match = thisMatch; }
}
if (match.type == 'none') { clearInputState(cm); return false; }
else if (match.type == 'partial') {
if (lastInsertModeKeyTimer) { window.clearTimeout(lastInsertModeKeyTimer); }
lastInsertModeKeyTimer = window.setTimeout(
function() { if (vim.insertMode && vim.inputState.keyBuffer) { clearInputState(cm); } },
getOption('insertModeEscKeysTimeout'));
return !keysAreChars;
}
if (lastInsertModeKeyTimer) { window.clearTimeout(lastInsertModeKeyTimer); }
if (keysAreChars) {
var selections = cm.listSelections();
for (var i = 0; i < selections.length; i++) {
var here = selections[i].head;
cm.replaceRange('', offsetCursor(here, 0, -(keys.length - 1)), here, '+input');
}
vimGlobalState.macroModeState.lastInsertModeChanges.changes.pop();
}
clearInputState(cm);
return match.command;
}
function handleKeyNonInsertMode() {
if (handleMacroRecording() || handleEsc()) { return true; }
var keys = vim.inputState.keyBuffer = vim.inputState.keyBuffer + key;
if (/^[1-9]\d*$/.test(keys)) { return true; }
var keysMatcher = /^(\d*)(.*)$/.exec(keys);
if (!keysMatcher) { clearInputState(cm); return false; }
var context = vim.visualMode ? 'visual' :
'normal';
var mainKey = keysMatcher[2] || keysMatcher[1];
if (vim.inputState.operatorShortcut && vim.inputState.operatorShortcut.slice(-1) == mainKey) {
// multikey operators act linewise by repeating only the last character
mainKey = vim.inputState.operatorShortcut;
}
var match = commandDispatcher.matchCommand(mainKey, defaultKeymap, vim.inputState, context);
if (match.type == 'none') { clearInputState(cm); return false; }
else if (match.type == 'partial') { return true; }
else if (match.type == 'clear') { clearInputState(cm); return true; }
vim.inputState.keyBuffer = '';
keysMatcher = /^(\d*)(.*)$/.exec(keys);
if (keysMatcher[1] && keysMatcher[1] != '0') {
vim.inputState.pushRepeatDigit(keysMatcher[1]);
}
return match.command;
}
var command;
if (vim.insertMode) { command = handleKeyInsertMode(); }
else { command = handleKeyNonInsertMode(); }
if (command === false) {
return !vim.insertMode && key.length === 1 ? function() { return true; } : undefined;
} else if (command === true) {
// TODO: Look into using CodeMirror's multi-key handling.
// Return no-op since we are caching the key. Counts as handled, but
// don't want act on it just yet.
return function() { return true; };
} else {
return function() {
return cm.operation(function() {
cm.curOp.isVimOp = true;
try {
if (command.type == 'keyToKey') {
doKeyToKey(command.toKeys);
} else {
commandDispatcher.processCommand(cm, vim, command);
}
} catch (e) {
// clear VIM state in case it's in a bad state.
cm.state.vim = undefined;
maybeInitVimState(cm);
if (!vimApi.suppressErrorLogging) {
console['log'](e);
}
throw e;
}
return true;
});
};
}
},
handleEx: function(cm, input) {
exCommandDispatcher.processCommand(cm, input);
},
defineMotion: defineMotion,
defineAction: defineAction,
defineOperator: defineOperator,
mapCommand: mapCommand,
_mapCommand: _mapCommand,
defineRegister: defineRegister,
exitVisualMode: exitVisualMode,
exitInsertMode: exitInsertMode
};
// Represents the current input state.
function InputState() {
this.prefixRepeat = [];
this.motionRepeat = [];
this.operator = null;
this.operatorArgs = null;
this.motion = null;
this.motionArgs = null;
this.keyBuffer = []; // For matching multi-key commands.
this.registerName = null; // Defaults to the unnamed register.
}
InputState.prototype.pushRepeatDigit = function(n) {
if (!this.operator) {
this.prefixRepeat = this.prefixRepeat.concat(n);
} else {
this.motionRepeat = this.motionRepeat.concat(n);
}
};
InputState.prototype.getRepeat = function() {
var repeat = 0;
if (this.prefixRepeat.length > 0 || this.motionRepeat.length > 0) {
repeat = 1;
if (this.prefixRepeat.length > 0) {
repeat *= parseInt(this.prefixRepeat.join(''), 10);
}
if (this.motionRepeat.length > 0) {
repeat *= parseInt(this.motionRepeat.join(''), 10);
}
}
return repeat;
};
function clearInputState(cm, reason) {
cm.state.vim.inputState = new InputState();
CodeMirror.signal(cm, 'vim-command-done', reason);
}
/*
* Register stores information about copy and paste registers. Besides
* text, a register must store whether it is linewise (i.e., when it is
* pasted, should it insert itself into a new line, or should the text be
* inserted at the cursor position.)
*/
function Register(text, linewise, blockwise) {
this.clear();
this.keyBuffer = [text || ''];
this.insertModeChanges = [];
this.searchQueries = [];
this.linewise = !!linewise;
this.blockwise = !!blockwise;
}
Register.prototype = {
setText: function(text, linewise, blockwise) {
this.keyBuffer = [text || ''];
this.linewise = !!linewise;
this.blockwise = !!blockwise;
},
pushText: function(text, linewise) {
// if this register has ever been set to linewise, use linewise.
if (linewise) {
if (!this.linewise) {
this.keyBuffer.push('\n');
}
this.linewise = true;
}
this.keyBuffer.push(text);
},
pushInsertModeChanges: function(changes) {
this.insertModeChanges.push(createInsertModeChanges(changes));
},
pushSearchQuery: function(query) {
this.searchQueries.push(query);
},
clear: function() {
this.keyBuffer = [];
this.insertModeChanges = [];
this.searchQueries = [];