-
Download the latest release
+
Current project page
+
+
This is old project page for CodeMirror version 2.x. Go
+ to codemirror.net for current
+ releases and events.
Support CodeMirror
@@ -280,190 +284,7 @@
Reading material
Browse the code
-
Releases
-
-
20-11-2012: Version 2.36 :
-
-
-
-
20-11-2012: Version 3.0, release candidate 1 :
-
-
New major version . Only partially
- backwards-compatible. See
- the upgrading
- guide for more information. Changes since beta 2:
-
-
-
-
22-10-2012: Version 2.35 :
-
-
- New (sub) mode: TypeScript .
- Don't overwrite (insert key) when pasting.
- Fix several bugs in markText /undo interaction.
- Better indentation of JavaScript code without semicolons.
- Add defineInitHook function.
- Full list of patches .
-
-
-
22-10-2012: Version 3.0, beta 2 :
-
-
- Fix page-based coordinate computation.
- Fix firing of gutterClick event.
- Add cursorHeight option.
- Fix bi-directional text regression.
- Add viewportMargin option.
- Directly handle mousewheel events (again, hopefully better).
- Make vertical cursor movement more robust (through widgets, big line gaps).
- Add flattenSpans option.
- Initialization in hidden state works again.
- Many optimizations. Poor responsiveness should be fixed.
- Full list of patches .
-
-
-
19-09-2012: Version 2.34 :
-
-
- New mode: Common Lisp .
- Fix right-click select-all on most browsers.
- Change the way highlighting happens: Saves memory and CPU cycles. compareStates is no longer needed. onHighlightComplete no longer works.
- Integrate mode (Markdown, XQuery, CSS, sTex) tests in central testsuite.
- Add a CodeMirror.version property.
- More robust handling of nested modes in formatting and closetag plug-ins.
- Un/redo now preserves marked text and bookmarks.
- Full list of patches.
-
-
-
19-09-2012: Version 3.0, beta 1 :
-
-
- Bi-directional text support.
- More powerful gutter model.
- Support for arbitrary text/widget height.
- In-line widgets.
- Generalized event handling.
-
-
-
23-08-2012: Version 2.33 :
-
-
- New mode: Sieve .
- New getViewPort and onViewportChange API.
- Configurable cursor blink rate.
- Make binding a key to false disabling handling (again).
- Show non-printing characters as red dots.
- More tweaks to the scrolling model.
- Expanded testsuite. Basic linter added.
- Remove most uses of innerHTML. Remove CodeMirror.htmlEscape.
- Full list of patches.
-
-
-
23-07-2012: Version 2.32 :
-
-
Emergency fix for a bug where an editor with
- line wrapping on IE will break when there is no
- scrollbar.
-
-
20-07-2012: Version 2.31 :
-
-
-
-
22-06-2012: Version 2.3 :
-
-
- New scrollbar implementation . Should flicker less. Changes DOM structure of the editor.
- New theme: vibrant-ink .
- Many extensions to the VIM keymap (including text objects).
- Add mode-multiplexing utility script.
- Fix bug where right-click paste works in read-only mode.
- Add a getScrollInfo method.
- Lots of other fixes .
-
-
-
23-05-2012: Version 2.25 :
-
-
- New mode: Erlang .
- Remove xmlpure mode (use xml.js ).
- Fix line-wrapping in Opera.
- Fix X Windows middle-click paste in Chrome.
- Fix bug that broke pasting of huge documents.
- Fix backspace and tab key repeat in Opera.
-
-
-
23-04-2012: Version 2.24 :
-
-
- Drop support for Internet Explorer 6 .
- New
- modes: Shell , Tiki
- wiki , Pig Latin .
- New themes: Ambiance , Blackboard .
- More control over drag/drop
- with dragDrop
- and onDragEvent
- options.
- Make HTML mode a bit less pedantic.
- Add compoundChange API method.
- Several fixes in undo history and line hiding.
- Remove (broken) support for catchall in key maps,
- add nofallthrough boolean field instead.
-
-
-
26-03-2012: Version 2.23 :
-
-
- Change default binding for tab [more]
-
- Starting in 2.23, these bindings are default:
-
Tab: Insert tab character
- Shift-tab: Reset line indentation to default
- Ctrl/Cmd-[: Reduce line indentation (old tab behaviour)
- Ctrl/Cmd-]: Increase line indentation (old shift-tab behaviour)
-
-
-
- New modes: XQuery and VBScript .
- Two new themes: lesser-dark and xq-dark .
- Differentiate between background and text styles in setLineClass .
- Fix drag-and-drop in IE9+.
- Extend charCoords
- and cursorCoords with a mode argument.
- Add autofocus option.
- Add findMarksAt method.
-
+
Releases
Older releases...
diff --git a/keymap/vim.js b/keymap/vim.js
index 10eb590b15..5e04252d8e 100644
--- a/keymap/vim.js
+++ b/keymap/vim.js
@@ -1,897 +1,2427 @@
-// Supported keybindings:
-//
-// Cursor movement:
-// h, j, k, l
-// e, E, w, W, b, B
-// Ctrl-f, Ctrl-b
-// Ctrl-n, Ctrl-p
-// $, ^, 0
-// G
-// ge, gE
-// gg
-// f
, F, t, T
-// Ctrl-o, Ctrl-i TODO (FIXME - Ctrl-O wont work in Chrome)
-// /, ?, n, N TODO (does not work)
-// #, * TODO
-//
-// Entering insert mode:
-// i, I, a, A, o, O
-// s
-// ce, cb
-// cc
-// S, C TODO
-// cf, cF, ct, cT
-//
-// Deleting text:
-// x, X
-// J
-// dd, D
-// de, db
-// df, dF, dt, dT
-//
-// Yanking and pasting:
-// yy, Y
-// p, P
-// p' TODO - test
-// y' TODO - test
-// m TODO - test
-//
-// Changing text in place:
-// ~
-// r
-//
-// Visual mode:
-// v, V TODO
-//
-// Misc:
-// . TODO
-//
+/**
+ * Supported keybindings:
+ *
+ * Motion:
+ * h, j, k, l
+ * e, E, w, W, b, B, ge, gE
+ * f, F, t, T
+ * $, ^, 0
+ * gg, G
+ * %
+ * ', `
+ *
+ * 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
+ * r
+ *
+ * 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() {
- var sdir = "f";
- var buf = "";
- var mark = {};
- var repeatCount = 0;
- function isLine(cm, line) { return line >= 0 && line < cm.lineCount(); }
- function emptyBuffer() { buf = ""; }
- function pushInBuffer(str) { buf += str; }
- function pushRepeatCountDigit(digit) {return function(cm) {repeatCount = (repeatCount * 10) + digit}; }
- function getCountOrOne() {
- var i = repeatCount;
- return i || 1;
- }
- function clearCount() {
- repeatCount = 0;
- }
- function iterTimes(func) {
- for (var i = 0, c = getCountOrOne(); i < c; ++i) func(i, i == c - 1);
- clearCount();
- }
- function countTimes(func) {
- if (typeof func == "string") func = CodeMirror.commands[func];
- return function(cm) { iterTimes(function (i, last) { func(cm, i, last); }); };
- }
-
- function iterObj(o, f) {
- for (var prop in o) if (o.hasOwnProperty(prop)) f(prop, o[prop]);
- }
- function iterList(l, f) {
- for (var i = 0; i < l.length; ++i) f(l[i]);
- }
- function toLetter(ch) {
- // T -> t, Shift-T -> T, '*' -> *, "Space" -> " "
- if (ch.slice(0, 6) == "Shift-") {
- return ch.slice(0, 1);
- } else {
- if (ch == "Space") return " ";
- if (ch.length == 3 && ch[0] == "'" && ch[2] == "'") return ch[1];
- return ch.toLowerCase();
- }
- }
- var SPECIAL_SYMBOLS = "~`!@#$%^&*()_-+=[{}]\\|/?.,<>:;\"\'1234567890";
- function toCombo(ch) {
- // t -> T, T -> Shift-T, * -> '*', " " -> "Space"
- if (ch == " ") return "Space";
- var specialIdx = SPECIAL_SYMBOLS.indexOf(ch);
- if (specialIdx != -1) return "'" + ch + "'";
- if (ch.toLowerCase() == ch) return ch.toUpperCase();
- return "Shift-" + ch.toUpperCase();
- }
-
- var word = [/\w/, /[^\w\s]/], bigWord = [/\S/];
- // Finds a word on the given line, and continue searching the next line if it can't find one.
- function findWord(cm, lineNum, pos, dir, regexps) {
- var line = cm.getLine(lineNum);
- while (true) {
- var stop = (dir > 0) ? line.length : -1;
- var wordStart = stop, wordEnd = stop;
- // Find bounds of next word.
- for (; pos != stop; pos += dir) {
- for (var i = 0; i < regexps.length; ++i) {
- if (regexps[i].test(line.charAt(pos))) {
- wordStart = pos;
- // Advance to end of word.
- for (; pos != stop && regexps[i].test(line.charAt(pos)); pos += dir) {}
- wordEnd = (dir > 0) ? pos : pos + 1;
- return {
- from: Math.min(wordStart, wordEnd),
- to: Math.max(wordStart, wordEnd),
- line: lineNum};
- }
- }
- }
- // Advance to next/prev line.
- lineNum += dir;
- if (!isLine(cm, lineNum)) return null;
- line = cm.getLine(lineNum);
- pos = (dir > 0) ? 0 : line.length;
- }
- }
- /**
- * @param {boolean} cm CodeMirror object.
- * @param {regexp} regexps Regular expressions for word characters.
- * @param {number} dir Direction, +/- 1.
- * @param {number} times Number of times to advance word.
- * @param {string} where Go to "start" or "end" of word, 'e' vs 'w'.
- * @param {boolean} yank Whether we are finding words to yank. If true,
- * do not go to the next line to look for the last word. This is to
- * prevent deleting new line on 'dw' at the end of a line.
- */
- function moveToWord(cm, regexps, dir, times, where, yank) {
- var cur = cm.getCursor();
- if (yank) {
- where = 'start';
- }
- for (var i = 0; i < times; i++) {
- var startCh = cur.ch, startLine = cur.line, word;
- while (true) {
- // Search and advance.
- word = findWord(cm, cur.line, cur.ch, dir, regexps);
- if (word) {
- if (yank && times == 1 && dir == 1 && cur.line != word.line) {
- // Stop at end of line of last word. Don't want to delete line return
- // for dw if the last deleted word is at the end of a line.
- cur.ch = cm.getLine(cur.line).length;
- break;
- } else {
- // 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 (dir > 0 && where == 'end') {
- // 'e'
- if (startCh != word.to - 1 || startLine != word.line) {
- cur.ch = word.to - 1;
- break;
+ '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).concat(['<', '>']);
+ 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 {
- cur.ch = word.to;
+ // 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);
}
- } else if (dir > 0 && where == 'start') {
- // 'w'
- if (startCh != word.from || startLine != word.line) {
- cur.ch = word.from;
- break;
- } else {
- cur.ch = word.to;
+ 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;
}
- } else if (dir < 0 && where == 'end') {
- // 'ge'
- if (startCh != word.to || startLine != word.line) {
- cur.ch = word.to;
- break;
- } else {
- cur.ch = word.from - 1;
+ // Matches whole comand. Return the command.
+ if (command.keys[keys.length - 1] == 'character') {
+ inputState.selectedCharacter = keys[keys.length - 1];
}
- } else if (dir < 0 && where == 'start') {
- // 'b'
- if (startCh != word.from || startLine != word.line) {
- cur.ch = word.from;
- break;
+ 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 {
+ if (vim.visualMode) {
+ showPrompt(cm, onPromptClose, ':', undefined, '\'<,\'>');
+ } 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 {
- cur.ch = word.from - 1;
+ 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);
+ updateMark(cm, vim, '<',
+ cursorIsBefore(selectionStart, selectionEnd) ? selectionStart
+ : selectionEnd);
+ updateMark(cm, vim, '>',
+ cursorIsBefore(selectionStart, selectionEnd) ? selectionEnd
+ : selectionStart);
+ } else if (!operator) {
+ curEnd = clipCursorToContent(cm, curEnd);
+ cm.setCursor(curEnd.line, curEnd.ch);
}
- } else {
- // No more words to be found. Move to end of document.
- for (; isLine(cm, cur.line + dir); cur.line += dir) {}
- cur.ch = (dir > 0) ? cm.getLine(cur.line).length : 0;
- break;
- }
- }
- }
- if (where == 'end' && yank) {
- // Include the last character of the word for actions.
- cur.ch++;
- }
- return cur;
- }
- function joinLineNext(cm) {
- var cur = cm.getCursor(), ch = cur.ch, line = cm.getLine(cur.line);
- CodeMirror.commands.goLineEnd(cm);
- if (cur.line != cm.lineCount()) {
- CodeMirror.commands.goLineEnd(cm);
- cm.replaceSelection(" ", "end");
- CodeMirror.commands.delCharRight(cm);
- }
- }
- function delTillMark(cm, cHar) {
- var i = mark[cHar];
- if (i === undefined) {
- // console.log("Mark not set"); // TODO - show in status bar
- return;
- }
- var l = cm.getCursor().line, start = i > l ? l : i, end = i > l ? i : l;
- cm.setCursor(start);
- for (var c = start; c <= end; c++) {
- pushInBuffer("\n" + cm.getLine(start));
- cm.removeLine(start);
- }
- }
- function yankTillMark(cm, cHar) {
- var i = mark[cHar];
- if (i === undefined) {
- // console.log("Mark not set"); // TODO - show in status bar
- return;
- }
- var l = cm.getCursor().line, start = i > l ? l : i, end = i > l ? i : l;
- for (var c = start; c <= end; c++) {
- pushInBuffer("\n" + cm.getLine(c));
- }
- cm.setCursor(start);
- }
- function goLineStartText(cm) {
- // Go to the start of the line where the text begins, or the end for whitespace-only lines
- var cur = cm.getCursor(), firstNonWS = cm.getLine(cur.line).search(/\S/);
- cm.setCursor(cur.line, firstNonWS == -1 ? line.length : firstNonWS, true);
- }
-
- function charIdxInLine(cm, cHar, motion_options) {
- // Search for cHar in line.
- // motion_options: {forward, inclusive}
- // If inclusive = true, include it too.
- // If forward = true, search forward, else search backwards.
- // If char is not found on this line, do nothing
- var cur = cm.getCursor(), line = cm.getLine(cur.line), idx;
- var ch = toLetter(cHar), mo = motion_options;
- if (mo.forward) {
- idx = line.indexOf(ch, cur.ch + 1);
- if (idx != -1 && mo.inclusive) idx += 1;
- } else {
- idx = line.lastIndexOf(ch, cur.ch);
- if (idx != -1 && !mo.inclusive) idx += 1;
- }
- return idx;
- }
-
- function moveTillChar(cm, cHar, motion_options) {
- // Move to cHar in line, as found by charIdxInLine.
- var idx = charIdxInLine(cm, cHar, motion_options), cur = cm.getCursor();
- if (idx != -1) cm.setCursor({line: cur.line, ch: idx});
- }
-
- function delTillChar(cm, cHar, motion_options) {
- // delete text in this line, untill cHar is met,
- // as found by charIdxInLine.
- // If char is not found on this line, do nothing
- var idx = charIdxInLine(cm, cHar, motion_options);
- var cur = cm.getCursor();
- if (idx !== -1) {
- if (motion_options.forward) {
- cm.replaceRange("", {line: cur.line, ch: cur.ch}, {line: cur.line, ch: idx});
- } else {
- cm.replaceRange("", {line: cur.line, ch: idx}, {line: cur.line, ch: cur.ch});
- }
- }
- }
-
- function enterInsertMode(cm) {
- // enter insert mode: switch mode and cursor
- clearCount();
- cm.setOption("keyMap", "vim-insert");
- }
-
- function dialog(cm, text, shortText, f) {
- if (cm.openDialog) cm.openDialog(text, f);
- else f(prompt(shortText, ""));
- }
- function showAlert(cm, text) {
- var esc = text.replace(/[<&]/, function(ch) { return ch == "<" ? "<" : "&"; });
- if (cm.openDialog) cm.openDialog(esc + " OK ");
- else alert(text);
- }
-
- // main keymap
- var map = CodeMirror.keyMap.vim = {
- // Pipe (|); TODO: should be *screen* chars, so need a util function to turn tabs into spaces?
- "'|'": function(cm) {
- cm.setCursor(cm.getCursor().line, getCountOrOne() - 1, true);
- clearCount();
- },
- "A": function(cm) {
- cm.setCursor(cm.getCursor().line, cm.getCursor().ch+1, true);
- enterInsertMode(cm);
- },
- "Shift-A": function(cm) { CodeMirror.commands.goLineEnd(cm); enterInsertMode(cm);},
- "I": function(cm) { enterInsertMode(cm);},
- "Shift-I": function(cm) { goLineStartText(cm); enterInsertMode(cm);},
- "O": function(cm) {
- CodeMirror.commands.goLineEnd(cm);
- CodeMirror.commands.newlineAndIndent(cm);
- enterInsertMode(cm);
- },
- "Shift-O": function(cm) {
- CodeMirror.commands.goLineStart(cm);
- cm.replaceSelection("\n", "start");
- cm.indentLine(cm.getCursor().line);
- enterInsertMode(cm);
- },
- "G": function(cm) { cm.setOption("keyMap", "vim-prefix-g");},
- "Shift-D": function(cm) {
- var cursor = cm.getCursor();
- var lineN = cursor.line;
- var line = cm.getLine(lineN);
- cm.setLine(lineN, line.slice(0, cursor.ch));
-
- emptyBuffer();
- pushInBuffer(line.slice(cursor.ch));
-
- if (repeatCount > 1) {
- // we've already done it once
- --repeatCount;
- // the lines dissapear (ie, cursor stays on the same lineN),
- // so only incremenet once
- ++lineN;
-
- iterTimes(function() {
- pushInBuffer(cm.getLine(lineN));
- cm.removeLine(lineN);
- });
+ }
+
+ 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);
+ }
+ }
}
- },
-
- "S": function (cm) {
- countTimes(function (_cm) {
- CodeMirror.commands.delCharRight(_cm);
- })(cm);
- enterInsertMode(cm);
- },
- "M": function(cm) {cm.setOption("keyMap", "vim-prefix-m"); mark = {};},
- "Y": function(cm) {cm.setOption("keyMap", "vim-prefix-y"); emptyBuffer();},
- "Shift-Y": function(cm) {
- emptyBuffer();
- iterTimes(function(i) { pushInBuffer("\n" + cm.getLine(cm.getCursor().line + i)); });
- },
- "/": function(cm) {var f = CodeMirror.commands.find; f && f(cm); sdir = "f";},
- "'?'": function(cm) {
- var f = CodeMirror.commands.find;
- if (f) { f(cm); CodeMirror.commands.findPrev(cm); sdir = "r"; }
- },
- "N": function(cm) {
- var fn = CodeMirror.commands.findNext;
- if (fn) sdir != "r" ? fn(cm) : CodeMirror.commands.findPrev(cm);
- },
- "Shift-N": function(cm) {
- var fn = CodeMirror.commands.findNext;
- if (fn) sdir != "r" ? CodeMirror.commands.findPrev(cm) : fn.findNext(cm);
- },
- "Shift-G": function(cm) {
- (repeatCount == 0) ? cm.setCursor(cm.lineCount()) : cm.setCursor(repeatCount - 1);
- clearCount();
- CodeMirror.commands.goLineStart(cm);
- },
- "':'": function(cm) {
- var exModeDialog = ': ';
- dialog(cm, exModeDialog, ':', function(command) {
- if (command.match(/^\d+$/)) {
- cm.setCursor(command - 1, cm.getCursor().ch);
+ };
+
+ /**
+ * 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 {
- showAlert(cm, "Bad command: " + command);
+ return cursor;
}
- });
- },
- nofallthrough: true, style: "fat-cursor"
- };
-
- // standard mode switching
- iterList(["d", "t", "T", "f", "F", "c", "r"], function (ch) {
- CodeMirror.keyMap.vim[toCombo(ch)] = function (cm) {
- cm.setOption("keyMap", "vim-prefix-" + ch);
- emptyBuffer();
+ },
+ 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];
+ }
};
- });
-
- // main num keymap
- // Add bindings that are influenced by number keys
- iterObj({
- "X": function(cm) {CodeMirror.commands.delCharRight(cm);},
- "P": function(cm) {
- var cur = cm.getCursor().line;
- if (buf!= "") {
- if (buf[0] == "\n") CodeMirror.commands.goLineEnd(cm);
- cm.replaceRange(buf, cm.getCursor());
- }
- },
- "Shift-X": function(cm) {CodeMirror.commands.delCharLeft(cm);},
- "Shift-J": function(cm) {joinLineNext(cm);},
- "Shift-P": function(cm) {
- var cur = cm.getCursor().line;
- if (buf!= "") {
- CodeMirror.commands.goLineUp(cm);
- CodeMirror.commands.goLineEnd(cm);
- cm.replaceSelection(buf, "end");
- }
- cm.setCursor(cur+1);
- },
- "'~'": function(cm) {
- var cur = cm.getCursor(), cHar = cm.getRange({line: cur.line, ch: cur.ch}, {line: cur.line, ch: cur.ch+1});
- cHar = cHar != cHar.toLowerCase() ? cHar.toLowerCase() : cHar.toUpperCase();
- cm.replaceRange(cHar, {line: cur.line, ch: cur.ch}, {line: cur.line, ch: cur.ch+1});
- cm.setCursor(cur.line, cur.ch+1);
- },
- "Ctrl-B": function(cm) {CodeMirror.commands.goPageUp(cm);},
- "Ctrl-F": function(cm) {CodeMirror.commands.goPageDown(cm);},
- "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown",
- "U": "undo", "Ctrl-R": "redo"
- }, function(key, cmd) { map[key] = countTimes(cmd); });
-
- // empty key maps
- iterList([
- "vim-prefix-d'",
- "vim-prefix-y'",
- "vim-prefix-df",
- "vim-prefix-dF",
- "vim-prefix-dt",
- "vim-prefix-dT",
- "vim-prefix-c",
- "vim-prefix-cf",
- "vim-prefix-cF",
- "vim-prefix-ct",
- "vim-prefix-cT",
- "vim-prefix-",
- "vim-prefix-f",
- "vim-prefix-F",
- "vim-prefix-t",
- "vim-prefix-T",
- "vim-prefix-r",
- "vim-prefix-m"
- ],
- function (prefix) {
- CodeMirror.keyMap[prefix] = {
- auto: "vim",
- nofallthrough: true,
- style: "fat-cursor"
- };
- });
- CodeMirror.keyMap["vim-prefix-g"] = {
- "E": countTimes(function(cm) { cm.setCursor(moveToWord(cm, word, -1, 1, "end"));}),
- "Shift-E": countTimes(function(cm) { cm.setCursor(moveToWord(cm, bigWord, -1, 1, "end"));}),
- "G": function (cm) {
- cm.setCursor({line: repeatCount - 1, ch: cm.getCursor().ch});
- clearCount();
- },
- auto: "vim", nofallthrough: true, style: "fat-cursor"
- };
+ 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);
+ }
+ };
- CodeMirror.keyMap["vim-prefix-d"] = {
- "D": countTimes(function(cm) {
- pushInBuffer("\n" + cm.getLine(cm.getCursor().line));
- cm.removeLine(cm.getCursor().line);
- cm.setOption("keyMap", "vim");
- }),
- "'": function(cm) {
- cm.setOption("keyMap", "vim-prefix-d'");
- emptyBuffer();
- },
- "B": function(cm) {
- var cur = cm.getCursor();
- var line = cm.getLine(cur.line);
- var index = line.lastIndexOf(" ", cur.ch);
+ 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);
+ }
+ }
+ updateMark(cm, vim, '<', cursorIsBefore(curStart, curEnd) ? curStart
+ : curEnd);
+ updateMark(cm, vim, '>', cursorIsBefore(curStart, curEnd) ? curEnd
+ : curStart);
+ },
+ 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;
+ updateMark(cm, vim, markName, 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));
+ }
+ };
- pushInBuffer(line.substring(index, cur.ch));
- cm.replaceRange("", {line: cur.line, ch: index}, cur);
- cm.setOption("keyMap", "vim");
- },
- nofallthrough: true, style: "fat-cursor"
- };
+ 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);
+ }
+ };
- CodeMirror.keyMap["vim-prefix-c"] = {
- "B": function (cm) {
- countTimes("delWordLeft")(cm);
- enterInsertMode(cm);
- },
- "C": function (cm) {
- iterTimes(function (i, last) {
- CodeMirror.commands.deleteLine(cm);
- if (i) {
- CodeMirror.commands.delCharRight(cm);
- if (last) CodeMirror.commands.deleteLine(cm);
+ /*
+ * 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];
}
- });
- enterInsertMode(cm);
- },
- nofallthrough: true, style: "fat-cursor"
- };
+ }
+ }
+ 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");
+ }
- iterList(["vim-prefix-d", "vim-prefix-c", "vim-prefix-"], function (prefix) {
- iterList(["f", "F", "T", "t"],
- function (ch) {
- CodeMirror.keyMap[prefix][toCombo(ch)] = function (cm) {
- cm.setOption("keyMap", prefix + ch);
- emptyBuffer();
- };
- });
- });
+ 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));
+ }
+ }
- var MOTION_OPTIONS = {
- "t": {inclusive: false, forward: true},
- "f": {inclusive: true, forward: true},
- "T": {inclusive: false, forward: false},
- "F": {inclusive: true, forward: false}
- };
+ // 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);
+ }
+ }
- function setupPrefixBindingForKey(m) {
- CodeMirror.keyMap["vim-prefix-m"][m] = function(cm) {
- mark[m] = cm.getCursor().line;
- };
- CodeMirror.keyMap["vim-prefix-d'"][m] = function(cm) {
- delTillMark(cm, m);
- };
- CodeMirror.keyMap["vim-prefix-y'"][m] = function(cm) {
- yankTillMark(cm, m);
- };
- CodeMirror.keyMap["vim-prefix-r"][m] = function (cm) {
- var cur = cm.getCursor();
- cm.replaceRange(toLetter(m),
- {line: cur.line, ch: cur.ch},
- {line: cur.line, ch: cur.ch + 1});
- CodeMirror.commands.goColumnLeft(cm);
- };
- // all commands, related to motions till char in line
- iterObj(MOTION_OPTIONS, function (ch, options) {
- CodeMirror.keyMap["vim-prefix-" + ch][m] = function(cm) {
- moveTillChar(cm, m, options);
- };
- CodeMirror.keyMap["vim-prefix-d" + ch][m] = function(cm) {
- delTillChar(cm, m, options);
- };
- CodeMirror.keyMap["vim-prefix-c" + ch][m] = function(cm) {
- delTillChar(cm, m, options);
- enterInsertMode(cm);
- };
- });
- }
- for (var i = 65; i < 65 + 26; i++) { // uppercase alphabet char codes
- var ch = String.fromCharCode(i);
- setupPrefixBindingForKey(toCombo(ch));
- setupPrefixBindingForKey(toCombo(ch.toLowerCase()));
- }
- for (var i = 0; i < SPECIAL_SYMBOLS.length; ++i) {
- setupPrefixBindingForKey(toCombo(SPECIAL_SYMBOLS.charAt(i)));
- }
- setupPrefixBindingForKey("Space");
-
- CodeMirror.keyMap["vim-prefix-y"] = {
- "Y": countTimes(function(cm, i, last) {
- pushInBuffer("\n" + cm.getLine(cm.getCursor().line + i));
- cm.setOption("keyMap", "vim");
- }),
- "'": function(cm) {cm.setOption("keyMap", "vim-prefix-y'"); emptyBuffer();},
- nofallthrough: true, style: "fat-cursor"
- };
+ // Expand the selection to line ends.
+ function expandSelectionToLine(cm, curStart, curEnd) {
+ curStart.ch = 0;
+ curEnd.ch = 0;
+ curEnd.line++;
+ }
- CodeMirror.keyMap["vim-insert"] = {
- // TODO: override navigation keys so that Esc will cancel automatic indentation from o, O, i_
- "Esc": function(cm) {
- cm.setCursor(cm.getCursor().line, cm.getCursor().ch-1, true);
- cm.setOption("keyMap", "vim");
- },
- "Ctrl-N": "autocomplete",
- "Ctrl-P": "autocomplete",
- fallthrough: ["default"]
- };
+ function findFirstNonWhiteSpaceCharacter(text) {
+ if (!text) {
+ return 0;
+ }
+ var firstNonWS = text.search(/\S/);
+ return firstNonWS == -1 ? text.length : firstNonWS;
+ }
- function findMatchedSymbol(cm, cur, symb) {
- var line = cur.line;
- var symb = symb ? symb : cm.getLine(line)[cur.ch];
+ 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]+/;
+ }
+ }
- // Are we at the opening or closing char
- var forwards = ['(', '[', '{'].indexOf(symb) != -1;
+ 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;
+ }
- var reverseSymb = (function(sym) {
- switch (sym) {
- case '(' : return ')';
- case '[' : return ']';
- case '{' : return '}';
- case ')' : return '(';
- case ']' : return '[';
- case '}' : return '{';
- default : return null;
+ if (inclusive) {
+ wordEnd++;
}
- })(symb);
- // Couldn't find a matching symbol, abort
- if (reverseSymb == null) return cur;
+ return { start: { line: cur.line, ch: wordStart },
+ end: { line: cur.line, ch: wordEnd }};
+ }
- // Tracking our imbalance in open/closing symbols. An opening symbol wii be
- // the first thing we pick up if moving forward, this isn't true moving backwards
- var disBal = forwards ? 0 : 1;
+ /*
+ * 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) {
- if (line == cur.line) {
- // First pass, do some special stuff
- var currLine = forwards ? cm.getLine(line).substr(cur.ch).split('') : cm.getLine(line).substr(0,cur.ch).split('').reverse();
- } else {
- var currLine = forwards ? cm.getLine(line).split('') : cm.getLine(line).split('').reverse();
+ 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.';
+ }
- for (var index = 0; index < currLine.length; index++) {
- if (currLine[index] == symb) disBal++;
- else if (currLine[index] == reverseSymb) disBal--;
+ /**
+ * @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;
+ }
- if (disBal == 0) {
- if (forwards && cur.line == line) return {line: line, ch: index + cur.ch};
- else if (forwards) return {line: line, ch: index};
- else return {line: line, ch: currLine.length - index - 1 };
+ 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 };
+ }
- if (forwards) line++;
- else line--;
+ 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 selectCompanionObject(cm, revSymb, inclusive) {
- var cur = cm.getCursor();
+ function updateMark(cm, vim, markName, pos) {
+ if (!inArray(markName, validMarks)) {
+ return;
+ }
+ if (vim.marks[markName]) {
+ vim.marks[markName].clear();
+ }
+ vim.marks[markName] = cm.setBookmark(pos);
+ }
- var end = findMatchedSymbol(cm, cur, revSymb);
- var start = findMatchedSymbol(cm, end);
- start.ch += inclusive ? 1 : 0;
- end.ch += inclusive ? 0 : 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;
+ }
- return {start: start, end: end};
- }
+ function findMatchedSymbol(cm, cur, symb) {
+ var line = cur.line;
+ symb = symb ? symb : cm.getLine(line).charAt(cur.ch);
- // 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 = undefined;
- var end = undefined;
- var firstIndex = chars.indexOf(symb);
+ // Are we at the opening or closing char
+ var forwards = inArray(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;
- 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
- }
+ var reverseSymb = ({
+ '(': ')', ')': '(',
+ '[': ']', ']': '[',
+ '{': '}', '}': '{'})[symb];
- // if we're currently on the symbol, we've got a start
- if (chars[cur.ch] == symb && end == null)
- start = cur.ch + 1; // assign start to ahead of the cursor
- else {
- // go backwards to find the start
- for (var i = cur.ch; i > -1 && start == null; i--)
- if (chars[i] == symb) start = i + 1;
- }
+ // Couldn't find a matching symbol, abort
+ if (!reverseSymb) {
+ return cur;
+ }
- // look forwards for the end symbol
- if (start != null && end == null) {
- for (var i = start, len = chars.length; i < len && end == null; i++) {
- if (chars[i] == symb) end = i;
+ // 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;
}
- // nothing found
- // FIXME still enters insert mode
- if (start == null || end == null) return {
- start: cur, end: cur
- };
+ function selectCompanionObject(cm, revSymb, inclusive) {
+ var cur = cm.getCursor();
- // include the symbols
- if (inclusive) {
- --start; ++end;
+ 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 };
}
- return {
- start: {line: cur.line, ch: start},
- end: {line: cur.line, ch: end}
- };
- }
-
- function offsetCursor(cm, line, ch) {
- var cur = cm.getCursor(); return {line: cur.line + line, ch: cur.ch + ch};
- }
-
- // These are the motion commands we use for navigation and selection with
- // certain other commands. All should return a cursor object.
- var motions = {
- "J": function(cm, times) { return offsetCursor(cm, times, 0); },
- "Down": function(cm, times) { return offsetCursor(cm, times, 0); },
- "K": function(cm, times) { return offsetCursor(cm, -times, 0); },
- "Up": function(cm, times) { return offsetCursor(cm, -times, 0); },
- "L": function(cm, times) { return offsetCursor(cm, 0, times); },
- "Right": function(cm, times) { return offsetCursor(cm, 0, times); },
- "Space": function(cm, times) { return offsetCursor(cm, 0, times); },
- "H": function(cm, times) { return offsetCursor(cm, 0, -times); },
- "Left": function(cm, times) { return offsetCursor(cm, 0, -times); },
- "Backspace": function(cm, times) { return offsetCursor(cm, 0, -times); },
- "B": function(cm, times, yank) { return moveToWord(cm, word, -1, times, 'start', yank); },
- "Shift-B": function(cm, times, yank) { return moveToWord(cm, bigWord, -1, times, 'start', yank); },
- "E": function(cm, times, yank) { return moveToWord(cm, word, 1, times, 'end', yank); },
- "Shift-E": function(cm, times, yank) { return moveToWord(cm, bigWord, 1, times, 'end', yank); },
- "W": function(cm, times, yank) { return moveToWord(cm, word, 1, times, 'start', yank); },
- "Shift-W": function(cm, times, yank) { return moveToWord(cm, bigWord, 1, times, 'start', yank); },
- "'^'": function(cm, times) {
- var cur = cm.getCursor(), line = cm.getLine(cur.line).split('');
- for (var i = 0; i < line.length; i++) {
- if (line[i].match(/[^\s]/)) return {line: cur.line, ch: index};
+ function regexLastIndexOf(string, pattern, startIndex) {
+ for (var i = !startIndex ? string.length : startIndex;
+ i >= 0; --i) {
+ if (pattern.test(string.charAt(i))) {
+ return i;
+ }
}
- return cur;
- },
- "'$'": function(cm) {
- var cur = cm.getCursor(), ch = cm.getLine(cur.line).length;
- return {line: cur.line, ch: ch};
- },
- "'%'": function(cm) { return findMatchedSymbol(cm, cm.getCursor()); },
- "Esc" : function(cm) { cm.setOption("keyMap", "vim"); repeatCount = 0; return cm.getCursor(); }
- };
+ return -1;
+ }
- // Map our movement actions each operator and non-operational movement
- iterObj(motions, function(key, motion) {
- CodeMirror.keyMap['vim-prefix-d'][key] = function(cm) {
- // Get our selected range
- var start = cm.getCursor();
- var end = motion(cm, repeatCount ? repeatCount : 1, true);
+ // 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
+ }
- // Set swap var if range is of negative length
- if ((start.line > end.line) || (start.line == end.line && start.ch > end.ch)) var swap = true;
+ // 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;
+ }
+ }
+ }
- // Take action, switching start and end if swap var is set
- pushInBuffer(cm.getRange(swap ? end : start, swap ? start : end));
- cm.replaceRange("", swap ? end : start, swap ? start : end);
+ // 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;
+ }
+ }
+ }
- // And clean up
- repeatCount = 0;
- cm.setOption("keyMap", "vim");
- };
+ // nothing found
+ if (!start || !end) {
+ return { start: cur, end: cur };
+ }
- CodeMirror.keyMap['vim-prefix-c'][key] = function(cm) {
- var start = cm.getCursor();
- var end = motion(cm, repeatCount ? repeatCount : 1, true);
+ // include the symbols
+ if (inclusive) {
+ --start; ++end;
+ }
- if ((start.line > end.line) || (start.line == end.line && start.ch > end.ch)) var swap = true;
- pushInBuffer(cm.getRange(swap ? end : start, swap ? start : end));
- cm.replaceRange("", swap ? end : start, swap ? start : end);
+ return {
+ start: { line: cur.line, ch: start },
+ end: { line: cur.line, ch: end }
+ };
+ }
- repeatCount = 0;
- cm.setOption('keyMap', 'vim-insert');
+ // 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, initialValue) {
+ if (cm.openDialog) {
+ cm.openDialog(text, callback, { bottom: true, value: initialValue });
+ }
+ 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('' + text +
+ ' OK ', function() {},
+ {bottom: true});
+ } else {
+ alert(text);
+ }
+ }
+ function makePrompt(prefix, desc) {
+ var raw = '';
+ if (prefix) {
+ raw += '' + prefix + ' ';
+ }
+ raw += ' ' +
+ '';
+ if (desc) {
+ raw += '';
+ raw += desc;
+ raw += ' ';
+ }
+ return raw;
+ }
+ var searchPromptDesc = '(Javascript regexp)';
+ function showPrompt(cm, onPromptClose, prefix, desc, initialValue) {
+ var shortText = (prefix || '') + ' ' + (desc || '');
+ dialog(cm, makePrompt(prefix, desc), shortText, onPromptClose,
+ initialValue);
+ }
+ 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(),
+ '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);
+ });}
+ /**
+ * Check if pos is in the specified range, INCLUSIVE.
+ * Range can be specified with 1 or 2 arguments.
+ * If the first range argument is an array, treat it as an array of line
+ * numbers. Match pos against any of the lines.
+ * If the first range argument is a number,
+ * if there is only 1 range argument, check if pos has the same line
+ * number
+ * if there are 2 range arguments, then check if pos is in between the two
+ * range arguments.
+ */
+ function isInRange(pos, start, end) {
+ if (typeof pos != 'number') {
+ // Assume it is a cursor position. Get the line number.
+ pos = pos.line;
+ }
+ if (start instanceof Array) {
+ return inArray(pos, start);
+ } else {
+ if (end) {
+ return (pos >= start && pos <= end);
+ } else {
+ return pos == start;
+ }
+ }
+ }
- CodeMirror.keyMap['vim-prefix-y'][key] = function(cm) {
- var start = cm.getCursor();
- var end = motion(cm, repeatCount ? repeatCount : 1, true);
+ // 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' },
+ { name: 'substitute', shortName: 's', type: 'builtIn'}
+ ];
+ var ExCommandDispatcher = function() {
+ this.buildCommandMap_();
+ };
+ ExCommandDispatcher.prototype = {
+ processCommand: function(cm, input) {
+ var inputStream = new CodeMirror.StringStream(input);
+ var params = {};
+ params.input = input;
+ try {
+ this.parseInput_(cm, inputStream, params);
+ } catch(e) {
+ showConfirm(cm, e);
+ return;
+ }
+ 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;
+ this.parseCommandArgs_(inputStream, params, command);
+ 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(cm, inputStream, result) {
+ inputStream.eatWhile(':');
+ // Parse range.
+ if (inputStream.eat('%')) {
+ result.line = 0;
+ result.lineEnd = cm.lineCount() - 1;
+ } else {
+ result.line = this.parseLineSpec_(cm, inputStream);
+ if (result.line && inputStream.eat(',')) {
+ result.lineEnd = this.parseLineSpec_(cm, inputStream);
+ }
+ }
- if ((start.line > end.line) || (start.line == end.line && start.ch > end.ch)) var swap = true;
- pushInBuffer(cm.getRange(swap ? end : start, swap ? start : end));
+ // Parse command name.
+ var commandMatch = inputStream.match(/^(\w+)/);
+ if (commandMatch) {
+ result.commandName = commandMatch[1];
+ } else {
+ result.commandName = inputStream.match(/.*/)[0];
+ }
- repeatCount = 0;
- cm.setOption("keyMap", "vim");
+ return result;
+ },
+ parseLineSpec_: function(cm, inputStream) {
+ var numberMatch = inputStream.match(/^(\d+)/);
+ if (numberMatch) {
+ return parseInt(numberMatch[1], 10) - 1;
+ }
+ switch (inputStream.next()) {
+ case '.':
+ return cm.getCursor().line;
+ case '$':
+ return cm.lineCount() - 1;
+ case '\'':
+ var mark = getVimState(cm).marks[inputStream.next()];
+ if (mark && mark.find()) {
+ return mark.find().line;
+ } else {
+ throw "Mark not set";
+ }
+ break;
+ default:
+ inputStream.backUp(1);
+ return cm.getCursor().line;
+ }
+ },
+ parseCommandArgs_: function(inputStream, params, command) {
+ if (inputStream.eol()) {
+ return;
+ }
+ params.argString = inputStream.match(/.*/)[0];
+ // Parse command-line arguments
+ var delim = command.argDelimiter || /\s+/;
+ var args = params.argString.split(delim);
+ if (args.length && args[0]) {
+ params.args = args;
+ }
+ },
+ 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 != ':' && 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 != ':' && 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)
+ });
+ }
+ }
+ }
};
- CodeMirror.keyMap['vim'][key] = function(cm) {
- var cur = motion(cm, repeatCount ? repeatCount : 1);
- cm.setCursor(cur.line, cur.ch);
+ // Converts a key string sequence of the form abd 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;
+ }
- repeatCount = 0;
- };
- });
-
- function addCountBindings(keyMapName) {
- // Add bindings for number keys
- keyMap = CodeMirror.keyMap[keyMapName];
- keyMap["0"] = function(cm) {
- if (repeatCount > 0) {
- pushRepeatCountDigit(0)(cm);
- } else {
- CodeMirror.commands.goLineStart(cm);
+ 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 }});
+ },
+ substitute: function(cm, params) {
+ var argString = params.argString;
+ var slashes = findUnescapedSlashes(argString);
+ if (slashes[0] !== 0) {
+ showConfirm(cm, 'Substitutions should be of the form ' +
+ ':s/pattern/replace/');
+ return;
+ }
+ var regexPart = argString.substring(slashes[0] + 1, slashes[1]);
+ var replacePart = '';
+ var flagsPart;
+ if (slashes[1]) {
+ replacePart = argString.substring(slashes[1] + 1, slashes[2]);
+ }
+ if (slashes[2]) {
+ flagsPart = argString.substring(slashes[2] + 1);
+ }
+ if (flagsPart) {
+ regexPart = regexPart + '/' + flagsPart;
+ }
+ updateSearchQuery(cm, regexPart, true /** ignoreCase */,
+ true /** smartCase */);
+ var state = getSearchState(cm);
+ var query = state.getQuery();
+ var startPos = clipCursorToContent(cm, { line: params.line || 0,
+ ch: 0 });
+ function doReplace() {
+ for (var cursor = cm.getSearchCursor(query, startPos);
+ cursor.findNext();) {
+ if (!isInRange(cursor.from(), params.line, params.lineEnd)) {
+ break;
+ }
+ var text = cm.getRange(cursor.from(), cursor.to());
+ var newText = text.replace(query, replacePart);
+ cursor.replace(newText);
+ }
+ var vim = getVimState(cm);
+ if (vim.visualMode) {
+ exitVisualMode(cm, vim);
+ }
+ }
+ if (cm.compoundChange) {
+ // Only exists in v2
+ cm.compoundChange(doReplace);
+ } else {
+ cm.operation(doReplace);
+ }
+ },
+ 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();
+ }
}
};
- for (var i = 1; i < 10; ++i) {
- keyMap[i] = pushRepeatCountDigit(i);
- }
- }
- addCountBindings('vim');
- addCountBindings('vim-prefix-d');
- addCountBindings('vim-prefix-y');
- addCountBindings('vim-prefix-c');
-
- // Create our keymaps for each operator and make xa and xi where x is an operator
- // change to the corrosponding keymap
- var operators = ['d', 'y', 'c'];
- iterList(operators, function(key, index, array) {
- CodeMirror.keyMap['vim-prefix-'+key+'a'] = {
- auto: 'vim', nofallthrough: true, style: "fat-cursor"
- };
- CodeMirror.keyMap['vim-prefix-'+key+'i'] = {
- auto: 'vim', nofallthrough: true, style: "fat-cursor"
- };
- CodeMirror.keyMap['vim-prefix-'+key]['A'] = function(cm) {
- repeatCount = 0;
- cm.setOption('keyMap', 'vim-prefix-' + key + 'a');
- };
+ 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);
+ };
+ }
- CodeMirror.keyMap['vim-prefix-'+key]['I'] = function(cm) {
- repeatCount = 0;
- cm.setOption('keyMap', 'vim-prefix-' + key + 'i');
+ 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_
+ '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']
};
- });
-
- function regexLastIndexOf(string, pattern, startIndex) {
- for (var i = startIndex == null ? string.length : startIndex; i >= 0; --i)
- if (pattern.test(string.charAt(i))) return i;
- return -1;
- }
-
- // Create our text object functions. They work similar to motions but they
- // return a start cursor as well
- var textObjectList = ['W', 'Shift-[', 'Shift-9', '[', "'", "Shift-'"];
- var textObjects = {
- 'W': function(cm, inclusive) {
- var cur = cm.getCursor();
- var line = cm.getLine(cur.line);
- var line_to_char = new String(line.substring(0, cur.ch));
- var start = regexLastIndexOf(line_to_char, /[^a-zA-Z0-9]/) + 1;
- var end = motions["E"](cm, 1) ;
-
- end.ch += inclusive ? 1 : 0 ;
- return {start: {line: cur.line, ch: start}, end: end };
- },
- 'Shift-[': function(cm, inclusive) { return selectCompanionObject(cm, '}', inclusive); },
- 'Shift-9': function(cm, inclusive) { return selectCompanionObject(cm, ')', inclusive); },
- '[': function(cm, inclusive) { return selectCompanionObject(cm, ']', inclusive); },
- "'": function(cm, inclusive) { return findBeginningAndEnd(cm, "'", inclusive); },
- "Shift-'": function(cm, inclusive) { return findBeginningAndEnd(cm, '"', inclusive); }
+ return vimApi;
};
-
- // One function to handle all operation upon text objects. Kinda funky but it works
- // better than rewriting this code six times
- function textObjectManipulation(cm, object, remove, insert, inclusive) {
- // Object is the text object, delete object if remove is true, enter insert
- // mode if insert is true, inclusive is the difference between a and i
- var tmp = textObjects[object](cm, inclusive);
- var start = tmp.start;
- var end = tmp.end;
-
- if ((start.line > end.line) || (start.line == end.line && start.ch > end.ch)) var swap = true ;
-
- pushInBuffer(cm.getRange(swap ? end : start, swap ? start : end));
- if (remove) cm.replaceRange("", swap ? end : start, swap ? start : end);
- if (insert) cm.setOption('keyMap', 'vim-insert');
- }
-
- // And finally build the keymaps up from the text objects
- for (var i = 0; i < textObjectList.length; ++i) {
- var object = textObjectList[i];
- (function(object) {
- CodeMirror.keyMap['vim-prefix-di'][object] = function(cm) { textObjectManipulation(cm, object, true, false, false); };
- CodeMirror.keyMap['vim-prefix-da'][object] = function(cm) { textObjectManipulation(cm, object, true, false, true); };
- CodeMirror.keyMap['vim-prefix-yi'][object] = function(cm) { textObjectManipulation(cm, object, false, false, false); };
- CodeMirror.keyMap['vim-prefix-ya'][object] = function(cm) { textObjectManipulation(cm, object, false, false, true); };
- CodeMirror.keyMap['vim-prefix-ci'][object] = function(cm) { textObjectManipulation(cm, object, true, true, false); };
- CodeMirror.keyMap['vim-prefix-ca'][object] = function(cm) { textObjectManipulation(cm, object, true, true, true); };
- })(object)
- }
-})();
+ // Initialize Vim and make it available as an API.
+ var vim = Vim();
+ CodeMirror.Vim = vim;
+}
+)();
diff --git a/lib/codemirror.css b/lib/codemirror.css
index 41b8d09e13..38bf55f952 100644
--- a/lib/codemirror.css
+++ b/lib/codemirror.css
@@ -153,6 +153,8 @@ div.CodeMirror-selected { background: #d9d9d9; }
.cm-s-default span.cm-quote {color: #090;}
.cm-s-default span.cm-hr {color: #999;}
.cm-s-default span.cm-link {color: #00c;}
+span.cm-negative {color: #d44;}
+span.cm-positive {color: #292;}
span.cm-header, span.cm-strong {font-weight: bold;}
span.cm-em {font-style: italic;}
diff --git a/lib/codemirror.js b/lib/codemirror.js
index 0c15442a0d..c8bf6d8317 100644
--- a/lib/codemirror.js
+++ b/lib/codemirror.js
@@ -168,6 +168,7 @@ window.CodeMirror = (function() {
else if (option == "tabSize") updateDisplay(true);
else if (option == "keyMap") keyMapChanged();
else if (option == "tabindex") input.tabIndex = value;
+ else if (option == "showCursorWhenSelecting") updateSelection();
if (option == "lineNumbers" || option == "gutter" || option == "firstLineNumber" ||
option == "theme" || option == "lineNumberFormatter") {
gutterChanged();
@@ -280,7 +281,9 @@ window.CodeMirror = (function() {
lineCount: function() {return doc.size;},
clipPos: clipPos,
getCursor: function(start) {
- if (start == null) start = sel.inverted;
+ if (start == null || start == "head") start = sel.inverted;
+ if (start == "anchor") start = !sel.inverted;
+ if (start == "end") start = false;
return copyPos(start ? sel.from : sel.to);
},
somethingSelected: function() {return !posEq(sel.from, sel.to);},
@@ -493,8 +496,12 @@ window.CodeMirror = (function() {
function doSelect(cur) {
if (type == "single") {
- setSelectionUser(start, cur);
- } else if (type == "double") {
+ setSelectionUser(clipPos(start), cur);
+ return;
+ }
+ startstart = clipPos(startstart);
+ startend = clipPos(startend);
+ if (type == "double") {
var word = findWordAt(cur);
if (posLess(cur, startstart)) setSelectionUser(word.from, startend);
else setSelectionUser(startstart, word.to);
@@ -619,7 +626,6 @@ window.CodeMirror = (function() {
}, 50);
var name = keyNames[e_prop(e, "keyCode")], handled = false;
- var flipCtrlCmd = opera && mac;
if (name == null || e.altGraphKey) return false;
if (e_prop(e, "altKey")) name = "Alt-" + name;
if (e_prop(e, flipCtrlCmd ? "metaKey" : "ctrlKey")) name = "Ctrl-" + name;
@@ -979,7 +985,8 @@ window.CodeMirror = (function() {
}
function focusInput() {
- if (options.readOnly != "nocursor") input.focus();
+ if (options.readOnly != "nocursor" && (ie_lt9 || document.activeElement != input))
+ input.focus();
}
function scrollCursorIntoView() {
@@ -1244,18 +1251,20 @@ window.CodeMirror = (function() {
var wrapOff = eltOffset(wrapper), lineOff = eltOffset(lineDiv);
inputDiv.style.top = Math.max(0, Math.min(scroller.offsetHeight, headPos.y + lineOff.top - wrapOff.top)) + "px";
inputDiv.style.left = Math.max(0, Math.min(scroller.offsetWidth, headPos.x + lineOff.left - wrapOff.left)) + "px";
- if (collapsed) {
+ if (collapsed || options.showCursorWhenSelecting) {
cursor.style.top = headPos.y + "px";
cursor.style.left = (options.lineWrapping ? Math.min(headPos.x, lineSpace.offsetWidth) : headPos.x) + "px";
cursor.style.display = "";
- selectionDiv.style.display = "none";
} else {
+ cursor.style.display = "none";
+ }
+ if (!collapsed) {
var sameLine = fromPos.y == toPos.y, fragment = document.createDocumentFragment();
var clientWidth = lineSpace.clientWidth || lineSpace.offsetWidth;
var clientHeight = lineSpace.clientHeight || lineSpace.offsetHeight;
var add = function(left, top, right, height) {
var rstyle = quirksMode ? "width: " + (!right ? clientWidth : clientWidth - right - left) + "px"
- : "right: " + right + "px";
+ : "right: " + (right - 1) + "px";
fragment.appendChild(elt("div", null, "CodeMirror-selected", "position: absolute; left: " + left +
"px; top: " + top + "px; " + rstyle + "; height: " + height + "px"));
};
@@ -1270,8 +1279,9 @@ window.CodeMirror = (function() {
if ((!sameLine || !sel.from.ch) && toPos.y < clientHeight - .5 * th)
add(0, toPos.y, clientWidth - toPos.x, th);
removeChildrenAndAdd(selectionDiv, fragment);
- cursor.style.display = "none";
selectionDiv.style.display = "";
+ } else {
+ selectionDiv.style.display = "none";
}
}
@@ -2009,6 +2019,7 @@ window.CodeMirror = (function() {
gutter: false,
fixedGutter: false,
firstLineNumber: 1,
+ showCursorWhenSelecting: false,
readOnly: false,
dragDrop: true,
onChange: null,
@@ -2060,7 +2071,11 @@ window.CodeMirror = (function() {
var modeObj = mfactory(options, spec);
if (modeExtensions.hasOwnProperty(spec.name)) {
var exts = modeExtensions[spec.name];
- for (var prop in exts) if (exts.hasOwnProperty(prop)) modeObj[prop] = exts[prop];
+ for (var prop in exts) {
+ if (!exts.hasOwnProperty(prop)) continue;
+ if (modeObj.hasOwnProperty(prop)) modeObj["_" + prop] = modeObj[prop];
+ modeObj[prop] = exts[prop];
+ }
}
modeObj.name = spec.name;
return modeObj;
@@ -2236,12 +2251,12 @@ window.CodeMirror = (function() {
if (textarea.form) {
// Deplorable hack to make the submit method do the right thing.
var rmSubmit = connect(textarea.form, "submit", save, true);
- var realSubmit = textarea.form.submit;
+ var form = textarea.form, realSubmit = form.submit;
textarea.form.submit = function wrappedSubmit() {
save();
- textarea.form.submit = realSubmit;
- textarea.form.submit();
- textarea.form.submit = wrappedSubmit;
+ form.submit = realSubmit;
+ form.submit();
+ form.submit = wrappedSubmit;
};
}
@@ -2264,18 +2279,24 @@ window.CodeMirror = (function() {
return instance;
};
- var gecko = /gecko\/\d{7}/i.test(navigator.userAgent);
+ var gecko = /gecko\/\d/i.test(navigator.userAgent);
var ie = /MSIE \d/.test(navigator.userAgent);
var ie_lt8 = /MSIE [1-7]\b/.test(navigator.userAgent);
var ie_lt9 = /MSIE [1-8]\b/.test(navigator.userAgent);
var quirksMode = ie && document.documentMode == 5;
var webkit = /WebKit\//.test(navigator.userAgent);
+ var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(navigator.userAgent);
var chrome = /Chrome\//.test(navigator.userAgent);
var opera = /Opera\//.test(navigator.userAgent);
var safari = /Apple Computer/.test(navigator.vendor);
var khtml = /KHTML\//.test(navigator.userAgent);
var mac_geLion = /Mac OS X 10\D([7-9]|\d\d)\D/.test(navigator.userAgent);
+ var opera_version = opera && navigator.userAgent.match(/Version\/(\d*\.\d*)/);
+ if (opera_version) opera_version = Number(opera_version[1]);
+ // Some browsers use the wrong event properties to signal cmd/ctrl on OS X
+ var flipCtrlCmd = mac && (qtwebkit || opera && (opera_version == null || opera_version < 12.11));
+
// Utility functions for working with state. Exported because modes
// sometimes need to do this.
function copyState(mode, state) {
@@ -2844,7 +2865,7 @@ window.CodeMirror = (function() {
if (line.parent == null) return null;
var cur = line.parent, no = indexOf(cur.lines, line);
for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) {
- for (var i = 0, e = chunk.children.length; ; ++i) {
+ for (var i = 0; ; ++i) {
if (chunk.children[i] == cur) break;
no += chunk.children[i].chunkSize();
}
@@ -3163,7 +3184,7 @@ window.CodeMirror = (function() {
for (var i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i;
})();
- CodeMirror.version = "2.36";
+ CodeMirror.version = "2.37";
return CodeMirror;
})();
diff --git a/lib/util/closetag.js b/lib/util/closetag.js
index 5096678473..0688a8875a 100644
--- a/lib/util/closetag.js
+++ b/lib/util/closetag.js
@@ -1,164 +1,165 @@
-/**
- * Tag-closer extension for CodeMirror.
- *
- * This extension adds a "closeTag" utility function that can be used with key bindings to
- * insert a matching end tag after the ">" character of a start tag has been typed. It can
- * also complete "" if a matching start tag is found. It will correctly ignore signal
- * characters for empty tags, comments, CDATA, etc.
- *
- * The function depends on internal parser state to identify tags. It is compatible with the
- * following CodeMirror modes and will ignore all others:
- * - htmlmixed
- * - xml
- *
- * See demos/closetag.html for a usage example.
- *
- * @author Nathan Williams
- * Contributed under the same license terms as CodeMirror.
- */
-(function() {
- /** Option that allows tag closing behavior to be toggled. Default is true. */
- CodeMirror.defaults['closeTagEnabled'] = true;
-
- /** Array of tag names to add indentation after the start tag for. Default is the list of block-level html tags. */
- CodeMirror.defaults['closeTagIndent'] = ['applet', 'blockquote', 'body', 'button', 'div', 'dl', 'fieldset', 'form', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'html', 'iframe', 'layer', 'legend', 'object', 'ol', 'p', 'select', 'table', 'ul'];
-
- /** Array of tag names where an end tag is forbidden. */
- CodeMirror.defaults['closeTagVoid'] = ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
-
- function innerState(cm, state) {
- return CodeMirror.innerMode(cm.getMode(), state).state;
- }
-
-
- /**
- * Call during key processing to close tags. Handles the key event if the tag is closed, otherwise throws CodeMirror.Pass.
- * - cm: The editor instance.
- * - ch: The character being processed.
- * - indent: Optional. An array of tag names to indent when closing. Omit or pass true to use the default indentation tag list defined in the 'closeTagIndent' option.
- * Pass false to disable indentation. Pass an array to override the default list of tag names.
- * - vd: Optional. An array of tag names that should not be closed. Omit to use the default void (end tag forbidden) tag list defined in the 'closeTagVoid' option. Ignored in xml mode.
- */
- CodeMirror.defineExtension("closeTag", function(cm, ch, indent, vd) {
- if (!cm.getOption('closeTagEnabled')) {
- throw CodeMirror.Pass;
- }
-
- /*
- * Relevant structure of token:
- *
- * htmlmixed
- * className
- * state
- * htmlState
- * type
- * tagName
- * context
- * tagName
- * mode
- *
- * xml
- * className
- * state
- * tagName
- * type
- */
-
- var pos = cm.getCursor();
- var tok = cm.getTokenAt(pos);
- var state = innerState(cm, tok.state);
-
- if (state) {
-
- if (ch == '>') {
- var type = state.type;
-
- if (tok.className == 'tag' && type == 'closeTag') {
- throw CodeMirror.Pass; // Don't process the '>' at the end of an end-tag.
- }
-
- cm.replaceSelection('>'); // Mode state won't update until we finish the tag.
- pos = {line: pos.line, ch: pos.ch + 1};
- cm.setCursor(pos);
-
- tok = cm.getTokenAt(cm.getCursor());
- state = innerState(cm, tok.state);
- if (!state) throw CodeMirror.Pass;
- var type = state.type;
-
- if (tok.className == 'tag' && type != 'selfcloseTag') {
- var tagName = state.tagName;
- if (tagName.length > 0 && shouldClose(cm, vd, tagName)) {
- insertEndTag(cm, indent, pos, tagName);
- }
- return;
- }
-
- // Undo the '>' insert and allow cm to handle the key instead.
- cm.setSelection({line: pos.line, ch: pos.ch - 1}, pos);
- cm.replaceSelection("");
-
- } else if (ch == '/') {
- if (tok.className == 'tag' && tok.string == '<') {
- var ctx = state.context, tagName = ctx ? ctx.tagName : '';
- if (tagName.length > 0) {
- completeEndTag(cm, pos, tagName);
- return;
- }
- }
- }
-
- }
-
- throw CodeMirror.Pass; // Bubble if not handled
- });
-
- function insertEndTag(cm, indent, pos, tagName) {
- if (shouldIndent(cm, indent, tagName)) {
- cm.replaceSelection('\n\n' + tagName + '>', 'end');
- cm.indentLine(pos.line + 1);
- cm.indentLine(pos.line + 2);
- cm.setCursor({line: pos.line + 1, ch: cm.getLine(pos.line + 1).length});
- } else {
- cm.replaceSelection('' + tagName + '>');
- cm.setCursor(pos);
- }
- }
-
- function shouldIndent(cm, indent, tagName) {
- if (typeof indent == 'undefined' || indent == null || indent == true) {
- indent = cm.getOption('closeTagIndent');
- }
- if (!indent) {
- indent = [];
- }
- return indexOf(indent, tagName.toLowerCase()) != -1;
- }
-
- function shouldClose(cm, vd, tagName) {
- if (cm.getOption('mode') == 'xml') {
- return true; // always close xml tags
- }
- if (typeof vd == 'undefined' || vd == null) {
- vd = cm.getOption('closeTagVoid');
- }
- if (!vd) {
- vd = [];
- }
- return indexOf(vd, tagName.toLowerCase()) == -1;
- }
-
- // C&P from codemirror.js...would be nice if this were visible to utilities.
- function indexOf(collection, elt) {
- if (collection.indexOf) return collection.indexOf(elt);
- for (var i = 0, e = collection.length; i < e; ++i)
- if (collection[i] == elt) return i;
- return -1;
- }
-
- function completeEndTag(cm, pos, tagName) {
- cm.replaceSelection('/' + tagName + '>');
- cm.setCursor({line: pos.line, ch: pos.ch + tagName.length + 2 });
- }
-
-})();
+/**
+ * Tag-closer extension for CodeMirror.
+ *
+ * This extension adds a "closeTag" utility function that can be used with key bindings to
+ * insert a matching end tag after the ">" character of a start tag has been typed. It can
+ * also complete "" if a matching start tag is found. It will correctly ignore signal
+ * characters for empty tags, comments, CDATA, etc.
+ *
+ * The function depends on internal parser state to identify tags. It is compatible with the
+ * following CodeMirror modes and will ignore all others:
+ * - htmlmixed
+ * - xml
+ *
+ * See demos/closetag.html for a usage example.
+ *
+ * @author Nathan Williams
+ * Contributed under the same license terms as CodeMirror.
+ */
+(function() {
+ /** Option that allows tag closing behavior to be toggled. Default is true. */
+ CodeMirror.defaults['closeTagEnabled'] = true;
+
+ /** Array of tag names to add indentation after the start tag for. Default is the list of block-level html tags. */
+ CodeMirror.defaults['closeTagIndent'] = ['applet', 'blockquote', 'body', 'button', 'div', 'dl', 'fieldset', 'form', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'html', 'iframe', 'layer', 'legend', 'object', 'ol', 'p', 'select', 'table', 'ul'];
+
+ /** Array of tag names where an end tag is forbidden. */
+ CodeMirror.defaults['closeTagVoid'] = ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
+
+ function innerXMLState(cm, state) {
+ var inner = CodeMirror.innerMode(cm.getMode(), state);
+ if (inner.mode.name == "xml") return inner.state;
+ }
+
+
+ /**
+ * Call during key processing to close tags. Handles the key event if the tag is closed, otherwise throws CodeMirror.Pass.
+ * - cm: The editor instance.
+ * - ch: The character being processed.
+ * - indent: Optional. An array of tag names to indent when closing. Omit or pass true to use the default indentation tag list defined in the 'closeTagIndent' option.
+ * Pass false to disable indentation. Pass an array to override the default list of tag names.
+ * - vd: Optional. An array of tag names that should not be closed. Omit to use the default void (end tag forbidden) tag list defined in the 'closeTagVoid' option. Ignored in xml mode.
+ */
+ CodeMirror.defineExtension("closeTag", function(cm, ch, indent, vd) {
+ if (!cm.getOption('closeTagEnabled')) {
+ throw CodeMirror.Pass;
+ }
+
+ /*
+ * Relevant structure of token:
+ *
+ * htmlmixed
+ * className
+ * state
+ * htmlState
+ * type
+ * tagName
+ * context
+ * tagName
+ * mode
+ *
+ * xml
+ * className
+ * state
+ * tagName
+ * type
+ */
+
+ var pos = cm.getCursor();
+ var tok = cm.getTokenAt(pos);
+ var state = innerXMLState(cm, tok.state);
+
+ if (state) {
+
+ if (ch == '>') {
+ var type = state.type;
+
+ if (tok.className == 'tag' && type == 'closeTag') {
+ throw CodeMirror.Pass; // Don't process the '>' at the end of an end-tag.
+ }
+
+ cm.replaceSelection('>'); // Mode state won't update until we finish the tag.
+ pos = {line: pos.line, ch: pos.ch + 1};
+ cm.setCursor(pos);
+
+ tok = cm.getTokenAt(cm.getCursor());
+ state = innerXMLState(cm, tok.state);
+ if (!state) throw CodeMirror.Pass;
+ var type = state.type;
+
+ if (tok.className == 'tag' && type != 'selfcloseTag') {
+ var tagName = state.tagName;
+ if (tagName.length > 0 && shouldClose(cm, vd, tagName)) {
+ insertEndTag(cm, indent, pos, tagName);
+ }
+ return;
+ }
+
+ // Undo the '>' insert and allow cm to handle the key instead.
+ cm.setSelection({line: pos.line, ch: pos.ch - 1}, pos);
+ cm.replaceSelection("");
+
+ } else if (ch == '/') {
+ if (tok.className == 'tag' && tok.string == '<') {
+ var ctx = state.context, tagName = ctx ? ctx.tagName : '';
+ if (tagName.length > 0) {
+ completeEndTag(cm, pos, tagName);
+ return;
+ }
+ }
+ }
+
+ }
+
+ throw CodeMirror.Pass; // Bubble if not handled
+ });
+
+ function insertEndTag(cm, indent, pos, tagName) {
+ if (shouldIndent(cm, indent, tagName)) {
+ cm.replaceSelection('\n\n' + tagName + '>', 'end');
+ cm.indentLine(pos.line + 1);
+ cm.indentLine(pos.line + 2);
+ cm.setCursor({line: pos.line + 1, ch: cm.getLine(pos.line + 1).length});
+ } else {
+ cm.replaceSelection('' + tagName + '>');
+ cm.setCursor(pos);
+ }
+ }
+
+ function shouldIndent(cm, indent, tagName) {
+ if (typeof indent == 'undefined' || indent == null || indent == true) {
+ indent = cm.getOption('closeTagIndent');
+ }
+ if (!indent) {
+ indent = [];
+ }
+ return indexOf(indent, tagName.toLowerCase()) != -1;
+ }
+
+ function shouldClose(cm, vd, tagName) {
+ if (cm.getOption('mode') == 'xml') {
+ return true; // always close xml tags
+ }
+ if (typeof vd == 'undefined' || vd == null) {
+ vd = cm.getOption('closeTagVoid');
+ }
+ if (!vd) {
+ vd = [];
+ }
+ return indexOf(vd, tagName.toLowerCase()) == -1;
+ }
+
+ // C&P from codemirror.js...would be nice if this were visible to utilities.
+ function indexOf(collection, elt) {
+ if (collection.indexOf) return collection.indexOf(elt);
+ for (var i = 0, e = collection.length; i < e; ++i)
+ if (collection[i] == elt) return i;
+ return -1;
+ }
+
+ function completeEndTag(cm, pos, tagName) {
+ cm.replaceSelection('/' + tagName + '>');
+ cm.setCursor({line: pos.line, ch: pos.ch + tagName.length + 2 });
+ }
+
+})();
diff --git a/lib/util/continuelist.js b/lib/util/continuelist.js
new file mode 100644
index 0000000000..8a2b8653e2
--- /dev/null
+++ b/lib/util/continuelist.js
@@ -0,0 +1,29 @@
+(function() {
+ CodeMirror.commands.newlineAndIndentContinueMarkdownList = function(cm) {
+ var pos = cm.getCursor(), token = cm.getTokenAt(pos);
+ var mode = CodeMirror.innerMode(cm.getMode(), token.state).mode;
+ var space;
+ if (token.className == "string") {
+ var full = cm.getRange({line: pos.line, ch: 0}, {line: pos.line, ch: token.end});
+ var listStart = /\*|\d+\./, listContinue;
+ if (token.string.search(listStart) == 0) {
+ var reg = /^[\W]*(\d+)\./g;
+ var matches = reg.exec(full);
+ if(matches)
+ listContinue = (parseInt(matches[1]) + 1) + ". ";
+ else
+ listContinue = "* ";
+ space = full.slice(0, token.start);
+ if (!/^\s*$/.test(space)) {
+ space = "";
+ for (var i = 0; i < token.start; ++i) space += " ";
+ }
+ }
+ }
+
+ if (space != null)
+ cm.replaceSelection("\n" + space + listContinue, "end");
+ else
+ cm.execCommand("newlineAndIndent");
+ };
+})();
diff --git a/lib/util/dialog.css b/lib/util/dialog.css
index 8c4f847920..2e7c0fc9b8 100644
--- a/lib/util/dialog.css
+++ b/lib/util/dialog.css
@@ -1,18 +1,23 @@
.CodeMirror-dialog {
- position: relative;
-}
-
-.CodeMirror-dialog > div {
position: absolute;
- top: 0; left: 0; right: 0;
+ left: 0; right: 0;
background: white;
- border-bottom: 1px solid #eee;
z-index: 15;
padding: .1em .8em;
overflow: hidden;
color: #333;
}
+.CodeMirror-dialog-top {
+ border-bottom: 1px solid #eee;
+ top: 0;
+}
+
+.CodeMirror-dialog-bottom {
+ border-top: 1px solid #eee;
+ bottom: 0;
+}
+
.CodeMirror-dialog input {
border: none;
outline: none;
@@ -24,4 +29,4 @@
.CodeMirror-dialog button {
font-size: 70%;
-}
\ No newline at end of file
+}
diff --git a/lib/util/dialog.js b/lib/util/dialog.js
index 7aad7eaecf..374f936cc7 100644
--- a/lib/util/dialog.js
+++ b/lib/util/dialog.js
@@ -1,16 +1,21 @@
// Open simple dialogs on top of an editor. Relies on dialog.css.
(function() {
- function dialogDiv(cm, template) {
+ function dialogDiv(cm, template, bottom) {
var wrap = cm.getWrapperElement();
- var dialog = wrap.insertBefore(document.createElement("div"), wrap.firstChild);
- dialog.className = "CodeMirror-dialog";
- dialog.innerHTML = '' + template + '
';
+ var dialog;
+ dialog = wrap.appendChild(document.createElement("div"));
+ if (bottom) {
+ dialog.className = "CodeMirror-dialog CodeMirror-dialog-bottom";
+ } else {
+ dialog.className = "CodeMirror-dialog CodeMirror-dialog-top";
+ }
+ dialog.innerHTML = template;
return dialog;
}
- CodeMirror.defineExtension("openDialog", function(template, callback) {
- var dialog = dialogDiv(this, template);
+ CodeMirror.defineExtension("openDialog", function(template, callback, options) {
+ var dialog = dialogDiv(this, template, options && options.bottom);
var closed = false, me = this;
function close() {
if (closed) return;
@@ -27,6 +32,7 @@
if (e.keyCode == 13) callback(inp.value);
}
});
+ if (options && options.value) inp.value = options.value;
inp.focus();
CodeMirror.connect(inp, "blur", close);
} else if (button = dialog.getElementsByTagName("button")[0]) {
@@ -40,8 +46,8 @@
return close;
});
- CodeMirror.defineExtension("openConfirm", function(template, callbacks) {
- var dialog = dialogDiv(this, template);
+ CodeMirror.defineExtension("openConfirm", function(template, callbacks, options) {
+ var dialog = dialogDiv(this, template, options && options.bottom);
var buttons = dialog.getElementsByTagName("button");
var closed = false, me = this, blurring = 1;
function close() {
@@ -67,4 +73,4 @@
CodeMirror.connect(b, "focus", function() { ++blurring; });
}
});
-})();
\ No newline at end of file
+})();
diff --git a/lib/util/formatting.js b/lib/util/formatting.js
index 00ffe78720..b5ebaeb969 100644
--- a/lib/util/formatting.js
+++ b/lib/util/formatting.js
@@ -1,138 +1,39 @@
-// ============== Formatting extensions ============================
(function() {
- // Define extensions for a few modes
+
CodeMirror.extendMode("css", {
commentStart: "/*",
commentEnd: "*/",
- wordWrapChars: [";", "\\{", "\\}"],
- autoFormatLineBreaks: function (text) {
- return text.replace(new RegExp("(;|\\{|\\})([^\r\n])", "g"), "$1\n$2");
+ newlineAfterToken: function(type, content) {
+ return /^[;{}]$/.test(content);
}
});
- function jsNonBreakableBlocks(text) {
- var nonBreakableRegexes = [/for\s*?\((.*?)\)/,
- /\"(.*?)(\"|$)/,
- /\'(.*?)(\'|$)/,
- /\/\*(.*?)(\*\/|$)/,
- /\/\/.*/];
- var nonBreakableBlocks = [];
- for (var i = 0; i < nonBreakableRegexes.length; i++) {
- var curPos = 0;
- while (curPos < text.length) {
- var m = text.substr(curPos).match(nonBreakableRegexes[i]);
- if (m != null) {
- nonBreakableBlocks.push({
- start: curPos + m.index,
- end: curPos + m.index + m[0].length
- });
- curPos += m.index + Math.max(1, m[0].length);
- }
- else { // No more matches
- break;
- }
- }
- }
- nonBreakableBlocks.sort(function (a, b) {
- return a.start - b.start;
- });
-
- return nonBreakableBlocks;
- }
-
CodeMirror.extendMode("javascript", {
commentStart: "/*",
commentEnd: "*/",
- wordWrapChars: [";", "\\{", "\\}"],
-
- autoFormatLineBreaks: function (text) {
- var curPos = 0;
- var split = this.jsonMode ? function(str) {
- return str.replace(/([,{])/g, "$1\n").replace(/}/g, "\n}");
- } : function(str) {
- return str.replace(/(;|\{|\})([^\r\n;])/g, "$1\n$2");
- };
- var nonBreakableBlocks = jsNonBreakableBlocks(text), res = "";
- if (nonBreakableBlocks != null) {
- for (var i = 0; i < nonBreakableBlocks.length; i++) {
- if (nonBreakableBlocks[i].start > curPos) { // Break lines till the block
- res += split(text.substring(curPos, nonBreakableBlocks[i].start));
- curPos = nonBreakableBlocks[i].start;
- }
- if (nonBreakableBlocks[i].start <= curPos
- && nonBreakableBlocks[i].end >= curPos) { // Skip non-breakable block
- res += text.substring(curPos, nonBreakableBlocks[i].end);
- curPos = nonBreakableBlocks[i].end;
- }
- }
- if (curPos < text.length)
- res += split(text.substr(curPos));
+ // FIXME semicolons inside of for
+ newlineAfterToken: function(type, content, textAfter, state) {
+ if (this.jsonMode) {
+ return /^[\[,{]$/.test(content) || /^}/.test(textAfter);
} else {
- res = split(text);
+ if (content == ";" && state.lexical && state.lexical.type == ")") return false;
+ return /^[;{}]$/.test(content) && !/^;/.test(textAfter);
}
- return res.replace(/^\n*|\n*$/, "");
}
});
CodeMirror.extendMode("xml", {
commentStart: "",
- wordWrapChars: [">"],
-
- autoFormatLineBreaks: function (text) {
- var lines = text.split("\n");
- var reProcessedPortion = new RegExp("(^\\s*?<|^[^<]*?)(.+)(>\\s*?$|[^>]*?$)");
- var reOpenBrackets = new RegExp("<", "g");
- var reCloseBrackets = new RegExp("(>)([^\r\n])", "g");
- for (var i = 0; i < lines.length; i++) {
- var mToProcess = lines[i].match(reProcessedPortion);
- if (mToProcess != null && mToProcess.length > 3) { // The line starts with whitespaces and ends with whitespaces
- lines[i] = mToProcess[1]
- + mToProcess[2].replace(reOpenBrackets, "\n$&").replace(reCloseBrackets, "$1\n$2")
- + mToProcess[3];
- continue;
- }
- }
- return lines.join("\n");
+ newlineAfterToken: function(type, content, textAfter) {
+ return type == "tag" && />$/.test(content) || /^") cut = stream.pos;
- found.push({from: start, to: cut, mode: mode});
- start = cut;
- mode = curMode;
- }
- if (stream.pos >= end) break;
- stream.start = stream.pos;
- }
- if (start < end) found.push({from: start, to: end, mode: mode});
- return found;
- }
-
// Comment/uncomment the specified range
CodeMirror.defineExtension("commentRange", function (isComment, from, to) {
- var curMode = localModeAt(this, from), cm = this;
- this.operation(function() {
+ var cm = this, curMode = CodeMirror.innerMode(cm.getMode(), cm.getTokenAt(from).state).mode;
+ cm.operation(function() {
if (isComment) { // Comment range
cm.replaceRange(curMode.commentEnd, to);
cm.replaceRange(curMode.commentStart, from);
@@ -168,27 +69,38 @@
// Applies automatic formatting to the specified range
CodeMirror.defineExtension("autoFormatRange", function (from, to) {
var cm = this;
- cm.operation(function () {
- for (var cur = from.line, end = to.line; cur <= end; ++cur) {
- var f = {line: cur, ch: cur == from.line ? from.ch : 0};
- var t = {line: cur, ch: cur == end ? to.ch : null};
- var modes = enumerateModesBetween(cm, cur, f.ch, t.ch), mangled = "";
- var text = cm.getRange(f, t);
- for (var i = 0; i < modes.length; ++i) {
- var part = modes.length > 1 ? text.slice(modes[i].from, modes[i].to) : text;
- if (mangled) mangled += "\n";
- if (modes[i].mode.autoFormatLineBreaks) {
- mangled += modes[i].mode.autoFormatLineBreaks(part);
- } else mangled += text;
- }
- if (mangled != text) {
- for (var count = 0, pos = mangled.indexOf("\n"); pos != -1; pos = mangled.indexOf("\n", pos + 1), ++count) {}
- cm.replaceRange(mangled, f, t);
- cur += count;
- end += count;
+ var outer = cm.getMode(), text = cm.getRange(from, to).split("\n");
+ var state = CodeMirror.copyState(outer, cm.getTokenAt(from).state);
+ var tabSize = cm.getOption("tabSize");
+
+ var out = "", lines = 0, atSol = from.ch == 0;
+ function newline() {
+ out += "\n";
+ atSol = true;
+ ++lines;
+ }
+
+ for (var i = 0; i < text.length; ++i) {
+ var stream = new CodeMirror.StringStream(text[i], tabSize);
+ while (!stream.eol()) {
+ var inner = CodeMirror.innerMode(outer, state);
+ var style = outer.token(stream, state), cur = stream.current();
+ stream.start = stream.pos;
+ if (!atSol || /\S/.test(cur)) {
+ out += cur;
+ atSol = false;
}
+ if (!atSol && inner.mode.newlineAfterToken &&
+ inner.mode.newlineAfterToken(style, cur, stream.string.slice(stream.pos) || text[i+1] || "", inner.state))
+ newline();
}
- for (var cur = from.line + 1; cur <= end; ++cur)
+ if (!stream.pos && outer.blankLine) outer.blankLine(state);
+ if (!atSol) newline();
+ }
+
+ cm.operation(function () {
+ cm.replaceRange(out, from, to);
+ for (var cur = from.line + 1, end = from.line + lines; cur <= end; ++cur)
cm.indentLine(cur, "smart");
cm.setSelection(from, cm.getCursor(false));
});
diff --git a/lib/util/javascript-hint.js b/lib/util/javascript-hint.js
index ff15adb3ac..f5e4ff760c 100644
--- a/lib/util/javascript-hint.js
+++ b/lib/util/javascript-hint.js
@@ -16,7 +16,7 @@
return arr.indexOf(item) != -1;
}
- function scriptHint(editor, keywords, getToken) {
+ function scriptHint(editor, keywords, getToken, options) {
// Find the token at the cursor
var cur = editor.getCursor(), token = getToken(editor, cur), tprop = token;
// If it's not a 'word-style' token, ignore the token.
@@ -47,14 +47,15 @@
if (!context) var context = [];
context.push(tprop);
}
- return {list: getCompletions(token, context, keywords),
+ return {list: getCompletions(token, context, keywords, options),
from: {line: cur.line, ch: token.start},
to: {line: cur.line, ch: token.end}};
}
- CodeMirror.javascriptHint = function(editor) {
+ CodeMirror.javascriptHint = function(editor, options) {
return scriptHint(editor, javascriptKeywords,
- function (e, cur) {return e.getTokenAt(cur);});
+ function (e, cur) {return e.getTokenAt(cur);},
+ options);
};
function getCoffeeScriptToken(editor, cur) {
@@ -75,8 +76,8 @@
return token;
}
- CodeMirror.coffeescriptHint = function(editor) {
- return scriptHint(editor, coffeescriptKeywords, getCoffeeScriptToken);
+ CodeMirror.coffeescriptHint = function(editor, options) {
+ return scriptHint(editor, coffeescriptKeywords, getCoffeeScriptToken, options);
};
var stringProps = ("charAt charCodeAt indexOf lastIndexOf substring substr slice trim trimLeft trimRight " +
@@ -89,7 +90,7 @@
var coffeescriptKeywords = ("and break catch class continue delete do else extends false finally for " +
"if in instanceof isnt new no not null of off on or return switch then throw true try typeof until void while with yes").split(" ");
- function getCompletions(token, context, keywords) {
+ function getCompletions(token, context, keywords, options) {
var found = [], start = token.string;
function maybeAdd(str) {
if (str.indexOf(start) == 0 && !arrayContains(found, str)) found.push(str);
@@ -105,13 +106,15 @@
// If this is a property, see if it belongs to some object we can
// find in the current environment.
var obj = context.pop(), base;
- if (obj.className == "variable")
- base = window[obj.string];
- else if (obj.className == "string")
+ if (obj.className == "variable") {
+ if (options && options.additionalContext)
+ base = options.additionalContext[obj.string];
+ base = base || window[obj.string];
+ } else if (obj.className == "string") {
base = "";
- else if (obj.className == "atom")
+ } else if (obj.className == "atom") {
base = 1;
- else if (obj.className == "function") {
+ } else if (obj.className == "function") {
if (window.jQuery != null && (obj.string == '$' || obj.string == 'jQuery') &&
(typeof window.jQuery == 'function'))
base = window.jQuery();
@@ -124,8 +127,9 @@
}
else {
// If not, just look in the window object and any local scope
- // (reading into JS mode internals to get at the local variables)
+ // (reading into JS mode internals to get at the local and global variables)
for (var v = token.state.localVars; v; v = v.next) maybeAdd(v.name);
+ for (var v = token.state.globalVars; v; v = v.next) maybeAdd(v.name);
gatherCompletions(window);
forEach(keywords, maybeAdd);
}
diff --git a/lib/util/multiplex.js b/lib/util/multiplex.js
index 214730839d..e77ff2a9c3 100644
--- a/lib/util/multiplex.js
+++ b/lib/util/multiplex.js
@@ -68,6 +68,24 @@ CodeMirror.multiplexingMode = function(outer /*, others */) {
return mode.indent(state.innerActive ? state.inner : state.outer, textAfter);
},
+ blankLine: function(state) {
+ var mode = state.innerActive ? state.innerActive.mode : outer;
+ if (mode.blankLine) {
+ mode.blankLine(state.innerActive ? state.inner : state.outer);
+ }
+ if (!state.innerActive) {
+ for (var i = 0; i < n_others; ++i) {
+ var other = others[i];
+ if (other.open === "\n") {
+ state.innerActive = other;
+ state.inner = CodeMirror.startState(other.mode, mode.indent ? mode.indent(state.outer, "") : 0);
+ }
+ }
+ } else if (mode.close === "\n") {
+ state.innerActive = state.inner = null;
+ }
+ },
+
electricChars: outer.electricChars,
innerMode: function(state) {
diff --git a/lib/util/searchcursor.js b/lib/util/searchcursor.js
index 1750aadc11..58fed74d00 100644
--- a/lib/util/searchcursor.js
+++ b/lib/util/searchcursor.js
@@ -18,7 +18,7 @@
var line = cm.getLine(pos.line).slice(0, pos.ch), match = query.exec(line), start = 0;
while (match) {
start += match.index + 1;
- line = line.slice(start);
+ line = line.slice(start);
query.lastIndex = 0;
var newmatch = query.exec(line);
if (newmatch) match = newmatch;
diff --git a/lib/util/simple-hint.js b/lib/util/simple-hint.js
index 0ce25f9650..b856a9df05 100644
--- a/lib/util/simple-hint.js
+++ b/lib/util/simple-hint.js
@@ -18,7 +18,7 @@
return;
}
- var result = getHints(editor);
+ var result = getHints(editor, givenOptions);
if (!result || !result.list.length) return;
var completions = result.list;
function insert(str) {
diff --git a/mode/clike/clike.js b/mode/clike/clike.js
index dfe294172f..c2b84c6837 100644
--- a/mode/clike/clike.js
+++ b/mode/clike/clike.js
@@ -1,5 +1,7 @@
CodeMirror.defineMode("clike", function(config, parserConfig) {
var indentUnit = config.indentUnit,
+ statementIndentUnit = parserConfig.statementIndentUnit || indentUnit,
+ dontAlignCalls = parserConfig.dontAlignCalls,
keywords = parserConfig.keywords || {},
builtin = parserConfig.builtin || {},
blockKeywords = parserConfig.blockKeywords || {},
@@ -89,7 +91,10 @@ CodeMirror.defineMode("clike", function(config, parserConfig) {
this.prev = prev;
}
function pushContext(state, col, type) {
- return state.context = new Context(state.indented, col, type, null, state.context);
+ var indent = state.indented;
+ if (state.context && state.context.type == "statement")
+ indent = state.context.indented;
+ return state.context = new Context(indent, col, type, null, state.context);
}
function popContext(state) {
var t = state.context.type;
@@ -123,7 +128,7 @@ CodeMirror.defineMode("clike", function(config, parserConfig) {
if (style == "comment" || style == "meta") return style;
if (ctx.align == null) ctx.align = true;
- if ((curPunc == ";" || curPunc == ":") && ctx.type == "statement") popContext(state);
+ if ((curPunc == ";" || curPunc == ":" || curPunc == ",") && ctx.type == "statement") popContext(state);
else if (curPunc == "{") pushContext(state, stream.column(), "}");
else if (curPunc == "[") pushContext(state, stream.column(), "]");
else if (curPunc == "(") pushContext(state, stream.column(), ")");
@@ -145,7 +150,8 @@ CodeMirror.defineMode("clike", function(config, parserConfig) {
var ctx = state.context, firstChar = textAfter && textAfter.charAt(0);
if (ctx.type == "statement" && firstChar == "}") ctx = ctx.prev;
var closing = firstChar == ctx.type;
- if (ctx.type == "statement") return ctx.indented + (firstChar == "{" ? 0 : indentUnit);
+ if (ctx.type == "statement") return ctx.indented + (firstChar == "{" ? 0 : statementIndentUnit);
+ else if (dontAlignCalls && ctx.type == ")" && !closing) return ctx.indented + statementIndentUnit;
else if (ctx.align) return ctx.column + (closing ? 0 : 1);
else return ctx.indented + (closing ? 0 : indentUnit);
},
@@ -166,7 +172,19 @@ CodeMirror.defineMode("clike", function(config, parserConfig) {
function cppHook(stream, state) {
if (!state.startOfLine) return false;
- stream.skipToEnd();
+ for (;;) {
+ if (stream.skipTo("\\")) {
+ stream.next();
+ if (stream.eol()) {
+ state.tokenize = cppHook;
+ break;
+ }
+ } else {
+ stream.skipToEnd();
+ state.tokenize = null;
+ break;
+ }
+ }
return "meta";
}
diff --git a/mode/css/css.js b/mode/css/css.js
index 87d5d7401e..f4ce9f9d77 100644
--- a/mode/css/css.js
+++ b/mode/css/css.js
@@ -237,6 +237,11 @@ CodeMirror.defineMode("css", function(config) {
else if (/[;{}\[\]\(\)]/.test(ch)) {
return ret(null, ch);
}
+ else if (ch == "u" && stream.match("rl(")) {
+ stream.backUp(1);
+ state.tokenize = tokenParenthesized;
+ return ret("property", "variable");
+ }
else {
stream.eatWhile(/[\w\\\-]/);
return ret("property", "variable");
@@ -267,7 +272,7 @@ CodeMirror.defineMode("css", function(config) {
return ret("comment", "comment");
}
- function tokenString(quote) {
+ function tokenString(quote, nonInclusive) {
return function(stream, state) {
var escaped = false, ch;
while ((ch = stream.next()) != null) {
@@ -275,11 +280,23 @@ CodeMirror.defineMode("css", function(config) {
break;
escaped = !escaped && ch == "\\";
}
- if (!escaped) state.tokenize = tokenBase;
+ if (!escaped) {
+ if (nonInclusive) stream.backUp(1);
+ state.tokenize = tokenBase;
+ }
return ret("string", "string");
};
}
+ function tokenParenthesized(stream, state) {
+ stream.next(); // Must be '('
+ if (!stream.match(/\s*[\"\']/, false))
+ state.tokenize = tokenString(")", true);
+ else
+ state.tokenize = tokenBase;
+ return ret(null, "(");
+ }
+
return {
startState: function(base) {
return {tokenize: tokenBase,
@@ -335,7 +352,7 @@ CodeMirror.defineMode("css", function(config) {
// sequence of selectors:
// One or more of the named type of selector chained with commas.
- if (stream.eatSpace()) return null;
+ if (state.tokenize == tokenBase && stream.eatSpace()) return null;
var style = state.tokenize(stream, state);
// Changing style returned based on context
diff --git a/mode/diff/diff.js b/mode/diff/diff.js
index 3402f3b331..9a0d90ea55 100644
--- a/mode/diff/diff.js
+++ b/mode/diff/diff.js
@@ -1,8 +1,8 @@
CodeMirror.defineMode("diff", function() {
var TOKEN_NAMES = {
- '+': 'tag',
- '-': 'string',
+ '+': 'positive',
+ '-': 'negative',
'@': 'meta'
};
diff --git a/mode/ecl/ecl.js b/mode/ecl/ecl.js
index c0e4479270..a8a2ee5e9a 100644
--- a/mode/ecl/ecl.js
+++ b/mode/ecl/ecl.js
@@ -1,203 +1,203 @@
-CodeMirror.defineMode("ecl", function(config) {
-
- function words(str) {
- var obj = {}, words = str.split(" ");
- for (var i = 0; i < words.length; ++i) obj[words[i]] = true;
- return obj;
- }
-
- function metaHook(stream, state) {
- if (!state.startOfLine) return false;
- stream.skipToEnd();
- return "meta";
- }
-
- function tokenAtString(stream, state) {
- var next;
- while ((next = stream.next()) != null) {
- if (next == '"' && !stream.eat('"')) {
- state.tokenize = null;
- break;
- }
- }
- return "string";
- }
-
- var indentUnit = config.indentUnit;
- var keyword = words("abs acos allnodes ascii asin asstring atan atan2 ave case choose choosen choosesets clustersize combine correlation cos cosh count covariance cron dataset dedup define denormalize distribute distributed distribution ebcdic enth error evaluate event eventextra eventname exists exp failcode failmessage fetch fromunicode getisvalid global graph group hash hash32 hash64 hashcrc hashmd5 having if index intformat isvalid iterate join keyunicode length library limit ln local log loop map matched matchlength matchposition matchtext matchunicode max merge mergejoin min nolocal nonempty normalize parse pipe power preload process project pull random range rank ranked realformat recordof regexfind regexreplace regroup rejected rollup round roundup row rowdiff sample set sin sinh sizeof soapcall sort sorted sqrt stepped stored sum table tan tanh thisnode topn tounicode transfer trim truncate typeof ungroup unicodeorder variance which workunit xmldecode xmlencode xmltext xmlunicode");
- var variable = words("apply assert build buildindex evaluate fail keydiff keypatch loadxml nothor notify output parallel sequential soapcall wait");
- var variable_2 = words("__compressed__ all and any as atmost before beginc++ best between case const counter csv descend encrypt end endc++ endmacro except exclusive expire export extend false few first flat from full function group header heading hole ifblock import in interface joined keep keyed last left limit load local locale lookup macro many maxcount maxlength min skew module named nocase noroot noscan nosort not of only opt or outer overwrite packed partition penalty physicallength pipe quote record relationship repeat return right scan self separator service shared skew skip sql store terminator thor threshold token transform trim true type unicodeorder unsorted validate virtual whole wild within xml xpath");
- var variable_3 = words("ascii big_endian boolean data decimal ebcdic integer pattern qstring real record rule set of string token udecimal unicode unsigned varstring varunicode");
- var builtin = words("checkpoint deprecated failcode failmessage failure global independent onwarning persist priority recovery stored success wait when");
- var blockKeywords = words("catch class do else finally for if switch try while");
- var atoms = words("true false null");
- var hooks = {"#": metaHook};
- var multiLineStrings;
- var isOperatorChar = /[+\-*&%=<>!?|\/]/;
-
- var curPunc;
-
- function tokenBase(stream, state) {
- var ch = stream.next();
- if (hooks[ch]) {
- var result = hooks[ch](stream, state);
- if (result !== false) return result;
- }
- if (ch == '"' || ch == "'") {
- state.tokenize = tokenString(ch);
- return state.tokenize(stream, state);
- }
- if (/[\[\]{}\(\),;\:\.]/.test(ch)) {
- curPunc = ch;
- return null;
- }
- if (/\d/.test(ch)) {
- stream.eatWhile(/[\w\.]/);
- return "number";
- }
- if (ch == "/") {
- if (stream.eat("*")) {
- state.tokenize = tokenComment;
- return tokenComment(stream, state);
- }
- if (stream.eat("/")) {
- stream.skipToEnd();
- return "comment";
- }
- }
- if (isOperatorChar.test(ch)) {
- stream.eatWhile(isOperatorChar);
- return "operator";
- }
- stream.eatWhile(/[\w\$_]/);
- var cur = stream.current().toLowerCase();
- if (keyword.propertyIsEnumerable(cur)) {
- if (blockKeywords.propertyIsEnumerable(cur)) curPunc = "newstatement";
- return "keyword";
- } else if (variable.propertyIsEnumerable(cur)) {
- if (blockKeywords.propertyIsEnumerable(cur)) curPunc = "newstatement";
- return "variable";
- } else if (variable_2.propertyIsEnumerable(cur)) {
- if (blockKeywords.propertyIsEnumerable(cur)) curPunc = "newstatement";
- return "variable-2";
- } else if (variable_3.propertyIsEnumerable(cur)) {
- if (blockKeywords.propertyIsEnumerable(cur)) curPunc = "newstatement";
- return "variable-3";
- } else if (builtin.propertyIsEnumerable(cur)) {
- if (blockKeywords.propertyIsEnumerable(cur)) curPunc = "newstatement";
- return "builtin";
- } else { //Data types are of from KEYWORD##
- var i = cur.length - 1;
- while(i >= 0 && (!isNaN(cur[i]) || cur[i] == '_'))
- --i;
-
- if (i > 0) {
- var cur2 = cur.substr(0, i + 1);
- if (variable_3.propertyIsEnumerable(cur2)) {
- if (blockKeywords.propertyIsEnumerable(cur2)) curPunc = "newstatement";
- return "variable-3";
- }
- }
- }
- if (atoms.propertyIsEnumerable(cur)) return "atom";
- return null;
- }
-
- function tokenString(quote) {
- return function(stream, state) {
- var escaped = false, next, end = false;
- while ((next = stream.next()) != null) {
- if (next == quote && !escaped) {end = true; break;}
- escaped = !escaped && next == "\\";
- }
- if (end || !(escaped || multiLineStrings))
- state.tokenize = tokenBase;
- return "string";
- };
- }
-
- function tokenComment(stream, state) {
- var maybeEnd = false, ch;
- while (ch = stream.next()) {
- if (ch == "/" && maybeEnd) {
- state.tokenize = tokenBase;
- break;
- }
- maybeEnd = (ch == "*");
- }
- return "comment";
- }
-
- function Context(indented, column, type, align, prev) {
- this.indented = indented;
- this.column = column;
- this.type = type;
- this.align = align;
- this.prev = prev;
- }
- function pushContext(state, col, type) {
- return state.context = new Context(state.indented, col, type, null, state.context);
- }
- function popContext(state) {
- var t = state.context.type;
- if (t == ")" || t == "]" || t == "}")
- state.indented = state.context.indented;
- return state.context = state.context.prev;
- }
-
- // Interface
-
- return {
- startState: function(basecolumn) {
- return {
- tokenize: null,
- context: new Context((basecolumn || 0) - indentUnit, 0, "top", false),
- indented: 0,
- startOfLine: true
- };
- },
-
- token: function(stream, state) {
- var ctx = state.context;
- if (stream.sol()) {
- if (ctx.align == null) ctx.align = false;
- state.indented = stream.indentation();
- state.startOfLine = true;
- }
- if (stream.eatSpace()) return null;
- curPunc = null;
- var style = (state.tokenize || tokenBase)(stream, state);
- if (style == "comment" || style == "meta") return style;
- if (ctx.align == null) ctx.align = true;
-
- if ((curPunc == ";" || curPunc == ":") && ctx.type == "statement") popContext(state);
- else if (curPunc == "{") pushContext(state, stream.column(), "}");
- else if (curPunc == "[") pushContext(state, stream.column(), "]");
- else if (curPunc == "(") pushContext(state, stream.column(), ")");
- else if (curPunc == "}") {
- while (ctx.type == "statement") ctx = popContext(state);
- if (ctx.type == "}") ctx = popContext(state);
- while (ctx.type == "statement") ctx = popContext(state);
- }
- else if (curPunc == ctx.type) popContext(state);
- else if (ctx.type == "}" || ctx.type == "top" || (ctx.type == "statement" && curPunc == "newstatement"))
- pushContext(state, stream.column(), "statement");
- state.startOfLine = false;
- return style;
- },
-
- indent: function(state, textAfter) {
- if (state.tokenize != tokenBase && state.tokenize != null) return 0;
- var ctx = state.context, firstChar = textAfter && textAfter.charAt(0);
- if (ctx.type == "statement" && firstChar == "}") ctx = ctx.prev;
- var closing = firstChar == ctx.type;
- if (ctx.type == "statement") return ctx.indented + (firstChar == "{" ? 0 : indentUnit);
- else if (ctx.align) return ctx.column + (closing ? 0 : 1);
- else return ctx.indented + (closing ? 0 : indentUnit);
- },
-
- electricChars: "{}"
- };
-});
-
-CodeMirror.defineMIME("text/x-ecl", "ecl");
+CodeMirror.defineMode("ecl", function(config) {
+
+ function words(str) {
+ var obj = {}, words = str.split(" ");
+ for (var i = 0; i < words.length; ++i) obj[words[i]] = true;
+ return obj;
+ }
+
+ function metaHook(stream, state) {
+ if (!state.startOfLine) return false;
+ stream.skipToEnd();
+ return "meta";
+ }
+
+ function tokenAtString(stream, state) {
+ var next;
+ while ((next = stream.next()) != null) {
+ if (next == '"' && !stream.eat('"')) {
+ state.tokenize = null;
+ break;
+ }
+ }
+ return "string";
+ }
+
+ var indentUnit = config.indentUnit;
+ var keyword = words("abs acos allnodes ascii asin asstring atan atan2 ave case choose choosen choosesets clustersize combine correlation cos cosh count covariance cron dataset dedup define denormalize distribute distributed distribution ebcdic enth error evaluate event eventextra eventname exists exp failcode failmessage fetch fromunicode getisvalid global graph group hash hash32 hash64 hashcrc hashmd5 having if index intformat isvalid iterate join keyunicode length library limit ln local log loop map matched matchlength matchposition matchtext matchunicode max merge mergejoin min nolocal nonempty normalize parse pipe power preload process project pull random range rank ranked realformat recordof regexfind regexreplace regroup rejected rollup round roundup row rowdiff sample set sin sinh sizeof soapcall sort sorted sqrt stepped stored sum table tan tanh thisnode topn tounicode transfer trim truncate typeof ungroup unicodeorder variance which workunit xmldecode xmlencode xmltext xmlunicode");
+ var variable = words("apply assert build buildindex evaluate fail keydiff keypatch loadxml nothor notify output parallel sequential soapcall wait");
+ var variable_2 = words("__compressed__ all and any as atmost before beginc++ best between case const counter csv descend encrypt end endc++ endmacro except exclusive expire export extend false few first flat from full function group header heading hole ifblock import in interface joined keep keyed last left limit load local locale lookup macro many maxcount maxlength min skew module named nocase noroot noscan nosort not of only opt or outer overwrite packed partition penalty physicallength pipe quote record relationship repeat return right scan self separator service shared skew skip sql store terminator thor threshold token transform trim true type unicodeorder unsorted validate virtual whole wild within xml xpath");
+ var variable_3 = words("ascii big_endian boolean data decimal ebcdic integer pattern qstring real record rule set of string token udecimal unicode unsigned varstring varunicode");
+ var builtin = words("checkpoint deprecated failcode failmessage failure global independent onwarning persist priority recovery stored success wait when");
+ var blockKeywords = words("catch class do else finally for if switch try while");
+ var atoms = words("true false null");
+ var hooks = {"#": metaHook};
+ var multiLineStrings;
+ var isOperatorChar = /[+\-*&%=<>!?|\/]/;
+
+ var curPunc;
+
+ function tokenBase(stream, state) {
+ var ch = stream.next();
+ if (hooks[ch]) {
+ var result = hooks[ch](stream, state);
+ if (result !== false) return result;
+ }
+ if (ch == '"' || ch == "'") {
+ state.tokenize = tokenString(ch);
+ return state.tokenize(stream, state);
+ }
+ if (/[\[\]{}\(\),;\:\.]/.test(ch)) {
+ curPunc = ch;
+ return null;
+ }
+ if (/\d/.test(ch)) {
+ stream.eatWhile(/[\w\.]/);
+ return "number";
+ }
+ if (ch == "/") {
+ if (stream.eat("*")) {
+ state.tokenize = tokenComment;
+ return tokenComment(stream, state);
+ }
+ if (stream.eat("/")) {
+ stream.skipToEnd();
+ return "comment";
+ }
+ }
+ if (isOperatorChar.test(ch)) {
+ stream.eatWhile(isOperatorChar);
+ return "operator";
+ }
+ stream.eatWhile(/[\w\$_]/);
+ var cur = stream.current().toLowerCase();
+ if (keyword.propertyIsEnumerable(cur)) {
+ if (blockKeywords.propertyIsEnumerable(cur)) curPunc = "newstatement";
+ return "keyword";
+ } else if (variable.propertyIsEnumerable(cur)) {
+ if (blockKeywords.propertyIsEnumerable(cur)) curPunc = "newstatement";
+ return "variable";
+ } else if (variable_2.propertyIsEnumerable(cur)) {
+ if (blockKeywords.propertyIsEnumerable(cur)) curPunc = "newstatement";
+ return "variable-2";
+ } else if (variable_3.propertyIsEnumerable(cur)) {
+ if (blockKeywords.propertyIsEnumerable(cur)) curPunc = "newstatement";
+ return "variable-3";
+ } else if (builtin.propertyIsEnumerable(cur)) {
+ if (blockKeywords.propertyIsEnumerable(cur)) curPunc = "newstatement";
+ return "builtin";
+ } else { //Data types are of from KEYWORD##
+ var i = cur.length - 1;
+ while(i >= 0 && (!isNaN(cur[i]) || cur[i] == '_'))
+ --i;
+
+ if (i > 0) {
+ var cur2 = cur.substr(0, i + 1);
+ if (variable_3.propertyIsEnumerable(cur2)) {
+ if (blockKeywords.propertyIsEnumerable(cur2)) curPunc = "newstatement";
+ return "variable-3";
+ }
+ }
+ }
+ if (atoms.propertyIsEnumerable(cur)) return "atom";
+ return null;
+ }
+
+ function tokenString(quote) {
+ return function(stream, state) {
+ var escaped = false, next, end = false;
+ while ((next = stream.next()) != null) {
+ if (next == quote && !escaped) {end = true; break;}
+ escaped = !escaped && next == "\\";
+ }
+ if (end || !(escaped || multiLineStrings))
+ state.tokenize = tokenBase;
+ return "string";
+ };
+ }
+
+ function tokenComment(stream, state) {
+ var maybeEnd = false, ch;
+ while (ch = stream.next()) {
+ if (ch == "/" && maybeEnd) {
+ state.tokenize = tokenBase;
+ break;
+ }
+ maybeEnd = (ch == "*");
+ }
+ return "comment";
+ }
+
+ function Context(indented, column, type, align, prev) {
+ this.indented = indented;
+ this.column = column;
+ this.type = type;
+ this.align = align;
+ this.prev = prev;
+ }
+ function pushContext(state, col, type) {
+ return state.context = new Context(state.indented, col, type, null, state.context);
+ }
+ function popContext(state) {
+ var t = state.context.type;
+ if (t == ")" || t == "]" || t == "}")
+ state.indented = state.context.indented;
+ return state.context = state.context.prev;
+ }
+
+ // Interface
+
+ return {
+ startState: function(basecolumn) {
+ return {
+ tokenize: null,
+ context: new Context((basecolumn || 0) - indentUnit, 0, "top", false),
+ indented: 0,
+ startOfLine: true
+ };
+ },
+
+ token: function(stream, state) {
+ var ctx = state.context;
+ if (stream.sol()) {
+ if (ctx.align == null) ctx.align = false;
+ state.indented = stream.indentation();
+ state.startOfLine = true;
+ }
+ if (stream.eatSpace()) return null;
+ curPunc = null;
+ var style = (state.tokenize || tokenBase)(stream, state);
+ if (style == "comment" || style == "meta") return style;
+ if (ctx.align == null) ctx.align = true;
+
+ if ((curPunc == ";" || curPunc == ":") && ctx.type == "statement") popContext(state);
+ else if (curPunc == "{") pushContext(state, stream.column(), "}");
+ else if (curPunc == "[") pushContext(state, stream.column(), "]");
+ else if (curPunc == "(") pushContext(state, stream.column(), ")");
+ else if (curPunc == "}") {
+ while (ctx.type == "statement") ctx = popContext(state);
+ if (ctx.type == "}") ctx = popContext(state);
+ while (ctx.type == "statement") ctx = popContext(state);
+ }
+ else if (curPunc == ctx.type) popContext(state);
+ else if (ctx.type == "}" || ctx.type == "top" || (ctx.type == "statement" && curPunc == "newstatement"))
+ pushContext(state, stream.column(), "statement");
+ state.startOfLine = false;
+ return style;
+ },
+
+ indent: function(state, textAfter) {
+ if (state.tokenize != tokenBase && state.tokenize != null) return 0;
+ var ctx = state.context, firstChar = textAfter && textAfter.charAt(0);
+ if (ctx.type == "statement" && firstChar == "}") ctx = ctx.prev;
+ var closing = firstChar == ctx.type;
+ if (ctx.type == "statement") return ctx.indented + (firstChar == "{" ? 0 : indentUnit);
+ else if (ctx.align) return ctx.column + (closing ? 0 : 1);
+ else return ctx.indented + (closing ? 0 : indentUnit);
+ },
+
+ electricChars: "{}"
+ };
+});
+
+CodeMirror.defineMIME("text/x-ecl", "ecl");
diff --git a/mode/ecl/index.html b/mode/ecl/index.html
index d6b41f4e5f..a3542edad6 100644
--- a/mode/ecl/index.html
+++ b/mode/ecl/index.html
@@ -1,42 +1,42 @@
-
-
-
- CodeMirror: ECL mode
-
-
-
-
-
-
-
- CodeMirror: ECL mode
-
-
-
- Based on CodeMirror's clike mode. For more information see HPCC Systems web site.
- MIME types defined: text/x-ecl.
-
-
-
+
+
+
+ CodeMirror: ECL mode
+
+
+
+
+
+
+
+ CodeMirror: ECL mode
+
+/*
+sample useless code to demonstrate ecl syntax highlighting
+this is a multiline comment!
+*/
+
+// this is a singleline comment!
+
+import ut;
+r :=
+ record
+ string22 s1 := '123';
+ integer4 i1 := 123;
+ end;
+#option('tmp', true);
+d := dataset('tmp::qb', r, thor);
+output(d);
+
+
+
+ Based on CodeMirror's clike mode. For more information see HPCC Systems web site.
+ MIME types defined: text/x-ecl.
+
+
+
diff --git a/mode/htmlembedded/htmlembedded.js b/mode/htmlembedded/htmlembedded.js
index b7888689f1..e183d67467 100644
--- a/mode/htmlembedded/htmlembedded.js
+++ b/mode/htmlembedded/htmlembedded.js
@@ -34,8 +34,8 @@ CodeMirror.defineMode("htmlembedded", function(config, parserConfig) {
htmlMixedMode = htmlMixedMode || CodeMirror.getMode(config, "htmlmixed");
return {
token : parserConfig.startOpen ? scriptingDispatch : htmlDispatch,
- htmlState : htmlMixedMode.startState(),
- scriptState : scriptingMode.startState()
+ htmlState : CodeMirror.startState(htmlMixedMode),
+ scriptState : CodeMirror.startState(scriptingMode)
};
},
@@ -46,7 +46,7 @@ CodeMirror.defineMode("htmlembedded", function(config, parserConfig) {
indent: function(state, textAfter) {
if (state.token == htmlDispatch)
return htmlMixedMode.indent(state.htmlState, textAfter);
- else
+ else if (scriptingMode.indent)
return scriptingMode.indent(state.scriptState, textAfter);
},
diff --git a/mode/htmlmixed/htmlmixed.js b/mode/htmlmixed/htmlmixed.js
index 9a15360dbd..f2dd01dd91 100644
--- a/mode/htmlmixed/htmlmixed.js
+++ b/mode/htmlmixed/htmlmixed.js
@@ -5,7 +5,7 @@ CodeMirror.defineMode("htmlmixed", function(config) {
function html(stream, state) {
var style = htmlMode.token(stream, state.htmlState);
- if (style == "tag" && stream.current() == ">" && state.htmlState.context) {
+ if (/(?:^|\s)tag(?:\s|$)/.test(style) && stream.current() == ">" && state.htmlState.context) {
if (/^script$/i.test(state.htmlState.context.tagName)) {
state.token = javascript;
state.localState = jsMode.startState(htmlMode.indent(state.htmlState, ""));
diff --git a/mode/http/http.js b/mode/http/http.js
new file mode 100644
index 0000000000..f861fbb609
--- /dev/null
+++ b/mode/http/http.js
@@ -0,0 +1,98 @@
+CodeMirror.defineMode("http", function() {
+ function failFirstLine(stream, state) {
+ stream.skipToEnd();
+ state.cur = header;
+ return "error";
+ }
+
+ function start(stream, state) {
+ if (stream.match(/^HTTP\/\d\.\d/)) {
+ state.cur = responseStatusCode;
+ return "keyword";
+ } else if (stream.match(/^[A-Z]+/) && /[ \t]/.test(stream.peek())) {
+ state.cur = requestPath;
+ return "keyword";
+ } else {
+ return failFirstLine(stream, state);
+ }
+ }
+
+ function responseStatusCode(stream, state) {
+ var code = stream.match(/^\d+/);
+ if (!code) return failFirstLine(stream, state);
+
+ state.cur = responseStatusText;
+ var status = Number(code[0]);
+ if (status >= 100 && status < 200) {
+ return "positive informational";
+ } else if (status >= 200 && status < 300) {
+ return "positive success";
+ } else if (status >= 300 && status < 400) {
+ return "positive redirect";
+ } else if (status >= 400 && status < 500) {
+ return "negative client-error";
+ } else if (status >= 500 && status < 600) {
+ return "negative server-error";
+ } else {
+ return "error";
+ }
+ }
+
+ function responseStatusText(stream, state) {
+ stream.skipToEnd();
+ state.cur = header;
+ return null;
+ }
+
+ function requestPath(stream, state) {
+ stream.eatWhile(/\S/);
+ state.cur = requestProtocol;
+ return "string-2";
+ }
+
+ function requestProtocol(stream, state) {
+ if (stream.match(/^HTTP\/\d\.\d$/)) {
+ state.cur = header;
+ return "keyword";
+ } else {
+ return failFirstLine(stream, state);
+ }
+ }
+
+ function header(stream, state) {
+ if (stream.sol() && !stream.eat(/[ \t]/)) {
+ if (stream.match(/^.*?:/)) {
+ return "atom";
+ } else {
+ stream.skipToEnd();
+ return "error";
+ }
+ } else {
+ stream.skipToEnd();
+ return "string";
+ }
+ }
+
+ function body(stream, state) {
+ stream.skipToEnd();
+ return null;
+ }
+
+ return {
+ token: function(stream, state) {
+ var cur = state.cur;
+ if (cur != header && cur != body && stream.eatSpace()) return null;
+ return cur(stream, state);
+ },
+
+ blankLine: function(state) {
+ state.cur = body;
+ },
+
+ startState: function() {
+ return {cur: start};
+ }
+ };
+});
+
+CodeMirror.defineMIME("message/http", "http");
diff --git a/mode/http/index.html b/mode/http/index.html
new file mode 100644
index 0000000000..124eb84f95
--- /dev/null
+++ b/mode/http/index.html
@@ -0,0 +1,32 @@
+
+
+
+
+ CodeMirror: HTTP mode
+
+
+
+
+
+
+
+ CodeMirror: HTTP mode
+
+
+POST /somewhere HTTP/1.1
+Host: example.com
+If-Modified-Since: Sat, 29 Oct 1994 19:43:31 GMT
+Content-Type: application/x-www-form-urlencoded;
+ charset=utf-8
+User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.11 (KHTML, like Gecko) Ubuntu/12.04 Chromium/20.0.1132.47 Chrome/20.0.1132.47 Safari/536.11
+
+This is the request body!
+
+
+
+
+ MIME types defined: message/http.
+
+
diff --git a/mode/javascript/javascript.js b/mode/javascript/javascript.js
index e5c150420e..f1e2001b15 100644
--- a/mode/javascript/javascript.js
+++ b/mode/javascript/javascript.js
@@ -196,12 +196,19 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
return true;
}
function register(varname) {
+ function inList(list) {
+ for (var v = list; v; v = v.next)
+ if (v.name == varname) return true;
+ return false;
+ }
var state = cx.state;
if (state.context) {
cx.marked = "def";
- for (var v = state.localVars; v; v = v.next)
- if (v.name == varname) return;
+ if (inList(state.localVars)) return;
state.localVars = {name: varname, next: state.localVars};
+ } else {
+ if (inList(state.globalVars)) return;
+ state.globalVars = {name: varname, next: state.globalVars};
}
}
@@ -364,6 +371,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
cc: [],
lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false),
localVars: parserConfig.localVars,
+ globalVars: parserConfig.globalVars,
context: parserConfig.localVars && {vars: parserConfig.localVars},
indented: 0
};
diff --git a/mode/markdown/index.html b/mode/markdown/index.html
index 92d5f1fb08..5d7452f1e8 100644
--- a/mode/markdown/index.html
+++ b/mode/markdown/index.html
@@ -5,6 +5,7 @@
CodeMirror: Markdown mode
+
@@ -328,8 +329,8 @@ CodeMirror: Markdown mode
var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
mode: 'markdown',
lineNumbers: true,
- matchBrackets: true,
- theme: "default"
+ theme: "default",
+ extraKeys: {"Enter": "newlineAndIndentContinueMarkdownList"}
});
diff --git a/mode/markdown/markdown.js b/mode/markdown/markdown.js
index d227fc9b91..1fb392032c 100644
--- a/mode/markdown/markdown.js
+++ b/mode/markdown/markdown.js
@@ -252,8 +252,8 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
return getType(state);
}
- if (ch === '!' && stream.match(/\[.*\] ?(?:\(|\[)/, false)) {
- stream.match(/\[.*\]/);
+ if (ch === '!' && stream.match(/\[[^\]]*\] ?(?:\(|\[)/, false)) {
+ stream.match(/\[[^\]]*\]/);
state.inline = state.f = linkHref;
return image;
}
diff --git a/mode/markdown/test.js b/mode/markdown/test.js
index 7572db510b..90c77e6683 100644
--- a/mode/markdown/test.js
+++ b/mode/markdown/test.js
@@ -719,6 +719,20 @@ MT.testMode(
]
);
+// Inline link with image
+MT.testMode(
+ 'linkImage',
+ '[](http://example.com/) bar',
+ [
+ 'link', '[',
+ 'tag', '![foo]',
+ 'string', '(http://example.com/)',
+ 'link', ']',
+ 'string', '(http://example.com/)',
+ null, ' bar'
+ ]
+);
+
// Inline link with Em
MT.testMode(
'linkEm',
diff --git a/mode/php/index.html b/mode/php/index.html
index 874b67562d..d771e28e6f 100644
--- a/mode/php/index.html
+++ b/mode/php/index.html
@@ -43,7 +43,7 @@ CodeMirror: PHP mode
Simple HTML/PHP mode based on
the C-like mode. Depends on XML,
- JavaScript, CSS, and C-like modes.
+ JavaScript, CSS, HTMLMixed, and C-like modes.
MIME types defined: application/x-httpd-php (HTML with PHP code), text/x-php (plain, non-wrapped PHP code).