diff --git a/plugins/tiddlywiki/codemirror/codemirroreditor.js b/plugins/tiddlywiki/codemirror/codemirroreditor.js index fa3ff205a..f6e288bdb 100644 --- a/plugins/tiddlywiki/codemirror/codemirroreditor.js +++ b/plugins/tiddlywiki/codemirror/codemirroreditor.js @@ -12,23 +12,59 @@ Extend the edit-text widget to use CodeMirror /*global $tw: false */ "use strict"; -if($tw.browser) { - require("$:/plugins/tiddlywiki/codemirror/codemirror.js"); -} +var CODEMIRROR_OPTIONS = "$:/config/CodeMirror", configOptions; +/* e.g. to allow vim key bindings + * { + * "require": [ + * "$:/plugins/tiddlywiki/codemirror/addon/dialog.js", + * "$:/plugins/tiddlywiki/codemirror/addon/searchcursor.js", + * "$:/plugins/tiddlywiki/codemirror/keymap/vim.js" + * ], + * "configuration": { + * "keyMap": "vim", + * "showCursorWhenSelecting": true + * } + *} + */ var EditTextWidget = require("$:/core/modules/widgets/edit-text.js")["edit-text"]; +if($tw.browser) { + require("$:/plugins/tiddlywiki/codemirror/codemirror.js"); + + configOptions = $tw.wiki.getTiddlerData(CODEMIRROR_OPTIONS,{}); + + if(configOptions) { + if(configOptions["require"]) { + if(typeof configOptions["require"] === 'object' && + Object.prototype.toString.call(configOptions["require"]) == '[object Array]') { + for (var idx=0; idx < configOptions["require"].length; idx++) { + require(configOptions["require"][idx]); + } + } + else { + require(configOptions["require"]); + } + } + EditTextWidget._configuration = configOptions["configuration"]; + } +} + /* The edit-text widget calls this method just after inserting its dom nodes */ EditTextWidget.prototype.postRender = function() { var self = this, - cm; - if($tw.browser && window.CodeMirror && this.editTag === "textarea") { - cm = CodeMirror.fromTextArea(this.domNodes[0],{ + cm, config, cv, cm_opts = { lineWrapping: true, lineNumbers: true - }); + }; + + if($tw.browser && window.CodeMirror && this.editTag === "textarea") { + if(EditTextWidget._configuration) { + for (cv in EditTextWidget._configuration) { cm_opts[cv] = EditTextWidget._configuration[cv]; } + } + cm = CodeMirror.fromTextArea(this.domNodes[0], cm_opts); cm.on("change",function() { self.saveChanges(cm.getValue()); }); diff --git a/plugins/tiddlywiki/codemirror/files/addon/dialog.css b/plugins/tiddlywiki/codemirror/files/addon/dialog.css new file mode 100644 index 000000000..2e7c0fc9b --- /dev/null +++ b/plugins/tiddlywiki/codemirror/files/addon/dialog.css @@ -0,0 +1,32 @@ +.CodeMirror-dialog { + position: absolute; + left: 0; right: 0; + background: white; + 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; + background: transparent; + width: 20em; + color: inherit; + font-family: monospace; +} + +.CodeMirror-dialog button { + font-size: 70%; +} diff --git a/plugins/tiddlywiki/codemirror/files/addon/dialog.js b/plugins/tiddlywiki/codemirror/files/addon/dialog.js new file mode 100644 index 000000000..499964fdb --- /dev/null +++ b/plugins/tiddlywiki/codemirror/files/addon/dialog.js @@ -0,0 +1,121 @@ +// Open simple dialogs on top of an editor. Relies on dialog.css. + +(function() { + function dialogDiv(cm, template, bottom) { + var wrap = cm.getWrapperElement(); + 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"; + } + if (typeof template == "string") { + dialog.innerHTML = template; + } else { // Assuming it's a detached DOM element. + dialog.appendChild(template); + } + return dialog; + } + + function closeNotification(cm, newVal) { + if (cm.state.currentNotificationClose) + cm.state.currentNotificationClose(); + cm.state.currentNotificationClose = newVal; + } + + CodeMirror.defineExtension("openDialog", function(template, callback, options) { + closeNotification(this, null); + var dialog = dialogDiv(this, template, options && options.bottom); + var closed = false, me = this; + function close() { + if (closed) return; + closed = true; + dialog.parentNode.removeChild(dialog); + } + var inp = dialog.getElementsByTagName("input")[0], button; + if (inp) { + CodeMirror.on(inp, "keydown", function(e) { + if (options && options.onKeyDown && options.onKeyDown(e, inp.value, close)) { return; } + if (e.keyCode == 13 || e.keyCode == 27) { + CodeMirror.e_stop(e); + close(); + me.focus(); + if (e.keyCode == 13) callback(inp.value); + } + }); + if (options && options.onKeyUp) { + CodeMirror.on(inp, "keyup", function(e) {options.onKeyUp(e, inp.value, close);}); + } + if (options && options.value) inp.value = options.value; + inp.focus(); + CodeMirror.on(inp, "blur", close); + } else if (button = dialog.getElementsByTagName("button")[0]) { + CodeMirror.on(button, "click", function() { + close(); + me.focus(); + }); + button.focus(); + CodeMirror.on(button, "blur", close); + } + return close; + }); + + CodeMirror.defineExtension("openConfirm", function(template, callbacks, options) { + closeNotification(this, null); + var dialog = dialogDiv(this, template, options && options.bottom); + var buttons = dialog.getElementsByTagName("button"); + var closed = false, me = this, blurring = 1; + function close() { + if (closed) return; + closed = true; + dialog.parentNode.removeChild(dialog); + me.focus(); + } + buttons[0].focus(); + for (var i = 0; i < buttons.length; ++i) { + var b = buttons[i]; + (function(callback) { + CodeMirror.on(b, "click", function(e) { + CodeMirror.e_preventDefault(e); + close(); + if (callback) callback(me); + }); + })(callbacks[i]); + CodeMirror.on(b, "blur", function() { + --blurring; + setTimeout(function() { if (blurring <= 0) close(); }, 200); + }); + CodeMirror.on(b, "focus", function() { ++blurring; }); + } + }); + + /* + * openNotification + * Opens a notification, that can be closed with an optional timer + * (default 5000ms timer) and always closes on click. + * + * If a notification is opened while another is opened, it will close the + * currently opened one and open the new one immediately. + */ + CodeMirror.defineExtension("openNotification", function(template, options) { + closeNotification(this, close); + var dialog = dialogDiv(this, template, options && options.bottom); + var duration = options && (options.duration === undefined ? 5000 : options.duration); + var closed = false, doneTimer; + + function close() { + if (closed) return; + closed = true; + clearTimeout(doneTimer); + dialog.parentNode.removeChild(dialog); + } + + CodeMirror.on(dialog, 'click', function(e) { + CodeMirror.e_preventDefault(e); + close(); + }); + if (duration) + doneTimer = setTimeout(close, options.duration); + }); +})(); diff --git a/plugins/tiddlywiki/codemirror/files/addon/searchcursor.js b/plugins/tiddlywiki/codemirror/files/addon/searchcursor.js new file mode 100644 index 000000000..c034d5865 --- /dev/null +++ b/plugins/tiddlywiki/codemirror/files/addon/searchcursor.js @@ -0,0 +1,143 @@ +(function(){ + var Pos = CodeMirror.Pos; + + function SearchCursor(doc, query, pos, caseFold) { + this.atOccurrence = false; this.doc = doc; + if (caseFold == null && typeof query == "string") caseFold = false; + + pos = pos ? doc.clipPos(pos) : Pos(0, 0); + this.pos = {from: pos, to: pos}; + + // The matches method is filled in based on the type of query. + // It takes a position and a direction, and returns an object + // describing the next occurrence of the query, or null if no + // more matches were found. + if (typeof query != "string") { // Regexp match + if (!query.global) query = new RegExp(query.source, query.ignoreCase ? "ig" : "g"); + this.matches = function(reverse, pos) { + if (reverse) { + query.lastIndex = 0; + var line = doc.getLine(pos.line).slice(0, pos.ch), cutOff = 0, match, start; + for (;;) { + query.lastIndex = cutOff; + var newMatch = query.exec(line); + if (!newMatch) break; + match = newMatch; + start = match.index; + cutOff = match.index + (match[0].length || 1); + if (cutOff == line.length) break; + } + var matchLen = (match && match[0].length) || 0; + if (!matchLen) { + if (start == 0 && line.length == 0) {match = undefined;} + else if (start != doc.getLine(pos.line).length) { + matchLen++; + } + } + } else { + query.lastIndex = pos.ch; + var line = doc.getLine(pos.line), match = query.exec(line); + var matchLen = (match && match[0].length) || 0; + var start = match && match.index; + if (start + matchLen != line.length && !matchLen) matchLen = 1; + } + if (match && matchLen) + return {from: Pos(pos.line, start), + to: Pos(pos.line, start + matchLen), + match: match}; + }; + } else { // String query + if (caseFold) query = query.toLowerCase(); + var fold = caseFold ? function(str){return str.toLowerCase();} : function(str){return str;}; + var target = query.split("\n"); + // Different methods for single-line and multi-line queries + if (target.length == 1) { + if (!query.length) { + // Empty string would match anything and never progress, so + // we define it to match nothing instead. + this.matches = function() {}; + } else { + this.matches = function(reverse, pos) { + var line = fold(doc.getLine(pos.line)), len = query.length, match; + if (reverse ? (pos.ch >= len && (match = line.lastIndexOf(query, pos.ch - len)) != -1) + : (match = line.indexOf(query, pos.ch)) != -1) + return {from: Pos(pos.line, match), + to: Pos(pos.line, match + len)}; + }; + } + } else { + this.matches = function(reverse, pos) { + var ln = pos.line, idx = (reverse ? target.length - 1 : 0), match = target[idx], line = fold(doc.getLine(ln)); + var offsetA = (reverse ? line.indexOf(match) + match.length : line.lastIndexOf(match)); + if (reverse ? offsetA > pos.ch || offsetA != match.length + : offsetA < pos.ch || offsetA != line.length - match.length) + return; + for (;;) { + if (reverse ? !ln : ln == doc.lineCount() - 1) return; + line = fold(doc.getLine(ln += reverse ? -1 : 1)); + match = target[reverse ? --idx : ++idx]; + if (idx > 0 && idx < target.length - 1) { + if (line != match) return; + else continue; + } + var offsetB = (reverse ? line.lastIndexOf(match) : line.indexOf(match) + match.length); + if (reverse ? offsetB != line.length - match.length : offsetB != match.length) + return; + var start = Pos(pos.line, offsetA), end = Pos(ln, offsetB); + return {from: reverse ? end : start, to: reverse ? start : end}; + } + }; + } + } + } + + SearchCursor.prototype = { + findNext: function() {return this.find(false);}, + findPrevious: function() {return this.find(true);}, + + find: function(reverse) { + var self = this, pos = this.doc.clipPos(reverse ? this.pos.from : this.pos.to); + function savePosAndFail(line) { + var pos = Pos(line, 0); + self.pos = {from: pos, to: pos}; + self.atOccurrence = false; + return false; + } + + for (;;) { + if (this.pos = this.matches(reverse, pos)) { + if (!this.pos.from || !this.pos.to) { console.log(this.matches, this.pos); } + this.atOccurrence = true; + return this.pos.match || true; + } + if (reverse) { + if (!pos.line) return savePosAndFail(0); + pos = Pos(pos.line-1, this.doc.getLine(pos.line-1).length); + } + else { + var maxLine = this.doc.lineCount(); + if (pos.line == maxLine - 1) return savePosAndFail(maxLine); + pos = Pos(pos.line + 1, 0); + } + } + }, + + from: function() {if (this.atOccurrence) return this.pos.from;}, + to: function() {if (this.atOccurrence) return this.pos.to;}, + + replace: function(newText) { + if (!this.atOccurrence) return; + var lines = CodeMirror.splitLines(newText); + this.doc.replaceRange(lines, this.pos.from, this.pos.to); + this.pos.to = Pos(this.pos.from.line + lines.length - 1, + lines[lines.length - 1].length + (lines.length == 1 ? this.pos.from.ch : 0)); + } + }; + + CodeMirror.defineExtension("getSearchCursor", function(query, pos, caseFold) { + return new SearchCursor(this.doc, query, pos, caseFold); + }); + CodeMirror.defineDocExtension("getSearchCursor", function(query, pos, caseFold) { + return new SearchCursor(this, query, pos, caseFold); + }); +})(); diff --git a/plugins/tiddlywiki/codemirror/files/codemirror.js b/plugins/tiddlywiki/codemirror/files/codemirror.js index 9f35191bf..f8b2af58a 100644 --- a/plugins/tiddlywiki/codemirror/files/codemirror.js +++ b/plugins/tiddlywiki/codemirror/files/codemirror.js @@ -1,4 +1,4 @@ -// CodeMirror version 3.19.1 +// CodeMirror version 3.20 // // CodeMirror is the only global var we claim window.CodeMirror = (function() { @@ -9,9 +9,13 @@ window.CodeMirror = (function() { // Crude, but necessary to handle a number of hard-to-feature-detect // bugs and behavior differences. var gecko = /gecko\/\d/i.test(navigator.userAgent); + // IE11 currently doesn't count as 'ie', since it has almost none of + // the same bugs as earlier versions. Use ie_gt10 to handle + // incompatibilities in that version. var ie = /MSIE \d/.test(navigator.userAgent); var ie_lt8 = ie && (document.documentMode == null || document.documentMode < 8); var ie_lt9 = ie && (document.documentMode == null || document.documentMode < 9); + var ie_gt10 = /Trident\/([7-9]|\d{2,})\./.test(navigator.userAgent); var webkit = /WebKit\//.test(navigator.userAgent); var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(navigator.userAgent); var chrome = /Chrome\//.test(navigator.userAgent); @@ -908,7 +912,7 @@ window.CodeMirror = (function() { doc.iter(doc.frontier, Math.min(doc.first + doc.size, cm.display.showingTo + 500), function(line) { if (doc.frontier >= cm.display.showingFrom) { // Visible var oldStyles = line.styles; - line.styles = highlightLine(cm, line, state); + line.styles = highlightLine(cm, line, state, true); var ischange = !oldStyles || oldStyles.length != line.styles.length; for (var i = 0; !ischange && i < oldStyles.length; ++i) ischange = oldStyles[i] != line.styles[i]; if (ischange) { @@ -917,7 +921,7 @@ window.CodeMirror = (function() { } line.stateAfter = copyState(doc.mode, state); } else { - processLine(cm, line, state); + processLine(cm, line.text, state); line.stateAfter = doc.frontier % 5 == 0 ? copyState(doc.mode, state) : null; } ++doc.frontier; @@ -961,7 +965,7 @@ window.CodeMirror = (function() { if (!state) state = startState(doc.mode); else state = copyState(doc.mode, state); doc.iter(pos, n, function(line) { - processLine(cm, line, state); + processLine(cm, line.text, state); var save = pos == n - 1 || pos % 5 == 0 || pos >= display.showingFrom && pos < display.showingTo; line.stateAfter = save ? copyState(doc.mode, state) : null; ++pos; @@ -1385,8 +1389,10 @@ window.CodeMirror = (function() { } if (!updated && op.selectionChanged) updateSelection(cm); if (op.updateScrollPos) { - display.scroller.scrollTop = display.scrollbarV.scrollTop = doc.scrollTop = newScrollPos.scrollTop; - display.scroller.scrollLeft = display.scrollbarH.scrollLeft = doc.scrollLeft = newScrollPos.scrollLeft; + var top = Math.max(0, Math.min(display.scroller.scrollHeight - display.scroller.clientHeight, newScrollPos.scrollTop)); + var left = Math.max(0, Math.min(display.scroller.scrollWidth - display.scroller.clientWidth, newScrollPos.scrollLeft)); + display.scroller.scrollTop = display.scrollbarV.scrollTop = doc.scrollTop = top; + display.scroller.scrollLeft = display.scrollbarH.scrollLeft = doc.scrollLeft = left; alignHorizontally(cm); if (op.scrollToPos) scrollPosIntoView(cm, clipPos(cm.doc, op.scrollToPos.from), @@ -1905,7 +1911,6 @@ window.CodeMirror = (function() { if (cm.state.draggingText) replaceRange(cm.doc, "", curFrom, curTo, "paste"); cm.replaceSelection(text, null, "paste"); focusInput(cm); - onFocus(cm); } } catch(e){} @@ -2761,10 +2766,10 @@ window.CodeMirror = (function() { for (var i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += "\t";} if (pos < indentation) indentString += spaceStr(indentation - pos); - if (indentString == curSpaceString) - setSelection(cm.doc, Pos(n, curSpaceString.length), Pos(n, curSpaceString.length)); - else + if (indentString != curSpaceString) replaceRange(cm.doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input"); + else if (doc.sel.head.line == n && doc.sel.head.ch < curSpaceString.length) + setSelection(doc, Pos(n, curSpaceString.length), Pos(n, curSpaceString.length), 1); line.stateAfter = null; } @@ -2865,7 +2870,7 @@ window.CodeMirror = (function() { CodeMirror.prototype = { constructor: CodeMirror, - focus: function(){window.focus(); focusInput(this); onFocus(this); fastPoll(this);}, + focus: function(){window.focus(); focusInput(this); fastPoll(this);}, setOption: function(option, value) { var options = this.options, old = options[option]; @@ -3278,8 +3283,14 @@ window.CodeMirror = (function() { clearCaches(cm); regChange(cm); }, true); + option("specialChars", /[\t\u0000-\u0019\u00ad\u200b\u2028\u2029\ufeff]/g, function(cm, val) { + cm.options.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g"); + cm.refresh(); + }, true); + option("specialCharPlaceholder", defaultSpecialCharPlaceholder, function(cm) {cm.refresh();}, true); option("electricChars", true); option("rtlMoveVisually", !windows); + option("wholeLineUpdateBefore", true); option("theme", "default", function(cm) { themeChanged(cm); @@ -3312,8 +3323,14 @@ window.CodeMirror = (function() { option("resetSelectionOnContextMenu", true); option("readOnly", false, function(cm, val) { - if (val == "nocursor") {onBlur(cm); cm.display.input.blur();} - else if (!val) resetInput(cm, true); + if (val == "nocursor") { + onBlur(cm); + cm.display.input.blur(); + cm.display.disabled = true; + } else { + cm.display.disabled = false; + if (!val) resetInput(cm, true); + } }); option("dragDrop", true); @@ -4247,6 +4264,7 @@ window.CodeMirror = (function() { this.height = estimateHeight ? estimateHeight(this) : 1; }; eventMixin(Line); + Line.prototype.lineNo = function() { return lineNo(this); }; function updateLine(line, text, markedSpans, estimateHeight) { line.text = text; @@ -4267,7 +4285,7 @@ window.CodeMirror = (function() { // Run the given mode's parser over a line, update the styles // array, which contains alternating fragments of text and CSS // classes. - function runMode(cm, text, mode, state, f) { + function runMode(cm, text, mode, state, f, forceToEnd) { var flattenSpans = mode.flattenSpans; if (flattenSpans == null) flattenSpans = cm.options.flattenSpans; var curStart = 0, curStyle = null; @@ -4276,6 +4294,7 @@ window.CodeMirror = (function() { while (!stream.eol()) { if (stream.pos > cm.options.maxHighlightLength) { flattenSpans = false; + if (forceToEnd) processLine(cm, text, state, stream.pos); stream.pos = text.length; style = null; } else { @@ -4295,12 +4314,14 @@ window.CodeMirror = (function() { } } - function highlightLine(cm, line, state) { + function highlightLine(cm, line, state, forceToEnd) { // A styles array always starts with a number identifying the // mode/overlays that it is based on (for easy invalidation). var st = [cm.state.modeGen]; // Compute the base array of styles - runMode(cm, line.text, cm.doc.mode, state, function(end, style) {st.push(end, style);}); + runMode(cm, line.text, cm.doc.mode, state, function(end, style) { + st.push(end, style); + }, forceToEnd); // Run overlays, adjust style array. for (var o = 0; o < cm.state.overlays.length; ++o) { @@ -4339,10 +4360,11 @@ window.CodeMirror = (function() { // Lightweight form of highlight -- proceed over this line and // update state, but don't save a style array. - function processLine(cm, line, state) { + function processLine(cm, text, state, startAt) { var mode = cm.doc.mode; - var stream = new StringStream(line.text, cm.options.tabSize); - if (line.text == "" && mode.blankLine) mode.blankLine(state); + var stream = new StringStream(text, cm.options.tabSize); + stream.start = stream.pos = startAt || 0; + if (text == "" && mode.blankLine) mode.blankLine(state); while (!stream.eol() && stream.pos <= cm.options.maxHighlightLength) { mode.token(stream, state); stream.start = stream.pos; @@ -4399,7 +4421,7 @@ window.CodeMirror = (function() { // Work around problem with the reported dimensions of single-char // direction spans on IE (issue #1129). See also the comment in // cursorCoords. - if (measure && ie && (order = getOrder(line))) { + if (measure && (ie || ie_gt10) && (order = getOrder(line))) { var l = order.length - 1; if (order[l].from == order[l].to) --l; var last = order[l], prev = order[l - 1]; @@ -4417,17 +4439,23 @@ window.CodeMirror = (function() { return builder; } - var tokenSpecialChars = /[\t\u0000-\u0019\u00ad\u200b\u2028\u2029\uFEFF]/g; + function defaultSpecialCharPlaceholder(ch) { + var token = elt("span", "\u2022", "cm-invalidchar"); + token.title = "\\u" + ch.charCodeAt(0).toString(16); + return token; + } + function buildToken(builder, text, style, startStyle, endStyle, title) { if (!text) return; - if (!tokenSpecialChars.test(text)) { + var special = builder.cm.options.specialChars; + if (!special.test(text)) { builder.col += text.length; var content = document.createTextNode(text); } else { var content = document.createDocumentFragment(), pos = 0; while (true) { - tokenSpecialChars.lastIndex = pos; - var m = tokenSpecialChars.exec(text); + special.lastIndex = pos; + var m = special.exec(text); var skipped = m ? m.index - pos : text.length - pos; if (skipped) { content.appendChild(document.createTextNode(text.slice(pos, pos + skipped))); @@ -4440,8 +4468,7 @@ window.CodeMirror = (function() { content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab")); builder.col += tabWidth; } else { - var token = elt("span", "\u2022", "cm-invalidchar"); - token.title = "\\u" + m[0].charCodeAt(0).toString(16); + var token = builder.cm.options.specialCharPlaceholder(m[0]); content.appendChild(token); builder.col += 1; } @@ -4594,7 +4621,8 @@ window.CodeMirror = (function() { var lastText = lst(text), lastSpans = spansFor(text.length - 1), nlines = to.line - from.line; // First adjust the line structure - if (from.ch == 0 && to.ch == 0 && lastText == "") { + if (from.ch == 0 && to.ch == 0 && lastText == "" && + (!doc.cm || doc.cm.options.wholeLineUpdateBefore)) { // This is a whole-line replace. Treated specially to make // sure line objects move the way they are supposed to. for (var i = 0, e = text.length - 1, added = []; i < e; ++i) @@ -5481,7 +5509,7 @@ window.CodeMirror = (function() { return true; } - var isExtendingChar = /[\u0300-\u036F\u0483-\u0487\u0488-\u0489\u0591-\u05BD\u05BF\u05C1-\u05C2\u05C4-\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7-\u06E8\u06EA-\u06ED\uA66F\uA670-\uA672\uA674-\uA67D\uA69F\udc00-\udfff]/; + var isExtendingChar = /[\u0300-\u036F\u0483-\u0487\u0488-\u0489\u0591-\u05BD\u05BF\u05C1-\u05C2\u05C4-\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7-\u06E8\u06EA-\u06ED\uA66F\u1DC0–\u1DFF\u20D0–\u20FF\uA670-\uA672\uA674-\uA67D\uA69F\udc00-\udfff\uFE20–\uFE2F]/; // DOM UTILITIES @@ -5910,7 +5938,7 @@ window.CodeMirror = (function() { // THE END - CodeMirror.version = "3.19.1"; + CodeMirror.version = "3.20.0"; return CodeMirror; })(); diff --git a/plugins/tiddlywiki/codemirror/files/keymap/emacs.js b/plugins/tiddlywiki/codemirror/files/keymap/emacs.js new file mode 100644 index 000000000..7a3dfb11a --- /dev/null +++ b/plugins/tiddlywiki/codemirror/files/keymap/emacs.js @@ -0,0 +1,387 @@ +(function() { + "use strict"; + + var Pos = CodeMirror.Pos; + function posEq(a, b) { return a.line == b.line && a.ch == b.ch; } + + // Kill 'ring' + + var killRing = []; + function addToRing(str) { + killRing.push(str); + if (killRing.length > 50) killRing.shift(); + } + function growRingTop(str) { + if (!killRing.length) return addToRing(str); + killRing[killRing.length - 1] += str; + } + function getFromRing(n) { return killRing[killRing.length - (n ? Math.min(n, 1) : 1)] || ""; } + function popFromRing() { if (killRing.length > 1) killRing.pop(); return getFromRing(); } + + var lastKill = null; + + function kill(cm, from, to, mayGrow, text) { + if (text == null) text = cm.getRange(from, to); + + if (mayGrow && lastKill && lastKill.cm == cm && posEq(from, lastKill.pos) && cm.isClean(lastKill.gen)) + growRingTop(text); + else + addToRing(text); + cm.replaceRange("", from, to, "+delete"); + + if (mayGrow) lastKill = {cm: cm, pos: from, gen: cm.changeGeneration()}; + else lastKill = null; + } + + // Boundaries of various units + + function byChar(cm, pos, dir) { + return cm.findPosH(pos, dir, "char", true); + } + + function byWord(cm, pos, dir) { + return cm.findPosH(pos, dir, "word", true); + } + + function byLine(cm, pos, dir) { + return cm.findPosV(pos, dir, "line", cm.doc.sel.goalColumn); + } + + function byPage(cm, pos, dir) { + return cm.findPosV(pos, dir, "page", cm.doc.sel.goalColumn); + } + + function byParagraph(cm, pos, dir) { + var no = pos.line, line = cm.getLine(no); + var sawText = /\S/.test(dir < 0 ? line.slice(0, pos.ch) : line.slice(pos.ch)); + var fst = cm.firstLine(), lst = cm.lastLine(); + for (;;) { + no += dir; + if (no < fst || no > lst) + return cm.clipPos(Pos(no - dir, dir < 0 ? 0 : null)); + line = cm.getLine(no); + var hasText = /\S/.test(line); + if (hasText) sawText = true; + else if (sawText) return Pos(no, 0); + } + } + + function bySentence(cm, pos, dir) { + var line = pos.line, ch = pos.ch; + var text = cm.getLine(pos.line), sawWord = false; + for (;;) { + var next = text.charAt(ch + (dir < 0 ? -1 : 0)); + if (!next) { // End/beginning of line reached + if (line == (dir < 0 ? cm.firstLine() : cm.lastLine())) return Pos(line, ch); + text = cm.getLine(line + dir); + if (!/\S/.test(text)) return Pos(line, ch); + line += dir; + ch = dir < 0 ? text.length : 0; + continue; + } + if (sawWord && /[!?.]/.test(next)) return Pos(line, ch + (dir > 0 ? 1 : 0)); + if (!sawWord) sawWord = /\w/.test(next); + ch += dir; + } + } + + function byExpr(cm, pos, dir) { + var wrap; + if (cm.findMatchingBracket && (wrap = cm.findMatchingBracket(pos, true)) + && wrap.match && (wrap.forward ? 1 : -1) == dir) + return dir > 0 ? Pos(wrap.to.line, wrap.to.ch + 1) : wrap.to; + + for (var first = true;; first = false) { + var token = cm.getTokenAt(pos); + var after = Pos(pos.line, dir < 0 ? token.start : token.end); + if (first && dir > 0 && token.end == pos.ch || !/\w/.test(token.string)) { + var newPos = cm.findPosH(after, dir, "char"); + if (posEq(after, newPos)) return pos; + else pos = newPos; + } else { + return after; + } + } + } + + // Prefixes (only crudely supported) + + function getPrefix(cm, precise) { + var digits = cm.state.emacsPrefix; + if (!digits) return precise ? null : 1; + clearPrefix(cm); + return digits == "-" ? -1 : Number(digits); + } + + function repeated(cmd) { + var f = typeof cmd == "string" ? function(cm) { cm.execCommand(cmd); } : cmd; + return function(cm) { + var prefix = getPrefix(cm); + f(cm); + for (var i = 1; i < prefix; ++i) f(cm); + }; + } + + function findEnd(cm, by, dir) { + var pos = cm.getCursor(), prefix = getPrefix(cm); + if (prefix < 0) { dir = -dir; prefix = -prefix; } + for (var i = 0; i < prefix; ++i) { + var newPos = by(cm, pos, dir); + if (posEq(newPos, pos)) break; + pos = newPos; + } + return pos; + } + + function move(by, dir) { + var f = function(cm) { + cm.extendSelection(findEnd(cm, by, dir)); + }; + f.motion = true; + return f; + } + + function killTo(cm, by, dir) { + kill(cm, cm.getCursor(), findEnd(cm, by, dir), true); + } + + function addPrefix(cm, digit) { + if (cm.state.emacsPrefix) { + if (digit != "-") cm.state.emacsPrefix += digit; + return; + } + // Not active yet + cm.state.emacsPrefix = digit; + cm.on("keyHandled", maybeClearPrefix); + cm.on("inputRead", maybeDuplicateInput); + } + + var prefixPreservingKeys = {"Alt-G": true, "Ctrl-X": true, "Ctrl-Q": true, "Ctrl-U": true}; + + function maybeClearPrefix(cm, arg) { + if (!cm.state.emacsPrefixMap && !prefixPreservingKeys.hasOwnProperty(arg)) + clearPrefix(cm); + } + + function clearPrefix(cm) { + cm.state.emacsPrefix = null; + cm.off("keyHandled", maybeClearPrefix); + cm.off("inputRead", maybeDuplicateInput); + } + + function maybeDuplicateInput(cm, event) { + var dup = getPrefix(cm); + if (dup > 1 && event.origin == "+input") { + var one = event.text.join("\n"), txt = ""; + for (var i = 1; i < dup; ++i) txt += one; + cm.replaceSelection(txt, "end", "+input"); + } + } + + function addPrefixMap(cm) { + cm.state.emacsPrefixMap = true; + cm.addKeyMap(prefixMap); + cm.on("keyHandled", maybeRemovePrefixMap); + cm.on("inputRead", maybeRemovePrefixMap); + } + + function maybeRemovePrefixMap(cm, arg) { + if (typeof arg == "string" && (/^\d$/.test(arg) || arg == "Ctrl-U")) return; + cm.removeKeyMap(prefixMap); + cm.state.emacsPrefixMap = false; + cm.off("keyHandled", maybeRemovePrefixMap); + cm.off("inputRead", maybeRemovePrefixMap); + } + + // Utilities + + function setMark(cm) { + cm.setCursor(cm.getCursor()); + cm.setExtending(true); + cm.on("change", function() { cm.setExtending(false); }); + } + + function getInput(cm, msg, f) { + if (cm.openDialog) + cm.openDialog(msg + ": ", f, {bottom: true}); + else + f(prompt(msg, "")); + } + + function operateOnWord(cm, op) { + var start = cm.getCursor(), end = cm.findPosH(start, 1, "word"); + cm.replaceRange(op(cm.getRange(start, end)), start, end); + cm.setCursor(end); + } + + function toEnclosingExpr(cm) { + var pos = cm.getCursor(), line = pos.line, ch = pos.ch; + var stack = []; + while (line >= cm.firstLine()) { + var text = cm.getLine(line); + for (var i = ch == null ? text.length : ch; i > 0;) { + var ch = text.charAt(--i); + if (ch == ")") + stack.push("("); + else if (ch == "]") + stack.push("["); + else if (ch == "}") + stack.push("{"); + else if (/[\(\{\[]/.test(ch) && (!stack.length || stack.pop() != ch)) + return cm.extendSelection(Pos(line, i)); + } + --line; ch = null; + } + } + + // Actual keymap + + var keyMap = CodeMirror.keyMap.emacs = { + "Ctrl-W": function(cm) {kill(cm, cm.getCursor("start"), cm.getCursor("end"));}, + "Ctrl-K": repeated(function(cm) { + var start = cm.getCursor(), end = cm.clipPos(Pos(start.line)); + var text = cm.getRange(start, end); + if (!/\S/.test(text)) { + text += "\n"; + end = Pos(start.line + 1, 0); + } + kill(cm, start, end, true, text); + }), + "Alt-W": function(cm) { + addToRing(cm.getSelection()); + }, + "Ctrl-Y": function(cm) { + var start = cm.getCursor(); + cm.replaceRange(getFromRing(getPrefix(cm)), start, start, "paste"); + cm.setSelection(start, cm.getCursor()); + }, + "Alt-Y": function(cm) {cm.replaceSelection(popFromRing());}, + + "Ctrl-Space": setMark, "Ctrl-Shift-2": setMark, + + "Ctrl-F": move(byChar, 1), "Ctrl-B": move(byChar, -1), + "Right": move(byChar, 1), "Left": move(byChar, -1), + "Ctrl-D": function(cm) { killTo(cm, byChar, 1); }, + "Delete": function(cm) { killTo(cm, byChar, 1); }, + "Ctrl-H": function(cm) { killTo(cm, byChar, -1); }, + "Backspace": function(cm) { killTo(cm, byChar, -1); }, + + "Alt-F": move(byWord, 1), "Alt-B": move(byWord, -1), + "Alt-D": function(cm) { killTo(cm, byWord, 1); }, + "Alt-Backspace": function(cm) { killTo(cm, byWord, -1); }, + + "Ctrl-N": move(byLine, 1), "Ctrl-P": move(byLine, -1), + "Down": move(byLine, 1), "Up": move(byLine, -1), + "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd", + "End": "goLineEnd", "Home": "goLineStart", + + "Alt-V": move(byPage, -1), "Ctrl-V": move(byPage, 1), + "PageUp": move(byPage, -1), "PageDown": move(byPage, 1), + + "Ctrl-Up": move(byParagraph, -1), "Ctrl-Down": move(byParagraph, 1), + + "Alt-A": move(bySentence, -1), "Alt-E": move(bySentence, 1), + "Alt-K": function(cm) { killTo(cm, bySentence, 1); }, + + "Ctrl-Alt-K": function(cm) { killTo(cm, byExpr, 1); }, + "Ctrl-Alt-Backspace": function(cm) { killTo(cm, byExpr, -1); }, + "Ctrl-Alt-F": move(byExpr, 1), "Ctrl-Alt-B": move(byExpr, -1), + + "Shift-Ctrl-Alt-2": function(cm) { + cm.setSelection(findEnd(cm, byExpr, 1), cm.getCursor()); + }, + "Ctrl-Alt-T": function(cm) { + var leftStart = byExpr(cm, cm.getCursor(), -1), leftEnd = byExpr(cm, leftStart, 1); + var rightEnd = byExpr(cm, leftEnd, 1), rightStart = byExpr(cm, rightEnd, -1); + cm.replaceRange(cm.getRange(rightStart, rightEnd) + cm.getRange(leftEnd, rightStart) + + cm.getRange(leftStart, leftEnd), leftStart, rightEnd); + }, + "Ctrl-Alt-U": repeated(toEnclosingExpr), + + "Alt-Space": function(cm) { + var pos = cm.getCursor(), from = pos.ch, to = pos.ch, text = cm.getLine(pos.line); + while (from && /\s/.test(text.charAt(from - 1))) --from; + while (to < text.length && /\s/.test(text.charAt(to))) ++to; + cm.replaceRange(" ", Pos(pos.line, from), Pos(pos.line, to)); + }, + "Ctrl-O": repeated(function(cm) { cm.replaceSelection("\n", "start"); }), + "Ctrl-T": repeated(function(cm) { + var pos = cm.getCursor(); + if (pos.ch < cm.getLine(pos.line).length) pos = Pos(pos.line, pos.ch + 1); + var from = cm.findPosH(pos, -2, "char"); + var range = cm.getRange(from, pos); + if (range.length != 2) return; + cm.setSelection(from, pos); + cm.replaceSelection(range.charAt(1) + range.charAt(0), "end"); + }), + + "Alt-C": repeated(function(cm) { + operateOnWord(cm, function(w) { + var letter = w.search(/\w/); + if (letter == -1) return w; + return w.slice(0, letter) + w.charAt(letter).toUpperCase() + w.slice(letter + 1).toLowerCase(); + }); + }), + "Alt-U": repeated(function(cm) { + operateOnWord(cm, function(w) { return w.toUpperCase(); }); + }), + "Alt-L": repeated(function(cm) { + operateOnWord(cm, function(w) { return w.toLowerCase(); }); + }), + + "Alt-;": "toggleComment", + + "Ctrl-/": repeated("undo"), "Shift-Ctrl--": repeated("undo"), + "Ctrl-Z": repeated("undo"), "Cmd-Z": repeated("undo"), + "Shift-Alt-,": "goDocStart", "Shift-Alt-.": "goDocEnd", + "Ctrl-S": "findNext", "Ctrl-R": "findPrev", "Ctrl-G": "clearSearch", "Shift-Alt-5": "replace", + "Alt-/": "autocomplete", + "Ctrl-J": "newlineAndIndent", "Enter": false, "Tab": "indentAuto", + + "Alt-G": function(cm) {cm.setOption("keyMap", "emacs-Alt-G");}, + "Ctrl-X": function(cm) {cm.setOption("keyMap", "emacs-Ctrl-X");}, + "Ctrl-Q": function(cm) {cm.setOption("keyMap", "emacs-Ctrl-Q");}, + "Ctrl-U": addPrefixMap + }; + + CodeMirror.keyMap["emacs-Ctrl-X"] = { + "Tab": function(cm) { + cm.indentSelection(getPrefix(cm, true) || cm.getOption("indentUnit")); + }, + "Ctrl-X": function(cm) { + cm.setSelection(cm.getCursor("head"), cm.getCursor("anchor")); + }, + + "Ctrl-S": "save", "Ctrl-W": "save", "S": "saveAll", "F": "open", "U": repeated("undo"), "K": "close", + "Delete": function(cm) { kill(cm, cm.getCursor(), bySentence(cm, cm.getCursor(), 1), true); }, + auto: "emacs", nofallthrough: true, disableInput: true + }; + + CodeMirror.keyMap["emacs-Alt-G"] = { + "G": function(cm) { + var prefix = getPrefix(cm, true); + if (prefix != null && prefix > 0) return cm.setCursor(prefix - 1); + + getInput(cm, "Goto line", function(str) { + var num; + if (str && !isNaN(num = Number(str)) && num == num|0 && num > 0) + cm.setCursor(num - 1); + }); + }, + auto: "emacs", nofallthrough: true, disableInput: true + }; + + CodeMirror.keyMap["emacs-Ctrl-Q"] = { + "Tab": repeated("insertTab"), + auto: "emacs", nofallthrough: true + }; + + var prefixMap = {"Ctrl-G": clearPrefix}; + function regPrefix(d) { + prefixMap[d] = function(cm) { addPrefix(cm, d); }; + keyMap["Ctrl-" + d] = function(cm) { addPrefix(cm, d); }; + prefixPreservingKeys["Ctrl-" + d] = true; + } + for (var i = 0; i < 10; ++i) regPrefix(String(i)); + regPrefix("-"); +})(); diff --git a/plugins/tiddlywiki/codemirror/files/keymap/extra.js b/plugins/tiddlywiki/codemirror/files/keymap/extra.js new file mode 100644 index 000000000..18dd5a979 --- /dev/null +++ b/plugins/tiddlywiki/codemirror/files/keymap/extra.js @@ -0,0 +1,43 @@ +// A number of additional default bindings that are too obscure to +// include in the core codemirror.js file. + +(function() { + "use strict"; + + var Pos = CodeMirror.Pos; + + function moveLines(cm, start, end, dist) { + if (!dist || start > end) return 0; + + var from = cm.clipPos(Pos(start, 0)), to = cm.clipPos(Pos(end)); + var text = cm.getRange(from, to); + + if (start <= cm.firstLine()) + cm.replaceRange("", from, Pos(to.line + 1, 0)); + else + cm.replaceRange("", Pos(from.line - 1), to); + var target = from.line + dist; + if (target <= cm.firstLine()) { + cm.replaceRange(text + "\n", Pos(target, 0)); + return cm.firstLine() - from.line; + } else { + var targetPos = cm.clipPos(Pos(target - 1)); + cm.replaceRange("\n" + text, targetPos); + return targetPos.line + 1 - from.line; + } + } + + function moveSelectedLines(cm, dist) { + var head = cm.getCursor("head"), anchor = cm.getCursor("anchor"); + cm.operation(function() { + var moved = moveLines(cm, Math.min(head.line, anchor.line), Math.max(head.line, anchor.line), dist); + cm.setSelection(Pos(anchor.line + moved, anchor.ch), Pos(head.line + moved, head.ch)); + }); + } + + CodeMirror.commands.moveLinesUp = function(cm) { moveSelectedLines(cm, -1); }; + CodeMirror.commands.moveLinesDown = function(cm) { moveSelectedLines(cm, 1); }; + + CodeMirror.keyMap["default"]["Alt-Up"] = "moveLinesUp"; + CodeMirror.keyMap["default"]["Alt-Down"] = "moveLinesDown"; +})(); diff --git a/plugins/tiddlywiki/codemirror/files/keymap/vim.js b/plugins/tiddlywiki/codemirror/files/keymap/vim.js new file mode 100644 index 000000000..dab10e21a --- /dev/null +++ b/plugins/tiddlywiki/codemirror/files/keymap/vim.js @@ -0,0 +1,3703 @@ +/** + * Supported keybindings: + * + * Motion: + * h, j, k, l + * gj, gk + * 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 + * zz, z., z, zt, zb, z- + * 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. + * + * Events: + * 'vim-mode-change' - raised on the editor anytime the current mode changes, + * Event object: {mode: "visual", subMode: "linewise"} + * + * Code structure: + * 1. Default keymap + * 2. Variable declarations and short basic helpers + * 3. Instance (External API) implementation + * 4. Internal state tracking objects (input state, counter) implementation + * and instanstiation + * 5. Key handler (the main command dispatcher) implementation + * 6. Motion, operator, and action implementations + * 7. Helper functions for the key handler, motions, operators, and actions + * 8. Set up Vim to work as a keymap for CodeMirror. + */ + +(function() { + 'use strict'; + + var defaultKeymap = [ + // Key to key mapping. This goes first to make it possible to override + // existing mappings. + { keys: [''], type: 'keyToKey', toKeys: ['h'] }, + { keys: [''], type: 'keyToKey', toKeys: ['l'] }, + { keys: [''], type: 'keyToKey', toKeys: ['k'] }, + { keys: [''], type: 'keyToKey', toKeys: ['j'] }, + { keys: [''], type: 'keyToKey', toKeys: ['l'] }, + { keys: [''], type: 'keyToKey', toKeys: ['h'] }, + { keys: [''], type: 'keyToKey', toKeys: ['W'] }, + { keys: [''], type: 'keyToKey', toKeys: ['B'] }, + { keys: [''], type: 'keyToKey', toKeys: ['w'] }, + { keys: [''], type: 'keyToKey', toKeys: ['b'] }, + { keys: [''], type: 'keyToKey', toKeys: ['j'] }, + { keys: [''], type: 'keyToKey', toKeys: ['k'] }, + { keys: ['C-['], type: 'keyToKey', toKeys: [''] }, + { keys: [''], type: 'keyToKey', toKeys: [''] }, + { keys: ['s'], type: 'keyToKey', toKeys: ['c', 'l'], context: 'normal' }, + { keys: ['s'], type: 'keyToKey', toKeys: ['x', 'i'], context: 'visual'}, + { keys: ['S'], type: 'keyToKey', toKeys: ['c', 'c'], context: 'normal' }, + { keys: ['S'], type: 'keyToKey', toKeys: ['d', 'c', 'c'], context: 'visual' }, + { keys: [''], type: 'keyToKey', toKeys: ['0'] }, + { keys: [''], type: 'keyToKey', toKeys: ['$'] }, + { keys: [''], type: 'keyToKey', toKeys: [''] }, + { keys: [''], type: 'keyToKey', toKeys: [''] }, + // Motions + { keys: ['H'], type: 'motion', + motion: 'moveToTopLine', + motionArgs: { linewise: true, toJumplist: true }}, + { keys: ['M'], type: 'motion', + motion: 'moveToMiddleLine', + motionArgs: { linewise: true, toJumplist: true }}, + { keys: ['L'], type: 'motion', + motion: 'moveToBottomLine', + motionArgs: { linewise: true, toJumplist: true }}, + { keys: ['h'], type: 'motion', + motion: 'moveByCharacters', + motionArgs: { forward: false }}, + { keys: ['l'], type: 'motion', + motion: 'moveByCharacters', + motionArgs: { forward: true }}, + { keys: ['j'], type: 'motion', + motion: 'moveByLines', + motionArgs: { forward: true, linewise: true }}, + { keys: ['k'], type: 'motion', + motion: 'moveByLines', + motionArgs: { forward: false, linewise: true }}, + { keys: ['g','j'], type: 'motion', + motion: 'moveByDisplayLines', + motionArgs: { forward: true }}, + { keys: ['g','k'], type: 'motion', + motion: 'moveByDisplayLines', + motionArgs: { forward: false }}, + { keys: ['w'], type: 'motion', + motion: 'moveByWords', + motionArgs: { forward: true, wordEnd: false }}, + { keys: ['W'], type: 'motion', + motion: 'moveByWords', + motionArgs: { forward: true, wordEnd: false, bigWord: true }}, + { keys: ['e'], type: 'motion', + motion: 'moveByWords', + motionArgs: { forward: true, wordEnd: true, inclusive: true }}, + { keys: ['E'], type: 'motion', + motion: 'moveByWords', + motionArgs: { forward: true, wordEnd: true, bigWord: true, + inclusive: true }}, + { keys: ['b'], type: 'motion', + motion: 'moveByWords', + motionArgs: { forward: false, wordEnd: false }}, + { keys: ['B'], type: 'motion', + motion: 'moveByWords', + motionArgs: { forward: false, wordEnd: false, bigWord: true }}, + { keys: ['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: ['{'], type: 'motion', motion: 'moveByParagraph', + motionArgs: { forward: false, toJumplist: true }}, + { keys: ['}'], type: 'motion', motion: 'moveByParagraph', + motionArgs: { forward: true, toJumplist: true }}, + { keys: [''], type: 'motion', + motion: 'moveByPage', motionArgs: { forward: true }}, + { keys: [''], type: 'motion', + motion: 'moveByPage', motionArgs: { forward: false }}, + { keys: [''], type: 'motion', + motion: 'moveByScroll', + motionArgs: { forward: true, explicitRepeat: true }}, + { keys: [''], type: 'motion', + motion: 'moveByScroll', + motionArgs: { forward: false, explicitRepeat: true }}, + { keys: ['g', 'g'], type: 'motion', + motion: 'moveToLineOrEdgeOfDocument', + motionArgs: { forward: false, explicitRepeat: true, linewise: true, toJumplist: true }}, + { keys: ['G'], type: 'motion', + motion: 'moveToLineOrEdgeOfDocument', + motionArgs: { forward: true, explicitRepeat: true, linewise: true, toJumplist: true }}, + { keys: ['0'], type: 'motion', motion: 'moveToStartOfLine' }, + { keys: ['^'], type: 'motion', + motion: 'moveToFirstNonWhiteSpaceCharacter' }, + { keys: ['+'], type: 'motion', + motion: 'moveByLines', + motionArgs: { forward: true, toFirstChar:true }}, + { keys: ['-'], type: 'motion', + motion: 'moveByLines', + motionArgs: { forward: false, toFirstChar:true }}, + { keys: ['_'], type: 'motion', + motion: 'moveByLines', + motionArgs: { forward: true, toFirstChar:true, repeatOffset:-1 }}, + { keys: ['$'], type: 'motion', + motion: 'moveToEol', + motionArgs: { inclusive: true }}, + { keys: ['%'], type: 'motion', + motion: 'moveToMatchedSymbol', + motionArgs: { inclusive: true, toJumplist: true }}, + { keys: ['f', 'character'], type: 'motion', + motion: 'moveToCharacter', + motionArgs: { forward: true , inclusive: true }}, + { keys: ['F', 'character'], type: 'motion', + motion: 'moveToCharacter', + motionArgs: { forward: false }}, + { keys: ['t', 'character'], type: 'motion', + motion: 'moveTillCharacter', + motionArgs: { forward: true, inclusive: true }}, + { keys: ['T', 'character'], type: 'motion', + motion: 'moveTillCharacter', + motionArgs: { forward: false }}, + { keys: [';'], type: 'motion', motion: 'repeatLastCharacterSearch', + motionArgs: { forward: true }}, + { keys: [','], type: 'motion', motion: 'repeatLastCharacterSearch', + motionArgs: { forward: false }}, + { keys: ['\'', 'character'], type: 'motion', motion: 'goToMark', + motionArgs: {toJumplist: true}}, + { keys: ['`', 'character'], type: 'motion', motion: 'goToMark', + motionArgs: {toJumplist: true}}, + { keys: [']', '`'], type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true } }, + { keys: ['[', '`'], type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false } }, + { keys: [']', '\''], type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true, linewise: true } }, + { keys: ['[', '\''], type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false, linewise: true } }, + { keys: [']', 'character'], type: 'motion', + motion: 'moveToSymbol', + motionArgs: { forward: true, toJumplist: true}}, + { keys: ['[', 'character'], type: 'motion', + motion: 'moveToSymbol', + motionArgs: { forward: false, toJumplist: true}}, + { keys: ['|'], type: 'motion', + motion: 'moveToColumn', + motionArgs: { }}, + // Operators + { keys: ['d'], type: 'operator', operator: 'delete' }, + { keys: ['y'], type: 'operator', operator: 'yank' }, + { keys: ['c'], type: 'operator', operator: 'change' }, + { 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', + motionArgs: { forward: true, toJumplist: true }}, + { keys: ['N'], type: 'motion', motion: 'findNext', + motionArgs: { forward: false, toJumplist: true }}, + // 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', + motion: 'moveToEol', motionArgs: { inclusive: true }, + operatorMotionArgs: { visualLine: true }}, + { keys: ['~'], type: 'operatorMotion', + operator: 'swapcase', operatorArgs: { shouldMoveCursor: true }, + motion: 'moveByCharacters', motionArgs: { forward: true }}, + // Actions + { keys: [''], type: 'action', action: 'jumpListWalk', + actionArgs: { forward: true }}, + { keys: [''], type: 'action', action: 'jumpListWalk', + actionArgs: { forward: false }}, + { keys: ['a'], type: 'action', action: 'enterInsertMode', isEdit: true, + actionArgs: { insertAt: 'charAfter' }}, + { keys: ['A'], type: 'action', action: 'enterInsertMode', isEdit: true, + actionArgs: { insertAt: 'eol' }}, + { keys: ['i'], type: 'action', action: 'enterInsertMode', isEdit: true, + actionArgs: { insertAt: 'inplace' }}, + { keys: ['I'], type: 'action', action: 'enterInsertMode', isEdit: true, + actionArgs: { insertAt: 'firstNonBlank' }}, + { keys: ['o'], type: 'action', action: 'newLineAndEnterInsertMode', + isEdit: true, interlaceInsertRepeat: true, + actionArgs: { after: true }}, + { keys: ['O'], type: 'action', action: 'newLineAndEnterInsertMode', + isEdit: true, interlaceInsertRepeat: true, + actionArgs: { after: false }}, + { keys: ['v'], type: 'action', action: 'toggleVisualMode' }, + { keys: ['V'], type: 'action', action: 'toggleVisualMode', + actionArgs: { linewise: true }}, + { keys: ['J'], type: 'action', action: 'joinLines', isEdit: true }, + { keys: ['p'], type: 'action', action: 'paste', isEdit: true, + actionArgs: { after: true, isEdit: true }}, + { keys: ['P'], type: 'action', action: 'paste', isEdit: true, + actionArgs: { after: false, isEdit: true }}, + { keys: ['r', 'character'], type: 'action', action: 'replace', isEdit: true }, + { keys: ['@', 'character'], type: 'action', action: 'replayMacro' }, + { keys: ['q', 'character'], type: 'action', action: 'enterMacroRecordMode' }, + // Handle Replace-mode as a special case of insert mode. + { keys: ['R'], type: 'action', action: 'enterInsertMode', isEdit: true, + actionArgs: { replace: true }}, + { keys: ['u'], type: 'action', action: 'undo' }, + { keys: [''], type: 'action', action: 'redo' }, + { keys: ['m', 'character'], type: 'action', action: 'setMark' }, + { keys: ['"', 'character'], type: 'action', action: 'setRegister' }, + { keys: ['z', 'z'], type: 'action', action: 'scrollToCursor', + actionArgs: { position: 'center' }}, + { keys: ['z', '.'], type: 'action', action: 'scrollToCursor', + actionArgs: { position: 'center' }, + motion: 'moveToFirstNonWhiteSpaceCharacter' }, + { keys: ['z', 't'], type: 'action', action: 'scrollToCursor', + actionArgs: { position: 'top' }}, + { keys: ['z', ''], type: 'action', action: 'scrollToCursor', + actionArgs: { position: 'top' }, + motion: 'moveToFirstNonWhiteSpaceCharacter' }, + { keys: ['z', '-'], type: 'action', action: 'scrollToCursor', + actionArgs: { position: 'bottom' }}, + { keys: ['z', 'b'], type: 'action', action: 'scrollToCursor', + actionArgs: { position: 'bottom' }, + motion: 'moveToFirstNonWhiteSpaceCharacter' }, + { keys: ['.'], type: 'action', action: 'repeatLastEdit' }, + { keys: [''], type: 'action', action: 'incrementNumberToken', + isEdit: true, + actionArgs: {increase: true, backtrack: false}}, + { keys: [''], type: 'action', action: 'incrementNumberToken', + isEdit: true, + actionArgs: {increase: false, backtrack: false}}, + // Text object motions + { keys: ['a', 'character'], type: 'motion', + motion: 'textObjectManipulation' }, + { keys: ['i', 'character'], type: 'motion', + motion: 'textObjectManipulation', + motionArgs: { textObjectInner: true }}, + // Search + { keys: ['/'], type: 'search', + searchArgs: { forward: true, querySrc: 'prompt', toJumplist: true }}, + { keys: ['?'], type: 'search', + searchArgs: { forward: false, querySrc: 'prompt', toJumplist: true }}, + { keys: ['*'], type: 'search', + searchArgs: { forward: true, querySrc: 'wordUnderCursor', toJumplist: true }}, + { keys: ['#'], type: 'search', + searchArgs: { forward: false, querySrc: 'wordUnderCursor', toJumplist: true }}, + // Ex command + { keys: [':'], type: 'ex' } + ]; + + var Vim = function() { + CodeMirror.defineOption('vimMode', false, function(cm, val) { + if (val) { + cm.setOption('keyMap', 'vim'); + CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"}); + cm.on('beforeSelectionChange', beforeSelectionChange); + maybeInitVimState(cm); + } else if (cm.state.vim) { + cm.setOption('keyMap', 'default'); + cm.off('beforeSelectionChange', beforeSelectionChange); + cm.state.vim = null; + } + }); + function beforeSelectionChange(cm, cur) { + var vim = cm.state.vim; + if (vim.insertMode || vim.exMode) return; + + var head = cur.head; + if (head.ch && head.ch == cm.doc.getLine(head.line).length) { + head.ch--; + } + } + + var numberRegex = /[\d]/; + 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 specialSymbols = '~`!@#$%^&*()_-+=[{}]\\|/?.,<>:;"\''.split(''); + var specialKeys = ['Left', 'Right', 'Up', 'Down', 'Space', 'Backspace', + 'Esc', 'Home', 'End', 'PageUp', 'PageDown', 'Enter']; + var validMarks = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['<', '>']); + var validRegisters = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['-', '"']); + + function isLine(cm, line) { + return line >= cm.firstLine() && line <= cm.lastLine(); + } + function isLowerCase(k) { + return (/^[a-z]$/).test(k); + } + function isMatchableSymbol(k) { + return '()[]{}'.indexOf(k) != -1; + } + function isNumber(k) { + return numberRegex.test(k); + } + function isUpperCase(k) { + return (/^[A-Z]$/).test(k); + } + function isWhiteSpaceString(k) { + return (/^\s*$/).test(k); + } + function inArray(val, arr) { + for (var i = 0; i < arr.length; i++) { + if (arr[i] == val) { + return true; + } + } + return false; + } + + var createCircularJumpList = function() { + var size = 100; + var pointer = -1; + var head = 0; + var tail = 0; + var buffer = new Array(size); + function add(cm, oldCur, newCur) { + var current = pointer % size; + var curMark = buffer[current]; + function useNextSlot(cursor) { + var next = ++pointer % size; + var trashMark = buffer[next]; + if (trashMark) { + trashMark.clear(); + } + buffer[next] = cm.setBookmark(cursor); + } + if (curMark) { + var markPos = curMark.find(); + // avoid recording redundant cursor position + if (markPos && !cursorEqual(markPos, oldCur)) { + useNextSlot(oldCur); + } + } else { + useNextSlot(oldCur); + } + useNextSlot(newCur); + head = pointer; + tail = pointer - size + 1; + if (tail < 0) { + tail = 0; + } + } + function move(cm, offset) { + pointer += offset; + if (pointer > head) { + pointer = head; + } else if (pointer < tail) { + pointer = tail; + } + var mark = buffer[(size + pointer) % size]; + // skip marks that are temporarily removed from text buffer + if (mark && !mark.find()) { + var inc = offset > 0 ? 1 : -1; + var newCur; + var oldCur = cm.getCursor(); + do { + pointer += inc; + mark = buffer[(size + pointer) % size]; + // skip marks that are the same as current position + if (mark && + (newCur = mark.find()) && + !cursorEqual(oldCur, newCur)) { + break; + } + } while (pointer < head && pointer > tail); + } + return mark; + } + return { + cachedCursor: undefined, //used for # and * jumps + add: add, + move: move + }; + }; + + var createMacroState = function() { + return { + macroKeyBuffer: [], + latestRegister: undefined, + inReplay: false, + lastInsertModeChanges: { + changes: [], // Change list + expectCursorActivityForChange: false // Set to true on change, false on cursorActivity. + }, + enteredMacroMode: undefined, + isMacroPlaying: false, + toggle: function(cm, registerName) { + if (this.enteredMacroMode) { //onExit + this.enteredMacroMode(); // close dialog + this.enteredMacroMode = undefined; + } else { //onEnter + this.latestRegister = registerName; + this.enteredMacroMode = cm.openDialog( + '(recording)['+registerName+']', null, {bottom:true}); + } + } + }; + }; + + + function maybeInitVimState(cm) { + if (!cm.state.vim) { + // Store instance state in the CodeMirror object. + cm.state.vim = { + inputState: new InputState(), + // Vim's input state that triggered the last edit, used to repeat + // motions and operators with '.'. + lastEditInputState: undefined, + // Vim's action command before the last edit, used to repeat actions + // with '.' and insert mode repeat. + lastEditActionCommand: undefined, + // When using jk for navigation, if you move from a longer line to a + // shorter line, the cursor may clip to the end of the shorter line. + // If j is pressed again and cursor goes to the next line, the + // cursor should go back to its horizontal position on the longer + // line if it can. This is to keep track of the horizontal position. + lastHPos: -1, + // Doing the same with screen-position for gj/gk + lastHSPos: -1, + // The last motion command run. Cleared if a non-motion command gets + // executed in between. + lastMotion: null, + marks: {}, + insertMode: false, + // Repeat count for changes made in insert mode, triggered by key + // sequences like 3,i. Only exists when insertMode is true. + insertModeRepeat: undefined, + visualMode: false, + // If we are in visual line mode. No effect if visualMode is false. + visualLine: false + }; + } + return cm.state.vim; + } + var vimGlobalState; + function resetVimGlobalState() { + vimGlobalState = { + // The current search query. + searchQuery: null, + // Whether we are searching backwards. + searchIsReversed: false, + jumpList: createCircularJumpList(), + macroModeState: createMacroState(), + // Recording latest f, t, F or T motion command. + lastChararacterSearch: {increment:0, forward:true, selectedCharacter:''}, + registerController: new RegisterController({}) + }; + } + + 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 vimGlobalState.registerController; + }, + // Testing hook. + resetVimGlobalState_: resetVimGlobalState, + + // Testing hook. + getVimGlobalState_: function() { + return vimGlobalState; + }, + + // Testing hook. + maybeInitVimState_: maybeInitVimState, + + InsertModeKey: InsertModeKey, + map: function(lhs, rhs) { + // Add user defined key bindings. + exCommandDispatcher.map(lhs, rhs); + }, + defineEx: function(name, prefix, func){ + if (name.indexOf(prefix) !== 0) { + throw new Error('(Vim.defineEx) "'+prefix+'" is not a prefix of "'+name+'", command not registered'); + } + exCommands[name]=func; + exCommandDispatcher.commandMap_[prefix]={name:name, shortName:prefix, type:'api'}; + }, + // 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 = maybeInitVimState(cm); + var macroModeState = vimGlobalState.macroModeState; + if (macroModeState.enteredMacroMode) { + if (key == 'q') { + actions.exitMacroRecordMode(); + vim.inputState = new InputState(); + return; + } + } + if (key == '') { + // Clear input state and get back to normal mode. + vim.inputState = new InputState(); + if (vim.visualMode) { + exitVisualMode(cm); + } + return; + } + // Enter visual mode when the mouse selects text. + if (!vim.visualMode && + !cursorEqual(cm.getCursor('head'), cm.getCursor('anchor'))) { + vim.visualMode = true; + vim.visualLine = false; + CodeMirror.signal(cm, "vim-mode-change", {mode: "visual"}); + cm.on('mousedown', exitVisualMode); + } + 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 { + if (macroModeState.enteredMacroMode) { + logKey(macroModeState, key); + } + commandDispatcher.processCommand(cm, vim, command); + } + }, + handleEx: function(cm, input) { + exCommandDispatcher.processCommand(cm, input); + } + }; + + // Represents the current input state. + function InputState() { + this.prefixRepeat = []; + this.motionRepeat = []; + + this.operator = null; + this.operatorArgs = null; + this.motion = null; + this.motionArgs = null; + this.keyBuffer = []; // For matching multi-key commands. + this.registerName = null; // Defaults to the 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) { + if (linewise && text.charAt(0) == '\n') { + text = text.slice(1) + '\n'; + } + if(linewise && text.charAt(text.length - 1) !== '\n'){ + text += '\n'; + } + // Lowercase and uppercase registers refer to the same register. + // Uppercase just means append. + var register = this.isValidRegister(registerName) ? + this.getRegister(registerName) : null; + // if no register/an invalid register was specified, things go to the + // default registers + if (!register) { + switch (operator) { + case 'yank': + // The 0 register contains the text from the most recent yank. + this.registers['0'] = new Register(text, linewise); + break; + case 'delete': + case 'change': + if (text.indexOf('\n') == -1) { + // Delete less than 1 line. Update the small delete register. + this.registers['-'] = new Register(text, linewise); + } else { + // Shift down the contents of the numbered registers and put the + // deleted text into register 1. + this.shiftNumericRegisters_(); + this.registers['1'] = new Register(text, linewise); + } + break; + } + // Make sure the unnamed register is set to what just happened + this.unamedRegister.set(text, linewise); + return; + } + + // If we've gotten to this point, we've actually specified a register + var append = isUpperCase(registerName); + if (append) { + register.append(text, linewise); + // The unamed register always has the same value as the last used + // register. + this.unamedRegister.append(text, linewise); + } else { + register.set(text, linewise); + this.unamedRegister.set(text, linewise); + } + }, + setRegisterText: function(name, text, linewise) { + this.getRegister(name).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); + var matchedCommands = []; + var selectedCharacter; + for (var i = 0; i < keyMap.length; i++) { + var command = keyMap[i]; + if (matchKeysPartial(keys, command.keys)) { + 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; + } + // Match commands that take as an argument. + if (command.keys[keys.length - 1] == 'character') { + selectedCharacter = keys[keys.length - 1]; + if(selectedCharacter.length>1){ + switch(selectedCharacter){ + case '': + selectedCharacter='\n'; + break; + case '': + selectedCharacter=' '; + break; + default: + continue; + } + } + } + // Add the command to the list of matched commands. Choose the best + // command later. + matchedCommands.push(command); + } + } + + // Returns the command if it is a full match, or null if not. + function getFullyMatchedCommandOrNull(command) { + 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 (command.keys[keys.length - 1] == 'character') { + inputState.selectedCharacter = selectedCharacter; + } + // Clear the buffer since a full match was found. + inputState.keyBuffer = []; + return command; + } + } + + if (!matchedCommands.length) { + // Clear the buffer since there were no matches. + inputState.keyBuffer = []; + return null; + } else if (matchedCommands.length == 1) { + return getFullyMatchedCommandOrNull(matchedCommands[0]); + } else { + // Find the best match in the list of matchedCommands. + var context = vim.visualMode ? 'visual' : 'normal'; + var bestMatch = matchedCommands[0]; // Default to first in the list. + for (var i = 0; i < matchedCommands.length; i++) { + if (matchedCommands[i].context == context) { + bestMatch = matchedCommands[i]; + break; + } + } + return getFullyMatchedCommandOrNull(bestMatch); + } + }, + processCommand: function(cm, vim, command) { + vim.inputState.repeatOverride = command.repeatOverride; + 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. + vim.inputState = new InputState(); + } + } + 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; + vim.inputState = new InputState(); + vim.lastMotion = null; + if (command.isEdit) { + this.recordLastEdit(vim, inputState, command); + } + 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) ? '/' : '?'; + var originalQuery = getSearchState(cm).getQuery(); + var originalScrollPos = cm.getScrollInfo(); + function handleQuery(query, ignoreCase, smartCase) { + try { + updateSearchQuery(cm, query, ignoreCase, smartCase); + } catch (e) { + showConfirm(cm, 'Invalid regex: ' + query); + return; + } + commandDispatcher.processMotion(cm, vim, { + type: 'motion', + motion: 'findNext', + motionArgs: { forward: true, toJumplist: command.searchArgs.toJumplist } + }); + } + function onPromptClose(query) { + cm.scrollTo(originalScrollPos.left, originalScrollPos.top); + handleQuery(query, true /** ignoreCase */, true /** smartCase */); + } + function onPromptKeyUp(_e, query) { + var parsedQuery; + try { + parsedQuery = updateSearchQuery(cm, query, + true /** ignoreCase */, true /** smartCase */); + } catch (e) { + // Swallow bad regexes for incremental search. + } + if (parsedQuery) { + cm.scrollIntoView(findNext(cm, !forward, parsedQuery), 30); + } else { + clearSearchHighlight(cm); + cm.scrollTo(originalScrollPos.left, originalScrollPos.top); + } + } + function onPromptKeyDown(e, _query, close) { + var keyName = CodeMirror.keyName(e); + if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[') { + updateSearchQuery(cm, originalQuery); + clearSearchHighlight(cm); + cm.scrollTo(originalScrollPos.left, originalScrollPos.top); + + CodeMirror.e_stop(e); + close(); + cm.focus(); + } + } + switch (command.searchArgs.querySrc) { + case 'prompt': + showPrompt(cm, { + onClose: onPromptClose, + prefix: promptPrefix, + desc: searchPromptDesc, + onKeyUp: onPromptKeyUp, + onKeyDown: onPromptKeyDown + }); + 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); + if (isKeyword) { + query = '\\b' + query + '\\b'; + } else { + query = escapeRegex(query); + } + + // cachedCursor is used to save the old position of the cursor + // when * or # causes vim to seek for the nearest word and shift + // the cursor before entering the motion. + vimGlobalState.jumpList.cachedCursor = cm.getCursor(); + cm.setCursor(word.start); + + handleQuery(query, true /** ignoreCase */, false /** smartCase */); + break; + } + }, + processEx: function(cm, vim, command) { + function onPromptClose(input) { + // Give the prompt some time to close so that if processCommand shows + // an error, the elements don't overlap. + exCommandDispatcher.processCommand(cm, input); + } + function onPromptKeyDown(e, _input, close) { + var keyName = CodeMirror.keyName(e); + if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[') { + CodeMirror.e_stop(e); + close(); + cm.focus(); + } + } + if (command.type == 'keyToEx') { + // Handle user defined Ex to Ex mappings + exCommandDispatcher.processCommand(cm, command.exArgs.input); + } else { + if (vim.visualMode) { + showPrompt(cm, { onClose: onPromptClose, prefix: ':', value: '\'<,\'>', + onKeyDown: onPromptKeyDown}); + } else { + showPrompt(cm, { onClose: onPromptClose, prefix: ':', + onKeyDown: onPromptKeyDown}); + } + } + }, + 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 (operator) { + this.recordLastEdit(vim, inputState); + } + if (inputState.repeatOverride !== undefined) { + // If repeatOverride is specified, that takes precedence over the + // input state's repeat. Used by Ex mode and can be user defined. + repeat = inputState.repeatOverride; + } 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; + vim.inputState = new InputState(); + if (motion) { + var motionResult = motions[motion](cm, motionArgs, vim); + vim.lastMotion = motions[motion]; + if (!motionResult) { + return; + } + if (motionArgs.toJumplist) { + var jumpList = vimGlobalState.jumpList; + // if the current motion is # or *, use cachedCursor + var cachedCursor = jumpList.cachedCursor; + if (cachedCursor) { + recordJumpPosition(cm, cachedCursor, motionResult); + delete jumpList.cachedCursor; + } else { + recordJumpPosition(cm, curOriginal, motionResult); + } + } + 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; + + var lastLine = cm.lastLine(); + if (selectionEnd.line > lastLine) { + selectionEnd.line = lastLine; + } + selectionEnd.ch = lineLength(cm, selectionEnd.line); + } else { + selectionEnd.ch = 0; + selectionStart.ch = lineLength(cm, selectionStart.line); + } + } + 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); + } + } + + 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 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); + } + } + }, + recordLastEdit: function(vim, inputState, actionCommand) { + var macroModeState = vimGlobalState.macroModeState; + if (macroModeState.inReplay) { return; } + vim.lastEditInputState = inputState; + vim.lastEditActionCommand = actionCommand; + macroModeState.lastInsertModeChanges.changes = []; + macroModeState.lastInsertModeChanges.expectCursorActivityForChange = false; + } + }; + + /** + * 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 = { + moveToTopLine: function(cm, motionArgs) { + var line = getUserVisibleLines(cm).top + motionArgs.repeat -1; + return { line: line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(line)) }; + }, + moveToMiddleLine: function(cm) { + var range = getUserVisibleLines(cm); + var line = Math.floor((range.top + range.bottom) * 0.5); + return { line: line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(line)) }; + }, + moveToBottomLine: function(cm, motionArgs) { + var line = getUserVisibleLines(cm).bottom - motionArgs.repeat +1; + return { line: line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(line)) }; + }, + 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) { + var state = getSearchState(cm); + var query = state.getQuery(); + if (!query) { + return; + } + var prev = !motionArgs.forward; + // If search is initiated with ? instead of /, negate direction. + prev = (state.isReversed()) ? !prev : prev; + highlightSearchMatches(cm, query); + return findNext(cm, prev/** prev */, query, motionArgs.repeat); + }, + goToMark: function(_cm, motionArgs, vim) { + var mark = vim.marks[motionArgs.selectedCharacter]; + if (mark) { + return mark.find(); + } + return null; + }, + jumpToMark: function(cm, motionArgs, vim) { + var best = cm.getCursor(); + for (var i = 0; i < motionArgs.repeat; i++) { + var cursor = best; + for (var key in vim.marks) { + if (!isLowerCase(key)) { + continue; + } + var mark = vim.marks[key].find(); + var isWrongDirection = (motionArgs.forward) ? + cursorIsBefore(mark, cursor) : cursorIsBefore(cursor, mark); + + if (isWrongDirection) { + continue; + } + if (motionArgs.linewise && (mark.line == cursor.line)) { + continue; + } + + var equal = cursorEqual(cursor, best); + var between = (motionArgs.forward) ? + cusrorIsBetween(cursor, mark, best) : + cusrorIsBetween(best, mark, cursor); + + if (equal || between) { + best = mark; + } + } + } + + if (motionArgs.linewise) { + // Vim places the cursor on the first non-whitespace character of + // the line if there is one, else it places the cursor at the end + // of the line, regardless of whether a mark was found. + best.ch = findFirstNonWhiteSpaceCharacter(cm.getLine(best.line)); + } + return best; + }, + 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 cur = cm.getCursor(); + var endCh = cur.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.moveByDisplayLines: + case this.moveByScroll: + case this.moveToColumn: + case this.moveToEol: + endCh = vim.lastHPos; + break; + default: + vim.lastHPos = endCh; + } + var repeat = motionArgs.repeat+(motionArgs.repeatOffset||0); + var line = motionArgs.forward ? cur.line + repeat : cur.line - repeat; + var first = cm.firstLine(); + var last = cm.lastLine(); + // Vim cancels linewise motions that start on an edge and move beyond + // that edge. It does not cancel motions that do not start on an edge. + if ((line < first && cur.line == first) || + (line > last && cur.line == last)) { + return; + } + if(motionArgs.toFirstChar){ + endCh=findFirstNonWhiteSpaceCharacter(cm.getLine(line)); + vim.lastHPos = endCh; + } + vim.lastHSPos = cm.charCoords({line:line, ch:endCh},'div').left; + return { line: line, ch: endCh }; + }, + moveByDisplayLines: function(cm, motionArgs, vim) { + var cur = cm.getCursor(); + switch (vim.lastMotion) { + case this.moveByDisplayLines: + case this.moveByScroll: + case this.moveByLines: + case this.moveToColumn: + case this.moveToEol: + break; + default: + vim.lastHSPos = cm.charCoords(cur,'div').left; + } + var repeat = motionArgs.repeat; + var res=cm.findPosV(cur,(motionArgs.forward ? repeat : -repeat),'line',vim.lastHSPos); + if (res.hitSide) { + if (motionArgs.forward) { + var lastCharCoords = cm.charCoords(res, 'div'); + var goalCoords = { top: lastCharCoords.top + 8, left: vim.lastHSPos }; + var res = cm.coordsChar(goalCoords, 'div'); + } else { + var resCoords = cm.charCoords({ line: cm.firstLine(), ch: 0}, 'div'); + resCoords.left = vim.lastHSPos; + res = cm.coordsChar(resCoords, 'div'); + } + } + vim.lastHPos = res.ch; + return res; + }, + 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; + }, + moveByParagraph: function(cm, motionArgs) { + var line = cm.getCursor().line; + var repeat = motionArgs.repeat; + var inc = motionArgs.forward ? 1 : -1; + for (var i = 0; i < repeat; i++) { + if ((!motionArgs.forward && line === cm.firstLine() ) || + (motionArgs.forward && line == cm.lastLine())) { + break; + } + line += inc; + while (line !== cm.firstLine() && line != cm.lastLine() && cm.getLine(line)) { + line += inc; + } + } + return { line: line, ch: 0 }; + }, + moveByScroll: function(cm, motionArgs, vim) { + var scrollbox = cm.getScrollInfo(); + var curEnd = null; + var repeat = motionArgs.repeat; + if (!repeat) { + repeat = scrollbox.clientHeight / (2 * cm.defaultTextHeight()); + } + var orig = cm.charCoords(cm.getCursor(), 'local'); + motionArgs.repeat = repeat; + var curEnd = motions.moveByDisplayLines(cm, motionArgs, vim); + if (!curEnd) { + return null; + } + var dest = cm.charCoords(curEnd, 'local'); + cm.scrollTo(null, scrollbox.top + dest.top - orig.top); + 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; + recordLastCharacterSearch(increment, motionArgs); + if(!curEnd)return cm.getCursor(); + curEnd.ch += increment; + return curEnd; + }, + moveToCharacter: function(cm, motionArgs) { + var repeat = motionArgs.repeat; + recordLastCharacterSearch(0, motionArgs); + return moveToCharacter(cm, repeat, motionArgs.forward, + motionArgs.selectedCharacter) || cm.getCursor(); + }, + moveToSymbol: function(cm, motionArgs) { + var repeat = motionArgs.repeat; + return findSymbol(cm, repeat, motionArgs.forward, + motionArgs.selectedCharacter) || cm.getCursor(); + }, + moveToColumn: function(cm, motionArgs, vim) { + var repeat = motionArgs.repeat; + // repeat is equivalent to which column we want to move to! + vim.lastHPos = repeat - 1; + vim.lastHSPos = cm.charCoords(cm.getCursor(),'div').left; + return moveToColumn(cm, repeat); + }, + moveToEol: function(cm, motionArgs, vim) { + var cur = cm.getCursor(); + vim.lastHPos = Infinity; + var retval={ line: cur.line + motionArgs.repeat - 1, ch: Infinity }; + var end=cm.clipPos(retval); + end.ch--; + vim.lastHSPos = cm.charCoords(end,'div').left; + return retval; + }, + 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(); + return { line: cursor.line, + ch: findFirstNonWhiteSpaceCharacter(cm.getLine(cursor.line)) }; + }, + moveToMatchedSymbol: function(cm) { + var cursor = cm.getCursor(); + var line = cursor.line; + var ch = cursor.ch; + var lineText = cm.getLine(line); + var symbol; + var startContext = cm.getTokenAt(cursor).type; + var startCtxLevel = getContextLevel(startContext); + do { + symbol = lineText.charAt(ch++); + if (symbol && isMatchableSymbol(symbol)) { + var endContext = cm.getTokenAt({line:line, ch:ch}).type; + var endCtxLevel = getContextLevel(endContext); + if (startCtxLevel >= endCtxLevel) { + break; + } + } + } while (symbol); + if (symbol) { + return findMatchedSymbol(cm, {line:line, ch:ch-1}, symbol); + } else { + return cursor; + } + }, + moveToStartOfLine: function(cm) { + var cursor = cm.getCursor(); + return { line: cursor.line, ch: 0 }; + }, + moveToLineOrEdgeOfDocument: function(cm, motionArgs) { + var lineNum = motionArgs.forward ? cm.lastLine() : cm.firstLine(); + if (motionArgs.repeatIsExplicit) { + lineNum = motionArgs.repeat - cm.getOption('firstLineNumber'); + } + 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]; + }, + repeatLastCharacterSearch: function(cm, motionArgs) { + var lastSearch = vimGlobalState.lastChararacterSearch; + var repeat = motionArgs.repeat; + var forward = motionArgs.forward === lastSearch.forward; + var increment = (lastSearch.increment ? 1 : 0) * (forward ? -1 : 1); + cm.moveH(-increment, 'char'); + motionArgs.inclusive = forward ? true : false; + var curEnd = moveToCharacter(cm, repeat, forward, lastSearch.selectedCharacter); + if (!curEnd) { + cm.moveH(increment, 'char'); + return cm.getCursor(); + } + curEnd.ch += increment; + return curEnd; + } + }; + + var operators = { + change: function(cm, operatorArgs, _vim, curStart, curEnd) { + vimGlobalState.registerController.pushText( + operatorArgs.registerName, 'change', cm.getRange(curStart, curEnd), + operatorArgs.linewise); + if (operatorArgs.linewise) { + // Push the next line back down, if there is a next line. + var replacement = curEnd.line > cm.lastLine() ? '' : '\n'; + cm.replaceRange(replacement, curStart, curEnd); + cm.indentLine(curStart.line, 'smart'); + // null ch so setCursor moves to end of line. + curStart.ch = null; + } else { + // Exclude trailing whitespace if the range is not all whitespace. + var text = cm.getRange(curStart, curEnd); + if (!isWhiteSpaceString(text)) { + var match = (/\s+$/).exec(text); + if (match) { + curEnd = offsetCursor(curEnd, 0, - match[0].length); + } + } + cm.replaceRange('', curStart, curEnd); + } + actions.enterInsertMode(cm, {}, cm.state.vim); + cm.setCursor(curStart); + }, + // delete is a javascript keyword. + 'delete': function(cm, operatorArgs, _vim, curStart, curEnd) { + // If the ending line is past the last line, inclusive, instead of + // including the trailing \n, include the \n before the starting line + if (operatorArgs.linewise && + curEnd.line > cm.lastLine() && curStart.line > cm.firstLine()) { + curStart.line--; + curStart.ch = lineLength(cm, curStart.line); + } + vimGlobalState.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); + if (!operatorArgs.shouldMoveCursor) { + cm.setCursor(curOriginal); + } + }, + yank: function(cm, operatorArgs, _vim, curStart, curEnd, curOriginal) { + vimGlobalState.registerController.pushText( + operatorArgs.registerName, 'yank', + cm.getRange(curStart, curEnd), operatorArgs.linewise); + cm.setCursor(curOriginal); + } + }; + + var actions = { + jumpListWalk: function(cm, actionArgs, vim) { + if (vim.visualMode) { + return; + } + var repeat = actionArgs.repeat; + var forward = actionArgs.forward; + var jumpList = vimGlobalState.jumpList; + + var mark = jumpList.move(cm, forward ? repeat : -repeat); + var markPos = mark ? mark.find() : undefined; + markPos = markPos ? markPos : cm.getCursor(); + cm.setCursor(markPos); + }, + scrollToCursor: function(cm, actionArgs) { + var lineNum = cm.getCursor().line; + var charCoords = cm.charCoords({line: lineNum, ch: 0}, 'local'); + var height = cm.getScrollInfo().clientHeight; + var y = charCoords.top; + var lineHeight = charCoords.bottom - y; + switch (actionArgs.position) { + case 'center': y = y - (height / 2) + lineHeight; + break; + case 'bottom': y = y - height + lineHeight*1.4; + break; + case 'top': y = y + lineHeight*0.4; + break; + } + cm.scrollTo(null, y); + }, + replayMacro: function(cm, actionArgs) { + var registerName = actionArgs.selectedCharacter; + var repeat = actionArgs.repeat; + var macroModeState = vimGlobalState.macroModeState; + if (registerName == '@') { + registerName = macroModeState.latestRegister; + } + var keyBuffer = parseRegisterToKeyBuffer(macroModeState, registerName); + while(repeat--){ + executeMacroKeyBuffer(cm, macroModeState, keyBuffer); + } + }, + exitMacroRecordMode: function() { + var macroModeState = vimGlobalState.macroModeState; + macroModeState.toggle(); + parseKeyBufferToRegister(macroModeState.latestRegister, + macroModeState.macroKeyBuffer); + }, + enterMacroRecordMode: function(cm, actionArgs) { + var macroModeState = vimGlobalState.macroModeState; + var registerName = actionArgs.selectedCharacter; + macroModeState.toggle(cm, registerName); + emptyMacroKeyBuffer(macroModeState); + }, + enterInsertMode: function(cm, actionArgs, vim) { + if (cm.getOption('readOnly')) { return; } + vim.insertMode = true; + vim.insertModeRepeat = actionArgs && actionArgs.repeat || 1; + 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)); + } else if (insertAt == 'firstNonBlank') { + cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm)); + } + cm.setOption('keyMap', 'vim-insert'); + if (actionArgs && actionArgs.replace) { + // Handle Replace-mode as a special case of insert mode. + cm.toggleOverwrite(true); + cm.setOption('keyMap', 'vim-replace'); + CodeMirror.signal(cm, "vim-mode-change", {mode: "replace"}); + } else { + cm.setOption('keyMap', 'vim-insert'); + CodeMirror.signal(cm, "vim-mode-change", {mode: "insert"}); + } + if (!vimGlobalState.macroModeState.inReplay) { + // Only record if not replaying. + cm.on('change', onChange); + cm.on('cursorActivity', onCursorActivity); + CodeMirror.on(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown); + } + }, + 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) { + cm.on('mousedown', exitVisualMode); + 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); + } + CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : ""}); + } else { + curStart = cm.getCursor('anchor'); + curEnd = cm.getCursor('head'); + 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.ch = cursorIsBefore(curStart, curEnd) ? 0 : + lineLength(cm, curStart.line); + curEnd.ch = cursorIsBefore(curStart, curEnd) ? + lineLength(cm, curEnd.line) : 0; + cm.setSelection(curStart, curEnd); + CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: "linewise"}); + } else if (vim.visualLine && !actionArgs.linewise) { + // v pressed in linewise visual mode. Switch to characterwise visual + // mode instead of exiting visual mode. + vim.visualLine = false; + CodeMirror.signal(cm, "vim-mode-change", {mode: "visual"}); + } else { + exitVisualMode(cm); + } + } + 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, vim) { + vim.insertMode = true; + var insertAt = cm.getCursor(); + if (insertAt.line === cm.firstLine() && !actionArgs.after) { + // Special case for inserting newline before start of document. + cm.replaceRange('\n', { line: cm.firstLine(), ch: 0 }); + cm.setCursor(cm.firstLine(), 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, { repeat: actionArgs.repeat }, vim); + }, + paste: function(cm, actionArgs) { + var cur = cm.getCursor(); + var register = vimGlobalState.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) { + cm.operation(function() { + repeatFn(cm, CodeMirror.commands.undo, actionArgs.repeat)(); + cm.setCursor(cm.getCursor('anchor')); + }); + }, + 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, vim) { + var replaceWith = actionArgs.selectedCharacter; + var curStart = cm.getCursor(); + var replaceTo; + var curEnd; + if(vim.visualMode){ + curStart=cm.getCursor('start'); + curEnd=cm.getCursor('end'); + // workaround to catch the character under the cursor + // existing workaround doesn't cover actions + curEnd=cm.clipPos({line: curEnd.line, ch: curEnd.ch+1}); + }else{ + var line = cm.getLine(curStart.line); + replaceTo = curStart.ch + actionArgs.repeat; + if (replaceTo > line.length) { + replaceTo=line.length; + } + curEnd = { line: curStart.line, ch: replaceTo }; + } + if(replaceWith=='\n'){ + if(!vim.visualMode) cm.replaceRange('', curStart, curEnd); + // special case, where vim help says to replace by just one line-break + (CodeMirror.commands.newlineAndIndentContinueComment || CodeMirror.commands.newlineAndIndent)(cm); + }else { + var replaceWithStr=cm.getRange(curStart, curEnd); + //replace all characters in range by selected, but keep linebreaks + replaceWithStr=replaceWithStr.replace(/[^\n]/g,replaceWith); + cm.replaceRange(replaceWithStr, curStart, curEnd); + if(vim.visualMode){ + cm.setCursor(curStart); + exitVisualMode(cm); + }else{ + cm.setCursor(offsetCursor(curEnd, 0, -1)); + } + } + }, + incrementNumberToken: function(cm, actionArgs) { + var cur = cm.getCursor(); + var lineStr = cm.getLine(cur.line); + var re = /-?\d+/g; + var match; + var start; + var end; + var numberStr; + var token; + while ((match = re.exec(lineStr)) !== null) { + token = match[0]; + start = match.index; + end = start + token.length; + if(cur.ch < end)break; + } + if(!actionArgs.backtrack && (end <= cur.ch))return; + if (token) { + var increment = actionArgs.increase ? 1 : -1; + var number = parseInt(token) + (increment * actionArgs.repeat); + var from = {ch:start, line:cur.line}; + var to = {ch:end, line:cur.line}; + numberStr = number.toString(); + cm.replaceRange(numberStr, from, to); + } else { + return; + } + cm.setCursor({line: cur.line, ch: start + numberStr.length - 1}); + }, + repeatLastEdit: function(cm, actionArgs, vim) { + var lastEditInputState = vim.lastEditInputState; + if (!lastEditInputState) { return; } + var repeat = actionArgs.repeat; + if (repeat && actionArgs.repeatIsExplicit) { + vim.lastEditInputState.repeatOverride = repeat; + } else { + repeat = vim.lastEditInputState.repeatOverride || repeat; + } + repeatLastEdit(cm, vim, repeat, false /** repeatForInsert */); + } + }; + + var textObjects = { + // TODO: lots of possible exceptions that can be thrown here. Try da( + // outside of a () block. + // TODO: implement text objects for the reverse like }. Should just be + // an additional mapping after moving to the defaultKeyMap. + 'w': function(cm, inclusive) { + return expandWordUnderCursor(cm, inclusive, true /** forward */, + false /** bigWord */); + }, + 'W': function(cm, inclusive) { + return expandWordUnderCursor(cm, inclusive, + true /** forward */, true /** bigWord */); + }, + '{': function(cm, inclusive) { + return selectCompanionObject(cm, '}', inclusive); + }, + '(': function(cm, inclusive) { + return selectCompanionObject(cm, ')', inclusive); + }, + '[': function(cm, inclusive) { + return selectCompanionObject(cm, ']', inclusive); + }, + '\'': function(cm, inclusive) { + return findBeginningAndEnd(cm, "'", inclusive); + }, + '"': function(cm, inclusive) { + return findBeginningAndEnd(cm, '"', inclusive); + } + }; + + /* + * Below are miscellaneous utility functions used by vim.js + */ + + /** + * Clips cursor to ensure that line is within the buffer's range + * If includeLineBreak is true, then allow cur.ch == lineLength. + */ + function clipCursorToContent(cm, cur, includeLineBreak) { + var line = Math.min(Math.max(cm.firstLine(), cur.line), cm.lastLine() ); + 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 }; + } + 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 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 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; + } + if (cur1.line == cur2.line && cur1.ch < cur2.ch) { + return true; + } + return false; + } + function cusrorIsBetween(cur1, cur2, cur3) { + // returns true if cur2 is between cur1 and cur3. + var cur1before2 = cursorIsBefore(cur1, cur2); + var cur2before3 = cursorIsBefore(cur2, cur3); + return cur1before2 && cur2before3; + } + 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(); + } + return s.replace(/^\s+|\s+$/g, ''); + } + function escapeRegex(s) { + return s.replace(/([.?*+$\[\]\/\\(){}|\-])/g, '\\$1'); + } + + function exitVisualMode(cm) { + cm.off('mousedown', exitVisualMode); + var vim = cm.state.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)); + } + CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"}); + } + + // 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); + // Only clip if the selection ends with trailing newline + whitespace + if (/\n\s*$/.test(selection)) { + var lines = selection.split('\n'); + // We know this is all whitepsace. + lines.pop(); + + // Cases: + // 1. Last word is an empty line - do not clip the trailing '\n' + // 2. Last word is not an empty line - clip the trailing '\n' + var line; + // Find the line containing the last word, and clip all whitespace up + // to it. + for (var line = lines.pop(); lines.length > 0 && line && isWhiteSpaceString(line); line = lines.pop()) { + curEnd.line--; + curEnd.ch = 0; + } + // If the last word is not an empty line, clip an additional newline + if (line) { + curEnd.line--; + curEnd.ch = lineLength(cm, curEnd.line); + } else { + curEnd.ch = 0; + } + } + } + + // Expand the selection to line ends. + function expandSelectionToLine(_cm, curStart, curEnd) { + curStart.ch = 0; + curEnd.ch = 0; + curEnd.line++; + } + + function findFirstNonWhiteSpaceCharacter(text) { + if (!text) { + return 0; + } + var firstNonWS = text.search(/\S/); + return firstNonWS == -1 ? text.length : firstNonWS; + } + + function expandWordUnderCursor(cm, inclusive, _forward, bigWord, noSymbol) { + var cur = cm.getCursor(); + var line = cm.getLine(cur.line); + var idx = cur.ch; + + // Seek to first word or non-whitespace character, depending on if + // noSymbol is true. + var textAfterIdx = line.substring(idx); + var firstMatchedChar; + if (noSymbol) { + firstMatchedChar = textAfterIdx.search(/\w/); + } else { + firstMatchedChar = textAfterIdx.search(/\S/); + } + if (firstMatchedChar == -1) { + return null; + } + idx += firstMatchedChar; + textAfterIdx = line.substring(idx); + var textBeforeIdx = line.substring(0, idx); + + var matchRegex; + // Greedy matchers for the "word" we are trying to expand. + if (bigWord) { + matchRegex = /^\S+/; + } else { + if ((/\w/).test(line.charAt(idx))) { + matchRegex = /^\w+/; + } else { + matchRegex = /^[^\w\s]+/; + } + } + + var wordAfterRegex = matchRegex.exec(textAfterIdx); + var wordStart = idx; + var wordEnd = idx + wordAfterRegex[0].length; + // TODO: Find a better way to do this. It will be slow on very long lines. + var revTextBeforeIdx = reverse(textBeforeIdx); + var wordBeforeRegex = matchRegex.exec(revTextBeforeIdx); + if (wordBeforeRegex) { + wordStart -= wordBeforeRegex[0].length; + } + + if (inclusive) { + // If present, trim all whitespace after word. + // Otherwise, trim all whitespace before word. + var textAfterWordEnd = line.substring(wordEnd); + var whitespacesAfterWord = textAfterWordEnd.match(/^\s*/)[0].length; + if (whitespacesAfterWord > 0) { + wordEnd += whitespacesAfterWord; + } else { + var revTrim = revTextBeforeIdx.length - wordStart; + var textBeforeWordStart = revTextBeforeIdx.substring(revTrim); + var whitespacesBeforeWord = textBeforeWordStart.match(/^\s*/)[0].length; + wordStart -= whitespacesBeforeWord; + } + } + + return { start: { line: cur.line, ch: wordStart }, + end: { line: cur.line, ch: wordEnd }}; + } + + function recordJumpPosition(cm, oldCur, newCur) { + if(!cursorEqual(oldCur, newCur)) { + vimGlobalState.jumpList.add(cm, oldCur, newCur); + } + } + + function recordLastCharacterSearch(increment, args) { + vimGlobalState.lastChararacterSearch.increment = increment; + vimGlobalState.lastChararacterSearch.forward = args.forward; + vimGlobalState.lastChararacterSearch.selectedCharacter = args.selectedCharacter; + } + + var symbolToMode = { + '(': 'bracket', ')': 'bracket', '{': 'bracket', '}': 'bracket', + '[': 'section', ']': 'section', + '*': 'comment', '/': 'comment', + 'm': 'method', 'M': 'method', + '#': 'preprocess' + }; + var findSymbolModes = { + bracket: { + isComplete: function(state) { + if (state.nextCh === state.symb) { + state.depth++; + if(state.depth >= 1)return true; + } else if (state.nextCh === state.reverseSymb) { + state.depth--; + } + return false; + } + }, + section: { + init: function(state) { + state.curMoveThrough = true; + state.symb = (state.forward ? ']' : '[') === state.symb ? '{' : '}'; + }, + isComplete: function(state) { + return state.index === 0 && state.nextCh === state.symb; + } + }, + comment: { + isComplete: function(state) { + var found = state.lastCh === '*' && state.nextCh === '/'; + state.lastCh = state.nextCh; + return found; + } + }, + // TODO: The original Vim implementation only operates on level 1 and 2. + // The current implementation doesn't check for code block level and + // therefore it operates on any levels. + method: { + init: function(state) { + state.symb = (state.symb === 'm' ? '{' : '}'); + state.reverseSymb = state.symb === '{' ? '}' : '{'; + }, + isComplete: function(state) { + if(state.nextCh === state.symb)return true; + return false; + } + }, + preprocess: { + init: function(state) { + state.index = 0; + }, + isComplete: function(state) { + if (state.nextCh === '#') { + var token = state.lineText.match(/#(\w+)/)[1]; + if (token === 'endif') { + if (state.forward && state.depth === 0) { + return true; + } + state.depth++; + } else if (token === 'if') { + if (!state.forward && state.depth === 0) { + return true; + } + state.depth--; + } + if(token === 'else' && state.depth === 0)return true; + } + return false; + } + } + }; + function findSymbol(cm, repeat, forward, symb) { + var cur = cm.getCursor(); + var increment = forward ? 1 : -1; + var endLine = forward ? cm.lineCount() : -1; + var curCh = cur.ch; + var line = cur.line; + var lineText = cm.getLine(line); + var state = { + lineText: lineText, + nextCh: lineText.charAt(curCh), + lastCh: null, + index: curCh, + symb: symb, + reverseSymb: (forward ? { ')': '(', '}': '{' } : { '(': ')', '{': '}' })[symb], + forward: forward, + depth: 0, + curMoveThrough: false + }; + var mode = symbolToMode[symb]; + if(!mode)return cur; + var init = findSymbolModes[mode].init; + var isComplete = findSymbolModes[mode].isComplete; + if(init)init(state); + while (line !== endLine && repeat) { + state.index += increment; + state.nextCh = state.lineText.charAt(state.index); + if (!state.nextCh) { + line += increment; + state.lineText = cm.getLine(line) || ''; + if (increment > 0) { + state.index = 0; + } else { + var lineLen = state.lineText.length; + state.index = (lineLen > 0) ? (lineLen-1) : 0; + } + state.nextCh = state.lineText.charAt(state.index); + } + if (isComplete(state)) { + cur.line = line; + cur.ch = state.index; + repeat--; + } + } + if (state.nextCh || state.curMoveThrough) { + return { line: line, ch: state.index }; + } + return cur; + } + + /* + * 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. + * @param {boolean} emptyLineIsWord True if empty lines should be treated + * as words. + * @return {Object{from:number, to:number, line: number}} The boundaries of + * the word, or null if there are no more words. + */ + function findWord(cm, cur, forward, bigWord, emptyLineIsWord) { + var lineNum = cur.line; + var pos = cur.ch; + var line = cm.getLine(lineNum); + var dir = forward ? 1 : -1; + var regexps = bigWord ? bigWordRegexp : wordRegexp; + + if (emptyLineIsWord && line == '') { + lineNum += dir; + line = cm.getLine(lineNum); + if (!isLine(cm, lineNum)) { + return null; + } + pos = (forward) ? 0 : line.length; + } + + while (true) { + if (emptyLineIsWord && line == '') { + return { from: 0, to: 0, line: lineNum }; + } + 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 new Error('The impossible happened.'); + } + + /** + * @param {CodeMirror} cm CodeMirror object. + * @param {int} repeat Number of words to move past. + * @param {boolean} forward True to search forward. False to search + * backward. + * @param {boolean} wordEnd True to move to end of word. False to move to + * beginning of word. + * @param {boolean} bigWord True if punctuation count as part of the word. + * False if only alphabet characters count as part of the word. + * @return {Cursor} The position the cursor should move to. + */ + function moveToWord(cm, repeat, forward, wordEnd, bigWord) { + var cur = cm.getCursor(); + var curStart = copyCursor(cur); + var words = []; + if (forward && !wordEnd || !forward && wordEnd) { + repeat++; + } + // For 'e', empty lines are not considered words, go figure. + var emptyLineIsWord = !(forward && wordEnd); + for (var i = 0; i < repeat; i++) { + var word = findWord(cm, cur, forward, bigWord, emptyLineIsWord); + if (!word) { + var eodCh = lineLength(cm, cm.lastLine()); + words.push(forward + ? {line: cm.lastLine(), from: eodCh, to: eodCh} + : {line: 0, from: 0, to: 0}); + break; + } + words.push(word); + cur = {line: word.line, ch: forward ? (word.to - 1) : word.from}; + } + var shortCircuit = words.length != repeat; + var firstWord = words[0]; + var lastWord = words.pop(); + if (forward && !wordEnd) { + // w + if (!shortCircuit && (firstWord.from != curStart.ch || firstWord.line != curStart.line)) { + // We did not start in the middle of a word. Discard the extra word at the end. + lastWord = words.pop(); + } + return {line: lastWord.line, ch: lastWord.from}; + } else if (forward && wordEnd) { + return {line: lastWord.line, ch: lastWord.to - 1}; + } else if (!forward && wordEnd) { + // ge + if (!shortCircuit && (firstWord.to != curStart.ch || firstWord.line != curStart.line)) { + // We did not start in the middle of a word. Discard the extra word at the end. + lastWord = words.pop(); + } + return {line: lastWord.line, ch: lastWord.to}; + } else { + // b + return {line: lastWord.line, ch: lastWord.from}; + } + } + + 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 null; + } + start = idx; + } + return { line: cm.getCursor().line, ch: idx }; + } + + function moveToColumn(cm, repeat) { + // repeat is always >= 1, so repeat - 1 always corresponds + // to the column we want to go to. + var line = cm.getCursor().line; + return clipCursorToContent(cm, { line: line, ch: repeat - 1 }); + } + + function updateMark(cm, vim, markName, pos) { + if (!inArray(markName, validMarks)) { + return; + } + if (vim.marks[markName]) { + vim.marks[markName].clear(); + } + vim.marks[markName] = cm.setBookmark(pos); + } + + function charIdxInLine(start, line, character, forward, includeChar) { + // Search for char in line. + // motion_options: {forward, includeChar} + // If includeChar = true, include it too. + // If forward = true, search forward, else search backwards. + // If char is not found on this line, do nothing + var idx; + if (forward) { + idx = line.indexOf(character, start + 1); + if (idx != -1 && !includeChar) { + idx -= 1; + } + } else { + idx = line.lastIndexOf(character, start - 1); + if (idx != -1 && !includeChar) { + idx += 1; + } + } + return idx; + } + + function getContextLevel(ctx) { + return (ctx === 'string' || ctx === 'comment') ? 1 : 0; + } + + function findMatchedSymbol(cm, cur, symb) { + var line = cur.line; + var ch = cur.ch; + symb = symb ? symb : cm.getLine(line).charAt(ch); + + var symbContext = cm.getTokenAt({line:line, ch:ch+1}).type; + var symbCtxLevel = getContextLevel(symbContext); + + var reverseSymb = ({ + '(': ')', ')': '(', + '[': ']', ']': '[', + '{': '}', '}': '{'})[symb]; + + // Couldn't find a matching symbol, abort + if (!reverseSymb) { + return cur; + } + + // set our increment to move forward (+1) or backwards (-1) + // depending on which bracket we're matching + var increment = ({'(': 1, '{': 1, '[': 1})[symb] || -1; + var endLine = increment === 1 ? cm.lineCount() : -1; + var depth = 1, nextCh = symb, index = 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 (line !== endLine && depth > 0) { + index += increment; + nextCh = lineText.charAt(index); + if (!nextCh) { + line += increment; + lineText = cm.getLine(line) || ''; + if (increment > 0) { + index = 0; + } else { + var lineLen = lineText.length; + index = (lineLen > 0) ? (lineLen-1) : 0; + } + nextCh = lineText.charAt(index); + } + var revSymbContext = cm.getTokenAt({line:line, ch:index+1}).type; + var revSymbCtxLevel = getContextLevel(revSymbContext); + if (symbCtxLevel >= revSymbCtxLevel) { + if (nextCh === symb) { + depth++; + } else if (nextCh === reverseSymb) { + depth--; + } + } + } + + if (nextCh) { + return { line: line, ch: index }; + } + return cur; + } + + function selectCompanionObject(cm, revSymb, inclusive) { + var cur = cm.getCursor(); + + var end = findMatchedSymbol(cm, cur, revSymb); + var start = findMatchedSymbol(cm, end); + start.ch += inclusive ? 1 : 0; + end.ch += inclusive ? 0 : 1; + + return { start: start, end: end }; + } + + // Takes in a symbol and a cursor and tries to simulate text objects that + // have identical opening and closing symbols + // TODO support across multiple lines + function findBeginningAndEnd(cm, symb, inclusive) { + var cur = cm.getCursor(); + var line = cm.getLine(cur.line); + var chars = line.split(''); + var start, end, i, len; + var firstIndex = chars.indexOf(symb); + + // the decision tree is to always look backwards for the beginning first, + // but if the cursor is in front of the first instance of the symb, + // then move the cursor forward + if (cur.ch < firstIndex) { + cur.ch = firstIndex; + // Why is this line even here??? + // cm.setCursor(cur.line, firstIndex+1); + } + // otherwise if the cursor is currently on the closing symbol + else if (firstIndex < cur.ch && chars[cur.ch] == symb) { + end = cur.ch; // assign end to the current cursor + --cur.ch; // make sure to look backwards + } + + // if we're currently on the symbol, we've got a start + if (chars[cur.ch] == symb && !end) { + start = cur.ch + 1; // assign start to ahead of the cursor + } else { + // go backwards to find the start + for (i = cur.ch; i > -1 && !start; i--) { + if (chars[i] == symb) { + start = i + 1; + } + } + } + + // look forwards for the end symbol + if (start && !end) { + for (i = start, len = chars.length; i < len && !end; i++) { + if (chars[i] == symb) { + end = i; + } + } + } + + // nothing found + if (!start || !end) { + return { start: cur, end: cur }; + } + + // include the symbols + if (inclusive) { + --start; ++end; + } + + return { + start: { line: cur.line, ch: start }, + end: { line: cur.line, ch: end } + }; + } + + // Search functions + function SearchState() {} + SearchState.prototype = { + getQuery: function() { + return vimGlobalState.query; + }, + setQuery: function(query) { + vimGlobalState.query = query; + }, + getOverlay: function() { + return this.searchOverlay; + }, + setOverlay: function(overlay) { + this.searchOverlay = overlay; + }, + isReversed: function() { + return vimGlobalState.isReversed; + }, + setReversed: function(reversed) { + vimGlobalState.isReversed = reversed; + } + }; + function getSearchState(cm) { + var vim = cm.state.vim; + return vim.searchState_ || (vim.searchState_ = new SearchState()); + } + function dialog(cm, template, shortText, onClose, options) { + if (cm.openDialog) { + cm.openDialog(template, onClose, { bottom: true, value: options.value, + onKeyDown: options.onKeyDown, onKeyUp: options.onKeyUp }); + } + else { + onClose(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(query, ignoreCase, smartCase) { + // Check if the query is already a regex. + if (query instanceof RegExp) { return query; } + // 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); + } + var regexp = new RegExp(regexPart, + (ignoreCase || forceIgnoreCase) ? 'i' : undefined); + return regexp; + } + function showConfirm(cm, text) { + if (cm.openNotification) { + cm.openNotification('' + text + '', + {bottom: true, duration: 5000}); + } 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, options) { + var shortText = (options.prefix || '') + ' ' + (options.desc || ''); + var prompt = makePrompt(options.prefix, options.desc); + dialog(cm, prompt, shortText, options.onClose, options); + } + 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; + } + // Returns true if the query is valid. + function updateSearchQuery(cm, rawQuery, ignoreCase, smartCase) { + if (!rawQuery) { + return; + } + var state = getSearchState(cm); + var query = parseQuery(rawQuery, !!ignoreCase, !!smartCase); + if (!query) { + return; + } + highlightSearchMatches(cm, query); + if (regexEqual(query, state.getQuery())) { + return query; + } + state.setQuery(query); + return query; + } + function searchOverlay(query) { + if (query.source.charAt(0) == '^') { + var matchSol = true; + } + return { + token: function(stream) { + if (matchSol && !stream.sol()) { + stream.skipToEnd(); + return; + } + var match = stream.match(query, false); + if (match) { + if (match[0].length == 0) { + // Matched empty string, skip to next. + stream.next(); + return 'searching'; + } + if (!stream.sol()) { + // Backtrack 1 to match \b + stream.backUp(1); + if (!query.exec(stream.next() + match[0])) { + stream.next(); + return null; + } + } + stream.match(query); + return 'searching'; + } + while (!stream.eol()) { + stream.next(); + if (stream.match(query, false)) break; + } + }, + query: query + }; + } + function highlightSearchMatches(cm, query) { + var overlay = getSearchState(cm).getOverlay(); + if (!overlay || query != overlay.query) { + if (overlay) { + cm.removeOverlay(overlay); + } + overlay = searchOverlay(query); + cm.addOverlay(overlay); + getSearchState(cm).setOverlay(overlay); + } + } + function findNext(cm, prev, query, repeat) { + if (repeat === undefined) { repeat = 1; } + return cm.operation(function() { + var pos = cm.getCursor(); + var cursor = cm.getSearchCursor(query, pos); + for (var i = 0; i < repeat; i++) { + var found = cursor.find(prev); + if (i == 0 && found && cursorEqual(cursor.from(), pos)) { found = cursor.find(prev); } + if (!found) { + // SearchCursor may have returned null because it hit EOF, wrap + // around and try again. + cursor = cm.getSearchCursor(query, + (prev) ? { line: cm.lastLine() } : {line: cm.firstLine(), ch: 0} ); + if (!cursor.find(prev)) { + return; + } + } + } + return cursor.from(); + }); + } + function clearSearchHighlight(cm) { + cm.removeOverlay(getSearchState(cm).getOverlay()); + getSearchState(cm).setOverlay(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; + } + } + } + function getUserVisibleLines(cm) { + var scrollInfo = cm.getScrollInfo(); + var occludeToleranceTop = 6; + var occludeToleranceBottom = 10; + var from = cm.coordsChar({left:0, top: occludeToleranceTop + scrollInfo.top}, 'local'); + var bottomY = scrollInfo.clientHeight - occludeToleranceBottom + scrollInfo.top; + var to = cm.coordsChar({left:0, top: bottomY}, 'local'); + return {top: from.line, bottom: to.line}; + } + + // 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: 'sort', shortName: 'sor', type: 'builtIn'}, + { name: 'substitute', shortName: 's', type: 'builtIn'}, + { name: 'nohlsearch', shortName: 'noh', type: 'builtIn'}, + { name: 'delmarks', shortName: 'delm', type: 'builtin'} + ]; + Vim.ExCommandDispatcher = function() { + this.buildCommandMap_(); + }; + Vim.ExCommandDispatcher.prototype = { + processCommand: function(cm, input) { + var vim = cm.state.vim; + if (vim.visualMode) { + exitVisualMode(cm); + } + 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++) { + CodeMirror.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; + } + try { + exCommands[commandName](cm, params); + } catch(e) { + showConfirm(cm, e); + } + }, + parseInput_: function(cm, inputStream, result) { + inputStream.eatWhile(':'); + // Parse range. + if (inputStream.eat('%')) { + result.line = cm.firstLine(); + result.lineEnd = cm.lastLine(); + } else { + result.line = this.parseLineSpec_(cm, inputStream); + if (result.line !== undefined && inputStream.eat(',')) { + result.lineEnd = this.parseLineSpec_(cm, inputStream); + } + } + + // Parse command name. + var commandMatch = inputStream.match(/^(\w+)/); + if (commandMatch) { + result.commandName = commandMatch[1]; + } else { + result.commandName = inputStream.match(/.*/)[0]; + } + + 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.lastLine(); + case '\'': + var mark = cm.state.vim.marks[inputStream.next()]; + if (mark && mark.find()) { + return mark.find().line; + } + throw new Error('Mark not set'); + default: + inputStream.backUp(1); + return undefined; + } + }, + 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 = trim(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 != ':' && 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) + }); + } + } + } + }; + + // Converts a key string sequence of the form abd into Vim's + // keymap representation. + function parseKeyString(str) { + var key, match; + var keys = []; + while (str) { + match = (/<\w+-.+?>|<\w+>|./).exec(str); + if(match === null)break; + key = match[0]; + str = str.substring(match.index + key.length); + keys.push(key); + } + return keys; + } + + var exCommands = { + map: function(cm, params) { + var mapArgs = params.args; + 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.processCommand(cm, cm.state.vim, { + type: 'motion', + motion: 'moveToLineOrEdgeOfDocument', + motionArgs: { forward: false, explicitRepeat: true, + linewise: true }, + repeatOverride: params.line+1}); + }, + sort: function(cm, params) { + var reverse, ignoreCase, unique, number; + function parseArgs() { + if (params.argString) { + var args = new CodeMirror.StringStream(params.argString); + if (args.eat('!')) { reverse = true; } + if (args.eol()) { return; } + if (!args.eatSpace()) { throw new Error('invalid arguments ' + args.match(/.*/)[0]); } + var opts = args.match(/[a-z]+/); + if (opts) { + opts = opts[0]; + ignoreCase = opts.indexOf('i') != -1; + unique = opts.indexOf('u') != -1; + var decimal = opts.indexOf('d') != -1 && 1; + var hex = opts.indexOf('x') != -1 && 1; + var octal = opts.indexOf('o') != -1 && 1; + if (decimal + hex + octal > 1) { throw new Error('invalid arguments'); } + number = decimal && 'decimal' || hex && 'hex' || octal && 'octal'; + } + if (args.eatSpace() && args.match(/\/.*\//)) { throw new Error('patterns not supported'); } + } + } + parseArgs(); + var lineStart = params.line || cm.firstLine(); + var lineEnd = params.lineEnd || params.line || cm.lastLine(); + if (lineStart == lineEnd) { return; } + var curStart = { line: lineStart, ch: 0 }; + var curEnd = { line: lineEnd, ch: lineLength(cm, lineEnd) }; + var text = cm.getRange(curStart, curEnd).split('\n'); + var numberRegex = (number == 'decimal') ? /(-?)([\d]+)/ : + (number == 'hex') ? /(-?)(?:0x)?([0-9a-f]+)/i : + (number == 'octal') ? /([0-7]+)/ : null; + var radix = (number == 'decimal') ? 10 : (number == 'hex') ? 16 : (number == 'octal') ? 8 : null; + var numPart = [], textPart = []; + if (number) { + for (var i = 0; i < text.length; i++) { + if (numberRegex.exec(text[i])) { + numPart.push(text[i]); + } else { + textPart.push(text[i]); + } + } + } else { + textPart = text; + } + function compareFn(a, b) { + if (reverse) { var tmp; tmp = a; a = b; b = tmp; } + if (ignoreCase) { a = a.toLowerCase(); b = b.toLowerCase(); } + var anum = number && numberRegex.exec(a); + var bnum = number && numberRegex.exec(b); + if (!anum) { return a < b ? -1 : 1; } + anum = parseInt((anum[1] + anum[2]).toLowerCase(), radix); + bnum = parseInt((bnum[1] + bnum[2]).toLowerCase(), radix); + return anum - bnum; + } + numPart.sort(compareFn); + textPart.sort(compareFn); + text = (!reverse) ? textPart.concat(numPart) : numPart.concat(textPart); + if (unique) { // Remove duplicate lines + var textOld = text; + var lastLine; + text = []; + for (var i = 0; i < textOld.length; i++) { + if (textOld[i] != lastLine) { + text.push(textOld[i]); + } + lastLine = textOld[i]; + } + } + cm.replaceRange(text.join('\n'), curStart, curEnd); + }, + substitute: function(cm, params) { + if (!cm.getSearchCursor) { + throw new Error('Search feature not available. Requires searchcursor.js or ' + + 'any other getSearchCursor implementation.'); + } + 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; + var count; + var confirm = false; // Whether to confirm each replace. + if (slashes[1]) { + replacePart = argString.substring(slashes[1] + 1, slashes[2]); + } + if (slashes[2]) { + // After the 3rd slash, we can have flags followed by a space followed + // by count. + var trailing = argString.substring(slashes[2] + 1).split(' '); + flagsPart = trailing[0]; + count = parseInt(trailing[1]); + } + if (flagsPart) { + if (flagsPart.indexOf('c') != -1) { + confirm = true; + flagsPart.replace('c', ''); + } + regexPart = regexPart + '/' + flagsPart; + } + if (regexPart) { + // If regex part is empty, then use the previous query. Otherwise use + // the regex part as the new query. + try { + updateSearchQuery(cm, regexPart, true /** ignoreCase */, + true /** smartCase */); + } catch (e) { + showConfirm(cm, 'Invalid regex: ' + regexPart); + return; + } + } + var state = getSearchState(cm); + var query = state.getQuery(); + var lineStart = (params.line !== undefined) ? params.line : cm.getCursor().line; + var lineEnd = params.lineEnd || lineStart; + if (count) { + lineStart = lineEnd; + lineEnd = lineStart + count - 1; + } + var startPos = clipCursorToContent(cm, { line: lineStart, ch: 0 }); + var cursor = cm.getSearchCursor(query, startPos); + doReplace(cm, confirm, lineStart, lineEnd, cursor, query, replacePart); + }, + 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(); + } + }, + nohlsearch: function(cm) { + clearSearchHighlight(cm); + }, + delmarks: function(cm, params) { + if (!params.argString || !params.argString.trim()) { + showConfirm(cm, 'Argument required'); + return; + } + + var state = cm.state.vim; + var stream = new CodeMirror.StringStream(params.argString.trim()); + while (!stream.eol()) { + stream.eatSpace(); + + // Record the streams position at the beginning of the loop for use + // in error messages. + var count = stream.pos; + + if (!stream.match(/[a-zA-Z]/, false)) { + showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count)); + return; + } + + var sym = stream.next(); + // Check if this symbol is part of a range + if (stream.match('-', true)) { + // This symbol is part of a range. + + // The range must terminate at an alphabetic character. + if (!stream.match(/[a-zA-Z]/, false)) { + showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count)); + return; + } + + var startMark = sym; + var finishMark = stream.next(); + // The range must terminate at an alphabetic character which + // shares the same case as the start of the range. + if (isLowerCase(startMark) && isLowerCase(finishMark) || + isUpperCase(startMark) && isUpperCase(finishMark)) { + var start = startMark.charCodeAt(0); + var finish = finishMark.charCodeAt(0); + if (start >= finish) { + showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count)); + return; + } + + // Because marks are always ASCII values, and we have + // determined that they are the same case, we can use + // their char codes to iterate through the defined range. + for (var j = 0; j <= finish - start; j++) { + var mark = String.fromCharCode(start + j); + delete state.marks[mark]; + } + } else { + showConfirm(cm, 'Invalid argument: ' + startMark + '-'); + return; + } + } else { + // This symbol is a valid mark, and is not part of a range. + delete state.marks[sym]; + } + } + } + }; + + var exCommandDispatcher = new Vim.ExCommandDispatcher(); + + /** + * @param {CodeMirror} cm CodeMirror instance we are in. + * @param {boolean} confirm Whether to confirm each replace. + * @param {Cursor} lineStart Line to start replacing from. + * @param {Cursor} lineEnd Line to stop replacing at. + * @param {RegExp} query Query for performing matches with. + * @param {string} replaceWith Text to replace matches with. May contain $1, + * $2, etc for replacing captured groups using Javascript replace. + */ + function doReplace(cm, confirm, lineStart, lineEnd, searchCursor, query, + replaceWith) { + // Set up all the functions. + cm.state.vim.exMode = true; + var done = false; + var lastPos = searchCursor.from(); + function replaceAll() { + cm.operation(function() { + while (!done) { + replace(); + next(); + } + stop(); + }); + } + function replace() { + var text = cm.getRange(searchCursor.from(), searchCursor.to()); + var newText = text.replace(query, replaceWith); + searchCursor.replace(newText); + } + function next() { + var found = searchCursor.findNext(); + if (!found) { + done = true; + } else if (isInRange(searchCursor.from(), lineStart, lineEnd)) { + cm.scrollIntoView(searchCursor.from(), 30); + cm.setSelection(searchCursor.from(), searchCursor.to()); + lastPos = searchCursor.from(); + done = false; + } else { + done = true; + } + } + function stop(close) { + if (close) { close(); } + cm.focus(); + if (lastPos) { + cm.setCursor(lastPos); + var vim = cm.state.vim; + vim.exMode = false; + vim.lastHPos = vim.lastHSPos = lastPos.ch; + } + } + function onPromptKeyDown(e, _value, close) { + // Swallow all keys. + CodeMirror.e_stop(e); + var keyName = CodeMirror.keyName(e); + switch (keyName) { + case 'Y': + replace(); next(); break; + case 'N': + next(); break; + case 'A': + cm.operation(replaceAll); break; + case 'L': + replace(); + // fall through and exit. + case 'Q': + case 'Esc': + case 'Ctrl-C': + case 'Ctrl-[': + stop(close); + break; + } + if (done) { stop(close); } + } + + // Actually do replace. + next(); + if (done) { + throw new Error('No matches for ' + query.source); + } + if (!confirm) { + replaceAll(); + return; + } + showPrompt(cm, { + prefix: 'replace with ' + replaceWith + ' (y/n/a/q/l)', + onKeyDown: onPromptKeyDown + }); + } + + // 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 cmKeyToVimKey(key, modifier) { + var vimKey = key; + if (isUpperCase(vimKey)) { + // 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 { + vimKey = vimKey.toLowerCase(); + } + } + if (modifier) { + // Vim will parse modifier+key combination as a single key. + vimKey = modifier.charAt(0) + '-' + vimKey; + } + var specialKey = ({Enter:'CR',Backspace:'BS',Delete:'Del'})[vimKey]; + vimKey = specialKey ? specialKey : vimKey; + vimKey = vimKey.length > 1 ? '<'+ vimKey + '>' : vimKey; + return vimKey; + } + + // Closure to bind CodeMirror, key, modifier. + function keyMapper(vimKey) { + return function(cm) { + CodeMirror.Vim.handleKey(cm, vimKey); + }; + } + + var cmToVimKeymap = { + 'nofallthrough': true, + 'disableInput': 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 + "'"; + } + var vimKey = cmKeyToVimKey(keys[i], modifier); + var cmKey = modifier ? modifier + '-' + key : key; + cmToVimKeymap[cmKey] = keyMapper(vimKey); + } + } + bindKeys(upperCaseAlphabet); + bindKeys(upperCaseAlphabet, 'Shift'); + bindKeys(upperCaseAlphabet, 'Ctrl'); + bindKeys(specialSymbols); + bindKeys(specialSymbols, 'Ctrl'); + bindKeys(numbers); + bindKeys(numbers, 'Ctrl'); + bindKeys(specialKeys); + bindKeys(specialKeys, 'Ctrl'); + return cmToVimKeymap; + } + CodeMirror.keyMap.vim = buildVimKeyMap(); + + function exitInsertMode(cm) { + var vim = cm.state.vim; + var inReplay = vimGlobalState.macroModeState.inReplay; + if (!inReplay) { + cm.off('change', onChange); + cm.off('cursorActivity', onCursorActivity); + CodeMirror.off(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown); + } + if (!inReplay && vim.insertModeRepeat > 1) { + // Perform insert mode repeat for commands like 3,a and 3,o. + repeatLastEdit(cm, vim, vim.insertModeRepeat - 1, + true /** repeatForInsert */); + vim.lastEditInputState.repeatOverride = vim.insertModeRepeat; + } + delete vim.insertModeRepeat; + cm.setCursor(cm.getCursor().line, cm.getCursor().ch-1, true); + vim.insertMode = false; + cm.setOption('keyMap', 'vim'); + cm.toggleOverwrite(false); // exit replace mode if we were in it. + CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"}); + } + + 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'] + }; + + CodeMirror.keyMap['vim-replace'] = { + 'Backspace': 'goCharLeft', + fallthrough: ['vim-insert'] + }; + + function parseRegisterToKeyBuffer(macroModeState, registerName) { + var match, key; + var register = vimGlobalState.registerController.getRegister(registerName); + var text = register.toString(); + var macroKeyBuffer = macroModeState.macroKeyBuffer; + emptyMacroKeyBuffer(macroModeState); + do { + match = (/<\w+-.+?>|<\w+>|./).exec(text); + if(match === null)break; + key = match[0]; + text = text.substring(match.index + key.length); + macroKeyBuffer.push(key); + } while (text); + return macroKeyBuffer; + } + + function parseKeyBufferToRegister(registerName, keyBuffer) { + var text = keyBuffer.join(''); + vimGlobalState.registerController.setRegisterText(registerName, text); + } + + function emptyMacroKeyBuffer(macroModeState) { + if(macroModeState.isMacroPlaying)return; + var macroKeyBuffer = macroModeState.macroKeyBuffer; + macroKeyBuffer.length = 0; + } + + function executeMacroKeyBuffer(cm, macroModeState, keyBuffer) { + macroModeState.isMacroPlaying = true; + for (var i = 0, len = keyBuffer.length; i < len; i++) { + CodeMirror.Vim.handleKey(cm, keyBuffer[i]); + }; + macroModeState.isMacroPlaying = false; + } + + function logKey(macroModeState, key) { + if(macroModeState.isMacroPlaying)return; + var macroKeyBuffer = macroModeState.macroKeyBuffer; + macroKeyBuffer.push(key); + } + + /** + * Listens for changes made in insert mode. + * Should only be active in insert mode. + */ + function onChange(_cm, changeObj) { + var macroModeState = vimGlobalState.macroModeState; + var lastChange = macroModeState.lastInsertModeChanges; + while (changeObj) { + lastChange.expectCursorActivityForChange = true; + if (changeObj.origin == '+input' || changeObj.origin == 'paste' + || changeObj.origin === undefined /* only in testing */) { + var text = changeObj.text.join('\n'); + lastChange.changes.push(text); + } + // Change objects may be chained with next. + changeObj = changeObj.next; + } + } + + /** + * Listens for any kind of cursor activity on CodeMirror. + * - For tracking cursor activity in insert mode. + * - Should only be active in insert mode. + */ + function onCursorActivity() { + var macroModeState = vimGlobalState.macroModeState; + var lastChange = macroModeState.lastInsertModeChanges; + if (lastChange.expectCursorActivityForChange) { + lastChange.expectCursorActivityForChange = false; + } else { + // Cursor moved outside the context of an edit. Reset the change. + lastChange.changes = []; + } + } + + /** Wrapper for special keys pressed in insert mode */ + function InsertModeKey(keyName) { + this.keyName = keyName; + } + + /** + * Handles raw key down events from the text area. + * - Should only be active in insert mode. + * - For recording deletes in insert mode. + */ + function onKeyEventTargetKeyDown(e) { + var macroModeState = vimGlobalState.macroModeState; + var lastChange = macroModeState.lastInsertModeChanges; + var keyName = CodeMirror.keyName(e); + function onKeyFound() { + lastChange.changes.push(new InsertModeKey(keyName)); + return true; + } + if (keyName.indexOf('Delete') != -1 || keyName.indexOf('Backspace') != -1) { + CodeMirror.lookupKey(keyName, ['vim-insert'], onKeyFound); + } + } + + /** + * Repeats the last edit, which includes exactly 1 command and at most 1 + * insert. Operator and motion commands are read from lastEditInputState, + * while action commands are read from lastEditActionCommand. + * + * If repeatForInsert is true, then the function was called by + * exitInsertMode to repeat the insert mode changes the user just made. The + * corresponding enterInsertMode call was made with a count. + */ + function repeatLastEdit(cm, vim, repeat, repeatForInsert) { + var macroModeState = vimGlobalState.macroModeState; + macroModeState.inReplay = true; + var isAction = !!vim.lastEditActionCommand; + var cachedInputState = vim.inputState; + function repeatCommand() { + if (isAction) { + commandDispatcher.processAction(cm, vim, vim.lastEditActionCommand); + } else { + commandDispatcher.evalInput(cm, vim); + } + } + function repeatInsert(repeat) { + if (macroModeState.lastInsertModeChanges.changes.length > 0) { + // For some reason, repeat cw in desktop VIM will does not repeat + // insert mode changes. Will conform to that behavior. + repeat = !vim.lastEditActionCommand ? 1 : repeat; + repeatLastInsertModeChanges(cm, repeat, macroModeState); + } + } + vim.inputState = vim.lastEditInputState; + if (isAction && vim.lastEditActionCommand.interlaceInsertRepeat) { + // o and O repeat have to be interlaced with insert repeats so that the + // insertions appear on separate lines instead of the last line. + for (var i = 0; i < repeat; i++) { + repeatCommand(); + repeatInsert(1); + } + } else { + if (!repeatForInsert) { + // Hack to get the cursor to end up at the right place. If I is + // repeated in insert mode repeat, cursor will be 1 insert + // change set left of where it should be. + repeatCommand(); + } + repeatInsert(repeat); + } + vim.inputState = cachedInputState; + if (vim.insertMode && !repeatForInsert) { + // Don't exit insert mode twice. If repeatForInsert is set, then we + // were called by an exitInsertMode call lower on the stack. + exitInsertMode(cm); + } + macroModeState.inReplay = false; + }; + + function repeatLastInsertModeChanges(cm, repeat, macroModeState) { + var lastChange = macroModeState.lastInsertModeChanges; + function keyHandler(binding) { + if (typeof binding == 'string') { + CodeMirror.commands[binding](cm); + } else { + binding(cm); + } + return true; + } + for (var i = 0; i < repeat; i++) { + for (var j = 0; j < lastChange.changes.length; j++) { + var change = lastChange.changes[j]; + if (change instanceof InsertModeKey) { + CodeMirror.lookupKey(change.keyName, ['vim-insert'], keyHandler); + } else { + var cur = cm.getCursor(); + cm.replaceRange(change, cur, cur); + } + } + } + } + + resetVimGlobalState(); + return vimApi; + }; + // Initialize Vim and make it available as an API. + CodeMirror.Vim = Vim(); +} +)(); diff --git a/plugins/tiddlywiki/codemirror/files/tiddlywiki.files b/plugins/tiddlywiki/codemirror/files/tiddlywiki.files index f98c809c0..6c7d19784 100644 --- a/plugins/tiddlywiki/codemirror/files/tiddlywiki.files +++ b/plugins/tiddlywiki/codemirror/files/tiddlywiki.files @@ -14,6 +14,41 @@ "title": "$:/plugins/tiddlywiki/codemirror/codemirror.css", "tags": "[[$:/tags/stylesheet]]" } + },{ + "file": "addon/dialog.css", + "fields": { + "type": "text/css", + "title": "$:/plugins/tiddlywiki/codemirror/addon/dialog.css", + "tags": "[[$:/tags/stylesheet]]" + } + },{ + "file": "addon/dialog.js", + "fields": { + "type": "application/javascript", + "title": "$:/plugins/tiddlywiki/codemirror/addon/dialog.js", + "module-type": "library" + } + },{ + "file": "addon/searchcursor.js", + "fields": { + "type": "application/javascript", + "title": "$:/plugins/tiddlywiki/codemirror/addon/searchcursor.js", + "module-type": "library" + } + },{ + "file": "keymap/vim.js", + "fields": { + "type": "application/javascript", + "title": "$:/plugins/tiddlywiki/codemirror/keymap/vim.js", + "module-type": "library" + } + },{ + "file": "keymap/emacs.js", + "fields": { + "type": "application/javascript", + "title": "$:/plugins/tiddlywiki/codemirror/keymap/emacs.js", + "module-type": "library" + } } ] }