123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298 |
- /**
- * Supported keybindings:
- *
- * Motion:
- * h, j, k, l
- * e, E, w, W, b, B, ge, gE
- * f<character>, F<character>, t<character>, T<character>
- * $, ^, 0
- * gg, G
- * %
- * '<character>, `<character>
- *
- * Operator:
- * d, y, c
- * dd, yy, cc
- * g~, g~g~
- * >, <, >>, <<
- *
- * Operator-Motion:
- * x, X, D, Y, C, ~
- *
- * Action:
- * a, i, s, A, I, S, o, O
- * J
- * u, Ctrl-r
- * m<character>
- * r<character>
- *
- * Modes:
- * ESC - leave insert mode, visual mode, and clear input state.
- * Ctrl-[, Ctrl-c - same as ESC.
- *
- * Registers: unamed, -, 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.
- *
- * 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 instanstiation
- * 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.
- */
- (function() {
- 'use strict';
- 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: ['Space'], type: 'keyToKey', toKeys: ['l'] },
- { keys: ['Backspace'], type: 'keyToKey', toKeys: ['h'] },
- { keys: ['Ctrl-Space'], type: 'keyToKey', toKeys: ['W'] },
- { keys: ['Ctrl-Backspace'], type: 'keyToKey', toKeys: ['B'] },
- { keys: ['Shift-Space'], type: 'keyToKey', toKeys: ['w'] },
- { keys: ['Shift-Backspace'], type: 'keyToKey', toKeys: ['b'] },
- { keys: ['Ctrl-n'], type: 'keyToKey', toKeys: ['j'] },
- { keys: ['Ctrl-p'], type: 'keyToKey', toKeys: ['k'] },
- { keys: ['Ctrl-['], type: 'keyToKey', toKeys: ['Esc'] },
- { keys: ['Ctrl-c'], type: 'keyToKey', toKeys: ['Esc'] },
- { keys: ['s'], type: 'keyToKey', toKeys: ['c', 'l'] },
- { keys: ['S'], type: 'keyToKey', toKeys: ['c', 'c'] },
- { keys: ['Home'], type: 'keyToKey', toKeys: ['0'] },
- { keys: ['End'], type: 'keyToKey', toKeys: ['$'] },
- { keys: ['PageUp'], type: 'keyToKey', toKeys: ['Ctrl-b'] },
- { keys: ['PageDown'], type: 'keyToKey', toKeys: ['Ctrl-f'] },
- // Motions
- { 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: ['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: ['g', 'e'], type: 'motion',
- motion: 'moveByWords',
- motionArgs: { forward: false, wordEnd: true, inclusive: true }},
- { keys: ['g', 'E'], type: 'motion',
- motion: 'moveByWords',
- motionArgs: { forward: false, wordEnd: true, bigWord: true,
- inclusive: true }},
- { keys: ['Ctrl-f'], type: 'motion',
- motion: 'moveByPage', motionArgs: { forward: true }},
- { keys: ['Ctrl-b'], type: 'motion',
- motion: 'moveByPage', motionArgs: { forward: false }},
- { keys: ['g', 'g'], type: 'motion',
- motion: 'moveToLineOrEdgeOfDocument',
- motionArgs: { forward: false, explicitRepeat: true, linewise: true }},
- { keys: ['G'], type: 'motion',
- motion: 'moveToLineOrEdgeOfDocument',
- motionArgs: { forward: true, explicitRepeat: true, linewise: true }},
- { keys: ['0'], type: 'motion', motion: 'moveToStartOfLine' },
- { keys: ['^'], type: 'motion',
- motion: 'moveToFirstNonWhiteSpaceCharacter' },
- { keys: ['$'], type: 'motion',
- motion: 'moveToEol',
- motionArgs: { inclusive: true }},
- { keys: ['%'], type: 'motion',
- motion: 'moveToMatchedSymbol',
- motionArgs: { inclusive: 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: ['\'', 'character'], type: 'motion', motion: 'goToMark' },
- { keys: ['`', 'character'], type: 'motion', motion: 'goToMark' },
- { keys: ['|'], type: 'motion',
- motion: 'moveToColumn',
- motionArgs: { }},
- // Operators
- { keys: ['d'], type: 'operator', operator: 'delete' },
- { keys: ['y'], type: 'operator', operator: 'yank' },
- { keys: ['c'], type: 'operator', operator: 'change',
- operatorArgs: { enterInsertMode: true } },
- { keys: ['>'], type: 'operator', operator: 'indent',
- operatorArgs: { indentRight: true }},
- { keys: ['<'], type: 'operator', operator: 'indent',
- operatorArgs: { indentRight: false }},
- { keys: ['g', '~'], type: 'operator', operator: 'swapcase' },
- { keys: ['n'], type: 'motion', motion: 'findNext' },
- { keys: ['N'], type: 'motion', motion: 'findPrev' },
- // 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 },
- operatorMotionArgs: { visualLine: true }},
- { keys: ['Y'], type: 'operatorMotion', operator: 'yank',
- motion: 'moveToEol', motionArgs: { inclusive: true },
- operatorMotionArgs: { visualLine: true }},
- { keys: ['C'], type: 'operatorMotion',
- operator: 'change', operatorArgs: { enterInsertMode: true },
- motion: 'moveToEol', motionArgs: { inclusive: true },
- operatorMotionArgs: { visualLine: true }},
- { keys: ['~'], type: 'operatorMotion', operator: 'swapcase',
- motion: 'moveByCharacters', motionArgs: { forward: true }},
- // Actions
- { keys: ['a'], type: 'action', action: 'enterInsertMode',
- actionArgs: { insertAt: 'charAfter' }},
- { keys: ['A'], type: 'action', action: 'enterInsertMode',
- actionArgs: { insertAt: 'eol' }},
- { keys: ['i'], type: 'action', action: 'enterInsertMode' },
- { keys: ['I'], type: 'action', action: 'enterInsertMode',
- motion: 'moveToFirstNonWhiteSpaceCharacter' },
- { keys: ['o'], type: 'action', action: 'newLineAndEnterInsertMode',
- actionArgs: { after: true }},
- { keys: ['O'], type: 'action', action: 'newLineAndEnterInsertMode',
- actionArgs: { after: false }},
- { keys: ['v'], type: 'action', action: 'toggleVisualMode' },
- { keys: ['V'], type: 'action', action: 'toggleVisualMode',
- actionArgs: { linewise: true }},
- { keys: ['J'], type: 'action', action: 'joinLines' },
- { keys: ['p'], type: 'action', action: 'paste',
- actionArgs: { after: true }},
- { keys: ['P'], type: 'action', action: 'paste',
- actionArgs: { after: false }},
- { keys: ['r', 'character'], type: 'action', action: 'replace' },
- { keys: ['u'], type: 'action', action: 'undo' },
- { keys: ['Ctrl-r'], type: 'action', action: 'redo' },
- { keys: ['m', 'character'], type: 'action', action: 'setMark' },
- { keys: ['\"', 'character'], type: 'action', action: 'setRegister' },
- { keys: [',', '/'], type: 'action', action: 'clearSearchHighlight' },
- // 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' }},
- { keys: ['?'], type: 'search',
- searchArgs: { forward: false, querySrc: 'prompt' }},
- { keys: ['*'], type: 'search',
- searchArgs: { forward: true, querySrc: 'wordUnderCursor' }},
- { keys: ['#'], type: 'search',
- searchArgs: { forward: false, querySrc: 'wordUnderCursor' }},
- // Ex command
- { keys: [':'], type: 'ex' }
- ];
- var Vim = function() {
- var alphabetRegex = /[A-Za-z]/;
- var numberRegex = /[\d]/;
- var whiteSpaceRegex = /\s/;
- var wordRegexp = [(/\w/), (/[^\w\s]/)], bigWordRegexp = [(/\S/)];
- 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 SPECIAL_SYMBOLS = '~`!@#$%^&*()_-+=[{}]\\|/?.,<>:;\"\'';
- var specialSymbols = SPECIAL_SYMBOLS.split('');
- var specialKeys = ['Left', 'Right', 'Up', 'Down', 'Space', 'Backspace',
- 'Esc', 'Home', 'End', 'PageUp', 'PageDown'];
- var validMarks = upperCaseAlphabet.concat(lowerCaseAlphabet).concat(
- numbers);
- var validRegisters = upperCaseAlphabet.concat(lowerCaseAlphabet).concat(
- numbers).concat('-\"'.split(''));
- function isAlphabet(k) {
- return alphabetRegex.test(k);
- }
- function isLine(cm, line) {
- return line >= 0 && line < cm.lineCount();
- }
- 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 (/^[A-Z]$/).test(k);
- }
- function isAlphanumeric(k) {
- return (/^[\w]$/).test(k);
- }
- function isWhiteSpace(k) {
- return whiteSpaceRegex.test(k);
- }
- function isWhiteSpaceString(k) {
- return (/^\s*$/).test(k);
- }
- function inRangeInclusive(x, start, end) {
- return x >= start && x <= end;
- }
- function inArray(val, arr) {
- for (var i = 0; i < arr.length; i++) {
- if (arr[i] == val) {
- return true;
- }
- }
- return false;
- }
- // Global Vim state. Call getVimGlobalState to get and initialize.
- var vimGlobalState;
- function getVimGlobalState() {
- if (!vimGlobalState) {
- vimGlobalState = {
- // The current search query.
- searchQuery: null,
- // Whether we are searching backwards.
- searchIsReversed: false,
- registerController: new RegisterController({})
- };
- }
- return vimGlobalState;
- }
- function getVimState(cm) {
- if (!cm.vimState) {
- // Store instance state in the CodeMirror object.
- cm.vimState = {
- inputState: new InputState(),
- // 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,
- // The last motion command run. Cleared if a non-motion command gets
- // executed in between.
- lastMotion: null,
- marks: {},
- visualMode: false,
- // If we are in visual line mode. No effect if visualMode is false.
- visualLine: false
- };
- }
- return cm.vimState;
- }
- var vimApi= {
- buildKeyMap: function() {
- // TODO: Convert keymap into dictionary format for fast lookup.
- },
- // Testing hook, though it might be useful to expose the register
- // controller anyways.
- getRegisterController: function() {
- return getVimGlobalState().registerController;
- },
- // Testing hook.
- clearVimGlobalState_: function() {
- vimGlobalState = null;
- },
- map: function(lhs, rhs) {
- // Add user defined key bindings.
- exCommandDispatcher.map(lhs, rhs);
- },
- // Initializes vim state variable on the CodeMirror object. Should only be
- // called lazily by handleKey or for testing.
- maybeInitState: function(cm) {
- getVimState(cm);
- },
- // This is the outermost function called by CodeMirror, after keys have
- // been mapped to their Vim equivalents.
- handleKey: function(cm, key) {
- var command;
- var vim = getVimState(cm);
- if (key == 'Esc') {
- // Clear input state and get back to normal mode.
- vim.inputState.reset();
- if (vim.visualMode) {
- exitVisualMode(cm, vim);
- }
- return;
- }
- if (vim.visualMode &&
- cursorEqual(cm.getCursor('head'), cm.getCursor('anchor'))) {
- // The selection was cleared. Exit visual mode.
- exitVisualMode(cm, vim);
- }
- if (!vim.visualMode &&
- !cursorEqual(cm.getCursor('head'), cm.getCursor('anchor'))) {
- vim.visualMode = true;
- vim.visualLine = false;
- }
- if (key != '0' || (key == '0' && vim.inputState.getRepeat() === 0)) {
- // Have to special case 0 since it's both a motion and a number.
- command = commandDispatcher.matchCommand(key, defaultKeymap, vim);
- }
- if (!command) {
- if (isNumber(key)) {
- // Increment count unless count is 0 and key is 0.
- vim.inputState.pushRepeatDigit(key);
- }
- return;
- }
- if (command.type == 'keyToKey') {
- // TODO: prevent infinite recursion.
- for (var i = 0; i < command.toKeys.length; i++) {
- this.handleKey(cm, command.toKeys[i]);
- }
- } else {
- commandDispatcher.processCommand(cm, vim, command);
- }
- }
- };
- // Represents the current input state.
- function InputState() {
- this.reset();
- }
- InputState.prototype.reset = function() {
- 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 unamed 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;
- };
- /*
- * 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) {
- this.clear();
- if (text) {
- this.set(text, linewise);
- }
- }
- Register.prototype = {
- set: function(text, linewise) {
- this.text = text;
- this.linewise = !!linewise;
- },
- append: function(text, linewise) {
- // if this register has ever been set to linewise, use linewise.
- if (linewise || this.linewise) {
- this.text += '\n' + text;
- this.linewise = true;
- } else {
- this.text += text;
- }
- },
- clear: function() {
- this.text = '';
- this.linewise = false;
- },
- toString: function() { return this.text; }
- };
- /*
- * vim registers allow you to keep many independent copy and paste buffers.
- * See http://usevim.com/2012/04/13/registers/ for an introduction.
- *
- * RegisterController keeps the state of all the registers. An initial
- * state may be passed in. The unnamed register '"' will always be
- * overridden.
- */
- function RegisterController(registers) {
- this.registers = registers;
- this.unamedRegister = registers['\"'] = new Register();
- }
- RegisterController.prototype = {
- pushText: function(registerName, operator, text, linewise) {
- // Lowercase and uppercase registers refer to the same register.
- // Uppercase just means append.
- var register = this.isValidRegister(registerName) ?
- this.getRegister(registerName) : null;
- // if no register/an invalid register was specified, things go to the
- // default registers
- if (!register) {
- switch (operator) {
- case 'yank':
- // The 0 register contains the text from the most recent yank.
- this.registers['0'] = new Register(text, linewise);
- break;
- case 'delete':
- case 'change':
- if (text.indexOf('\n') == -1) {
- // Delete less than 1 line. Update the small delete register.
- this.registers['-'] = new Register(text, linewise);
- } else {
- // Shift down the contents of the numbered registers and put the
- // deleted text into register 1.
- this.shiftNumericRegisters_();
- this.registers['1'] = new Register(text, linewise);
- }
- break;
- }
- // Make sure the unnamed register is set to what just happened
- this.unamedRegister.set(text, linewise);
- return;
- }
- // If we've gotten to this point, we've actually specified a register
- var append = isUpperCase(registerName);
- if (append) {
- register.append(text, linewise);
- // The unamed register always has the same value as the last used
- // register.
- this.unamedRegister.append(text, linewise);
- } else {
- register.set(text, linewise);
- this.unamedRegister.set(text, linewise);
- }
- },
- // Gets the register named @name. If one of @name doesn't already exist,
- // create it. If @name is invalid, return the unamedRegister.
- getRegister: function(name) {
- if (!this.isValidRegister(name)) {
- return this.unamedRegister;
- }
- name = name.toLowerCase();
- if (!this.registers[name]) {
- this.registers[name] = new Register();
- }
- return this.registers[name];
- },
- isValidRegister: function(name) {
- return name && inArray(name, validRegisters);
- },
- shiftNumericRegisters_: function() {
- for (var i = 9; i >= 2; i--) {
- this.registers[i] = this.getRegister('' + (i - 1));
- }
- }
- };
- var commandDispatcher = {
- matchCommand: function(key, keyMap, vim) {
- var inputState = vim.inputState;
- var keys = inputState.keyBuffer.concat(key);
- for (var i = 0; i < keyMap.length; i++) {
- var command = keyMap[i];
- if (matchKeysPartial(keys, command.keys)) {
- if (keys.length < command.keys.length) {
- // Matches part of a multi-key command. Buffer and wait for next
- // stroke.
- inputState.keyBuffer.push(key);
- return null;
- } else {
- if (inputState.operator && command.type == 'action') {
- // Ignore matched action commands after an operator. Operators
- // only operate on motions. This check is really for text
- // objects since aW, a[ etcs conflicts with a.
- continue;
- }
- // Matches whole comand. Return the command.
- if (command.keys[keys.length - 1] == 'character') {
- inputState.selectedCharacter = keys[keys.length - 1];
- }
- inputState.keyBuffer = [];
- return command;
- }
- }
- }
- // Clear the buffer since there are no partial matches.
- inputState.keyBuffer = [];
- return null;
- },
- processCommand: function(cm, vim, command) {
- switch (command.type) {
- case 'motion':
- this.processMotion(cm, vim, command);
- break;
- case 'operator':
- this.processOperator(cm, vim, command);
- break;
- case 'operatorMotion':
- this.processOperatorMotion(cm, vim, command);
- break;
- case 'action':
- this.processAction(cm, vim, command);
- break;
- case 'search':
- this.processSearch(cm, vim, command);
- break;
- case 'ex':
- case 'keyToEx':
- this.processEx(cm, vim, command);
- break;
- default:
- break;
- }
- },
- processMotion: function(cm, vim, command) {
- vim.inputState.motion = command.motion;
- vim.inputState.motionArgs = copyArgs(command.motionArgs);
- this.evalInput(cm, vim);
- },
- processOperator: function(cm, vim, command) {
- var inputState = vim.inputState;
- if (inputState.operator) {
- if (inputState.operator == command.operator) {
- // Typing an operator twice like 'dd' makes the operator operate
- // linewise
- inputState.motion = 'expandToLine';
- inputState.motionArgs = { linewise: true };
- this.evalInput(cm, vim);
- return;
- } else {
- // 2 different operators in a row doesn't make sense.
- inputState.reset();
- }
- }
- inputState.operator = command.operator;
- inputState.operatorArgs = copyArgs(command.operatorArgs);
- if (vim.visualMode) {
- // Operating on a selection in visual mode. We don't need a motion.
- this.evalInput(cm, vim);
- }
- },
- processOperatorMotion: function(cm, vim, command) {
- var visualMode = vim.visualMode;
- var operatorMotionArgs = copyArgs(command.operatorMotionArgs);
- if (operatorMotionArgs) {
- // Operator motions may have special behavior in visual mode.
- if (visualMode && operatorMotionArgs.visualLine) {
- vim.visualLine = true;
- }
- }
- this.processOperator(cm, vim, command);
- if (!visualMode) {
- this.processMotion(cm, vim, command);
- }
- },
- processAction: function(cm, vim, command) {
- var inputState = vim.inputState;
- var repeat = inputState.getRepeat();
- var repeatIsExplicit = !!repeat;
- var actionArgs = copyArgs(command.actionArgs) || {};
- if (inputState.selectedCharacter) {
- actionArgs.selectedCharacter = inputState.selectedCharacter;
- }
- // Actions may or may not have motions and operators. Do these first.
- if (command.operator) {
- this.processOperator(cm, vim, command);
- }
- if (command.motion) {
- this.processMotion(cm, vim, command);
- }
- if (command.motion || command.operator) {
- this.evalInput(cm, vim);
- }
- actionArgs.repeat = repeat || 1;
- actionArgs.repeatIsExplicit = repeatIsExplicit;
- actionArgs.registerName = inputState.registerName;
- inputState.reset();
- vim.lastMotion = null,
- actions[command.action](cm, actionArgs, vim);
- },
- processSearch: function(cm, vim, command) {
- if (!cm.getSearchCursor) {
- // Search depends on SearchCursor.
- return;
- }
- var forward = command.searchArgs.forward;
- getSearchState(cm).setReversed(!forward);
- var promptPrefix = (forward) ? '/' : '?';
- function handleQuery(query, ignoreCase, smartCase) {
- updateSearchQuery(cm, query, ignoreCase, smartCase);
- commandDispatcher.processMotion(cm, vim, {
- type: 'motion',
- motion: 'findNext'
- });
- }
- function onPromptClose(query) {
- handleQuery(query, true /** ignoreCase */, true /** smartCase */);
- }
- switch (command.searchArgs.querySrc) {
- case 'prompt':
- showPrompt(cm, onPromptClose, promptPrefix, searchPromptDesc);
- break;
- case 'wordUnderCursor':
- var word = expandWordUnderCursor(cm, false /** inclusive */,
- true /** forward */, false /** bigWord */,
- true /** noSymbol */);
- var isKeyword = true;
- if (!word) {
- word = expandWordUnderCursor(cm, false /** inclusive */,
- true /** forward */, false /** bigWord */,
- false /** noSymbol */);
- isKeyword = false;
- }
- if (!word) {
- return;
- }
- var query = cm.getLine(word.start.line).substring(word.start.ch,
- word.end.ch + 1);
- if (isKeyword) {
- query = '\\b' + query + '\\b';
- } else {
- query = escapeRegex(query);
- }
- cm.setCursor(word.start);
- handleQuery(query, true /** ignoreCase */, false /** smartCase */);
- break;
- }
- },
- processEx: function(cm, vim, command) {
- function onPromptClose(input) {
- exCommandDispatcher.processCommand(cm, input);
- }
- if (command.type == 'keyToEx') {
- // Handle user defined Ex to Ex mappings
- exCommandDispatcher.processCommand(cm, command.exArgs.input);
- } else {
- showPrompt(cm, onPromptClose, ':');
- }
- },
- evalInput: function(cm, vim) {
- // If the motion comand is set, execute both the operator and motion.
- // Otherwise return.
- var inputState = vim.inputState;
- var motion = inputState.motion;
- var motionArgs = inputState.motionArgs || {};
- var operator = inputState.operator;
- var operatorArgs = inputState.operatorArgs || {};
- var registerName = inputState.registerName;
- var selectionEnd = cm.getCursor('head');
- var selectionStart = cm.getCursor('anchor');
- // The difference between cur and selection cursors are that cur is
- // being operated on and ignores that there is a selection.
- var curStart = copyCursor(selectionEnd);
- var curOriginal = copyCursor(curStart);
- var curEnd;
- var repeat;
- if (motionArgs.repeat !== undefined) {
- // If motionArgs specifies a repeat, that takes precedence over the
- // input state's repeat. Used by Ex mode and can be user defined.
- repeat = inputState.motionArgs.repeat;
- } else {
- repeat = inputState.getRepeat();
- }
- if (repeat > 0 && motionArgs.explicitRepeat) {
- motionArgs.repeatIsExplicit = true;
- } else if (motionArgs.noRepeat ||
- (!motionArgs.explicitRepeat && repeat === 0)) {
- repeat = 1;
- motionArgs.repeatIsExplicit = false;
- }
- if (inputState.selectedCharacter) {
- // If there is a character input, stick it in all of the arg arrays.
- motionArgs.selectedCharacter = operatorArgs.selectedCharacter =
- inputState.selectedCharacter;
- }
- motionArgs.repeat = repeat;
- inputState.reset();
- if (motion) {
- var motionResult = motions[motion](cm, motionArgs, vim);
- vim.lastMotion = motions[motion];
- if (!motionResult) {
- return;
- }
- if (motionResult instanceof Array) {
- curStart = motionResult[0];
- curEnd = motionResult[1];
- } else {
- curEnd = motionResult;
- }
- // TODO: Handle null returns from motion commands better.
- if (!curEnd) {
- curEnd = { ch: curStart.ch, line: curStart.line };
- }
- if (vim.visualMode) {
- // Check if the selection crossed over itself. Will need to shift
- // the start point if that happened.
- if (cursorIsBefore(selectionStart, selectionEnd) &&
- (cursorEqual(selectionStart, curEnd) ||
- cursorIsBefore(curEnd, selectionStart))) {
- // The end of the selection has moved from after the start to
- // before the start. We will shift the start right by 1.
- selectionStart.ch += 1;
- } else if (cursorIsBefore(selectionEnd, selectionStart) &&
- (cursorEqual(selectionStart, curEnd) ||
- cursorIsBefore(selectionStart, curEnd))) {
- // The opposite happened. We will shift the start left by 1.
- selectionStart.ch -= 1;
- }
- selectionEnd = curEnd;
- if (vim.visualLine) {
- if (cursorIsBefore(selectionStart, selectionEnd)) {
- selectionStart.ch = 0;
- selectionEnd.ch = lineLength(cm, selectionEnd.line);
- } else {
- selectionEnd.ch = 0;
- selectionStart.ch = lineLength(cm, selectionStart.line);
- }
- }
- // Need to set the cursor to clear the selection. Otherwise,
- // CodeMirror can't figure out that we changed directions...
- cm.setCursor(selectionStart);
- cm.setSelection(selectionStart, selectionEnd);
- } else if (!operator) {
- curEnd = clipCursorToContent(cm, curEnd);
- cm.setCursor(curEnd.line, curEnd.ch);
- }
- }
- if (operator) {
- var inverted = false;
- vim.lastMotion = null;
- operatorArgs.repeat = repeat; // Indent in visual mode needs this.
- if (vim.visualMode) {
- curStart = selectionStart;
- curEnd = selectionEnd;
- motionArgs.inclusive = true;
- }
- // Swap start and end if motion was backward.
- if (cursorIsBefore(curEnd, curStart)) {
- var tmp = curStart;
- curStart = curEnd;
- curEnd = tmp;
- inverted = true;
- }
- if (motionArgs.inclusive && !(vim.visualMode && inverted)) {
- // Move the selection end one to the right to include the last
- // character.
- curEnd.ch++;
- }
- var linewise = motionArgs.linewise ||
- (vim.visualMode && vim.visualLine);
- if (linewise) {
- // Expand selection to entire line.
- expandSelectionToLine(cm, curStart, curEnd);
- } else if (motionArgs.forward) {
- // Clip to trailing newlines only if we the motion goes forward.
- clipToLine(cm, curStart, curEnd);
- }
- operatorArgs.registerName = registerName;
- // Keep track of linewise as it affects how paste and change behave.
- operatorArgs.linewise = linewise;
- operators[operator](cm, operatorArgs, vim, curStart,
- curEnd, curOriginal);
- if (vim.visualMode) {
- exitVisualMode(cm, vim);
- }
- if (operatorArgs.enterInsertMode) {
- actions.enterInsertMode(cm);
- }
- }
- }
- };
- /**
- * typedef {Object{line:number,ch:number}} Cursor An object containing the
- * position of the cursor.
- */
- // All of the functions below return Cursor objects.
- var motions = {
- expandToLine: function(cm, motionArgs) {
- // Expands forward to end of line, and then to next line if repeat is
- // >1. Does not handle backward motion!
- var cur = cm.getCursor();
- return { line: cur.line + motionArgs.repeat - 1, ch: Infinity };
- },
- findNext: function(cm, motionArgs, vim) {
- return findNext(cm, false /** prev */, motionArgs.repeat);
- },
- findPrev: function(cm, motionArgs, vim) {
- return findNext(cm, true /** prev */, motionArgs.repeat);
- },
- goToMark: function(cm, motionArgs, vim) {
- var mark = vim.marks[motionArgs.selectedCharacter];
- if (mark) {
- return mark.find();
- }
- return null;
- },
- moveByCharacters: function(cm, motionArgs) {
- var cur = cm.getCursor();
- var repeat = motionArgs.repeat;
- var ch = motionArgs.forward ? cur.ch + repeat : cur.ch - repeat;
- return { line: cur.line, ch: ch };
- },
- moveByLines: function(cm, motionArgs, vim) {
- var endCh = cm.getCursor().ch;
- // Depending what our last motion was, we may want to do different
- // things. If our last motion was moving vertically, we want to
- // preserve the HPos from our last horizontal move. If our last motion
- // was going to the end of a line, moving vertically we should go to
- // the end of the line, etc.
- switch (vim.lastMotion) {
- case this.moveByLines:
- case this.moveToColumn:
- case this.moveToEol:
- endCh = vim.lastHPos;
- break;
- default:
- vim.lastHPos = endCh;
- }
- var cur = cm.getCursor();
- var repeat = motionArgs.repeat;
- var line = motionArgs.forward ? cur.line + repeat : cur.line - repeat;
- if (line < 0 || line > cm.lineCount() - 1) {
- return null;
- }
- return { line: line, ch: endCh };
- },
- moveByPage: function(cm, motionArgs) {
- // CodeMirror only exposes functions that move the cursor page down, so
- // doing this bad hack to move the cursor and move it back. evalInput
- // will move the cursor to where it should be in the end.
- var curStart = cm.getCursor();
- var repeat = motionArgs.repeat;
- cm.moveV((motionArgs.forward ? repeat : -repeat), 'page');
- var curEnd = cm.getCursor();
- cm.setCursor(curStart);
- return curEnd;
- },
- moveByWords: function(cm, motionArgs) {
- return moveToWord(cm, motionArgs.repeat, !!motionArgs.forward,
- !!motionArgs.wordEnd, !!motionArgs.bigWord);
- },
- moveTillCharacter: function(cm, motionArgs) {
- var repeat = motionArgs.repeat;
- var curEnd = moveToCharacter(cm, repeat, motionArgs.forward,
- motionArgs.selectedCharacter);
- var increment = motionArgs.forward ? -1 : 1;
- curEnd.ch += increment;
- return curEnd;
- },
- moveToCharacter: function(cm, motionArgs) {
- var repeat = motionArgs.repeat;
- return moveToCharacter(cm, repeat, motionArgs.forward,
- motionArgs.selectedCharacter);
- },
- moveToColumn: function(cm, motionArgs, vim) {
- var repeat = motionArgs.repeat;
- // repeat is equivalent to which column we want to move to!
- vim.lastHPos = repeat - 1;
- return moveToColumn(cm, repeat);
- },
- moveToEol: function(cm, motionArgs, vim) {
- var cur = cm.getCursor();
- vim.lastHPos = Infinity;
- return { line: cur.line + motionArgs.repeat - 1, ch: Infinity };
- },
- moveToFirstNonWhiteSpaceCharacter: function(cm) {
- // Go to the start of the line where the text begins, or the end for
- // whitespace-only lines
- var cursor = cm.getCursor();
- var line = cm.getLine(cursor.line);
- return { line: cursor.line,
- ch: findFirstNonWhiteSpaceCharacter(cm.getLine(cursor.line)) };
- },
- moveToMatchedSymbol: function(cm, motionArgs) {
- var cursor = cm.getCursor();
- var symbol = cm.getLine(cursor.line).charAt(cursor.ch);
- if (isMatchableSymbol(symbol)) {
- return findMatchedSymbol(cm, cm.getCursor(), motionArgs.symbol);
- } else {
- return cursor;
- }
- },
- moveToStartOfLine: function(cm) {
- var cursor = cm.getCursor();
- return { line: cursor.line, ch: 0 };
- },
- moveToLineOrEdgeOfDocument: function(cm, motionArgs) {
- var lineNum = motionArgs.forward ? cm.lineCount() - 1 : 0;
- if (motionArgs.repeatIsExplicit) {
- lineNum = motionArgs.repeat - 1;
- }
- return { line: lineNum,
- ch: findFirstNonWhiteSpaceCharacter(cm.getLine(lineNum)) };
- },
- textObjectManipulation: function(cm, motionArgs) {
- var character = motionArgs.selectedCharacter;
- // Inclusive is the difference between a and i
- // TODO: Instead of using the additional text object map to perform text
- // object operations, merge the map into the defaultKeyMap and use
- // motionArgs to define behavior. Define separate entries for 'aw',
- // 'iw', 'a[', 'i[', etc.
- var inclusive = !motionArgs.textObjectInner;
- if (!textObjects[character]) {
- // No text object defined for this, don't move.
- return null;
- }
- var tmp = textObjects[character](cm, inclusive);
- var start = tmp.start;
- var end = tmp.end;
- return [start, end];
- }
- };
- var operators = {
- change: function(cm, operatorArgs, vim, curStart, curEnd) {
- getVimGlobalState().registerController.pushText(
- operatorArgs.registerName, 'change', cm.getRange(curStart, curEnd),
- operatorArgs.linewise);
- if (operatorArgs.linewise) {
- // Delete starting at the first nonwhitespace character of the first
- // line, instead of from the start of the first line. This way we get
- // an indent when we get into insert mode. This behavior isn't quite
- // correct because we should treat this as a completely new line, and
- // indent should be whatever codemirror thinks is the right indent.
- // But cm.indentLine doesn't seem work on empty lines.
- // TODO: Fix the above.
- curStart.ch =
- findFirstNonWhiteSpaceCharacter(cm.getLine(curStart.line));
- // Insert an additional newline so that insert mode can start there.
- // curEnd should be on the first character of the new line.
- cm.replaceRange('\n', curStart, curEnd);
- } else {
- cm.replaceRange('', curStart, curEnd);
- }
- cm.setCursor(curStart);
- },
- // delete is a javascript keyword.
- 'delete': function(cm, operatorArgs, vim, curStart, curEnd) {
- getVimGlobalState().registerController.pushText(
- operatorArgs.registerName, 'delete', cm.getRange(curStart, curEnd),
- operatorArgs.linewise);
- cm.replaceRange('', curStart, curEnd);
- if (operatorArgs.linewise) {
- cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm));
- } else {
- cm.setCursor(curStart);
- }
- },
- indent: function(cm, operatorArgs, vim, curStart, curEnd) {
- var startLine = curStart.line;
- var endLine = curEnd.line;
- // In visual mode, n> shifts the selection right n times, instead of
- // shifting n lines right once.
- var repeat = (vim.visualMode) ? operatorArgs.repeat : 1;
- if (operatorArgs.linewise) {
- // The only way to delete a newline is to delete until the start of
- // the next line, so in linewise mode evalInput will include the next
- // line. We don't want this in indent, so we go back a line.
- endLine--;
- }
- for (var i = startLine; i <= endLine; i++) {
- for (var j = 0; j < repeat; j++) {
- cm.indentLine(i, operatorArgs.indentRight);
- }
- }
- cm.setCursor(curStart);
- cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm));
- },
- swapcase: function(cm, operatorArgs, vim, curStart, curEnd, curOriginal) {
- var toSwap = cm.getRange(curStart, curEnd);
- var swapped = '';
- for (var i = 0; i < toSwap.length; i++) {
- var character = toSwap.charAt(i);
- swapped += isUpperCase(character) ? character.toLowerCase() :
- character.toUpperCase();
- }
- cm.replaceRange(swapped, curStart, curEnd);
- cm.setCursor(curOriginal);
- },
- yank: function(cm, operatorArgs, vim, curStart, curEnd, curOriginal) {
- getVimGlobalState().registerController.pushText(
- operatorArgs.registerName, 'yank',
- cm.getRange(curStart, curEnd), operatorArgs.linewise);
- cm.setCursor(curOriginal);
- }
- };
- var actions = {
- clearSearchHighlight: clearSearchHighlight,
- enterInsertMode: function(cm, actionArgs) {
- var insertAt = (actionArgs) ? actionArgs.insertAt : null;
- if (insertAt == 'eol') {
- var cursor = cm.getCursor();
- cursor = { line: cursor.line, ch: lineLength(cm, cursor.line) };
- cm.setCursor(cursor);
- } else if (insertAt == 'charAfter') {
- cm.setCursor(offsetCursor(cm.getCursor(), 0, 1));
- }
- cm.setOption('keyMap', 'vim-insert');
- },
- toggleVisualMode: function(cm, actionArgs, vim) {
- var repeat = actionArgs.repeat;
- var curStart = cm.getCursor();
- var curEnd;
- // TODO: The repeat should actually select number of characters/lines
- // equal to the repeat times the size of the previous visual
- // operation.
- if (!vim.visualMode) {
- vim.visualMode = true;
- vim.visualLine = !!actionArgs.linewise;
- if (vim.visualLine) {
- curStart.ch = 0;
- curEnd = clipCursorToContent(cm, {
- line: curStart.line + repeat - 1,
- ch: lineLength(cm, curStart.line)
- }, true /** includeLineBreak */);
- } else {
- curEnd = clipCursorToContent(cm, {
- line: curStart.line,
- ch: curStart.ch + repeat
- }, true /** includeLineBreak */);
- }
- // Make the initial selection.
- if (!actionArgs.repeatIsExplicit && !vim.visualLine) {
- // This is a strange case. Here the implicit repeat is 1. The
- // following commands lets the cursor hover over the 1 character
- // selection.
- cm.setCursor(curEnd);
- cm.setSelection(curEnd, curStart);
- } else {
- cm.setSelection(curStart, curEnd);
- }
- } else {
- if (!vim.visualLine && actionArgs.linewise) {
- // Shift-V pressed in characterwise visual mode. Switch to linewise
- // visual mode instead of exiting visual mode.
- vim.visualLine = true;
- curStart = cm.getCursor('anchor');
- curEnd = cm.getCursor('head');
- curStart.ch = cursorIsBefore(curStart, curEnd) ? 0 :
- lineLength(cm, curStart.line);
- curEnd.ch = cursorIsBefore(curStart, curEnd) ?
- lineLength(cm, curEnd.line) : 0;
- cm.setSelection(curStart, curEnd);
- } else {
- exitVisualMode(cm, vim);
- }
- }
- },
- joinLines: function(cm, actionArgs, vim) {
- var curStart, curEnd;
- if (vim.visualMode) {
- curStart = cm.getCursor('anchor');
- curEnd = cm.getCursor('head');
- curEnd.ch = lineLength(cm, curEnd.line) - 1;
- } else {
- // Repeat is the number of lines to join. Minimum 2 lines.
- var repeat = Math.max(actionArgs.repeat, 2);
- curStart = cm.getCursor();
- curEnd = clipCursorToContent(cm, { line: curStart.line + repeat - 1,
- ch: Infinity });
- }
- var finalCh = 0;
- cm.operation(function() {
- for (var i = curStart.line; i < curEnd.line; i++) {
- finalCh = lineLength(cm, curStart.line);
- var tmp = { line: curStart.line + 1,
- ch: lineLength(cm, curStart.line + 1) };
- var text = cm.getRange(curStart, tmp);
- text = text.replace(/\n\s*/g, ' ');
- cm.replaceRange(text, curStart, tmp);
- }
- var curFinalPos = { line: curStart.line, ch: finalCh };
- cm.setCursor(curFinalPos);
- });
- },
- newLineAndEnterInsertMode: function(cm, actionArgs) {
- var insertAt = cm.getCursor();
- if (insertAt.line === 0 && !actionArgs.after) {
- // Special case for inserting newline before start of document.
- cm.replaceRange('\n', { line: 0, ch: 0 });
- cm.setCursor(0, 0);
- } else {
- insertAt.line = (actionArgs.after) ? insertAt.line :
- insertAt.line - 1;
- insertAt.ch = lineLength(cm, insertAt.line);
- cm.setCursor(insertAt);
- var newlineFn = CodeMirror.commands.newlineAndIndentContinueComment ||
- CodeMirror.commands.newlineAndIndent;
- newlineFn(cm);
- }
- this.enterInsertMode(cm);
- },
- paste: function(cm, actionArgs, vim) {
- var cur = cm.getCursor();
- var register = getVimGlobalState().registerController.getRegister(
- actionArgs.registerName);
- if (!register.text) {
- return;
- }
- for (var text = '', i = 0; i < actionArgs.repeat; i++) {
- text += register.text;
- }
- var linewise = register.linewise;
- if (linewise) {
- if (actionArgs.after) {
- // Move the newline at the end to the start instead, and paste just
- // before the newline character of the line we are on right now.
- text = '\n' + text.slice(0, text.length - 1);
- cur.ch = lineLength(cm, cur.line);
- } else {
- cur.ch = 0;
- }
- } else {
- cur.ch += actionArgs.after ? 1 : 0;
- }
- cm.replaceRange(text, cur);
- // Now fine tune the cursor to where we want it.
- var curPosFinal;
- var idx;
- if (linewise && actionArgs.after) {
- curPosFinal = { line: cur.line + 1,
- ch: findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line + 1)) };
- } else if (linewise && !actionArgs.after) {
- curPosFinal = { line: cur.line,
- ch: findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line)) };
- } else if (!linewise && actionArgs.after) {
- idx = cm.indexFromPos(cur);
- curPosFinal = cm.posFromIndex(idx + text.length - 1);
- } else {
- idx = cm.indexFromPos(cur);
- curPosFinal = cm.posFromIndex(idx + text.length);
- }
- cm.setCursor(curPosFinal);
- },
- undo: function(cm, actionArgs) {
- repeatFn(cm, CodeMirror.commands.undo, actionArgs.repeat)();
- },
- redo: function(cm, actionArgs) {
- repeatFn(cm, CodeMirror.commands.redo, actionArgs.repeat)();
- },
- setRegister: function(cm, actionArgs, vim) {
- vim.inputState.registerName = actionArgs.selectedCharacter;
- },
- setMark: function(cm, actionArgs, vim) {
- var markName = actionArgs.selectedCharacter;
- if (!inArray(markName, validMarks)) {
- return;
- }
- if (vim.marks[markName]) {
- vim.marks[markName].clear();
- }
- vim.marks[markName] = cm.setBookmark(cm.getCursor());
- },
- replace: function(cm, actionArgs) {
- var replaceWith = actionArgs.selectedCharacter;
- var curStart = cm.getCursor();
- var line = cm.getLine(curStart.line);
- var replaceTo = curStart.ch + actionArgs.repeat;
- if (replaceTo > line.length) {
- return;
- }
- var curEnd = { line: curStart.line, ch: replaceTo };
- var replaceWithStr = '';
- for (var i = 0; i < curEnd.ch - curStart.ch; i++) {
- replaceWithStr += replaceWith;
- }
- cm.replaceRange(replaceWithStr, curStart, curEnd);
- cm.setCursor(offsetCursor(curEnd, 0, -1));
- }
- };
- var textObjects = {
- // TODO: lots of possible exceptions that can be thrown here. Try da(
- // outside of a () block.
- // TODO: implement text objects for the reverse like }. Should just be
- // an additional mapping after moving to the defaultKeyMap.
- 'w': function(cm, inclusive) {
- return expandWordUnderCursor(cm, inclusive, true /** forward */,
- false /** bigWord */);
- },
- 'W': function(cm, inclusive) {
- return expandWordUnderCursor(cm, inclusive,
- true /** forward */, true /** bigWord */);
- },
- '{': function(cm, inclusive) {
- return selectCompanionObject(cm, '}', inclusive);
- },
- '(': function(cm, inclusive) {
- return selectCompanionObject(cm, ')', inclusive);
- },
- '[': function(cm, inclusive) {
- return selectCompanionObject(cm, ']', inclusive);
- },
- '\'': function(cm, inclusive) {
- return findBeginningAndEnd(cm, "'", inclusive);
- },
- '\"': function(cm, inclusive) {
- return findBeginningAndEnd(cm, '"', inclusive);
- }
- };
- /*
- * Below are miscellaneous utility functions used by vim.js
- */
- /**
- * Clips cursor to ensure that:
- * 0 <= cur.ch < lineLength
- * AND
- * 0 <= cur.line < lineCount
- * If includeLineBreak is true, then allow cur.ch == lineLength.
- */
- function clipCursorToContent(cm, cur, includeLineBreak) {
- var line = Math.min(Math.max(0, cur.line), cm.lineCount() - 1);
- var maxCh = lineLength(cm, line) - 1;
- maxCh = (includeLineBreak) ? maxCh + 1 : maxCh;
- var ch = Math.min(Math.max(0, cur.ch), maxCh);
- return { line: line, ch: ch };
- }
- // Merge arguments in place, for overriding arguments.
- function mergeArgs(to, from) {
- for (var prop in from) {
- if (from.hasOwnProperty(prop)) {
- to[prop] = from[prop];
- }
- }
- }
- function copyArgs(args) {
- var ret = {};
- for (var prop in args) {
- if (args.hasOwnProperty(prop)) {
- ret[prop] = args[prop];
- }
- }
- return ret;
- }
- function offsetCursor(cur, offsetLine, offsetCh) {
- return { line: cur.line + offsetLine, ch: cur.ch + offsetCh };
- }
- function arrayEq(a1, a2) {
- if (a1.length != a2.length) {
- return false;
- }
- for (var i = 0; i < a1.length; i++) {
- if (a1[i] != a2[i]) {
- return false;
- }
- }
- return true;
- }
- function matchKeysPartial(pressed, mapped) {
- for (var i = 0; i < pressed.length; i++) {
- // 'character' means any character. For mark, register commads, etc.
- if (pressed[i] != mapped[i] && mapped[i] != 'character') {
- return false;
- }
- }
- return true;
- }
- function arrayIsSubsetFromBeginning(small, big) {
- for (var i = 0; i < small.length; i++) {
- if (small[i] != big[i]) {
- return false;
- }
- }
- return true;
- }
- function repeatFn(cm, fn, repeat) {
- return function() {
- for (var i = 0; i < repeat; i++) {
- fn(cm);
- }
- };
- }
- function copyCursor(cur) {
- return { line: cur.line, ch: cur.ch };
- }
- function cursorEqual(cur1, cur2) {
- return cur1.ch == cur2.ch && cur1.line == cur2.line;
- }
- function cursorIsBefore(cur1, cur2) {
- if (cur1.line < cur2.line) {
- return true;
- } else if (cur1.line == cur2.line && cur1.ch < cur2.ch) {
- return true;
- }
- return false;
- }
- function lineLength(cm, lineNum) {
- return cm.getLine(lineNum).length;
- }
- function reverse(s){
- return s.split("").reverse().join("");
- }
- function trim(s) {
- if (s.trim) {
- return s.trim();
- } else {
- return s.replace(/^\s+|\s+$/g, '');
- }
- }
- function escapeRegex(s) {
- return s.replace(/([.?*+$\[\]\/\\(){}|\-])/g, "\\$1");
- }
- function exitVisualMode(cm, vim) {
- vim.visualMode = false;
- vim.visualLine = false;
- var selectionStart = cm.getCursor('anchor');
- var selectionEnd = cm.getCursor('head');
- if (!cursorEqual(selectionStart, selectionEnd)) {
- // Clear the selection and set the cursor only if the selection has not
- // already been cleared. Otherwise we risk moving the cursor somewhere
- // it's not supposed to be.
- cm.setCursor(clipCursorToContent(cm, selectionEnd));
- }
- }
- // Remove any trailing newlines from the selection. For
- // example, with the caret at the start of the last word on the line,
- // 'dw' should word, but not the newline, while 'w' should advance the
- // caret to the first character of the next line.
- function clipToLine(cm, curStart, curEnd) {
- var selection = cm.getRange(curStart, curEnd);
- var lines = selection.split('\n');
- if (lines.length > 1 && isWhiteSpaceString(lines.pop())) {
- curEnd.line--;
- curEnd.ch = lineLength(cm, curEnd.line);
- }
- }
- // Expand the selection to line ends.
- function expandSelectionToLine(cm, curStart, curEnd) {
- curStart.ch = 0;
- curEnd.ch = 0;
- curEnd.line++;
- }
- function findFirstNonWhiteSpaceCharacter(text) {
- if (!text) {
- return 0;
- }
- var firstNonWS = text.search(/\S/);
- return firstNonWS == -1 ? text.length : firstNonWS;
- }
- function expandWordUnderCursor(cm, inclusive, forward, bigWord, noSymbol) {
- var cur = cm.getCursor();
- var line = cm.getLine(cur.line);
- var idx = cur.ch;
- // Seek to first word or non-whitespace character, depending on if
- // noSymbol is true.
- var textAfterIdx = line.substring(idx);
- var firstMatchedChar;
- if (noSymbol) {
- firstMatchedChar = textAfterIdx.search(/\w/);
- } else {
- firstMatchedChar = textAfterIdx.search(/\S/);
- }
- if (firstMatchedChar == -1) {
- return null;
- }
- idx += firstMatchedChar;
- textAfterIdx = line.substring(idx);
- var textBeforeIdx = line.substring(0, idx);
- var matchRegex;
- // Greedy matchers for the "word" we are trying to expand.
- if (bigWord) {
- matchRegex = /^\S+/;
- } else {
- if ((/\w/).test(line.charAt(idx))) {
- matchRegex = /^\w+/;
- } else {
- matchRegex = /^[^\w\s]+/;
- }
- }
- var wordAfterRegex = matchRegex.exec(textAfterIdx);
- var wordStart = idx;
- var wordEnd = idx + wordAfterRegex[0].length - 1;
- // TODO: Find a better way to do this. It will be slow on very long lines.
- var wordBeforeRegex = matchRegex.exec(reverse(textBeforeIdx));
- if (wordBeforeRegex) {
- wordStart -= wordBeforeRegex[0].length;
- }
- if (inclusive) {
- wordEnd++;
- }
- return { start: { line: cur.line, ch: wordStart },
- end: { line: cur.line, ch: wordEnd }};
- }
- /*
- * Returns the boundaries of the next word. If the cursor in the middle of
- * the word, then returns the boundaries of the current word, starting at
- * the cursor. If the cursor is at the start/end of a word, and we are going
- * forward/backward, respectively, find the boundaries of the next word.
- *
- * @param {CodeMirror} cm CodeMirror object.
- * @param {Cursor} cur The cursor position.
- * @param {boolean} forward True to search forward. False to search
- * backward.
- * @param {boolean} bigWord True if punctuation count as part of the word.
- * False if only [a-zA-Z0-9] characters count as part of the word.
- * @return {Object{from:number, to:number, line: number}} The boundaries of
- * the word, or null if there are no more words.
- */
- // TODO: Treat empty lines (with no whitespace) as words.
- function findWord(cm, cur, forward, bigWord) {
- var lineNum = cur.line;
- var pos = cur.ch;
- var line = cm.getLine(lineNum);
- var dir = forward ? 1 : -1;
- var regexps = bigWord ? bigWordRegexp : wordRegexp;
- while (true) {
- var stop = (dir > 0) ? line.length : -1;
- var wordStart = stop, wordEnd = stop;
- // Find bounds of next word.
- while (pos != stop) {
- var foundWord = false;
- for (var i = 0; i < regexps.length && !foundWord; ++i) {
- if (regexps[i].test(line.charAt(pos))) {
- wordStart = pos;
- // Advance to end of word.
- while (pos != stop && regexps[i].test(line.charAt(pos))) {
- pos += dir;
- }
- wordEnd = pos;
- foundWord = wordStart != wordEnd;
- if (wordStart == cur.ch && lineNum == cur.line &&
- wordEnd == wordStart + dir) {
- // We started at the end of a word. Find the next one.
- continue;
- } else {
- return {
- from: Math.min(wordStart, wordEnd + 1),
- to: Math.max(wordStart, wordEnd),
- line: lineNum };
- }
- }
- }
- if (!foundWord) {
- pos += dir;
- }
- }
- // Advance to next/prev line.
- lineNum += dir;
- if (!isLine(cm, lineNum)) {
- return null;
- }
- line = cm.getLine(lineNum);
- pos = (dir > 0) ? 0 : line.length;
- }
- // Should never get here.
- throw 'The impossible happened.';
- }
- /**
- * @param {CodeMirror} cm CodeMirror object.
- * @param {int} repeat Number of words to move past.
- * @param {boolean} forward True to search forward. False to search
- * backward.
- * @param {boolean} wordEnd True to move to end of word. False to move to
- * beginning of word.
- * @param {boolean} bigWord True if punctuation count as part of the word.
- * False if only alphabet characters count as part of the word.
- * @return {Cursor} The position the cursor should move to.
- */
- function moveToWord(cm, repeat, forward, wordEnd, bigWord) {
- var cur = cm.getCursor();
- for (var i = 0; i < repeat; i++) {
- var startCh = cur.ch, startLine = cur.line, word;
- var movedToNextWord = false;
- while (!movedToNextWord) {
- // Search and advance.
- word = findWord(cm, cur, forward, bigWord);
- movedToNextWord = true;
- if (word) {
- // Move to the word we just found. If by moving to the word we end
- // up in the same spot, then move an extra character and search
- // again.
- cur.line = word.line;
- if (forward && wordEnd) {
- // 'e'
- cur.ch = word.to - 1;
- } else if (forward && !wordEnd) {
- // 'w'
- if (inRangeInclusive(cur.ch, word.from, word.to) &&
- word.line == startLine) {
- // Still on the same word. Go to the next one.
- movedToNextWord = false;
- cur.ch = word.to - 1;
- } else {
- cur.ch = word.from;
- }
- } else if (!forward && wordEnd) {
- // 'ge'
- if (inRangeInclusive(cur.ch, word.from, word.to) &&
- word.line == startLine) {
- // still on the same word. Go to the next one.
- movedToNextWord = false;
- cur.ch = word.from;
- } else {
- cur.ch = word.to;
- }
- } else if (!forward && !wordEnd) {
- // 'b'
- cur.ch = word.from;
- }
- } else {
- // No more words to be found. Move to the end.
- if (forward) {
- return { line: cur.line, ch: lineLength(cm, cur.line) };
- } else {
- return { line: cur.line, ch: 0 };
- }
- }
- }
- }
- return cur;
- }
- function moveToCharacter(cm, repeat, forward, character) {
- var cur = cm.getCursor();
- var start = cur.ch;
- var idx;
- for (var i = 0; i < repeat; i ++) {
- var line = cm.getLine(cur.line);
- idx = charIdxInLine(start, line, character, forward, true);
- if (idx == -1) {
- return cur;
- }
- start = idx;
- }
- return { line: cm.getCursor().line, ch: idx };
- }
- function moveToColumn(cm, repeat) {
- // repeat is always >= 1, so repeat - 1 always corresponds
- // to the column we want to go to.
- var line = cm.getCursor().line;
- return clipCursorToContent(cm, { line: line, ch: repeat - 1 });
- }
- function charIdxInLine(start, line, character, forward, includeChar) {
- // Search for char in line.
- // motion_options: {forward, includeChar}
- // If includeChar = true, include it too.
- // If forward = true, search forward, else search backwards.
- // If char is not found on this line, do nothing
- var idx;
- if (forward) {
- idx = line.indexOf(character, start + 1);
- if (idx != -1 && !includeChar) {
- idx -= 1;
- }
- } else {
- idx = line.lastIndexOf(character, start - 1);
- if (idx != -1 && !includeChar) {
- idx += 1;
- }
- }
- return idx;
- }
- function findMatchedSymbol(cm, cur, symb) {
- var line = cur.line;
- symb = symb ? symb : cm.getLine(line).charAt(cur.ch);
- // Are we at the opening or closing char
- var forwards = inArray(symb, ['(', '[', '{']);
- var reverseSymb = ({
- '(': ')', ')': '(',
- '[': ']', ']': '[',
- '{': '}', '}': '{'})[symb];
- // Couldn't find a matching symbol, abort
- if (!reverseSymb) {
- return cur;
- }
- // set our increment to move forward (+1) or backwards (-1)
- // depending on which bracket we're matching
- var increment = ({'(': 1, '{': 1, '[': 1})[symb] || -1;
- var depth = 1, nextCh = symb, index = cur.ch, lineText = cm.getLine(line);
- // Simple search for closing paren--just count openings and closings till
- // we find our match
- // TODO: use info from CodeMirror to ignore closing brackets in comments
- // and quotes, etc.
- while (nextCh && depth > 0) {
- index += increment;
- nextCh = lineText.charAt(index);
- if (!nextCh) {
- line += increment;
- index = 0;
- lineText = cm.getLine(line) || '';
- nextCh = lineText.charAt(index);
- }
- if (nextCh === symb) {
- depth++;
- } else if (nextCh === reverseSymb) {
- depth--;
- }
- }
- if (nextCh) {
- return { line: line, ch: index };
- }
- return cur;
- }
- function selectCompanionObject(cm, revSymb, inclusive) {
- var cur = cm.getCursor();
- var end = findMatchedSymbol(cm, cur, revSymb);
- var start = findMatchedSymbol(cm, end);
- start.ch += inclusive ? 1 : 0;
- end.ch += inclusive ? 0 : 1;
- return { start: start, end: end };
- }
- function regexLastIndexOf(string, pattern, startIndex) {
- for (var i = !startIndex ? string.length : startIndex;
- i >= 0; --i) {
- if (pattern.test(string.charAt(i))) {
- return i;
- }
- }
- return -1;
- }
- // Takes in a symbol and a cursor and tries to simulate text objects that
- // have identical opening and closing symbols
- // TODO support across multiple lines
- function findBeginningAndEnd(cm, symb, inclusive) {
- var cur = cm.getCursor();
- var line = cm.getLine(cur.line);
- var chars = line.split('');
- var start, end, i, len;
- var firstIndex = chars.indexOf(symb);
- // the decision tree is to always look backwards for the beginning first,
- // but if the cursor is in front of the first instance of the symb,
- // then move the cursor forward
- if (cur.ch < firstIndex) {
- cur.ch = firstIndex;
- // Why is this line even here???
- // cm.setCursor(cur.line, firstIndex+1);
- }
- // otherwise if the cursor is currently on the closing symbol
- else if (firstIndex < cur.ch && chars[cur.ch] == symb) {
- end = cur.ch; // assign end to the current cursor
- --cur.ch; // make sure to look backwards
- }
- // if we're currently on the symbol, we've got a start
- if (chars[cur.ch] == symb && !end) {
- start = cur.ch + 1; // assign start to ahead of the cursor
- } else {
- // go backwards to find the start
- for (i = cur.ch; i > -1 && !start; i--) {
- if (chars[i] == symb) {
- start = i + 1;
- }
- }
- }
- // look forwards for the end symbol
- if (start && !end) {
- for (i = start, len = chars.length; i < len && !end; i++) {
- if (chars[i] == symb) {
- end = i;
- }
- }
- }
- // nothing found
- if (!start || !end) {
- return { start: cur, end: cur };
- }
- // include the symbols
- if (inclusive) {
- --start; ++end;
- }
- return {
- start: { line: cur.line, ch: start },
- end: { line: cur.line, ch: end }
- };
- }
- // Search functions
- function SearchState() {
- // Highlighted text that match the query.
- this.marked = null;
- }
- SearchState.prototype = {
- getQuery: function() {
- return getVimGlobalState().query;
- },
- setQuery: function(query) {
- getVimGlobalState().query = query;
- },
- getMarked: function() {
- return this.marked;
- },
- setMarked: function(marked) {
- this.marked = marked;
- },
- isReversed: function() {
- return getVimGlobalState().isReversed;
- },
- setReversed: function(reversed) {
- getVimGlobalState().isReversed = reversed;
- }
- };
- function getSearchState(cm) {
- var vim = getVimState(cm);
- return vim.searchState_ || (vim.searchState_ = new SearchState());
- }
- function dialog(cm, text, shortText, callback) {
- if (cm.openDialog) {
- cm.openDialog(text, callback, {bottom: true});
- }
- else {
- callback(prompt(shortText, ""));
- }
- }
- function findUnescapedSlashes(str) {
- var escapeNextChar = false;
- var slashes = [];
- for (var i = 0; i < str.length; i++) {
- var c = str.charAt(i);
- if (!escapeNextChar && c == '/') {
- slashes.push(i);
- }
- escapeNextChar = (c == '\\');
- }
- return slashes;
- }
- /**
- * Extract the regular expression from the query and return a Regexp object.
- * Returns null if the query is blank.
- * If ignoreCase is passed in, the Regexp object will have the 'i' flag set.
- * If smartCase is passed in, and the query contains upper case letters,
- * then ignoreCase is overridden, and the 'i' flag will not be set.
- * If the query contains the /i in the flag part of the regular expression,
- * then both ignoreCase and smartCase are ignored, and 'i' will be passed
- * through to the Regex object.
- */
- function parseQuery(cm, query, ignoreCase, smartCase) {
- // First try to extract regex + flags from the input. If no flags found,
- // extract just the regex. IE does not accept flags directly defined in
- // the regex string in the form /regex/flags
- var slashes = findUnescapedSlashes(query);
- var regexPart;
- var forceIgnoreCase;
- if (!slashes.length) {
- // Query looks like 'regexp'
- regexPart = query;
- } else {
- // Query looks like 'regexp/...'
- regexPart = query.substring(0, slashes[0]);
- var flagsPart = query.substring(slashes[0]);
- forceIgnoreCase = (flagsPart.indexOf('i') != -1);
- }
- if (!regexPart) {
- return null;
- }
- if (smartCase) {
- ignoreCase = (/^[^A-Z]*$/).test(regexPart);
- }
- try {
- var regexp = new RegExp(regexPart,
- (ignoreCase || forceIgnoreCase) ? 'i' : undefined);
- return regexp;
- } catch (e) {
- showConfirm(cm, 'Invalid regex: ' + regexPart);
- }
- }
- function showConfirm(cm, text) {
- if (cm.openConfirm) {
- cm.openConfirm('<span style="color: red">' + text +
- '</span> <button type="button">OK</button>', function() {},
- {bottom: true});
- } else {
- alert(text);
- }
- }
- function makePrompt(prefix, desc) {
- var raw = '';
- if (prefix) {
- raw += '<span style="font-family: monospace">' + prefix + '</span>';
- }
- raw += '<input type="text"/> ' +
- '<span style="color: #888">';
- if (desc) {
- raw += '<span style="color: #888">';
- raw += desc;
- raw += '</span>';
- }
- return raw;
- }
- var searchPromptDesc = '(Javascript regexp)';
- function showPrompt(cm, onPromptClose, prefix, desc) {
- var shortText = (prefix || '') + ' ' + (desc || '');
- dialog(cm, makePrompt(prefix, desc), shortText, onPromptClose);
- }
- function regexEqual(r1, r2) {
- if (r1 instanceof RegExp && r2 instanceof RegExp) {
- var props = ["global", "multiline", "ignoreCase", "source"];
- for (var i = 0; i < props.length; i++) {
- var prop = props[i];
- if (r1[prop] !== r2[prop]) {
- return(false);
- }
- }
- return(true);
- }
- return(false);
- }
- function updateSearchQuery(cm, rawQuery, ignoreCase, smartCase) {
- cm.operation(function() {
- var state = getSearchState(cm);
- if (!rawQuery) {
- return;
- }
- var query = parseQuery(cm, rawQuery, !!ignoreCase, !!smartCase);
- if (!query) {
- return;
- }
- if (regexEqual(query, state.getQuery())) {
- return;
- }
- clearSearchHighlight(cm);
- highlightSearchMatches(cm, query);
- state.setQuery(query);
- });
- }
- function highlightSearchMatches(cm, query) {
- // TODO: Highlight only text inside the viewport. Highlighting everything
- // is inefficient and expensive.
- if (cm.lineCount() < 2000) { // This is too expensive on big documents.
- var marked = [];
- for (var cursor = cm.getSearchCursor(query);
- cursor.findNext();) {
- marked.push(cm.markText(cursor.from(), cursor.to(),
- { className: 'CodeMirror-searching' }));
- }
- getSearchState(cm).setMarked(marked);
- }
- }
- function findNext(cm, prev, repeat) {
- return cm.operation(function() {
- var state = getSearchState(cm);
- var query = state.getQuery();
- if (!query) {
- return;
- }
- if (!state.getMarked()) {
- highlightSearchMatches(cm, query);
- }
- var pos = cm.getCursor();
- // If search is initiated with ? instead of /, negate direction.
- prev = (state.isReversed()) ? !prev : prev;
- if (!prev) {
- pos.ch += 1;
- }
- var cursor = cm.getSearchCursor(query, pos);
- for (var i = 0; i < repeat; i++) {
- if (!cursor.find(prev)) {
- // SearchCursor may have returned null because it hit EOF, wrap
- // around and try again.
- cursor = cm.getSearchCursor(query,
- (prev) ? { line: cm.lineCount() - 1} : {line: 0, ch: 0} );
- if (!cursor.find(prev)) {
- return;
- }
- }
- }
- return cursor.from();
- });}
- function clearSearchHighlight(cm) {
- cm.operation(function() {
- var state = getSearchState(cm);
- if (!state.getQuery()) {
- return;
- }
- var marked = state.getMarked();
- if (!marked) {
- return;
- }
- for (var i = 0; i < marked.length; ++i) {
- marked[i].clear();
- }
- state.setMarked(null);
- });}
- // Ex command handling
- // 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: 'map', type: 'builtIn' },
- { name: 'write', shortName: 'w', type: 'builtIn' },
- { name: 'undo', shortName: 'u', type: 'builtIn' },
- { name: 'redo', shortName: 'red', type: 'builtIn' }
- ];
- var ExCommandDispatcher = function() {
- this.buildCommandMap_();
- };
- ExCommandDispatcher.prototype = {
- processCommand: function(cm, input) {
- var params = this.parseInput_(input);
- var commandName;
- if (!params.commandName) {
- // If only a line range is defined, move to the line.
- if (params.line !== undefined) {
- commandName = 'move';
- }
- } else {
- var command = this.matchCommand_(params.commandName);
- if (command) {
- commandName = command.name;
- if (command.type == 'exToKey') {
- // Handle Ex to Key mapping.
- for (var i = 0; i < command.toKeys.length; i++) {
- vim.handleKey(cm, command.toKeys[i]);
- }
- return;
- } else if (command.type == 'exToEx') {
- // Handle Ex to Ex mapping.
- this.processCommand(cm, command.toInput);
- return;
- }
- }
- }
- if (!commandName) {
- showConfirm(cm, 'Not an editor command ":' + input + '"');
- return;
- }
- exCommands[commandName](cm, params);
- },
- parseInput_: function(input) {
- var result = {};
- result.input = input;
- var idx = 0;
- // Trim preceding ':'.
- var colons = (/^:+/).exec(input);
- if (colons) {
- idx += colons[0].length;
- }
- // Parse range.
- var numberMatch = (/^(\d+)/).exec(input.substring(idx));
- if (numberMatch) {
- result.line = parseInt(numberMatch[1], 10);
- idx += numberMatch[0].length;
- }
- // Parse command name.
- var commandMatch = (/^(\w+)/).exec(input.substring(idx));
- if (commandMatch) {
- result.commandName = commandMatch[1];
- idx += commandMatch[1].length;
- }
- // Parse command-line arguments
- var args = trim(input.substring(idx)).split(/\s+/);
- if (args.length && args[0]) {
- result.commandArgs = args;
- }
- return result;
- },
- matchCommand_: function(commandName) {
- // Return the command in the command map that matches the shortest
- // prefix of the passed in command name. The match is guaranteed to be
- // unambiguous if the defaultExCommandMap's shortNames are set up
- // correctly. (see @code{defaultExCommandMap}).
- for (var i = commandName.length; i > 0; i--) {
- var prefix = commandName.substring(0, i);
- if (this.commandMap_[prefix]) {
- var command = this.commandMap_[prefix];
- if (command.name.indexOf(commandName) === 0) {
- return command;
- }
- }
- }
- return null;
- },
- buildCommandMap_: function() {
- this.commandMap_ = {};
- for (var i = 0; i < defaultExCommandMap.length; i++) {
- var command = defaultExCommandMap[i];
- var key = command.shortName || command.name;
- this.commandMap_[key] = command;
- }
- },
- map: function(lhs, rhs) {
- if (lhs.charAt(0) == ':') {
- var commandName = lhs.substring(1);
- if (rhs.charAt(0) == ':') {
- // Ex to Ex mapping
- this.commandMap_[commandName] = {
- name: commandName,
- type: 'exToEx',
- toInput: rhs.substring(1)
- };
- } else {
- // Ex to key mapping
- this.commandMap_[commandName] = {
- name: commandName,
- type: 'exToKey',
- toKeys: parseKeyString(rhs)
- };
- }
- } else {
- if (rhs.charAt(0) == ':') {
- // Key to Ex mapping.
- defaultKeymap.unshift({
- keys: parseKeyString(lhs),
- type: 'keyToEx',
- exArgs: { input: rhs.substring(1) }});
- } else {
- // Key to key mapping
- defaultKeymap.unshift({
- keys: parseKeyString(lhs),
- type: 'keyToKey',
- toKeys: parseKeyString(rhs)
- });
- }
- }
- }
- };
- // Converts a key string sequence of the form a<C-w>bd<Left> into Vim's
- // keymap representation.
- function parseKeyString(str) {
- var idx = 0;
- var keys = [];
- while (idx < str.length) {
- if (str.charAt(idx) != '<') {
- keys.push(str.charAt(idx));
- idx++;
- continue;
- }
- // Vim key notation here means desktop Vim key-notation.
- // See :help key-notation in desktop Vim.
- var vimKeyNotationStart = ++idx;
- while (str.charAt(idx++) != '>') {}
- var vimKeyNotation = str.substring(vimKeyNotationStart, idx - 1);
- var match = (/^C-(.+)$/).exec(vimKeyNotation);
- if (match) {
- var key;
- switch (match[1]) {
- case 'BS':
- key = 'Backspace';
- break;
- case 'CR':
- key = 'Enter';
- break;
- case 'Del':
- key = 'Delete';
- break;
- default:
- key = match[1];
- break;
- }
- keys.push('Ctrl-' + key);
- }
- }
- return keys;
- }
- var exCommands = {
- map: function(cm, params) {
- var mapArgs = params.commandArgs;
- if (!mapArgs || mapArgs.length < 2) {
- if (cm) {
- showConfirm(cm, 'Invalid mapping: ' + params.input);
- }
- return;
- }
- exCommandDispatcher.map(mapArgs[0], mapArgs[1], cm);
- },
- move: function(cm, params) {
- commandDispatcher.processMotion(cm, getVimState(cm), {
- motion: 'moveToLineOrEdgeOfDocument',
- motionArgs: { forward: false, explicitRepeat: true,
- linewise: true, repeat: params.line }});
- },
- redo: CodeMirror.commands.redo,
- undo: CodeMirror.commands.undo,
- write: function(cm) {
- if (CodeMirror.commands.save) {
- // If a save command is defined, call it.
- CodeMirror.commands.save(cm);
- } else {
- // Saves to text area if no save command is defined.
- cm.save();
- }
- }
- };
- var exCommandDispatcher = new ExCommandDispatcher();
- // Register Vim with CodeMirror
- function buildVimKeyMap() {
- /**
- * Handle the raw key event from CodeMirror. Translate the
- * Shift + key modifier to the resulting letter, while preserving other
- * modifers.
- */
- // TODO: Figure out a way to catch capslock.
- function handleKeyEvent_(cm, key, modifier) {
- if (isUpperCase(key)) {
- // Convert to lower case if shift is not the modifier since the key
- // we get from CodeMirror is always upper case.
- if (modifier == 'Shift') {
- modifier = null;
- }
- else {
- key = key.toLowerCase();
- }
- }
- if (modifier) {
- // Vim will parse modifier+key combination as a single key.
- key = modifier + '-' + key;
- }
- vim.handleKey(cm, key);
- }
- // Closure to bind CodeMirror, key, modifier.
- function keyMapper(key, modifier) {
- return function(cm) {
- handleKeyEvent_(cm, key, modifier);
- };
- }
- var modifiers = ['Shift', 'Ctrl'];
- var keyMap = {
- 'nofallthrough': true,
- 'style': 'fat-cursor'
- };
- function bindKeys(keys, modifier) {
- for (var i = 0; i < keys.length; i++) {
- var key = keys[i];
- if (!modifier && inArray(key, specialSymbols)) {
- // Wrap special symbols with '' because that's how CodeMirror binds
- // them.
- key = "'" + key + "'";
- }
- if (modifier) {
- keyMap[modifier + '-' + key] = keyMapper(keys[i], modifier);
- } else {
- keyMap[key] = keyMapper(keys[i]);
- }
- }
- }
- bindKeys(upperCaseAlphabet);
- bindKeys(upperCaseAlphabet, 'Shift');
- bindKeys(upperCaseAlphabet, 'Ctrl');
- bindKeys(specialSymbols);
- bindKeys(specialSymbols, 'Ctrl');
- bindKeys(numbers);
- bindKeys(numbers, 'Ctrl');
- bindKeys(specialKeys);
- bindKeys(specialKeys, 'Ctrl');
- return keyMap;
- }
- CodeMirror.keyMap.vim = buildVimKeyMap();
- function exitInsertMode(cm) {
- cm.setCursor(cm.getCursor().line, cm.getCursor().ch-1, true);
- cm.setOption('keyMap', 'vim');
- }
- CodeMirror.keyMap['vim-insert'] = {
- // TODO: override navigation keys so that Esc will cancel automatic
- // indentation from o, O, i_<CR>
- 'Esc': exitInsertMode,
- 'Ctrl-[': exitInsertMode,
- 'Ctrl-C': exitInsertMode,
- 'Ctrl-N': 'autocomplete',
- 'Ctrl-P': 'autocomplete',
- 'Enter': function(cm) {
- var fn = CodeMirror.commands.newlineAndIndentContinueComment ||
- CodeMirror.commands.newlineAndIndent;
- fn(cm);
- },
- fallthrough: ['default']
- };
- return vimApi;
- };
- // Initialize Vim and make it available as an API.
- var vim = Vim();
- CodeMirror.Vim = vim;
- }
- )();
|