%PDF- %PDF-
Direktori : /home/lightco1/www/lightcolab.com/media/editors/codemirror/lib/ |
Current File : /home/lightco1/www/lightcolab.com/media/editors/codemirror/lib/addons.js |
// CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: http://codemirror.net/LICENSE (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS mod(require("../../lib/codemirror")); else if (typeof define == "function" && define.amd) // AMD define(["../../lib/codemirror"], mod); else // Plain browser env mod(CodeMirror); })(function(CodeMirror) { "use strict"; CodeMirror.defineOption("fullScreen", false, function(cm, val, old) { if (old == CodeMirror.Init) old = false; if (!old == !val) return; if (val) setFullscreen(cm); else setNormal(cm); }); function setFullscreen(cm) { var wrap = cm.getWrapperElement(); cm.state.fullScreenRestore = {scrollTop: window.pageYOffset, scrollLeft: window.pageXOffset, width: wrap.style.width, height: wrap.style.height}; wrap.style.width = ""; wrap.style.height = "auto"; wrap.className += " CodeMirror-fullscreen"; document.documentElement.style.overflow = "hidden"; cm.refresh(); } function setNormal(cm) { var wrap = cm.getWrapperElement(); wrap.className = wrap.className.replace(/\s*CodeMirror-fullscreen\b/, ""); document.documentElement.style.overflow = ""; var info = cm.state.fullScreenRestore; wrap.style.width = info.width; wrap.style.height = info.height; window.scrollTo(info.scrollLeft, info.scrollTop); cm.refresh(); } }); // CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: http://codemirror.net/LICENSE (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS mod(require("../../lib/codemirror")); else if (typeof define == "function" && define.amd) // AMD define(["../../lib/codemirror"], mod); else // Plain browser env mod(CodeMirror); })(function(CodeMirror) { CodeMirror.defineExtension("addPanel", function(node, options) { options = options || {}; if (!this.state.panels) initPanels(this); var info = this.state.panels; var wrapper = info.wrapper; var cmWrapper = this.getWrapperElement(); if (options.after instanceof Panel && !options.after.cleared) { wrapper.insertBefore(node, options.before.node.nextSibling); } else if (options.before instanceof Panel && !options.before.cleared) { wrapper.insertBefore(node, options.before.node); } else if (options.replace instanceof Panel && !options.replace.cleared) { wrapper.insertBefore(node, options.replace.node); options.replace.clear(); } else if (options.position == "bottom") { wrapper.appendChild(node); } else if (options.position == "before-bottom") { wrapper.insertBefore(node, cmWrapper.nextSibling); } else if (options.position == "after-top") { wrapper.insertBefore(node, cmWrapper); } else { wrapper.insertBefore(node, wrapper.firstChild); } var height = (options && options.height) || node.offsetHeight; this._setSize(null, info.heightLeft -= height); info.panels++; if (options.stable && isAtTop(this, node)) this.scrollTo(null, this.getScrollInfo().top + height) return new Panel(this, node, options, height); }); function Panel(cm, node, options, height) { this.cm = cm; this.node = node; this.options = options; this.height = height; this.cleared = false; } Panel.prototype.clear = function() { if (this.cleared) return; this.cleared = true; var info = this.cm.state.panels; this.cm._setSize(null, info.heightLeft += this.height); if (this.options.stable && isAtTop(this.cm, this.node)) this.cm.scrollTo(null, this.cm.getScrollInfo().top - this.height) info.wrapper.removeChild(this.node); if (--info.panels == 0) removePanels(this.cm); }; Panel.prototype.changed = function(height) { var newHeight = height == null ? this.node.offsetHeight : height; var info = this.cm.state.panels; this.cm._setSize(null, info.heightLeft -= (newHeight - this.height)); this.height = newHeight; }; function initPanels(cm) { var wrap = cm.getWrapperElement(); var style = window.getComputedStyle ? window.getComputedStyle(wrap) : wrap.currentStyle; var height = parseInt(style.height); var info = cm.state.panels = { setHeight: wrap.style.height, heightLeft: height, panels: 0, wrapper: document.createElement("div") }; wrap.parentNode.insertBefore(info.wrapper, wrap); var hasFocus = cm.hasFocus(); info.wrapper.appendChild(wrap); if (hasFocus) cm.focus(); cm._setSize = cm.setSize; if (height != null) cm.setSize = function(width, newHeight) { if (newHeight == null) return this._setSize(width, newHeight); info.setHeight = newHeight; if (typeof newHeight != "number") { var px = /^(\d+\.?\d*)px$/.exec(newHeight); if (px) { newHeight = Number(px[1]); } else { info.wrapper.style.height = newHeight; newHeight = info.wrapper.offsetHeight; info.wrapper.style.height = ""; } } cm._setSize(width, info.heightLeft += (newHeight - height)); height = newHeight; }; } function removePanels(cm) { var info = cm.state.panels; cm.state.panels = null; var wrap = cm.getWrapperElement(); info.wrapper.parentNode.replaceChild(wrap, info.wrapper); wrap.style.height = info.setHeight; cm.setSize = cm._setSize; cm.setSize(); } function isAtTop(cm, dom) { for (var sibling = dom.nextSibling; sibling; sibling = sibling.nextSibling) if (sibling == cm.getWrapperElement()) return true return false } }); // CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: http://codemirror.net/LICENSE (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS mod(require("../../lib/codemirror")); else if (typeof define == "function" && define.amd) // AMD define(["../../lib/codemirror"], mod); else // Plain browser env mod(CodeMirror); })(function(CodeMirror) { var defaults = { pairs: "()[]{}''\"\"", triples: "", explode: "[]{}" }; var Pos = CodeMirror.Pos; CodeMirror.defineOption("autoCloseBrackets", false, function(cm, val, old) { if (old && old != CodeMirror.Init) { cm.removeKeyMap(keyMap); cm.state.closeBrackets = null; } if (val) { ensureBound(getOption(val, "pairs")) cm.state.closeBrackets = val; cm.addKeyMap(keyMap); } }); function getOption(conf, name) { if (name == "pairs" && typeof conf == "string") return conf; if (typeof conf == "object" && conf[name] != null) return conf[name]; return defaults[name]; } var keyMap = {Backspace: handleBackspace, Enter: handleEnter}; function ensureBound(chars) { for (var i = 0; i < chars.length; i++) { var ch = chars.charAt(i), key = "'" + ch + "'" if (!keyMap[key]) keyMap[key] = handler(ch) } } ensureBound(defaults.pairs + "`") function handler(ch) { return function(cm) { return handleChar(cm, ch); }; } function getConfig(cm) { var deflt = cm.state.closeBrackets; if (!deflt || deflt.override) return deflt; var mode = cm.getModeAt(cm.getCursor()); return mode.closeBrackets || deflt; } function handleBackspace(cm) { var conf = getConfig(cm); if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass; var pairs = getOption(conf, "pairs"); var ranges = cm.listSelections(); for (var i = 0; i < ranges.length; i++) { if (!ranges[i].empty()) return CodeMirror.Pass; var around = charsAround(cm, ranges[i].head); if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass; } for (var i = ranges.length - 1; i >= 0; i--) { var cur = ranges[i].head; cm.replaceRange("", Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1), "+delete"); } } function handleEnter(cm) { var conf = getConfig(cm); var explode = conf && getOption(conf, "explode"); if (!explode || cm.getOption("disableInput")) return CodeMirror.Pass; var ranges = cm.listSelections(); for (var i = 0; i < ranges.length; i++) { if (!ranges[i].empty()) return CodeMirror.Pass; var around = charsAround(cm, ranges[i].head); if (!around || explode.indexOf(around) % 2 != 0) return CodeMirror.Pass; } cm.operation(function() { var linesep = cm.lineSeparator() || "\n"; cm.replaceSelection(linesep + linesep, null); cm.execCommand("goCharLeft"); ranges = cm.listSelections(); for (var i = 0; i < ranges.length; i++) { var line = ranges[i].head.line; cm.indentLine(line, null, true); cm.indentLine(line + 1, null, true); } }); } function contractSelection(sel) { var inverted = CodeMirror.cmpPos(sel.anchor, sel.head) > 0; return {anchor: new Pos(sel.anchor.line, sel.anchor.ch + (inverted ? -1 : 1)), head: new Pos(sel.head.line, sel.head.ch + (inverted ? 1 : -1))}; } function handleChar(cm, ch) { var conf = getConfig(cm); if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass; var pairs = getOption(conf, "pairs"); var pos = pairs.indexOf(ch); if (pos == -1) return CodeMirror.Pass; var triples = getOption(conf, "triples"); var identical = pairs.charAt(pos + 1) == ch; var ranges = cm.listSelections(); var opening = pos % 2 == 0; var type; for (var i = 0; i < ranges.length; i++) { var range = ranges[i], cur = range.head, curType; var next = cm.getRange(cur, Pos(cur.line, cur.ch + 1)); if (opening && !range.empty()) { curType = "surround"; } else if ((identical || !opening) && next == ch) { if (identical && stringStartsAfter(cm, cur)) curType = "both"; else if (triples.indexOf(ch) >= 0 && cm.getRange(cur, Pos(cur.line, cur.ch + 3)) == ch + ch + ch) curType = "skipThree"; else curType = "skip"; } else if (identical && cur.ch > 1 && triples.indexOf(ch) >= 0 && cm.getRange(Pos(cur.line, cur.ch - 2), cur) == ch + ch) { if (cur.ch > 2 && /\bstring/.test(cm.getTokenTypeAt(Pos(cur.line, cur.ch - 2)))) return CodeMirror.Pass; curType = "addFour"; } else if (identical) { var prev = cur.ch == 0 ? " " : cm.getRange(Pos(cur.line, cur.ch - 1), cur) if (!CodeMirror.isWordChar(next) && prev != ch && !CodeMirror.isWordChar(prev)) curType = "both"; else return CodeMirror.Pass; } else if (opening && (cm.getLine(cur.line).length == cur.ch || isClosingBracket(next, pairs) || /\s/.test(next))) { curType = "both"; } else { return CodeMirror.Pass; } if (!type) type = curType; else if (type != curType) return CodeMirror.Pass; } var left = pos % 2 ? pairs.charAt(pos - 1) : ch; var right = pos % 2 ? ch : pairs.charAt(pos + 1); cm.operation(function() { if (type == "skip") { cm.execCommand("goCharRight"); } else if (type == "skipThree") { for (var i = 0; i < 3; i++) cm.execCommand("goCharRight"); } else if (type == "surround") { var sels = cm.getSelections(); for (var i = 0; i < sels.length; i++) sels[i] = left + sels[i] + right; cm.replaceSelections(sels, "around"); sels = cm.listSelections().slice(); for (var i = 0; i < sels.length; i++) sels[i] = contractSelection(sels[i]); cm.setSelections(sels); } else if (type == "both") { cm.replaceSelection(left + right, null); cm.triggerElectric(left + right); cm.execCommand("goCharLeft"); } else if (type == "addFour") { cm.replaceSelection(left + left + left + left, "before"); cm.execCommand("goCharRight"); } }); } function isClosingBracket(ch, pairs) { var pos = pairs.lastIndexOf(ch); return pos > -1 && pos % 2 == 1; } function charsAround(cm, pos) { var str = cm.getRange(Pos(pos.line, pos.ch - 1), Pos(pos.line, pos.ch + 1)); return str.length == 2 ? str : null; } function stringStartsAfter(cm, pos) { var token = cm.getTokenAt(Pos(pos.line, pos.ch + 1)) return /\bstring/.test(token.type) && token.start == pos.ch && (pos.ch == 0 || !/\bstring/.test(cm.getTokenTypeAt(pos))) } }); // CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: http://codemirror.net/LICENSE /** * Tag-closer extension for CodeMirror. * * This extension adds an "autoCloseTags" option that can be set to * either true to get the default behavior, or an object to further * configure its behavior. * * These are supported options: * * `whenClosing` (default true) * Whether to autoclose when the '/' of a closing tag is typed. * `whenOpening` (default true) * Whether to autoclose the tag when the final '>' of an opening * tag is typed. * `dontCloseTags` (default is empty tags for HTML, none for XML) * An array of tag names that should not be autoclosed. * `indentTags` (default is block tags for HTML, none for XML) * An array of tag names that should, when opened, cause a * blank line to be added inside the tag, and the blank line and * closing line to be indented. * * See demos/closetag.html for a usage example. */ (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS mod(require("../../lib/codemirror"), require("../fold/xml-fold")); else if (typeof define == "function" && define.amd) // AMD define(["../../lib/codemirror", "../fold/xml-fold"], mod); else // Plain browser env mod(CodeMirror); })(function(CodeMirror) { CodeMirror.defineOption("autoCloseTags", false, function(cm, val, old) { if (old != CodeMirror.Init && old) cm.removeKeyMap("autoCloseTags"); if (!val) return; var map = {name: "autoCloseTags"}; if (typeof val != "object" || val.whenClosing) map["'/'"] = function(cm) { return autoCloseSlash(cm); }; if (typeof val != "object" || val.whenOpening) map["'>'"] = function(cm) { return autoCloseGT(cm); }; cm.addKeyMap(map); }); var htmlDontClose = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"]; var htmlIndent = ["applet", "blockquote", "body", "button", "div", "dl", "fieldset", "form", "frameset", "h1", "h2", "h3", "h4", "h5", "h6", "head", "html", "iframe", "layer", "legend", "object", "ol", "p", "select", "table", "ul"]; function autoCloseGT(cm) { if (cm.getOption("disableInput")) return CodeMirror.Pass; var ranges = cm.listSelections(), replacements = []; var opt = cm.getOption("autoCloseTags"); for (var i = 0; i < ranges.length; i++) { if (!ranges[i].empty()) return CodeMirror.Pass; var pos = ranges[i].head, tok = cm.getTokenAt(pos); var inner = CodeMirror.innerMode(cm.getMode(), tok.state), state = inner.state; if (inner.mode.name != "xml" || !state.tagName) return CodeMirror.Pass; var html = inner.mode.configuration == "html"; var dontCloseTags = (typeof opt == "object" && opt.dontCloseTags) || (html && htmlDontClose); var indentTags = (typeof opt == "object" && opt.indentTags) || (html && htmlIndent); var tagName = state.tagName; if (tok.end > pos.ch) tagName = tagName.slice(0, tagName.length - tok.end + pos.ch); var lowerTagName = tagName.toLowerCase(); // Don't process the '>' at the end of an end-tag or self-closing tag if (!tagName || tok.type == "string" && (tok.end != pos.ch || !/[\"\']/.test(tok.string.charAt(tok.string.length - 1)) || tok.string.length == 1) || tok.type == "tag" && state.type == "closeTag" || tok.string.indexOf("/") == (tok.string.length - 1) || // match something like <someTagName /> dontCloseTags && indexOf(dontCloseTags, lowerTagName) > -1 || closingTagExists(cm, tagName, pos, state, true)) return CodeMirror.Pass; var indent = indentTags && indexOf(indentTags, lowerTagName) > -1; replacements[i] = {indent: indent, text: ">" + (indent ? "\n\n" : "") + "</" + tagName + ">", newPos: indent ? CodeMirror.Pos(pos.line + 1, 0) : CodeMirror.Pos(pos.line, pos.ch + 1)}; } var dontIndentOnAutoClose = (typeof opt == "object" && opt.dontIndentOnAutoClose); for (var i = ranges.length - 1; i >= 0; i--) { var info = replacements[i]; cm.replaceRange(info.text, ranges[i].head, ranges[i].anchor, "+insert"); var sel = cm.listSelections().slice(0); sel[i] = {head: info.newPos, anchor: info.newPos}; cm.setSelections(sel); if (!dontIndentOnAutoClose && info.indent) { cm.indentLine(info.newPos.line, null, true); cm.indentLine(info.newPos.line + 1, null, true); } } } function autoCloseCurrent(cm, typingSlash) { var ranges = cm.listSelections(), replacements = []; var head = typingSlash ? "/" : "</"; var opt = cm.getOption("autoCloseTags"); var dontIndentOnAutoClose = (typeof opt == "object" && opt.dontIndentOnSlash); for (var i = 0; i < ranges.length; i++) { if (!ranges[i].empty()) return CodeMirror.Pass; var pos = ranges[i].head, tok = cm.getTokenAt(pos); var inner = CodeMirror.innerMode(cm.getMode(), tok.state), state = inner.state; if (typingSlash && (tok.type == "string" || tok.string.charAt(0) != "<" || tok.start != pos.ch - 1)) return CodeMirror.Pass; // Kludge to get around the fact that we are not in XML mode // when completing in JS/CSS snippet in htmlmixed mode. Does not // work for other XML embedded languages (there is no general // way to go from a mixed mode to its current XML state). var replacement; if (inner.mode.name != "xml") { if (cm.getMode().name == "htmlmixed" && inner.mode.name == "javascript") replacement = head + "script"; else if (cm.getMode().name == "htmlmixed" && inner.mode.name == "css") replacement = head + "style"; else return CodeMirror.Pass; } else { if (!state.context || !state.context.tagName || closingTagExists(cm, state.context.tagName, pos, state)) return CodeMirror.Pass; replacement = head + state.context.tagName; } if (cm.getLine(pos.line).charAt(tok.end) != ">") replacement += ">"; replacements[i] = replacement; } cm.replaceSelections(replacements); ranges = cm.listSelections(); if (!dontIndentOnAutoClose) { for (var i = 0; i < ranges.length; i++) if (i == ranges.length - 1 || ranges[i].head.line < ranges[i + 1].head.line) cm.indentLine(ranges[i].head.line); } } function autoCloseSlash(cm) { if (cm.getOption("disableInput")) return CodeMirror.Pass; return autoCloseCurrent(cm, true); } CodeMirror.commands.closeTag = function(cm) { return autoCloseCurrent(cm); }; function indexOf(collection, elt) { if (collection.indexOf) return collection.indexOf(elt); for (var i = 0, e = collection.length; i < e; ++i) if (collection[i] == elt) return i; return -1; } // If xml-fold is loaded, we use its functionality to try and verify // whether a given tag is actually unclosed. function closingTagExists(cm, tagName, pos, state, newTag) { if (!CodeMirror.scanForClosingTag) return false; var end = Math.min(cm.lastLine() + 1, pos.line + 500); var nextClose = CodeMirror.scanForClosingTag(cm, pos, null, end); if (!nextClose || nextClose.tag != tagName) return false; var cx = state.context; // If the immediate wrapping context contains onCx instances of // the same tag, a closing tag only exists if there are at least // that many closing tags of that type following. for (var onCx = newTag ? 1 : 0; cx && cx.tagName == tagName; cx = cx.prev) ++onCx; pos = nextClose.to; for (var i = 1; i < onCx; i++) { var next = CodeMirror.scanForClosingTag(cm, pos, null, end); if (!next || next.tag != tagName) return false; pos = next.to; } return true; } }); // CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: http://codemirror.net/LICENSE (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS mod(require("../../lib/codemirror")); else if (typeof define == "function" && define.amd) // AMD define(["../../lib/codemirror"], mod); else // Plain browser env mod(CodeMirror); })(function(CodeMirror) { var ie_lt8 = /MSIE \d/.test(navigator.userAgent) && (document.documentMode == null || document.documentMode < 8); var Pos = CodeMirror.Pos; var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<"}; function findMatchingBracket(cm, where, config) { var line = cm.getLineHandle(where.line), pos = where.ch - 1; var afterCursor = config && config.afterCursor if (afterCursor == null) afterCursor = /(^| )cm-fat-cursor($| )/.test(cm.getWrapperElement().className) // A cursor is defined as between two characters, but in in vim command mode // (i.e. not insert mode), the cursor is visually represented as a // highlighted box on top of the 2nd character. Otherwise, we allow matches // from before or after the cursor. var match = (!afterCursor && pos >= 0 && matching[line.text.charAt(pos)]) || matching[line.text.charAt(++pos)]; if (!match) return null; var dir = match.charAt(1) == ">" ? 1 : -1; if (config && config.strict && (dir > 0) != (pos == where.ch)) return null; var style = cm.getTokenTypeAt(Pos(where.line, pos + 1)); var found = scanForBracket(cm, Pos(where.line, pos + (dir > 0 ? 1 : 0)), dir, style || null, config); if (found == null) return null; return {from: Pos(where.line, pos), to: found && found.pos, match: found && found.ch == match.charAt(0), forward: dir > 0}; } // bracketRegex is used to specify which type of bracket to scan // should be a regexp, e.g. /[[\]]/ // // Note: If "where" is on an open bracket, then this bracket is ignored. // // Returns false when no bracket was found, null when it reached // maxScanLines and gave up function scanForBracket(cm, where, dir, style, config) { var maxScanLen = (config && config.maxScanLineLength) || 10000; var maxScanLines = (config && config.maxScanLines) || 1000; var stack = []; var re = config && config.bracketRegex ? config.bracketRegex : /[(){}[\]]/; var lineEnd = dir > 0 ? Math.min(where.line + maxScanLines, cm.lastLine() + 1) : Math.max(cm.firstLine() - 1, where.line - maxScanLines); for (var lineNo = where.line; lineNo != lineEnd; lineNo += dir) { var line = cm.getLine(lineNo); if (!line) continue; var pos = dir > 0 ? 0 : line.length - 1, end = dir > 0 ? line.length : -1; if (line.length > maxScanLen) continue; if (lineNo == where.line) pos = where.ch - (dir < 0 ? 1 : 0); for (; pos != end; pos += dir) { var ch = line.charAt(pos); if (re.test(ch) && (style === undefined || cm.getTokenTypeAt(Pos(lineNo, pos + 1)) == style)) { var match = matching[ch]; if ((match.charAt(1) == ">") == (dir > 0)) stack.push(ch); else if (!stack.length) return {pos: Pos(lineNo, pos), ch: ch}; else stack.pop(); } } } return lineNo - dir == (dir > 0 ? cm.lastLine() : cm.firstLine()) ? false : null; } function matchBrackets(cm, autoclear, config) { // Disable brace matching in long lines, since it'll cause hugely slow updates var maxHighlightLen = cm.state.matchBrackets.maxHighlightLineLength || 1000; var marks = [], ranges = cm.listSelections(); for (var i = 0; i < ranges.length; i++) { var match = ranges[i].empty() && findMatchingBracket(cm, ranges[i].head, config); if (match && cm.getLine(match.from.line).length <= maxHighlightLen) { var style = match.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket"; marks.push(cm.markText(match.from, Pos(match.from.line, match.from.ch + 1), {className: style})); if (match.to && cm.getLine(match.to.line).length <= maxHighlightLen) marks.push(cm.markText(match.to, Pos(match.to.line, match.to.ch + 1), {className: style})); } } if (marks.length) { // Kludge to work around the IE bug from issue #1193, where text // input stops going to the textare whever this fires. if (ie_lt8 && cm.state.focused) cm.focus(); var clear = function() { cm.operation(function() { for (var i = 0; i < marks.length; i++) marks[i].clear(); }); }; if (autoclear) setTimeout(clear, 800); else return clear; } } function doMatchBrackets(cm) { cm.operation(function() { if (cm.state.matchBrackets.currentlyHighlighted) { cm.state.matchBrackets.currentlyHighlighted(); cm.state.matchBrackets.currentlyHighlighted = null; } cm.state.matchBrackets.currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets); }); } CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) { if (old && old != CodeMirror.Init) { cm.off("cursorActivity", doMatchBrackets); if (cm.state.matchBrackets && cm.state.matchBrackets.currentlyHighlighted) { cm.state.matchBrackets.currentlyHighlighted(); cm.state.matchBrackets.currentlyHighlighted = null; } } if (val) { cm.state.matchBrackets = typeof val == "object" ? val : {}; cm.on("cursorActivity", doMatchBrackets); } }); CodeMirror.defineExtension("matchBrackets", function() {matchBrackets(this, true);}); CodeMirror.defineExtension("findMatchingBracket", function(pos, config, oldConfig){ // Backwards-compatibility kludge if (oldConfig || typeof config == "boolean") { if (!oldConfig) { config = config ? {strict: true} : null } else { oldConfig.strict = config config = oldConfig } } return findMatchingBracket(this, pos, config) }); CodeMirror.defineExtension("scanForBracket", function(pos, dir, style, config){ return scanForBracket(this, pos, dir, style, config); }); }); // CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: http://codemirror.net/LICENSE (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS mod(require("../../lib/codemirror"), require("../fold/xml-fold")); else if (typeof define == "function" && define.amd) // AMD define(["../../lib/codemirror", "../fold/xml-fold"], mod); else // Plain browser env mod(CodeMirror); })(function(CodeMirror) { "use strict"; CodeMirror.defineOption("matchTags", false, function(cm, val, old) { if (old && old != CodeMirror.Init) { cm.off("cursorActivity", doMatchTags); cm.off("viewportChange", maybeUpdateMatch); clear(cm); } if (val) { cm.state.matchBothTags = typeof val == "object" && val.bothTags; cm.on("cursorActivity", doMatchTags); cm.on("viewportChange", maybeUpdateMatch); doMatchTags(cm); } }); function clear(cm) { if (cm.state.tagHit) cm.state.tagHit.clear(); if (cm.state.tagOther) cm.state.tagOther.clear(); cm.state.tagHit = cm.state.tagOther = null; } function doMatchTags(cm) { cm.state.failedTagMatch = false; cm.operation(function() { clear(cm); if (cm.somethingSelected()) return; var cur = cm.getCursor(), range = cm.getViewport(); range.from = Math.min(range.from, cur.line); range.to = Math.max(cur.line + 1, range.to); var match = CodeMirror.findMatchingTag(cm, cur, range); if (!match) return; if (cm.state.matchBothTags) { var hit = match.at == "open" ? match.open : match.close; if (hit) cm.state.tagHit = cm.markText(hit.from, hit.to, {className: "CodeMirror-matchingtag"}); } var other = match.at == "close" ? match.open : match.close; if (other) cm.state.tagOther = cm.markText(other.from, other.to, {className: "CodeMirror-matchingtag"}); else cm.state.failedTagMatch = true; }); } function maybeUpdateMatch(cm) { if (cm.state.failedTagMatch) doMatchTags(cm); } CodeMirror.commands.toMatchingTag = function(cm) { var found = CodeMirror.findMatchingTag(cm, cm.getCursor()); if (found) { var other = found.at == "close" ? found.open : found.close; if (other) cm.extendSelection(other.to, other.from); } }; }); // CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: http://codemirror.net/LICENSE (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS mod(require("../../lib/codemirror")); else if (typeof define == "function" && define.amd) // AMD define(["../../lib/codemirror"], mod); else // Plain browser env mod(CodeMirror); })(function(CodeMirror) { "use strict"; CodeMirror.registerHelper("fold", "brace", function(cm, start) { var line = start.line, lineText = cm.getLine(line); var tokenType; function findOpening(openCh) { for (var at = start.ch, pass = 0;;) { var found = at <= 0 ? -1 : lineText.lastIndexOf(openCh, at - 1); if (found == -1) { if (pass == 1) break; pass = 1; at = lineText.length; continue; } if (pass == 1 && found < start.ch) break; tokenType = cm.getTokenTypeAt(CodeMirror.Pos(line, found + 1)); if (!/^(comment|string)/.test(tokenType)) return found + 1; at = found - 1; } } var startToken = "{", endToken = "}", startCh = findOpening("{"); if (startCh == null) { startToken = "[", endToken = "]"; startCh = findOpening("["); } if (startCh == null) return; var count = 1, lastLine = cm.lastLine(), end, endCh; outer: for (var i = line; i <= lastLine; ++i) { var text = cm.getLine(i), pos = i == line ? startCh : 0; for (;;) { var nextOpen = text.indexOf(startToken, pos), nextClose = text.indexOf(endToken, pos); if (nextOpen < 0) nextOpen = text.length; if (nextClose < 0) nextClose = text.length; pos = Math.min(nextOpen, nextClose); if (pos == text.length) break; if (cm.getTokenTypeAt(CodeMirror.Pos(i, pos + 1)) == tokenType) { if (pos == nextOpen) ++count; else if (!--count) { end = i; endCh = pos; break outer; } } ++pos; } } if (end == null || line == end && endCh == startCh) return; return {from: CodeMirror.Pos(line, startCh), to: CodeMirror.Pos(end, endCh)}; }); CodeMirror.registerHelper("fold", "import", function(cm, start) { function hasImport(line) { if (line < cm.firstLine() || line > cm.lastLine()) return null; var start = cm.getTokenAt(CodeMirror.Pos(line, 1)); if (!/\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1)); if (start.type != "keyword" || start.string != "import") return null; // Now find closing semicolon, return its position for (var i = line, e = Math.min(cm.lastLine(), line + 10); i <= e; ++i) { var text = cm.getLine(i), semi = text.indexOf(";"); if (semi != -1) return {startCh: start.end, end: CodeMirror.Pos(i, semi)}; } } var startLine = start.line, has = hasImport(startLine), prev; if (!has || hasImport(startLine - 1) || ((prev = hasImport(startLine - 2)) && prev.end.line == startLine - 1)) return null; for (var end = has.end;;) { var next = hasImport(end.line + 1); if (next == null) break; end = next.end; } return {from: cm.clipPos(CodeMirror.Pos(startLine, has.startCh + 1)), to: end}; }); CodeMirror.registerHelper("fold", "include", function(cm, start) { function hasInclude(line) { if (line < cm.firstLine() || line > cm.lastLine()) return null; var start = cm.getTokenAt(CodeMirror.Pos(line, 1)); if (!/\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1)); if (start.type == "meta" && start.string.slice(0, 8) == "#include") return start.start + 8; } var startLine = start.line, has = hasInclude(startLine); if (has == null || hasInclude(startLine - 1) != null) return null; for (var end = startLine;;) { var next = hasInclude(end + 1); if (next == null) break; ++end; } return {from: CodeMirror.Pos(startLine, has + 1), to: cm.clipPos(CodeMirror.Pos(end))}; }); }); // CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: http://codemirror.net/LICENSE (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS mod(require("../../lib/codemirror")); else if (typeof define == "function" && define.amd) // AMD define(["../../lib/codemirror"], mod); else // Plain browser env mod(CodeMirror); })(function(CodeMirror) { "use strict"; function doFold(cm, pos, options, force) { if (options && options.call) { var finder = options; options = null; } else { var finder = getOption(cm, options, "rangeFinder"); } if (typeof pos == "number") pos = CodeMirror.Pos(pos, 0); var minSize = getOption(cm, options, "minFoldSize"); function getRange(allowFolded) { var range = finder(cm, pos); if (!range || range.to.line - range.from.line < minSize) return null; var marks = cm.findMarksAt(range.from); for (var i = 0; i < marks.length; ++i) { if (marks[i].__isFold && force !== "fold") { if (!allowFolded) return null; range.cleared = true; marks[i].clear(); } } return range; } var range = getRange(true); if (getOption(cm, options, "scanUp")) while (!range && pos.line > cm.firstLine()) { pos = CodeMirror.Pos(pos.line - 1, 0); range = getRange(false); } if (!range || range.cleared || force === "unfold") return; var myWidget = makeWidget(cm, options); CodeMirror.on(myWidget, "mousedown", function(e) { myRange.clear(); CodeMirror.e_preventDefault(e); }); var myRange = cm.markText(range.from, range.to, { replacedWith: myWidget, clearOnEnter: getOption(cm, options, "clearOnEnter"), __isFold: true }); myRange.on("clear", function(from, to) { CodeMirror.signal(cm, "unfold", cm, from, to); }); CodeMirror.signal(cm, "fold", cm, range.from, range.to); } function makeWidget(cm, options) { var widget = getOption(cm, options, "widget"); if (typeof widget == "string") { var text = document.createTextNode(widget); widget = document.createElement("span"); widget.appendChild(text); widget.className = "CodeMirror-foldmarker"; } else if (widget) { widget = widget.cloneNode(true) } return widget; } // Clumsy backwards-compatible interface CodeMirror.newFoldFunction = function(rangeFinder, widget) { return function(cm, pos) { doFold(cm, pos, {rangeFinder: rangeFinder, widget: widget}); }; }; // New-style interface CodeMirror.defineExtension("foldCode", function(pos, options, force) { doFold(this, pos, options, force); }); CodeMirror.defineExtension("isFolded", function(pos) { var marks = this.findMarksAt(pos); for (var i = 0; i < marks.length; ++i) if (marks[i].__isFold) return true; }); CodeMirror.commands.toggleFold = function(cm) { cm.foldCode(cm.getCursor()); }; CodeMirror.commands.fold = function(cm) { cm.foldCode(cm.getCursor(), null, "fold"); }; CodeMirror.commands.unfold = function(cm) { cm.foldCode(cm.getCursor(), null, "unfold"); }; CodeMirror.commands.foldAll = function(cm) { cm.operation(function() { for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++) cm.foldCode(CodeMirror.Pos(i, 0), null, "fold"); }); }; CodeMirror.commands.unfoldAll = function(cm) { cm.operation(function() { for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++) cm.foldCode(CodeMirror.Pos(i, 0), null, "unfold"); }); }; CodeMirror.registerHelper("fold", "combine", function() { var funcs = Array.prototype.slice.call(arguments, 0); return function(cm, start) { for (var i = 0; i < funcs.length; ++i) { var found = funcs[i](cm, start); if (found) return found; } }; }); CodeMirror.registerHelper("fold", "auto", function(cm, start) { var helpers = cm.getHelpers(start, "fold"); for (var i = 0; i < helpers.length; i++) { var cur = helpers[i](cm, start); if (cur) return cur; } }); var defaultOptions = { rangeFinder: CodeMirror.fold.auto, widget: "\u2194", minFoldSize: 0, scanUp: false, clearOnEnter: true }; CodeMirror.defineOption("foldOptions", null); function getOption(cm, options, name) { if (options && options[name] !== undefined) return options[name]; var editorOptions = cm.options.foldOptions; if (editorOptions && editorOptions[name] !== undefined) return editorOptions[name]; return defaultOptions[name]; } CodeMirror.defineExtension("foldOption", function(options, name) { return getOption(this, options, name); }); }); // CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: http://codemirror.net/LICENSE (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS mod(require("../../lib/codemirror"), require("./foldcode")); else if (typeof define == "function" && define.amd) // AMD define(["../../lib/codemirror", "./foldcode"], mod); else // Plain browser env mod(CodeMirror); })(function(CodeMirror) { "use strict"; CodeMirror.defineOption("foldGutter", false, function(cm, val, old) { if (old && old != CodeMirror.Init) { cm.clearGutter(cm.state.foldGutter.options.gutter); cm.state.foldGutter = null; cm.off("gutterClick", onGutterClick); cm.off("change", onChange); cm.off("viewportChange", onViewportChange); cm.off("fold", onFold); cm.off("unfold", onFold); cm.off("swapDoc", onChange); } if (val) { cm.state.foldGutter = new State(parseOptions(val)); updateInViewport(cm); cm.on("gutterClick", onGutterClick); cm.on("change", onChange); cm.on("viewportChange", onViewportChange); cm.on("fold", onFold); cm.on("unfold", onFold); cm.on("swapDoc", onChange); } }); var Pos = CodeMirror.Pos; function State(options) { this.options = options; this.from = this.to = 0; } function parseOptions(opts) { if (opts === true) opts = {}; if (opts.gutter == null) opts.gutter = "CodeMirror-foldgutter"; if (opts.indicatorOpen == null) opts.indicatorOpen = "CodeMirror-foldgutter-open"; if (opts.indicatorFolded == null) opts.indicatorFolded = "CodeMirror-foldgutter-folded"; return opts; } function isFolded(cm, line) { var marks = cm.findMarks(Pos(line, 0), Pos(line + 1, 0)); for (var i = 0; i < marks.length; ++i) if (marks[i].__isFold && marks[i].find().from.line == line) return marks[i]; } function marker(spec) { if (typeof spec == "string") { var elt = document.createElement("div"); elt.className = spec + " CodeMirror-guttermarker-subtle"; return elt; } else { return spec.cloneNode(true); } } function updateFoldInfo(cm, from, to) { var opts = cm.state.foldGutter.options, cur = from; var minSize = cm.foldOption(opts, "minFoldSize"); var func = cm.foldOption(opts, "rangeFinder"); cm.eachLine(from, to, function(line) { var mark = null; if (isFolded(cm, cur)) { mark = marker(opts.indicatorFolded); } else { var pos = Pos(cur, 0); var range = func && func(cm, pos); if (range && range.to.line - range.from.line >= minSize) mark = marker(opts.indicatorOpen); } cm.setGutterMarker(line, opts.gutter, mark); ++cur; }); } function updateInViewport(cm) { var vp = cm.getViewport(), state = cm.state.foldGutter; if (!state) return; cm.operation(function() { updateFoldInfo(cm, vp.from, vp.to); }); state.from = vp.from; state.to = vp.to; } function onGutterClick(cm, line, gutter) { var state = cm.state.foldGutter; if (!state) return; var opts = state.options; if (gutter != opts.gutter) return; var folded = isFolded(cm, line); if (folded) folded.clear(); else cm.foldCode(Pos(line, 0), opts.rangeFinder); } function onChange(cm) { var state = cm.state.foldGutter; if (!state) return; var opts = state.options; state.from = state.to = 0; clearTimeout(state.changeUpdate); state.changeUpdate = setTimeout(function() { updateInViewport(cm); }, opts.foldOnChangeTimeSpan || 600); } function onViewportChange(cm) { var state = cm.state.foldGutter; if (!state) return; var opts = state.options; clearTimeout(state.changeUpdate); state.changeUpdate = setTimeout(function() { var vp = cm.getViewport(); if (state.from == state.to || vp.from - state.to > 20 || state.from - vp.to > 20) { updateInViewport(cm); } else { cm.operation(function() { if (vp.from < state.from) { updateFoldInfo(cm, vp.from, state.from); state.from = vp.from; } if (vp.to > state.to) { updateFoldInfo(cm, state.to, vp.to); state.to = vp.to; } }); } }, opts.updateViewportTimeSpan || 400); } function onFold(cm, from) { var state = cm.state.foldGutter; if (!state) return; var line = from.line; if (line >= state.from && line < state.to) updateFoldInfo(cm, line, line + 1); } }); // CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: http://codemirror.net/LICENSE (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS mod(require("../../lib/codemirror")); else if (typeof define == "function" && define.amd) // AMD define(["../../lib/codemirror"], mod); else // Plain browser env mod(CodeMirror); })(function(CodeMirror) { "use strict"; var Pos = CodeMirror.Pos; function cmp(a, b) { return a.line - b.line || a.ch - b.ch; } var nameStartChar = "A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD"; var nameChar = nameStartChar + "\-\:\.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040"; var xmlTagStart = new RegExp("<(/?)([" + nameStartChar + "][" + nameChar + "]*)", "g"); function Iter(cm, line, ch, range) { this.line = line; this.ch = ch; this.cm = cm; this.text = cm.getLine(line); this.min = range ? Math.max(range.from, cm.firstLine()) : cm.firstLine(); this.max = range ? Math.min(range.to - 1, cm.lastLine()) : cm.lastLine(); } function tagAt(iter, ch) { var type = iter.cm.getTokenTypeAt(Pos(iter.line, ch)); return type && /\btag\b/.test(type); } function nextLine(iter) { if (iter.line >= iter.max) return; iter.ch = 0; iter.text = iter.cm.getLine(++iter.line); return true; } function prevLine(iter) { if (iter.line <= iter.min) return; iter.text = iter.cm.getLine(--iter.line); iter.ch = iter.text.length; return true; } function toTagEnd(iter) { for (;;) { var gt = iter.text.indexOf(">", iter.ch); if (gt == -1) { if (nextLine(iter)) continue; else return; } if (!tagAt(iter, gt + 1)) { iter.ch = gt + 1; continue; } var lastSlash = iter.text.lastIndexOf("/", gt); var selfClose = lastSlash > -1 && !/\S/.test(iter.text.slice(lastSlash + 1, gt)); iter.ch = gt + 1; return selfClose ? "selfClose" : "regular"; } } function toTagStart(iter) { for (;;) { var lt = iter.ch ? iter.text.lastIndexOf("<", iter.ch - 1) : -1; if (lt == -1) { if (prevLine(iter)) continue; else return; } if (!tagAt(iter, lt + 1)) { iter.ch = lt; continue; } xmlTagStart.lastIndex = lt; iter.ch = lt; var match = xmlTagStart.exec(iter.text); if (match && match.index == lt) return match; } } function toNextTag(iter) { for (;;) { xmlTagStart.lastIndex = iter.ch; var found = xmlTagStart.exec(iter.text); if (!found) { if (nextLine(iter)) continue; else return; } if (!tagAt(iter, found.index + 1)) { iter.ch = found.index + 1; continue; } iter.ch = found.index + found[0].length; return found; } } function toPrevTag(iter) { for (;;) { var gt = iter.ch ? iter.text.lastIndexOf(">", iter.ch - 1) : -1; if (gt == -1) { if (prevLine(iter)) continue; else return; } if (!tagAt(iter, gt + 1)) { iter.ch = gt; continue; } var lastSlash = iter.text.lastIndexOf("/", gt); var selfClose = lastSlash > -1 && !/\S/.test(iter.text.slice(lastSlash + 1, gt)); iter.ch = gt + 1; return selfClose ? "selfClose" : "regular"; } } function findMatchingClose(iter, tag) { var stack = []; for (;;) { var next = toNextTag(iter), end, startLine = iter.line, startCh = iter.ch - (next ? next[0].length : 0); if (!next || !(end = toTagEnd(iter))) return; if (end == "selfClose") continue; if (next[1]) { // closing tag for (var i = stack.length - 1; i >= 0; --i) if (stack[i] == next[2]) { stack.length = i; break; } if (i < 0 && (!tag || tag == next[2])) return { tag: next[2], from: Pos(startLine, startCh), to: Pos(iter.line, iter.ch) }; } else { // opening tag stack.push(next[2]); } } } function findMatchingOpen(iter, tag) { var stack = []; for (;;) { var prev = toPrevTag(iter); if (!prev) return; if (prev == "selfClose") { toTagStart(iter); continue; } var endLine = iter.line, endCh = iter.ch; var start = toTagStart(iter); if (!start) return; if (start[1]) { // closing tag stack.push(start[2]); } else { // opening tag for (var i = stack.length - 1; i >= 0; --i) if (stack[i] == start[2]) { stack.length = i; break; } if (i < 0 && (!tag || tag == start[2])) return { tag: start[2], from: Pos(iter.line, iter.ch), to: Pos(endLine, endCh) }; } } } CodeMirror.registerHelper("fold", "xml", function(cm, start) { var iter = new Iter(cm, start.line, 0); for (;;) { var openTag = toNextTag(iter), end; if (!openTag || !(end = toTagEnd(iter)) || iter.line != start.line) return; if (!openTag[1] && end != "selfClose") { var startPos = Pos(iter.line, iter.ch); var endPos = findMatchingClose(iter, openTag[2]); return endPos && {from: startPos, to: endPos.from}; } } }); CodeMirror.findMatchingTag = function(cm, pos, range) { var iter = new Iter(cm, pos.line, pos.ch, range); if (iter.text.indexOf(">") == -1 && iter.text.indexOf("<") == -1) return; var end = toTagEnd(iter), to = end && Pos(iter.line, iter.ch); var start = end && toTagStart(iter); if (!end || !start || cmp(iter, pos) > 0) return; var here = {from: Pos(iter.line, iter.ch), to: to, tag: start[2]}; if (end == "selfClose") return {open: here, close: null, at: "open"}; if (start[1]) { // closing tag return {open: findMatchingOpen(iter, start[2]), close: here, at: "close"}; } else { // opening tag iter = new Iter(cm, to.line, to.ch, range); return {open: here, close: findMatchingClose(iter, start[2]), at: "open"}; } }; CodeMirror.findEnclosingTag = function(cm, pos, range, tag) { var iter = new Iter(cm, pos.line, pos.ch, range); for (;;) { var open = findMatchingOpen(iter, tag); if (!open) break; var forward = new Iter(cm, pos.line, pos.ch, range); var close = findMatchingClose(forward, open.tag); if (close) return {open: open, close: close}; } }; // Used by addon/edit/closetag.js CodeMirror.scanForClosingTag = function(cm, pos, name, end) { var iter = new Iter(cm, pos.line, pos.ch, end ? {from: 0, to: end} : null); return findMatchingClose(iter, name); }; }); // CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: http://codemirror.net/LICENSE (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS mod(require("../../lib/codemirror"), "cjs"); else if (typeof define == "function" && define.amd) // AMD define(["../../lib/codemirror"], function(CM) { mod(CM, "amd"); }); else // Plain browser env mod(CodeMirror, "plain"); })(function(CodeMirror, env) { if (!CodeMirror.modeURL) CodeMirror.modeURL = "../mode/%N/%N.js"; var loading = {}; function splitCallback(cont, n) { var countDown = n; return function() { if (--countDown == 0) cont(); }; } function ensureDeps(mode, cont) { var deps = CodeMirror.modes[mode].dependencies; if (!deps) return cont(); var missing = []; for (var i = 0; i < deps.length; ++i) { if (!CodeMirror.modes.hasOwnProperty(deps[i])) missing.push(deps[i]); } if (!missing.length) return cont(); var split = splitCallback(cont, missing.length); for (var i = 0; i < missing.length; ++i) CodeMirror.requireMode(missing[i], split); } CodeMirror.requireMode = function(mode, cont) { if (typeof mode != "string") mode = mode.name; if (CodeMirror.modes.hasOwnProperty(mode)) return ensureDeps(mode, cont); if (loading.hasOwnProperty(mode)) return loading[mode].push(cont); var file = CodeMirror.modeURL.replace(/%N/g, mode); if (env == "plain") { var script = document.createElement("script"); script.src = file; var others = document.getElementsByTagName("script")[0]; var list = loading[mode] = [cont]; CodeMirror.on(script, "load", function() { ensureDeps(mode, function() { for (var i = 0; i < list.length; ++i) list[i](); }); }); others.parentNode.insertBefore(script, others); } else if (env == "cjs") { require(file); cont(); } else if (env == "amd") { requirejs([file], cont); } }; CodeMirror.autoLoadMode = function(instance, mode) { if (!CodeMirror.modes.hasOwnProperty(mode)) CodeMirror.requireMode(mode, function() { instance.setOption("mode", instance.getOption("mode")); }); }; }); // CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: http://codemirror.net/LICENSE (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS mod(require("../../lib/codemirror")); else if (typeof define == "function" && define.amd) // AMD define(["../../lib/codemirror"], mod); else // Plain browser env mod(CodeMirror); })(function(CodeMirror) { "use strict"; CodeMirror.multiplexingMode = function(outer /*, others */) { // Others should be {open, close, mode [, delimStyle] [, innerStyle]} objects var others = Array.prototype.slice.call(arguments, 1); function indexOf(string, pattern, from, returnEnd) { if (typeof pattern == "string") { var found = string.indexOf(pattern, from); return returnEnd && found > -1 ? found + pattern.length : found; } var m = pattern.exec(from ? string.slice(from) : string); return m ? m.index + from + (returnEnd ? m[0].length : 0) : -1; } return { startState: function() { return { outer: CodeMirror.startState(outer), innerActive: null, inner: null }; }, copyState: function(state) { return { outer: CodeMirror.copyState(outer, state.outer), innerActive: state.innerActive, inner: state.innerActive && CodeMirror.copyState(state.innerActive.mode, state.inner) }; }, token: function(stream, state) { if (!state.innerActive) { var cutOff = Infinity, oldContent = stream.string; for (var i = 0; i < others.length; ++i) { var other = others[i]; var found = indexOf(oldContent, other.open, stream.pos); if (found == stream.pos) { if (!other.parseDelimiters) stream.match(other.open); state.innerActive = other; state.inner = CodeMirror.startState(other.mode, outer.indent ? outer.indent(state.outer, "") : 0); return other.delimStyle && (other.delimStyle + " " + other.delimStyle + "-open"); } else if (found != -1 && found < cutOff) { cutOff = found; } } if (cutOff != Infinity) stream.string = oldContent.slice(0, cutOff); var outerToken = outer.token(stream, state.outer); if (cutOff != Infinity) stream.string = oldContent; return outerToken; } else { var curInner = state.innerActive, oldContent = stream.string; if (!curInner.close && stream.sol()) { state.innerActive = state.inner = null; return this.token(stream, state); } var found = curInner.close ? indexOf(oldContent, curInner.close, stream.pos, curInner.parseDelimiters) : -1; if (found == stream.pos && !curInner.parseDelimiters) { stream.match(curInner.close); state.innerActive = state.inner = null; return curInner.delimStyle && (curInner.delimStyle + " " + curInner.delimStyle + "-close"); } if (found > -1) stream.string = oldContent.slice(0, found); var innerToken = curInner.mode.token(stream, state.inner); if (found > -1) stream.string = oldContent; if (found == stream.pos && curInner.parseDelimiters) state.innerActive = state.inner = null; if (curInner.innerStyle) { if (innerToken) innerToken = innerToken + " " + curInner.innerStyle; else innerToken = curInner.innerStyle; } return innerToken; } }, indent: function(state, textAfter) { var mode = state.innerActive ? state.innerActive.mode : outer; if (!mode.indent) return CodeMirror.Pass; return mode.indent(state.innerActive ? state.inner : state.outer, textAfter); }, blankLine: function(state) { var mode = state.innerActive ? state.innerActive.mode : outer; if (mode.blankLine) { mode.blankLine(state.innerActive ? state.inner : state.outer); } if (!state.innerActive) { for (var i = 0; i < others.length; ++i) { var other = others[i]; if (other.open === "\n") { state.innerActive = other; state.inner = CodeMirror.startState(other.mode, mode.indent ? mode.indent(state.outer, "") : 0); } } } else if (state.innerActive.close === "\n") { state.innerActive = state.inner = null; } }, electricChars: outer.electricChars, innerMode: function(state) { return state.inner ? {state: state.inner, mode: state.innerActive.mode} : {state: state.outer, mode: outer}; } }; }; }); // CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: http://codemirror.net/LICENSE (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS mod(require("../../lib/codemirror")); else if (typeof define == "function" && define.amd) // AMD define(["../../lib/codemirror"], mod); else // Plain browser env mod(CodeMirror); })(function(CodeMirror) { "use strict"; CodeMirror.defineExtension("annotateScrollbar", function(options) { if (typeof options == "string") options = {className: options}; return new Annotation(this, options); }); CodeMirror.defineOption("scrollButtonHeight", 0); function Annotation(cm, options) { this.cm = cm; this.options = options; this.buttonHeight = options.scrollButtonHeight || cm.getOption("scrollButtonHeight"); this.annotations = []; this.doRedraw = this.doUpdate = null; this.div = cm.getWrapperElement().appendChild(document.createElement("div")); this.div.style.cssText = "position: absolute; right: 0; top: 0; z-index: 7; pointer-events: none"; this.computeScale(); function scheduleRedraw(delay) { clearTimeout(self.doRedraw); self.doRedraw = setTimeout(function() { self.redraw(); }, delay); } var self = this; cm.on("refresh", this.resizeHandler = function() { clearTimeout(self.doUpdate); self.doUpdate = setTimeout(function() { if (self.computeScale()) scheduleRedraw(20); }, 100); }); cm.on("markerAdded", this.resizeHandler); cm.on("markerCleared", this.resizeHandler); if (options.listenForChanges !== false) cm.on("change", this.changeHandler = function() { scheduleRedraw(250); }); } Annotation.prototype.computeScale = function() { var cm = this.cm; var hScale = (cm.getWrapperElement().clientHeight - cm.display.barHeight - this.buttonHeight * 2) / cm.getScrollerElement().scrollHeight if (hScale != this.hScale) { this.hScale = hScale; return true; } }; Annotation.prototype.update = function(annotations) { this.annotations = annotations; this.redraw(); }; Annotation.prototype.redraw = function(compute) { if (compute !== false) this.computeScale(); var cm = this.cm, hScale = this.hScale; var frag = document.createDocumentFragment(), anns = this.annotations; var wrapping = cm.getOption("lineWrapping"); var singleLineH = wrapping && cm.defaultTextHeight() * 1.5; var curLine = null, curLineObj = null; function getY(pos, top) { if (curLine != pos.line) { curLine = pos.line; curLineObj = cm.getLineHandle(curLine); } if ((curLineObj.widgets && curLineObj.widgets.length) || (wrapping && curLineObj.height > singleLineH)) return cm.charCoords(pos, "local")[top ? "top" : "bottom"]; var topY = cm.heightAtLine(curLineObj, "local"); return topY + (top ? 0 : curLineObj.height); } var lastLine = cm.lastLine() if (cm.display.barWidth) for (var i = 0, nextTop; i < anns.length; i++) { var ann = anns[i]; if (ann.to.line > lastLine) continue; var top = nextTop || getY(ann.from, true) * hScale; var bottom = getY(ann.to, false) * hScale; while (i < anns.length - 1) { if (anns[i + 1].to.line > lastLine) break; nextTop = getY(anns[i + 1].from, true) * hScale; if (nextTop > bottom + .9) break; ann = anns[++i]; bottom = getY(ann.to, false) * hScale; } if (bottom == top) continue; var height = Math.max(bottom - top, 3); var elt = frag.appendChild(document.createElement("div")); elt.style.cssText = "position: absolute; right: 0px; width: " + Math.max(cm.display.barWidth - 1, 2) + "px; top: " + (top + this.buttonHeight) + "px; height: " + height + "px"; elt.className = this.options.className; if (ann.id) { elt.setAttribute("annotation-id", ann.id); } } this.div.textContent = ""; this.div.appendChild(frag); }; Annotation.prototype.clear = function() { this.cm.off("refresh", this.resizeHandler); this.cm.off("markerAdded", this.resizeHandler); this.cm.off("markerCleared", this.resizeHandler); if (this.changeHandler) this.cm.off("change", this.changeHandler); this.div.parentNode.removeChild(this.div); }; }); // CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: http://codemirror.net/LICENSE (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS mod(require("../../lib/codemirror")); else if (typeof define == "function" && define.amd) // AMD define(["../../lib/codemirror"], mod); else // Plain browser env mod(CodeMirror); })(function(CodeMirror) { "use strict"; function Bar(cls, orientation, scroll) { this.orientation = orientation; this.scroll = scroll; this.screen = this.total = this.size = 1; this.pos = 0; this.node = document.createElement("div"); this.node.className = cls + "-" + orientation; this.inner = this.node.appendChild(document.createElement("div")); var self = this; CodeMirror.on(this.inner, "mousedown", function(e) { if (e.which != 1) return; CodeMirror.e_preventDefault(e); var axis = self.orientation == "horizontal" ? "pageX" : "pageY"; var start = e[axis], startpos = self.pos; function done() { CodeMirror.off(document, "mousemove", move); CodeMirror.off(document, "mouseup", done); } function move(e) { if (e.which != 1) return done(); self.moveTo(startpos + (e[axis] - start) * (self.total / self.size)); } CodeMirror.on(document, "mousemove", move); CodeMirror.on(document, "mouseup", done); }); CodeMirror.on(this.node, "click", function(e) { CodeMirror.e_preventDefault(e); var innerBox = self.inner.getBoundingClientRect(), where; if (self.orientation == "horizontal") where = e.clientX < innerBox.left ? -1 : e.clientX > innerBox.right ? 1 : 0; else where = e.clientY < innerBox.top ? -1 : e.clientY > innerBox.bottom ? 1 : 0; self.moveTo(self.pos + where * self.screen); }); function onWheel(e) { var moved = CodeMirror.wheelEventPixels(e)[self.orientation == "horizontal" ? "x" : "y"]; var oldPos = self.pos; self.moveTo(self.pos + moved); if (self.pos != oldPos) CodeMirror.e_preventDefault(e); } CodeMirror.on(this.node, "mousewheel", onWheel); CodeMirror.on(this.node, "DOMMouseScroll", onWheel); } Bar.prototype.setPos = function(pos, force) { if (pos < 0) pos = 0; if (pos > this.total - this.screen) pos = this.total - this.screen; if (!force && pos == this.pos) return false; this.pos = pos; this.inner.style[this.orientation == "horizontal" ? "left" : "top"] = (pos * (this.size / this.total)) + "px"; return true }; Bar.prototype.moveTo = function(pos) { if (this.setPos(pos)) this.scroll(pos, this.orientation); } var minButtonSize = 10; Bar.prototype.update = function(scrollSize, clientSize, barSize) { var sizeChanged = this.screen != clientSize || this.total != scrollSize || this.size != barSize if (sizeChanged) { this.screen = clientSize; this.total = scrollSize; this.size = barSize; } var buttonSize = this.screen * (this.size / this.total); if (buttonSize < minButtonSize) { this.size -= minButtonSize - buttonSize; buttonSize = minButtonSize; } this.inner.style[this.orientation == "horizontal" ? "width" : "height"] = buttonSize + "px"; this.setPos(this.pos, sizeChanged); }; function SimpleScrollbars(cls, place, scroll) { this.addClass = cls; this.horiz = new Bar(cls, "horizontal", scroll); place(this.horiz.node); this.vert = new Bar(cls, "vertical", scroll); place(this.vert.node); this.width = null; } SimpleScrollbars.prototype.update = function(measure) { if (this.width == null) { var style = window.getComputedStyle ? window.getComputedStyle(this.horiz.node) : this.horiz.node.currentStyle; if (style) this.width = parseInt(style.height); } var width = this.width || 0; var needsH = measure.scrollWidth > measure.clientWidth + 1; var needsV = measure.scrollHeight > measure.clientHeight + 1; this.vert.node.style.display = needsV ? "block" : "none"; this.horiz.node.style.display = needsH ? "block" : "none"; if (needsV) { this.vert.update(measure.scrollHeight, measure.clientHeight, measure.viewHeight - (needsH ? width : 0)); this.vert.node.style.bottom = needsH ? width + "px" : "0"; } if (needsH) { this.horiz.update(measure.scrollWidth, measure.clientWidth, measure.viewWidth - (needsV ? width : 0) - measure.barLeft); this.horiz.node.style.right = needsV ? width + "px" : "0"; this.horiz.node.style.left = measure.barLeft + "px"; } return {right: needsV ? width : 0, bottom: needsH ? width : 0}; }; SimpleScrollbars.prototype.setScrollTop = function(pos) { this.vert.setPos(pos); }; SimpleScrollbars.prototype.setScrollLeft = function(pos) { this.horiz.setPos(pos); }; SimpleScrollbars.prototype.clear = function() { var parent = this.horiz.node.parentNode; parent.removeChild(this.horiz.node); parent.removeChild(this.vert.node); }; CodeMirror.scrollbarModel.simple = function(place, scroll) { return new SimpleScrollbars("CodeMirror-simplescroll", place, scroll); }; CodeMirror.scrollbarModel.overlay = function(place, scroll) { return new SimpleScrollbars("CodeMirror-overlayscroll", place, scroll); }; }); // CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: http://codemirror.net/LICENSE (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS mod(require("../../lib/codemirror"), require("./searchcursor"), require("../scroll/annotatescrollbar")); else if (typeof define == "function" && define.amd) // AMD define(["../../lib/codemirror", "./searchcursor", "../scroll/annotatescrollbar"], mod); else // Plain browser env mod(CodeMirror); })(function(CodeMirror) { "use strict"; CodeMirror.defineExtension("showMatchesOnScrollbar", function(query, caseFold, options) { if (typeof options == "string") options = {className: options}; if (!options) options = {}; return new SearchAnnotation(this, query, caseFold, options); }); function SearchAnnotation(cm, query, caseFold, options) { this.cm = cm; this.options = options; var annotateOptions = {listenForChanges: false}; for (var prop in options) annotateOptions[prop] = options[prop]; if (!annotateOptions.className) annotateOptions.className = "CodeMirror-search-match"; this.annotation = cm.annotateScrollbar(annotateOptions); this.query = query; this.caseFold = caseFold; this.gap = {from: cm.firstLine(), to: cm.lastLine() + 1}; this.matches = []; this.update = null; this.findMatches(); this.annotation.update(this.matches); var self = this; cm.on("change", this.changeHandler = function(_cm, change) { self.onChange(change); }); } var MAX_MATCHES = 1000; SearchAnnotation.prototype.findMatches = function() { if (!this.gap) return; for (var i = 0; i < this.matches.length; i++) { var match = this.matches[i]; if (match.from.line >= this.gap.to) break; if (match.to.line >= this.gap.from) this.matches.splice(i--, 1); } var cursor = this.cm.getSearchCursor(this.query, CodeMirror.Pos(this.gap.from, 0), this.caseFold); var maxMatches = this.options && this.options.maxMatches || MAX_MATCHES; while (cursor.findNext()) { var match = {from: cursor.from(), to: cursor.to()}; if (match.from.line >= this.gap.to) break; this.matches.splice(i++, 0, match); if (this.matches.length > maxMatches) break; } this.gap = null; }; function offsetLine(line, changeStart, sizeChange) { if (line <= changeStart) return line; return Math.max(changeStart, line + sizeChange); } SearchAnnotation.prototype.onChange = function(change) { var startLine = change.from.line; var endLine = CodeMirror.changeEnd(change).line; var sizeChange = endLine - change.to.line; if (this.gap) { this.gap.from = Math.min(offsetLine(this.gap.from, startLine, sizeChange), change.from.line); this.gap.to = Math.max(offsetLine(this.gap.to, startLine, sizeChange), change.from.line); } else { this.gap = {from: change.from.line, to: endLine + 1}; } if (sizeChange) for (var i = 0; i < this.matches.length; i++) { var match = this.matches[i]; var newFrom = offsetLine(match.from.line, startLine, sizeChange); if (newFrom != match.from.line) match.from = CodeMirror.Pos(newFrom, match.from.ch); var newTo = offsetLine(match.to.line, startLine, sizeChange); if (newTo != match.to.line) match.to = CodeMirror.Pos(newTo, match.to.ch); } clearTimeout(this.update); var self = this; this.update = setTimeout(function() { self.updateAfterChange(); }, 250); }; SearchAnnotation.prototype.updateAfterChange = function() { this.findMatches(); this.annotation.update(this.matches); }; SearchAnnotation.prototype.clear = function() { this.cm.off("change", this.changeHandler); this.annotation.clear(); }; }); // CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: http://codemirror.net/LICENSE // Highlighting text that matches the selection // // Defines an option highlightSelectionMatches, which, when enabled, // will style strings that match the selection throughout the // document. // // The option can be set to true to simply enable it, or to a // {minChars, style, wordsOnly, showToken, delay} object to explicitly // configure it. minChars is the minimum amount of characters that should be // selected for the behavior to occur, and style is the token style to // apply to the matches. This will be prefixed by "cm-" to create an // actual CSS class name. If wordsOnly is enabled, the matches will be // highlighted only if the selected text is a word. showToken, when enabled, // will cause the current token to be highlighted when nothing is selected. // delay is used to specify how much time to wait, in milliseconds, before // highlighting the matches. If annotateScrollbar is enabled, the occurences // will be highlighted on the scrollbar via the matchesonscrollbar addon. (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS mod(require("../../lib/codemirror"), require("./matchesonscrollbar")); else if (typeof define == "function" && define.amd) // AMD define(["../../lib/codemirror", "./matchesonscrollbar"], mod); else // Plain browser env mod(CodeMirror); })(function(CodeMirror) { "use strict"; var defaults = { style: "matchhighlight", minChars: 2, delay: 100, wordsOnly: false, annotateScrollbar: false, showToken: false, trim: true } function State(options) { this.options = {} for (var name in defaults) this.options[name] = (options && options.hasOwnProperty(name) ? options : defaults)[name] this.overlay = this.timeout = null; this.matchesonscroll = null; this.active = false; } CodeMirror.defineOption("highlightSelectionMatches", false, function(cm, val, old) { if (old && old != CodeMirror.Init) { removeOverlay(cm); clearTimeout(cm.state.matchHighlighter.timeout); cm.state.matchHighlighter = null; cm.off("cursorActivity", cursorActivity); cm.off("focus", onFocus) } if (val) { var state = cm.state.matchHighlighter = new State(val); if (cm.hasFocus()) { state.active = true highlightMatches(cm) } else { cm.on("focus", onFocus) } cm.on("cursorActivity", cursorActivity); } }); function cursorActivity(cm) { var state = cm.state.matchHighlighter; if (state.active || cm.hasFocus()) scheduleHighlight(cm, state) } function onFocus(cm) { var state = cm.state.matchHighlighter if (!state.active) { state.active = true scheduleHighlight(cm, state) } } function scheduleHighlight(cm, state) { clearTimeout(state.timeout); state.timeout = setTimeout(function() {highlightMatches(cm);}, state.options.delay); } function addOverlay(cm, query, hasBoundary, style) { var state = cm.state.matchHighlighter; cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style)); if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) { var searchFor = hasBoundary ? new RegExp("\\b" + query + "\\b") : query; state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false, {className: "CodeMirror-selection-highlight-scrollbar"}); } } function removeOverlay(cm) { var state = cm.state.matchHighlighter; if (state.overlay) { cm.removeOverlay(state.overlay); state.overlay = null; if (state.matchesonscroll) { state.matchesonscroll.clear(); state.matchesonscroll = null; } } } function highlightMatches(cm) { cm.operation(function() { var state = cm.state.matchHighlighter; removeOverlay(cm); if (!cm.somethingSelected() && state.options.showToken) { var re = state.options.showToken === true ? /[\w$]/ : state.options.showToken; var cur = cm.getCursor(), line = cm.getLine(cur.line), start = cur.ch, end = start; while (start && re.test(line.charAt(start - 1))) --start; while (end < line.length && re.test(line.charAt(end))) ++end; if (start < end) addOverlay(cm, line.slice(start, end), re, state.options.style); return; } var from = cm.getCursor("from"), to = cm.getCursor("to"); if (from.line != to.line) return; if (state.options.wordsOnly && !isWord(cm, from, to)) return; var selection = cm.getRange(from, to) if (state.options.trim) selection = selection.replace(/^\s+|\s+$/g, "") if (selection.length >= state.options.minChars) addOverlay(cm, selection, false, state.options.style); }); } function isWord(cm, from, to) { var str = cm.getRange(from, to); if (str.match(/^\w+$/) !== null) { if (from.ch > 0) { var pos = {line: from.line, ch: from.ch - 1}; var chr = cm.getRange(pos, from); if (chr.match(/\W/) === null) return false; } if (to.ch < cm.getLine(from.line).length) { var pos = {line: to.line, ch: to.ch + 1}; var chr = cm.getRange(to, pos); if (chr.match(/\W/) === null) return false; } return true; } else return false; } function boundariesAround(stream, re) { return (!stream.start || !re.test(stream.string.charAt(stream.start - 1))) && (stream.pos == stream.string.length || !re.test(stream.string.charAt(stream.pos))); } function makeOverlay(query, hasBoundary, style) { return {token: function(stream) { if (stream.match(query) && (!hasBoundary || boundariesAround(stream, hasBoundary))) return style; stream.next(); stream.skipTo(query.charAt(0)) || stream.skipToEnd(); }}; } }); // CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: http://codemirror.net/LICENSE (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS mod(require("../../lib/codemirror")) else if (typeof define == "function" && define.amd) // AMD define(["../../lib/codemirror"], mod) else // Plain browser env mod(CodeMirror) })(function(CodeMirror) { "use strict" var Pos = CodeMirror.Pos function regexpFlags(regexp) { var flags = regexp.flags return flags != null ? flags : (regexp.ignoreCase ? "i" : "") + (regexp.global ? "g" : "") + (regexp.multiline ? "m" : "") } function ensureGlobal(regexp) { return regexp.global ? regexp : new RegExp(regexp.source, regexpFlags(regexp) + "g") } function maybeMultiline(regexp) { return /\\s|\\n|\n|\\W|\\D|\[\^/.test(regexp.source) } function searchRegexpForward(doc, regexp, start) { regexp = ensureGlobal(regexp) for (var line = start.line, ch = start.ch, last = doc.lastLine(); line <= last; line++, ch = 0) { regexp.lastIndex = ch var string = doc.getLine(line), match = regexp.exec(string) if (match) return {from: Pos(line, match.index), to: Pos(line, match.index + match[0].length), match: match} } } function searchRegexpForwardMultiline(doc, regexp, start) { if (!maybeMultiline(regexp)) return searchRegexpForward(doc, regexp, start) regexp = ensureGlobal(regexp) var string, chunk = 1 for (var line = start.line, last = doc.lastLine(); line <= last;) { // This grows the search buffer in exponentially-sized chunks // between matches, so that nearby matches are fast and don't // require concatenating the whole document (in case we're // searching for something that has tons of matches), but at the // same time, the amount of retries is limited. for (var i = 0; i < chunk; i++) { var curLine = doc.getLine(line++) string = string == null ? curLine : string + "\n" + curLine } chunk = chunk * 2 regexp.lastIndex = start.ch var match = regexp.exec(string) if (match) { var before = string.slice(0, match.index).split("\n"), inside = match[0].split("\n") var startLine = start.line + before.length - 1, startCh = before[before.length - 1].length return {from: Pos(startLine, startCh), to: Pos(startLine + inside.length - 1, inside.length == 1 ? startCh + inside[0].length : inside[inside.length - 1].length), match: match} } } } function lastMatchIn(string, regexp) { var cutOff = 0, match for (;;) { regexp.lastIndex = cutOff var newMatch = regexp.exec(string) if (!newMatch) return match match = newMatch cutOff = match.index + (match[0].length || 1) if (cutOff == string.length) return match } } function searchRegexpBackward(doc, regexp, start) { regexp = ensureGlobal(regexp) for (var line = start.line, ch = start.ch, first = doc.firstLine(); line >= first; line--, ch = -1) { var string = doc.getLine(line) if (ch > -1) string = string.slice(0, ch) var match = lastMatchIn(string, regexp) if (match) return {from: Pos(line, match.index), to: Pos(line, match.index + match[0].length), match: match} } } function searchRegexpBackwardMultiline(doc, regexp, start) { regexp = ensureGlobal(regexp) var string, chunk = 1 for (var line = start.line, first = doc.firstLine(); line >= first;) { for (var i = 0; i < chunk; i++) { var curLine = doc.getLine(line--) string = string == null ? curLine.slice(0, start.ch) : curLine + "\n" + string } chunk *= 2 var match = lastMatchIn(string, regexp) if (match) { var before = string.slice(0, match.index).split("\n"), inside = match[0].split("\n") var startLine = line + before.length, startCh = before[before.length - 1].length return {from: Pos(startLine, startCh), to: Pos(startLine + inside.length - 1, inside.length == 1 ? startCh + inside[0].length : inside[inside.length - 1].length), match: match} } } } var doFold, noFold if (String.prototype.normalize) { doFold = function(str) { return str.normalize("NFD").toLowerCase() } noFold = function(str) { return str.normalize("NFD") } } else { doFold = function(str) { return str.toLowerCase() } noFold = function(str) { return str } } // Maps a position in a case-folded line back to a position in the original line // (compensating for codepoints increasing in number during folding) function adjustPos(orig, folded, pos, foldFunc) { if (orig.length == folded.length) return pos for (var min = 0, max = pos + Math.max(0, orig.length - folded.length);;) { if (min == max) return min var mid = (min + max) >> 1 var len = foldFunc(orig.slice(0, mid)).length if (len == pos) return mid else if (len > pos) max = mid else min = mid + 1 } } function searchStringForward(doc, query, start, caseFold) { // Empty string would match anything and never progress, so we // define it to match nothing instead. if (!query.length) return null var fold = caseFold ? doFold : noFold var lines = fold(query).split(/\r|\n\r?/) search: for (var line = start.line, ch = start.ch, last = doc.lastLine() + 1 - lines.length; line <= last; line++, ch = 0) { var orig = doc.getLine(line).slice(ch), string = fold(orig) if (lines.length == 1) { var found = string.indexOf(lines[0]) if (found == -1) continue search var start = adjustPos(orig, string, found, fold) + ch return {from: Pos(line, adjustPos(orig, string, found, fold) + ch), to: Pos(line, adjustPos(orig, string, found + lines[0].length, fold) + ch)} } else { var cutFrom = string.length - lines[0].length if (string.slice(cutFrom) != lines[0]) continue search for (var i = 1; i < lines.length - 1; i++) if (fold(doc.getLine(line + i)) != lines[i]) continue search var end = doc.getLine(line + lines.length - 1), endString = fold(end), lastLine = lines[lines.length - 1] if (endString.slice(0, lastLine.length) != lastLine) continue search return {from: Pos(line, adjustPos(orig, string, cutFrom, fold) + ch), to: Pos(line + lines.length - 1, adjustPos(end, endString, lastLine.length, fold))} } } } function searchStringBackward(doc, query, start, caseFold) { if (!query.length) return null var fold = caseFold ? doFold : noFold var lines = fold(query).split(/\r|\n\r?/) search: for (var line = start.line, ch = start.ch, first = doc.firstLine() - 1 + lines.length; line >= first; line--, ch = -1) { var orig = doc.getLine(line) if (ch > -1) orig = orig.slice(0, ch) var string = fold(orig) if (lines.length == 1) { var found = string.lastIndexOf(lines[0]) if (found == -1) continue search return {from: Pos(line, adjustPos(orig, string, found, fold)), to: Pos(line, adjustPos(orig, string, found + lines[0].length, fold))} } else { var lastLine = lines[lines.length - 1] if (string.slice(0, lastLine.length) != lastLine) continue search for (var i = 1, start = line - lines.length + 1; i < lines.length - 1; i++) if (fold(doc.getLine(start + i)) != lines[i]) continue search var top = doc.getLine(line + 1 - lines.length), topString = fold(top) if (topString.slice(topString.length - lines[0].length) != lines[0]) continue search return {from: Pos(line + 1 - lines.length, adjustPos(top, topString, top.length - lines[0].length, fold)), to: Pos(line, adjustPos(orig, string, lastLine.length, fold))} } } } function SearchCursor(doc, query, pos, options) { this.atOccurrence = false this.doc = doc pos = pos ? doc.clipPos(pos) : Pos(0, 0) this.pos = {from: pos, to: pos} var caseFold if (typeof options == "object") { caseFold = options.caseFold } else { // Backwards compat for when caseFold was the 4th argument caseFold = options options = null } if (typeof query == "string") { if (caseFold == null) caseFold = false this.matches = function(reverse, pos) { return (reverse ? searchStringBackward : searchStringForward)(doc, query, pos, caseFold) } } else { query = ensureGlobal(query) if (!options || options.multiline !== false) this.matches = function(reverse, pos) { return (reverse ? searchRegexpBackwardMultiline : searchRegexpForwardMultiline)(doc, query, pos) } else this.matches = function(reverse, pos) { return (reverse ? searchRegexpBackward : searchRegexpForward)(doc, query, pos) } } } SearchCursor.prototype = { findNext: function() {return this.find(false)}, findPrevious: function() {return this.find(true)}, find: function(reverse) { var result = this.matches(reverse, this.doc.clipPos(reverse ? this.pos.from : this.pos.to)) // Implements weird auto-growing behavior on null-matches for // backwards-compatiblity with the vim code (unfortunately) while (result && CodeMirror.cmpPos(result.from, result.to) == 0) { if (reverse) { if (result.from.ch) result.from = Pos(result.from.line, result.from.ch - 1) else if (result.from.line == this.doc.firstLine()) result = null else result = this.matches(reverse, this.doc.clipPos(Pos(result.from.line - 1))) } else { if (result.to.ch < this.doc.getLine(result.to.line).length) result.to = Pos(result.to.line, result.to.ch + 1) else if (result.to.line == this.doc.lastLine()) result = null else result = this.matches(reverse, Pos(result.to.line + 1, 0)) } } if (result) { this.pos = result this.atOccurrence = true return this.pos.match || true } else { var end = Pos(reverse ? this.doc.firstLine() : this.doc.lastLine() + 1, 0) this.pos = {from: end, to: end} return this.atOccurrence = false } }, from: function() {if (this.atOccurrence) return this.pos.from}, to: function() {if (this.atOccurrence) return this.pos.to}, replace: function(newText, origin) { if (!this.atOccurrence) return var lines = CodeMirror.splitLines(newText) this.doc.replaceRange(lines, this.pos.from, this.pos.to, origin) 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) }) CodeMirror.defineExtension("selectMatches", function(query, caseFold) { var ranges = [] var cur = this.getSearchCursor(query, this.getCursor("from"), caseFold) while (cur.findNext()) { if (CodeMirror.cmpPos(cur.to(), this.getCursor("to")) > 0) break ranges.push({anchor: cur.from(), head: cur.to()}) } if (ranges.length) this.setSelections(ranges, 0) }) }); // CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: http://codemirror.net/LICENSE (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS mod(require("../../lib/codemirror")); else if (typeof define == "function" && define.amd) // AMD define(["../../lib/codemirror"], mod); else // Plain browser env mod(CodeMirror); })(function(CodeMirror) { "use strict"; var WRAP_CLASS = "CodeMirror-activeline"; var BACK_CLASS = "CodeMirror-activeline-background"; var GUTT_CLASS = "CodeMirror-activeline-gutter"; CodeMirror.defineOption("styleActiveLine", false, function(cm, val, old) { var prev = old == CodeMirror.Init ? false : old; if (val == prev) return if (prev) { cm.off("beforeSelectionChange", selectionChange); clearActiveLines(cm); delete cm.state.activeLines; } if (val) { cm.state.activeLines = []; updateActiveLines(cm, cm.listSelections()); cm.on("beforeSelectionChange", selectionChange); } }); function clearActiveLines(cm) { for (var i = 0; i < cm.state.activeLines.length; i++) { cm.removeLineClass(cm.state.activeLines[i], "wrap", WRAP_CLASS); cm.removeLineClass(cm.state.activeLines[i], "background", BACK_CLASS); cm.removeLineClass(cm.state.activeLines[i], "gutter", GUTT_CLASS); } } function sameArray(a, b) { if (a.length != b.length) return false; for (var i = 0; i < a.length; i++) if (a[i] != b[i]) return false; return true; } function updateActiveLines(cm, ranges) { var active = []; for (var i = 0; i < ranges.length; i++) { var range = ranges[i]; var option = cm.getOption("styleActiveLine"); if (typeof option == "object" && option.nonEmpty ? range.anchor.line != range.head.line : !range.empty()) continue var line = cm.getLineHandleVisualStart(range.head.line); if (active[active.length - 1] != line) active.push(line); } if (sameArray(cm.state.activeLines, active)) return; cm.operation(function() { clearActiveLines(cm); for (var i = 0; i < active.length; i++) { cm.addLineClass(active[i], "wrap", WRAP_CLASS); cm.addLineClass(active[i], "background", BACK_CLASS); cm.addLineClass(active[i], "gutter", GUTT_CLASS); } cm.state.activeLines = active; }); } function selectionChange(cm, sel) { updateActiveLines(cm, sel.ranges); } }); // CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: http://codemirror.net/LICENSE /** * Supported keybindings: * Too many to list. Refer to defaultKeyMap below. * * Supported Ex commands: * Refer to defaultExCommandMap below. * * Registers: unnamed, -, a-z, A-Z, 0-9 * (Does not respect the special case for number registers when delete * operator is made with these commands: %, (, ), , /, ?, n, N, {, } ) * TODO: Implement the remaining registers. * * Marks: a-z, A-Z, and 0-9 * TODO: Implement the remaining special marks. They have more complex * behavior. * * Events: * 'vim-mode-change' - raised on the editor anytime the current mode changes, * Event object: {mode: "visual", subMode: "linewise"} * * Code structure: * 1. Default keymap * 2. Variable declarations and short basic helpers * 3. Instance (External API) implementation * 4. Internal state tracking objects (input state, counter) implementation * and instantiation * 5. Key handler (the main command dispatcher) implementation * 6. Motion, operator, and action implementations * 7. Helper functions for the key handler, motions, operators, and actions * 8. Set up Vim to work as a keymap for CodeMirror. * 9. Ex command implementations. */ (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS mod(require("../lib/codemirror"), require("../addon/search/searchcursor"), require("../addon/dialog/dialog"), require("../addon/edit/matchbrackets.js")); else if (typeof define == "function" && define.amd) // AMD define(["../lib/codemirror", "../addon/search/searchcursor", "../addon/dialog/dialog", "../addon/edit/matchbrackets"], mod); else // Plain browser env mod(CodeMirror); })(function(CodeMirror) { 'use strict'; var defaultKeymap = [ // Key to key mapping. This goes first to make it possible to override // existing mappings. { keys: '<Left>', type: 'keyToKey', toKeys: 'h' }, { keys: '<Right>', type: 'keyToKey', toKeys: 'l' }, { keys: '<Up>', type: 'keyToKey', toKeys: 'k' }, { keys: '<Down>', type: 'keyToKey', toKeys: 'j' }, { keys: '<Space>', type: 'keyToKey', toKeys: 'l' }, { keys: '<BS>', type: 'keyToKey', toKeys: 'h', context: 'normal'}, { keys: '<C-Space>', type: 'keyToKey', toKeys: 'W' }, { keys: '<C-BS>', type: 'keyToKey', toKeys: 'B', context: 'normal' }, { keys: '<S-Space>', type: 'keyToKey', toKeys: 'w' }, { keys: '<S-BS>', type: 'keyToKey', toKeys: 'b', context: 'normal' }, { keys: '<C-n>', type: 'keyToKey', toKeys: 'j' }, { keys: '<C-p>', type: 'keyToKey', toKeys: 'k' }, { keys: '<C-[>', type: 'keyToKey', toKeys: '<Esc>' }, { keys: '<C-c>', type: 'keyToKey', toKeys: '<Esc>' }, { keys: '<C-[>', type: 'keyToKey', toKeys: '<Esc>', context: 'insert' }, { keys: '<C-c>', type: 'keyToKey', toKeys: '<Esc>', context: 'insert' }, { keys: 's', type: 'keyToKey', toKeys: 'cl', context: 'normal' }, { keys: 's', type: 'keyToKey', toKeys: 'c', context: 'visual'}, { keys: 'S', type: 'keyToKey', toKeys: 'cc', context: 'normal' }, { keys: 'S', type: 'keyToKey', toKeys: 'VdO', context: 'visual' }, { keys: '<Home>', type: 'keyToKey', toKeys: '0' }, { keys: '<End>', type: 'keyToKey', toKeys: '$' }, { keys: '<PageUp>', type: 'keyToKey', toKeys: '<C-b>' }, { keys: '<PageDown>', type: 'keyToKey', toKeys: '<C-f>' }, { keys: '<CR>', type: 'keyToKey', toKeys: 'j^', context: 'normal' }, { keys: '<Ins>', type: 'action', action: 'toggleOverwrite', context: 'insert' }, // Motions { keys: 'H', type: 'motion', motion: 'moveToTopLine', motionArgs: { linewise: true, toJumplist: true }}, { keys: 'M', type: 'motion', motion: 'moveToMiddleLine', motionArgs: { linewise: true, toJumplist: true }}, { keys: 'L', type: 'motion', motion: 'moveToBottomLine', motionArgs: { linewise: true, toJumplist: true }}, { keys: 'h', type: 'motion', motion: 'moveByCharacters', motionArgs: { forward: false }}, { keys: 'l', type: 'motion', motion: 'moveByCharacters', motionArgs: { forward: true }}, { keys: 'j', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, linewise: true }}, { keys: 'k', type: 'motion', motion: 'moveByLines', motionArgs: { forward: false, linewise: true }}, { keys: 'gj', type: 'motion', motion: 'moveByDisplayLines', motionArgs: { forward: true }}, { keys: 'gk', type: 'motion', motion: 'moveByDisplayLines', motionArgs: { forward: false }}, { keys: 'w', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: false }}, { keys: 'W', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: false, bigWord: true }}, { keys: 'e', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: true, inclusive: true }}, { keys: 'E', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: true, bigWord: true, inclusive: true }}, { keys: 'b', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false }}, { keys: 'B', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false, bigWord: true }}, { keys: 'ge', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: true, inclusive: true }}, { keys: 'gE', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: true, bigWord: true, inclusive: true }}, { keys: '{', type: 'motion', motion: 'moveByParagraph', motionArgs: { forward: false, toJumplist: true }}, { keys: '}', type: 'motion', motion: 'moveByParagraph', motionArgs: { forward: true, toJumplist: true }}, { keys: '<C-f>', type: 'motion', motion: 'moveByPage', motionArgs: { forward: true }}, { keys: '<C-b>', type: 'motion', motion: 'moveByPage', motionArgs: { forward: false }}, { keys: '<C-d>', type: 'motion', motion: 'moveByScroll', motionArgs: { forward: true, explicitRepeat: true }}, { keys: '<C-u>', type: 'motion', motion: 'moveByScroll', motionArgs: { forward: false, explicitRepeat: true }}, { keys: 'gg', type: 'motion', motion: 'moveToLineOrEdgeOfDocument', motionArgs: { forward: false, explicitRepeat: true, linewise: true, toJumplist: true }}, { keys: 'G', type: 'motion', motion: 'moveToLineOrEdgeOfDocument', motionArgs: { forward: true, explicitRepeat: true, linewise: true, toJumplist: true }}, { keys: '0', type: 'motion', motion: 'moveToStartOfLine' }, { keys: '^', type: 'motion', motion: 'moveToFirstNonWhiteSpaceCharacter' }, { keys: '+', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, toFirstChar:true }}, { keys: '-', type: 'motion', motion: 'moveByLines', motionArgs: { forward: false, toFirstChar:true }}, { keys: '_', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, toFirstChar:true, repeatOffset:-1 }}, { keys: '$', type: 'motion', motion: 'moveToEol', motionArgs: { inclusive: true }}, { keys: '%', type: 'motion', motion: 'moveToMatchedSymbol', motionArgs: { inclusive: true, toJumplist: true }}, { keys: 'f<character>', type: 'motion', motion: 'moveToCharacter', motionArgs: { forward: true , inclusive: true }}, { keys: 'F<character>', type: 'motion', motion: 'moveToCharacter', motionArgs: { forward: false }}, { keys: 't<character>', type: 'motion', motion: 'moveTillCharacter', motionArgs: { forward: true, inclusive: true }}, { keys: 'T<character>', type: 'motion', motion: 'moveTillCharacter', motionArgs: { forward: false }}, { keys: ';', type: 'motion', motion: 'repeatLastCharacterSearch', motionArgs: { forward: true }}, { keys: ',', type: 'motion', motion: 'repeatLastCharacterSearch', motionArgs: { forward: false }}, { keys: '\'<character>', type: 'motion', motion: 'goToMark', motionArgs: {toJumplist: true, linewise: true}}, { keys: '`<character>', type: 'motion', motion: 'goToMark', motionArgs: {toJumplist: true}}, { keys: ']`', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true } }, { keys: '[`', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false } }, { keys: ']\'', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true, linewise: true } }, { keys: '[\'', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false, linewise: true } }, // the next two aren't motions but must come before more general motion declarations { keys: ']p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: true, isEdit: true, matchIndent: true}}, { keys: '[p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: false, isEdit: true, matchIndent: true}}, { keys: ']<character>', type: 'motion', motion: 'moveToSymbol', motionArgs: { forward: true, toJumplist: true}}, { keys: '[<character>', type: 'motion', motion: 'moveToSymbol', motionArgs: { forward: false, toJumplist: true}}, { keys: '|', type: 'motion', motion: 'moveToColumn'}, { keys: 'o', type: 'motion', motion: 'moveToOtherHighlightedEnd', context:'visual'}, { keys: 'O', type: 'motion', motion: 'moveToOtherHighlightedEnd', motionArgs: {sameLine: true}, context:'visual'}, // Operators { keys: 'd', type: 'operator', operator: 'delete' }, { keys: 'y', type: 'operator', operator: 'yank' }, { keys: 'c', type: 'operator', operator: 'change' }, { keys: '>', type: 'operator', operator: 'indent', operatorArgs: { indentRight: true }}, { keys: '<', type: 'operator', operator: 'indent', operatorArgs: { indentRight: false }}, { keys: 'g~', type: 'operator', operator: 'changeCase' }, { keys: 'gu', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: true}, isEdit: true }, { keys: 'gU', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: false}, isEdit: true }, { keys: 'n', type: 'motion', motion: 'findNext', motionArgs: { forward: true, toJumplist: true }}, { keys: 'N', type: 'motion', motion: 'findNext', motionArgs: { forward: false, toJumplist: true }}, // Operator-Motion dual commands { keys: 'x', type: 'operatorMotion', operator: 'delete', motion: 'moveByCharacters', motionArgs: { forward: true }, operatorMotionArgs: { visualLine: false }}, { keys: 'X', type: 'operatorMotion', operator: 'delete', motion: 'moveByCharacters', motionArgs: { forward: false }, operatorMotionArgs: { visualLine: true }}, { keys: 'D', type: 'operatorMotion', operator: 'delete', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'}, { keys: 'D', type: 'operator', operator: 'delete', operatorArgs: { linewise: true }, context: 'visual'}, { keys: 'Y', type: 'operatorMotion', operator: 'yank', motion: 'expandToLine', motionArgs: { linewise: true }, context: 'normal'}, { keys: 'Y', type: 'operator', operator: 'yank', operatorArgs: { linewise: true }, context: 'visual'}, { keys: 'C', type: 'operatorMotion', operator: 'change', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'}, { keys: 'C', type: 'operator', operator: 'change', operatorArgs: { linewise: true }, context: 'visual'}, { keys: '~', type: 'operatorMotion', operator: 'changeCase', motion: 'moveByCharacters', motionArgs: { forward: true }, operatorArgs: { shouldMoveCursor: true }, context: 'normal'}, { keys: '~', type: 'operator', operator: 'changeCase', context: 'visual'}, { keys: '<C-w>', type: 'operatorMotion', operator: 'delete', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false }, context: 'insert' }, // Actions { keys: '<C-i>', type: 'action', action: 'jumpListWalk', actionArgs: { forward: true }}, { keys: '<C-o>', type: 'action', action: 'jumpListWalk', actionArgs: { forward: false }}, { keys: '<C-e>', type: 'action', action: 'scroll', actionArgs: { forward: true, linewise: true }}, { keys: '<C-y>', type: 'action', action: 'scroll', actionArgs: { forward: false, linewise: true }}, { keys: 'a', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'charAfter' }, context: 'normal' }, { keys: 'A', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'eol' }, context: 'normal' }, { keys: 'A', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'endOfSelectedArea' }, context: 'visual' }, { keys: 'i', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'inplace' }, context: 'normal' }, { keys: 'I', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'firstNonBlank'}, context: 'normal' }, { keys: 'I', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'startOfSelectedArea' }, context: 'visual' }, { keys: 'o', type: 'action', action: 'newLineAndEnterInsertMode', isEdit: true, interlaceInsertRepeat: true, actionArgs: { after: true }, context: 'normal' }, { keys: 'O', type: 'action', action: 'newLineAndEnterInsertMode', isEdit: true, interlaceInsertRepeat: true, actionArgs: { after: false }, context: 'normal' }, { keys: 'v', type: 'action', action: 'toggleVisualMode' }, { keys: 'V', type: 'action', action: 'toggleVisualMode', actionArgs: { linewise: true }}, { keys: '<C-v>', type: 'action', action: 'toggleVisualMode', actionArgs: { blockwise: true }}, { keys: '<C-q>', type: 'action', action: 'toggleVisualMode', actionArgs: { blockwise: true }}, { keys: 'gv', type: 'action', action: 'reselectLastSelection' }, { keys: 'J', type: 'action', action: 'joinLines', isEdit: true }, { keys: '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', context: 'normal' }, { keys: 'u', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: true}, context: 'visual', isEdit: true }, { keys: 'U', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: false}, context: 'visual', isEdit: true }, { keys: '<C-r>', type: 'action', action: 'redo' }, { keys: 'm<character>', type: 'action', action: 'setMark' }, { keys: '"<character>', type: 'action', action: 'setRegister' }, { keys: 'zz', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'center' }}, { keys: 'z.', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'center' }, motion: 'moveToFirstNonWhiteSpaceCharacter' }, { keys: 'zt', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'top' }}, { keys: 'z<CR>', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'top' }, motion: 'moveToFirstNonWhiteSpaceCharacter' }, { keys: 'z-', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'bottom' }}, { keys: 'zb', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'bottom' }, motion: 'moveToFirstNonWhiteSpaceCharacter' }, { keys: '.', type: 'action', action: 'repeatLastEdit' }, { keys: '<C-a>', type: 'action', action: 'incrementNumberToken', isEdit: true, actionArgs: {increase: true, backtrack: false}}, { keys: '<C-x>', type: 'action', action: 'incrementNumberToken', isEdit: true, actionArgs: {increase: false, backtrack: false}}, { keys: '<C-t>', type: 'action', action: 'indent', actionArgs: { indentRight: true }, context: 'insert' }, { keys: '<C-d>', type: 'action', action: 'indent', actionArgs: { indentRight: false }, context: 'insert' }, // Text object motions { keys: 'a<character>', type: 'motion', motion: 'textObjectManipulation' }, { keys: 'i<character>', type: 'motion', motion: 'textObjectManipulation', motionArgs: { textObjectInner: true }}, // Search { keys: '/', type: 'search', searchArgs: { forward: true, querySrc: 'prompt', toJumplist: true }}, { keys: '?', type: 'search', searchArgs: { forward: false, querySrc: 'prompt', toJumplist: true }}, { keys: '*', type: 'search', searchArgs: { forward: true, querySrc: 'wordUnderCursor', wholeWordOnly: true, toJumplist: true }}, { keys: '#', type: 'search', searchArgs: { forward: false, querySrc: 'wordUnderCursor', wholeWordOnly: true, toJumplist: true }}, { keys: 'g*', type: 'search', searchArgs: { forward: true, querySrc: 'wordUnderCursor', toJumplist: true }}, { keys: 'g#', type: 'search', searchArgs: { forward: false, querySrc: 'wordUnderCursor', toJumplist: true }}, // Ex command { keys: ':', type: 'ex' } ]; /** * Ex commands * Care must be taken when adding to the default Ex command map. For any * pair of commands that have a shared prefix, at least one of their * shortNames must not match the prefix of the other command. */ var defaultExCommandMap = [ { name: 'colorscheme', shortName: 'colo' }, { name: 'map' }, { name: 'imap', shortName: 'im' }, { name: 'nmap', shortName: 'nm' }, { name: 'vmap', shortName: 'vm' }, { name: 'unmap' }, { name: 'write', shortName: 'w' }, { name: 'undo', shortName: 'u' }, { name: 'redo', shortName: 'red' }, { name: 'set', shortName: 'se' }, { name: 'set', shortName: 'se' }, { name: 'setlocal', shortName: 'setl' }, { name: 'setglobal', shortName: 'setg' }, { name: 'sort', shortName: 'sor' }, { name: 'substitute', shortName: 's', possiblyAsync: true }, { name: 'nohlsearch', shortName: 'noh' }, { name: 'yank', shortName: 'y' }, { name: 'delmarks', shortName: 'delm' }, { name: 'registers', shortName: 'reg', excludeFromCommandHistory: true }, { name: 'global', shortName: 'g' } ]; var Pos = CodeMirror.Pos; var Vim = function() { function enterVimMode(cm) { cm.setOption('disableInput', true); cm.setOption('showCursorWhenSelecting', false); CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"}); cm.on('cursorActivity', onCursorActivity); maybeInitVimState(cm); CodeMirror.on(cm.getInputField(), 'paste', getOnPasteFn(cm)); } function leaveVimMode(cm) { cm.setOption('disableInput', false); cm.off('cursorActivity', onCursorActivity); CodeMirror.off(cm.getInputField(), 'paste', getOnPasteFn(cm)); cm.state.vim = null; } function detachVimMap(cm, next) { if (this == CodeMirror.keyMap.vim) { CodeMirror.rmClass(cm.getWrapperElement(), "cm-fat-cursor"); if (cm.getOption("inputStyle") == "contenteditable" && document.body.style.caretColor != null) { disableFatCursorMark(cm); cm.getInputField().style.caretColor = ""; } } if (!next || next.attach != attachVimMap) leaveVimMode(cm); } function attachVimMap(cm, prev) { if (this == CodeMirror.keyMap.vim) { CodeMirror.addClass(cm.getWrapperElement(), "cm-fat-cursor"); if (cm.getOption("inputStyle") == "contenteditable" && document.body.style.caretColor != null) { enableFatCursorMark(cm); cm.getInputField().style.caretColor = "transparent"; } } if (!prev || prev.attach != attachVimMap) enterVimMode(cm); } function fatCursorMarks(cm) { var ranges = cm.listSelections(), result = [] for (var i = 0; i < ranges.length; i++) { var range = ranges[i] if (range.empty()) { if (range.anchor.ch < cm.getLine(range.anchor.line).length) { result.push(cm.markText(range.anchor, Pos(range.anchor.line, range.anchor.ch + 1), {className: "cm-fat-cursor-mark"})) } else { var widget = document.createElement("span") widget.textContent = "\u00a0" widget.className = "cm-fat-cursor-mark" result.push(cm.setBookmark(range.anchor, {widget: widget})) } } } return result } function updateFatCursorMark(cm) { var marks = cm.state.fatCursorMarks if (marks) for (var i = 0; i < marks.length; i++) marks[i].clear() cm.state.fatCursorMarks = fatCursorMarks(cm) } function enableFatCursorMark(cm) { cm.state.fatCursorMarks = fatCursorMarks(cm) cm.on("cursorActivity", updateFatCursorMark) } function disableFatCursorMark(cm) { var marks = cm.state.fatCursorMarks if (marks) for (var i = 0; i < marks.length; i++) marks[i].clear() cm.state.fatCursorMarks = null cm.off("cursorActivity", updateFatCursorMark) } // Deprecated, simply setting the keymap works again. CodeMirror.defineOption('vimMode', false, function(cm, val, prev) { if (val && cm.getOption("keyMap") != "vim") cm.setOption("keyMap", "vim"); else if (!val && prev != CodeMirror.Init && /^vim/.test(cm.getOption("keyMap"))) cm.setOption("keyMap", "default"); }); function cmKey(key, cm) { if (!cm) { return undefined; } if (this[key]) { return this[key]; } var vimKey = cmKeyToVimKey(key); if (!vimKey) { return false; } var cmd = CodeMirror.Vim.findKey(cm, vimKey); if (typeof cmd == 'function') { CodeMirror.signal(cm, 'vim-keypress', vimKey); } return cmd; } var modifiers = {'Shift': 'S', 'Ctrl': 'C', 'Alt': 'A', 'Cmd': 'D', 'Mod': 'A'}; var specialKeys = {Enter:'CR',Backspace:'BS',Delete:'Del',Insert:'Ins'}; function cmKeyToVimKey(key) { if (key.charAt(0) == '\'') { // Keypress character binding of format "'a'" return key.charAt(1); } var pieces = key.split(/-(?!$)/); var lastPiece = pieces[pieces.length - 1]; if (pieces.length == 1 && pieces[0].length == 1) { // No-modifier bindings use literal character bindings above. Skip. return false; } else if (pieces.length == 2 && pieces[0] == 'Shift' && lastPiece.length == 1) { // Ignore Shift+char bindings as they should be handled by literal character. return false; } var hasCharacter = false; for (var i = 0; i < pieces.length; i++) { var piece = pieces[i]; if (piece in modifiers) { pieces[i] = modifiers[piece]; } else { hasCharacter = true; } if (piece in specialKeys) { pieces[i] = specialKeys[piece]; } } if (!hasCharacter) { // Vim does not support modifier only keys. return false; } // TODO: Current bindings expect the character to be lower case, but // it looks like vim key notation uses upper case. if (isUpperCase(lastPiece)) { pieces[pieces.length - 1] = lastPiece.toLowerCase(); } return '<' + pieces.join('-') + '>'; } function getOnPasteFn(cm) { var vim = cm.state.vim; if (!vim.onPasteFn) { vim.onPasteFn = function() { if (!vim.insertMode) { cm.setCursor(offsetCursor(cm.getCursor(), 0, 1)); actions.enterInsertMode(cm, {}, vim); } }; } return vim.onPasteFn; } var numberRegex = /[\d]/; var wordCharTest = [CodeMirror.isWordChar, function(ch) { return ch && !CodeMirror.isWordChar(ch) && !/\s/.test(ch); }], bigWordCharTest = [function(ch) { return /\S/.test(ch); }]; function makeKeyRange(start, size) { var keys = []; for (var i = start; i < start + size; i++) { keys.push(String.fromCharCode(i)); } return keys; } var upperCaseAlphabet = makeKeyRange(65, 26); var lowerCaseAlphabet = makeKeyRange(97, 26); var numbers = makeKeyRange(48, 10); var validMarks = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['<', '>']); var validRegisters = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['-', '"', '.', ':', '/']); 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 options = {}; function defineOption(name, defaultValue, type, aliases, callback) { if (defaultValue === undefined && !callback) { throw Error('defaultValue is required unless callback is provided'); } if (!type) { type = 'string'; } options[name] = { type: type, defaultValue: defaultValue, callback: callback }; if (aliases) { for (var i = 0; i < aliases.length; i++) { options[aliases[i]] = options[name]; } } if (defaultValue) { setOption(name, defaultValue); } } function setOption(name, value, cm, cfg) { var option = options[name]; cfg = cfg || {}; var scope = cfg.scope; if (!option) { return new Error('Unknown option: ' + name); } if (option.type == 'boolean') { if (value && value !== true) { return new Error('Invalid argument: ' + name + '=' + value); } else if (value !== false) { // Boolean options are set to true if value is not defined. value = true; } } if (option.callback) { if (scope !== 'local') { option.callback(value, undefined); } if (scope !== 'global' && cm) { option.callback(value, cm); } } else { if (scope !== 'local') { option.value = option.type == 'boolean' ? !!value : value; } if (scope !== 'global' && cm) { cm.state.vim.options[name] = {value: value}; } } } function getOption(name, cm, cfg) { var option = options[name]; cfg = cfg || {}; var scope = cfg.scope; if (!option) { return new Error('Unknown option: ' + name); } if (option.callback) { var local = cm && option.callback(undefined, cm); if (scope !== 'global' && local !== undefined) { return local; } if (scope !== 'local') { return option.callback(); } return; } else { var local = (scope !== 'global') && (cm && cm.state.vim.options[name]); return (local || (scope !== 'local') && option || {}).value; } } defineOption('filetype', undefined, 'string', ['ft'], function(name, cm) { // Option is local. Do nothing for global. if (cm === undefined) { return; } // The 'filetype' option proxies to the CodeMirror 'mode' option. if (name === undefined) { var mode = cm.getOption('mode'); return mode == 'null' ? '' : mode; } else { var mode = name == '' ? 'null' : name; cm.setOption('mode', mode); } }); var createCircularJumpList = function() { var size = 100; var pointer = -1; var head = 0; var tail = 0; var buffer = new Array(size); function add(cm, oldCur, newCur) { var current = pointer % size; var curMark = buffer[current]; function useNextSlot(cursor) { var next = ++pointer % size; var trashMark = buffer[next]; if (trashMark) { trashMark.clear(); } buffer[next] = cm.setBookmark(cursor); } if (curMark) { var markPos = curMark.find(); // avoid recording redundant cursor position if (markPos && !cursorEqual(markPos, oldCur)) { useNextSlot(oldCur); } } else { useNextSlot(oldCur); } useNextSlot(newCur); head = pointer; tail = pointer - size + 1; if (tail < 0) { tail = 0; } } function move(cm, offset) { pointer += offset; if (pointer > head) { pointer = head; } else if (pointer < tail) { pointer = tail; } var mark = buffer[(size + pointer) % size]; // skip marks that are temporarily removed from text buffer if (mark && !mark.find()) { var inc = offset > 0 ? 1 : -1; var newCur; var oldCur = cm.getCursor(); do { pointer += inc; mark = buffer[(size + pointer) % size]; // skip marks that are the same as current position if (mark && (newCur = mark.find()) && !cursorEqual(oldCur, newCur)) { break; } } while (pointer < head && pointer > tail); } return mark; } return { cachedCursor: undefined, //used for # and * jumps add: add, move: move }; }; // Returns an object to track the changes associated insert mode. It // clones the object that is passed in, or creates an empty object one if // none is provided. var createInsertModeChanges = function(c) { if (c) { // Copy construction return { changes: c.changes, expectCursorActivityForChange: c.expectCursorActivityForChange }; } return { // Change list changes: [], // Set to true on change, false on cursorActivity. expectCursorActivityForChange: false }; }; function MacroModeState() { this.latestRegister = undefined; this.isPlaying = false; this.isRecording = false; this.replaySearchQueries = []; this.onRecordingDone = undefined; this.lastInsertModeChanges = createInsertModeChanges(); } MacroModeState.prototype = { exitMacroRecordMode: function() { var macroModeState = vimGlobalState.macroModeState; if (macroModeState.onRecordingDone) { macroModeState.onRecordingDone(); // close dialog } macroModeState.onRecordingDone = undefined; macroModeState.isRecording = false; }, enterMacroRecordMode: function(cm, registerName) { var register = vimGlobalState.registerController.getRegister(registerName); if (register) { register.clear(); this.latestRegister = registerName; if (cm.openDialog) { this.onRecordingDone = cm.openDialog( '(recording)['+registerName+']', null, {bottom:true}); } this.isRecording = true; } } }; function maybeInitVimState(cm) { if (!cm.state.vim) { // Store instance state in the CodeMirror object. cm.state.vim = { inputState: new InputState(), // Vim's input state that triggered the last edit, used to repeat // motions and operators with '.'. lastEditInputState: undefined, // Vim's action command before the last edit, used to repeat actions // with '.' and insert mode repeat. lastEditActionCommand: undefined, // When using jk for navigation, if you move from a longer line to a // shorter line, the cursor may clip to the end of the shorter line. // If j is pressed again and cursor goes to the next line, the // cursor should go back to its horizontal position on the longer // line if it can. This is to keep track of the horizontal position. lastHPos: -1, // Doing the same with screen-position for gj/gk lastHSPos: -1, // The last motion command run. Cleared if a non-motion command gets // executed in between. lastMotion: null, marks: {}, // Mark for rendering fake cursor for visual mode. fakeCursor: null, insertMode: false, // Repeat count for changes made in insert mode, triggered by key // sequences like 3,i. Only exists when insertMode is true. insertModeRepeat: undefined, visualMode: false, // If we are in visual line mode. No effect if visualMode is false. visualLine: false, visualBlock: false, lastSelection: null, lastPastedText: null, sel: {}, // Buffer-local/window-local values of vim options. options: {} }; } return cm.state.vim; } var vimGlobalState; function resetVimGlobalState() { vimGlobalState = { // The current search query. searchQuery: null, // Whether we are searching backwards. searchIsReversed: false, // Replace part of the last substituted pattern lastSubstituteReplacePart: undefined, jumpList: createCircularJumpList(), macroModeState: new MacroModeState, // Recording latest f, t, F or T motion command. lastCharacterSearch: {increment:0, forward:true, selectedCharacter:''}, registerController: new RegisterController({}), // search history buffer searchHistoryController: new HistoryController(), // ex Command history buffer exCommandHistoryController : new HistoryController() }; for (var optionName in options) { var option = options[optionName]; option.value = option.defaultValue; } } var lastInsertModeKeyTimer; var vimApi= { 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, suppressErrorLogging: false, InsertModeKey: InsertModeKey, map: function(lhs, rhs, ctx) { // Add user defined key bindings. exCommandDispatcher.map(lhs, rhs, ctx); }, unmap: function(lhs, ctx) { exCommandDispatcher.unmap(lhs, ctx); }, // TODO: Expose setOption and getOption as instance methods. Need to decide how to namespace // them, or somehow make them work with the existing CodeMirror setOption/getOption API. setOption: setOption, getOption: getOption, defineOption: defineOption, defineEx: function(name, prefix, func){ if (!prefix) { prefix = name; } else if (name.indexOf(prefix) !== 0) { throw new Error('(Vim.defineEx) "'+prefix+'" is not a prefix of "'+name+'", command not registered'); } exCommands[name]=func; exCommandDispatcher.commandMap_[prefix]={name:name, shortName:prefix, type:'api'}; }, handleKey: function (cm, key, origin) { var command = this.findKey(cm, key, origin); if (typeof command === 'function') { return command(); } }, /** * This is the outermost function called by CodeMirror, after keys have * been mapped to their Vim equivalents. * * Finds a command based on the key (and cached keys if there is a * multi-key sequence). Returns `undefined` if no key is matched, a noop * function if a partial match is found (multi-key), and a function to * execute the bound command if a a key is matched. The function always * returns true. */ findKey: function(cm, key, origin) { var vim = maybeInitVimState(cm); function handleMacroRecording() { var macroModeState = vimGlobalState.macroModeState; if (macroModeState.isRecording) { if (key == 'q') { macroModeState.exitMacroRecordMode(); clearInputState(cm); return true; } if (origin != 'mapping') { logKey(macroModeState, key); } } } function handleEsc() { if (key == '<Esc>') { // Clear input state and get back to normal mode. clearInputState(cm); if (vim.visualMode) { exitVisualMode(cm); } else if (vim.insertMode) { exitInsertMode(cm); } return true; } } function doKeyToKey(keys) { // TODO: prevent infinite recursion. var match; while (keys) { // Pull off one command key, which is either a single character // or a special sequence wrapped in '<' and '>', e.g. '<Space>'. match = (/<\w+-.+?>|<\w+>|./).exec(keys); key = match[0]; keys = keys.substring(match.index + key.length); CodeMirror.Vim.handleKey(cm, key, 'mapping'); } } function handleKeyInsertMode() { if (handleEsc()) { return true; } var keys = vim.inputState.keyBuffer = vim.inputState.keyBuffer + key; var keysAreChars = key.length == 1; var match = commandDispatcher.matchCommand(keys, defaultKeymap, vim.inputState, 'insert'); // Need to check all key substrings in insert mode. while (keys.length > 1 && match.type != 'full') { var keys = vim.inputState.keyBuffer = keys.slice(1); var thisMatch = commandDispatcher.matchCommand(keys, defaultKeymap, vim.inputState, 'insert'); if (thisMatch.type != 'none') { match = thisMatch; } } if (match.type == 'none') { clearInputState(cm); return false; } else if (match.type == 'partial') { if (lastInsertModeKeyTimer) { window.clearTimeout(lastInsertModeKeyTimer); } lastInsertModeKeyTimer = window.setTimeout( function() { if (vim.insertMode && vim.inputState.keyBuffer) { clearInputState(cm); } }, getOption('insertModeEscKeysTimeout')); return !keysAreChars; } if (lastInsertModeKeyTimer) { window.clearTimeout(lastInsertModeKeyTimer); } if (keysAreChars) { var selections = cm.listSelections(); for (var i = 0; i < selections.length; i++) { var here = selections[i].head; cm.replaceRange('', offsetCursor(here, 0, -(keys.length - 1)), here, '+input'); } vimGlobalState.macroModeState.lastInsertModeChanges.changes.pop(); } clearInputState(cm); return match.command; } function handleKeyNonInsertMode() { if (handleMacroRecording() || handleEsc()) { return true; }; var keys = vim.inputState.keyBuffer = vim.inputState.keyBuffer + key; if (/^[1-9]\d*$/.test(keys)) { return true; } var keysMatcher = /^(\d*)(.*)$/.exec(keys); if (!keysMatcher) { clearInputState(cm); return false; } var context = vim.visualMode ? 'visual' : 'normal'; var match = commandDispatcher.matchCommand(keysMatcher[2] || keysMatcher[1], defaultKeymap, vim.inputState, context); if (match.type == 'none') { clearInputState(cm); return false; } else if (match.type == 'partial') { return true; } vim.inputState.keyBuffer = ''; var keysMatcher = /^(\d*)(.*)$/.exec(keys); if (keysMatcher[1] && keysMatcher[1] != '0') { vim.inputState.pushRepeatDigit(keysMatcher[1]); } return match.command; } var command; if (vim.insertMode) { command = handleKeyInsertMode(); } else { command = handleKeyNonInsertMode(); } if (command === false) { return undefined; } else if (command === true) { // TODO: Look into using CodeMirror's multi-key handling. // Return no-op since we are caching the key. Counts as handled, but // don't want act on it just yet. return function() { return true; }; } else { return function() { return cm.operation(function() { cm.curOp.isVimOp = true; try { if (command.type == 'keyToKey') { doKeyToKey(command.toKeys); } else { commandDispatcher.processCommand(cm, vim, command); } } catch (e) { // clear VIM state in case it's in a bad state. cm.state.vim = undefined; maybeInitVimState(cm); if (!CodeMirror.Vim.suppressErrorLogging) { console['log'](e); } throw e; } return true; }); }; } }, handleEx: function(cm, input) { exCommandDispatcher.processCommand(cm, input); }, defineMotion: defineMotion, defineAction: defineAction, defineOperator: defineOperator, mapCommand: mapCommand, _mapCommand: _mapCommand, defineRegister: defineRegister, exitVisualMode: exitVisualMode, exitInsertMode: exitInsertMode }; // Represents the current input state. function InputState() { this.prefixRepeat = []; this.motionRepeat = []; this.operator = null; this.operatorArgs = null; this.motion = null; this.motionArgs = null; this.keyBuffer = []; // For matching multi-key commands. this.registerName = null; // Defaults to the unnamed register. } InputState.prototype.pushRepeatDigit = function(n) { if (!this.operator) { this.prefixRepeat = this.prefixRepeat.concat(n); } else { this.motionRepeat = this.motionRepeat.concat(n); } }; InputState.prototype.getRepeat = function() { var repeat = 0; if (this.prefixRepeat.length > 0 || this.motionRepeat.length > 0) { repeat = 1; if (this.prefixRepeat.length > 0) { repeat *= parseInt(this.prefixRepeat.join(''), 10); } if (this.motionRepeat.length > 0) { repeat *= parseInt(this.motionRepeat.join(''), 10); } } return repeat; }; function clearInputState(cm, reason) { cm.state.vim.inputState = new InputState(); CodeMirror.signal(cm, 'vim-command-done', reason); } /* * Register stores information about copy and paste registers. Besides * text, a register must store whether it is linewise (i.e., when it is * pasted, should it insert itself into a new line, or should the text be * inserted at the cursor position.) */ function Register(text, linewise, blockwise) { this.clear(); this.keyBuffer = [text || '']; this.insertModeChanges = []; this.searchQueries = []; this.linewise = !!linewise; this.blockwise = !!blockwise; } Register.prototype = { setText: function(text, linewise, blockwise) { this.keyBuffer = [text || '']; this.linewise = !!linewise; this.blockwise = !!blockwise; }, pushText: function(text, linewise) { // if this register has ever been set to linewise, use linewise. if (linewise) { if (!this.linewise) { this.keyBuffer.push('\n'); } this.linewise = true; } this.keyBuffer.push(text); }, pushInsertModeChanges: function(changes) { this.insertModeChanges.push(createInsertModeChanges(changes)); }, pushSearchQuery: function(query) { this.searchQueries.push(query); }, clear: function() { this.keyBuffer = []; this.insertModeChanges = []; this.searchQueries = []; this.linewise = false; }, toString: function() { return this.keyBuffer.join(''); } }; /** * Defines an external register. * * The name should be a single character that will be used to reference the register. * The register should support setText, pushText, clear, and toString(). See Register * for a reference implementation. */ function defineRegister(name, register) { var registers = vimGlobalState.registerController.registers; if (!name || name.length != 1) { throw Error('Register name must be 1 character'); } if (registers[name]) { throw Error('Register already defined ' + name); } registers[name] = register; validRegisters.push(name); } /* * 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.unnamedRegister = registers['"'] = new Register(); registers['.'] = new Register(); registers[':'] = new Register(); registers['/'] = new Register(); } RegisterController.prototype = { pushText: function(registerName, operator, text, linewise, blockwise) { 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, blockwise); 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.unnamedRegister.setText(text, linewise, blockwise); return; } // If we've gotten to this point, we've actually specified a register var append = isUpperCase(registerName); if (append) { register.pushText(text, linewise); } else { register.setText(text, linewise, blockwise); } // The unnamed register always has the same value as the last used // register. this.unnamedRegister.setText(register.toString(), linewise); }, // Gets the register named @name. If one of @name doesn't already exist, // create it. If @name is invalid, return the unnamedRegister. getRegister: function(name) { if (!this.isValidRegister(name)) { return this.unnamedRegister; } 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)); } } }; function HistoryController() { this.historyBuffer = []; this.iterator = 0; this.initialPrefix = null; } HistoryController.prototype = { // the input argument here acts a user entered prefix for a small time // until we start autocompletion in which case it is the autocompleted. nextMatch: function (input, up) { var historyBuffer = this.historyBuffer; var dir = up ? -1 : 1; if (this.initialPrefix === null) this.initialPrefix = input; for (var i = this.iterator + dir; up ? i >= 0 : i < historyBuffer.length; i+= dir) { var element = historyBuffer[i]; for (var j = 0; j <= element.length; j++) { if (this.initialPrefix == element.substring(0, j)) { this.iterator = i; return element; } } } // should return the user input in case we reach the end of buffer. if (i >= historyBuffer.length) { this.iterator = historyBuffer.length; return this.initialPrefix; } // return the last autocompleted query or exCommand as it is. if (i < 0 ) return input; }, pushInput: function(input) { var index = this.historyBuffer.indexOf(input); if (index > -1) this.historyBuffer.splice(index, 1); if (input.length) this.historyBuffer.push(input); }, reset: function() { this.initialPrefix = null; this.iterator = this.historyBuffer.length; } }; var commandDispatcher = { matchCommand: function(keys, keyMap, inputState, context) { var matches = commandMatches(keys, keyMap, context, inputState); if (!matches.full && !matches.partial) { return {type: 'none'}; } else if (!matches.full && matches.partial) { return {type: 'partial'}; } var bestMatch; for (var i = 0; i < matches.full.length; i++) { var match = matches.full[i]; if (!bestMatch) { bestMatch = match; } } if (bestMatch.keys.slice(-11) == '<character>') { var character = lastChar(keys); if (!character) return {type: 'none'}; inputState.selectedCharacter = character; } return {type: 'full', command: 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. clearInputState(cm); } } 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; clearInputState(cm); 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; var wholeWordOnly = command.searchArgs.wholeWordOnly; getSearchState(cm).setReversed(!forward); var promptPrefix = (forward) ? '/' : '?'; var originalQuery = getSearchState(cm).getQuery(); var originalScrollPos = cm.getScrollInfo(); function handleQuery(query, ignoreCase, smartCase) { vimGlobalState.searchHistoryController.pushInput(query); vimGlobalState.searchHistoryController.reset(); try { updateSearchQuery(cm, query, ignoreCase, smartCase); } catch (e) { showConfirm(cm, 'Invalid regex: ' + query); clearInputState(cm); 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 */); var macroModeState = vimGlobalState.macroModeState; if (macroModeState.isRecording) { logSearchQuery(macroModeState, query); } } function onPromptKeyUp(e, query, close) { var keyName = CodeMirror.keyName(e), up, offset; if (keyName == 'Up' || keyName == 'Down') { up = keyName == 'Up' ? true : false; offset = e.target ? e.target.selectionEnd : 0; query = vimGlobalState.searchHistoryController.nextMatch(query, up) || ''; close(query); if (offset && e.target) e.target.selectionEnd = e.target.selectionStart = Math.min(offset, e.target.value.length); } else { if ( keyName != 'Left' && keyName != 'Right' && keyName != 'Ctrl' && keyName != 'Alt' && keyName != 'Shift') vimGlobalState.searchHistoryController.reset(); } 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-[' || (keyName == 'Backspace' && query == '')) { vimGlobalState.searchHistoryController.pushInput(query); vimGlobalState.searchHistoryController.reset(); updateSearchQuery(cm, originalQuery); clearSearchHighlight(cm); cm.scrollTo(originalScrollPos.left, originalScrollPos.top); CodeMirror.e_stop(e); clearInputState(cm); close(); cm.focus(); } else if (keyName == 'Up' || keyName == 'Down') { CodeMirror.e_stop(e); } else if (keyName == 'Ctrl-U') { // Ctrl-U clears input. CodeMirror.e_stop(e); close(''); } } switch (command.searchArgs.querySrc) { case 'prompt': var macroModeState = vimGlobalState.macroModeState; if (macroModeState.isPlaying) { var query = macroModeState.replaySearchQueries.shift(); handleQuery(query, true /** ignoreCase */, false /** smartCase */); } else { 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 && wholeWordOnly) { 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. vimGlobalState.exCommandHistoryController.pushInput(input); vimGlobalState.exCommandHistoryController.reset(); exCommandDispatcher.processCommand(cm, input); } function onPromptKeyDown(e, input, close) { var keyName = CodeMirror.keyName(e), up, offset; if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[' || (keyName == 'Backspace' && input == '')) { vimGlobalState.exCommandHistoryController.pushInput(input); vimGlobalState.exCommandHistoryController.reset(); CodeMirror.e_stop(e); clearInputState(cm); close(); cm.focus(); } if (keyName == 'Up' || keyName == 'Down') { CodeMirror.e_stop(e); up = keyName == 'Up' ? true : false; offset = e.target ? e.target.selectionEnd : 0; input = vimGlobalState.exCommandHistoryController.nextMatch(input, up) || ''; close(input); if (offset && e.target) e.target.selectionEnd = e.target.selectionStart = Math.min(offset, e.target.value.length); } else if (keyName == 'Ctrl-U') { // Ctrl-U clears input. CodeMirror.e_stop(e); close(''); } else { if ( keyName != 'Left' && keyName != 'Right' && keyName != 'Ctrl' && keyName != 'Alt' && keyName != 'Shift') vimGlobalState.exCommandHistoryController.reset(); } } 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 command 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 sel = vim.sel; // TODO: Make sure cm and vim selections are identical outside visual mode. var origHead = copyCursor(vim.visualMode ? clipCursorToContent(cm, sel.head): cm.getCursor('head')); var origAnchor = copyCursor(vim.visualMode ? clipCursorToContent(cm, sel.anchor) : cm.getCursor('anchor')); var oldHead = copyCursor(origHead); var oldAnchor = copyCursor(origAnchor); var newHead, newAnchor; 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; clearInputState(cm); if (motion) { var motionResult = motions[motion](cm, origHead, 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, origHead, motionResult); } } if (motionResult instanceof Array) { newAnchor = motionResult[0]; newHead = motionResult[1]; } else { newHead = motionResult; } // TODO: Handle null returns from motion commands better. if (!newHead) { newHead = copyCursor(origHead); } if (vim.visualMode) { if (!(vim.visualBlock && newHead.ch === Infinity)) { newHead = clipCursorToContent(cm, newHead, vim.visualBlock); } if (newAnchor) { newAnchor = clipCursorToContent(cm, newAnchor, true); } newAnchor = newAnchor || oldAnchor; sel.anchor = newAnchor; sel.head = newHead; updateCmSelection(cm); updateMark(cm, vim, '<', cursorIsBefore(newAnchor, newHead) ? newAnchor : newHead); updateMark(cm, vim, '>', cursorIsBefore(newAnchor, newHead) ? newHead : newAnchor); } else if (!operator) { newHead = clipCursorToContent(cm, newHead); cm.setCursor(newHead.line, newHead.ch); } } if (operator) { if (operatorArgs.lastSel) { // Replaying a visual mode operation newAnchor = oldAnchor; var lastSel = operatorArgs.lastSel; var lineOffset = Math.abs(lastSel.head.line - lastSel.anchor.line); var chOffset = Math.abs(lastSel.head.ch - lastSel.anchor.ch); if (lastSel.visualLine) { // Linewise Visual mode: The same number of lines. newHead = Pos(oldAnchor.line + lineOffset, oldAnchor.ch); } else if (lastSel.visualBlock) { // Blockwise Visual mode: The same number of lines and columns. newHead = Pos(oldAnchor.line + lineOffset, oldAnchor.ch + chOffset); } else if (lastSel.head.line == lastSel.anchor.line) { // Normal Visual mode within one line: The same number of characters. newHead = Pos(oldAnchor.line, oldAnchor.ch + chOffset); } else { // Normal Visual mode with several lines: The same number of lines, in the // last line the same number of characters as in the last line the last time. newHead = Pos(oldAnchor.line + lineOffset, oldAnchor.ch); } vim.visualMode = true; vim.visualLine = lastSel.visualLine; vim.visualBlock = lastSel.visualBlock; sel = vim.sel = { anchor: newAnchor, head: newHead }; updateCmSelection(cm); } else if (vim.visualMode) { operatorArgs.lastSel = { anchor: copyCursor(sel.anchor), head: copyCursor(sel.head), visualBlock: vim.visualBlock, visualLine: vim.visualLine }; } var curStart, curEnd, linewise, mode; var cmSel; if (vim.visualMode) { // Init visual op curStart = cursorMin(sel.head, sel.anchor); curEnd = cursorMax(sel.head, sel.anchor); linewise = vim.visualLine || operatorArgs.linewise; mode = vim.visualBlock ? 'block' : linewise ? 'line' : 'char'; cmSel = makeCmSelection(cm, { anchor: curStart, head: curEnd }, mode); if (linewise) { var ranges = cmSel.ranges; if (mode == 'block') { // Linewise operators in visual block mode extend to end of line for (var i = 0; i < ranges.length; i++) { ranges[i].head.ch = lineLength(cm, ranges[i].head.line); } } else if (mode == 'line') { ranges[0].head = Pos(ranges[0].head.line + 1, 0); } } } else { // Init motion op curStart = copyCursor(newAnchor || oldAnchor); curEnd = copyCursor(newHead || oldHead); if (cursorIsBefore(curEnd, curStart)) { var tmp = curStart; curStart = curEnd; curEnd = tmp; } linewise = motionArgs.linewise || operatorArgs.linewise; 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); } mode = 'char'; var exclusive = !motionArgs.inclusive || linewise; cmSel = makeCmSelection(cm, { anchor: curStart, head: curEnd }, mode, exclusive); } cm.setSelections(cmSel.ranges, cmSel.primary); vim.lastMotion = null; operatorArgs.repeat = repeat; // For indent in visual mode. operatorArgs.registerName = registerName; // Keep track of linewise as it affects how paste and change behave. operatorArgs.linewise = linewise; var operatorMoveTo = operators[operator]( cm, operatorArgs, cmSel.ranges, oldAnchor, newHead); if (vim.visualMode) { exitVisualMode(cm, operatorMoveTo != null); } if (operatorMoveTo) { cm.setCursor(operatorMoveTo); } } }, recordLastEdit: function(vim, inputState, actionCommand) { var macroModeState = vimGlobalState.macroModeState; if (macroModeState.isPlaying) { 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, _head, motionArgs) { var line = getUserVisibleLines(cm).top + motionArgs.repeat -1; return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line))); }, moveToMiddleLine: function(cm) { var range = getUserVisibleLines(cm); var line = Math.floor((range.top + range.bottom) * 0.5); return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line))); }, moveToBottomLine: function(cm, _head, motionArgs) { var line = getUserVisibleLines(cm).bottom - motionArgs.repeat +1; return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line))); }, expandToLine: function(_cm, head, motionArgs) { // Expands forward to end of line, and then to next line if repeat is // >1. Does not handle backward motion! var cur = head; return Pos(cur.line + motionArgs.repeat - 1, Infinity); }, findNext: function(cm, _head, 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, _head, motionArgs, vim) { var pos = getMarkPos(cm, vim, motionArgs.selectedCharacter); if (pos) { return motionArgs.linewise ? { line: pos.line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(pos.line)) } : pos; } return null; }, moveToOtherHighlightedEnd: function(cm, _head, motionArgs, vim) { if (vim.visualBlock && motionArgs.sameLine) { var sel = vim.sel; return [ clipCursorToContent(cm, Pos(sel.anchor.line, sel.head.ch)), clipCursorToContent(cm, Pos(sel.head.line, sel.anchor.ch)) ]; } else { return ([vim.sel.head, vim.sel.anchor]); } }, jumpToMark: function(cm, head, motionArgs, vim) { var best = head; 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) ? cursorIsBetween(cursor, mark, best) : cursorIsBetween(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 = Pos(best.line, findFirstNonWhiteSpaceCharacter(cm.getLine(best.line))); } return best; }, moveByCharacters: function(_cm, head, motionArgs) { var cur = head; var repeat = motionArgs.repeat; var ch = motionArgs.forward ? cur.ch + repeat : cur.ch - repeat; return Pos(cur.line, ch); }, moveByLines: function(cm, head, motionArgs, vim) { var cur = head; 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 go to line begin or line end when cursor at first/last line and // move to previous/next line is triggered. if (line < first && cur.line == first){ return this.moveToStartOfLine(cm, head, motionArgs, vim); }else if (line > last && cur.line == last){ return this.moveToEol(cm, head, motionArgs, vim); } if (motionArgs.toFirstChar){ endCh=findFirstNonWhiteSpaceCharacter(cm.getLine(line)); vim.lastHPos = endCh; } vim.lastHSPos = cm.charCoords(Pos(line, endCh),'div').left; return Pos(line, endCh); }, moveByDisplayLines: function(cm, head, motionArgs, vim) { var cur = head; 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(Pos(cm.firstLine(), 0), 'div'); resCoords.left = vim.lastHSPos; res = cm.coordsChar(resCoords, 'div'); } } vim.lastHPos = res.ch; return res; }, moveByPage: function(cm, head, 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 = head; var repeat = motionArgs.repeat; return cm.findPosV(curStart, (motionArgs.forward ? repeat : -repeat), 'page'); }, moveByParagraph: function(cm, head, motionArgs) { var dir = motionArgs.forward ? 1 : -1; return findParagraph(cm, head, motionArgs.repeat, dir); }, moveByScroll: function(cm, head, 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(head, 'local'); motionArgs.repeat = repeat; var curEnd = motions.moveByDisplayLines(cm, head, 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, head, motionArgs) { return moveToWord(cm, head, motionArgs.repeat, !!motionArgs.forward, !!motionArgs.wordEnd, !!motionArgs.bigWord); }, moveTillCharacter: function(cm, _head, 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 null; curEnd.ch += increment; return curEnd; }, moveToCharacter: function(cm, head, motionArgs) { var repeat = motionArgs.repeat; recordLastCharacterSearch(0, motionArgs); return moveToCharacter(cm, repeat, motionArgs.forward, motionArgs.selectedCharacter) || head; }, moveToSymbol: function(cm, head, motionArgs) { var repeat = motionArgs.repeat; return findSymbol(cm, repeat, motionArgs.forward, motionArgs.selectedCharacter) || head; }, moveToColumn: function(cm, head, 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(head,'div').left; return moveToColumn(cm, repeat); }, moveToEol: function(cm, head, motionArgs, vim) { var cur = head; vim.lastHPos = Infinity; var retval= Pos(cur.line + motionArgs.repeat - 1, Infinity); var end=cm.clipPos(retval); end.ch--; vim.lastHSPos = cm.charCoords(end,'div').left; return retval; }, moveToFirstNonWhiteSpaceCharacter: function(cm, head) { // Go to the start of the line where the text begins, or the end for // whitespace-only lines var cursor = head; return Pos(cursor.line, findFirstNonWhiteSpaceCharacter(cm.getLine(cursor.line))); }, moveToMatchedSymbol: function(cm, head) { var cursor = head; var line = cursor.line; var ch = cursor.ch; var lineText = cm.getLine(line); var symbol; for (; ch < lineText.length; ch++) { symbol = lineText.charAt(ch); if (symbol && isMatchableSymbol(symbol)) { var style = cm.getTokenTypeAt(Pos(line, ch + 1)); if (style !== "string" && style !== "comment") { break; } } } if (ch < lineText.length) { var matched = cm.findMatchingBracket(Pos(line, ch)); return matched.to; } else { return cursor; } }, moveToStartOfLine: function(_cm, head) { return Pos(head.line, 0); }, moveToLineOrEdgeOfDocument: function(cm, _head, motionArgs) { var lineNum = motionArgs.forward ? cm.lastLine() : cm.firstLine(); if (motionArgs.repeatIsExplicit) { lineNum = motionArgs.repeat - cm.getOption('firstLineNumber'); } return Pos(lineNum, findFirstNonWhiteSpaceCharacter(cm.getLine(lineNum))); }, textObjectManipulation: function(cm, head, motionArgs, vim) { // TODO: lots of possible exceptions that can be thrown here. Try da( // outside of a () block. // TODO: adding <> >< to this map doesn't work, presumably because // they're operators var mirroredPairs = {'(': ')', ')': '(', '{': '}', '}': '{', '[': ']', ']': '['}; var selfPaired = {'\'': true, '"': true}; var character = motionArgs.selectedCharacter; // 'b' refers to '()' block. // 'B' refers to '{}' block. if (character == 'b') { character = '('; } else if (character == 'B') { character = '{'; } // 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; var tmp; if (mirroredPairs[character]) { tmp = selectCompanionObject(cm, head, character, inclusive); } else if (selfPaired[character]) { tmp = findBeginningAndEnd(cm, head, character, inclusive); } else if (character === 'W') { tmp = expandWordUnderCursor(cm, inclusive, true /** forward */, true /** bigWord */); } else if (character === 'w') { tmp = expandWordUnderCursor(cm, inclusive, true /** forward */, false /** bigWord */); } else if (character === 'p') { tmp = findParagraph(cm, head, motionArgs.repeat, 0, inclusive); motionArgs.linewise = true; if (vim.visualMode) { if (!vim.visualLine) { vim.visualLine = true; } } else { var operatorArgs = vim.inputState.operatorArgs; if (operatorArgs) { operatorArgs.linewise = true; } tmp.end.line--; } } else { // No text object defined for this, don't move. return null; } if (!cm.state.vim.visualMode) { return [tmp.start, tmp.end]; } else { return expandSelection(cm, tmp.start, tmp.end); } }, repeatLastCharacterSearch: function(cm, head, motionArgs) { var lastSearch = vimGlobalState.lastCharacterSearch; 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 head; } curEnd.ch += increment; return curEnd; } }; function defineMotion(name, fn) { motions[name] = fn; } function fillArray(val, times) { var arr = []; for (var i = 0; i < times; i++) { arr.push(val); } return arr; } /** * An operator acts on a text selection. It receives the list of selections * as input. The corresponding CodeMirror selection is guaranteed to * match the input selection. */ var operators = { change: function(cm, args, ranges) { var finalHead, text; var vim = cm.state.vim; vimGlobalState.macroModeState.lastInsertModeChanges.inVisualBlock = vim.visualBlock; if (!vim.visualMode) { var anchor = ranges[0].anchor, head = ranges[0].head; text = cm.getRange(anchor, head); var lastState = vim.lastEditInputState || {}; if (lastState.motion == "moveByWords" && !isWhiteSpaceString(text)) { // Exclude trailing whitespace if the range is not all whitespace. var match = (/\s+$/).exec(text); if (match && lastState.motionArgs && lastState.motionArgs.forward) { head = offsetCursor(head, 0, - match[0].length); text = text.slice(0, - match[0].length); } } var prevLineEnd = new Pos(anchor.line - 1, Number.MAX_VALUE); var wasLastLine = cm.firstLine() == cm.lastLine(); if (head.line > cm.lastLine() && args.linewise && !wasLastLine) { cm.replaceRange('', prevLineEnd, head); } else { cm.replaceRange('', anchor, head); } if (args.linewise) { // Push the next line back down, if there is a next line. if (!wasLastLine) { cm.setCursor(prevLineEnd); CodeMirror.commands.newlineAndIndent(cm); } // make sure cursor ends up at the end of the line. anchor.ch = Number.MAX_VALUE; } finalHead = anchor; } else { text = cm.getSelection(); var replacement = fillArray('', ranges.length); cm.replaceSelections(replacement); finalHead = cursorMin(ranges[0].head, ranges[0].anchor); } vimGlobalState.registerController.pushText( args.registerName, 'change', text, args.linewise, ranges.length > 1); actions.enterInsertMode(cm, {head: finalHead}, cm.state.vim); }, // delete is a javascript keyword. 'delete': function(cm, args, ranges) { var finalHead, text; var vim = cm.state.vim; if (!vim.visualBlock) { var anchor = ranges[0].anchor, head = ranges[0].head; if (args.linewise && head.line != cm.firstLine() && anchor.line == cm.lastLine() && anchor.line == head.line - 1) { // Special case for dd on last line (and first line). if (anchor.line == cm.firstLine()) { anchor.ch = 0; } else { anchor = Pos(anchor.line - 1, lineLength(cm, anchor.line - 1)); } } text = cm.getRange(anchor, head); cm.replaceRange('', anchor, head); finalHead = anchor; if (args.linewise) { finalHead = motions.moveToFirstNonWhiteSpaceCharacter(cm, anchor); } } else { text = cm.getSelection(); var replacement = fillArray('', ranges.length); cm.replaceSelections(replacement); finalHead = ranges[0].anchor; } vimGlobalState.registerController.pushText( args.registerName, 'delete', text, args.linewise, vim.visualBlock); var includeLineBreak = vim.insertMode return clipCursorToContent(cm, finalHead, includeLineBreak); }, indent: function(cm, args, ranges) { var vim = cm.state.vim; var startLine = ranges[0].anchor.line; var endLine = vim.visualBlock ? ranges[ranges.length - 1].anchor.line : ranges[0].head.line; // In visual mode, n> shifts the selection right n times, instead of // shifting n lines right once. var repeat = (vim.visualMode) ? args.repeat : 1; if (args.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, args.indentRight); } } return motions.moveToFirstNonWhiteSpaceCharacter(cm, ranges[0].anchor); }, changeCase: function(cm, args, ranges, oldAnchor, newHead) { var selections = cm.getSelections(); var swapped = []; var toLower = args.toLower; for (var j = 0; j < selections.length; j++) { var toSwap = selections[j]; var text = ''; if (toLower === true) { text = toSwap.toLowerCase(); } else if (toLower === false) { text = toSwap.toUpperCase(); } else { for (var i = 0; i < toSwap.length; i++) { var character = toSwap.charAt(i); text += isUpperCase(character) ? character.toLowerCase() : character.toUpperCase(); } } swapped.push(text); } cm.replaceSelections(swapped); if (args.shouldMoveCursor){ return newHead; } else if (!cm.state.vim.visualMode && args.linewise && ranges[0].anchor.line + 1 == ranges[0].head.line) { return motions.moveToFirstNonWhiteSpaceCharacter(cm, oldAnchor); } else if (args.linewise){ return oldAnchor; } else { return cursorMin(ranges[0].anchor, ranges[0].head); } }, yank: function(cm, args, ranges, oldAnchor) { var vim = cm.state.vim; var text = cm.getSelection(); var endPos = vim.visualMode ? cursorMin(vim.sel.anchor, vim.sel.head, ranges[0].head, ranges[0].anchor) : oldAnchor; vimGlobalState.registerController.pushText( args.registerName, 'yank', text, args.linewise, vim.visualBlock); return endPos; } }; function defineOperator(name, fn) { operators[name] = fn; } 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); }, scroll: function(cm, actionArgs, vim) { if (vim.visualMode) { return; } var repeat = actionArgs.repeat || 1; var lineHeight = cm.defaultTextHeight(); var top = cm.getScrollInfo().top; var delta = lineHeight * repeat; var newPos = actionArgs.forward ? top + delta : top - delta; var cursor = copyCursor(cm.getCursor()); var cursorCoords = cm.charCoords(cursor, 'local'); if (actionArgs.forward) { if (newPos > cursorCoords.top) { cursor.line += (newPos - cursorCoords.top) / lineHeight; cursor.line = Math.ceil(cursor.line); cm.setCursor(cursor); cursorCoords = cm.charCoords(cursor, 'local'); cm.scrollTo(null, cursorCoords.top); } else { // Cursor stays within bounds. Just reposition the scroll window. cm.scrollTo(null, newPos); } } else { var newBottom = newPos + cm.getScrollInfo().clientHeight; if (newBottom < cursorCoords.bottom) { cursor.line -= (cursorCoords.bottom - newBottom) / lineHeight; cursor.line = Math.floor(cursor.line); cm.setCursor(cursor); cursorCoords = cm.charCoords(cursor, 'local'); cm.scrollTo( null, cursorCoords.bottom - cm.getScrollInfo().clientHeight); } else { // Cursor stays within bounds. Just reposition the scroll window. cm.scrollTo(null, newPos); } } }, scrollToCursor: function(cm, actionArgs) { var lineNum = cm.getCursor().line; var charCoords = cm.charCoords(Pos(lineNum, 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; break; } cm.scrollTo(null, y); }, replayMacro: function(cm, actionArgs, vim) { var registerName = actionArgs.selectedCharacter; var repeat = actionArgs.repeat; var macroModeState = vimGlobalState.macroModeState; if (registerName == '@') { registerName = macroModeState.latestRegister; } while(repeat--){ executeMacroRegister(cm, vim, macroModeState, registerName); } }, enterMacroRecordMode: function(cm, actionArgs) { var macroModeState = vimGlobalState.macroModeState; var registerName = actionArgs.selectedCharacter; if (vimGlobalState.registerController.isValidRegister(registerName)) { macroModeState.enterMacroRecordMode(cm, registerName); } }, toggleOverwrite: function(cm) { if (!cm.state.overwrite) { cm.toggleOverwrite(true); cm.setOption('keyMap', 'vim-replace'); CodeMirror.signal(cm, "vim-mode-change", {mode: "replace"}); } else { cm.toggleOverwrite(false); cm.setOption('keyMap', 'vim-insert'); CodeMirror.signal(cm, "vim-mode-change", {mode: "insert"}); } }, 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; var sel = vim.sel; var head = actionArgs.head || cm.getCursor('head'); var height = cm.listSelections().length; if (insertAt == 'eol') { head = Pos(head.line, lineLength(cm, head.line)); } else if (insertAt == 'charAfter') { head = offsetCursor(head, 0, 1); } else if (insertAt == 'firstNonBlank') { head = motions.moveToFirstNonWhiteSpaceCharacter(cm, head); } else if (insertAt == 'startOfSelectedArea') { if (!vim.visualBlock) { if (sel.head.line < sel.anchor.line) { head = sel.head; } else { head = Pos(sel.anchor.line, 0); } } else { head = Pos( Math.min(sel.head.line, sel.anchor.line), Math.min(sel.head.ch, sel.anchor.ch)); height = Math.abs(sel.head.line - sel.anchor.line) + 1; } } else if (insertAt == 'endOfSelectedArea') { if (!vim.visualBlock) { if (sel.head.line >= sel.anchor.line) { head = offsetCursor(sel.head, 0, 1); } else { head = Pos(sel.anchor.line, 0); } } else { head = Pos( Math.min(sel.head.line, sel.anchor.line), Math.max(sel.head.ch + 1, sel.anchor.ch)); height = Math.abs(sel.head.line - sel.anchor.line) + 1; } } else if (insertAt == 'inplace') { if (vim.visualMode){ return; } } cm.setOption('disableInput', false); 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.toggleOverwrite(false); cm.setOption('keyMap', 'vim-insert'); CodeMirror.signal(cm, "vim-mode-change", {mode: "insert"}); } if (!vimGlobalState.macroModeState.isPlaying) { // Only record if not replaying. cm.on('change', onChange); CodeMirror.on(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown); } if (vim.visualMode) { exitVisualMode(cm); } selectForInsert(cm, head, height); }, toggleVisualMode: function(cm, actionArgs, vim) { var repeat = actionArgs.repeat; var anchor = cm.getCursor(); var head; // 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) { // Entering visual mode vim.visualMode = true; vim.visualLine = !!actionArgs.linewise; vim.visualBlock = !!actionArgs.blockwise; head = clipCursorToContent( cm, Pos(anchor.line, anchor.ch + repeat - 1), true /** includeLineBreak */); vim.sel = { anchor: anchor, head: head }; CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : vim.visualBlock ? "blockwise" : ""}); updateCmSelection(cm); updateMark(cm, vim, '<', cursorMin(anchor, head)); updateMark(cm, vim, '>', cursorMax(anchor, head)); } else if (vim.visualLine ^ actionArgs.linewise || vim.visualBlock ^ actionArgs.blockwise) { // Toggling between modes vim.visualLine = !!actionArgs.linewise; vim.visualBlock = !!actionArgs.blockwise; CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : vim.visualBlock ? "blockwise" : ""}); updateCmSelection(cm); } else { exitVisualMode(cm); } }, reselectLastSelection: function(cm, _actionArgs, vim) { var lastSelection = vim.lastSelection; if (vim.visualMode) { updateLastSelection(cm, vim); } if (lastSelection) { var anchor = lastSelection.anchorMark.find(); var head = lastSelection.headMark.find(); if (!anchor || !head) { // If the marks have been destroyed due to edits, do nothing. return; } vim.sel = { anchor: anchor, head: head }; vim.visualMode = true; vim.visualLine = lastSelection.visualLine; vim.visualBlock = lastSelection.visualBlock; updateCmSelection(cm); updateMark(cm, vim, '<', cursorMin(anchor, head)); updateMark(cm, vim, '>', cursorMax(anchor, head)); CodeMirror.signal(cm, 'vim-mode-change', { mode: 'visual', subMode: vim.visualLine ? 'linewise' : vim.visualBlock ? 'blockwise' : ''}); } }, joinLines: function(cm, actionArgs, vim) { var curStart, curEnd; if (vim.visualMode) { curStart = cm.getCursor('anchor'); curEnd = cm.getCursor('head'); if (cursorIsBefore(curEnd, curStart)) { var tmp = curEnd; curEnd = curStart; curStart = tmp; } 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, Pos(curStart.line + repeat - 1, Infinity)); } var finalCh = 0; for (var i = curStart.line; i < curEnd.line; i++) { finalCh = lineLength(cm, curStart.line); var tmp = Pos(curStart.line + 1, lineLength(cm, curStart.line + 1)); var text = cm.getRange(curStart, tmp); text = text.replace(/\n\s*/g, ' '); cm.replaceRange(text, curStart, tmp); } var curFinalPos = Pos(curStart.line, finalCh); if (vim.visualMode) { exitVisualMode(cm, false); } cm.setCursor(curFinalPos); }, newLineAndEnterInsertMode: function(cm, actionArgs, vim) { vim.insertMode = true; var insertAt = copyCursor(cm.getCursor()); if (insertAt.line === cm.firstLine() && !actionArgs.after) { // Special case for inserting newline before start of document. cm.replaceRange('\n', Pos(cm.firstLine(), 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, vim) { var cur = copyCursor(cm.getCursor()); var register = vimGlobalState.registerController.getRegister( actionArgs.registerName); var text = register.toString(); if (!text) { return; } if (actionArgs.matchIndent) { var tabSize = cm.getOption("tabSize"); // length that considers tabs and tabSize var whitespaceLength = function(str) { var tabs = (str.split("\t").length - 1); var spaces = (str.split(" ").length - 1); return tabs * tabSize + spaces * 1; }; var currentLine = cm.getLine(cm.getCursor().line); var indent = whitespaceLength(currentLine.match(/^\s*/)[0]); // chomp last newline b/c don't want it to match /^\s*/gm var chompedText = text.replace(/\n$/, ''); var wasChomped = text !== chompedText; var firstIndent = whitespaceLength(text.match(/^\s*/)[0]); var text = chompedText.replace(/^\s*/gm, function(wspace) { var newIndent = indent + (whitespaceLength(wspace) - firstIndent); if (newIndent < 0) { return ""; } else if (cm.getOption("indentWithTabs")) { var quotient = Math.floor(newIndent / tabSize); return Array(quotient + 1).join('\t'); } else { return Array(newIndent + 1).join(' '); } }); text += wasChomped ? "\n" : ""; } if (actionArgs.repeat > 1) { var text = Array(actionArgs.repeat + 1).join(text); } var linewise = register.linewise; var blockwise = register.blockwise; if (linewise) { if(vim.visualMode) { text = vim.visualLine ? text.slice(0, -1) : '\n' + text.slice(0, text.length - 1) + '\n'; } else 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 { if (blockwise) { text = text.split('\n'); for (var i = 0; i < text.length; i++) { text[i] = (text[i] == '') ? ' ' : text[i]; } } cur.ch += actionArgs.after ? 1 : 0; } var curPosFinal; var idx; if (vim.visualMode) { // save the pasted text for reselection if the need arises vim.lastPastedText = text; var lastSelectionCurEnd; var selectedArea = getSelectedAreaRange(cm, vim); var selectionStart = selectedArea[0]; var selectionEnd = selectedArea[1]; var selectedText = cm.getSelection(); var selections = cm.listSelections(); var emptyStrings = new Array(selections.length).join('1').split('1'); // save the curEnd marker before it get cleared due to cm.replaceRange. if (vim.lastSelection) { lastSelectionCurEnd = vim.lastSelection.headMark.find(); } // push the previously selected text to unnamed register vimGlobalState.registerController.unnamedRegister.setText(selectedText); if (blockwise) { // first delete the selected text cm.replaceSelections(emptyStrings); // Set new selections as per the block length of the yanked text selectionEnd = Pos(selectionStart.line + text.length-1, selectionStart.ch); cm.setCursor(selectionStart); selectBlock(cm, selectionEnd); cm.replaceSelections(text); curPosFinal = selectionStart; } else if (vim.visualBlock) { cm.replaceSelections(emptyStrings); cm.setCursor(selectionStart); cm.replaceRange(text, selectionStart, selectionStart); curPosFinal = selectionStart; } else { cm.replaceRange(text, selectionStart, selectionEnd); curPosFinal = cm.posFromIndex(cm.indexFromPos(selectionStart) + text.length - 1); } // restore the the curEnd marker if(lastSelectionCurEnd) { vim.lastSelection.headMark = cm.setBookmark(lastSelectionCurEnd); } if (linewise) { curPosFinal.ch=0; } } else { if (blockwise) { cm.setCursor(cur); for (var i = 0; i < text.length; i++) { var line = cur.line+i; if (line > cm.lastLine()) { cm.replaceRange('\n', Pos(line, 0)); } var lastCh = lineLength(cm, line); if (lastCh < cur.ch) { extendLineToColumn(cm, line, cur.ch); } } cm.setCursor(cur); selectBlock(cm, Pos(cur.line + text.length-1, cur.ch)); cm.replaceSelections(text); curPosFinal = cur; } else { cm.replaceRange(text, cur); // Now fine tune the cursor to where we want it. if (linewise && actionArgs.after) { curPosFinal = Pos( cur.line + 1, findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line + 1))); } else if (linewise && !actionArgs.after) { curPosFinal = Pos( cur.line, 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); } } } if (vim.visualMode) { exitVisualMode(cm, false); } 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; var selections = cm.listSelections(); if (vim.visualMode) { curStart = cm.getCursor('start'); curEnd = cm.getCursor('end'); } else { var line = cm.getLine(curStart.line); replaceTo = curStart.ch + actionArgs.repeat; if (replaceTo > line.length) { replaceTo=line.length; } curEnd = Pos(curStart.line, 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); if (vim.visualBlock) { // Tabs are split in visua block before replacing var spaces = new Array(cm.getOption("tabSize")+1).join(' '); replaceWithStr = cm.getSelection(); replaceWithStr = replaceWithStr.replace(/\t/g, spaces).replace(/[^\n]/g, replaceWith).split('\n'); cm.replaceSelections(replaceWithStr); } else { cm.replaceRange(replaceWithStr, curStart, curEnd); } if (vim.visualMode) { curStart = cursorIsBefore(selections[0].anchor, selections[0].head) ? selections[0].anchor : selections[0].head; cm.setCursor(curStart); exitVisualMode(cm, false); } else { cm.setCursor(offsetCursor(curEnd, 0, -1)); } } }, incrementNumberToken: function(cm, actionArgs) { var cur = cm.getCursor(); var lineStr = cm.getLine(cur.line); var re = /(-?)(?:(0x)([\da-f]+)|(0b|0|)(\d+))/gi; var match; var start; var end; var numberStr; while ((match = re.exec(lineStr)) !== null) { start = match.index; end = start + match[0].length; if (cur.ch < end)break; } if (!actionArgs.backtrack && (end <= cur.ch))return; if (match) { var baseStr = match[2] || match[4] var digits = match[3] || match[5] var increment = actionArgs.increase ? 1 : -1; var base = {'0b': 2, '0': 8, '': 10, '0x': 16}[baseStr.toLowerCase()]; var number = parseInt(match[1] + digits, base) + (increment * actionArgs.repeat); numberStr = number.toString(base); var zeroPadding = baseStr ? new Array(digits.length - numberStr.length + 1 + match[1].length).join('0') : '' if (numberStr.charAt(0) === '-') { numberStr = '-' + baseStr + zeroPadding + numberStr.substr(1); } else { numberStr = baseStr + zeroPadding + numberStr; } var from = Pos(cur.line, start); var to = Pos(cur.line, end); cm.replaceRange(numberStr, from, to); } else { return; } cm.setCursor(Pos(cur.line, 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 */); }, indent: function(cm, actionArgs) { cm.indentLine(cm.getCursor().line, actionArgs.indentRight); }, exitInsertMode: exitInsertMode }; function defineAction(name, fn) { actions[name] = fn; } /* * 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 Pos(line, 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) { if (typeof offsetLine === 'object') { offsetCh = offsetLine.ch; offsetLine = offsetLine.line; } return Pos(cur.line + offsetLine, cur.ch + offsetCh); } function getOffset(anchor, head) { return { line: head.line - anchor.line, ch: head.line - anchor.line }; } function commandMatches(keys, keyMap, context, inputState) { // Partial matches are not applied. They inform the key handler // that the current key sequence is a subsequence of a valid key // sequence, so that the key buffer is not cleared. var match, partial = [], full = []; for (var i = 0; i < keyMap.length; i++) { var command = keyMap[i]; if (context == 'insert' && command.context != 'insert' || command.context && command.context != context || inputState.operator && command.type == 'action' || !(match = commandMatch(keys, command.keys))) { continue; } if (match == 'partial') { partial.push(command); } if (match == 'full') { full.push(command); } } return { partial: partial.length && partial, full: full.length && full }; } function commandMatch(pressed, mapped) { if (mapped.slice(-11) == '<character>') { // Last character matches anything. var prefixLen = mapped.length - 11; var pressedPrefix = pressed.slice(0, prefixLen); var mappedPrefix = mapped.slice(0, prefixLen); return pressedPrefix == mappedPrefix && pressed.length > prefixLen ? 'full' : mappedPrefix.indexOf(pressedPrefix) == 0 ? 'partial' : false; } else { return pressed == mapped ? 'full' : mapped.indexOf(pressed) == 0 ? 'partial' : false; } } function lastChar(keys) { var match = /^.*(<[^>]+>)$/.exec(keys); var selectedCharacter = match ? match[1] : keys.slice(-1); if (selectedCharacter.length > 1){ switch(selectedCharacter){ case '<CR>': selectedCharacter='\n'; break; case '<Space>': selectedCharacter=' '; break; default: selectedCharacter=''; break; } } return selectedCharacter; } function repeatFn(cm, fn, repeat) { return function() { for (var i = 0; i < repeat; i++) { fn(cm); } }; } function copyCursor(cur) { return Pos(cur.line, 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 cursorMin(cur1, cur2) { if (arguments.length > 2) { cur2 = cursorMin.apply(undefined, Array.prototype.slice.call(arguments, 1)); } return cursorIsBefore(cur1, cur2) ? cur1 : cur2; } function cursorMax(cur1, cur2) { if (arguments.length > 2) { cur2 = cursorMax.apply(undefined, Array.prototype.slice.call(arguments, 1)); } return cursorIsBefore(cur1, cur2) ? cur2 : cur1; } function cursorIsBetween(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 trim(s) { if (s.trim) { return s.trim(); } return s.replace(/^\s+|\s+$/g, ''); } function escapeRegex(s) { return s.replace(/([.?*+$\[\]\/\\(){}|\-])/g, '\\$1'); } function extendLineToColumn(cm, lineNum, column) { var endCh = lineLength(cm, lineNum); var spaces = new Array(column-endCh+1).join(' '); cm.setCursor(Pos(lineNum, endCh)); cm.replaceRange(spaces, cm.getCursor()); } // This functions selects a rectangular block // of text with selectionEnd as any of its corner // Height of block: // Difference in selectionEnd.line and first/last selection.line // Width of the block: // Distance between selectionEnd.ch and any(first considered here) selection.ch function selectBlock(cm, selectionEnd) { var selections = [], ranges = cm.listSelections(); var head = copyCursor(cm.clipPos(selectionEnd)); var isClipped = !cursorEqual(selectionEnd, head); var curHead = cm.getCursor('head'); var primIndex = getIndex(ranges, curHead); var wasClipped = cursorEqual(ranges[primIndex].head, ranges[primIndex].anchor); var max = ranges.length - 1; var index = max - primIndex > primIndex ? max : 0; var base = ranges[index].anchor; var firstLine = Math.min(base.line, head.line); var lastLine = Math.max(base.line, head.line); var baseCh = base.ch, headCh = head.ch; var dir = ranges[index].head.ch - baseCh; var newDir = headCh - baseCh; if (dir > 0 && newDir <= 0) { baseCh++; if (!isClipped) { headCh--; } } else if (dir < 0 && newDir >= 0) { baseCh--; if (!wasClipped) { headCh++; } } else if (dir < 0 && newDir == -1) { baseCh--; headCh++; } for (var line = firstLine; line <= lastLine; line++) { var range = {anchor: new Pos(line, baseCh), head: new Pos(line, headCh)}; selections.push(range); } cm.setSelections(selections); selectionEnd.ch = headCh; base.ch = baseCh; return base; } function selectForInsert(cm, head, height) { var sel = []; for (var i = 0; i < height; i++) { var lineHead = offsetCursor(head, i, 0); sel.push({anchor: lineHead, head: lineHead}); } cm.setSelections(sel, 0); } // getIndex returns the index of the cursor in the selections. function getIndex(ranges, cursor, end) { for (var i = 0; i < ranges.length; i++) { var atAnchor = end != 'head' && cursorEqual(ranges[i].anchor, cursor); var atHead = end != 'anchor' && cursorEqual(ranges[i].head, cursor); if (atAnchor || atHead) { return i; } } return -1; } function getSelectedAreaRange(cm, vim) { var lastSelection = vim.lastSelection; var getCurrentSelectedAreaRange = function() { var selections = cm.listSelections(); var start = selections[0]; var end = selections[selections.length-1]; var selectionStart = cursorIsBefore(start.anchor, start.head) ? start.anchor : start.head; var selectionEnd = cursorIsBefore(end.anchor, end.head) ? end.head : end.anchor; return [selectionStart, selectionEnd]; }; var getLastSelectedAreaRange = function() { var selectionStart = cm.getCursor(); var selectionEnd = cm.getCursor(); var block = lastSelection.visualBlock; if (block) { var width = block.width; var height = block.height; selectionEnd = Pos(selectionStart.line + height, selectionStart.ch + width); var selections = []; // selectBlock creates a 'proper' rectangular block. // We do not want that in all cases, so we manually set selections. for (var i = selectionStart.line; i < selectionEnd.line; i++) { var anchor = Pos(i, selectionStart.ch); var head = Pos(i, selectionEnd.ch); var range = {anchor: anchor, head: head}; selections.push(range); } cm.setSelections(selections); } else { var start = lastSelection.anchorMark.find(); var end = lastSelection.headMark.find(); var line = end.line - start.line; var ch = end.ch - start.ch; selectionEnd = {line: selectionEnd.line + line, ch: line ? selectionEnd.ch : ch + selectionEnd.ch}; if (lastSelection.visualLine) { selectionStart = Pos(selectionStart.line, 0); selectionEnd = Pos(selectionEnd.line, lineLength(cm, selectionEnd.line)); } cm.setSelection(selectionStart, selectionEnd); } return [selectionStart, selectionEnd]; }; if (!vim.visualMode) { // In case of replaying the action. return getLastSelectedAreaRange(); } else { return getCurrentSelectedAreaRange(); } } // Updates the previous selection with the current selection's values. This // should only be called in visual mode. function updateLastSelection(cm, vim) { var anchor = vim.sel.anchor; var head = vim.sel.head; // To accommodate the effect of lastPastedText in the last selection if (vim.lastPastedText) { head = cm.posFromIndex(cm.indexFromPos(anchor) + vim.lastPastedText.length); vim.lastPastedText = null; } vim.lastSelection = {'anchorMark': cm.setBookmark(anchor), 'headMark': cm.setBookmark(head), 'anchor': copyCursor(anchor), 'head': copyCursor(head), 'visualMode': vim.visualMode, 'visualLine': vim.visualLine, 'visualBlock': vim.visualBlock}; } function expandSelection(cm, start, end) { var sel = cm.state.vim.sel; var head = sel.head; var anchor = sel.anchor; var tmp; if (cursorIsBefore(end, start)) { tmp = end; end = start; start = tmp; } if (cursorIsBefore(head, anchor)) { head = cursorMin(start, head); anchor = cursorMax(anchor, end); } else { anchor = cursorMin(start, anchor); head = cursorMax(head, end); head = offsetCursor(head, 0, -1); if (head.ch == -1 && head.line != cm.firstLine()) { head = Pos(head.line - 1, lineLength(cm, head.line - 1)); } } return [anchor, head]; } /** * Updates the CodeMirror selection to match the provided vim selection. * If no arguments are given, it uses the current vim selection state. */ function updateCmSelection(cm, sel, mode) { var vim = cm.state.vim; sel = sel || vim.sel; var mode = mode || vim.visualLine ? 'line' : vim.visualBlock ? 'block' : 'char'; var cmSel = makeCmSelection(cm, sel, mode); cm.setSelections(cmSel.ranges, cmSel.primary); updateFakeCursor(cm); } function makeCmSelection(cm, sel, mode, exclusive) { var head = copyCursor(sel.head); var anchor = copyCursor(sel.anchor); if (mode == 'char') { var headOffset = !exclusive && !cursorIsBefore(sel.head, sel.anchor) ? 1 : 0; var anchorOffset = cursorIsBefore(sel.head, sel.anchor) ? 1 : 0; head = offsetCursor(sel.head, 0, headOffset); anchor = offsetCursor(sel.anchor, 0, anchorOffset); return { ranges: [{anchor: anchor, head: head}], primary: 0 }; } else if (mode == 'line') { if (!cursorIsBefore(sel.head, sel.anchor)) { anchor.ch = 0; var lastLine = cm.lastLine(); if (head.line > lastLine) { head.line = lastLine; } head.ch = lineLength(cm, head.line); } else { head.ch = 0; anchor.ch = lineLength(cm, anchor.line); } return { ranges: [{anchor: anchor, head: head}], primary: 0 }; } else if (mode == 'block') { var top = Math.min(anchor.line, head.line), left = Math.min(anchor.ch, head.ch), bottom = Math.max(anchor.line, head.line), right = Math.max(anchor.ch, head.ch) + 1; var height = bottom - top + 1; var primary = head.line == top ? 0 : height - 1; var ranges = []; for (var i = 0; i < height; i++) { ranges.push({ anchor: Pos(top + i, left), head: Pos(top + i, right) }); } return { ranges: ranges, primary: primary }; } } function getHead(cm) { var cur = cm.getCursor('head'); if (cm.getSelection().length == 1) { // Small corner case when only 1 character is selected. The "real" // head is the left of head and anchor. cur = cursorMin(cur, cm.getCursor('anchor')); } return cur; } /** * If moveHead is set to false, the CodeMirror selection will not be * touched. The caller assumes the responsibility of putting the cursor * in the right place. */ function exitVisualMode(cm, moveHead) { var vim = cm.state.vim; if (moveHead !== false) { cm.setCursor(clipCursorToContent(cm, vim.sel.head)); } updateLastSelection(cm, vim); vim.visualMode = false; vim.visualLine = false; vim.visualBlock = false; CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"}); if (vim.fakeCursor) { vim.fakeCursor.clear(); } } // 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 whitespace. 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 = getHead(cm); 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 test = noSymbol ? wordCharTest[0] : bigWordCharTest [0]; while (!test(line.charAt(idx))) { idx++; if (idx >= line.length) { return null; } } if (bigWord) { test = bigWordCharTest[0]; } else { test = wordCharTest[0]; if (!test(line.charAt(idx))) { test = wordCharTest[1]; } } var end = idx, start = idx; while (test(line.charAt(end)) && end < line.length) { end++; } while (test(line.charAt(start)) && start >= 0) { start--; } start++; if (inclusive) { // If present, include all whitespace after word. // Otherwise, include all whitespace before word, except indentation. var wordEnd = end; while (/\s/.test(line.charAt(end)) && end < line.length) { end++; } if (wordEnd == end) { var wordStart = start; while (/\s/.test(line.charAt(start - 1)) && start > 0) { start--; } if (!start) { start = wordStart; } } } return { start: Pos(cur.line, start), end: Pos(cur.line, end) }; } function recordJumpPosition(cm, oldCur, newCur) { if (!cursorEqual(oldCur, newCur)) { vimGlobalState.jumpList.add(cm, oldCur, newCur); } } function recordLastCharacterSearch(increment, args) { vimGlobalState.lastCharacterSearch.increment = increment; vimGlobalState.lastCharacterSearch.forward = args.forward; vimGlobalState.lastCharacterSearch.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 = copyCursor(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 Pos(line, 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 charTests = bigWord ? bigWordCharTest: wordCharTest; 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 < charTests.length && !foundWord; ++i) { if (charTests[i](line.charAt(pos))) { wordStart = pos; // Advance to end of word. while (pos != stop && charTests[i](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; } } /** * @param {CodeMirror} cm CodeMirror object. * @param {Pos} cur The position to start from. * @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, cur, repeat, forward, wordEnd, bigWord) { 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 = Pos(word.line, 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 Pos(lastWord.line, lastWord.from); } else if (forward && wordEnd) { return Pos(lastWord.line, 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 Pos(lastWord.line, lastWord.to); } else { // b return Pos(lastWord.line, 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 Pos(cm.getCursor().line, 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, Pos(line, 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 findParagraph(cm, head, repeat, dir, inclusive) { var line = head.line; var min = cm.firstLine(); var max = cm.lastLine(); var start, end, i = line; function isEmpty(i) { return !cm.getLine(i); } function isBoundary(i, dir, any) { if (any) { return isEmpty(i) != isEmpty(i + dir); } return !isEmpty(i) && isEmpty(i + dir); } if (dir) { while (min <= i && i <= max && repeat > 0) { if (isBoundary(i, dir)) { repeat--; } i += dir; } return new Pos(i, 0); } var vim = cm.state.vim; if (vim.visualLine && isBoundary(line, 1, true)) { var anchor = vim.sel.anchor; if (isBoundary(anchor.line, -1, true)) { if (!inclusive || anchor.line != line) { line += 1; } } } var startState = isEmpty(line); for (i = line; i <= max && repeat; i++) { if (isBoundary(i, 1, true)) { if (!inclusive || isEmpty(i) != startState) { repeat--; } } } end = new Pos(i, 0); // select boundary before paragraph for the last one if (i > max && !startState) { startState = true; } else { inclusive = false; } for (i = line; i > min; i--) { if (!inclusive || isEmpty(i) == startState || i == line) { if (isBoundary(i, -1, true)) { break; } } } start = new Pos(i, 0); return { start: start, end: end }; } // TODO: perhaps this finagling of start and end positions belonds // in codemirror/replaceRange? function selectCompanionObject(cm, head, symb, inclusive) { var cur = head, start, end; var bracketRegexp = ({ '(': /[()]/, ')': /[()]/, '[': /[[\]]/, ']': /[[\]]/, '{': /[{}]/, '}': /[{}]/})[symb]; var openSym = ({ '(': '(', ')': '(', '[': '[', ']': '[', '{': '{', '}': '{'})[symb]; var curChar = cm.getLine(cur.line).charAt(cur.ch); // Due to the behavior of scanForBracket, we need to add an offset if the // cursor is on a matching open bracket. var offset = curChar === openSym ? 1 : 0; start = cm.scanForBracket(Pos(cur.line, cur.ch + offset), -1, null, {'bracketRegex': bracketRegexp}); end = cm.scanForBracket(Pos(cur.line, cur.ch + offset), 1, null, {'bracketRegex': bracketRegexp}); if (!start || !end) { return { start: cur, end: cur }; } start = start.pos; end = end.pos; if ((start.line == end.line && start.ch > end.ch) || (start.line > end.line)) { var tmp = start; start = end; end = tmp; } if (inclusive) { end.ch += 1; } else { start.ch += 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, head, symb, inclusive) { var cur = copyCursor(head); 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: Pos(cur.line, start), end: Pos(cur.line, end) }; } // Search functions defineOption('pcre', true, 'boolean'); 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; }, getScrollbarAnnotate: function() { return this.annotate; }, setScrollbarAnnotate: function(annotate) { this.annotate = annotate; } }; 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, selectValueOnOpen: false}); } else { onClose(prompt(shortText, '')); } } function splitBySlash(argString) { var slashes = findUnescapedSlashes(argString) || []; if (!slashes.length) return []; var tokens = []; // in case of strings like foo/bar if (slashes[0] !== 0) return; for (var i = 0; i < slashes.length; i++) { if (typeof slashes[i] == 'number') tokens.push(argString.substring(slashes[i] + 1, slashes[i+1])); } return tokens; } 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 = !escapeNextChar && (c == '\\'); } return slashes; } // Translates a search string from ex (vim) syntax into javascript form. function translateRegex(str) { // When these match, add a '\' if unescaped or remove one if escaped. var specials = '|(){'; // Remove, but never add, a '\' for these. var unescape = '}'; var escapeNextChar = false; var out = []; for (var i = -1; i < str.length; i++) { var c = str.charAt(i) || ''; var n = str.charAt(i+1) || ''; var specialComesNext = (n && specials.indexOf(n) != -1); if (escapeNextChar) { if (c !== '\\' || !specialComesNext) { out.push(c); } escapeNextChar = false; } else { if (c === '\\') { escapeNextChar = true; // Treat the unescape list as special for removing, but not adding '\'. if (n && unescape.indexOf(n) != -1) { specialComesNext = true; } // Not passing this test means removing a '\'. if (!specialComesNext || n === '\\') { out.push(c); } } else { out.push(c); if (specialComesNext && n !== '\\') { out.push('\\'); } } } } return out.join(''); } // Translates the replace part of a search and replace from ex (vim) syntax into // javascript form. Similar to translateRegex, but additionally fixes back references // (translates '\[0..9]' to '$[0..9]') and follows different rules for escaping '$'. var charUnescapes = {'\\n': '\n', '\\r': '\r', '\\t': '\t'}; function translateRegexReplace(str) { var escapeNextChar = false; var out = []; for (var i = -1; i < str.length; i++) { var c = str.charAt(i) || ''; var n = str.charAt(i+1) || ''; if (charUnescapes[c + n]) { out.push(charUnescapes[c+n]); i++; } else if (escapeNextChar) { // At any point in the loop, escapeNextChar is true if the previous // character was a '\' and was not escaped. out.push(c); escapeNextChar = false; } else { if (c === '\\') { escapeNextChar = true; if ((isNumber(n) || n === '$')) { out.push('$'); } else if (n !== '/' && n !== '\\') { out.push('\\'); } } else { if (c === '$') { out.push('$'); } out.push(c); if (n === '/') { out.push('\\'); } } } } return out.join(''); } // Unescape \ and / in the replace part, for PCRE mode. var unescapes = {'\\/': '/', '\\\\': '\\', '\\n': '\n', '\\r': '\r', '\\t': '\t'}; function unescapeRegexReplace(str) { var stream = new CodeMirror.StringStream(str); var output = []; while (!stream.eol()) { // Search for \. while (stream.peek() && stream.peek() != '\\') { output.push(stream.next()); } var matched = false; for (var matcher in unescapes) { if (stream.match(matcher, true)) { matched = true; output.push(unescapes[matcher]); break; } } if (!matched) { // Don't change anything output.push(stream.next()); } } return output.join(''); } /** * 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) { // First update the last search register var lastSearchRegister = vimGlobalState.registerController.getRegister('/'); lastSearchRegister.setText(query); // 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 (!getOption('pcre')) { regexPart = translateRegex(regexPart); } 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('<span style="color: red">' + text + '</span>', {bottom: true, duration: 5000}); } else { alert(text); } } function makePrompt(prefix, desc) { var raw = '<span style="font-family: monospace; white-space: pre">' + (prefix || "") + '<input type="text"></span>'; if (desc) raw += ' <span style="color: #888">' + desc + '</span>'; 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 searchState = getSearchState(cm); var overlay = searchState.getOverlay(); if (!overlay || query != overlay.query) { if (overlay) { cm.removeOverlay(overlay); } overlay = searchOverlay(query); cm.addOverlay(overlay); if (cm.showMatchesOnScrollbar) { if (searchState.getScrollbarAnnotate()) { searchState.getScrollbarAnnotate().clear(); } searchState.setScrollbarAnnotate(cm.showMatchesOnScrollbar(query)); } searchState.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) ? Pos(cm.lastLine()) : Pos(cm.firstLine(), 0) ); if (!cursor.find(prev)) { return; } } } return cursor.from(); }); } function clearSearchHighlight(cm) { var state = getSearchState(cm); cm.removeOverlay(getSearchState(cm).getOverlay()); state.setOverlay(null); if (state.getScrollbarAnnotate()) { state.getScrollbarAnnotate().clear(); state.setScrollbarAnnotate(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}; } function getMarkPos(cm, vim, markName) { if (markName == '\'') { var history = cm.doc.history.done; var event = history[history.length - 2]; return event && event.ranges && event.ranges[0].head; } else if (markName == '.') { if (cm.doc.history.lastModTime == 0) { return // If no changes, bail out; don't bother to copy or reverse history array. } else { var changeHistory = cm.doc.history.done.filter(function(el){ if (el.changes !== undefined) { return el } }); changeHistory.reverse(); var lastEditPos = changeHistory[0].changes[0].to; } return lastEditPos; } var mark = vim.marks[markName]; return mark && mark.find(); } var ExCommandDispatcher = function() { this.buildCommandMap_(); }; ExCommandDispatcher.prototype = { processCommand: function(cm, input, opt_params) { var that = this; cm.operation(function () { cm.curOp.isVimOp = true; that._processCommand(cm, input, opt_params); }); }, _processCommand: function(cm, input, opt_params) { var vim = cm.state.vim; var commandHistoryRegister = vimGlobalState.registerController.getRegister(':'); var previousCommand = commandHistoryRegister.toString(); if (vim.visualMode) { exitVisualMode(cm); } var inputStream = new CodeMirror.StringStream(input); // update ": with the latest command whether valid or invalid commandHistoryRegister.setText(input); var params = opt_params || {}; params.input = input; try { this.parseInput_(cm, inputStream, params); } catch(e) { showConfirm(cm, e); throw e; } var command; var commandName; if (!params.commandName) { // If only a line range is defined, move to the line. if (params.line !== undefined) { commandName = 'move'; } } else { command = this.matchCommand_(params.commandName); if (command) { commandName = command.name; if (command.excludeFromCommandHistory) { commandHistoryRegister.setText(previousCommand); } 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], 'mapping'); } 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); // Possibly asynchronous commands (e.g. substitute, which might have a // user confirmation), are responsible for calling the callback when // done. All others have it taken care of for them here. if ((!command || !command.possiblyAsync) && params.callback) { params.callback(); } } catch(e) { showConfirm(cm, e); throw 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) { // Absolute line number plus offset (N+M or N-M) is probably a typo, // not something the user actually wanted. (NB: vim does allow this.) return parseInt(numberMatch[1], 10) - 1; } switch (inputStream.next()) { case '.': return this.parseLineSpecOffset_(inputStream, cm.getCursor().line); case '$': return this.parseLineSpecOffset_(inputStream, cm.lastLine()); case '\'': var markName = inputStream.next(); var markPos = getMarkPos(cm, cm.state.vim, markName); if (!markPos) throw new Error('Mark not set'); return this.parseLineSpecOffset_(inputStream, markPos.line); case '-': case '+': inputStream.backUp(1); // Offset is relative to current line if not otherwise specified. return this.parseLineSpecOffset_(inputStream, cm.getCursor().line); default: inputStream.backUp(1); return undefined; } }, parseLineSpecOffset_: function(inputStream, line) { var offsetMatch = inputStream.match(/^([+-])?(\d+)/); if (offsetMatch) { var offset = parseInt(offsetMatch[2], 10); if (offsetMatch[1] == "-") { line -= offset; } else { line += offset; } } return line; }, parseCommandArgs_: function(inputStream, params, command) { if (inputStream.eol()) { return; } params.argString = inputStream.match(/.*/)[0]; // Parse command-line arguments var delim = command.argDelimiter || /\s+/; var args = 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, ctx) { if (lhs != ':' && lhs.charAt(0) == ':') { if (ctx) { throw Error('Mode not supported for ex mappings'); } 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), user: true }; } else { // Ex to key mapping this.commandMap_[commandName] = { name: commandName, type: 'exToKey', toKeys: rhs, user: true }; } } else { if (rhs != ':' && rhs.charAt(0) == ':') { // Key to Ex mapping. var mapping = { keys: lhs, type: 'keyToEx', exArgs: { input: rhs.substring(1) } }; if (ctx) { mapping.context = ctx; } defaultKeymap.unshift(mapping); } else { // Key to key mapping var mapping = { keys: lhs, type: 'keyToKey', toKeys: rhs }; if (ctx) { mapping.context = ctx; } defaultKeymap.unshift(mapping); } } }, unmap: function(lhs, ctx) { if (lhs != ':' && lhs.charAt(0) == ':') { // Ex to Ex or Ex to key mapping if (ctx) { throw Error('Mode not supported for ex mappings'); } var commandName = lhs.substring(1); if (this.commandMap_[commandName] && this.commandMap_[commandName].user) { delete this.commandMap_[commandName]; return; } } else { // Key to Ex or key to key mapping var keys = lhs; for (var i = 0; i < defaultKeymap.length; i++) { if (keys == defaultKeymap[i].keys && defaultKeymap[i].context === ctx) { defaultKeymap.splice(i, 1); return; } } } throw Error('No such mapping.'); } }; var exCommands = { colorscheme: function(cm, params) { if (!params.args || params.args.length < 1) { showConfirm(cm, cm.getOption('theme')); return; } cm.setOption('theme', params.args[0]); }, map: function(cm, params, ctx) { var mapArgs = params.args; if (!mapArgs || mapArgs.length < 2) { if (cm) { showConfirm(cm, 'Invalid mapping: ' + params.input); } return; } exCommandDispatcher.map(mapArgs[0], mapArgs[1], ctx); }, imap: function(cm, params) { this.map(cm, params, 'insert'); }, nmap: function(cm, params) { this.map(cm, params, 'normal'); }, vmap: function(cm, params) { this.map(cm, params, 'visual'); }, unmap: function(cm, params, ctx) { var mapArgs = params.args; if (!mapArgs || mapArgs.length < 1) { if (cm) { showConfirm(cm, 'No such mapping: ' + params.input); } return; } exCommandDispatcher.unmap(mapArgs[0], ctx); }, 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}); }, set: function(cm, params) { var setArgs = params.args; // Options passed through to the setOption/getOption calls. May be passed in by the // local/global versions of the set command var setCfg = params.setCfg || {}; if (!setArgs || setArgs.length < 1) { if (cm) { showConfirm(cm, 'Invalid mapping: ' + params.input); } return; } var expr = setArgs[0].split('='); var optionName = expr[0]; var value = expr[1]; var forceGet = false; if (optionName.charAt(optionName.length - 1) == '?') { // If post-fixed with ?, then the set is actually a get. if (value) { throw Error('Trailing characters: ' + params.argString); } optionName = optionName.substring(0, optionName.length - 1); forceGet = true; } if (value === undefined && optionName.substring(0, 2) == 'no') { // To set boolean options to false, the option name is prefixed with // 'no'. optionName = optionName.substring(2); value = false; } var optionIsBoolean = options[optionName] && options[optionName].type == 'boolean'; if (optionIsBoolean && value == undefined) { // Calling set with a boolean option sets it to true. value = true; } // If no value is provided, then we assume this is a get. if (!optionIsBoolean && value === undefined || forceGet) { var oldValue = getOption(optionName, cm, setCfg); if (oldValue instanceof Error) { showConfirm(cm, oldValue.message); } else if (oldValue === true || oldValue === false) { showConfirm(cm, ' ' + (oldValue ? '' : 'no') + optionName); } else { showConfirm(cm, ' ' + optionName + '=' + oldValue); } } else { var setOptionReturn = setOption(optionName, value, cm, setCfg); if (setOptionReturn instanceof Error) { showConfirm(cm, setOptionReturn.message); } } }, setlocal: function (cm, params) { // setCfg is passed through to setOption params.setCfg = {scope: 'local'}; this.set(cm, params); }, setglobal: function (cm, params) { // setCfg is passed through to setOption params.setCfg = {scope: 'global'}; this.set(cm, params); }, registers: function(cm, params) { var regArgs = params.args; var registers = vimGlobalState.registerController.registers; var regInfo = '----------Registers----------<br><br>'; if (!regArgs) { for (var registerName in registers) { var text = registers[registerName].toString(); if (text.length) { regInfo += '"' + registerName + ' ' + text + '<br>'; } } } else { var registerName; regArgs = regArgs.join(''); for (var i = 0; i < regArgs.length; i++) { registerName = regArgs.charAt(i); if (!vimGlobalState.registerController.isValidRegister(registerName)) { continue; } var register = registers[registerName] || new Register(); regInfo += '"' + registerName + ' ' + register.toString() + '<br>'; } } showConfirm(cm, regInfo); }, sort: function(cm, params) { var reverse, ignoreCase, unique, number, pattern; function parseArgs() { if (params.argString) { var args = new CodeMirror.StringStream(params.argString); if (args.eat('!')) { reverse = true; } if (args.eol()) { return; } if (!args.eatSpace()) { return 'Invalid arguments'; } var opts = args.match(/([dinuox]+)?\s*(\/.+\/)?\s*/); if (!opts && !args.eol()) { return 'Invalid arguments'; } if (opts[1]) { ignoreCase = opts[1].indexOf('i') != -1; unique = opts[1].indexOf('u') != -1; var decimal = opts[1].indexOf('d') != -1 || opts[1].indexOf('n') != -1 && 1; var hex = opts[1].indexOf('x') != -1 && 1; var octal = opts[1].indexOf('o') != -1 && 1; if (decimal + hex + octal > 1) { return 'Invalid arguments'; } number = decimal && 'decimal' || hex && 'hex' || octal && 'octal'; } if (opts[2]) { pattern = new RegExp(opts[2].substr(1, opts[2].length - 2), ignoreCase ? 'i' : ''); } } } var err = parseArgs(); if (err) { showConfirm(cm, err + ': ' + params.argString); return; } var lineStart = params.line || cm.firstLine(); var lineEnd = params.lineEnd || params.line || cm.lastLine(); if (lineStart == lineEnd) { return; } var curStart = Pos(lineStart, 0); var curEnd = Pos(lineEnd, lineLength(cm, lineEnd)); var text = cm.getRange(curStart, curEnd).split('\n'); var numberRegex = pattern ? pattern : (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 || pattern) { for (var i = 0; i < text.length; i++) { var matchPart = pattern ? text[i].match(pattern) : null; if (matchPart && matchPart[0] != '') { numPart.push(matchPart); } else if (!pattern && 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; } function comparePatternFn(a, b) { if (reverse) { var tmp; tmp = a; a = b; b = tmp; } if (ignoreCase) { a[0] = a[0].toLowerCase(); b[0] = b[0].toLowerCase(); } return (a[0] < b[0]) ? -1 : 1; } numPart.sort(pattern ? comparePatternFn : compareFn); if (pattern) { for (var i = 0; i < numPart.length; i++) { numPart[i] = numPart[i].input; } } else if (!number) { 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); }, global: function(cm, params) { // a global command is of the form // :[range]g/pattern/[cmd] // argString holds the string /pattern/[cmd] var argString = params.argString; if (!argString) { showConfirm(cm, 'Regular Expression missing from global'); return; } // range is specified here var lineStart = (params.line !== undefined) ? params.line : cm.firstLine(); var lineEnd = params.lineEnd || params.line || cm.lastLine(); // get the tokens from argString var tokens = splitBySlash(argString); var regexPart = argString, cmd; if (tokens.length) { regexPart = tokens[0]; cmd = tokens.slice(1, tokens.length).join('/'); } 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; } } // now that we have the regexPart, search for regex matches in the // specified range of lines var query = getSearchState(cm).getQuery(); var matchedLines = [], content = ''; for (var i = lineStart; i <= lineEnd; i++) { var matched = query.test(cm.getLine(i)); if (matched) { matchedLines.push(i+1); content+= cm.getLine(i) + '<br>'; } } // if there is no [cmd], just display the list of matched lines if (!cmd) { showConfirm(cm, content); return; } var index = 0; var nextCommand = function() { if (index < matchedLines.length) { var command = matchedLines[index] + cmd; exCommandDispatcher.processCommand(cm, command, { callback: nextCommand }); } index++; }; nextCommand(); }, 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 tokens = argString ? splitBySlash(argString) : []; var regexPart, replacePart = '', trailing, flagsPart, count; var confirm = false; // Whether to confirm each replace. var global = false; // True to replace all instances on a line, false to replace only 1. if (tokens.length) { regexPart = tokens[0]; replacePart = tokens[1]; if (regexPart && regexPart[regexPart.length - 1] === '$') { regexPart = regexPart.slice(0, regexPart.length - 1) + '\\n'; replacePart = replacePart ? replacePart + '\n' : '\n'; } if (replacePart !== undefined) { if (getOption('pcre')) { replacePart = unescapeRegexReplace(replacePart); } else { replacePart = translateRegexReplace(replacePart); } vimGlobalState.lastSubstituteReplacePart = replacePart; } trailing = tokens[2] ? tokens[2].split(' ') : []; } else { // either the argString is empty or its of the form ' hello/world' // actually splitBySlash returns a list of tokens // only if the string starts with a '/' if (argString && argString.length) { showConfirm(cm, 'Substitutions should be of the form ' + ':s/pattern/replace/'); return; } } // After the 3rd slash, we can have flags followed by a space followed // by count. if (trailing) { flagsPart = trailing[0]; count = parseInt(trailing[1]); if (flagsPart) { if (flagsPart.indexOf('c') != -1) { confirm = true; flagsPart.replace('c', ''); } if (flagsPart.indexOf('g') != -1) { global = true; flagsPart.replace('g', ''); } 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; } } replacePart = replacePart || vimGlobalState.lastSubstituteReplacePart; if (replacePart === undefined) { showConfirm(cm, 'No previous substitute regular expression'); 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 (lineStart == cm.firstLine() && lineEnd == cm.lastLine()) { lineEnd = Infinity; } if (count) { lineStart = lineEnd; lineEnd = lineStart + count - 1; } var startPos = clipCursorToContent(cm, Pos(lineStart, 0)); var cursor = cm.getSearchCursor(query, startPos); doReplace(cm, confirm, global, lineStart, lineEnd, cursor, query, replacePart, params.callback); }, 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 if (cm.save) { // Saves to text area if no save command is defined and cm.save() is available. cm.save(); } }, nohlsearch: function(cm) { clearSearchHighlight(cm); }, yank: function (cm) { var cur = copyCursor(cm.getCursor()); var line = cur.line; var lineText = cm.getLine(line); vimGlobalState.registerController.pushText( '0', 'yank', lineText, true, true); }, delmarks: function(cm, params) { if (!params.argString || !trim(params.argString)) { showConfirm(cm, 'Argument required'); return; } var state = cm.state.vim; var stream = new CodeMirror.StringStream(trim(params.argString)); 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 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. * @param {function()} callback A callback for when the replace is done. */ function doReplace(cm, confirm, global, lineStart, lineEnd, searchCursor, query, replaceWith, callback) { // 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() { // The below only loops to skip over multiple occurrences on the same // line when 'global' is not true. while(searchCursor.findNext() && isInRange(searchCursor.from(), lineStart, lineEnd)) { if (!global && lastPos && searchCursor.from().line == lastPos.line) { continue; } cm.scrollIntoView(searchCursor.from(), 30); cm.setSelection(searchCursor.from(), searchCursor.to()); lastPos = searchCursor.from(); done = false; return; } 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; } if (callback) { callback(); } } 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': // replaceAll contains a call to close of its own. We don't want it // to fire too early or multiple times. var savedCallback = callback; callback = undefined; cm.operation(replaceAll); callback = savedCallback; break; case 'L': replace(); // fall through and exit. case 'Q': case 'Esc': case 'Ctrl-C': case 'Ctrl-[': stop(close); break; } if (done) { stop(close); } return true; } // Actually do replace. next(); if (done) { showConfirm(cm, 'No matches for ' + query.source); return; } if (!confirm) { replaceAll(); if (callback) { callback(); }; return; } showPrompt(cm, { prefix: 'replace with <strong>' + replaceWith + '</strong> (y/n/a/q/l)', onKeyDown: onPromptKeyDown }); } CodeMirror.keyMap.vim = { attach: attachVimMap, detach: detachVimMap, call: cmKey }; function exitInsertMode(cm) { var vim = cm.state.vim; var macroModeState = vimGlobalState.macroModeState; var insertModeChangeRegister = vimGlobalState.registerController.getRegister('.'); var isPlaying = macroModeState.isPlaying; var lastChange = macroModeState.lastInsertModeChanges; // In case of visual block, the insertModeChanges are not saved as a // single word, so we convert them to a single word // so as to update the ". register as expected in real vim. var text = []; if (!isPlaying) { var selLength = lastChange.inVisualBlock && vim.lastSelection ? vim.lastSelection.visualBlock.height : 1; var changes = lastChange.changes; var text = []; var i = 0; // In case of multiple selections in blockwise visual, // the inserted text, for example: 'f<Backspace>oo', is stored as // 'f', 'f', InsertModeKey 'o', 'o', 'o', 'o'. (if you have a block with 2 lines). // We push the contents of the changes array as per the following: // 1. In case of InsertModeKey, just increment by 1. // 2. In case of a character, jump by selLength (2 in the example). while (i < changes.length) { // This loop will convert 'ff<bs>oooo' to 'f<bs>oo'. text.push(changes[i]); if (changes[i] instanceof InsertModeKey) { i++; } else { i+= selLength; } } lastChange.changes = text; cm.off('change', onChange); CodeMirror.off(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown); } if (!isPlaying && 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; vim.insertMode = false; cm.setCursor(cm.getCursor().line, cm.getCursor().ch-1); cm.setOption('keyMap', 'vim'); cm.setOption('disableInput', true); cm.toggleOverwrite(false); // exit replace mode if we were in it. // update the ". register before exiting insert mode insertModeChangeRegister.setText(lastChange.changes.join('')); CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"}); if (macroModeState.isRecording) { logInsertModeChange(macroModeState); } } function _mapCommand(command) { defaultKeymap.unshift(command); } function mapCommand(keys, type, name, args, extra) { var command = {keys: keys, type: type}; command[type] = name; command[type + "Args"] = args; for (var key in extra) command[key] = extra[key]; _mapCommand(command); } // The timeout in milliseconds for the two-character ESC keymap should be // adjusted according to your typing speed to prevent false positives. defineOption('insertModeEscKeysTimeout', 200, 'number'); CodeMirror.keyMap['vim-insert'] = { // TODO: override navigation keys so that Esc will cancel automatic // indentation from o, O, i_<CR> fallthrough: ['default'], attach: attachVimMap, detach: detachVimMap, call: cmKey }; CodeMirror.keyMap['vim-replace'] = { 'Backspace': 'goCharLeft', fallthrough: ['vim-insert'], attach: attachVimMap, detach: detachVimMap, call: cmKey }; function executeMacroRegister(cm, vim, macroModeState, registerName) { var register = vimGlobalState.registerController.getRegister(registerName); if (registerName == ':') { // Read-only register containing last Ex command. if (register.keyBuffer[0]) { exCommandDispatcher.processCommand(cm, register.keyBuffer[0]); } macroModeState.isPlaying = false; return; } var keyBuffer = register.keyBuffer; var imc = 0; macroModeState.isPlaying = true; macroModeState.replaySearchQueries = register.searchQueries.slice(0); for (var i = 0; i < keyBuffer.length; i++) { var text = keyBuffer[i]; var match, key; while (text) { // Pull off one command key, which is either a single character // or a special sequence wrapped in '<' and '>', e.g. '<Space>'. match = (/<\w+-.+?>|<\w+>|./).exec(text); key = match[0]; text = text.substring(match.index + key.length); CodeMirror.Vim.handleKey(cm, key, 'macro'); if (vim.insertMode) { var changes = register.insertModeChanges[imc++].changes; vimGlobalState.macroModeState.lastInsertModeChanges.changes = changes; repeatInsertModeChanges(cm, changes, 1); exitInsertMode(cm); } } }; macroModeState.isPlaying = false; } function logKey(macroModeState, key) { if (macroModeState.isPlaying) { return; } var registerName = macroModeState.latestRegister; var register = vimGlobalState.registerController.getRegister(registerName); if (register) { register.pushText(key); } } function logInsertModeChange(macroModeState) { if (macroModeState.isPlaying) { return; } var registerName = macroModeState.latestRegister; var register = vimGlobalState.registerController.getRegister(registerName); if (register && register.pushInsertModeChanges) { register.pushInsertModeChanges(macroModeState.lastInsertModeChanges); } } function logSearchQuery(macroModeState, query) { if (macroModeState.isPlaying) { return; } var registerName = macroModeState.latestRegister; var register = vimGlobalState.registerController.getRegister(registerName); if (register && register.pushSearchQuery) { register.pushSearchQuery(query); } } /** * 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; if (!macroModeState.isPlaying) { while(changeObj) { lastChange.expectCursorActivityForChange = true; if (changeObj.origin == '+input' || changeObj.origin == 'paste' || changeObj.origin === undefined /* only in testing */) { var text = changeObj.text.join('\n'); if (lastChange.maybeReset) { lastChange.changes = []; lastChange.maybeReset = false; } if (cm.state.overwrite && !/\n/.test(text)) { lastChange.changes.push([text]); } else { lastChange.changes.push(text); } } // Change objects may be chained with next. changeObj = changeObj.next; } } } /** * Listens for any kind of cursor activity on CodeMirror. */ function onCursorActivity(cm) { var vim = cm.state.vim; if (vim.insertMode) { // Tracking cursor activity in insert mode (for macro support). var macroModeState = vimGlobalState.macroModeState; if (macroModeState.isPlaying) { return; } var lastChange = macroModeState.lastInsertModeChanges; if (lastChange.expectCursorActivityForChange) { lastChange.expectCursorActivityForChange = false; } else { // Cursor moved outside the context of an edit. Reset the change. lastChange.maybeReset = true; } } else if (!cm.curOp.isVimOp) { handleExternalSelection(cm, vim); } if (vim.visualMode) { updateFakeCursor(cm); } } function updateFakeCursor(cm) { var vim = cm.state.vim; var from = clipCursorToContent(cm, copyCursor(vim.sel.head)); var to = offsetCursor(from, 0, 1); if (vim.fakeCursor) { vim.fakeCursor.clear(); } vim.fakeCursor = cm.markText(from, to, {className: 'cm-animate-fat-cursor'}); } function handleExternalSelection(cm, vim) { var anchor = cm.getCursor('anchor'); var head = cm.getCursor('head'); // Enter or exit visual mode to match mouse selection. if (vim.visualMode && !cm.somethingSelected()) { exitVisualMode(cm, false); } else if (!vim.visualMode && !vim.insertMode && cm.somethingSelected()) { vim.visualMode = true; vim.visualLine = false; CodeMirror.signal(cm, "vim-mode-change", {mode: "visual"}); } if (vim.visualMode) { // Bind CodeMirror selection model to vim selection model. // Mouse selections are considered visual characterwise. var headOffset = !cursorIsBefore(head, anchor) ? -1 : 0; var anchorOffset = cursorIsBefore(head, anchor) ? -1 : 0; head = offsetCursor(head, 0, headOffset); anchor = offsetCursor(anchor, 0, anchorOffset); vim.sel = { anchor: anchor, head: head }; updateMark(cm, vim, '<', cursorMin(head, anchor)); updateMark(cm, vim, '>', cursorMax(head, anchor)); } else if (!vim.insertMode) { // Reset lastHPos if selection was modified by something outside of vim mode e.g. by mouse. vim.lastHPos = cm.getCursor().ch; } } /** 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); if (!keyName) { return; } function onKeyFound() { if (lastChange.maybeReset) { lastChange.changes = []; lastChange.maybeReset = false; } 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.isPlaying = 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 does not repeat // insert mode changes. Will conform to that behavior. repeat = !vim.lastEditActionCommand ? 1 : repeat; var changeObject = macroModeState.lastInsertModeChanges; repeatInsertModeChanges(cm, changeObject.changes, repeat); } } 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.isPlaying = false; }; function repeatInsertModeChanges(cm, changes, repeat) { function keyHandler(binding) { if (typeof binding == 'string') { CodeMirror.commands[binding](cm); } else { binding(cm); } return true; } var head = cm.getCursor('head'); var inVisualBlock = vimGlobalState.macroModeState.lastInsertModeChanges.inVisualBlock; if (inVisualBlock) { // Set up block selection again for repeating the changes. var vim = cm.state.vim; var lastSel = vim.lastSelection; var offset = getOffset(lastSel.anchor, lastSel.head); selectForInsert(cm, head, offset.line + 1); repeat = cm.listSelections().length; cm.setCursor(head); } for (var i = 0; i < repeat; i++) { if (inVisualBlock) { cm.setCursor(offsetCursor(head, i, 0)); } for (var j = 0; j < changes.length; j++) { var change = changes[j]; if (change instanceof InsertModeKey) { CodeMirror.lookupKey(change.keyName, 'vim-insert', keyHandler); } else if (typeof change == "string") { var cur = cm.getCursor(); cm.replaceRange(change, cur, cur); } else { var start = cm.getCursor(); var end = offsetCursor(start, 0, change[0].length); cm.replaceRange(change[0], start, end); } } } if (inVisualBlock) { cm.setCursor(offsetCursor(head, 0, 1)); } } resetVimGlobalState(); return vimApi; }; // Initialize Vim and make it available as an API. CodeMirror.Vim = Vim(); }); // CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: http://codemirror.net/LICENSE (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS mod(require("../lib/codemirror")); else if (typeof define == "function" && define.amd) // AMD define(["../lib/codemirror"], mod); else // Plain browser env mod(CodeMirror); })(function(CodeMirror) { "use strict"; CodeMirror.modeInfo = [ {name: "APL", mime: "text/apl", mode: "apl", ext: ["dyalog", "apl"]}, {name: "PGP", mimes: ["application/pgp", "application/pgp-encrypted", "application/pgp-keys", "application/pgp-signature"], mode: "asciiarmor", ext: ["asc", "pgp", "sig"]}, {name: "ASN.1", mime: "text/x-ttcn-asn", mode: "asn.1", ext: ["asn", "asn1"]}, {name: "Asterisk", mime: "text/x-asterisk", mode: "asterisk", file: /^extensions\.conf$/i}, {name: "Brainfuck", mime: "text/x-brainfuck", mode: "brainfuck", ext: ["b", "bf"]}, {name: "C", mime: "text/x-csrc", mode: "clike", ext: ["c", "h", "ino"]}, {name: "C++", mime: "text/x-c++src", mode: "clike", ext: ["cpp", "c++", "cc", "cxx", "hpp", "h++", "hh", "hxx"], alias: ["cpp"]}, {name: "Cobol", mime: "text/x-cobol", mode: "cobol", ext: ["cob", "cpy"]}, {name: "C#", mime: "text/x-csharp", mode: "clike", ext: ["cs"], alias: ["csharp"]}, {name: "Clojure", mime: "text/x-clojure", mode: "clojure", ext: ["clj", "cljc", "cljx"]}, {name: "ClojureScript", mime: "text/x-clojurescript", mode: "clojure", ext: ["cljs"]}, {name: "Closure Stylesheets (GSS)", mime: "text/x-gss", mode: "css", ext: ["gss"]}, {name: "CMake", mime: "text/x-cmake", mode: "cmake", ext: ["cmake", "cmake.in"], file: /^CMakeLists.txt$/}, {name: "CoffeeScript", mimes: ["application/vnd.coffeescript", "text/coffeescript", "text/x-coffeescript"], mode: "coffeescript", ext: ["coffee"], alias: ["coffee", "coffee-script"]}, {name: "Common Lisp", mime: "text/x-common-lisp", mode: "commonlisp", ext: ["cl", "lisp", "el"], alias: ["lisp"]}, {name: "Cypher", mime: "application/x-cypher-query", mode: "cypher", ext: ["cyp", "cypher"]}, {name: "Cython", mime: "text/x-cython", mode: "python", ext: ["pyx", "pxd", "pxi"]}, {name: "Crystal", mime: "text/x-crystal", mode: "crystal", ext: ["cr"]}, {name: "CSS", mime: "text/css", mode: "css", ext: ["css"]}, {name: "CQL", mime: "text/x-cassandra", mode: "sql", ext: ["cql"]}, {name: "D", mime: "text/x-d", mode: "d", ext: ["d"]}, {name: "Dart", mimes: ["application/dart", "text/x-dart"], mode: "dart", ext: ["dart"]}, {name: "diff", mime: "text/x-diff", mode: "diff", ext: ["diff", "patch"]}, {name: "Django", mime: "text/x-django", mode: "django"}, {name: "Dockerfile", mime: "text/x-dockerfile", mode: "dockerfile", file: /^Dockerfile$/}, {name: "DTD", mime: "application/xml-dtd", mode: "dtd", ext: ["dtd"]}, {name: "Dylan", mime: "text/x-dylan", mode: "dylan", ext: ["dylan", "dyl", "intr"]}, {name: "EBNF", mime: "text/x-ebnf", mode: "ebnf"}, {name: "ECL", mime: "text/x-ecl", mode: "ecl", ext: ["ecl"]}, {name: "edn", mime: "application/edn", mode: "clojure", ext: ["edn"]}, {name: "Eiffel", mime: "text/x-eiffel", mode: "eiffel", ext: ["e"]}, {name: "Elm", mime: "text/x-elm", mode: "elm", ext: ["elm"]}, {name: "Embedded Javascript", mime: "application/x-ejs", mode: "htmlembedded", ext: ["ejs"]}, {name: "Embedded Ruby", mime: "application/x-erb", mode: "htmlembedded", ext: ["erb"]}, {name: "Erlang", mime: "text/x-erlang", mode: "erlang", ext: ["erl"]}, {name: "Esper", mime: "text/x-esper", mode: "sql"}, {name: "Factor", mime: "text/x-factor", mode: "factor", ext: ["factor"]}, {name: "FCL", mime: "text/x-fcl", mode: "fcl"}, {name: "Forth", mime: "text/x-forth", mode: "forth", ext: ["forth", "fth", "4th"]}, {name: "Fortran", mime: "text/x-fortran", mode: "fortran", ext: ["f", "for", "f77", "f90"]}, {name: "F#", mime: "text/x-fsharp", mode: "mllike", ext: ["fs"], alias: ["fsharp"]}, {name: "Gas", mime: "text/x-gas", mode: "gas", ext: ["s"]}, {name: "Gherkin", mime: "text/x-feature", mode: "gherkin", ext: ["feature"]}, {name: "GitHub Flavored Markdown", mime: "text/x-gfm", mode: "gfm", file: /^(readme|contributing|history).md$/i}, {name: "Go", mime: "text/x-go", mode: "go", ext: ["go"]}, {name: "Groovy", mime: "text/x-groovy", mode: "groovy", ext: ["groovy", "gradle"], file: /^Jenkinsfile$/}, {name: "HAML", mime: "text/x-haml", mode: "haml", ext: ["haml"]}, {name: "Haskell", mime: "text/x-haskell", mode: "haskell", ext: ["hs"]}, {name: "Haskell (Literate)", mime: "text/x-literate-haskell", mode: "haskell-literate", ext: ["lhs"]}, {name: "Haxe", mime: "text/x-haxe", mode: "haxe", ext: ["hx"]}, {name: "HXML", mime: "text/x-hxml", mode: "haxe", ext: ["hxml"]}, {name: "ASP.NET", mime: "application/x-aspx", mode: "htmlembedded", ext: ["aspx"], alias: ["asp", "aspx"]}, {name: "HTML", mime: "text/html", mode: "htmlmixed", ext: ["html", "htm", "handlebars", "hbs"], alias: ["xhtml"]}, {name: "HTTP", mime: "message/http", mode: "http"}, {name: "IDL", mime: "text/x-idl", mode: "idl", ext: ["pro"]}, {name: "Pug", mime: "text/x-pug", mode: "pug", ext: ["jade", "pug"], alias: ["jade"]}, {name: "Java", mime: "text/x-java", mode: "clike", ext: ["java"]}, {name: "Java Server Pages", mime: "application/x-jsp", mode: "htmlembedded", ext: ["jsp"], alias: ["jsp"]}, {name: "JavaScript", mimes: ["text/javascript", "text/ecmascript", "application/javascript", "application/x-javascript", "application/ecmascript"], mode: "javascript", ext: ["js"], alias: ["ecmascript", "js", "node"]}, {name: "JSON", mimes: ["application/json", "application/x-json"], mode: "javascript", ext: ["json", "map"], alias: ["json5"]}, {name: "JSON-LD", mime: "application/ld+json", mode: "javascript", ext: ["jsonld"], alias: ["jsonld"]}, {name: "JSX", mime: "text/jsx", mode: "jsx", ext: ["jsx"]}, {name: "Jinja2", mime: "null", mode: "jinja2"}, {name: "Julia", mime: "text/x-julia", mode: "julia", ext: ["jl"]}, {name: "Kotlin", mime: "text/x-kotlin", mode: "clike", ext: ["kt"]}, {name: "LESS", mime: "text/x-less", mode: "css", ext: ["less"]}, {name: "LiveScript", mime: "text/x-livescript", mode: "livescript", ext: ["ls"], alias: ["ls"]}, {name: "Lua", mime: "text/x-lua", mode: "lua", ext: ["lua"]}, {name: "Markdown", mime: "text/x-markdown", mode: "markdown", ext: ["markdown", "md", "mkd"]}, {name: "mIRC", mime: "text/mirc", mode: "mirc"}, {name: "MariaDB SQL", mime: "text/x-mariadb", mode: "sql"}, {name: "Mathematica", mime: "text/x-mathematica", mode: "mathematica", ext: ["m", "nb"]}, {name: "Modelica", mime: "text/x-modelica", mode: "modelica", ext: ["mo"]}, {name: "MUMPS", mime: "text/x-mumps", mode: "mumps", ext: ["mps"]}, {name: "MS SQL", mime: "text/x-mssql", mode: "sql"}, {name: "mbox", mime: "application/mbox", mode: "mbox", ext: ["mbox"]}, {name: "MySQL", mime: "text/x-mysql", mode: "sql"}, {name: "Nginx", mime: "text/x-nginx-conf", mode: "nginx", file: /nginx.*\.conf$/i}, {name: "NSIS", mime: "text/x-nsis", mode: "nsis", ext: ["nsh", "nsi"]}, {name: "NTriples", mimes: ["application/n-triples", "application/n-quads", "text/n-triples"], mode: "ntriples", ext: ["nt", "nq"]}, {name: "Objective-C", mime: "text/x-objectivec", mode: "clike", ext: ["m", "mm"], alias: ["objective-c", "objc"]}, {name: "OCaml", mime: "text/x-ocaml", mode: "mllike", ext: ["ml", "mli", "mll", "mly"]}, {name: "Octave", mime: "text/x-octave", mode: "octave", ext: ["m"]}, {name: "Oz", mime: "text/x-oz", mode: "oz", ext: ["oz"]}, {name: "Pascal", mime: "text/x-pascal", mode: "pascal", ext: ["p", "pas"]}, {name: "PEG.js", mime: "null", mode: "pegjs", ext: ["jsonld"]}, {name: "Perl", mime: "text/x-perl", mode: "perl", ext: ["pl", "pm"]}, {name: "PHP", mimes: ["text/x-php", "application/x-httpd-php", "application/x-httpd-php-open"], mode: "php", ext: ["php", "php3", "php4", "php5", "php7", "phtml"]}, {name: "Pig", mime: "text/x-pig", mode: "pig", ext: ["pig"]}, {name: "Plain Text", mime: "text/plain", mode: "null", ext: ["txt", "text", "conf", "def", "list", "log"]}, {name: "PLSQL", mime: "text/x-plsql", mode: "sql", ext: ["pls"]}, {name: "PowerShell", mime: "application/x-powershell", mode: "powershell", ext: ["ps1", "psd1", "psm1"]}, {name: "Properties files", mime: "text/x-properties", mode: "properties", ext: ["properties", "ini", "in"], alias: ["ini", "properties"]}, {name: "ProtoBuf", mime: "text/x-protobuf", mode: "protobuf", ext: ["proto"]}, {name: "Python", mime: "text/x-python", mode: "python", ext: ["BUILD", "bzl", "py", "pyw"], file: /^(BUCK|BUILD)$/}, {name: "Puppet", mime: "text/x-puppet", mode: "puppet", ext: ["pp"]}, {name: "Q", mime: "text/x-q", mode: "q", ext: ["q"]}, {name: "R", mime: "text/x-rsrc", mode: "r", ext: ["r", "R"], alias: ["rscript"]}, {name: "reStructuredText", mime: "text/x-rst", mode: "rst", ext: ["rst"], alias: ["rst"]}, {name: "RPM Changes", mime: "text/x-rpm-changes", mode: "rpm"}, {name: "RPM Spec", mime: "text/x-rpm-spec", mode: "rpm", ext: ["spec"]}, {name: "Ruby", mime: "text/x-ruby", mode: "ruby", ext: ["rb"], alias: ["jruby", "macruby", "rake", "rb", "rbx"]}, {name: "Rust", mime: "text/x-rustsrc", mode: "rust", ext: ["rs"]}, {name: "SAS", mime: "text/x-sas", mode: "sas", ext: ["sas"]}, {name: "Sass", mime: "text/x-sass", mode: "sass", ext: ["sass"]}, {name: "Scala", mime: "text/x-scala", mode: "clike", ext: ["scala"]}, {name: "Scheme", mime: "text/x-scheme", mode: "scheme", ext: ["scm", "ss"]}, {name: "SCSS", mime: "text/x-scss", mode: "css", ext: ["scss"]}, {name: "Shell", mimes: ["text/x-sh", "application/x-sh"], mode: "shell", ext: ["sh", "ksh", "bash"], alias: ["bash", "sh", "zsh"], file: /^PKGBUILD$/}, {name: "Sieve", mime: "application/sieve", mode: "sieve", ext: ["siv", "sieve"]}, {name: "Slim", mimes: ["text/x-slim", "application/x-slim"], mode: "slim", ext: ["slim"]}, {name: "Smalltalk", mime: "text/x-stsrc", mode: "smalltalk", ext: ["st"]}, {name: "Smarty", mime: "text/x-smarty", mode: "smarty", ext: ["tpl"]}, {name: "Solr", mime: "text/x-solr", mode: "solr"}, {name: "SML", mime: "text/x-sml", mode: "mllike", ext: ["sml", "sig", "fun", "smackspec"]}, {name: "Soy", mime: "text/x-soy", mode: "soy", ext: ["soy"], alias: ["closure template"]}, {name: "SPARQL", mime: "application/sparql-query", mode: "sparql", ext: ["rq", "sparql"], alias: ["sparul"]}, {name: "Spreadsheet", mime: "text/x-spreadsheet", mode: "spreadsheet", alias: ["excel", "formula"]}, {name: "SQL", mime: "text/x-sql", mode: "sql", ext: ["sql"]}, {name: "SQLite", mime: "text/x-sqlite", mode: "sql"}, {name: "Squirrel", mime: "text/x-squirrel", mode: "clike", ext: ["nut"]}, {name: "Stylus", mime: "text/x-styl", mode: "stylus", ext: ["styl"]}, {name: "Swift", mime: "text/x-swift", mode: "swift", ext: ["swift"]}, {name: "sTeX", mime: "text/x-stex", mode: "stex"}, {name: "LaTeX", mime: "text/x-latex", mode: "stex", ext: ["text", "ltx"], alias: ["tex"]}, {name: "SystemVerilog", mime: "text/x-systemverilog", mode: "verilog", ext: ["v", "sv", "svh"]}, {name: "Tcl", mime: "text/x-tcl", mode: "tcl", ext: ["tcl"]}, {name: "Textile", mime: "text/x-textile", mode: "textile", ext: ["textile"]}, {name: "TiddlyWiki ", mime: "text/x-tiddlywiki", mode: "tiddlywiki"}, {name: "Tiki wiki", mime: "text/tiki", mode: "tiki"}, {name: "TOML", mime: "text/x-toml", mode: "toml", ext: ["toml"]}, {name: "Tornado", mime: "text/x-tornado", mode: "tornado"}, {name: "troff", mime: "text/troff", mode: "troff", ext: ["1", "2", "3", "4", "5", "6", "7", "8", "9"]}, {name: "TTCN", mime: "text/x-ttcn", mode: "ttcn", ext: ["ttcn", "ttcn3", "ttcnpp"]}, {name: "TTCN_CFG", mime: "text/x-ttcn-cfg", mode: "ttcn-cfg", ext: ["cfg"]}, {name: "Turtle", mime: "text/turtle", mode: "turtle", ext: ["ttl"]}, {name: "TypeScript", mime: "application/typescript", mode: "javascript", ext: ["ts"], alias: ["ts"]}, {name: "TypeScript-JSX", mime: "text/typescript-jsx", mode: "jsx", ext: ["tsx"], alias: ["tsx"]}, {name: "Twig", mime: "text/x-twig", mode: "twig"}, {name: "Web IDL", mime: "text/x-webidl", mode: "webidl", ext: ["webidl"]}, {name: "VB.NET", mime: "text/x-vb", mode: "vb", ext: ["vb"]}, {name: "VBScript", mime: "text/vbscript", mode: "vbscript", ext: ["vbs"]}, {name: "Velocity", mime: "text/velocity", mode: "velocity", ext: ["vtl"]}, {name: "Verilog", mime: "text/x-verilog", mode: "verilog", ext: ["v"]}, {name: "VHDL", mime: "text/x-vhdl", mode: "vhdl", ext: ["vhd", "vhdl"]}, {name: "Vue.js Component", mimes: ["script/x-vue", "text/x-vue"], mode: "vue", ext: ["vue"]}, {name: "XML", mimes: ["application/xml", "text/xml"], mode: "xml", ext: ["xml", "xsl", "xsd", "svg"], alias: ["rss", "wsdl", "xsd"]}, {name: "XQuery", mime: "application/xquery", mode: "xquery", ext: ["xy", "xquery"]}, {name: "Yacas", mime: "text/x-yacas", mode: "yacas", ext: ["ys"]}, {name: "YAML", mimes: ["text/x-yaml", "text/yaml"], mode: "yaml", ext: ["yaml", "yml"], alias: ["yml"]}, {name: "Z80", mime: "text/x-z80", mode: "z80", ext: ["z80"]}, {name: "mscgen", mime: "text/x-mscgen", mode: "mscgen", ext: ["mscgen", "mscin", "msc"]}, {name: "xu", mime: "text/x-xu", mode: "mscgen", ext: ["xu"]}, {name: "msgenny", mime: "text/x-msgenny", mode: "mscgen", ext: ["msgenny"]} ]; // Ensure all modes have a mime property for backwards compatibility for (var i = 0; i < CodeMirror.modeInfo.length; i++) { var info = CodeMirror.modeInfo[i]; if (info.mimes) info.mime = info.mimes[0]; } CodeMirror.findModeByMIME = function(mime) { mime = mime.toLowerCase(); for (var i = 0; i < CodeMirror.modeInfo.length; i++) { var info = CodeMirror.modeInfo[i]; if (info.mime == mime) return info; if (info.mimes) for (var j = 0; j < info.mimes.length; j++) if (info.mimes[j] == mime) return info; } if (/\+xml$/.test(mime)) return CodeMirror.findModeByMIME("application/xml") if (/\+json$/.test(mime)) return CodeMirror.findModeByMIME("application/json") }; CodeMirror.findModeByExtension = function(ext) { for (var i = 0; i < CodeMirror.modeInfo.length; i++) { var info = CodeMirror.modeInfo[i]; if (info.ext) for (var j = 0; j < info.ext.length; j++) if (info.ext[j] == ext) return info; } }; CodeMirror.findModeByFileName = function(filename) { for (var i = 0; i < CodeMirror.modeInfo.length; i++) { var info = CodeMirror.modeInfo[i]; if (info.file && info.file.test(filename)) return info; } var dot = filename.lastIndexOf("."); var ext = dot > -1 && filename.substring(dot + 1, filename.length); if (ext) return CodeMirror.findModeByExtension(ext); }; CodeMirror.findModeByName = function(name) { name = name.toLowerCase(); for (var i = 0; i < CodeMirror.modeInfo.length; i++) { var info = CodeMirror.modeInfo[i]; if (info.name.toLowerCase() == name) return info; if (info.alias) for (var j = 0; j < info.alias.length; j++) if (info.alias[j].toLowerCase() == name) return info; } }; });