From 7cd3ffbcbd3645ace41f97cd6db2aaa07061aad7 Mon Sep 17 00:00:00 2001 From: ilvalle Date: Tue, 25 Mar 2014 12:56:39 +0100 Subject: [PATCH] codemirror 4.0.3 (new upstream major release) --- .../codemirror/addon/comment/comment.js | 28 +- .../static/codemirror/addon/dialog/dialog.js | 12 +- .../codemirror/addon/display/fullscreen.js | 11 +- .../codemirror/addon/display/placeholder.js | 11 +- .../codemirror/addon/edit/closebrackets.js | 113 +- .../static/codemirror/addon/edit/closetag.js | 95 +- .../codemirror/addon/edit/continuelist.js | 42 +- .../codemirror/addon/edit/matchbrackets.js | 113 +- .../codemirror/addon/edit/trailingspace.js | 37 +- .../codemirror/addon/fold/brace-fold.js | 15 +- .../codemirror/addon/fold/comment-fold.js | 12 + .../static/codemirror/addon/fold/foldcode.js | 37 +- .../codemirror/addon/fold/foldgutter.js | 11 +- .../codemirror/addon/fold/indent-fold.js | 13 +- .../static/codemirror/addon/fold/xml-fold.js | 13 +- .../static/codemirror/addon/hint/html-hint.js | 14 +- .../codemirror/addon/hint/javascript-hint.js | 13 +- .../static/codemirror/addon/hint/pig-hint.js | 121 - .../codemirror/addon/hint/python-hint.js | 14 +- .../static/codemirror/addon/hint/show-hint.js | 56 +- .../static/codemirror/addon/hint/xml-hint.js | 12 +- .../addon/lint/coffeescript-lint.js | 13 +- .../codemirror/addon/lint/javascript-lint.js | 12 +- .../static/codemirror/addon/lint/json-lint.js | 13 +- .../static/codemirror/addon/lint/lint.js | 20 +- .../addon/merge/dep/diff_match_patch.js | 2 +- .../static/codemirror/addon/merge/merge.js | 30 +- .../static/codemirror/addon/mode/loadmode.js | 11 +- .../static/codemirror/addon/mode/multiplex.js | 12 + .../static/codemirror/addon/mode/overlay.js | 15 +- .../codemirror/addon/runmode/colorize.js | 14 +- .../addon/runmode/runmode-standalone.js | 2 - .../codemirror/addon/runmode/runmode.js | 12 + .../codemirror/addon/runmode/runmode.node.js | 8 +- .../addon/search/match-highlighter.js | 15 +- .../static/codemirror/addon/search/search.js | 33 +- .../codemirror/addon/search/searchcursor.js | 23 +- .../codemirror/addon/selection/active-line.js | 55 +- .../addon/selection/mark-selection.js | 25 +- .../static/codemirror/bin/source-highlight | 18 +- .../admin/static/codemirror/keymap/emacs.js | 19 +- .../admin/static/codemirror/keymap/vim.js | 888 ++- .../static/codemirror/lib/codemirror.css | 41 +- .../admin/static/codemirror/lib/codemirror.js | 4912 +++++++++++------ .../admin/static/codemirror/mode/css/css.js | 44 +- .../mode/htmlembedded/htmlembedded.js | 12 + .../codemirror/mode/htmlmixed/htmlmixed.js | 17 +- .../codemirror/mode/javascript/javascript.js | 34 +- .../admin/static/codemirror/mode/meta.js | 18 +- .../static/codemirror/mode/python/python.js | 34 +- .../admin/static/codemirror/mode/xml/xml.js | 33 +- .../static/codemirror/theme/mdn-like.css | 44 + .../static/codemirror/theme/solarized.css | 1 + 53 files changed, 4662 insertions(+), 2561 deletions(-) delete mode 100644 applications/admin/static/codemirror/addon/hint/pig-hint.js create mode 100644 applications/admin/static/codemirror/theme/mdn-like.css diff --git a/applications/admin/static/codemirror/addon/comment/comment.js b/applications/admin/static/codemirror/addon/comment/comment.js index 5975b0bf..1eb9a05c 100644 --- a/applications/admin/static/codemirror/addon/comment/comment.js +++ b/applications/admin/static/codemirror/addon/comment/comment.js @@ -1,4 +1,11 @@ -(function() { +(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 noOptions = {}; @@ -11,8 +18,21 @@ } CodeMirror.commands.toggleComment = function(cm) { - var from = cm.getCursor("start"), to = cm.getCursor("end"); - cm.uncomment(from, to) || cm.lineComment(from, to); + var minLine = Infinity, ranges = cm.listSelections(), mode = null; + for (var i = ranges.length - 1; i >= 0; i--) { + var from = ranges[i].from(), to = ranges[i].to(); + if (from.line >= minLine) continue; + if (to.line >= minLine) to = Pos(minLine, 0); + minLine = from.line; + if (mode == null) { + if (cm.uncomment(from, to)) mode = "un"; + else { cm.lineComment(from, to); mode = "line"; } + } else if (mode == "un") { + cm.uncomment(from, to); + } else { + cm.lineComment(from, to); + } + } }; CodeMirror.defineExtension("lineComment", function(from, to, options) { @@ -146,4 +166,4 @@ }); return true; }); -})(); +}); diff --git a/applications/admin/static/codemirror/addon/dialog/dialog.js b/applications/admin/static/codemirror/addon/dialog/dialog.js index 41d7bf86..586b7370 100644 --- a/applications/admin/static/codemirror/addon/dialog/dialog.js +++ b/applications/admin/static/codemirror/addon/dialog/dialog.js @@ -1,6 +1,13 @@ // Open simple dialogs on top of an editor. Relies on dialog.css. -(function() { +(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) { function dialogDiv(cm, template, bottom) { var wrap = cm.getWrapperElement(); var dialog; @@ -39,6 +46,7 @@ CodeMirror.on(inp, "keydown", function(e) { if (options && options.onKeyDown && options.onKeyDown(e, inp.value, close)) { return; } if (e.keyCode == 13 || e.keyCode == 27) { + inp.blur(); CodeMirror.e_stop(e); close(); me.focus(); @@ -119,4 +127,4 @@ if (duration) doneTimer = setTimeout(close, options.duration); }); -})(); +}); diff --git a/applications/admin/static/codemirror/addon/display/fullscreen.js b/applications/admin/static/codemirror/addon/display/fullscreen.js index a442f6a4..e39c6e16 100644 --- a/applications/admin/static/codemirror/addon/display/fullscreen.js +++ b/applications/admin/static/codemirror/addon/display/fullscreen.js @@ -1,4 +1,11 @@ -(function() { +(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) { @@ -28,4 +35,4 @@ window.scrollTo(info.scrollLeft, info.scrollTop); cm.refresh(); } -})(); +}); diff --git a/applications/admin/static/codemirror/addon/display/placeholder.js b/applications/admin/static/codemirror/addon/display/placeholder.js index 748afe72..0fdc9b0d 100644 --- a/applications/admin/static/codemirror/addon/display/placeholder.js +++ b/applications/admin/static/codemirror/addon/display/placeholder.js @@ -1,4 +1,11 @@ -(function() { +(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.defineOption("placeholder", "", function(cm, val, old) { var prev = old && old != CodeMirror.Init; if (val && !prev) { @@ -45,4 +52,4 @@ function isEmpty(cm) { return (cm.lineCount() === 1) && (cm.getLine(0) === ""); } -})(); +}); diff --git a/applications/admin/static/codemirror/addon/edit/closebrackets.js b/applications/admin/static/codemirror/addon/edit/closebrackets.js index 0575222b..f48ad881 100644 --- a/applications/admin/static/codemirror/addon/edit/closebrackets.js +++ b/applications/admin/static/codemirror/addon/edit/closebrackets.js @@ -1,4 +1,11 @@ -(function() { +(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 DEFAULT_BRACKETS = "()[]{}''\"\""; var DEFAULT_EXPLODE_ON_ENTER = "[]{}"; var SPACE_CHAR_REGEX = /\s/; @@ -28,57 +35,89 @@ var map = { name : "autoCloseBrackets", Backspace: function(cm) { - if (cm.somethingSelected() || cm.getOption("disableInput")) return CodeMirror.Pass; - var cur = cm.getCursor(), around = charsAround(cm, cur); - if (around && pairs.indexOf(around) % 2 == 0) + if (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 || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass; + } + for (var i = ranges.length - 1; i >= 0; i--) { + var cur = ranges[i].head; cm.replaceRange("", CodeMirror.Pos(cur.line, cur.ch - 1), CodeMirror.Pos(cur.line, cur.ch + 1)); - else - return CodeMirror.Pass; + } } }; var closingBrackets = ""; for (var i = 0; i < pairs.length; i += 2) (function(left, right) { if (left != right) closingBrackets += right; - function surround(cm) { - var selection = cm.getSelection(); - cm.replaceSelection(left + selection + right); - } - function maybeOverwrite(cm) { - var cur = cm.getCursor(), ahead = cm.getRange(cur, CodeMirror.Pos(cur.line, cur.ch + 1)); - if (ahead != right || cm.somethingSelected()) return CodeMirror.Pass; - else cm.execCommand("goCharRight"); - } map["'" + left + "'"] = function(cm) { - if (left == "'" && cm.getTokenAt(cm.getCursor()).type == "comment" || - cm.getOption("disableInput")) - return CodeMirror.Pass; - if (cm.somethingSelected()) return surround(cm); - if (left == right && maybeOverwrite(cm) != CodeMirror.Pass) return; - var cur = cm.getCursor(), ahead = CodeMirror.Pos(cur.line, cur.ch + 1); - var line = cm.getLine(cur.line), nextChar = line.charAt(cur.ch), curChar = cur.ch > 0 ? line.charAt(cur.ch - 1) : ""; - if (left == right && CodeMirror.isWordChar(curChar)) - return CodeMirror.Pass; - if (line.length == cur.ch || closingBrackets.indexOf(nextChar) >= 0 || SPACE_CHAR_REGEX.test(nextChar)) - cm.replaceSelection(left + right, {head: ahead, anchor: ahead}); - else - return CodeMirror.Pass; + if (cm.getOption("disableInput")) return CodeMirror.Pass; + var ranges = cm.listSelections(), type, next; + for (var i = 0; i < ranges.length; i++) { + var range = ranges[i], cur = range.head, curType; + if (left == "'" && cm.getTokenTypeAt(cur) == "comment") + return CodeMirror.Pass; + var next = cm.getRange(cur, CodeMirror.Pos(cur.line, cur.ch + 1)); + if (!range.empty()) + curType = "surround"; + else if (left == right && next == right) + curType = "skip"; + else if (left == right && CodeMirror.isWordChar(next)) + return CodeMirror.Pass; + else if (cm.getLine(cur.line).length == cur.ch || closingBrackets.indexOf(next) >= 0 || SPACE_CHAR_REGEX.test(next)) + curType = "both"; + else + return CodeMirror.Pass; + if (!type) type = curType; + else if (type != curType) return CodeMirror.Pass; + } + + if (type == "skip") { + 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"); + } else if (type == "both") { + cm.replaceSelection(left + right, null); + cm.execCommand("goCharLeft"); + } + }; + if (left != right) map["'" + right + "'"] = function(cm) { + var ranges = cm.listSelections(); + for (var i = 0; i < ranges.length; i++) { + var range = ranges[i]; + if (!range.empty() || + cm.getRange(range.head, CodeMirror.Pos(range.head.line, range.head.ch + 1)) != right) + return CodeMirror.Pass; + } + cm.execCommand("goCharRight"); }; - if (left != right) map["'" + right + "'"] = maybeOverwrite; })(pairs.charAt(i), pairs.charAt(i + 1)); return map; } function buildExplodeHandler(pairs) { return function(cm) { - var cur = cm.getCursor(), around = charsAround(cm, cur); - if (!around || pairs.indexOf(around) % 2 != 0 || cm.getOption("disableInput")) - return CodeMirror.Pass; + if (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 || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass; + } cm.operation(function() { - var newPos = CodeMirror.Pos(cur.line + 1, 0); - cm.replaceSelection("\n\n", {anchor: newPos, head: newPos}, "+input"); - cm.indentLine(cur.line + 1, null, true); - cm.indentLine(cur.line + 2, null, true); + cm.replaceSelection("\n\n", 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); + } }); }; } -})(); +}); diff --git a/applications/admin/static/codemirror/addon/edit/closetag.js b/applications/admin/static/codemirror/addon/edit/closetag.js index cad776a7..c7c0701b 100644 --- a/applications/admin/static/codemirror/addon/edit/closetag.js +++ b/applications/admin/static/codemirror/addon/edit/closetag.js @@ -22,7 +22,14 @@ * See demos/closetag.html for a usage example. */ -(function() { +(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"); @@ -41,47 +48,63 @@ "h5", "h6", "head", "html", "iframe", "layer", "legend", "object", "ol", "p", "select", "table", "ul"]; function autoCloseGT(cm) { - var pos = cm.getCursor(), tok = cm.getTokenAt(pos); - var inner = CodeMirror.innerMode(cm.getMode(), tok.state), state = inner.state; - if (inner.mode.name != "xml" || !state.tagName || cm.getOption("disableInput")) return CodeMirror.Pass; + if (cm.getOption("disableInput")) return CodeMirror.Pass; + var ranges = cm.listSelections(), replacements = []; + 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 opt = cm.getOption("autoCloseTags"), html = inner.mode.configuration == "html"; + var dontCloseTags = (typeof opt == "object" && opt.dontCloseTags) || (html && htmlDontClose); + var indentTags = (typeof opt == "object" && opt.indentTags) || (html && htmlIndent); - var opt = cm.getOption("autoCloseTags"), 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 + dontCloseTags && indexOf(dontCloseTags, lowerTagName) > -1 || + CodeMirror.scanForClosingTag && CodeMirror.scanForClosingTag(cm, pos, tagName, + Math.min(cm.lastLine() + 1, pos.line + 50))) + return CodeMirror.Pass; - 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 - dontCloseTags && indexOf(dontCloseTags, lowerTagName) > -1 || - CodeMirror.scanForClosingTag && CodeMirror.scanForClosingTag(cm, pos, tagName, - Math.min(cm.lastLine() + 1, pos.line + 50))) - return CodeMirror.Pass; + var indent = indentTags && indexOf(indentTags, lowerTagName) > -1; + replacements[i] = {indent: indent, + text: ">" + (indent ? "\n\n" : "") + "", + newPos: indent ? CodeMirror.Pos(pos.line + 1, 0) : CodeMirror.Pos(pos.line, pos.ch + 1)}; + } - var doIndent = indentTags && indexOf(indentTags, lowerTagName) > -1; - var curPos = doIndent ? CodeMirror.Pos(pos.line + 1, 0) : CodeMirror.Pos(pos.line, pos.ch + 1); - cm.replaceSelection(">" + (doIndent ? "\n\n" : "") + "", - {head: curPos, anchor: curPos}); - if (doIndent) { - cm.indentLine(pos.line + 1, null, true); - cm.indentLine(pos.line + 2, null); + 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 (info.indent) { + cm.indentLine(info.newPos.line, null, true); + cm.indentLine(info.newPos.line + 1, null, true); + } } } function autoCloseSlash(cm) { - var pos = cm.getCursor(), tok = cm.getTokenAt(pos); - var inner = CodeMirror.innerMode(cm.getMode(), tok.state), state = inner.state; - if (tok.type == "string" || tok.string.charAt(0) != "<" || - tok.start != pos.ch - 1 || inner.mode.name != "xml" || - cm.getOption("disableInput")) - return CodeMirror.Pass; - - var tagName = state.context && state.context.tagName; - if (tagName) cm.replaceSelection("/" + tagName + ">", "end"); + if (cm.getOption("disableInput")) return CodeMirror.Pass; + var ranges = cm.listSelections(), replacements = []; + 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 (tok.type == "string" || tok.string.charAt(0) != "<" || + tok.start != pos.ch - 1 || inner.mode.name != "xml" || + !state.context || !state.context.tagName) + return CodeMirror.Pass; + replacements[i] = "/" + state.context.tagName + ">"; + } + cm.replaceSelections(replacements); } function indexOf(collection, elt) { @@ -90,4 +113,4 @@ if (collection[i] == elt) return i; return -1; } -})(); +}); diff --git a/applications/admin/static/codemirror/addon/edit/continuelist.js b/applications/admin/static/codemirror/addon/edit/continuelist.js index 190b41dc..2946aa6a 100644 --- a/applications/admin/static/codemirror/addon/edit/continuelist.js +++ b/applications/admin/static/codemirror/addon/edit/continuelist.js @@ -1,27 +1,35 @@ -(function() { - 'use strict'; +(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 listRE = /^(\s*)([*+-]|(\d+)\.)(\s*)/, - unorderedBullets = '*+-'; + unorderedBullets = "*+-"; CodeMirror.commands.newlineAndIndentContinueMarkdownList = function(cm) { if (cm.getOption("disableInput")) return CodeMirror.Pass; + var ranges = cm.listSelections(), replacements = []; + for (var i = 0; i < ranges.length; i++) { + var pos = ranges[i].head, match; + var inList = cm.getStateAfter(pos.line).list !== false; - var pos = cm.getCursor(), - inList = cm.getStateAfter(pos.line).list !== false, - match; + if (!ranges[i].empty() || !inList || !(match = cm.getLine(pos.line).match(listRE))) { + cm.execCommand("newlineAndIndent"); + return; + } + var indent = match[1], after = match[4]; + var bullet = unorderedBullets.indexOf(match[2]) >= 0 + ? match[2] + : (parseInt(match[3], 10) + 1) + "."; - if (!inList || !(match = cm.getLine(pos.line).match(listRE))) { - cm.execCommand('newlineAndIndent'); - return; + replacements[i] = "\n" + indent + bullet + after; } - var indent = match[1], after = match[4]; - var bullet = unorderedBullets.indexOf(match[2]) >= 0 - ? match[2] - : (parseInt(match[3], 10) + 1) + '.'; - - cm.replaceSelection('\n' + indent + bullet + after, 'end'); + cm.replaceSelections(replacements); }; - -}()); +}); diff --git a/applications/admin/static/codemirror/addon/edit/matchbrackets.js b/applications/admin/static/codemirror/addon/edit/matchbrackets.js index 465b6cca..576ec143 100644 --- a/applications/admin/static/codemirror/addon/edit/matchbrackets.js +++ b/applications/admin/static/codemirror/addon/edit/matchbrackets.js @@ -1,73 +1,91 @@ -(function() { +(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, strict) { - var state = cm.state.matchBrackets; - var maxScanLen = (state && state.maxScanLineLength) || 10000; - var maxScanLines = (state && state.maxScanLines) || 100; - var cur = where || cm.getCursor(), line = cm.getLineHandle(cur.line), pos = cur.ch - 1; + function findMatchingBracket(cm, where, strict, config) { + var line = cm.getLineHandle(where.line), pos = where.ch - 1; var match = (pos >= 0 && matching[line.text.charAt(pos)]) || matching[line.text.charAt(++pos)]; if (!match) return null; - var forward = match.charAt(1) == ">", d = forward ? 1 : -1; - if (strict && forward != (pos == cur.ch)) return null; - var style = cm.getTokenTypeAt(Pos(cur.line, pos + 1)); + var dir = match.charAt(1) == ">" ? 1 : -1; + if (strict && (dir > 0) != (pos == where.ch)) return null; + var style = cm.getTokenTypeAt(Pos(where.line, pos + 1)); - var stack = [line.text.charAt(pos)], re = /[(){}[\]]/; - function scan(line, lineNo, start) { - if (!line.text) return; - var pos = forward ? 0 : line.text.length - 1, end = forward ? line.text.length : -1; - if (line.text.length > maxScanLen) return null; - if (start != null) pos = start + d; - for (; pos != end; pos += d) { - var ch = line.text.charAt(pos); - if (re.test(ch) && cm.getTokenTypeAt(Pos(lineNo, pos + 1)) == style) { + var found = scanForBracket(cm, Pos(where.line, pos + (dir > 0 ? 1 : 0)), dir, style || null, config); + return {from: Pos(where.line, pos), to: found && found.pos, + match: found && found.ch == match.charAt(0), forward: dir > 0}; + } + + function scanForBracket(cm, where, dir, style, config) { + var maxScanLen = (config && config.maxScanLineLength) || 10000; + var maxScanLines = (config && config.maxScanLines) || 500; + + var stack = [], re = /[(){}[\]]/; + 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) == ">" == forward) stack.push(ch); - else if (stack.pop() != match.charAt(0)) return {pos: pos, match: false}; - else if (!stack.length) return {pos: pos, match: true}; + if ((match.charAt(1) == ">") == (dir > 0)) stack.push(ch); + else if (!stack.length) return {pos: Pos(lineNo, pos), ch: ch}; + else stack.pop(); } } } - for (var i = cur.line, found, e = forward ? Math.min(i + maxScanLines, cm.lineCount()) : Math.max(-1, i - maxScanLines); i != e; i+=d) { - if (i == cur.line) found = scan(line, i, pos); - else found = scan(cm.getLineHandle(i), i); - if (found) break; - } - return {from: Pos(cur.line, pos), to: found && Pos(i, found.pos), - match: found && found.match, forward: forward}; } - function matchBrackets(cm, autoclear) { + 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 found = findMatchingBracket(cm); - if (!found || cm.getLine(found.from.line).length > maxHighlightLen || - found.to && cm.getLine(found.to.line).length > maxHighlightLen) - return; + var marks = [], ranges = cm.listSelections(); + for (var i = 0; i < ranges.length; i++) { + var match = ranges[i].empty() && findMatchingBracket(cm, ranges[i].head, false, config); + if (match && cm.getLine(match.from.line).length <= maxHighlightLen && + match.to && cm.getLine(match.to.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) + marks.push(cm.markText(match.to, Pos(match.to.line, match.to.ch + 1), {className: style})); + } + } - var style = found.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket"; - var one = cm.markText(found.from, Pos(found.from.line, found.from.ch + 1), {className: style}); - var two = found.to && cm.markText(found.to, Pos(found.to.line, found.to.ch + 1), {className: style}); - // Kludge to work around the IE bug from issue #1193, where text - // input stops going to the textarea whenever this fires. - if (ie_lt8 && cm.state.focused) cm.display.input.focus(); - var clear = function() { - cm.operation(function() { one.clear(); two && two.clear(); }); - }; - if (autoclear) setTimeout(clear, 800); - else return clear; + 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.display.input.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; + } } var currentlyHighlighted = null; function doMatchBrackets(cm) { cm.operation(function() { if (currentlyHighlighted) {currentlyHighlighted(); currentlyHighlighted = null;} - if (!cm.somethingSelected()) currentlyHighlighted = matchBrackets(cm, false); + currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets); }); } @@ -84,4 +102,7 @@ CodeMirror.defineExtension("findMatchingBracket", function(pos, strict){ return findMatchingBracket(this, pos, strict); }); -})(); + CodeMirror.defineExtension("scanForBracket", function(pos, dir, style){ + return scanForBracket(this, pos, dir, style); + }); +}); diff --git a/applications/admin/static/codemirror/addon/edit/trailingspace.js b/applications/admin/static/codemirror/addon/edit/trailingspace.js index f6bb0264..ec07221e 100644 --- a/applications/admin/static/codemirror/addon/edit/trailingspace.js +++ b/applications/admin/static/codemirror/addon/edit/trailingspace.js @@ -1,15 +1,24 @@ -CodeMirror.defineOption("showTrailingSpace", false, function(cm, val, prev) { - if (prev == CodeMirror.Init) prev = false; - if (prev && !val) - cm.removeOverlay("trailingspace"); - else if (!prev && val) - cm.addOverlay({ - token: function(stream) { - for (var l = stream.string.length, i = l; i && /\s/.test(stream.string.charAt(i - 1)); --i) {} - if (i > stream.pos) { stream.pos = i; return null; } - stream.pos = l; - return "trailingspace"; - }, - name: "trailingspace" - }); +(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.defineOption("showTrailingSpace", false, function(cm, val, prev) { + if (prev == CodeMirror.Init) prev = false; + if (prev && !val) + cm.removeOverlay("trailingspace"); + else if (!prev && val) + cm.addOverlay({ + token: function(stream) { + for (var l = stream.string.length, i = l; i && /\s/.test(stream.string.charAt(i - 1)); --i) {} + if (i > stream.pos) { stream.pos = i; return null; } + stream.pos = l; + return "trailingspace"; + }, + name: "trailingspace" + }); + }); }); diff --git a/applications/admin/static/codemirror/addon/fold/brace-fold.js b/applications/admin/static/codemirror/addon/fold/brace-fold.js index 2560b2b9..f0ee6202 100644 --- a/applications/admin/static/codemirror/addon/fold/brace-fold.js +++ b/applications/admin/static/codemirror/addon/fold/brace-fold.js @@ -1,3 +1,13 @@ +(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 startCh, tokenType; @@ -45,7 +55,6 @@ CodeMirror.registerHelper("fold", "brace", function(cm, start) { return {from: CodeMirror.Pos(line, startCh), to: CodeMirror.Pos(end, endCh)}; }); -CodeMirror.braceRangeFinder = CodeMirror.fold.brace; // deprecated CodeMirror.registerHelper("fold", "import", function(cm, start) { function hasImport(line) { @@ -70,7 +79,6 @@ CodeMirror.registerHelper("fold", "import", function(cm, start) { } return {from: cm.clipPos(CodeMirror.Pos(start, has.startCh + 1)), to: end}; }); -CodeMirror.importRangeFinder = CodeMirror.fold["import"]; // deprecated CodeMirror.registerHelper("fold", "include", function(cm, start) { function hasInclude(line) { @@ -90,4 +98,5 @@ CodeMirror.registerHelper("fold", "include", function(cm, start) { return {from: CodeMirror.Pos(start, has + 1), to: cm.clipPos(CodeMirror.Pos(end))}; }); -CodeMirror.includeRangeFinder = CodeMirror.fold.include; // deprecated + +}); diff --git a/applications/admin/static/codemirror/addon/fold/comment-fold.js b/applications/admin/static/codemirror/addon/fold/comment-fold.js index 26e72f09..d72c5479 100644 --- a/applications/admin/static/codemirror/addon/fold/comment-fold.js +++ b/applications/admin/static/codemirror/addon/fold/comment-fold.js @@ -1,3 +1,13 @@ +(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.registerGlobalHelper("fold", "comment", function(mode) { return mode.blockCommentStart && mode.blockCommentEnd; }, function(cm, start) { @@ -40,3 +50,5 @@ CodeMirror.registerGlobalHelper("fold", "comment", function(mode) { return {from: CodeMirror.Pos(line, startCh), to: CodeMirror.Pos(end, endCh)}; }); + +}); diff --git a/applications/admin/static/codemirror/addon/fold/foldcode.js b/applications/admin/static/codemirror/addon/fold/foldcode.js index 5c00d709..d7a0bb5e 100644 --- a/applications/admin/static/codemirror/addon/fold/foldcode.js +++ b/applications/admin/static/codemirror/addon/fold/foldcode.js @@ -1,4 +1,11 @@ -(function() { +(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) { @@ -62,9 +69,33 @@ doFold(this, pos, options, force); }); - CodeMirror.commands.fold = function(cm) { + 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); @@ -83,4 +114,4 @@ if (cur) return cur; } }); -})(); +}); diff --git a/applications/admin/static/codemirror/addon/fold/foldgutter.js b/applications/admin/static/codemirror/addon/fold/foldgutter.js index a4f3bb31..9caba59a 100644 --- a/applications/admin/static/codemirror/addon/fold/foldgutter.js +++ b/applications/admin/static/codemirror/addon/fold/foldgutter.js @@ -1,4 +1,11 @@ -(function() { +(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) { @@ -121,4 +128,4 @@ if (line >= state.from && line < state.to) updateFoldInfo(cm, line, line + 1); } -})(); +}); diff --git a/applications/admin/static/codemirror/addon/fold/indent-fold.js b/applications/admin/static/codemirror/addon/fold/indent-fold.js index 434c2bc5..d0130836 100644 --- a/applications/admin/static/codemirror/addon/fold/indent-fold.js +++ b/applications/admin/static/codemirror/addon/fold/indent-fold.js @@ -1,3 +1,13 @@ +(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", "indent", function(cm, start) { var tabSize = cm.getOption("tabSize"), firstLine = cm.getLine(start.line); if (!/\S/.test(firstLine)) return; @@ -27,4 +37,5 @@ CodeMirror.registerHelper("fold", "indent", function(cm, start) { to: CodeMirror.Pos(lastLineInFold, cm.getLine(lastLineInFold).length) }; }); -CodeMirror.indentRangeFinder = CodeMirror.fold.indent; // deprecated + +}); diff --git a/applications/admin/static/codemirror/addon/fold/xml-fold.js b/applications/admin/static/codemirror/addon/fold/xml-fold.js index db5aed70..d554e2fc 100644 --- a/applications/admin/static/codemirror/addon/fold/xml-fold.js +++ b/applications/admin/static/codemirror/addon/fold/xml-fold.js @@ -1,4 +1,11 @@ -(function() { +(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; @@ -136,8 +143,6 @@ } } }); - CodeMirror.tagRangeFinder = CodeMirror.fold.xml; // deprecated - 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; @@ -170,4 +175,4 @@ var iter = new Iter(cm, pos.line, pos.ch, end ? {from: 0, to: end} : null); return !!findMatchingClose(iter, name); }; -})(); +}); diff --git a/applications/admin/static/codemirror/addon/hint/html-hint.js b/applications/admin/static/codemirror/addon/hint/html-hint.js index cf256851..cbe7c61a 100755 --- a/applications/admin/static/codemirror/addon/hint/html-hint.js +++ b/applications/admin/static/codemirror/addon/hint/html-hint.js @@ -1,4 +1,13 @@ -(function () { +(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 langs = "ab aa af ak sq am ar an hy as av ae ay az bm ba eu be bn bh bi bs br bg my ca ch ce ny zh cv kw co cr hr cs da dv nl dz en eo et ee fo fj fi fr ff gl ka de el gn gu ht ha he hz hi ho hu ia id ie ga ig ik io is it iu ja jv kl kn kr ks kk km ki rw ky kv kg ko ku kj la lb lg li ln lo lt lu lv gv mk mg ms ml mt mi mr mh mn na nv nb nd ne ng nn no ii nr oc oj cu om or os pa pi fa pl ps pt qu rm rn ro ru sa sc sd se sm sg sr gd sn si sk sl so st es su sw ss sv ta te tg th ti bo tk tl tn to tr ts tt tw ty ug uk ur uz ve vi vo wa cy wo fy xh yi yo za zu".split(" "); var targets = ["_blank", "_self", "_top", "_parent"]; var charsets = ["ascii", "utf-8", "utf-16", "latin1", "latin1"]; @@ -332,6 +341,5 @@ if (options) for (var opt in options) local[opt] = options[opt]; return CodeMirror.hint.xml(cm, local); } - CodeMirror.htmlHint = htmlHint; // deprecated CodeMirror.registerHelper("hint", "html", htmlHint); -})(); +}); diff --git a/applications/admin/static/codemirror/addon/hint/javascript-hint.js b/applications/admin/static/codemirror/addon/hint/javascript-hint.js index c347c206..305bb85a 100644 --- a/applications/admin/static/codemirror/addon/hint/javascript-hint.js +++ b/applications/admin/static/codemirror/addon/hint/javascript-hint.js @@ -1,4 +1,11 @@ -(function () { +(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 Pos = CodeMirror.Pos; function forEach(arr, f) { @@ -47,7 +54,6 @@ function (e, cur) {return e.getTokenAt(cur);}, options); }; - CodeMirror.javascriptHint = javascriptHint; // deprecated CodeMirror.registerHelper("hint", "javascript", javascriptHint); function getCoffeeScriptToken(editor, cur) { @@ -71,7 +77,6 @@ function coffeescriptHint(editor, options) { return scriptHint(editor, coffeescriptKeywords, getCoffeeScriptToken, options); } - CodeMirror.coffeescriptHint = coffeescriptHint; // deprecated CodeMirror.registerHelper("hint", "coffeescript", coffeescriptHint); var stringProps = ("charAt charCodeAt indexOf lastIndexOf substring substr slice trim trimLeft trimRight " + @@ -128,4 +133,4 @@ } return found; } -})(); +}); diff --git a/applications/admin/static/codemirror/addon/hint/pig-hint.js b/applications/admin/static/codemirror/addon/hint/pig-hint.js deleted file mode 100644 index 6c0c5237..00000000 --- a/applications/admin/static/codemirror/addon/hint/pig-hint.js +++ /dev/null @@ -1,121 +0,0 @@ -(function () { - "use strict"; - - function forEach(arr, f) { - for (var i = 0, e = arr.length; i < e; ++i) f(arr[i]); - } - - function arrayContains(arr, item) { - if (!Array.prototype.indexOf) { - var i = arr.length; - while (i--) { - if (arr[i] === item) { - return true; - } - } - return false; - } - return arr.indexOf(item) != -1; - } - - function scriptHint(editor, _keywords, getToken) { - // Find the token at the cursor - var cur = editor.getCursor(), token = getToken(editor, cur), tprop = token; - // If it's not a 'word-style' token, ignore the token. - - if (!/^[\w$_]*$/.test(token.string)) { - token = tprop = {start: cur.ch, end: cur.ch, string: "", state: token.state, - className: token.string == ":" ? "pig-type" : null}; - } - - if (!context) var context = []; - context.push(tprop); - - var completionList = getCompletions(token, context); - completionList = completionList.sort(); - //prevent autocomplete for last word, instead show dropdown with one word - if(completionList.length == 1) { - completionList.push(" "); - } - - return {list: completionList, - from: CodeMirror.Pos(cur.line, token.start), - to: CodeMirror.Pos(cur.line, token.end)}; - } - - function pigHint(editor) { - return scriptHint(editor, pigKeywordsU, function (e, cur) {return e.getTokenAt(cur);}); - } - CodeMirror.pigHint = pigHint; // deprecated - CodeMirror.registerHelper("hint", "pig", pigHint); - - var pigKeywords = "VOID IMPORT RETURNS DEFINE LOAD FILTER FOREACH ORDER CUBE DISTINCT COGROUP " - + "JOIN CROSS UNION SPLIT INTO IF OTHERWISE ALL AS BY USING INNER OUTER ONSCHEMA PARALLEL " - + "PARTITION GROUP AND OR NOT GENERATE FLATTEN ASC DESC IS STREAM THROUGH STORE MAPREDUCE " - + "SHIP CACHE INPUT OUTPUT STDERROR STDIN STDOUT LIMIT SAMPLE LEFT RIGHT FULL EQ GT LT GTE LTE " - + "NEQ MATCHES TRUE FALSE"; - var pigKeywordsU = pigKeywords.split(" "); - var pigKeywordsL = pigKeywords.toLowerCase().split(" "); - - var pigTypes = "BOOLEAN INT LONG FLOAT DOUBLE CHARARRAY BYTEARRAY BAG TUPLE MAP"; - var pigTypesU = pigTypes.split(" "); - var pigTypesL = pigTypes.toLowerCase().split(" "); - - var pigBuiltins = "ABS ACOS ARITY ASIN ATAN AVG BAGSIZE BINSTORAGE BLOOM BUILDBLOOM CBRT CEIL " - + "CONCAT COR COS COSH COUNT COUNT_STAR COV CONSTANTSIZE CUBEDIMENSIONS DIFF DISTINCT DOUBLEABS " - + "DOUBLEAVG DOUBLEBASE DOUBLEMAX DOUBLEMIN DOUBLEROUND DOUBLESUM EXP FLOOR FLOATABS FLOATAVG " - + "FLOATMAX FLOATMIN FLOATROUND FLOATSUM GENERICINVOKER INDEXOF INTABS INTAVG INTMAX INTMIN " - + "INTSUM INVOKEFORDOUBLE INVOKEFORFLOAT INVOKEFORINT INVOKEFORLONG INVOKEFORSTRING INVOKER " - + "ISEMPTY JSONLOADER JSONMETADATA JSONSTORAGE LAST_INDEX_OF LCFIRST LOG LOG10 LOWER LONGABS " - + "LONGAVG LONGMAX LONGMIN LONGSUM MAX MIN MAPSIZE MONITOREDUDF NONDETERMINISTIC OUTPUTSCHEMA " - + "PIGSTORAGE PIGSTREAMING RANDOM REGEX_EXTRACT REGEX_EXTRACT_ALL REPLACE ROUND SIN SINH SIZE " - + "SQRT STRSPLIT SUBSTRING SUM STRINGCONCAT STRINGMAX STRINGMIN STRINGSIZE TAN TANH TOBAG " - + "TOKENIZE TOMAP TOP TOTUPLE TRIM TEXTLOADER TUPLESIZE UCFIRST UPPER UTF8STORAGECONVERTER"; - var pigBuiltinsU = pigBuiltins.split(" ").join("() ").split(" "); - var pigBuiltinsL = pigBuiltins.toLowerCase().split(" ").join("() ").split(" "); - var pigBuiltinsC = ("BagSize BinStorage Bloom BuildBloom ConstantSize CubeDimensions DoubleAbs " - + "DoubleAvg DoubleBase DoubleMax DoubleMin DoubleRound DoubleSum FloatAbs FloatAvg FloatMax " - + "FloatMin FloatRound FloatSum GenericInvoker IntAbs IntAvg IntMax IntMin IntSum " - + "InvokeForDouble InvokeForFloat InvokeForInt InvokeForLong InvokeForString Invoker " - + "IsEmpty JsonLoader JsonMetadata JsonStorage LongAbs LongAvg LongMax LongMin LongSum MapSize " - + "MonitoredUDF Nondeterministic OutputSchema PigStorage PigStreaming StringConcat StringMax " - + "StringMin StringSize TextLoader TupleSize Utf8StorageConverter").split(" ").join("() ").split(" "); - - function getCompletions(token, context) { - var found = [], start = token.string; - function maybeAdd(str) { - if (str.lastIndexOf(start, 0) == 0 && !arrayContains(found, str)) found.push(str); - } - - function gatherCompletions(obj) { - if(obj == ":") { - forEach(pigTypesL, maybeAdd); - } - else { - forEach(pigBuiltinsU, maybeAdd); - forEach(pigBuiltinsL, maybeAdd); - forEach(pigBuiltinsC, maybeAdd); - forEach(pigTypesU, maybeAdd); - forEach(pigTypesL, maybeAdd); - forEach(pigKeywordsU, maybeAdd); - forEach(pigKeywordsL, maybeAdd); - } - } - - if (context) { - // If this is a property, see if it belongs to some object we can - // find in the current environment. - var obj = context.pop(), base; - - if (obj.type == "variable") - base = obj.string; - else if(obj.type == "variable-3") - base = ":" + obj.string; - - while (base != null && context.length) - base = base[context.pop().string]; - if (base != null) gatherCompletions(base); - } - return found; - } -})(); diff --git a/applications/admin/static/codemirror/addon/hint/python-hint.js b/applications/admin/static/codemirror/addon/hint/python-hint.js index 248284e8..eebfcc76 100644 --- a/applications/admin/static/codemirror/addon/hint/python-hint.js +++ b/applications/admin/static/codemirror/addon/hint/python-hint.js @@ -1,4 +1,13 @@ -(function () { +(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 forEach(arr, f) { for (var i = 0, e = arr.length; i < e; ++i) f(arr[i]); } @@ -40,7 +49,6 @@ function pythonHint(editor) { return scriptHint(editor, pythonKeywordsU, function (e, cur) {return e.getTokenAt(cur);}); } - CodeMirror.pythonHint = pythonHint; // deprecated CodeMirror.registerHelper("hint", "python", pythonHint); var pythonKeywords = "and del from not while as elif global or with assert else if pass yield" @@ -88,4 +96,4 @@ } return found; } -})(); +}); diff --git a/applications/admin/static/codemirror/addon/hint/show-hint.js b/applications/admin/static/codemirror/addon/hint/show-hint.js index 7e03d113..6f04c565 100644 --- a/applications/admin/static/codemirror/addon/hint/show-hint.js +++ b/applications/admin/static/codemirror/addon/hint/show-hint.js @@ -1,4 +1,11 @@ -(function() { +(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 HINT_ELEMENT_CLASS = "CodeMirror-hint"; @@ -6,7 +13,7 @@ CodeMirror.showHint = function(cm, getHints, options) { // We want a single cursor position. - if (cm.somethingSelected()) return; + if (cm.listSelections().length > 1 || cm.somethingSelected()) return; if (getHints == null) { if (options && options.async) return; else getHints = CodeMirror.hint.auto; @@ -46,7 +53,7 @@ pick: function(data, i) { var completion = data.list[i]; if (completion.hint) completion.hint(this.cm, data, completion); - else this.cm.replaceRange(getText(completion), data.from, data.to); + else this.cm.replaceRange(getText(completion), completion.from||data.from, completion.to||data.to); CodeMirror.signal(data, "pick", completion); this.close(); }, @@ -193,8 +200,24 @@ var winW = window.innerWidth || Math.max(document.body.offsetWidth, document.documentElement.offsetWidth); var winH = window.innerHeight || Math.max(document.body.offsetHeight, document.documentElement.offsetHeight); (options.container || document.body).appendChild(hints); - var box = hints.getBoundingClientRect(); - var overlapX = box.right - winW, overlapY = box.bottom - winH; + var box = hints.getBoundingClientRect(), overlapY = box.bottom - winH; + if (overlapY > 0) { + var height = box.bottom - box.top, curTop = box.top - (pos.bottom - pos.top); + if (curTop - height > 0) { // Fits above cursor + hints.style.top = (top = curTop - height) + "px"; + below = false; + } else if (height > winH) { + hints.style.height = (winH - 5) + "px"; + hints.style.top = (top = pos.bottom - box.top) + "px"; + var cursor = cm.getCursor(); + if (data.from.ch != cursor.ch) { + pos = cm.cursorCoords(cursor); + hints.style.left = (left = pos.left) + "px"; + box = hints.getBoundingClientRect(); + } + } + } + var overlapX = box.left - winW; if (overlapX > 0) { if (box.right - box.left > winW) { hints.style.width = (winW - 5) + "px"; @@ -202,17 +225,6 @@ } hints.style.left = (left = pos.left - overlapX) + "px"; } - if (overlapY > 0) { - var height = box.bottom - box.top; - if (box.top - (pos.bottom - pos.top) - height > 0) { - overlapY = height + (pos.bottom - pos.top); - below = false; - } else if (height > winH) { - hints.style.height = (winH - 5) + "px"; - overlapY -= height - winH; - } - hints.style.top = (top = pos.bottom - overlapY) + "px"; - } cm.addKeyMap(this.keyMap = buildKeyMap(options, { moveFocus: function(n, avoidWrap) { widget.changeActive(widget.selectedHint + n, avoidWrap); }, @@ -220,7 +232,8 @@ menuSize: function() { return widget.screenAmount(); }, length: completions.length, close: function() { completion.close(); }, - pick: function() { widget.pick(); } + pick: function() { widget.pick(); }, + data: data })); if (options.closeOnUnfocus !== false) { @@ -303,15 +316,16 @@ }; CodeMirror.registerHelper("hint", "auto", function(cm, options) { - var helpers = cm.getHelpers(cm.getCursor(), "hint"); + var helpers = cm.getHelpers(cm.getCursor(), "hint"), words; if (helpers.length) { for (var i = 0; i < helpers.length; i++) { var cur = helpers[i](cm, options); if (cur && cur.list.length) return cur; } - } else { - var words = cm.getHelper(cm.getCursor(), "hintWords"); + } else if (words = cm.getHelper(cm.getCursor(), "hintWords")) { if (words) return CodeMirror.hint.fromList(cm, {words: words}); + } else if (CodeMirror.hint.anyword) { + return CodeMirror.hint.anyword(cm, options); } }); @@ -332,4 +346,4 @@ }); CodeMirror.commands.autocomplete = CodeMirror.showHint; -})(); +}); diff --git a/applications/admin/static/codemirror/addon/hint/xml-hint.js b/applications/admin/static/codemirror/addon/hint/xml-hint.js index 69f2b771..85756490 100644 --- a/applications/admin/static/codemirror/addon/hint/xml-hint.js +++ b/applications/admin/static/codemirror/addon/hint/xml-hint.js @@ -1,4 +1,11 @@ -(function() { +(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; @@ -64,6 +71,5 @@ }; } - CodeMirror.xmlHint = getHints; // deprecated CodeMirror.registerHelper("hint", "xml", getHints); -})(); +}); diff --git a/applications/admin/static/codemirror/addon/lint/coffeescript-lint.js b/applications/admin/static/codemirror/addon/lint/coffeescript-lint.js index 7f55a294..6df17f8f 100644 --- a/applications/admin/static/codemirror/addon/lint/coffeescript-lint.js +++ b/applications/admin/static/codemirror/addon/lint/coffeescript-lint.js @@ -2,6 +2,16 @@ // declare global: coffeelint +(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("lint", "coffeescript", function(text) { var found = []; var parseError = function(err) { @@ -24,4 +34,5 @@ CodeMirror.registerHelper("lint", "coffeescript", function(text) { } return found; }); -CodeMirror.coffeeValidator = CodeMirror.lint.coffeescript; // deprecated + +}); diff --git a/applications/admin/static/codemirror/addon/lint/javascript-lint.js b/applications/admin/static/codemirror/addon/lint/javascript-lint.js index 7123ab7e..bbb51083 100644 --- a/applications/admin/static/codemirror/addon/lint/javascript-lint.js +++ b/applications/admin/static/codemirror/addon/lint/javascript-lint.js @@ -1,4 +1,11 @@ -(function() { +(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"; // declare global: JSHINT @@ -19,7 +26,6 @@ } CodeMirror.registerHelper("lint", "javascript", validator); - CodeMirror.javascriptValidator = CodeMirror.lint.javascript; // deprecated function cleanup(error) { // All problems are warnings by default @@ -123,4 +129,4 @@ } } } -})(); +}); diff --git a/applications/admin/static/codemirror/addon/lint/json-lint.js b/applications/admin/static/codemirror/addon/lint/json-lint.js index e10e11bc..1f5f82d0 100644 --- a/applications/admin/static/codemirror/addon/lint/json-lint.js +++ b/applications/admin/static/codemirror/addon/lint/json-lint.js @@ -2,6 +2,16 @@ // declare global: jsonlint +(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("lint", "json", function(text) { var found = []; jsonlint.parseError = function(str, hash) { @@ -14,4 +24,5 @@ CodeMirror.registerHelper("lint", "json", function(text) { catch(e) {} return found; }); -CodeMirror.jsonValidator = CodeMirror.lint.json; // deprecated + +}); diff --git a/applications/admin/static/codemirror/addon/lint/lint.js b/applications/admin/static/codemirror/addon/lint/lint.js index 6121735d..393a6890 100644 --- a/applications/admin/static/codemirror/addon/lint/lint.js +++ b/applications/admin/static/codemirror/addon/lint/lint.js @@ -1,4 +1,11 @@ -(function() { +(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 GUTTER_ID = "CodeMirror-lint-markers"; var SEVERITIES = /^(?:error|warning)$/; @@ -170,7 +177,7 @@ if (!/\bCodeMirror-lint-mark-/.test((e.target || e.srcElement).className)) return; for (var i = 0; i < nearby.length; i += 2) { var spans = cm.findMarksAt(cm.coordsChar({left: e.clientX + nearby[i], - top: e.clientY + nearby[i + 1]})); + top: e.clientY + nearby[i + 1]}, "client")); for (var j = 0; j < spans.length; ++j) { var span = spans[j], ann = span.__annotation; if (ann) return popupSpanTooltip(ann, e); @@ -178,7 +185,7 @@ } } - function optionHandler(cm, val, old) { + CodeMirror.defineOption("lint", false, function(cm, val, old) { if (old && old != CodeMirror.Init) { clearMarks(cm); cm.off("change", onChange); @@ -196,8 +203,5 @@ startLinting(cm); } - } - - CodeMirror.defineOption("lintWith", false, optionHandler); // deprecated - CodeMirror.defineOption("lint", false, optionHandler); // deprecated -})(); + }); +}); diff --git a/applications/admin/static/codemirror/addon/merge/dep/diff_match_patch.js b/applications/admin/static/codemirror/addon/merge/dep/diff_match_patch.js index ac34105f..9d615dc9 100644 --- a/applications/admin/static/codemirror/addon/merge/dep/diff_match_patch.js +++ b/applications/admin/static/codemirror/addon/merge/dep/diff_match_patch.js @@ -47,4 +47,4 @@ j=!1):-1===g&&1==h.diffs.length&&0==h.diffs[0][0]&&i.length>2*b?(h.length1+=i.le diff_match_patch.prototype.patch_fromText=function(a){var b=[];if(!a)return b;a=a.split("\n");for(var c=0,d=/^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@$/;c 0 ? a : b; } function posEq(a, b) { return a.line == b.line && a.ch == b.ch; } -})(); +}); diff --git a/applications/admin/static/codemirror/addon/mode/loadmode.js b/applications/admin/static/codemirror/addon/mode/loadmode.js index 60fafbb1..e08c2813 100644 --- a/applications/admin/static/codemirror/addon/mode/loadmode.js +++ b/applications/admin/static/codemirror/addon/mode/loadmode.js @@ -1,4 +1,11 @@ -(function() { +(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) { if (!CodeMirror.modeURL) CodeMirror.modeURL = "../mode/%N/%N.js"; var loading = {}; @@ -48,4 +55,4 @@ instance.setOption("mode", instance.getOption("mode")); }); }; -}()); +}); diff --git a/applications/admin/static/codemirror/addon/mode/multiplex.js b/applications/admin/static/codemirror/addon/mode/multiplex.js index 614ab1ad..07385c35 100644 --- a/applications/admin/static/codemirror/addon/mode/multiplex.js +++ b/applications/admin/static/codemirror/addon/mode/multiplex.js @@ -1,3 +1,13 @@ +(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); @@ -101,3 +111,5 @@ CodeMirror.multiplexingMode = function(outer /*, others */) { } }; }; + +}); diff --git a/applications/admin/static/codemirror/addon/mode/overlay.js b/applications/admin/static/codemirror/addon/mode/overlay.js index b7928a7b..6f556a13 100644 --- a/applications/admin/static/codemirror/addon/mode/overlay.js +++ b/applications/admin/static/codemirror/addon/mode/overlay.js @@ -6,8 +6,17 @@ // overlay wins, unless the combine argument was true, in which case // the styles are combined. -// overlayParser is the old, deprecated name -CodeMirror.overlayMode = CodeMirror.overlayParser = function(base, overlay, combine) { +(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.overlayMode = function(base, overlay, combine) { return { startState: function() { return { @@ -57,3 +66,5 @@ CodeMirror.overlayMode = CodeMirror.overlayParser = function(base, overlay, comb } }; }; + +}); diff --git a/applications/admin/static/codemirror/addon/runmode/colorize.js b/applications/admin/static/codemirror/addon/runmode/colorize.js index 62286d21..0f9530b1 100644 --- a/applications/admin/static/codemirror/addon/runmode/colorize.js +++ b/applications/admin/static/codemirror/addon/runmode/colorize.js @@ -1,4 +1,12 @@ -CodeMirror.colorize = (function() { +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror"), require("./runmode")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror", "./runmode"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; var isBlock = /^(p|li|div|h\\d|pre|blockquote|td)$/; @@ -10,7 +18,7 @@ CodeMirror.colorize = (function() { } } - return function(collection, defaultMode) { + CodeMirror.colorize = function(collection, defaultMode) { if (!collection) collection = document.body.getElementsByTagName("pre"); for (var i = 0; i < collection.length; ++i) { @@ -26,4 +34,4 @@ CodeMirror.colorize = (function() { node.className += " cm-s-default"; } }; -})(); +}); diff --git a/applications/admin/static/codemirror/addon/runmode/runmode-standalone.js b/applications/admin/static/codemirror/addon/runmode/runmode-standalone.js index ec06ba11..eaa2b8f2 100644 --- a/applications/admin/static/codemirror/addon/runmode/runmode-standalone.js +++ b/applications/admin/static/codemirror/addon/runmode/runmode-standalone.js @@ -1,5 +1,3 @@ -/* Just enough of CodeMirror to run runMode under node.js */ - window.CodeMirror = {}; (function() { diff --git a/applications/admin/static/codemirror/addon/runmode/runmode.js b/applications/admin/static/codemirror/addon/runmode/runmode.js index 2cafa811..351840e0 100644 --- a/applications/admin/static/codemirror/addon/runmode/runmode.js +++ b/applications/admin/static/codemirror/addon/runmode/runmode.js @@ -1,3 +1,13 @@ +(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.runMode = function(string, modespec, callback, options) { var mode = CodeMirror.getMode(CodeMirror.defaults, modespec); var ie = /MSIE \d/.test(navigator.userAgent); @@ -54,3 +64,5 @@ CodeMirror.runMode = function(string, modespec, callback, options) { } } }; + +}); diff --git a/applications/admin/static/codemirror/addon/runmode/runmode.node.js b/applications/admin/static/codemirror/addon/runmode/runmode.node.js index e8bccb0c..74c39be7 100644 --- a/applications/admin/static/codemirror/addon/runmode/runmode.node.js +++ b/applications/admin/static/codemirror/addon/runmode/runmode.node.js @@ -1,5 +1,7 @@ /* Just enough of CodeMirror to run runMode under node.js */ +// declare global: StringStream + function splitLines(string){ return string.split(/\r?\n|\r/); }; function StringStream(string) { @@ -92,14 +94,14 @@ exports.resolveMode = function(spec) { else return spec || {name: "null"}; }; exports.getMode = function(options, spec) { - spec = exports.resolveMode(mimeModes[spec]); + spec = exports.resolveMode(spec); var mfactory = modes[spec.name]; if (!mfactory) throw new Error("Unknown mode: " + spec); return mfactory(options, spec); }; exports.registerHelper = exports.registerGlobalHelper = Math.min; -exports.runMode = function(string, modespec, callback) { +exports.runMode = function(string, modespec, callback, options) { var mode = exports.getMode({indentUnit: 2}, modespec); var lines = splitLines(string), state = (options && options.state) || exports.startState(mode); for (var i = 0, e = lines.length; i < e; ++i) { @@ -112,3 +114,5 @@ exports.runMode = function(string, modespec, callback) { } } }; + +require.cache[require.resolve("../../lib/codemirror")] = require.cache[require.resolve("./runmode.node")]; diff --git a/applications/admin/static/codemirror/addon/search/match-highlighter.js b/applications/admin/static/codemirror/addon/search/match-highlighter.js index e5cbeaca..d9c818b8 100644 --- a/applications/admin/static/codemirror/addon/search/match-highlighter.js +++ b/applications/admin/static/codemirror/addon/search/match-highlighter.js @@ -12,7 +12,16 @@ // actual CSS class name. showToken, when enabled, will cause the // current token to be highlighted when nothing is selected. -(function() { +(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 DEFAULT_MIN_CHARS = 2; var DEFAULT_TOKEN_STYLE = "matchhighlight"; var DEFAULT_DELAY = 100; @@ -68,7 +77,7 @@ return; } if (cm.getCursor("head").line != cm.getCursor("anchor").line) return; - var selection = cm.getSelection().replace(/^\s+|\s+$/g, ""); + var selection = cm.getSelections()[0].replace(/^\s+|\s+$/g, ""); if (selection.length >= state.minChars) cm.addOverlay(state.overlay = makeOverlay(selection, false, state.style)); }); @@ -88,4 +97,4 @@ stream.skipTo(query.charAt(0)) || stream.skipToEnd(); }}; } -})(); +}); diff --git a/applications/admin/static/codemirror/addon/search/search.js b/applications/admin/static/codemirror/addon/search/search.js index 049f72f3..19f51f1d 100644 --- a/applications/admin/static/codemirror/addon/search/search.js +++ b/applications/admin/static/codemirror/addon/search/search.js @@ -6,7 +6,15 @@ // replace by making sure the match is no longer selected when hitting // Ctrl-G. -(function() { +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror"), require("./searchcursor"), require("../dialog/dialog")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror", "./searchcursor", "../dialog/dialog"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; function searchOverlay(query, caseInsensitive) { var startChar; if (typeof query == "string") { @@ -16,16 +24,11 @@ } else { query = new RegExp("^(?:" + query.source + ")", query.ignoreCase ? "i" : ""); } - if (typeof query == "string") return {token: function(stream) { - if (stream.match(query)) return "searching"; - stream.next(); - stream.skipTo(query.charAt(0)) || stream.skipToEnd(); - }}; return {token: function(stream) { if (stream.match(query)) return "searching"; while (!stream.eol()) { stream.next(); - if (startChar) + if (startChar && !caseInsensitive) stream.skipTo(startChar) || stream.skipToEnd(); if (stream.match(query, false)) break; } @@ -56,7 +59,13 @@ } function parseQuery(query) { var isRE = query.match(/^\/(.*)\/([a-z]*)$/); - return isRE ? new RegExp(isRE[1], isRE[2].indexOf("i") == -1 ? "" : "i") : query; + if (isRE) { + query = new RegExp(isRE[1], isRE[2].indexOf("i") == -1 ? "" : "i"); + if (query.test("")) query = /x^/; + } else if (query == "") { + query = /x^/; + } + return query; } var queryDialog = 'Search: (Use /re/ syntax for regexp search)'; @@ -68,7 +77,7 @@ if (!query || state.query) return; state.query = parseQuery(query); cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query)); - state.overlay = searchOverlay(state.query); + state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query)); cm.addOverlay(state.overlay); state.posFrom = state.posTo = cm.getCursor(); findNext(cm, rev); @@ -107,7 +116,7 @@ for (var cursor = getSearchCursor(cm, query); cursor.findNext();) { if (typeof query != "string") { var match = cm.getRange(cursor.from(), cursor.to()).match(query); - cursor.replace(text.replace(/\$(\d)/, function(_, i) {return match[i];})); + cursor.replace(text.replace(/\$(\d)/g, function(_, i) {return match[i];})); } else cursor.replace(text); } }); @@ -128,7 +137,7 @@ }; var doReplace = function(match) { cursor.replace(typeof query == "string" ? text : - text.replace(/\$(\d)/, function(_, i) {return match[i];})); + text.replace(/\$(\d)/g, function(_, i) {return match[i];})); advance(); }; advance(); @@ -143,4 +152,4 @@ CodeMirror.commands.clearSearch = clearSearch; CodeMirror.commands.replace = replace; CodeMirror.commands.replaceAll = function(cm) {replace(cm, true);}; -})(); +}); diff --git a/applications/admin/static/codemirror/addon/search/searchcursor.js b/applications/admin/static/codemirror/addon/search/searchcursor.js index 711cf4ce..899f44c4 100644 --- a/applications/admin/static/codemirror/addon/search/searchcursor.js +++ b/applications/admin/static/codemirror/addon/search/searchcursor.js @@ -1,4 +1,12 @@ -(function(){ +(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 SearchCursor(doc, query, pos, caseFold) { @@ -164,4 +172,15 @@ CodeMirror.defineDocExtension("getSearchCursor", function(query, pos, caseFold) { return new SearchCursor(this, query, pos, caseFold); }); -})(); + + CodeMirror.defineExtension("selectMatches", function(query, caseFold) { + var ranges = [], next; + var cur = this.getSearchCursor(query, this.getCursor("from"), caseFold); + while (next = 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); + }); +}); diff --git a/applications/admin/static/codemirror/addon/selection/active-line.js b/applications/admin/static/codemirror/addon/selection/active-line.js index 7cf7793c..a818f109 100644 --- a/applications/admin/static/codemirror/addon/selection/active-line.js +++ b/applications/admin/static/codemirror/addon/selection/active-line.js @@ -4,7 +4,14 @@ // active line's wrapping
the CSS class "CodeMirror-activeline", // and gives its background
the class "CodeMirror-activeline-background". -(function() { +(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"; @@ -12,34 +19,48 @@ CodeMirror.defineOption("styleActiveLine", false, function(cm, val, old) { var prev = old && old != CodeMirror.Init; if (val && !prev) { - updateActiveLine(cm, cm.getCursor().line); + cm.state.activeLines = []; + updateActiveLines(cm, cm.listSelections()); cm.on("beforeSelectionChange", selectionChange); } else if (!val && prev) { cm.off("beforeSelectionChange", selectionChange); - clearActiveLine(cm); - delete cm.state.activeLine; + clearActiveLines(cm); + delete cm.state.activeLines; } }); - function clearActiveLine(cm) { - if ("activeLine" in cm.state) { - cm.removeLineClass(cm.state.activeLine, "wrap", WRAP_CLASS); - cm.removeLineClass(cm.state.activeLine, "background", BACK_CLASS); + 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); } } - function updateActiveLine(cm, selectedLine) { - var line = cm.getLineHandleVisualStart(selectedLine); - if (cm.state.activeLine == line) return; + 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 line = cm.getLineHandleVisualStart(ranges[i].head.line); + if (active[active.length - 1] != line) active.push(line); + } + if (sameArray(cm.state.activeLines, active)) return; cm.operation(function() { - clearActiveLine(cm); - cm.addLineClass(line, "wrap", WRAP_CLASS); - cm.addLineClass(line, "background", BACK_CLASS); - cm.state.activeLine = line; + 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.state.activeLines = active; }); } function selectionChange(cm, sel) { - updateActiveLine(cm, sel.head.line); + updateActiveLines(cm, sel.ranges); } -})(); +}); diff --git a/applications/admin/static/codemirror/addon/selection/mark-selection.js b/applications/admin/static/codemirror/addon/selection/mark-selection.js index c97776e4..ae0d3931 100644 --- a/applications/admin/static/codemirror/addon/selection/mark-selection.js +++ b/applications/admin/static/codemirror/addon/selection/mark-selection.js @@ -4,7 +4,14 @@ // selected text the CSS class given as option value, or // "CodeMirror-selectedtext" when the value is not a string. -(function() { +(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("styleSelectedText", false, function(cm, val, old) { @@ -34,10 +41,7 @@ var CHUNK_SIZE = 8; var Pos = CodeMirror.Pos; - - function cmp(pos1, pos2) { - return pos1.line - pos2.line || pos1.ch - pos2.ch; - } + var cmp = CodeMirror.cmpPos; function coverRange(cm, from, to, addAt) { if (cmp(from, to) == 0) return; @@ -63,13 +67,16 @@ function reset(cm) { clear(cm); - var from = cm.getCursor("start"), to = cm.getCursor("end"); - coverRange(cm, from, to); + var ranges = cm.listSelections(); + for (var i = 0; i < ranges.length; i++) + coverRange(cm, ranges[i].from(), ranges[i].to()); } function update(cm) { + if (!cm.somethingSelected()) return clear(cm); + if (cm.listSelections().length > 1) return reset(cm); + var from = cm.getCursor("start"), to = cm.getCursor("end"); - if (cmp(from, to) == 0) return clear(cm); var array = cm.state.markedSelection; if (!array.length) return coverRange(cm, from, to); @@ -105,4 +112,4 @@ } } } -})(); +}); diff --git a/applications/admin/static/codemirror/bin/source-highlight b/applications/admin/static/codemirror/bin/source-highlight index 7596ed77..6d15f1ae 100755 --- a/applications/admin/static/codemirror/bin/source-highlight +++ b/applications/admin/static/codemirror/bin/source-highlight @@ -8,7 +8,7 @@ var fs = require("fs"); -CodeMirror = require("../addon/runmode/runmode.node.js"); +var CodeMirror = require("../addon/runmode/runmode.node.js"); require("../mode/meta.js"); var sPos = process.argv.indexOf("-s"); @@ -26,21 +26,11 @@ CodeMirror.modeInfo.forEach(function(info) { } }); -function ensureMode(name) { - if (CodeMirror.modes[name] || name == "null") return; - try { - require("../mode/" + name + "/" + name + ".js"); - } catch(e) { - console.error("Could not load mode " + name + "."); - process.exit(1); - } - var obj = CodeMirror.modes[name]; - if (obj.dependencies) obj.dependencies.forEach(ensureMode); -} -ensureMode(modeName); +if (!CodeMirror.modes[modeName]) + require("../mode/" + modeName + "/" + modeName + ".js"); function esc(str) { - return str.replace(/[<&]/, function(ch) { return ch == "&" ? "&" : "<"; }); + return str.replace(/[<&]/g, function(ch) { return ch == "&" ? "&" : "<"; }); } var code = fs.readFileSync("/dev/stdin", "utf8"); diff --git a/applications/admin/static/codemirror/keymap/emacs.js b/applications/admin/static/codemirror/keymap/emacs.js index 592238bc..8d3ab62d 100644 --- a/applications/admin/static/codemirror/keymap/emacs.js +++ b/applications/admin/static/codemirror/keymap/emacs.js @@ -1,4 +1,11 @@ -(function() { +(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; @@ -174,7 +181,7 @@ if (dup > 1 && event.origin == "+input") { var one = event.text.join("\n"), txt = ""; for (var i = 1; i < dup; ++i) txt += one; - cm.replaceSelection(txt, "end", "+input"); + cm.replaceSelection(txt); } } @@ -197,7 +204,7 @@ function setMark(cm) { cm.setCursor(cm.getCursor()); - cm.setExtending(true); + cm.setExtending(!cm.getExtending()); cm.on("change", function() { cm.setExtending(false); }); } @@ -266,7 +273,7 @@ cm.replaceRange(getFromRing(getPrefix(cm)), start, start, "paste"); cm.setSelection(start, cm.getCursor()); }, - "Alt-Y": function(cm) {cm.replaceSelection(popFromRing());}, + "Alt-Y": function(cm) {cm.replaceSelection(popFromRing(), "around", "paste");}, "Ctrl-Space": setMark, "Ctrl-Shift-2": setMark, @@ -323,7 +330,7 @@ var range = cm.getRange(from, pos); if (range.length != 2) return; cm.setSelection(from, pos); - cm.replaceSelection(range.charAt(1) + range.charAt(0), "end"); + cm.replaceSelection(range.charAt(1) + range.charAt(0), null, "+transpose"); }), "Alt-C": repeated(function(cm) { @@ -395,4 +402,4 @@ } for (var i = 0; i < 10; ++i) regPrefix(String(i)); regPrefix("-"); -})(); +}); diff --git a/applications/admin/static/codemirror/keymap/vim.js b/applications/admin/static/codemirror/keymap/vim.js index 75b4e454..f9cdfd54 100644 --- a/applications/admin/static/codemirror/keymap/vim.js +++ b/applications/admin/static/codemirror/keymap/vim.js @@ -32,7 +32,7 @@ * ESC - leave insert mode, visual mode, and clear input state. * Ctrl-[, Ctrl-c - same as ESC. * - * Registers: unamed, -, a-z, A-Z, 0-9 + * 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. @@ -56,7 +56,14 @@ * 8. Set up Vim to work as a keymap for CodeMirror. */ -(function() { +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../lib/codemirror"), require("../addon/search/searchcursor"), require("../addon/dialog/dialog")); + else if (typeof define == "function" && define.amd) // AMD + define(["../lib/codemirror", "../addon/search/searchcursor", "../addon/dialog/dialog"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { 'use strict'; var defaultKeymap = [ @@ -74,7 +81,7 @@ { keys: [''], type: 'keyToKey', toKeys: ['b'] }, { keys: [''], type: 'keyToKey', toKeys: ['j'] }, { keys: [''], type: 'keyToKey', toKeys: ['k'] }, - { keys: ['C-['], type: 'keyToKey', toKeys: [''] }, + { keys: [''], type: 'keyToKey', toKeys: [''] }, { keys: [''], type: 'keyToKey', toKeys: [''] }, { keys: ['s'], type: 'keyToKey', toKeys: ['c', 'l'], context: 'normal' }, { keys: ['s'], type: 'keyToKey', toKeys: ['x', 'i'], context: 'visual'}, @@ -210,6 +217,7 @@ { keys: ['|'], type: 'motion', motion: 'moveToColumn', motionArgs: { }}, + { keys: ['o'], type: 'motion', motion: 'moveToOtherHighlightedEnd', motionArgs: { },context:'visual'}, // Operators { keys: ['d'], type: 'operator', operator: 'delete' }, { keys: ['y'], type: 'operator', operator: 'yank' }, @@ -271,6 +279,7 @@ { keys: ['v'], type: 'action', action: 'toggleVisualMode' }, { keys: ['V'], type: 'action', action: 'toggleVisualMode', actionArgs: { linewise: true }}, + { keys: ['g', 'v'], 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 }}, @@ -327,6 +336,8 @@ { keys: [':'], type: 'ex' } ]; + var Pos = CodeMirror.Pos; + var Vim = function() { CodeMirror.defineOption('vimMode', false, function(cm, val) { if (val) { @@ -344,13 +355,16 @@ cm.state.vim = null; } }); - function beforeSelectionChange(cm, cur) { + function beforeSelectionChange(cm, obj) { var vim = cm.state.vim; if (vim.insertMode || vim.exMode) return; - var head = cur.head; + var head = obj.ranges[0].head; + var anchor = obj.ranges[0].anchor; if (head.ch && head.ch == cm.doc.getLine(head.line).length) { - head.ch--; + var pos = Pos(head.line, head.ch - 1); + obj.update([{anchor: cursorEqual(head, anchor) ? pos : anchor, + head: pos}]); } } function getOnPasteFn(cm) { @@ -411,6 +425,41 @@ return false; } + var options = {}; + function defineOption(name, defaultValue, type) { + if (defaultValue === undefined) { throw Error('defaultValue is required'); } + if (!type) { type = 'string'; } + options[name] = { + type: type, + defaultValue: defaultValue + }; + setOption(name, defaultValue); + } + + function setOption(name, value) { + var option = options[name]; + if (!option) { + throw Error('Unknown option: ' + name); + } + if (option.type == 'boolean') { + if (value && value !== true) { + throw Error('Invalid argument: ' + name + '=' + value); + } else if (value !== false) { + // Boolean options are set to true if value is not defined. + value = true; + } + } + option.value = option.type == 'boolean' ? !!value : value; + } + + function getOption(name) { + var option = options[name]; + if (!option) { + throw Error('Unknown option: ' + name); + } + return option.value; + } + var createCircularJumpList = function() { var size = 100; var pointer = -1; @@ -477,30 +526,51 @@ }; }; - var createMacroState = function() { + // 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 { - macroKeyBuffer: [], - latestRegister: undefined, - inReplay: false, - lastInsertModeChanges: { - changes: [], // Change list - expectCursorActivityForChange: false // Set to true on change, false on cursorActivity. - }, - enteredMacroMode: undefined, - isMacroPlaying: false, - toggle: function(cm, registerName) { - if (this.enteredMacroMode) { //onExit - this.enteredMacroMode(); // close dialog - this.enteredMacroMode = undefined; - } else { //onEnter - this.latestRegister = registerName; - this.enteredMacroMode = cm.openDialog( - '(recording)['+registerName+']', null, {bottom:true}); - } - } + // 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.onRecordingDone = undefined; + this.lastInsertModeChanges = createInsertModeChanges(); + } + MacroModeState.prototype = { + exitMacroRecordMode: function() { + var macroModeState = vimGlobalState.macroModeState; + 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; + this.onRecordingDone = cm.openDialog( + '(recording)['+registerName+']', null, {bottom:true}); + this.isRecording = true; + } + } + }; function maybeInitVimState(cm) { if (!cm.state.vim) { @@ -531,7 +601,8 @@ insertModeRepeat: undefined, visualMode: false, // If we are in visual line mode. No effect if visualMode is false. - visualLine: false + visualLine: false, + lastSelection: null }; } return cm.state.vim; @@ -544,11 +615,15 @@ // Whether we are searching backwards. searchIsReversed: false, jumpList: createCircularJumpList(), - macroModeState: createMacroState(), + macroModeState: new MacroModeState, // Recording latest f, t, F or T motion command. lastChararacterSearch: {increment:0, forward:true, selectedCharacter:''}, registerController: new RegisterController({}) }; + for (var optionName in options) { + var option = options[optionName]; + option.value = option.defaultValue; + } } var vimApi= { @@ -576,6 +651,9 @@ // Add user defined key bindings. exCommandDispatcher.map(lhs, rhs, ctx); }, + setOption: setOption, + getOption: getOption, + defineOption: defineOption, defineEx: function(name, prefix, func){ if (name.indexOf(prefix) !== 0) { throw new Error('(Vim.defineEx) "'+prefix+'" is not a prefix of "'+name+'", command not registered'); @@ -589,9 +667,9 @@ var command; var vim = maybeInitVimState(cm); var macroModeState = vimGlobalState.macroModeState; - if (macroModeState.enteredMacroMode) { + if (macroModeState.isRecording) { if (key == 'q') { - actions.exitMacroRecordMode(); + macroModeState.exitMacroRecordMode(); vim.inputState = new InputState(); return; } @@ -621,6 +699,9 @@ // Increment count unless count is 0 and key is 0. vim.inputState.pushRepeatDigit(key); } + if (macroModeState.isRecording) { + logKey(macroModeState, key); + } return; } if (command.type == 'keyToKey') { @@ -629,7 +710,7 @@ this.handleKey(cm, command.toKeys[i]); } } else { - if (macroModeState.enteredMacroMode) { + if (macroModeState.isRecording) { logKey(macroModeState, key); } commandDispatcher.processCommand(cm, vim, command); @@ -650,7 +731,7 @@ this.motion = null; this.motionArgs = null; this.keyBuffer = []; // For matching multi-key commands. - this.registerName = null; // Defaults to the unamed register. + this.registerName = null; // Defaults to the unnamed register. } InputState.prototype.pushRepeatDigit = function(n) { if (!this.operator) { @@ -681,29 +762,34 @@ */ function Register(text, linewise) { this.clear(); - if (text) { - this.set(text, linewise); - } + this.keyBuffer = [text || '']; + this.insertModeChanges = []; + this.linewise = !!linewise; } Register.prototype = { - set: function(text, linewise) { - this.text = text; + setText: function(text, linewise) { + this.keyBuffer = [text || '']; this.linewise = !!linewise; }, - append: function(text, linewise) { + pushText: function(text, linewise) { // if this register has ever been set to linewise, use linewise. if (linewise || this.linewise) { - this.text += '\n' + text; + this.keyBuffer.push('\n'); this.linewise = true; - } else { - this.text += text; } + this.keyBuffer.push(text); + }, + pushInsertModeChanges: function(changes) { + this.insertModeChanges.push(createInsertModeChanges(changes)); }, clear: function() { - this.text = ''; + this.keyBuffer = []; + this.insertModeChanges = []; this.linewise = false; }, - toString: function() { return this.text; } + toString: function() { + return this.keyBuffer.join(''); + } }; /* @@ -716,14 +802,14 @@ */ function RegisterController(registers) { this.registers = registers; - this.unamedRegister = registers['"'] = new Register(); + this.unnamedRegister = registers['"'] = new Register(); } RegisterController.prototype = { pushText: function(registerName, operator, text, linewise) { if (linewise && text.charAt(0) == '\n') { text = text.slice(1) + '\n'; } - if(linewise && text.charAt(text.length - 1) !== '\n'){ + if (linewise && text.charAt(text.length - 1) !== '\n'){ text += '\n'; } // Lowercase and uppercase registers refer to the same register. @@ -752,7 +838,7 @@ break; } // Make sure the unnamed register is set to what just happened - this.unamedRegister.set(text, linewise); + this.unnamedRegister.setText(text, linewise); return; } @@ -760,22 +846,19 @@ var append = isUpperCase(registerName); if (append) { register.append(text, linewise); - // The unamed register always has the same value as the last used + // The unnamed register always has the same value as the last used // register. - this.unamedRegister.append(text, linewise); + this.unnamedRegister.append(text, linewise); } else { - register.set(text, linewise); - this.unamedRegister.set(text, linewise); + register.setText(text, linewise); + this.unnamedRegister.setText(text, linewise); } }, - setRegisterText: function(name, text, linewise) { - this.getRegister(name).set(text, linewise); - }, // Gets the register named @name. If one of @name doesn't already exist, - // create it. If @name is invalid, return the unamedRegister. + // create it. If @name is invalid, return the unnamedRegister. getRegister: function(name) { if (!this.isValidRegister(name)) { - return this.unamedRegister; + return this.unnamedRegister; } name = name.toLowerCase(); if (!this.registers[name]) { @@ -811,7 +894,7 @@ // Match commands that take as an argument. if (command.keys[keys.length - 1] == 'character') { selectedCharacter = keys[keys.length - 1]; - if(selectedCharacter.length>1){ + if (selectedCharacter.length>1){ switch(selectedCharacter){ case '': selectedCharacter='\n'; @@ -1099,8 +1182,8 @@ var operator = inputState.operator; var operatorArgs = inputState.operatorArgs || {}; var registerName = inputState.registerName; - var selectionEnd = cm.getCursor('head'); - var selectionStart = cm.getCursor('anchor'); + var selectionEnd = copyCursor(cm.getCursor('head')); + var selectionStart = copyCursor(cm.getCursor('anchor')); // The difference between cur and selection cursors are that cur is // being operated on and ignores that there is a selection. var curStart = copyCursor(selectionEnd); @@ -1156,7 +1239,7 @@ } // TODO: Handle null returns from motion commands better. if (!curEnd) { - curEnd = { ch: curStart.ch, line: curStart.line }; + curEnd = Pos(curStart.line, curStart.ch); } if (vim.visualMode) { // Check if the selection crossed over itself. Will need to shift @@ -1174,6 +1257,7 @@ selectionStart.ch -= 1; } selectionEnd = curEnd; + selectionStart = (motionResult instanceof Array) ? curStart : selectionStart; if (vim.visualLine) { if (cursorIsBefore(selectionStart, selectionEnd)) { selectionStart.ch = 0; @@ -1243,7 +1327,7 @@ }, recordLastEdit: function(vim, inputState, actionCommand) { var macroModeState = vimGlobalState.macroModeState; - if (macroModeState.inReplay) { return; } + if (macroModeState.isPlaying) { return; } vim.lastEditInputState = inputState; vim.lastEditActionCommand = actionCommand; macroModeState.lastInsertModeChanges.changes = []; @@ -1259,22 +1343,22 @@ var motions = { moveToTopLine: function(cm, motionArgs) { var line = getUserVisibleLines(cm).top + motionArgs.repeat -1; - return { line: line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(line)) }; + 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 { line: line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(line)) }; + return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line))); }, moveToBottomLine: function(cm, motionArgs) { var line = getUserVisibleLines(cm).bottom - motionArgs.repeat +1; - return { line: line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(line)) }; + return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line))); }, expandToLine: function(cm, motionArgs) { // Expands forward to end of line, and then to next line if repeat is // >1. Does not handle backward motion! var cur = cm.getCursor(); - return { line: cur.line + motionArgs.repeat - 1, ch: Infinity }; + return Pos(cur.line + motionArgs.repeat - 1, Infinity); }, findNext: function(cm, motionArgs) { var state = getSearchState(cm); @@ -1295,6 +1379,16 @@ } return null; }, + moveToOtherHighlightedEnd: function(cm) { + var curEnd = copyCursor(cm.getCursor('head')); + var curStart = copyCursor(cm.getCursor('anchor')); + if (cursorIsBefore(curStart, curEnd)) { + curEnd.ch += 1; + } else if (cursorIsBefore(curEnd, curStart)) { + curStart.ch -= 1; + } + return ([curEnd,curStart]); + }, jumpToMark: function(cm, motionArgs, vim) { var best = cm.getCursor(); for (var i = 0; i < motionArgs.repeat; i++) { @@ -1329,7 +1423,7 @@ // Vim places the cursor on the first non-whitespace character of // the line if there is one, else it places the cursor at the end // of the line, regardless of whether a mark was found. - best.ch = findFirstNonWhiteSpaceCharacter(cm.getLine(best.line)); + best = Pos(best.line, findFirstNonWhiteSpaceCharacter(cm.getLine(best.line))); } return best; }, @@ -1337,7 +1431,7 @@ var cur = cm.getCursor(); var repeat = motionArgs.repeat; var ch = motionArgs.forward ? cur.ch + repeat : cur.ch - repeat; - return { line: cur.line, ch: ch }; + return Pos(cur.line, ch); }, moveByLines: function(cm, motionArgs, vim) { var cur = cm.getCursor(); @@ -1368,12 +1462,12 @@ (line > last && cur.line == last)) { return; } - if(motionArgs.toFirstChar){ + if (motionArgs.toFirstChar){ endCh=findFirstNonWhiteSpaceCharacter(cm.getLine(line)); vim.lastHPos = endCh; } - vim.lastHSPos = cm.charCoords({line:line, ch:endCh},'div').left; - return { line: line, ch: endCh }; + vim.lastHSPos = cm.charCoords(Pos(line, endCh),'div').left; + return Pos(line, endCh); }, moveByDisplayLines: function(cm, motionArgs, vim) { var cur = cm.getCursor(); @@ -1395,7 +1489,7 @@ var goalCoords = { top: lastCharCoords.top + 8, left: vim.lastHSPos }; var res = cm.coordsChar(goalCoords, 'div'); } else { - var resCoords = cm.charCoords({ line: cm.firstLine(), ch: 0}, 'div'); + var resCoords = cm.charCoords(Pos(cm.firstLine(), 0), 'div'); resCoords.left = vim.lastHSPos; res = cm.coordsChar(resCoords, 'div'); } @@ -1428,7 +1522,7 @@ line += inc; } } - return { line: line, ch: 0 }; + return Pos(line, 0); }, moveByScroll: function(cm, motionArgs, vim) { var scrollbox = cm.getScrollInfo(); @@ -1457,7 +1551,7 @@ motionArgs.selectedCharacter); var increment = motionArgs.forward ? -1 : 1; recordLastCharacterSearch(increment, motionArgs); - if(!curEnd)return cm.getCursor(); + if (!curEnd) return null; curEnd.ch += increment; return curEnd; }, @@ -1482,7 +1576,7 @@ moveToEol: function(cm, motionArgs, vim) { var cur = cm.getCursor(); vim.lastHPos = Infinity; - var retval={ line: cur.line + motionArgs.repeat - 1, ch: Infinity }; + var retval= Pos(cur.line + motionArgs.repeat - 1, Infinity); var end=cm.clipPos(retval); end.ch--; vim.lastHSPos = cm.charCoords(end,'div').left; @@ -1492,8 +1586,8 @@ // Go to the start of the line where the text begins, or the end for // whitespace-only lines var cursor = cm.getCursor(); - return { line: cursor.line, - ch: findFirstNonWhiteSpaceCharacter(cm.getLine(cursor.line)) }; + return Pos(cursor.line, + findFirstNonWhiteSpaceCharacter(cm.getLine(cursor.line))); }, moveToMatchedSymbol: function(cm) { var cursor = cm.getCursor(); @@ -1506,7 +1600,7 @@ do { symbol = lineText.charAt(ch++); if (symbol && isMatchableSymbol(symbol)) { - var endContext = cm.getTokenAt({line:line, ch:ch}).type; + var endContext = cm.getTokenAt(Pos(line, ch)).type; var endCtxLevel = getContextLevel(endContext); if (startCtxLevel >= endCtxLevel) { break; @@ -1514,40 +1608,62 @@ } } while (symbol); if (symbol) { - return findMatchedSymbol(cm, {line:line, ch:ch-1}, symbol); + return findMatchedSymbol(cm, Pos(line, ch-1), symbol); } else { return cursor; } }, moveToStartOfLine: function(cm) { var cursor = cm.getCursor(); - return { line: cursor.line, ch: 0 }; + return Pos(cursor.line, 0); }, moveToLineOrEdgeOfDocument: function(cm, motionArgs) { var lineNum = motionArgs.forward ? cm.lastLine() : cm.firstLine(); if (motionArgs.repeatIsExplicit) { lineNum = motionArgs.repeat - cm.getOption('firstLineNumber'); } - return { line: lineNum, - ch: findFirstNonWhiteSpaceCharacter(cm.getLine(lineNum)) }; + return Pos(lineNum, + findFirstNonWhiteSpaceCharacter(cm.getLine(lineNum))); }, textObjectManipulation: function(cm, motionArgs) { + // 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; + // Inclusive is the difference between a and i // TODO: Instead of using the additional text object map to perform text // object operations, merge the map into the defaultKeyMap and use // motionArgs to define behavior. Define separate entries for 'aw', // 'iw', 'a[', 'i[', etc. var inclusive = !motionArgs.textObjectInner; - if (!textObjects[character]) { + + var tmp; + if (mirroredPairs[character]) { + tmp = selectCompanionObject(cm, mirroredPairs[character], inclusive); + } else if (selfPaired[character]) { + tmp = findBeginningAndEnd(cm, 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 { // No text object defined for this, don't move. return null; } - var tmp = textObjects[character](cm, inclusive); - var start = tmp.start; - var end = tmp.end; - return [start, end]; + + return [tmp.start, tmp.end]; }, + repeatLastCharacterSearch: function(cm, motionArgs) { var lastSearch = vimGlobalState.lastChararacterSearch; var repeat = motionArgs.repeat; @@ -1674,7 +1790,7 @@ var top = cm.getScrollInfo().top; var delta = lineHeight * repeat; var newPos = actionArgs.forward ? top + delta : top - delta; - var cursor = cm.getCursor(); + var cursor = copyCursor(cm.getCursor()); var cursorCoords = cm.charCoords(cursor, 'local'); if (actionArgs.forward) { if (newPos > cursorCoords.top) { @@ -1704,7 +1820,7 @@ }, scrollToCursor: function(cm, actionArgs) { var lineNum = cm.getCursor().line; - var charCoords = cm.charCoords({line: lineNum, ch: 0}, 'local'); + var charCoords = cm.charCoords(Pos(lineNum, 0), 'local'); var height = cm.getScrollInfo().clientHeight; var y = charCoords.top; var lineHeight = charCoords.bottom - y; @@ -1718,29 +1834,21 @@ } cm.scrollTo(null, y); }, - replayMacro: function(cm, actionArgs) { + replayMacro: function(cm, actionArgs, vim) { var registerName = actionArgs.selectedCharacter; var repeat = actionArgs.repeat; var macroModeState = vimGlobalState.macroModeState; if (registerName == '@') { registerName = macroModeState.latestRegister; } - var keyBuffer = parseRegisterToKeyBuffer(macroModeState, registerName); while(repeat--){ - executeMacroKeyBuffer(cm, macroModeState, keyBuffer); + executeMacroRegister(cm, vim, macroModeState, registerName); } }, - exitMacroRecordMode: function() { - var macroModeState = vimGlobalState.macroModeState; - macroModeState.toggle(); - parseKeyBufferToRegister(macroModeState.latestRegister, - macroModeState.macroKeyBuffer); - }, enterMacroRecordMode: function(cm, actionArgs) { var macroModeState = vimGlobalState.macroModeState; var registerName = actionArgs.selectedCharacter; - macroModeState.toggle(cm, registerName); - emptyMacroKeyBuffer(macroModeState); + macroModeState.enterMacroRecordMode(cm, registerName); }, enterInsertMode: function(cm, actionArgs, vim) { if (cm.getOption('readOnly')) { return; } @@ -1749,7 +1857,7 @@ var insertAt = (actionArgs) ? actionArgs.insertAt : null; if (insertAt == 'eol') { var cursor = cm.getCursor(); - cursor = { line: cursor.line, ch: lineLength(cm, cursor.line) }; + cursor = Pos(cursor.line, lineLength(cm, cursor.line)); cm.setCursor(cursor); } else if (insertAt == 'charAfter') { cm.setCursor(offsetCursor(cm.getCursor(), 0, 1)); @@ -1767,7 +1875,7 @@ cm.setOption('keyMap', 'vim-insert'); CodeMirror.signal(cm, "vim-mode-change", {mode: "insert"}); } - if (!vimGlobalState.macroModeState.inReplay) { + if (!vimGlobalState.macroModeState.isPlaying) { // Only record if not replaying. cm.on('change', onChange); cm.on('cursorActivity', onCursorActivity); @@ -1787,15 +1895,13 @@ vim.visualLine = !!actionArgs.linewise; if (vim.visualLine) { curStart.ch = 0; - curEnd = clipCursorToContent(cm, { - line: curStart.line + repeat - 1, - ch: lineLength(cm, curStart.line) - }, true /** includeLineBreak */); + curEnd = clipCursorToContent( + cm, Pos(curStart.line + repeat - 1, lineLength(cm, curStart.line)), + true /** includeLineBreak */); } else { - curEnd = clipCursorToContent(cm, { - line: curStart.line, - ch: curStart.ch + repeat - }, true /** includeLineBreak */); + curEnd = clipCursorToContent( + cm, Pos(curStart.line, curStart.ch + repeat), + true /** includeLineBreak */); } // Make the initial selection. if (!actionArgs.repeatIsExplicit && !vim.visualLine) { @@ -1835,6 +1941,21 @@ updateMark(cm, vim, '>', cursorIsBefore(curStart, curEnd) ? curEnd : curStart); }, + reselectLastSelection: function(cm, _actionArgs, vim) { + if (vim.lastSelection) { + var lastSelection = vim.lastSelection; + cm.setSelection(lastSelection.curStart, lastSelection.curEnd); + if (lastSelection.visualLine) { + vim.visualMode = true; + vim.visualLine = true; + } + else { + vim.visualMode = true; + vim.visualLine = false; + } + CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : ""}); + } + }, joinLines: function(cm, actionArgs, vim) { var curStart, curEnd; if (vim.visualMode) { @@ -1845,29 +1966,29 @@ // Repeat is the number of lines to join. Minimum 2 lines. var repeat = Math.max(actionArgs.repeat, 2); curStart = cm.getCursor(); - curEnd = clipCursorToContent(cm, { line: curStart.line + repeat - 1, - ch: Infinity }); + curEnd = clipCursorToContent(cm, Pos(curStart.line + repeat - 1, + Infinity)); } var finalCh = 0; cm.operation(function() { for (var i = curStart.line; i < curEnd.line; i++) { finalCh = lineLength(cm, curStart.line); - var tmp = { line: curStart.line + 1, - ch: lineLength(cm, curStart.line + 1) }; + var 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 = { line: curStart.line, ch: finalCh }; + var curFinalPos = Pos(curStart.line, finalCh); cm.setCursor(curFinalPos); }); }, newLineAndEnterInsertMode: function(cm, actionArgs, vim) { vim.insertMode = true; - var insertAt = cm.getCursor(); + var insertAt = copyCursor(cm.getCursor()); if (insertAt.line === cm.firstLine() && !actionArgs.after) { // Special case for inserting newline before start of document. - cm.replaceRange('\n', { line: cm.firstLine(), ch: 0 }); + cm.replaceRange('\n', Pos(cm.firstLine(), 0)); cm.setCursor(cm.firstLine(), 0); } else { insertAt.line = (actionArgs.after) ? insertAt.line : @@ -1881,14 +2002,15 @@ this.enterInsertMode(cm, { repeat: actionArgs.repeat }, vim); }, paste: function(cm, actionArgs) { - var cur = cm.getCursor(); + var cur = copyCursor(cm.getCursor()); var register = vimGlobalState.registerController.getRegister( actionArgs.registerName); - if (!register.text) { + var text = register.toString(); + if (!text) { return; } - for (var text = '', i = 0; i < actionArgs.repeat; i++) { - text += register.text; + if (actionArgs.repeat > 1) { + var text = Array(actionArgs.repeat + 1).join(text); } var linewise = register.linewise; if (linewise) { @@ -1908,11 +2030,13 @@ var curPosFinal; var idx; if (linewise && actionArgs.after) { - curPosFinal = { line: cur.line + 1, - ch: findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line + 1)) }; + curPosFinal = Pos( + cur.line + 1, + findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line + 1))); } else if (linewise && !actionArgs.after) { - curPosFinal = { line: cur.line, - ch: findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line)) }; + 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); @@ -1943,22 +2067,22 @@ var curStart = cm.getCursor(); var replaceTo; var curEnd; - if(vim.visualMode){ + if (vim.visualMode){ curStart=cm.getCursor('start'); curEnd=cm.getCursor('end'); // workaround to catch the character under the cursor // existing workaround doesn't cover actions - curEnd=cm.clipPos({line: curEnd.line, ch: curEnd.ch+1}); + curEnd=cm.clipPos(Pos(curEnd.line, curEnd.ch+1)); }else{ var line = cm.getLine(curStart.line); replaceTo = curStart.ch + actionArgs.repeat; if (replaceTo > line.length) { replaceTo=line.length; } - curEnd = { line: curStart.line, ch: replaceTo }; + curEnd = Pos(curStart.line, replaceTo); } - if(replaceWith=='\n'){ - if(!vim.visualMode) cm.replaceRange('', curStart, curEnd); + 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 { @@ -1966,7 +2090,7 @@ //replace all characters in range by selected, but keep linebreaks replaceWithStr=replaceWithStr.replace(/[^\n]/g,replaceWith); cm.replaceRange(replaceWithStr, curStart, curEnd); - if(vim.visualMode){ + if (vim.visualMode){ cm.setCursor(curStart); exitVisualMode(cm); }else{ @@ -1987,20 +2111,20 @@ token = match[0]; start = match.index; end = start + token.length; - if(cur.ch < end)break; + if (cur.ch < end)break; } - if(!actionArgs.backtrack && (end <= cur.ch))return; + if (!actionArgs.backtrack && (end <= cur.ch))return; if (token) { var increment = actionArgs.increase ? 1 : -1; var number = parseInt(token) + (increment * actionArgs.repeat); - var from = {ch:start, line:cur.line}; - var to = {ch:end, line:cur.line}; + var from = Pos(cur.line, start); + var to = Pos(cur.line, end); numberStr = number.toString(); cm.replaceRange(numberStr, from, to); } else { return; } - cm.setCursor({line: cur.line, ch: start + numberStr.length - 1}); + cm.setCursor(Pos(cur.line, start + numberStr.length - 1)); }, repeatLastEdit: function(cm, actionArgs, vim) { var lastEditInputState = vim.lastEditInputState; @@ -2015,36 +2139,6 @@ } }; - var textObjects = { - // TODO: lots of possible exceptions that can be thrown here. Try da( - // outside of a () block. - // TODO: implement text objects for the reverse like }. Should just be - // an additional mapping after moving to the defaultKeyMap. - 'w': function(cm, inclusive) { - return expandWordUnderCursor(cm, inclusive, true /** forward */, - false /** bigWord */); - }, - 'W': function(cm, inclusive) { - return expandWordUnderCursor(cm, inclusive, - true /** forward */, true /** bigWord */); - }, - '{': function(cm, inclusive) { - return selectCompanionObject(cm, '}', inclusive); - }, - '(': function(cm, inclusive) { - return selectCompanionObject(cm, ')', inclusive); - }, - '[': function(cm, inclusive) { - return selectCompanionObject(cm, ']', inclusive); - }, - '\'': function(cm, inclusive) { - return findBeginningAndEnd(cm, "'", inclusive); - }, - '"': function(cm, inclusive) { - return findBeginningAndEnd(cm, '"', inclusive); - } - }; - /* * Below are miscellaneous utility functions used by vim.js */ @@ -2058,7 +2152,7 @@ var maxCh = lineLength(cm, line) - 1; maxCh = (includeLineBreak) ? maxCh + 1 : maxCh; var ch = Math.min(Math.max(0, cur.ch), maxCh); - return { line: line, ch: ch }; + return Pos(line, ch); } function copyArgs(args) { var ret = {}; @@ -2070,7 +2164,7 @@ return ret; } function offsetCursor(cur, offsetLine, offsetCh) { - return { line: cur.line + offsetLine, ch: cur.ch + offsetCh }; + return Pos(cur.line + offsetLine, cur.ch + offsetCh); } function matchKeysPartial(pressed, mapped) { for (var i = 0; i < pressed.length; i++) { @@ -2089,7 +2183,7 @@ }; } function copyCursor(cur) { - return { line: cur.line, ch: cur.ch }; + return Pos(cur.line, cur.ch); } function cursorEqual(cur1, cur2) { return cur1.ch == cur2.ch && cur1.line == cur2.line; @@ -2128,6 +2222,10 @@ function exitVisualMode(cm) { cm.off('mousedown', exitVisualMode); var vim = cm.state.vim; + // can't use selection state here because yank has already reset its cursor + vim.lastSelection = {'curStart': vim.marks['<'].find(), + 'curEnd': vim.marks['>'].find(), 'visualMode': vim.visualMode, + 'visualLine': vim.visualLine}; vim.visualMode = false; vim.visualLine = false; var selectionStart = cm.getCursor('anchor'); @@ -2246,12 +2344,12 @@ } } - return { start: { line: cur.line, ch: wordStart }, - end: { line: cur.line, ch: wordEnd }}; + return { start: Pos(cur.line, wordStart), + end: Pos(cur.line, wordEnd) }; } function recordJumpPosition(cm, oldCur, newCur) { - if(!cursorEqual(oldCur, newCur)) { + if (!cursorEqual(oldCur, newCur)) { vimGlobalState.jumpList.add(cm, oldCur, newCur); } } @@ -2274,7 +2372,7 @@ isComplete: function(state) { if (state.nextCh === state.symb) { state.depth++; - if(state.depth >= 1)return true; + if (state.depth >= 1)return true; } else if (state.nextCh === state.reverseSymb) { state.depth--; } @@ -2306,7 +2404,7 @@ state.reverseSymb = state.symb === '{' ? '}' : '{'; }, isComplete: function(state) { - if(state.nextCh === state.symb)return true; + if (state.nextCh === state.symb)return true; return false; } }, @@ -2328,14 +2426,14 @@ } state.depth--; } - if(token === 'else' && state.depth === 0)return true; + if (token === 'else' && state.depth === 0)return true; } return false; } } }; function findSymbol(cm, repeat, forward, symb) { - var cur = cm.getCursor(); + var cur = copyCursor(cm.getCursor()); var increment = forward ? 1 : -1; var endLine = forward ? cm.lineCount() : -1; var curCh = cur.ch; @@ -2353,10 +2451,10 @@ curMoveThrough: false }; var mode = symbolToMode[symb]; - if(!mode)return cur; + if (!mode)return cur; var init = findSymbolModes[mode].init; var isComplete = findSymbolModes[mode].isComplete; - if(init)init(state); + if (init) { init(state); } while (line !== endLine && repeat) { state.index += increment; state.nextCh = state.lineText.charAt(state.index); @@ -2378,7 +2476,7 @@ } } if (state.nextCh || state.curMoveThrough) { - return { line: line, ch: state.index }; + return Pos(line, state.index); } return cur; } @@ -2492,7 +2590,7 @@ break; } words.push(word); - cur = {line: word.line, ch: forward ? (word.to - 1) : word.from}; + cur = Pos(word.line, forward ? (word.to - 1) : word.from); } var shortCircuit = words.length != repeat; var firstWord = words[0]; @@ -2503,19 +2601,19 @@ // We did not start in the middle of a word. Discard the extra word at the end. lastWord = words.pop(); } - return {line: lastWord.line, ch: lastWord.from}; + return Pos(lastWord.line, lastWord.from); } else if (forward && wordEnd) { - return {line: lastWord.line, ch: lastWord.to - 1}; + 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 {line: lastWord.line, ch: lastWord.to}; + return Pos(lastWord.line, lastWord.to); } else { // b - return {line: lastWord.line, ch: lastWord.from}; + return Pos(lastWord.line, lastWord.from); } } @@ -2531,14 +2629,14 @@ } start = idx; } - return { line: cm.getCursor().line, ch: 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, { line: line, ch: repeat - 1 }); + return clipCursorToContent(cm, Pos(line, repeat - 1)); } function updateMark(cm, vim, markName, pos) { @@ -2581,7 +2679,7 @@ var ch = cur.ch; symb = symb ? symb : cm.getLine(line).charAt(ch); - var symbContext = cm.getTokenAt({line:line, ch:ch+1}).type; + var symbContext = cm.getTokenAt(Pos(line, ch + 1)).type; var symbCtxLevel = getContextLevel(symbContext); var reverseSymb = ({ @@ -2617,7 +2715,7 @@ } nextCh = lineText.charAt(index); } - var revSymbContext = cm.getTokenAt({line:line, ch:index+1}).type; + var revSymbContext = cm.getTokenAt(Pos(line, index + 1)).type; var revSymbCtxLevel = getContextLevel(revSymbContext); if (symbCtxLevel >= revSymbCtxLevel) { if (nextCh === symb) { @@ -2629,18 +2727,30 @@ } if (nextCh) { - return { line: line, ch: index }; + return Pos(line, index); } return cur; } + // TODO: perhaps this finagling of start and end positions belonds + // in codmirror/replaceRange? function selectCompanionObject(cm, revSymb, inclusive) { - var cur = cm.getCursor(); - + var cur = copyCursor(cm.getCursor()); var end = findMatchedSymbol(cm, cur, revSymb); var start = findMatchedSymbol(cm, end); - start.ch += inclusive ? 1 : 0; - end.ch += inclusive ? 0 : 1; + + 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 }; } @@ -2649,7 +2759,7 @@ // have identical opening and closing symbols // TODO support across multiple lines function findBeginningAndEnd(cm, symb, inclusive) { - var cur = cm.getCursor(); + var cur = copyCursor(cm.getCursor()); var line = cm.getLine(cur.line); var chars = line.split(''); var start, end, i, len; @@ -2701,12 +2811,13 @@ } return { - start: { line: cur.line, ch: start }, - end: { line: cur.line, ch: end } + start: Pos(cur.line, start), + end: Pos(cur.line, end) }; } // Search functions + defineOption('pcre', true, 'boolean'); function SearchState() {} SearchState.prototype = { getQuery: function() { @@ -2750,10 +2861,109 @@ if (!escapeNextChar && c == '/') { slashes.push(i); } - escapeNextChar = (c == '\\'); + 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 = (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 (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 '$'. + 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 (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. + 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()); + } + if (stream.match('\\/', true)) { + // \/ => / + output.push('/'); + } else if (stream.match('\\\\', true)) { + // \\ => \ + output.push('\\'); + } else { + // 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. @@ -2785,6 +2995,9 @@ if (!regexPart) { return null; } + if (!getOption('pcre')) { + regexPart = translateRegex(regexPart); + } if (smartCase) { ignoreCase = (/^[^A-Z]*$/).test(regexPart); } @@ -2909,7 +3122,7 @@ // SearchCursor may have returned null because it hit EOF, wrap // around and try again. cursor = cm.getSearchCursor(query, - (prev) ? { line: cm.lastLine() } : {line: cm.firstLine(), ch: 0} ); + (prev) ? Pos(cm.lastLine()) : Pos(cm.firstLine(), 0) ); if (!cursor.find(prev)) { return; } @@ -2966,13 +3179,16 @@ { name: 'map' }, { 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: 'set' }, { name: 'sort', shortName: 'sor' }, { name: 'substitute', shortName: 's' }, { name: 'nohlsearch', shortName: 'noh' }, - { name: 'delmarks', shortName: 'delm' } + { name: 'delmarks', shortName: 'delm' }, + { name: 'registers', shortName: 'reg' } ]; Vim.ExCommandDispatcher = function() { this.buildCommandMap_(); @@ -2990,7 +3206,7 @@ this.parseInput_(cm, inputStream, params); } catch(e) { showConfirm(cm, e); - return; + throw e; } var commandName; if (!params.commandName) { @@ -3116,14 +3332,16 @@ this.commandMap_[commandName] = { name: commandName, type: 'exToEx', - toInput: rhs.substring(1) + toInput: rhs.substring(1), + user: true }; } else { // Ex to key mapping this.commandMap_[commandName] = { name: commandName, type: 'exToKey', - toKeys: parseKeyString(rhs) + toKeys: parseKeyString(rhs), + user: true }; } } else { @@ -3132,7 +3350,8 @@ var mapping = { keys: parseKeyString(lhs), type: 'keyToEx', - exArgs: { input: rhs.substring(1) }}; + exArgs: { input: rhs.substring(1) }, + user: true}; if (ctx) { mapping.context = ctx; } defaultKeymap.unshift(mapping); } else { @@ -3140,12 +3359,45 @@ var mapping = { keys: parseKeyString(lhs), type: 'keyToKey', - toKeys: parseKeyString(rhs) + toKeys: parseKeyString(rhs), + user: true }; if (ctx) { mapping.context = ctx; } defaultKeymap.unshift(mapping); } } + }, + unmap: function(lhs, ctx) { + var arrayEquals = function(a, b) { + if (a === b) return true; + if (a == null || b == null) return true; + if (a.length != b.length) return false; + for (var i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false; + } + return true; + }; + 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 = parseKeyString(lhs); + for (var i = 0; i < defaultKeymap.length; i++) { + if (arrayEquals(keys, defaultKeymap[i].keys) + && defaultKeymap[i].context === ctx + && defaultKeymap[i].user) { + defaultKeymap.splice(i, 1); + return; + } + } + } + throw Error('No such mapping.'); } }; @@ -3156,7 +3408,7 @@ var keys = []; while (str) { match = (/<\w+-.+?>|<\w+>|./).exec(str); - if(match === null)break; + if (match === null)break; key = match[0]; str = str.substring(match.index + key.length); keys.push(key); @@ -3177,6 +3429,16 @@ }, 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', @@ -3185,6 +3447,73 @@ linewise: true }, repeatOverride: params.line+1}); }, + set: function(cm, params) { + var setArgs = params.args; + 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 (!optionIsBoolean && !value || forceGet) { + var oldValue = getOption(optionName); + // If no value is provided, then we assume this is a get. + if (oldValue === true || oldValue === false) { + showConfirm(cm, ' ' + (oldValue ? '' : 'no') + optionName); + } else { + showConfirm(cm, ' ' + optionName + '=' + oldValue); + } + } else { + setOption(optionName, value); + } + }, + registers: function(cm,params) { + var regArgs = params.args; + var registers = vimGlobalState.registerController.registers; + var regInfo = '----------Registers----------

'; + if (!regArgs) { + for (var registerName in registers) { + var text = registers[registerName].toString(); + if (text.length) { + regInfo += '"' + registerName + ' ' + text + '
'; + } + } + } 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.text + '
'; + } + } + showConfirm(cm, regInfo); + }, sort: function(cm, params) { var reverse, ignoreCase, unique, number; function parseArgs() { @@ -3215,8 +3544,8 @@ var lineStart = params.line || cm.firstLine(); var lineEnd = params.lineEnd || params.line || cm.lastLine(); if (lineStart == lineEnd) { return; } - var curStart = { line: lineStart, ch: 0 }; - var curEnd = { line: lineEnd, ch: lineLength(cm, lineEnd) }; + var curStart = Pos(lineStart, 0); + var curEnd = Pos(lineEnd, lineLength(cm, lineEnd)); var text = cm.getRange(curStart, curEnd).split('\n'); var numberRegex = (number == 'decimal') ? /(-?)([\d]+)/ : (number == 'hex') ? /(-?)(?:0x)?([0-9a-f]+)/i : @@ -3279,6 +3608,11 @@ var confirm = false; // Whether to confirm each replace. if (slashes[1]) { replacePart = argString.substring(slashes[1] + 1, slashes[2]); + if (getOption('pcre')) { + replacePart = unescapeRegexReplace(replacePart); + } else { + replacePart = translateRegexReplace(replacePart); + } } if (slashes[2]) { // After the 3rd slash, we can have flags followed by a space followed @@ -3313,7 +3647,7 @@ lineStart = lineEnd; lineEnd = lineStart + count - 1; } - var startPos = clipCursorToContent(cm, { line: lineStart, ch: 0 }); + var startPos = clipCursorToContent(cm, Pos(lineStart, 0)); var cursor = cm.getSearchCursor(query, startPos); doReplace(cm, confirm, lineStart, lineEnd, cursor, query, replacePart); }, @@ -3495,18 +3829,10 @@ * Shift + key modifier to the resulting letter, while preserving other * modifers. */ - // TODO: Figure out a way to catch capslock. function cmKeyToVimKey(key, modifier) { var vimKey = key; - if (isUpperCase(vimKey)) { - // Convert to lower case if shift is not the modifier since the key - // we get from CodeMirror is always upper case. - if (modifier == 'Shift') { - modifier = null; - } - else { + if (isUpperCase(vimKey) && modifier == 'Ctrl') { vimKey = vimKey.toLowerCase(); - } } if (modifier) { // Vim will parse modifier+key combination as a single key. @@ -3532,9 +3858,9 @@ function bindKeys(keys, modifier) { for (var i = 0; i < keys.length; i++) { var key = keys[i]; - if (!modifier && inArray(key, specialSymbols)) { - // Wrap special symbols with '' because that's how CodeMirror binds - // them. + if (!modifier && key.length == 1) { + // Wrap all keys without modifiers with '' to identify them by their + // key characters instead of key identifiers. key = "'" + key + "'"; } var vimKey = cmKeyToVimKey(keys[i], modifier); @@ -3543,7 +3869,7 @@ } } bindKeys(upperCaseAlphabet); - bindKeys(upperCaseAlphabet, 'Shift'); + bindKeys(lowerCaseAlphabet); bindKeys(upperCaseAlphabet, 'Ctrl'); bindKeys(specialSymbols); bindKeys(specialSymbols, 'Ctrl'); @@ -3557,25 +3883,29 @@ function exitInsertMode(cm) { var vim = cm.state.vim; - var inReplay = vimGlobalState.macroModeState.inReplay; - if (!inReplay) { + var macroModeState = vimGlobalState.macroModeState; + var isPlaying = macroModeState.isPlaying; + if (!isPlaying) { cm.off('change', onChange); cm.off('cursorActivity', onCursorActivity); CodeMirror.off(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown); } - if (!inReplay && vim.insertModeRepeat > 1) { + 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; - cm.setCursor(cm.getCursor().line, cm.getCursor().ch-1, true); + cm.setCursor(cm.getCursor().line, cm.getCursor().ch-1); vim.insertMode = false; cm.setOption('keyMap', 'vim'); cm.setOption('disableInput', true); cm.toggleOverwrite(false); // exit replace mode if we were in it. CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"}); + if (macroModeState.isRecording) { + logInsertModeChange(macroModeState); + } } CodeMirror.keyMap['vim-insert'] = { @@ -3599,45 +3929,47 @@ fallthrough: ['vim-insert'] }; - function parseRegisterToKeyBuffer(macroModeState, registerName) { - var match, key; + function executeMacroRegister(cm, vim, macroModeState, registerName) { var register = vimGlobalState.registerController.getRegister(registerName); - var text = register.toString(); - var macroKeyBuffer = macroModeState.macroKeyBuffer; - emptyMacroKeyBuffer(macroModeState); - do { - match = (/<\w+-.+?>|<\w+>|./).exec(text); - if(match === null)break; - key = match[0]; - text = text.substring(match.index + key.length); - macroKeyBuffer.push(key); - } while (text); - return macroKeyBuffer; - } - - function parseKeyBufferToRegister(registerName, keyBuffer) { - var text = keyBuffer.join(''); - vimGlobalState.registerController.setRegisterText(registerName, text); - } - - function emptyMacroKeyBuffer(macroModeState) { - if(macroModeState.isMacroPlaying)return; - var macroKeyBuffer = macroModeState.macroKeyBuffer; - macroKeyBuffer.length = 0; - } - - function executeMacroKeyBuffer(cm, macroModeState, keyBuffer) { - macroModeState.isMacroPlaying = true; - for (var i = 0, len = keyBuffer.length; i < len; i++) { - CodeMirror.Vim.handleKey(cm, keyBuffer[i]); + var keyBuffer = register.keyBuffer; + var imc = 0; + macroModeState.isPlaying = true; + 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. ''. + match = (/<\w+-.+?>|<\w+>|./).exec(text); + key = match[0]; + text = text.substring(match.index + key.length); + CodeMirror.Vim.handleKey(cm, key); + if (vim.insertMode) { + repeatInsertModeChanges( + cm, register.insertModeChanges[imc++].changes, 1); + exitInsertMode(cm); + } + } }; - macroModeState.isMacroPlaying = false; + macroModeState.isPlaying = false; } function logKey(macroModeState, key) { - if(macroModeState.isMacroPlaying)return; - var macroKeyBuffer = macroModeState.macroKeyBuffer; - macroKeyBuffer.push(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(macroModeState.lastInsertModeChanges); + } } /** @@ -3647,15 +3979,17 @@ function onChange(_cm, changeObj) { var macroModeState = vimGlobalState.macroModeState; var lastChange = macroModeState.lastInsertModeChanges; - while (changeObj) { - lastChange.expectCursorActivityForChange = true; - if (changeObj.origin == '+input' || changeObj.origin == 'paste' - || changeObj.origin === undefined /* only in testing */) { - var text = changeObj.text.join('\n'); - lastChange.changes.push(text); + 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'); + lastChange.changes.push(text); + } + // Change objects may be chained with next. + changeObj = changeObj.next; } - // Change objects may be chained with next. - changeObj = changeObj.next; } } @@ -3666,6 +4000,7 @@ */ function onCursorActivity() { var macroModeState = vimGlobalState.macroModeState; + if (macroModeState.isPlaying) { return; } var lastChange = macroModeState.lastInsertModeChanges; if (lastChange.expectCursorActivityForChange) { lastChange.expectCursorActivityForChange = false; @@ -3709,7 +4044,7 @@ */ function repeatLastEdit(cm, vim, repeat, repeatForInsert) { var macroModeState = vimGlobalState.macroModeState; - macroModeState.inReplay = true; + macroModeState.isPlaying = true; var isAction = !!vim.lastEditActionCommand; var cachedInputState = vim.inputState; function repeatCommand() { @@ -3721,10 +4056,15 @@ } function repeatInsert(repeat) { if (macroModeState.lastInsertModeChanges.changes.length > 0) { - // For some reason, repeat cw in desktop VIM will does not repeat + // For some reason, repeat cw in desktop VIM does not repeat // insert mode changes. Will conform to that behavior. repeat = !vim.lastEditActionCommand ? 1 : repeat; - repeatLastInsertModeChanges(cm, repeat, macroModeState); + var changeObject = macroModeState.lastInsertModeChanges; + // This isn't strictly necessary, but since lastInsertModeChanges is + // supposed to be immutable during replay, this helps catch bugs. + macroModeState.lastInsertModeChanges = {}; + repeatInsertModeChanges(cm, changeObject.changes, repeat); + macroModeState.lastInsertModeChanges = changeObject; } } vim.inputState = vim.lastEditInputState; @@ -3750,11 +4090,10 @@ // were called by an exitInsertMode call lower on the stack. exitInsertMode(cm); } - macroModeState.inReplay = false; + macroModeState.isPlaying = false; }; - function repeatLastInsertModeChanges(cm, repeat, macroModeState) { - var lastChange = macroModeState.lastInsertModeChanges; + function repeatInsertModeChanges(cm, changes, repeat) { function keyHandler(binding) { if (typeof binding == 'string') { CodeMirror.commands[binding](cm); @@ -3764,8 +4103,8 @@ return true; } for (var i = 0; i < repeat; i++) { - for (var j = 0; j < lastChange.changes.length; j++) { - var change = lastChange.changes[j]; + for (var j = 0; j < changes.length; j++) { + var change = changes[j]; if (change instanceof InsertModeKey) { CodeMirror.lookupKey(change.keyName, ['vim-insert'], keyHandler); } else { @@ -3781,5 +4120,4 @@ }; // Initialize Vim and make it available as an API. CodeMirror.Vim = Vim(); -} -)(); +}); diff --git a/applications/admin/static/codemirror/lib/codemirror.css b/applications/admin/static/codemirror/lib/codemirror.css index 23eaf74d..d263e44b 100644 --- a/applications/admin/static/codemirror/lib/codemirror.css +++ b/applications/admin/static/codemirror/lib/codemirror.css @@ -36,13 +36,14 @@ min-width: 20px; text-align: right; color: #999; + -moz-box-sizing: content-box; + box-sizing: content-box; } /* CURSOR */ .CodeMirror div.CodeMirror-cursor { border-left: 1px solid black; - z-index: 3; } /* Shown when moving in bi-directional text */ .CodeMirror div.CodeMirror-secondarycursor { @@ -52,13 +53,17 @@ width: auto; border: 0; background: #7e7; - z-index: 1; } /* Can style cursor different in overwrite (non-insert) mode */ -.CodeMirror div.CodeMirror-cursor.CodeMirror-overwrite {} +div.CodeMirror-overwrite div.CodeMirror-cursor {} .cm-tab { display: inline-block; } +.CodeMirror-ruler { + border-left: 1px solid #ccc; + position: absolute; +} + /* DEFAULT THEME */ .cm-s-default .cm-keyword {color: #708;} @@ -114,7 +119,7 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} /* 30px is the magic margin used to hide the element's real scrollbars */ /* See overflow: hidden in .CodeMirror */ margin-bottom: -30px; margin-right: -30px; - padding-bottom: 30px; padding-right: 30px; + padding-bottom: 30px; height: 100%; outline: none; /* Prevent dragging from highlighting the element */ position: relative; @@ -123,6 +128,9 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} } .CodeMirror-sizer { position: relative; + border-right: 30px solid transparent; + -moz-box-sizing: content-box; + box-sizing: content-box; } /* The fake, visible scrollbars. Used to force redraw during scrolling @@ -197,16 +205,7 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} white-space: pre-wrap; word-break: normal; } -.CodeMirror-code pre { - border-right: 30px solid transparent; - width: -webkit-fit-content; - width: -moz-fit-content; - width: fit-content; -} -.CodeMirror-wrap .CodeMirror-code pre { - border-right: none; - width: auto; -} + .CodeMirror-linebackground { position: absolute; left: 0; right: 0; top: 0; bottom: 0; @@ -236,11 +235,16 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} .CodeMirror div.CodeMirror-cursor { position: absolute; - visibility: hidden; border-right: none; width: 0; } -.CodeMirror-focused div.CodeMirror-cursor { + +div.CodeMirror-cursors { + visibility: hidden; + position: relative; + z-index: 1; +} +.CodeMirror-focused div.CodeMirror-cursors { visibility: visible; } @@ -255,9 +259,12 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} /* IE7 hack to prevent it from returning funny offsetTops on the spans */ .CodeMirror span { *vertical-align: text-bottom; } +/* Used to force a border model for a node */ +.cm-force-border { padding-right: .1px; } + @media print { /* Hide the cursor when printing */ - .CodeMirror div.CodeMirror-cursor { + .CodeMirror div.CodeMirror-cursors { visibility: hidden; } } diff --git a/applications/admin/static/codemirror/lib/codemirror.js b/applications/admin/static/codemirror/lib/codemirror.js index d82bd143..c3205cc1 100644 --- a/applications/admin/static/codemirror/lib/codemirror.js +++ b/applications/admin/static/codemirror/lib/codemirror.js @@ -1,26 +1,36 @@ -// CodeMirror version 3.21 +// This is CodeMirror (http://codemirror.net), a code editor +// implemented in JavaScript on top of the browser's DOM. // -// CodeMirror is the only global var we claim -window.CodeMirror = (function() { +// You can find some technical background for some of the code below +// at http://marijnhaverbeke.nl/blog/#cm-internals . + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + module.exports = mod(); + else if (typeof define == "function" && define.amd) // AMD + return define([], mod); + else // Plain browser env + this.CodeMirror = mod(); +})(function() { "use strict"; // BROWSER SNIFFING - // Crude, but necessary to handle a number of hard-to-feature-detect - // bugs and behavior differences. + // Kludges for bugs and behavior differences that can't be feature + // detected are enabled based on userAgent etc sniffing. + var gecko = /gecko\/\d/i.test(navigator.userAgent); - // IE11 currently doesn't count as 'ie', since it has almost none of - // the same bugs as earlier versions. Use ie_gt10 to handle - // incompatibilities in that version. - var old_ie = /MSIE \d/.test(navigator.userAgent); - var ie_lt8 = old_ie && (document.documentMode == null || document.documentMode < 8); - var ie_lt9 = old_ie && (document.documentMode == null || document.documentMode < 9); - var ie_gt10 = /Trident\/([7-9]|\d{2,})\./.test(navigator.userAgent); - var ie = old_ie || ie_gt10; + // ie_uptoN means Internet Explorer version N or lower + var ie_upto10 = /MSIE \d/.test(navigator.userAgent); + var ie_upto7 = ie_upto10 && (document.documentMode == null || document.documentMode < 8); + var ie_upto8 = ie_upto10 && (document.documentMode == null || document.documentMode < 9); + var ie_upto9 = ie_upto10 && (document.documentMode == null || document.documentMode < 10); + var ie_11up = /Trident\/([7-9]|\d{2,})\./.test(navigator.userAgent); + var ie = ie_upto10 || ie_11up; var webkit = /WebKit\//.test(navigator.userAgent); var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(navigator.userAgent); var chrome = /Chrome\//.test(navigator.userAgent); - var opera = /Opera\//.test(navigator.userAgent); + var presto = /Opera\//.test(navigator.userAgent); var safari = /Apple Computer/.test(navigator.vendor); var khtml = /KHTML\//.test(navigator.userAgent); var mac_geLion = /Mac OS X 1\d\D([7-9]|\d\d)\D/.test(navigator.userAgent); @@ -33,152 +43,182 @@ window.CodeMirror = (function() { var mac = ios || /Mac/.test(navigator.platform); var windows = /win/i.test(navigator.platform); - var opera_version = opera && navigator.userAgent.match(/Version\/(\d*\.\d*)/); - if (opera_version) opera_version = Number(opera_version[1]); - if (opera_version && opera_version >= 15) { opera = false; webkit = true; } + var presto_version = presto && navigator.userAgent.match(/Version\/(\d*\.\d*)/); + if (presto_version) presto_version = Number(presto_version[1]); + if (presto_version && presto_version >= 15) { presto = false; webkit = true; } // Some browsers use the wrong event properties to signal cmd/ctrl on OS X - var flipCtrlCmd = mac && (qtwebkit || opera && (opera_version == null || opera_version < 12.11)); - var captureMiddleClick = gecko || (old_ie && !ie_lt9); + var flipCtrlCmd = mac && (qtwebkit || presto && (presto_version == null || presto_version < 12.11)); + var captureRightClick = gecko || (ie && !ie_upto8); - // Optimize some code when these features are not used + // Optimize some code when these features are not used. var sawReadOnlySpans = false, sawCollapsedSpans = false; - // CONSTRUCTOR + // EDITOR CONSTRUCTOR + + // A CodeMirror instance represents an editor. This is the object + // that user code is usually dealing with. function CodeMirror(place, options) { if (!(this instanceof CodeMirror)) return new CodeMirror(place, options); this.options = options = options || {}; // Determine effective options based on given values and defaults. - for (var opt in defaults) if (!options.hasOwnProperty(opt) && defaults.hasOwnProperty(opt)) + for (var opt in defaults) if (!options.hasOwnProperty(opt)) options[opt] = defaults[opt]; setGuttersForLineNumbers(options); - var docStart = typeof options.value == "string" ? 0 : options.value.first; - var display = this.display = makeDisplay(place, docStart); + var doc = options.value; + if (typeof doc == "string") doc = new Doc(doc, options.mode); + this.doc = doc; + + var display = this.display = new Display(place, doc); display.wrapper.CodeMirror = this; updateGutters(this); - if (options.autofocus && !mobile) focusInput(this); - - this.state = {keyMaps: [], - overlays: [], - modeGen: 0, - overwrite: false, focused: false, - suppressEdits: false, - pasteIncoming: false, cutIncoming: false, - draggingText: false, - highlight: new Delayed()}; - themeChanged(this); if (options.lineWrapping) this.display.wrapper.className += " CodeMirror-wrap"; + if (options.autofocus && !mobile) focusInput(this); - var doc = options.value; - if (typeof doc == "string") doc = new Doc(options.value, options.mode); - operation(this, attachDoc)(this, doc); + this.state = { + keyMaps: [], // stores maps added by addKeyMap + overlays: [], // highlighting overlays, as added by addOverlay + modeGen: 0, // bumped when mode/overlay changes, used to invalidate highlighting info + overwrite: false, focused: false, + suppressEdits: false, // used to disable editing during key handlers when in readOnly mode + pasteIncoming: false, cutIncoming: false, // help recognize paste/cut edits in readInput + draggingText: false, + highlight: new Delayed() // stores highlight worker timeout + }; // Override magic textarea content restore that IE sometimes does // on our hidden textarea on reload - if (old_ie) setTimeout(bind(resetInput, this, true), 20); + if (ie_upto10) setTimeout(bind(resetInput, this, true), 20); registerEventHandlers(this); - // IE throws unspecified error in certain cases, when - // trying to access activeElement before onload - var hasFocus; try { hasFocus = (document.activeElement == display.input); } catch(e) { } - if (hasFocus || (options.autofocus && !mobile)) setTimeout(bind(onFocus, this), 20); - else onBlur(this); - operation(this, function() { - for (var opt in optionHandlers) - if (optionHandlers.propertyIsEnumerable(opt)) - optionHandlers[opt](this, options[opt], Init); - for (var i = 0; i < initHooks.length; ++i) initHooks[i](this); - })(); + var cm = this; + runInOp(this, function() { + cm.curOp.forceUpdate = true; + attachDoc(cm, doc); + + if ((options.autofocus && !mobile) || activeElt() == display.input) + setTimeout(bind(onFocus, cm), 20); + else + onBlur(cm); + + for (var opt in optionHandlers) if (optionHandlers.hasOwnProperty(opt)) + optionHandlers[opt](cm, options[opt], Init); + for (var i = 0; i < initHooks.length; ++i) initHooks[i](cm); + }); } // DISPLAY CONSTRUCTOR - function makeDisplay(place, docStart) { - var d = {}; + // The display handles the DOM integration, both for input reading + // and content drawing. It holds references to DOM nodes and + // display-related state. - var input = d.input = elt("textarea", null, null, "position: absolute; padding: 0; width: 1px; height: 1em; outline: none; font-size: 4px;"); + function Display(place, doc) { + var d = this; + + // The semihidden textarea that is focused when the editor is + // focused, and receives input. + var input = d.input = elt("textarea", null, null, "position: absolute; padding: 0; width: 1px; height: 1em; outline: none"); + // The textarea is kept positioned near the cursor to prevent the + // fact that it'll be scrolled into view on input from scrolling + // our fake cursor out of view. On webkit, when wrap=off, paste is + // very slow. So make the area wide instead. if (webkit) input.style.width = "1000px"; else input.setAttribute("wrap", "off"); - // if border: 0; -- iOS fails to open keyboard (issue #1287) + // If border: 0; -- iOS fails to open keyboard (issue #1287) if (ios) input.style.border = "1px solid black"; input.setAttribute("autocorrect", "off"); input.setAttribute("autocapitalize", "off"); input.setAttribute("spellcheck", "false"); // Wraps and hides input textarea d.inputDiv = elt("div", [input], null, "overflow: hidden; position: relative; width: 3px; height: 0px;"); - // The actual fake scrollbars. - d.scrollbarH = elt("div", [elt("div", null, null, "height: 1px")], "CodeMirror-hscrollbar"); - d.scrollbarV = elt("div", [elt("div", null, null, "width: 1px")], "CodeMirror-vscrollbar"); + // The fake scrollbar elements. + d.scrollbarH = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar"); + d.scrollbarV = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar"); + // Covers bottom-right square when both scrollbars are present. d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler"); + // Covers bottom of gutter when coverGutterNextToScrollbar is on + // and h scrollbar is present. d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler"); - // DIVs containing the selection and the actual code + // Will contain the actual code, positioned to cover the viewport. d.lineDiv = elt("div", null, "CodeMirror-code"); + // Elements are added to these to represent selection and cursors. d.selectionDiv = elt("div", null, null, "position: relative; z-index: 1"); - // Blinky cursor, and element used to ensure cursor fits at the end of a line - d.cursor = elt("div", "\u00a0", "CodeMirror-cursor"); - // Secondary cursor, shown when on a 'jump' in bi-directional text - d.otherCursor = elt("div", "\u00a0", "CodeMirror-cursor CodeMirror-secondarycursor"); - // Used to measure text size + d.cursorDiv = elt("div", null, "CodeMirror-cursors"); + // A visibility: hidden element used to find the size of things. d.measure = elt("div", null, "CodeMirror-measure"); + // When lines outside of the viewport are measured, they are drawn in this. + d.lineMeasure = elt("div", null, "CodeMirror-measure"); // Wraps everything that needs to exist inside the vertically-padded coordinate system - d.lineSpace = elt("div", [d.measure, d.selectionDiv, d.lineDiv, d.cursor, d.otherCursor], - null, "position: relative; outline: none"); - // Moved around its parent to cover visible view + d.lineSpace = elt("div", [d.measure, d.lineMeasure, d.selectionDiv, d.cursorDiv, d.lineDiv], + null, "position: relative; outline: none"); + // Moved around its parent to cover visible view. d.mover = elt("div", [elt("div", [d.lineSpace], "CodeMirror-lines")], null, "position: relative"); - // Set to the height of the text, causes scrolling + // Set to the height of the document, allowing scrolling. d.sizer = elt("div", [d.mover], "CodeMirror-sizer"); - // D is needed because behavior of elts with overflow: auto and padding is inconsistent across browsers + // Behavior of elts with overflow: auto and padding is + // inconsistent across browsers. This is used to ensure the + // scrollable area is big enough. d.heightForcer = elt("div", null, null, "position: absolute; height: " + scrollerCutOff + "px; width: 1px;"); - // Will contain the gutters, if any + // Will contain the gutters, if any. d.gutters = elt("div", null, "CodeMirror-gutters"); d.lineGutter = null; - // Provides scrolling + // Actual scrollable element. d.scroller = elt("div", [d.sizer, d.heightForcer, d.gutters], "CodeMirror-scroll"); d.scroller.setAttribute("tabIndex", "-1"); // The element in which the editor lives. d.wrapper = elt("div", [d.inputDiv, d.scrollbarH, d.scrollbarV, d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror"); - // Work around IE7 z-index bug - if (ie_lt8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; } - if (place.appendChild) place.appendChild(d.wrapper); else place(d.wrapper); + // Work around IE7 z-index bug (not perfect, hence IE7 not really being supported) + if (ie_upto7) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; } // Needed to hide big blue blinking cursor on Mobile Safari if (ios) input.style.width = "0px"; if (!webkit) d.scroller.draggable = true; // Needed to handle Tab key in KHTML if (khtml) { d.inputDiv.style.height = "1px"; d.inputDiv.style.position = "absolute"; } // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8). - else if (ie_lt8) d.scrollbarH.style.minWidth = d.scrollbarV.style.minWidth = "18px"; + if (ie_upto7) d.scrollbarH.style.minHeight = d.scrollbarV.style.minWidth = "18px"; - // Current visible range (may be bigger than the view window). - d.viewOffset = d.lastSizeC = 0; - d.showingFrom = d.showingTo = docStart; + if (place.appendChild) place.appendChild(d.wrapper); + else place(d.wrapper); + + // Current rendered range (may be bigger than the view window). + d.viewFrom = d.viewTo = doc.first; + // Information about the rendered lines. + d.view = []; + // Holds info about a single rendered line when it was rendered + // for measurement, while not in view. + d.externalMeasured = null; + // Empty space (in pixels) above the view + d.viewOffset = 0; + d.lastSizeC = 0; + d.updateLineNumbers = null; // Used to only resize the line number gutter when necessary (when // the amount of lines crosses a boundary that makes its width change) d.lineNumWidth = d.lineNumInnerWidth = d.lineNumChars = null; // See readInput and resetInput d.prevInput = ""; - // Set to true when a non-horizontal-scrolling widget is added. As - // an optimization, widget aligning is skipped when d is false. + // Set to true when a non-horizontal-scrolling line widget is + // added. As an optimization, line widget aligning is skipped when + // this is false. d.alignWidgets = false; - // Flag that indicates whether we currently expect input to appear - // (after some event like 'keypress' or 'input') and are polling - // intensively. + // Flag that indicates whether we expect input to appear real soon + // now (after some event like 'keypress' or 'input') and are + // polling intensively. d.pollingFast = false; // Self-resetting timeout for the poller d.poll = new Delayed(); - d.cachedCharWidth = d.cachedTextHeight = null; - d.measureLineCache = []; - d.measureLineCachePos = 0; + d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; // Tracks when resetInput has punted to just putting a short - // string instead of the (large) selection. + // string into the textarea instead of the full selection. d.inaccurateSelection = false; // Tracks the maximum line length so that the horizontal scrollbar @@ -190,7 +230,8 @@ window.CodeMirror = (function() { // Used for measuring wheel scrolling granularity d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null; - return d; + // True when shift is held down. + d.shift = false; } // STATE UPDATES @@ -219,7 +260,7 @@ window.CodeMirror = (function() { cm.display.sizer.style.minWidth = ""; } else { cm.display.wrapper.className = cm.display.wrapper.className.replace(" CodeMirror-wrap", ""); - computeMaxLength(cm); + findMaxLine(cm); } estimateLineHeights(cm); regChange(cm); @@ -227,16 +268,24 @@ window.CodeMirror = (function() { setTimeout(function(){updateScrollbars(cm);}, 100); } + // Returns a function that estimates the height of a line, to use as + // first approximation until the line becomes visible (and is thus + // properly measurable). function estimateHeight(cm) { var th = textHeight(cm.display), wrapping = cm.options.lineWrapping; var perLine = wrapping && Math.max(5, cm.display.scroller.clientWidth / charWidth(cm.display) - 3); return function(line) { - if (lineIsHidden(cm.doc, line)) - return 0; - else if (wrapping) - return (Math.ceil(line.text.length / perLine) || 1) * th; + if (lineIsHidden(cm.doc, line)) return 0; + + var widgetsHeight = 0; + if (line.widgets) for (var i = 0; i < line.widgets.length; i++) { + if (line.widgets[i].height) widgetsHeight += line.widgets[i].height; + } + + if (wrapping) + return widgetsHeight + (Math.ceil(line.text.length / perLine) || 1) * th; else - return th; + return widgetsHeight + th; }; } @@ -266,6 +315,8 @@ window.CodeMirror = (function() { setTimeout(function(){alignHorizontally(cm);}, 20); } + // Rebuild the gutter elements, ensure the margin to the left of the + // code matches their width. function updateGutters(cm) { var gutters = cm.display.gutters, specs = cm.options.gutters; removeChildren(gutters); @@ -278,33 +329,40 @@ window.CodeMirror = (function() { } } gutters.style.display = i ? "" : "none"; + var width = gutters.offsetWidth; + cm.display.sizer.style.marginLeft = width + "px"; + if (i) cm.display.scrollbarH.style.left = cm.options.fixedGutter ? width + "px" : 0; } - function lineLength(doc, line) { + // Compute the character length of a line, taking into account + // collapsed ranges (see markText) that might hide parts, and join + // other lines onto it. + function lineLength(line) { if (line.height == 0) return 0; var len = line.text.length, merged, cur = line; while (merged = collapsedSpanAtStart(cur)) { - var found = merged.find(); - cur = getLine(doc, found.from.line); + var found = merged.find(0, true); + cur = found.from.line; len += found.from.ch - found.to.ch; } cur = line; while (merged = collapsedSpanAtEnd(cur)) { - var found = merged.find(); + var found = merged.find(0, true); len -= cur.text.length - found.from.ch; - cur = getLine(doc, found.to.line); + cur = found.to.line; len += cur.text.length - found.to.ch; } return len; } - function computeMaxLength(cm) { + // Find the longest line in the document. + function findMaxLine(cm) { var d = cm.display, doc = cm.doc; d.maxLine = getLine(doc, doc.first); - d.maxLineLength = lineLength(doc, d.maxLine); + d.maxLineLength = lineLength(d.maxLine); d.maxLineChanged = true; doc.iter(function(line) { - var len = lineLength(doc, line); + var len = lineLength(line); if (len > d.maxLineLength) { d.maxLineLength = len; d.maxLine = line; @@ -326,21 +384,33 @@ window.CodeMirror = (function() { // SCROLLBARS + // Prepare DOM reads needed to update the scrollbars. Done in one + // shot to minimize update/measure roundtrips. + function measureForScrollbars(cm) { + var scroll = cm.display.scroller; + return { + clientHeight: scroll.clientHeight, + barHeight: cm.display.scrollbarV.clientHeight, + scrollWidth: scroll.scrollWidth, clientWidth: scroll.clientWidth, + barWidth: cm.display.scrollbarH.clientWidth, + docHeight: Math.round(cm.doc.height + paddingVert(cm.display)) + }; + } + // Re-synchronize the fake scrollbars with the actual size of the - // content. Optionally force a scrollTop. - function updateScrollbars(cm) { - var d = cm.display, docHeight = cm.doc.height; - var totalHeight = docHeight + paddingVert(d); - d.sizer.style.minHeight = d.heightForcer.style.top = totalHeight + "px"; - d.gutters.style.height = Math.max(totalHeight, d.scroller.clientHeight - scrollerCutOff) + "px"; - var scrollHeight = Math.max(totalHeight, d.scroller.scrollHeight); - var needsH = d.scroller.scrollWidth > (d.scroller.clientWidth + 1); - var needsV = scrollHeight > (d.scroller.clientHeight + 1); + // content. + function updateScrollbars(cm, measure) { + if (!measure) measure = measureForScrollbars(cm); + var d = cm.display; + var scrollHeight = measure.docHeight + scrollerCutOff; + var needsH = measure.scrollWidth > measure.clientWidth; + var needsV = scrollHeight > measure.clientHeight; if (needsV) { d.scrollbarV.style.display = "block"; d.scrollbarV.style.bottom = needsH ? scrollbarWidth(d.measure) + "px" : "0"; + // A bug in IE8 can cause this value to be negative, so guard it. d.scrollbarV.firstChild.style.height = - (scrollHeight - d.scroller.clientHeight + d.scrollbarV.clientHeight) + "px"; + Math.max(0, scrollHeight - measure.clientHeight + (measure.barHeight || d.scrollbarV.clientHeight)) + "px"; } else { d.scrollbarV.style.display = ""; d.scrollbarV.firstChild.style.height = "0"; @@ -349,7 +419,7 @@ window.CodeMirror = (function() { d.scrollbarH.style.display = "block"; d.scrollbarH.style.right = needsV ? scrollbarWidth(d.measure) + "px" : "0"; d.scrollbarH.firstChild.style.width = - (d.scroller.scrollWidth - d.scroller.clientWidth + d.scrollbarH.clientWidth) + "px"; + (measure.scrollWidth - measure.clientWidth + (measure.barWidth || d.scrollbarH.clientWidth)) + "px"; } else { d.scrollbarH.style.display = ""; d.scrollbarH.firstChild.style.width = "0"; @@ -366,33 +436,61 @@ window.CodeMirror = (function() { if (mac_geLion && scrollbarWidth(d.measure) === 0) { d.scrollbarV.style.minWidth = d.scrollbarH.style.minHeight = mac_geMountainLion ? "18px" : "12px"; - d.scrollbarV.style.pointerEvents = d.scrollbarH.style.pointerEvents = "none"; + var barMouseDown = function(e) { + if (e_target(e) != d.scrollbarV && e_target(e) != d.scrollbarH) + operation(cm, onMouseDown)(e); + }; + on(d.scrollbarV, "mousedown", barMouseDown); + on(d.scrollbarH, "mousedown", barMouseDown); } } + // Compute the lines that are visible in a given viewport (defaults + // the the current scroll position). viewPort may contain top, + // height, and ensure (see op.scrollToPos) properties. function visibleLines(display, doc, viewPort) { - var top = display.scroller.scrollTop, height = display.wrapper.clientHeight; - if (typeof viewPort == "number") top = viewPort; - else if (viewPort) {top = viewPort.top; height = viewPort.bottom - viewPort.top;} + var top = viewPort && viewPort.top != null ? viewPort.top : display.scroller.scrollTop; top = Math.floor(top - paddingTop(display)); - var bottom = Math.ceil(top + height); - return {from: lineAtHeight(doc, top), to: lineAtHeight(doc, bottom)}; + var bottom = viewPort && viewPort.bottom != null ? viewPort.bottom : top + display.wrapper.clientHeight; + + var from = lineAtHeight(doc, top), to = lineAtHeight(doc, bottom); + // Ensure is a {from: {line, ch}, to: {line, ch}} object, and + // forces those lines into the viewport (if possible). + if (viewPort && viewPort.ensure) { + var ensureFrom = viewPort.ensure.from.line, ensureTo = viewPort.ensure.to.line; + if (ensureFrom < from) + return {from: ensureFrom, + to: lineAtHeight(doc, heightAtLine(getLine(doc, ensureFrom)) + display.wrapper.clientHeight)}; + if (Math.min(ensureTo, doc.lastLine()) >= to) + return {from: lineAtHeight(doc, heightAtLine(getLine(doc, ensureTo)) - display.wrapper.clientHeight), + to: ensureTo}; + } + return {from: from, to: to}; } // LINE NUMBERS + // Re-align line numbers and gutter marks to compensate for + // horizontal scrolling. function alignHorizontally(cm) { - var display = cm.display; + var display = cm.display, view = display.view; if (!display.alignWidgets && (!display.gutters.firstChild || !cm.options.fixedGutter)) return; var comp = compensateForHScroll(display) - display.scroller.scrollLeft + cm.doc.scrollLeft; - var gutterW = display.gutters.offsetWidth, l = comp + "px"; - for (var n = display.lineDiv.firstChild; n; n = n.nextSibling) if (n.alignable) { - for (var i = 0, a = n.alignable; i < a.length; ++i) a[i].style.left = l; + var gutterW = display.gutters.offsetWidth, left = comp + "px"; + for (var i = 0; i < view.length; i++) if (!view[i].hidden) { + if (cm.options.fixedGutter && view[i].gutter) + view[i].gutter.style.left = left; + var align = view[i].alignable; + if (align) for (var j = 0; j < align.length; j++) + align[j].style.left = left; } if (cm.options.fixedGutter) display.gutters.style.left = (comp + gutterW) + "px"; } + // Used to ensure that the line number gutter is still the right + // size for the current document size. Returns true when an update + // is needed. function maybeUpdateLineNumberWidth(cm) { if (!cm.options.lineNumbers) return false; var doc = cm.doc, last = lineNumberFor(cm.options, doc.first + doc.size - 1), display = cm.display; @@ -405,6 +503,9 @@ window.CodeMirror = (function() { display.lineNumWidth = display.lineNumInnerWidth + padding; display.lineNumChars = display.lineNumInnerWidth ? last.length : -1; display.lineGutter.style.width = display.lineNumWidth + "px"; + var width = display.gutters.offsetWidth; + display.scrollbarH.style.left = cm.options.fixedGutter ? width + "px" : 0; + display.sizer.style.marginLeft = width + "px"; return true; } return false; @@ -413,192 +514,181 @@ window.CodeMirror = (function() { function lineNumberFor(options, i) { return String(options.lineNumberFormatter(i + options.firstLineNumber)); } + + // Computes display.scroller.scrollLeft + display.gutters.offsetWidth, + // but using getBoundingClientRect to get a sub-pixel-accurate + // result. function compensateForHScroll(display) { - return getRect(display.scroller).left - getRect(display.sizer).left; + return display.scroller.getBoundingClientRect().left - display.sizer.getBoundingClientRect().left; } // DISPLAY DRAWING - function updateDisplay(cm, changes, viewPort, forced) { - var oldFrom = cm.display.showingFrom, oldTo = cm.display.showingTo, updated; + // Updates the display, selection, and scrollbars, using the + // information in display.view to find out which nodes are no longer + // up-to-date. Tries to bail out early when no changes are needed, + // unless forced is true. + // Returns true if an actual update happened, false otherwise. + function updateDisplay(cm, viewPort, forced) { + var oldFrom = cm.display.viewFrom, oldTo = cm.display.viewTo, updated; var visible = visibleLines(cm.display, cm.doc, viewPort); for (var first = true;; first = false) { var oldWidth = cm.display.scroller.clientWidth; - if (!updateDisplayInner(cm, changes, visible, forced)) break; + if (!updateDisplayInner(cm, visible, forced)) break; updated = true; - changes = []; + + // If the max line changed since it was last measured, measure it, + // and ensure the document's width matches it. + if (cm.display.maxLineChanged && !cm.options.lineWrapping) + adjustContentWidth(cm); + + var barMeasure = measureForScrollbars(cm); updateSelection(cm); - updateScrollbars(cm); + setDocumentHeight(cm, barMeasure); + updateScrollbars(cm, barMeasure); if (first && cm.options.lineWrapping && oldWidth != cm.display.scroller.clientWidth) { forced = true; continue; } forced = false; - // Clip forced viewport to actual scrollable area - if (viewPort) - viewPort = Math.min(cm.display.scroller.scrollHeight - cm.display.scroller.clientHeight, - typeof viewPort == "number" ? viewPort : viewPort.top); + // Clip forced viewport to actual scrollable area. + if (viewPort && viewPort.top != null) + viewPort = {top: Math.min(barMeasure.docHeight - scrollerCutOff - barMeasure.clientHeight, viewPort.top)}; + // Updated line heights might result in the drawn area not + // actually covering the viewport. Keep looping until it does. visible = visibleLines(cm.display, cm.doc, viewPort); - if (visible.from >= cm.display.showingFrom && visible.to <= cm.display.showingTo) + if (visible.from >= cm.display.viewFrom && visible.to <= cm.display.viewTo) break; } + cm.display.updateLineNumbers = null; if (updated) { signalLater(cm, "update", cm); - if (cm.display.showingFrom != oldFrom || cm.display.showingTo != oldTo) - signalLater(cm, "viewportChange", cm, cm.display.showingFrom, cm.display.showingTo); + if (cm.display.viewFrom != oldFrom || cm.display.viewTo != oldTo) + signalLater(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo); } return updated; } - // Uses a set of changes plus the current scroll position to - // determine which DOM updates have to be made, and makes the - // updates. - function updateDisplayInner(cm, changes, visible, forced) { + // Does the actual updating of the line display. Bails out + // (returning false) when there is nothing to be done and forced is + // false. + function updateDisplayInner(cm, visible, forced) { var display = cm.display, doc = cm.doc; if (!display.wrapper.offsetWidth) { - display.showingFrom = display.showingTo = doc.first; - display.viewOffset = 0; + resetView(cm); return; } // Bail out if the visible area is already rendered and nothing changed. - if (!forced && changes.length == 0 && - visible.from > display.showingFrom && visible.to < display.showingTo) + if (!forced && visible.from >= display.viewFrom && visible.to <= display.viewTo && + countDirtyView(cm) == 0) return; if (maybeUpdateLineNumberWidth(cm)) - changes = [{from: doc.first, to: doc.first + doc.size}]; - var gutterW = display.sizer.style.marginLeft = display.gutters.offsetWidth + "px"; - display.scrollbarH.style.left = cm.options.fixedGutter ? gutterW : "0"; - - // Used to determine which lines need their line numbers updated - var positionsChangedFrom = Infinity; - if (cm.options.lineNumbers) - for (var i = 0; i < changes.length; ++i) - if (changes[i].diff && changes[i].from < positionsChangedFrom) { positionsChangedFrom = changes[i].from; } + resetView(cm); + var dims = getDimensions(cm); + // Compute a suitable new viewport (from & to) var end = doc.first + doc.size; var from = Math.max(visible.from - cm.options.viewportMargin, doc.first); var to = Math.min(end, visible.to + cm.options.viewportMargin); - if (display.showingFrom < from && from - display.showingFrom < 20) from = Math.max(doc.first, display.showingFrom); - if (display.showingTo > to && display.showingTo - to < 20) to = Math.min(end, display.showingTo); + if (display.viewFrom < from && from - display.viewFrom < 20) from = Math.max(doc.first, display.viewFrom); + if (display.viewTo > to && display.viewTo - to < 20) to = Math.min(end, display.viewTo); if (sawCollapsedSpans) { - from = lineNo(visualLine(doc, getLine(doc, from))); - while (to < end && lineIsHidden(doc, getLine(doc, to))) ++to; + from = visualLineNo(cm.doc, from); + to = visualLineEndNo(cm.doc, to); } - // Create a range of theoretically intact lines, and punch holes - // in that using the change info. - var intact = [{from: Math.max(display.showingFrom, doc.first), - to: Math.min(display.showingTo, end)}]; - if (intact[0].from >= intact[0].to) intact = []; - else intact = computeIntact(intact, changes); - // When merged lines are present, we might have to reduce the - // intact ranges because changes in continued fragments of the - // intact lines do require the lines to be redrawn. - if (sawCollapsedSpans) - for (var i = 0; i < intact.length; ++i) { - var range = intact[i], merged; - while (merged = collapsedSpanAtEnd(getLine(doc, range.to - 1))) { - var newTo = merged.find().from.line; - if (newTo > range.from) range.to = newTo; - else { intact.splice(i--, 1); break; } - } - } - - // Clip off the parts that won't be visible - var intactLines = 0; - for (var i = 0; i < intact.length; ++i) { - var range = intact[i]; - if (range.from < from) range.from = from; - if (range.to > to) range.to = to; - if (range.from >= range.to) intact.splice(i--, 1); - else intactLines += range.to - range.from; - } - if (!forced && intactLines == to - from && from == display.showingFrom && to == display.showingTo) { - updateViewOffset(cm); - return; - } - intact.sort(function(a, b) {return a.from - b.from;}); - - // Avoid crashing on IE's "unspecified error" when in iframes - try { - var focused = document.activeElement; - } catch(e) {} - if (intactLines < (to - from) * .7) display.lineDiv.style.display = "none"; - patchDisplay(cm, from, to, intact, positionsChangedFrom); - display.lineDiv.style.display = ""; - if (focused && document.activeElement != focused && focused.offsetHeight) focused.focus(); - - var different = from != display.showingFrom || to != display.showingTo || + var different = from != display.viewFrom || to != display.viewTo || display.lastSizeC != display.wrapper.clientHeight; - // This is just a bogus formula that detects when the editor is - // resized or the font size changes. + adjustView(cm, from, to); + + display.viewOffset = heightAtLine(getLine(cm.doc, display.viewFrom)); + // Position the mover div to align with the current scroll position + cm.display.mover.style.top = display.viewOffset + "px"; + + var toUpdate = countDirtyView(cm); + if (!different && toUpdate == 0 && !forced) return; + + // For big changes, we hide the enclosing element during the + // update, since that speeds up the operations on most browsers. + var focused = activeElt(); + if (toUpdate > 4) display.lineDiv.style.display = "none"; + patchDisplay(cm, display.updateLineNumbers, dims); + if (toUpdate > 4) display.lineDiv.style.display = ""; + // There might have been a widget with a focused element that got + // hidden or updated, if so re-focus it. + if (focused && activeElt() != focused && focused.offsetHeight) focused.focus(); + + // Prevent selection and cursors from interfering with the scroll + // width. + removeChildren(display.cursorDiv); + removeChildren(display.selectionDiv); + if (different) { display.lastSizeC = display.wrapper.clientHeight; startWorker(cm, 400); } - display.showingFrom = from; display.showingTo = to; - display.gutters.style.height = ""; updateHeightsInViewport(cm); - updateViewOffset(cm); return true; } + function adjustContentWidth(cm) { + var display = cm.display; + var width = measureChar(cm, display.maxLine, display.maxLine.text.length).left; + display.maxLineChanged = false; + var minWidth = Math.max(0, width + 3); + var maxScrollLeft = Math.max(0, display.sizer.offsetLeft + minWidth + scrollerCutOff - display.scroller.clientWidth); + display.sizer.style.minWidth = minWidth + "px"; + if (maxScrollLeft < cm.doc.scrollLeft) + setScrollLeft(cm, Math.min(display.scroller.scrollLeft, maxScrollLeft), true); + } + + function setDocumentHeight(cm, measure) { + cm.display.sizer.style.minHeight = cm.display.heightForcer.style.top = measure.docHeight + "px"; + cm.display.gutters.style.height = Math.max(measure.docHeight, measure.clientHeight - scrollerCutOff) + "px"; + } + + // Read the actual heights of the rendered lines, and update their + // stored heights to match. function updateHeightsInViewport(cm) { var display = cm.display; var prevBottom = display.lineDiv.offsetTop; - for (var node = display.lineDiv.firstChild, height; node; node = node.nextSibling) if (node.lineObj) { - if (ie_lt8) { - var bot = node.offsetTop + node.offsetHeight; + for (var i = 0; i < display.view.length; i++) { + var cur = display.view[i], height; + if (cur.hidden) continue; + if (ie_upto7) { + var bot = cur.node.offsetTop + cur.node.offsetHeight; height = bot - prevBottom; prevBottom = bot; } else { - var box = getRect(node); + var box = cur.node.getBoundingClientRect(); height = box.bottom - box.top; } - var diff = node.lineObj.height - height; + var diff = cur.line.height - height; if (height < 2) height = textHeight(display); if (diff > .001 || diff < -.001) { - updateLineHeight(node.lineObj, height); - var widgets = node.lineObj.widgets; - if (widgets) for (var i = 0; i < widgets.length; ++i) - widgets[i].height = widgets[i].node.offsetHeight; + updateLineHeight(cur.line, height); + updateWidgetHeight(cur.line); + if (cur.rest) for (var j = 0; j < cur.rest.length; j++) + updateWidgetHeight(cur.rest[j]); } } } - function updateViewOffset(cm) { - var off = cm.display.viewOffset = heightAtLine(cm, getLine(cm.doc, cm.display.showingFrom)); - // Position the mover div to align with the current virtual scroll position - cm.display.mover.style.top = off + "px"; - } - - function computeIntact(intact, changes) { - for (var i = 0, l = changes.length || 0; i < l; ++i) { - var change = changes[i], intact2 = [], diff = change.diff || 0; - for (var j = 0, l2 = intact.length; j < l2; ++j) { - var range = intact[j]; - if (change.to <= range.from && change.diff) { - intact2.push({from: range.from + diff, to: range.to + diff}); - } else if (change.to <= range.from || change.from >= range.to) { - intact2.push(range); - } else { - if (change.from > range.from) - intact2.push({from: range.from, to: change.from}); - if (change.to < range.to) - intact2.push({from: change.to + diff, to: range.to + diff}); - } - } - intact = intact2; - } - return intact; + // Read and store the height of line widgets associated with the + // given line. + function updateWidgetHeight(line) { + if (line.widgets) for (var i = 0; i < line.widgets.length; ++i) + line.widgets[i].height = line.widgets[i].node.offsetHeight; } + // Do a bulk-read of the DOM positions and sizes needed to draw the + // view, so that we don't interleave reading and writing to the DOM. function getDimensions(cm) { var d = cm.display, left = {}, width = {}; for (var n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) { @@ -612,154 +702,207 @@ window.CodeMirror = (function() { wrapperWidth: d.wrapper.clientWidth}; } - function patchDisplay(cm, from, to, intact, updateNumbersFrom) { - var dims = getDimensions(cm); + // Sync the actual display DOM structure with display.view, removing + // nodes for lines that are no longer in view, and creating the ones + // that are not there yet, and updating the ones that are out of + // date. + function patchDisplay(cm, updateNumbersFrom, dims) { var display = cm.display, lineNumbers = cm.options.lineNumbers; - if (!intact.length && (!webkit || !cm.display.currentWheelTarget)) - removeChildren(display.lineDiv); var container = display.lineDiv, cur = container.firstChild; function rm(node) { var next = node.nextSibling; - if (webkit && mac && cm.display.currentWheelTarget == node) { + // Works around a throw-scroll bug in OS X Webkit + if (webkit && mac && cm.display.currentWheelTarget == node) node.style.display = "none"; - node.lineObj = null; - } else { + else node.parentNode.removeChild(node); - } return next; } - var nextIntact = intact.shift(), lineN = from; - cm.doc.iter(from, to, function(line) { - if (nextIntact && nextIntact.to == lineN) nextIntact = intact.shift(); - if (lineIsHidden(cm.doc, line)) { - if (line.height != 0) updateLineHeight(line, 0); - if (line.widgets && cur && cur.previousSibling) for (var i = 0; i < line.widgets.length; ++i) { - var w = line.widgets[i]; - if (w.showIfHidden) { - var prev = cur.previousSibling; - if (/pre/i.test(prev.nodeName)) { - var wrap = elt("div", null, null, "position: relative"); - prev.parentNode.replaceChild(wrap, prev); - wrap.appendChild(prev); - prev = wrap; - } - var wnode = prev.appendChild(elt("div", [w.node], "CodeMirror-linewidget")); - if (!w.handleMouseEvents) wnode.ignoreEvents = true; - positionLineWidget(w, wnode, prev, dims); - } + var view = display.view, lineN = display.viewFrom; + // Loop over the elements in the view, syncing cur (the DOM nodes + // in display.lineDiv) with the view as we go. + for (var i = 0; i < view.length; i++) { + var lineView = view[i]; + if (lineView.hidden) { + } else if (!lineView.node) { // Not drawn yet + var node = buildLineElement(cm, lineView, lineN, dims); + container.insertBefore(node, cur); + } else { // Already drawn + while (cur != lineView.node) cur = rm(cur); + var updateNumber = lineNumbers && updateNumbersFrom != null && + updateNumbersFrom <= lineN && lineView.lineNumber; + if (lineView.changes) { + if (indexOf(lineView.changes, "gutter") > -1) updateNumber = false; + updateLineForChanges(cm, lineView, lineN, dims); } - } else if (nextIntact && nextIntact.from <= lineN && nextIntact.to > lineN) { - // This line is intact. Skip to the actual node. Update its - // line number if needed. - while (cur.lineObj != line) cur = rm(cur); - if (lineNumbers && updateNumbersFrom <= lineN && cur.lineNumber) - setTextContent(cur.lineNumber, lineNumberFor(cm.options, lineN)); - cur = cur.nextSibling; - } else { - // For lines with widgets, make an attempt to find and reuse - // the existing element, so that widgets aren't needlessly - // removed and re-inserted into the dom - if (line.widgets) for (var j = 0, search = cur, reuse; search && j < 20; ++j, search = search.nextSibling) - if (search.lineObj == line && /div/i.test(search.nodeName)) { reuse = search; break; } - // This line needs to be generated. - var lineNode = buildLineElement(cm, line, lineN, dims, reuse); - if (lineNode != reuse) { - container.insertBefore(lineNode, cur); - } else { - while (cur != reuse) cur = rm(cur); - cur = cur.nextSibling; + if (updateNumber) { + removeChildren(lineView.lineNumber); + lineView.lineNumber.appendChild(document.createTextNode(lineNumberFor(cm.options, lineN))); } - - lineNode.lineObj = line; + cur = lineView.node.nextSibling; } - ++lineN; - }); + lineN += lineView.size; + } while (cur) cur = rm(cur); } - function buildLineElement(cm, line, lineNo, dims, reuse) { - var built = buildLineContent(cm, line), lineElement = built.pre; - var markers = line.gutterMarkers, display = cm.display, wrap; - - var bgClass = built.bgClass ? built.bgClass + " " + (line.bgClass || "") : line.bgClass; - if (!cm.options.lineNumbers && !markers && !bgClass && !line.wrapClass && !line.widgets) - return lineElement; - - // Lines with gutter elements, widgets or a background class need - // to be wrapped again, and have the extra elements added to the - // wrapper div - - if (reuse) { - reuse.alignable = null; - var isOk = true, widgetsSeen = 0, insertBefore = null; - for (var n = reuse.firstChild, next; n; n = next) { - next = n.nextSibling; - if (!/\bCodeMirror-linewidget\b/.test(n.className)) { - reuse.removeChild(n); - } else { - for (var i = 0; i < line.widgets.length; ++i) { - var widget = line.widgets[i]; - if (widget.node == n.firstChild) { - if (!widget.above && !insertBefore) insertBefore = n; - positionLineWidget(widget, n, reuse, dims); - ++widgetsSeen; - break; - } - } - if (i == line.widgets.length) { isOk = false; break; } - } - } - reuse.insertBefore(lineElement, insertBefore); - if (isOk && widgetsSeen == line.widgets.length) { - wrap = reuse; - reuse.className = line.wrapClass || ""; - } + // When an aspect of a line changes, a string is added to + // lineView.changes. This updates the relevant part of the line's + // DOM structure. + function updateLineForChanges(cm, lineView, lineN, dims) { + for (var j = 0; j < lineView.changes.length; j++) { + var type = lineView.changes[j]; + if (type == "text") updateLineText(cm, lineView); + else if (type == "gutter") updateLineGutter(cm, lineView, lineN, dims); + else if (type == "class") updateLineClasses(lineView); + else if (type == "widget") updateLineWidgets(lineView, dims); } - if (!wrap) { - wrap = elt("div", null, line.wrapClass, "position: relative"); - wrap.appendChild(lineElement); + lineView.changes = null; + } + + // Lines with gutter elements, widgets or a background class need to + // be wrapped, and have the extra elements added to the wrapper div + function ensureLineWrapped(lineView) { + if (lineView.node == lineView.text) { + lineView.node = elt("div", null, null, "position: relative"); + if (lineView.text.parentNode) + lineView.text.parentNode.replaceChild(lineView.node, lineView.text); + lineView.node.appendChild(lineView.text); + if (ie_upto7) lineView.node.style.zIndex = 2; } - // Kludge to make sure the styled element lies behind the selection (by z-index) - if (bgClass) - wrap.insertBefore(elt("div", null, bgClass + " CodeMirror-linebackground"), wrap.firstChild); + return lineView.node; + } + + function updateLineBackground(lineView) { + var cls = lineView.bgClass ? lineView.bgClass + " " + (lineView.line.bgClass || "") : lineView.line.bgClass; + if (cls) cls += " CodeMirror-linebackground"; + if (lineView.background) { + if (cls) lineView.background.className = cls; + else { lineView.background.parentNode.removeChild(lineView.background); lineView.background = null; } + } else if (cls) { + var wrap = ensureLineWrapped(lineView); + lineView.background = wrap.insertBefore(elt("div", null, cls), wrap.firstChild); + } + } + + // Wrapper around buildLineContent which will reuse the structure + // in display.externalMeasured when possible. + function getLineContent(cm, lineView) { + var ext = cm.display.externalMeasured; + if (ext && ext.line == lineView.line) { + cm.display.externalMeasured = null; + lineView.measure = ext.measure; + return ext.built; + } + return buildLineContent(cm, lineView); + } + + // Redraw the line's text. Interacts with the background and text + // classes because the mode may output tokens that influence these + // classes. + function updateLineText(cm, lineView) { + var cls = lineView.text.className; + var built = getLineContent(cm, lineView); + if (lineView.text == lineView.node) lineView.node = built.pre; + lineView.text.parentNode.replaceChild(built.pre, lineView.text); + lineView.text = built.pre; + if (built.bgClass != lineView.bgClass || built.textClass != lineView.textClass) { + lineView.bgClass = built.bgClass; + lineView.textClass = built.textClass; + updateLineClasses(lineView); + } else if (cls) { + lineView.text.className = cls; + } + } + + function updateLineClasses(lineView) { + updateLineBackground(lineView); + if (lineView.line.wrapClass) + ensureLineWrapped(lineView).className = lineView.line.wrapClass; + else if (lineView.node != lineView.text) + lineView.node.className = ""; + var textClass = lineView.textClass ? lineView.textClass + " " + (lineView.line.textClass || "") : lineView.line.textClass; + lineView.text.className = textClass || ""; + } + + function updateLineGutter(cm, lineView, lineN, dims) { + if (lineView.gutter) { + lineView.node.removeChild(lineView.gutter); + lineView.gutter = null; + } + var markers = lineView.line.gutterMarkers; if (cm.options.lineNumbers || markers) { - var gutterWrap = wrap.insertBefore(elt("div", null, "CodeMirror-gutter-wrapper", "position: absolute; left: " + - (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px"), - lineElement); - if (cm.options.fixedGutter) (wrap.alignable || (wrap.alignable = [])).push(gutterWrap); + var wrap = ensureLineWrapped(lineView); + var gutterWrap = lineView.gutter = + wrap.insertBefore(elt("div", null, "CodeMirror-gutter-wrapper", "position: absolute; left: " + + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px"), + lineView.text); if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"])) - wrap.lineNumber = gutterWrap.appendChild( - elt("div", lineNumberFor(cm.options, lineNo), + lineView.lineNumber = gutterWrap.appendChild( + elt("div", lineNumberFor(cm.options, lineN), "CodeMirror-linenumber CodeMirror-gutter-elt", "left: " + dims.gutterLeft["CodeMirror-linenumbers"] + "px; width: " - + display.lineNumInnerWidth + "px")); - if (markers) - for (var k = 0; k < cm.options.gutters.length; ++k) { - var id = cm.options.gutters[k], found = markers.hasOwnProperty(id) && markers[id]; - if (found) - gutterWrap.appendChild(elt("div", [found], "CodeMirror-gutter-elt", "left: " + - dims.gutterLeft[id] + "px; width: " + dims.gutterWidth[id] + "px")); - } + + cm.display.lineNumInnerWidth + "px")); + if (markers) for (var k = 0; k < cm.options.gutters.length; ++k) { + var id = cm.options.gutters[k], found = markers.hasOwnProperty(id) && markers[id]; + if (found) + gutterWrap.appendChild(elt("div", [found], "CodeMirror-gutter-elt", "left: " + + dims.gutterLeft[id] + "px; width: " + dims.gutterWidth[id] + "px")); + } } - if (ie_lt8) wrap.style.zIndex = 2; - if (line.widgets && wrap != reuse) for (var i = 0, ws = line.widgets; i < ws.length; ++i) { + } + + function updateLineWidgets(lineView, dims) { + if (lineView.alignable) lineView.alignable = null; + for (var node = lineView.node.firstChild, next; node; node = next) { + var next = node.nextSibling; + if (node.className == "CodeMirror-linewidget") + lineView.node.removeChild(node); + } + insertLineWidgets(lineView, dims); + } + + // Build a line's DOM representation from scratch + function buildLineElement(cm, lineView, lineN, dims) { + var built = getLineContent(cm, lineView); + lineView.text = lineView.node = built.pre; + if (built.bgClass) lineView.bgClass = built.bgClass; + if (built.textClass) lineView.textClass = built.textClass; + + updateLineClasses(lineView); + updateLineGutter(cm, lineView, lineN, dims); + insertLineWidgets(lineView, dims); + return lineView.node; + } + + // A lineView may contain multiple logical lines (when merged by + // collapsed spans). The widgets for all of them need to be drawn. + function insertLineWidgets(lineView, dims) { + insertLineWidgetsFor(lineView.line, lineView, dims, true); + if (lineView.rest) for (var i = 0; i < lineView.rest.length; i++) + insertLineWidgetsFor(lineView.rest[i], lineView, dims, false); + } + + function insertLineWidgetsFor(line, lineView, dims, allowAbove) { + if (!line.widgets) return; + var wrap = ensureLineWrapped(lineView); + for (var i = 0, ws = line.widgets; i < ws.length; ++i) { var widget = ws[i], node = elt("div", [widget.node], "CodeMirror-linewidget"); if (!widget.handleMouseEvents) node.ignoreEvents = true; - positionLineWidget(widget, node, wrap, dims); - if (widget.above) - wrap.insertBefore(node, cm.options.lineNumbers && line.height != 0 ? gutterWrap : lineElement); + positionLineWidget(widget, node, lineView, dims); + if (allowAbove && widget.above) + wrap.insertBefore(node, lineView.gutter || lineView.text); else wrap.appendChild(node); signalLater(widget, "redraw"); } - return wrap; } - function positionLineWidget(widget, node, wrap, dims) { + function positionLineWidget(widget, node, lineView, dims) { if (widget.noHScroll) { - (wrap.alignable || (wrap.alignable = [])).push(node); + (lineView.alignable || (lineView.alignable = [])).push(node); var width = dims.wrapperWidth; node.style.left = dims.fixedPos + "px"; if (!widget.coverGutter) { @@ -775,57 +918,367 @@ window.CodeMirror = (function() { } } + // POSITION OBJECT + + // A Pos instance represents a position within the text. + var Pos = CodeMirror.Pos = function(line, ch) { + if (!(this instanceof Pos)) return new Pos(line, ch); + this.line = line; this.ch = ch; + }; + + // Compare two positions, return 0 if they are the same, a negative + // number when a is less, and a positive number otherwise. + var cmp = CodeMirror.cmpPos = function(a, b) { return a.line - b.line || a.ch - b.ch; }; + + function copyPos(x) {return Pos(x.line, x.ch);} + function maxPos(a, b) { return cmp(a, b) < 0 ? b : a; } + function minPos(a, b) { return cmp(a, b) < 0 ? a : b; } + // SELECTION / CURSOR - function updateSelection(cm) { - var display = cm.display; - var collapsed = posEq(cm.doc.sel.from, cm.doc.sel.to); - if (collapsed || cm.options.showCursorWhenSelecting) - updateSelectionCursor(cm); - else - display.cursor.style.display = display.otherCursor.style.display = "none"; - if (!collapsed) - updateSelectionRange(cm); - else - display.selectionDiv.style.display = "none"; + // Selection objects are immutable. A new one is created every time + // the selection changes. A selection is one or more non-overlapping + // (and non-touching) ranges, sorted, and an integer that indicates + // which one is the primary selection (the one that's scrolled into + // view, that getCursor returns, etc). + function Selection(ranges, primIndex) { + this.ranges = ranges; + this.primIndex = primIndex; + } - // Move the hidden textarea near the cursor to prevent scrolling artifacts - if (cm.options.moveInputWithCursor) { - var headPos = cursorCoords(cm, cm.doc.sel.head, "div"); - var wrapOff = getRect(display.wrapper), lineOff = getRect(display.lineDiv); - display.inputDiv.style.top = Math.max(0, Math.min(display.wrapper.clientHeight - 10, - headPos.top + lineOff.top - wrapOff.top)) + "px"; - display.inputDiv.style.left = Math.max(0, Math.min(display.wrapper.clientWidth - 10, - headPos.left + lineOff.left - wrapOff.left)) + "px"; + Selection.prototype = { + primary: function() { return this.ranges[this.primIndex]; }, + equals: function(other) { + if (other == this) return true; + if (other.primIndex != this.primIndex || other.ranges.length != this.ranges.length) return false; + for (var i = 0; i < this.ranges.length; i++) { + var here = this.ranges[i], there = other.ranges[i]; + if (cmp(here.anchor, there.anchor) != 0 || cmp(here.head, there.head) != 0) return false; + } + return true; + }, + deepCopy: function() { + for (var out = [], i = 0; i < this.ranges.length; i++) + out[i] = new Range(copyPos(this.ranges[i].anchor), copyPos(this.ranges[i].head)); + return new Selection(out, this.primIndex); + }, + somethingSelected: function() { + for (var i = 0; i < this.ranges.length; i++) + if (!this.ranges[i].empty()) return true; + return false; + }, + contains: function(pos, end) { + if (!end) end = pos; + for (var i = 0; i < this.ranges.length; i++) { + var range = this.ranges[i]; + if (cmp(end, range.from()) >= 0 && cmp(pos, range.to()) <= 0) + return i; + } + return -1; + } + }; + + function Range(anchor, head) { + this.anchor = anchor; this.head = head; + } + + Range.prototype = { + from: function() { return minPos(this.anchor, this.head); }, + to: function() { return maxPos(this.anchor, this.head); }, + empty: function() { + return this.head.line == this.anchor.line && this.head.ch == this.anchor.ch; + } + }; + + // Take an unsorted, potentially overlapping set of ranges, and + // build a selection out of it. 'Consumes' ranges array (modifying + // it). + function normalizeSelection(ranges, primIndex) { + var prim = ranges[primIndex]; + ranges.sort(function(a, b) { return cmp(a.from(), b.from()); }); + primIndex = indexOf(ranges, prim); + for (var i = 1; i < ranges.length; i++) { + var cur = ranges[i], prev = ranges[i - 1]; + if (cmp(prev.to(), cur.from()) >= 0) { + var from = minPos(prev.from(), cur.from()), to = maxPos(prev.to(), cur.to()); + var inv = prev.empty() ? cur.from() == cur.head : prev.from() == prev.head; + if (i <= primIndex) --primIndex; + ranges.splice(--i, 2, new Range(inv ? to : from, inv ? from : to)); + } + } + return new Selection(ranges, primIndex); + } + + function simpleSelection(anchor, head) { + return new Selection([new Range(anchor, head || anchor)], 0); + } + + // Most of the external API clips given positions to make sure they + // actually exist within the document. + function clipLine(doc, n) {return Math.max(doc.first, Math.min(n, doc.first + doc.size - 1));} + function clipPos(doc, pos) { + if (pos.line < doc.first) return Pos(doc.first, 0); + var last = doc.first + doc.size - 1; + if (pos.line > last) return Pos(last, getLine(doc, last).text.length); + return clipToLen(pos, getLine(doc, pos.line).text.length); + } + function clipToLen(pos, linelen) { + var ch = pos.ch; + if (ch == null || ch > linelen) return Pos(pos.line, linelen); + else if (ch < 0) return Pos(pos.line, 0); + else return pos; + } + function isLine(doc, l) {return l >= doc.first && l < doc.first + doc.size;} + function clipPosArray(doc, array) { + for (var out = [], i = 0; i < array.length; i++) out[i] = clipPos(doc, array[i]); + return out; + } + + // SELECTION UPDATES + + // The 'scroll' parameter given to many of these indicated whether + // the new cursor position should be scrolled into view after + // modifying the selection. + + // If shift is held or the extend flag is set, extends a range to + // include a given position (and optionally a second position). + // Otherwise, simply returns the range between the given positions. + // Used for cursor motion and such. + function extendRange(doc, range, head, other) { + if (doc.cm && doc.cm.display.shift || doc.extend) { + var anchor = range.anchor; + if (other) { + var posBefore = cmp(head, anchor) < 0; + if (posBefore != (cmp(other, anchor) < 0)) { + anchor = head; + head = other; + } else if (posBefore != (cmp(head, other) < 0)) { + head = other; + } + } + return new Range(anchor, head); + } else { + return new Range(other || head, head); } } - // No selection, plain cursor - function updateSelectionCursor(cm) { - var display = cm.display, pos = cursorCoords(cm, cm.doc.sel.head, "div"); - display.cursor.style.left = pos.left + "px"; - display.cursor.style.top = pos.top + "px"; - display.cursor.style.height = Math.max(0, pos.bottom - pos.top) * cm.options.cursorHeight + "px"; - display.cursor.style.display = ""; - - if (pos.other) { - display.otherCursor.style.display = ""; - display.otherCursor.style.left = pos.other.left + "px"; - display.otherCursor.style.top = pos.other.top + "px"; - display.otherCursor.style.height = (pos.other.bottom - pos.other.top) * .85 + "px"; - } else { display.otherCursor.style.display = "none"; } + // Extend the primary selection range, discard the rest. + function extendSelection(doc, head, other, options) { + setSelection(doc, new Selection([extendRange(doc, doc.sel.primary(), head, other)], 0), options); } - // Highlight selection - function updateSelectionRange(cm) { - var display = cm.display, doc = cm.doc, sel = cm.doc.sel; + // Extend all selections (pos is an array of selections with length + // equal the number of selections) + function extendSelections(doc, heads, options) { + for (var out = [], i = 0; i < doc.sel.ranges.length; i++) + out[i] = extendRange(doc, doc.sel.ranges[i], heads[i], null); + var newSel = normalizeSelection(out, doc.sel.primIndex); + setSelection(doc, newSel, options); + } + + // Updates a single range in the selection. + function replaceOneSelection(doc, i, range, options) { + var ranges = doc.sel.ranges.slice(0); + ranges[i] = range; + setSelection(doc, normalizeSelection(ranges, doc.sel.primIndex), options); + } + + // Reset the selection to a single range. + function setSimpleSelection(doc, anchor, head, options) { + setSelection(doc, simpleSelection(anchor, head), options); + } + + // Give beforeSelectionChange handlers a change to influence a + // selection update. + function filterSelectionChange(doc, sel) { + var obj = { + ranges: sel.ranges, + update: function(ranges) { + this.ranges = []; + for (var i = 0; i < ranges.length; i++) + this.ranges[i] = new Range(clipPos(doc, ranges[i].anchor), + clipPos(doc, ranges[i].head)); + } + }; + signal(doc, "beforeSelectionChange", doc, obj); + if (doc.cm) signal(doc.cm, "beforeSelectionChange", doc.cm, obj); + if (obj.ranges != sel.ranges) return normalizeSelection(obj.ranges, obj.ranges.length - 1); + else return sel; + } + + function setSelectionReplaceHistory(doc, sel, options) { + var done = doc.history.done, last = lst(done); + if (last && last.ranges) { + done[done.length - 1] = sel; + setSelectionNoUndo(doc, sel, options); + } else { + setSelection(doc, sel, options); + } + } + + // Set a new selection. + function setSelection(doc, sel, options) { + setSelectionNoUndo(doc, sel, options); + addSelectionToHistory(doc, doc.sel, doc.cm ? doc.cm.curOp.id : NaN, options); + } + + function setSelectionNoUndo(doc, sel, options) { + if (hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange")) + sel = filterSelectionChange(doc, sel); + + var bias = cmp(sel.primary().head, doc.sel.primary().head) < 0 ? -1 : 1; + setSelectionInner(doc, skipAtomicInSelection(doc, sel, bias, true)); + + if (!(options && options.scroll === false) && doc.cm) + ensureCursorVisible(doc.cm); + } + + function setSelectionInner(doc, sel) { + if (sel.equals(doc.sel)) return; + + doc.sel = sel; + + if (doc.cm) + doc.cm.curOp.updateInput = doc.cm.curOp.selectionChanged = + doc.cm.curOp.cursorActivity = true; + signalLater(doc, "cursorActivity", doc); + } + + // Verify that the selection does not partially select any atomic + // marked ranges. + function reCheckSelection(doc) { + setSelectionInner(doc, skipAtomicInSelection(doc, doc.sel, null, false), sel_dontScroll); + } + + // Return a selection that does not partially select any atomic + // ranges. + function skipAtomicInSelection(doc, sel, bias, mayClear) { + var out; + for (var i = 0; i < sel.ranges.length; i++) { + var range = sel.ranges[i]; + var newAnchor = skipAtomic(doc, range.anchor, bias, mayClear); + var newHead = skipAtomic(doc, range.head, bias, mayClear); + if (out || newAnchor != range.anchor || newHead != range.head) { + if (!out) out = sel.ranges.slice(0, i); + out[i] = new Range(newAnchor, newHead); + } + } + return out ? normalizeSelection(out, sel.primIndex) : sel; + } + + // Ensure a given position is not inside an atomic range. + function skipAtomic(doc, pos, bias, mayClear) { + var flipped = false, curPos = pos; + var dir = bias || 1; + doc.cantEdit = false; + search: for (;;) { + var line = getLine(doc, curPos.line); + if (line.markedSpans) { + for (var i = 0; i < line.markedSpans.length; ++i) { + var sp = line.markedSpans[i], m = sp.marker; + if ((sp.from == null || (m.inclusiveLeft ? sp.from <= curPos.ch : sp.from < curPos.ch)) && + (sp.to == null || (m.inclusiveRight ? sp.to >= curPos.ch : sp.to > curPos.ch))) { + if (mayClear) { + signal(m, "beforeCursorEnter"); + if (m.explicitlyCleared) { + if (!line.markedSpans) break; + else {--i; continue;} + } + } + if (!m.atomic) continue; + var newPos = m.find(dir < 0 ? -1 : 1); + if (cmp(newPos, curPos) == 0) { + newPos.ch += dir; + if (newPos.ch < 0) { + if (newPos.line > doc.first) newPos = clipPos(doc, Pos(newPos.line - 1)); + else newPos = null; + } else if (newPos.ch > line.text.length) { + if (newPos.line < doc.first + doc.size - 1) newPos = Pos(newPos.line + 1, 0); + else newPos = null; + } + if (!newPos) { + if (flipped) { + // Driven in a corner -- no valid cursor position found at all + // -- try again *with* clearing, if we didn't already + if (!mayClear) return skipAtomic(doc, pos, bias, true); + // Otherwise, turn off editing until further notice, and return the start of the doc + doc.cantEdit = true; + return Pos(doc.first, 0); + } + flipped = true; newPos = pos; dir = -dir; + } + } + curPos = newPos; + continue search; + } + } + } + return curPos; + } + } + + // SELECTION DRAWING + + // Redraw the selection and/or cursor + function updateSelection(cm) { + var display = cm.display, doc = cm.doc; + var curFragment = document.createDocumentFragment(); + var selFragment = document.createDocumentFragment(); + + for (var i = 0; i < doc.sel.ranges.length; i++) { + var range = doc.sel.ranges[i]; + var collapsed = range.empty(); + if (collapsed || cm.options.showCursorWhenSelecting) + updateSelectionCursor(cm, range, curFragment); + if (!collapsed) + updateSelectionRange(cm, range, selFragment); + } + + // Move the hidden textarea near the cursor to prevent scrolling artifacts + if (cm.options.moveInputWithCursor) { + var headPos = cursorCoords(cm, doc.sel.primary().head, "div"); + var wrapOff = display.wrapper.getBoundingClientRect(), lineOff = display.lineDiv.getBoundingClientRect(); + var top = Math.max(0, Math.min(display.wrapper.clientHeight - 10, + headPos.top + lineOff.top - wrapOff.top)); + var left = Math.max(0, Math.min(display.wrapper.clientWidth - 10, + headPos.left + lineOff.left - wrapOff.left)); + display.inputDiv.style.top = top + "px"; + display.inputDiv.style.left = left + "px"; + } + + removeChildrenAndAdd(display.cursorDiv, curFragment); + removeChildrenAndAdd(display.selectionDiv, selFragment); + } + + // Draws a cursor for the given range + function updateSelectionCursor(cm, range, output) { + var pos = cursorCoords(cm, range.head, "div"); + + var cursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor")); + cursor.style.left = pos.left + "px"; + cursor.style.top = pos.top + "px"; + cursor.style.height = Math.max(0, pos.bottom - pos.top) * cm.options.cursorHeight + "px"; + + if (pos.other) { + // Secondary cursor, shown when on a 'jump' in bi-directional text + var otherCursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor CodeMirror-secondarycursor")); + otherCursor.style.display = ""; + otherCursor.style.left = pos.other.left + "px"; + otherCursor.style.top = pos.other.top + "px"; + otherCursor.style.height = (pos.other.bottom - pos.other.top) * .85 + "px"; + } + } + + // Draws the given range as a highlighted selection + function updateSelectionRange(cm, range, output) { + var display = cm.display, doc = cm.doc; var fragment = document.createDocumentFragment(); - var clientWidth = display.lineSpace.offsetWidth, pl = paddingLeft(cm.display); + var padding = paddingH(cm.display), leftSide = padding.left, rightSide = display.lineSpace.offsetWidth - padding.right; function add(left, top, width, bottom) { if (top < 0) top = 0; fragment.appendChild(elt("div", null, "CodeMirror-selected", "position: absolute; left: " + left + - "px; top: " + top + "px; width: " + (width == null ? clientWidth - left : width) + + "px; top: " + top + "px; width: " + (width == null ? rightSide - left : width) + "px; height: " + (bottom - top) + "px")); } @@ -848,44 +1301,44 @@ window.CodeMirror = (function() { left = leftPos.left; right = rightPos.right; } - if (fromArg == null && from == 0) left = pl; + if (fromArg == null && from == 0) left = leftSide; if (rightPos.top - leftPos.top > 3) { // Different lines, draw top part add(left, leftPos.top, null, leftPos.bottom); - left = pl; + left = leftSide; if (leftPos.bottom < rightPos.top) add(left, leftPos.bottom, null, rightPos.top); } - if (toArg == null && to == lineLen) right = clientWidth; + if (toArg == null && to == lineLen) right = rightSide; if (!start || leftPos.top < start.top || leftPos.top == start.top && leftPos.left < start.left) start = leftPos; if (!end || rightPos.bottom > end.bottom || rightPos.bottom == end.bottom && rightPos.right > end.right) end = rightPos; - if (left < pl + 1) left = pl; + if (left < leftSide + 1) left = leftSide; add(left, rightPos.top, right - left, rightPos.bottom); }); return {start: start, end: end}; } - if (sel.from.line == sel.to.line) { - drawForLine(sel.from.line, sel.from.ch, sel.to.ch); + var sFrom = range.from(), sTo = range.to(); + if (sFrom.line == sTo.line) { + drawForLine(sFrom.line, sFrom.ch, sTo.ch); } else { - var fromLine = getLine(doc, sel.from.line), toLine = getLine(doc, sel.to.line); - var singleVLine = visualLine(doc, fromLine) == visualLine(doc, toLine); - var leftEnd = drawForLine(sel.from.line, sel.from.ch, singleVLine ? fromLine.text.length : null).end; - var rightStart = drawForLine(sel.to.line, singleVLine ? 0 : null, sel.to.ch).start; + var fromLine = getLine(doc, sFrom.line), toLine = getLine(doc, sTo.line); + var singleVLine = visualLine(fromLine) == visualLine(toLine); + var leftEnd = drawForLine(sFrom.line, sFrom.ch, singleVLine ? fromLine.text.length + 1 : null).end; + var rightStart = drawForLine(sTo.line, singleVLine ? 0 : null, sTo.ch).start; if (singleVLine) { if (leftEnd.top < rightStart.top - 2) { add(leftEnd.right, leftEnd.top, null, leftEnd.bottom); - add(pl, rightStart.top, rightStart.left, rightStart.bottom); + add(leftSide, rightStart.top, rightStart.left, rightStart.bottom); } else { add(leftEnd.right, leftEnd.top, rightStart.left - leftEnd.right, leftEnd.bottom); } } if (leftEnd.bottom < rightStart.top) - add(pl, leftEnd.bottom, null, rightStart.top); + add(leftSide, leftEnd.bottom, null, rightStart.top); } - removeChildrenAndAdd(display.selectionDiv, fragment); - display.selectionDiv.style.display = ""; + output.appendChild(fragment); } // Cursor-blinking @@ -894,37 +1347,35 @@ window.CodeMirror = (function() { var display = cm.display; clearInterval(display.blinker); var on = true; - display.cursor.style.visibility = display.otherCursor.style.visibility = ""; + display.cursorDiv.style.visibility = ""; if (cm.options.cursorBlinkRate > 0) display.blinker = setInterval(function() { - display.cursor.style.visibility = display.otherCursor.style.visibility = (on = !on) ? "" : "hidden"; + display.cursorDiv.style.visibility = (on = !on) ? "" : "hidden"; }, cm.options.cursorBlinkRate); } // HIGHLIGHT WORKER function startWorker(cm, time) { - if (cm.doc.mode.startState && cm.doc.frontier < cm.display.showingTo) + if (cm.doc.mode.startState && cm.doc.frontier < cm.display.viewTo) cm.state.highlight.set(time, bind(highlightWorker, cm)); } function highlightWorker(cm) { var doc = cm.doc; if (doc.frontier < doc.first) doc.frontier = doc.first; - if (doc.frontier >= cm.display.showingTo) return; + if (doc.frontier >= cm.display.viewTo) return; var end = +new Date + cm.options.workTime; var state = copyState(doc.mode, getStateBefore(cm, doc.frontier)); - var changed = [], prevChange; - doc.iter(doc.frontier, Math.min(doc.first + doc.size, cm.display.showingTo + 500), function(line) { - if (doc.frontier >= cm.display.showingFrom) { // Visible + + runInOp(cm, function() { + doc.iter(doc.frontier, Math.min(doc.first + doc.size, cm.display.viewTo + 500), function(line) { + if (doc.frontier >= cm.display.viewFrom) { // Visible var oldStyles = line.styles; line.styles = highlightLine(cm, line, state, true); var ischange = !oldStyles || oldStyles.length != line.styles.length; for (var i = 0; !ischange && i < oldStyles.length; ++i) ischange = oldStyles[i] != line.styles[i]; - if (ischange) { - if (prevChange && prevChange.end == doc.frontier) prevChange.end++; - else changed.push(prevChange = {start: doc.frontier, end: doc.frontier + 1}); - } + if (ischange) regLineChange(cm, doc.frontier, "text"); line.stateAfter = copyState(doc.mode, state); } else { processLine(cm, line.text, state); @@ -936,11 +1387,7 @@ window.CodeMirror = (function() { return true; } }); - if (changed.length) - operation(cm, function() { - for (var i = 0; i < changed.length; ++i) - regChange(this, changed[i].start, changed[i].end); - })(); + }); } // Finds the line to start with when starting a parse. Tries to @@ -972,7 +1419,7 @@ window.CodeMirror = (function() { else state = copyState(doc.mode, state); doc.iter(pos, n, function(line) { processLine(cm, line.text, state); - var save = pos == n - 1 || pos % 5 == 0 || pos >= display.showingFrom && pos < display.showingTo; + var save = pos == n - 1 || pos % 5 == 0 || pos >= display.viewFrom && pos < display.viewTo; line.stateAfter = save ? copyState(doc.mode, state) : null; ++pos; }); @@ -984,183 +1431,222 @@ window.CodeMirror = (function() { function paddingTop(display) {return display.lineSpace.offsetTop;} function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight;} - function paddingLeft(display) { - var e = removeChildrenAndAdd(display.measure, elt("pre", null, null, "text-align: left")).appendChild(elt("span", "x")); - return e.offsetLeft; + function paddingH(display) { + if (display.cachedPaddingH) return display.cachedPaddingH; + var e = removeChildrenAndAdd(display.measure, elt("pre", "x")); + var style = window.getComputedStyle ? window.getComputedStyle(e) : e.currentStyle; + return display.cachedPaddingH = {left: parseInt(style.paddingLeft), + right: parseInt(style.paddingRight)}; } - function measureChar(cm, line, ch, data, bias) { - var dir = -1; - data = data || measureLine(cm, line); - if (data.crude) { - var left = data.left + ch * data.width; - return {left: left, right: left + data.width, top: data.top, bottom: data.bottom}; - } - - for (var pos = ch;; pos += dir) { - var r = data[pos]; - if (r) break; - if (dir < 0 && pos == 0) dir = 1; - } - bias = pos > ch ? "left" : pos < ch ? "right" : bias; - if (bias == "left" && r.leftSide) r = r.leftSide; - else if (bias == "right" && r.rightSide) r = r.rightSide; - return {left: pos < ch ? r.right : r.left, - right: pos > ch ? r.left : r.right, - top: r.top, - bottom: r.bottom}; - } - - function findCachedMeasurement(cm, line) { - var cache = cm.display.measureLineCache; - for (var i = 0; i < cache.length; ++i) { - var memo = cache[i]; - if (memo.text == line.text && memo.markedSpans == line.markedSpans && - cm.display.scroller.clientWidth == memo.width && - memo.classes == line.textClass + "|" + line.wrapClass) - return memo; - } - } - - function clearCachedMeasurement(cm, line) { - var exists = findCachedMeasurement(cm, line); - if (exists) exists.text = exists.measure = exists.markedSpans = null; - } - - function measureLine(cm, line) { - // First look in the cache - var cached = findCachedMeasurement(cm, line); - if (cached) return cached.measure; - - // Failing that, recompute and store result in cache - var measure = measureLineInner(cm, line); - var cache = cm.display.measureLineCache; - var memo = {text: line.text, width: cm.display.scroller.clientWidth, - markedSpans: line.markedSpans, measure: measure, - classes: line.textClass + "|" + line.wrapClass}; - if (cache.length == 16) cache[++cm.display.measureLineCachePos % 16] = memo; - else cache.push(memo); - return measure; - } - - function measureLineInner(cm, line) { - if (!cm.options.lineWrapping && line.text.length >= cm.options.crudeMeasuringFrom) - return crudelyMeasureLine(cm, line); - - var display = cm.display, measure = emptyArray(line.text.length); - var pre = buildLineContent(cm, line, measure, true).pre; - - // IE does not cache element positions of inline elements between - // calls to getBoundingClientRect. This makes the loop below, - // which gathers the positions of all the characters on the line, - // do an amount of layout work quadratic to the number of - // characters. When line wrapping is off, we try to improve things - // by first subdividing the line into a bunch of inline blocks, so - // that IE can reuse most of the layout information from caches - // for those blocks. This does interfere with line wrapping, so it - // doesn't work when wrapping is on, but in that case the - // situation is slightly better, since IE does cache line-wrapping - // information and only recomputes per-line. - if (old_ie && !ie_lt8 && !cm.options.lineWrapping && pre.childNodes.length > 100) { - var fragment = document.createDocumentFragment(); - var chunk = 10, n = pre.childNodes.length; - for (var i = 0, chunks = Math.ceil(n / chunk); i < chunks; ++i) { - var wrap = elt("div", null, null, "display: inline-block"); - for (var j = 0; j < chunk && n; ++j) { - wrap.appendChild(pre.firstChild); - --n; - } - fragment.appendChild(wrap); - } - pre.appendChild(fragment); - } - - removeChildrenAndAdd(display.measure, pre); - - var outer = getRect(display.lineDiv); - var vranges = [], data = emptyArray(line.text.length), maxBot = pre.offsetHeight; - // Work around an IE7/8 bug where it will sometimes have randomly - // replaced our pre with a clone at this point. - if (ie_lt9 && display.measure.first != pre) - removeChildrenAndAdd(display.measure, pre); - - function measureRect(rect) { - var top = rect.top - outer.top, bot = rect.bottom - outer.top; - if (bot > maxBot) bot = maxBot; - if (top < 0) top = 0; - for (var i = vranges.length - 2; i >= 0; i -= 2) { - var rtop = vranges[i], rbot = vranges[i+1]; - if (rtop > bot || rbot < top) continue; - if (rtop <= top && rbot >= bot || - top <= rtop && bot >= rbot || - Math.min(bot, rbot) - Math.max(top, rtop) >= (bot - top) >> 1) { - vranges[i] = Math.min(top, rtop); - vranges[i+1] = Math.max(bot, rbot); - break; + // Ensure the lineView.wrapping.heights array is populated. This is + // an array of bottom offsets for the lines that make up a drawn + // line. When lineWrapping is on, there might be more than one + // height. + function ensureLineHeights(cm, lineView, rect) { + var wrapping = cm.options.lineWrapping; + var curWidth = wrapping && cm.display.scroller.clientWidth; + if (!lineView.measure.heights || wrapping && lineView.measure.width != curWidth) { + var heights = lineView.measure.heights = []; + if (wrapping) { + lineView.measure.width = curWidth; + var rects = lineView.text.firstChild.getClientRects(); + for (var i = 0; i < rects.length - 1; i++) { + var cur = rects[i], next = rects[i + 1]; + if (Math.abs(cur.bottom - next.bottom) > 2) + heights.push((cur.bottom + next.top) / 2 - rect.top); } } - if (i < 0) { i = vranges.length; vranges.push(top, bot); } - return {left: rect.left - outer.left, - right: rect.right - outer.left, - top: i, bottom: null}; - } - function finishRect(rect) { - rect.bottom = vranges[rect.top+1]; - rect.top = vranges[rect.top]; + heights.push(rect.bottom - rect.top); } + } - for (var i = 0, cur; i < measure.length; ++i) if (cur = measure[i]) { - var node = cur, rect = null; - // A widget might wrap, needs special care - if (/\bCodeMirror-widget\b/.test(cur.className) && cur.getClientRects) { - if (cur.firstChild.nodeType == 1) node = cur.firstChild; - var rects = node.getClientRects(); - if (rects.length > 1) { - rect = data[i] = measureRect(rects[0]); - rect.rightSide = measureRect(rects[rects.length - 1]); - } + // Find a line map (mapping character offsets to text nodes) and a + // measurement cache for the given line number. (A line view might + // contain multiple lines when collapsed ranges are present.) + function mapFromLineView(lineView, line, lineN) { + if (lineView.line == line) + return {map: lineView.measure.map, cache: lineView.measure.cache}; + for (var i = 0; i < lineView.rest.length; i++) + if (lineView.rest[i] == line) + return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i]}; + for (var i = 0; i < lineView.rest.length; i++) + if (lineNo(lineView.rest[i]) > lineN) + return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i], before: true}; + } + + // Render a line into the hidden node display.externalMeasured. Used + // when measurement is needed for a line that's not in the viewport. + function updateExternalMeasurement(cm, line) { + line = visualLine(line); + var lineN = lineNo(line); + var view = cm.display.externalMeasured = new LineView(cm.doc, line, lineN); + view.lineN = lineN; + var built = view.built = buildLineContent(cm, view); + view.text = built.pre; + removeChildrenAndAdd(cm.display.lineMeasure, built.pre); + return view; + } + + // Get a {top, bottom, left, right} box (in line-local coordinates) + // for a given character. + function measureChar(cm, line, ch, bias) { + return measureCharPrepared(cm, prepareMeasureForLine(cm, line), ch, bias); + } + + // Find a line view that corresponds to the given line number. + function findViewForLine(cm, lineN) { + if (lineN >= cm.display.viewFrom && lineN < cm.display.viewTo) + return cm.display.view[findViewIndex(cm, lineN)]; + var ext = cm.display.externalMeasured; + if (ext && lineN >= ext.lineN && lineN < ext.lineN + ext.size) + return ext; + } + + // Measurement can be split in two steps, the set-up work that + // applies to the whole line, and the measurement of the actual + // character. Functions like coordsChar, that need to do a lot of + // measurements in a row, can thus ensure that the set-up work is + // only done once. + function prepareMeasureForLine(cm, line) { + var lineN = lineNo(line); + var view = findViewForLine(cm, lineN); + if (view && !view.text) + view = null; + else if (view && view.changes) + updateLineForChanges(cm, view, lineN, getDimensions(cm)); + if (!view) + view = updateExternalMeasurement(cm, line); + + var info = mapFromLineView(view, line, lineN); + return { + line: line, view: view, rect: null, + map: info.map, cache: info.cache, before: info.before, + hasHeights: false + }; + } + + // Given a prepared measurement object, measures the position of an + // actual character (or fetches it from the cache). + function measureCharPrepared(cm, prepared, ch, bias) { + if (prepared.before) ch = -1; + var key = ch + (bias || ""), found; + if (prepared.cache.hasOwnProperty(key)) { + found = prepared.cache[key]; + } else { + if (!prepared.rect) + prepared.rect = prepared.view.text.getBoundingClientRect(); + if (!prepared.hasHeights) { + ensureLineHeights(cm, prepared.view, prepared.rect); + prepared.hasHeights = true; } - if (!rect) rect = data[i] = measureRect(getRect(node)); - if (cur.measureRight) rect.right = getRect(cur.measureRight).left - outer.left; - if (cur.leftSide) rect.leftSide = measureRect(getRect(cur.leftSide)); + found = measureCharInner(cm, prepared, ch, bias); + if (!found.bogus) prepared.cache[key] = found; } - removeChildren(cm.display.measure); - for (var i = 0, cur; i < data.length; ++i) if (cur = data[i]) { - finishRect(cur); - if (cur.leftSide) finishRect(cur.leftSide); - if (cur.rightSide) finishRect(cur.rightSide); - } - return data; + return {left: found.left, right: found.right, top: found.top, bottom: found.bottom}; } - function crudelyMeasureLine(cm, line) { - var copy = new Line(line.text.slice(0, 100), null); - if (line.textClass) copy.textClass = line.textClass; - var measure = measureLineInner(cm, copy); - var left = measureChar(cm, copy, 0, measure, "left"); - var right = measureChar(cm, copy, 99, measure, "right"); - return {crude: true, top: left.top, left: left.left, bottom: left.bottom, width: (right.right - left.left) / 100}; + var nullRect = {left: 0, right: 0, top: 0, bottom: 0}; + + function measureCharInner(cm, prepared, ch, bias) { + var map = prepared.map; + + var node, start, end, collapse; + // First, search the line map for the text node corresponding to, + // or closest to, the target character. + for (var i = 0; i < map.length; i += 3) { + var mStart = map[i], mEnd = map[i + 1]; + if (ch < mStart) { + start = 0; end = 1; + collapse = "left"; + } else if (ch < mEnd) { + start = ch - mStart; + end = start + 1; + } else if (i == map.length - 3 || ch == mEnd && map[i + 3] > ch) { + end = mEnd - mStart; + start = end - 1; + if (ch >= mEnd) collapse = "right"; + } + if (start != null) { + node = map[i + 2]; + if (mStart == mEnd && bias == (node.insertLeft ? "left" : "right")) + collapse = bias; + if (bias == "left" && start == 0) + while (i && map[i - 2] == map[i - 3] && map[i - 1].insertLeft) { + node = map[(i -= 3) + 2]; + collapse = "left"; + } + if (bias == "right" && start == mEnd - mStart) + while (i < map.length - 3 && map[i + 3] == map[i + 4] && !map[i + 5].insertLeft) { + node = map[(i += 3) + 2]; + collapse = "right"; + } + break; + } + } + + var rect; + if (node.nodeType == 3) { // If it is a text node, use a range to retrieve the coordinates. + while (start && isExtendingChar(prepared.line.text.charAt(mStart + start))) --start; + while (mStart + end < mEnd && isExtendingChar(prepared.line.text.charAt(mStart + end))) ++end; + if (ie_upto8 && start == 0 && end == mEnd - mStart) { + rect = node.parentNode.getBoundingClientRect(); + } else if (ie && cm.options.lineWrapping) { + var rects = range(node, start, end).getClientRects(); + if (rects.length) + rect = rects[bias == "right" ? rects.length - 1 : 0]; + else + rect = nullRect; + } else { + rect = range(node, start, end).getBoundingClientRect(); + } + } else { // If it is a widget, simply get the box for the whole widget. + if (start > 0) collapse = bias = "right"; + var rects; + if (cm.options.lineWrapping && (rects = node.getClientRects()).length > 1) + rect = rects[bias == "right" ? rects.length - 1 : 0]; + else + rect = node.getBoundingClientRect(); + } + if (ie_upto8 && !start && (!rect || !rect.left && !rect.right)) { + var rSpan = node.parentNode.getClientRects()[0]; + if (rSpan) + rect = {left: rSpan.left, right: rSpan.left + charWidth(cm.display), top: rSpan.top, bottom: rSpan.bottom}; + else + rect = nullRect; + } + + var top, bot = (rect.bottom + rect.top) / 2 - prepared.rect.top; + var heights = prepared.view.measure.heights; + for (var i = 0; i < heights.length - 1; i++) + if (bot < heights[i]) break; + top = i ? heights[i - 1] : 0; bot = heights[i]; + var result = {left: (collapse == "right" ? rect.right : rect.left) - prepared.rect.left, + right: (collapse == "left" ? rect.left : rect.right) - prepared.rect.left, + top: top, bottom: bot}; + if (!rect.left && !rect.right) result.bogus = true; + return result; } - function measureLineWidth(cm, line) { - var hasBadSpan = false; - if (line.markedSpans) for (var i = 0; i < line.markedSpans; ++i) { - var sp = line.markedSpans[i]; - if (sp.collapsed && (sp.to == null || sp.to == line.text.length)) hasBadSpan = true; + function clearLineMeasurementCacheFor(lineView) { + if (lineView.measure) { + lineView.measure.cache = {}; + lineView.measure.heights = null; + if (lineView.rest) for (var i = 0; i < lineView.rest.length; i++) + lineView.measure.caches[i] = {}; } - var cached = !hasBadSpan && findCachedMeasurement(cm, line); - if (cached || line.text.length >= cm.options.crudeMeasuringFrom) - return measureChar(cm, line, line.text.length, cached && cached.measure, "right").right; + } - var pre = buildLineContent(cm, line, null, true).pre; - var end = pre.appendChild(zeroWidthElement(cm.display.measure)); - removeChildrenAndAdd(cm.display.measure, pre); - return getRect(end).right - getRect(cm.display.lineDiv).left; + function clearLineMeasurementCache(cm) { + cm.display.externalMeasure = null; + removeChildren(cm.display.lineMeasure); + for (var i = 0; i < cm.display.view.length; i++) + clearLineMeasurementCacheFor(cm.display.view[i]); } function clearCaches(cm) { - cm.display.measureLineCache.length = cm.display.measureLineCachePos = 0; - cm.display.cachedCharWidth = cm.display.cachedTextHeight = null; + clearLineMeasurementCache(cm); + cm.display.cachedCharWidth = cm.display.cachedTextHeight = cm.display.cachedPaddingH = null; if (!cm.options.lineWrapping) cm.display.maxLineChanged = true; cm.display.lineNumChars = null; } @@ -1168,7 +1654,9 @@ window.CodeMirror = (function() { function pageScrollX() { return window.pageXOffset || (document.documentElement || document.body).scrollLeft; } function pageScrollY() { return window.pageYOffset || (document.documentElement || document.body).scrollTop; } - // Context is one of "line", "div" (display.lineDiv), "local"/null (editor), or "page" + // Converts a {top, bottom, left, right} box from line-local + // coordinates into another coordinate system. Context may be one of + // "line", "div" (display.lineDiv), "local"/null (editor), or "page". function intoCoordSystem(cm, lineObj, rect, context) { if (lineObj.widgets) for (var i = 0; i < lineObj.widgets.length; ++i) if (lineObj.widgets[i].above) { var size = widgetHeight(lineObj.widgets[i]); @@ -1176,11 +1664,11 @@ window.CodeMirror = (function() { } if (context == "line") return rect; if (!context) context = "local"; - var yOff = heightAtLine(cm, lineObj); + var yOff = heightAtLine(lineObj); if (context == "local") yOff += paddingTop(cm.display); else yOff -= cm.display.viewOffset; if (context == "page" || context == "window") { - var lOff = getRect(cm.display.lineSpace); + var lOff = cm.display.lineSpace.getBoundingClientRect(); yOff += lOff.top + (context == "window" ? 0 : pageScrollY()); var xOff = lOff.left + (context == "window" ? 0 : pageScrollX()); rect.left += xOff; rect.right += xOff; @@ -1189,8 +1677,8 @@ window.CodeMirror = (function() { return rect; } - // Context may be "window", "page", "div", or "local"/null - // Result is in "div" coords + // Coverts a box from "div" coords to another coordinate system. + // Context may be "window", "page", "div", or "local"/null. function fromCoordSystem(cm, coords, context) { if (context == "div") return coords; var left = coords.left, top = coords.top; @@ -1199,25 +1687,28 @@ window.CodeMirror = (function() { left -= pageScrollX(); top -= pageScrollY(); } else if (context == "local" || !context) { - var localBox = getRect(cm.display.sizer); + var localBox = cm.display.sizer.getBoundingClientRect(); left += localBox.left; top += localBox.top; } - var lineSpaceBox = getRect(cm.display.lineSpace); + var lineSpaceBox = cm.display.lineSpace.getBoundingClientRect(); return {left: left - lineSpaceBox.left, top: top - lineSpaceBox.top}; } function charCoords(cm, pos, context, lineObj, bias) { if (!lineObj) lineObj = getLine(cm.doc, pos.line); - return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch, null, bias), context); + return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch, bias), context); } - function cursorCoords(cm, pos, context, lineObj, measurement) { + // Returns a box for a given cursor position, which may have an + // 'other' property containing the position of the secondary cursor + // on a bidi boundary. + function cursorCoords(cm, pos, context, lineObj, preparedMeasure) { lineObj = lineObj || getLine(cm.doc, pos.line); - if (!measurement) measurement = measureLine(cm, lineObj); + if (!preparedMeasure) preparedMeasure = prepareMeasureForLine(cm, lineObj); function get(ch, right) { - var m = measureChar(cm, lineObj, ch, measurement, right ? "right" : "left"); + var m = measureCharPrepared(cm, preparedMeasure, ch, right ? "right" : "left"); if (right) m.left = m.right; else m.right = m.left; return intoCoordSystem(cm, lineObj, m, context); } @@ -1243,43 +1734,59 @@ window.CodeMirror = (function() { return val; } + // Used to cheaply estimate the coordinates for a position. Used for + // intermediate scroll updates. + function estimateCoords(cm, pos) { + var left = 0, pos = clipPos(cm.doc, pos); + if (!cm.options.lineWrapping) left = charWidth(cm.display) * pos.ch; + var lineObj = getLine(cm.doc, pos.line); + var top = heightAtLine(lineObj) + paddingTop(cm.display); + return {left: left, right: left, top: top, bottom: top + lineObj.height}; + } + + // Positions returned by coordsChar contain some extra information. + // xRel is the relative x position of the input coordinates compared + // to the found position (so xRel > 0 means the coordinates are to + // the right of the character position, for example). When outside + // is true, that means the coordinates lie outside the line's + // vertical range. function PosWithInfo(line, ch, outside, xRel) { - var pos = new Pos(line, ch); + var pos = Pos(line, ch); pos.xRel = xRel; if (outside) pos.outside = true; return pos; } - // Coords must be lineSpace-local + // Compute the character position closest to the given coordinates. + // Input must be lineSpace-local ("div" coordinate system). function coordsChar(cm, x, y) { var doc = cm.doc; y += cm.display.viewOffset; if (y < 0) return PosWithInfo(doc.first, 0, true, -1); - var lineNo = lineAtHeight(doc, y), last = doc.first + doc.size - 1; - if (lineNo > last) + var lineN = lineAtHeight(doc, y), last = doc.first + doc.size - 1; + if (lineN > last) return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, true, 1); if (x < 0) x = 0; + var lineObj = getLine(doc, lineN); for (;;) { - var lineObj = getLine(doc, lineNo); - var found = coordsCharInner(cm, lineObj, lineNo, x, y); + var found = coordsCharInner(cm, lineObj, lineN, x, y); var merged = collapsedSpanAtEnd(lineObj); - var mergedPos = merged && merged.find(); + var mergedPos = merged && merged.find(0, true); if (merged && (found.ch > mergedPos.from.ch || found.ch == mergedPos.from.ch && found.xRel > 0)) - lineNo = mergedPos.to.line; + lineN = lineNo(lineObj = mergedPos.to.line); else return found; } } function coordsCharInner(cm, lineObj, lineNo, x, y) { - var innerOff = y - heightAtLine(cm, lineObj); + var innerOff = y - heightAtLine(lineObj); var wrongLine = false, adjust = 2 * cm.display.wrapper.clientWidth; - var measurement = measureLine(cm, lineObj); + var preparedMeasure = prepareMeasureForLine(cm, lineObj); function getX(ch) { - var sp = cursorCoords(cm, Pos(lineNo, ch), "line", - lineObj, measurement); + var sp = cursorCoords(cm, Pos(lineNo, ch), "line", lineObj, preparedMeasure); wrongLine = true; if (innerOff > sp.bottom) return sp.left - adjust; else if (innerOff < sp.top) return sp.left + adjust; @@ -1299,7 +1806,7 @@ window.CodeMirror = (function() { var xDiff = x - (ch == from ? fromX : toX); while (isExtendingChar(lineObj.text.charAt(ch))) ++ch; var pos = PosWithInfo(lineNo, ch, ch == from ? fromOutside : toOutside, - xDiff < 0 ? -1 : xDiff ? 1 : 0); + xDiff < -1 ? -1 : xDiff > 1 ? 1 : 0); return pos; } var step = Math.ceil(dist / 2), middle = from + step; @@ -1314,6 +1821,7 @@ window.CodeMirror = (function() { } var measureText; + // Compute the default text height. function textHeight(display) { if (display.cachedTextHeight != null) return display.cachedTextHeight; if (measureText == null) { @@ -1333,84 +1841,88 @@ window.CodeMirror = (function() { return height || 1; } + // Compute the default character width. function charWidth(display) { if (display.cachedCharWidth != null) return display.cachedCharWidth; - var anchor = elt("span", "x"); + var anchor = elt("span", "xxxxxxxxxx"); var pre = elt("pre", [anchor]); removeChildrenAndAdd(display.measure, pre); - var width = anchor.offsetWidth; + var rect = anchor.getBoundingClientRect(), width = (rect.right - rect.left) / 10; if (width > 2) display.cachedCharWidth = width; return width || 10; } // OPERATIONS - // Operations are used to wrap changes in such a way that each - // change won't have to update the cursor and display (which would - // be awkward, slow, and error-prone), but instead updates are - // batched and then all combined and executed at once. + // Operations are used to wrap a series of changes to the editor + // state in such a way that each change won't have to update the + // cursor and display (which would be awkward, slow, and + // error-prone). Instead, display updates are batched and then all + // combined and executed at once. var nextOpId = 0; + // Start a new operation. function startOperation(cm) { cm.curOp = { - // An array of ranges of lines that have to be updated. See - // updateDisplay. - changes: [], - forceUpdate: false, - updateInput: null, - userSelChange: null, - textChanged: null, - selectionChanged: false, - cursorActivity: false, - updateMaxLine: false, - updateScrollPos: false, - id: ++nextOpId + viewChanged: false, // Flag that indicates that lines might need to be redrawn + startHeight: cm.doc.height, // Used to detect need to update scrollbar + forceUpdate: false, // Used to force a redraw + updateInput: null, // Whether to reset the input textarea + typing: false, // Whether this reset should be careful to leave existing text (for compositing) + changeObjs: null, // Accumulated changes, for firing change events + cursorActivity: false, // Whether to fire a cursorActivity event + selectionChanged: false, // Whether the selection needs to be redrawn + updateMaxLine: false, // Set when the widest line needs to be determined anew + scrollLeft: null, scrollTop: null, // Intermediate scroll position, not pushed to DOM yet + scrollToPos: null, // Used to scroll to a specific position + id: ++nextOpId // Unique ID }; if (!delayedCallbackDepth++) delayedCallbacks = []; } + // Finish an operation, updating the display and signalling delayed events function endOperation(cm) { var op = cm.curOp, doc = cm.doc, display = cm.display; cm.curOp = null; - if (op.updateMaxLine) computeMaxLength(cm); - if (display.maxLineChanged && !cm.options.lineWrapping && display.maxLine) { - var width = measureLineWidth(cm, display.maxLine); - display.sizer.style.minWidth = Math.max(0, width + 3 + scrollerCutOff) + "px"; - display.maxLineChanged = false; - var maxScrollLeft = Math.max(0, display.sizer.offsetLeft + display.sizer.offsetWidth - display.scroller.clientWidth); - if (maxScrollLeft < doc.scrollLeft && !op.updateScrollPos) - setScrollLeft(cm, Math.min(display.scroller.scrollLeft, maxScrollLeft), true); - } - var newScrollPos, updated; - if (op.updateScrollPos) { - newScrollPos = op.updateScrollPos; - } else if (op.selectionChanged && display.scroller.clientHeight) { // don't rescroll if not visible - var coords = cursorCoords(cm, doc.sel.head); - newScrollPos = calculateScrollPos(cm, coords.left, coords.top, coords.left, coords.bottom); - } - if (op.changes.length || op.forceUpdate || newScrollPos && newScrollPos.scrollTop != null) { - updated = updateDisplay(cm, op.changes, newScrollPos && newScrollPos.scrollTop, op.forceUpdate); + if (op.updateMaxLine) findMaxLine(cm); + + // If it looks like an update might be needed, call updateDisplay + if (op.viewChanged || op.forceUpdate || op.scrollTop != null || + op.scrollToPos && (op.scrollToPos.from.line < display.viewFrom || + op.scrollToPos.to.line >= display.viewTo) || + display.maxLineChanged && cm.options.lineWrapping) { + var updated = updateDisplay(cm, {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate); if (cm.display.scroller.offsetHeight) cm.doc.scrollTop = cm.display.scroller.scrollTop; } + // If no update was run, but the selection changed, redraw that. if (!updated && op.selectionChanged) updateSelection(cm); - if (op.updateScrollPos) { - var top = Math.max(0, Math.min(display.scroller.scrollHeight - display.scroller.clientHeight, newScrollPos.scrollTop)); - var left = Math.max(0, Math.min(display.scroller.scrollWidth - display.scroller.clientWidth, newScrollPos.scrollLeft)); + if (!updated && op.startHeight != cm.doc.height) updateScrollbars(cm); + + // Propagate the scroll position to the actual DOM scroller + if (op.scrollTop != null && display.scroller.scrollTop != op.scrollTop) { + var top = Math.max(0, Math.min(display.scroller.scrollHeight - display.scroller.clientHeight, op.scrollTop)); display.scroller.scrollTop = display.scrollbarV.scrollTop = doc.scrollTop = top; + } + if (op.scrollLeft != null && display.scroller.scrollLeft != op.scrollLeft) { + var left = Math.max(0, Math.min(display.scroller.scrollWidth - display.scroller.clientWidth, op.scrollLeft)); display.scroller.scrollLeft = display.scrollbarH.scrollLeft = doc.scrollLeft = left; alignHorizontally(cm); - if (op.scrollToPos) - scrollPosIntoView(cm, clipPos(cm.doc, op.scrollToPos.from), - clipPos(cm.doc, op.scrollToPos.to), op.scrollToPos.margin); - } else if (newScrollPos) { - scrollCursorIntoView(cm); } + // If we need to scroll a specific position into view, do so. + if (op.scrollToPos) { + var coords = scrollPosIntoView(cm, clipPos(cm.doc, op.scrollToPos.from), + clipPos(cm.doc, op.scrollToPos.to), op.scrollToPos.margin); + if (op.scrollToPos.isCursor && cm.state.focused) maybeScrollWindow(cm, coords); + } + if (op.selectionChanged) restartBlink(cm); if (cm.state.focused && op.updateInput) - resetInput(cm, op.userSelChange); + resetInput(cm, op.typing); + // Fire events for markers that are hidden/unidden by editing or + // undoing var hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers; if (hidden) for (var i = 0; i < hidden.length; ++i) if (!hidden[i].lines.length) signal(hidden[i], "hide"); @@ -1422,47 +1934,242 @@ window.CodeMirror = (function() { delayed = delayedCallbacks; delayedCallbacks = null; } - if (op.textChanged) - signal(cm, "change", cm, op.textChanged); + // Fire change events, and delayed event handlers + if (op.changeObjs) { + for (var i = 0; i < op.changeObjs.length; i++) + signal(cm, "change", cm, op.changeObjs[i]); + signal(cm, "changes", cm, op.changeObjs); + } if (op.cursorActivity) signal(cm, "cursorActivity", cm); if (delayed) for (var i = 0; i < delayed.length; ++i) delayed[i](); } - // Wraps a function in an operation. Returns the wrapped function. - function operation(cm1, f) { - return function() { - var cm = cm1 || this, withOp = !cm.curOp; - if (withOp) startOperation(cm); - try { var result = f.apply(cm, arguments); } - finally { if (withOp) endOperation(cm); } - return result; - }; - } - function docOperation(f) { - return function() { - var withOp = this.cm && !this.cm.curOp, result; - if (withOp) startOperation(this.cm); - try { result = f.apply(this, arguments); } - finally { if (withOp) endOperation(this.cm); } - return result; - }; - } + // Run the given function in an operation function runInOp(cm, f) { - var withOp = !cm.curOp, result; - if (withOp) startOperation(cm); - try { result = f(); } - finally { if (withOp) endOperation(cm); } - return result; + if (cm.curOp) return f(); + startOperation(cm); + try { return f(); } + finally { endOperation(cm); } + } + // Wraps a function in an operation. Returns the wrapped function. + function operation(cm, f) { + return function() { + if (cm.curOp) return f.apply(cm, arguments); + startOperation(cm); + try { return f.apply(cm, arguments); } + finally { endOperation(cm); } + }; + } + // Used to add methods to editor and doc instances, wrapping them in + // operations. + function methodOp(f) { + return function() { + if (this.curOp) return f.apply(this, arguments); + startOperation(this); + try { return f.apply(this, arguments); } + finally { endOperation(this); } + }; + } + function docMethodOp(f) { + return function() { + var cm = this.cm; + if (!cm || cm.curOp) return f.apply(this, arguments); + startOperation(cm); + try { return f.apply(this, arguments); } + finally { endOperation(cm); } + }; } + // VIEW TRACKING + + // These objects are used to represent the visible (currently drawn) + // part of the document. A LineView may correspond to multiple + // logical lines, if those are connected by collapsed ranges. + function LineView(doc, line, lineN) { + // The starting line + this.line = line; + // Continuing lines, if any + this.rest = visualLineContinued(line); + // Number of logical lines in this visual line + this.size = this.rest ? lineNo(lst(this.rest)) - lineN + 1 : 1; + this.node = this.text = null; + this.hidden = lineIsHidden(doc, line); + } + + // Create a range of LineView objects for the given lines. + function buildViewArray(cm, from, to) { + var array = [], nextPos; + for (var pos = from; pos < to; pos = nextPos) { + var view = new LineView(cm.doc, getLine(cm.doc, pos), pos); + nextPos = pos + view.size; + array.push(view); + } + return array; + } + + // Updates the display.view data structure for a given change to the + // document. From and to are in pre-change coordinates. Lendiff is + // the amount of lines added or subtracted by the change. This is + // used for changes that span multiple lines, or change the way + // lines are divided into visual lines. regLineChange (below) + // registers single-line changes. function regChange(cm, from, to, lendiff) { if (from == null) from = cm.doc.first; if (to == null) to = cm.doc.first + cm.doc.size; - cm.curOp.changes.push({from: from, to: to, diff: lendiff}); + if (!lendiff) lendiff = 0; + + var display = cm.display; + if (lendiff && to < display.viewTo && + (display.updateLineNumbers == null || display.updateLineNumbers > from)) + display.updateLineNumbers = from; + + cm.curOp.viewChanged = true; + + if (from >= display.viewTo) { // Change after + if (sawCollapsedSpans && visualLineNo(cm.doc, from) < display.viewTo) + resetView(cm); + } else if (to <= display.viewFrom) { // Change before + if (sawCollapsedSpans && visualLineEndNo(cm.doc, to + lendiff) > display.viewFrom) { + resetView(cm); + } else { + display.viewFrom += lendiff; + display.viewTo += lendiff; + } + } else if (from <= display.viewFrom && to >= display.viewTo) { // Full overlap + resetView(cm); + } else if (from <= display.viewFrom) { // Top overlap + var cut = viewCuttingPoint(cm, to, to + lendiff, 1); + if (cut) { + display.view = display.view.slice(cut.index); + display.viewFrom = cut.lineN; + display.viewTo += lendiff; + } else { + resetView(cm); + } + } else if (to >= display.viewTo) { // Bottom overlap + var cut = viewCuttingPoint(cm, from, from, -1); + if (cut) { + display.view = display.view.slice(0, cut.index); + display.viewTo = cut.lineN; + } else { + resetView(cm); + } + } else { // Gap in the middle + var cutTop = viewCuttingPoint(cm, from, from, -1); + var cutBot = viewCuttingPoint(cm, to, to + lendiff, 1); + if (cutTop && cutBot) { + display.view = display.view.slice(0, cutTop.index) + .concat(buildViewArray(cm, cutTop.lineN, cutBot.lineN)) + .concat(display.view.slice(cutBot.index)); + display.viewTo += lendiff; + } else { + resetView(cm); + } + } + + var ext = display.externalMeasured; + if (ext) { + if (to < ext.lineN) + ext.lineN += lendiff; + else if (from < ext.lineN + ext.size) + display.externalMeasured = null; + } + } + + // Register a change to a single line. Type must be one of "text", + // "gutter", "class", "widget" + function regLineChange(cm, line, type) { + cm.curOp.viewChanged = true; + var display = cm.display, ext = cm.display.externalMeasured; + if (ext && line >= ext.lineN && line < ext.lineN + ext.size) + display.externalMeasured = null; + + if (line < display.viewFrom || line >= display.viewTo) return; + var lineView = display.view[findViewIndex(cm, line)]; + if (lineView.node == null) return; + var arr = lineView.changes || (lineView.changes = []); + if (indexOf(arr, type) == -1) arr.push(type); + } + + // Clear the view. + function resetView(cm) { + cm.display.viewFrom = cm.display.viewTo = cm.doc.first; + cm.display.view = []; + cm.display.viewOffset = 0; + } + + // Find the view element corresponding to a given line. Return null + // when the line isn't visible. + function findViewIndex(cm, n) { + if (n >= cm.display.viewTo) return null; + n -= cm.display.viewFrom; + if (n < 0) return null; + var view = cm.display.view; + for (var i = 0; i < view.length; i++) { + n -= view[i].size; + if (n < 0) return i; + } + } + + function viewCuttingPoint(cm, oldN, newN, dir) { + var index = findViewIndex(cm, oldN), diff, view = cm.display.view; + if (!sawCollapsedSpans) return {index: index, lineN: newN}; + for (var i = 0, n = cm.display.viewFrom; i < index; i++) + n += view[i].size; + if (n != oldN) { + if (dir > 0) { + if (index == view.length - 1) return null; + diff = (n + view[index].size) - oldN; + index++; + } else { + diff = n - oldN; + } + oldN += diff; newN += diff; + } + while (visualLineNo(cm.doc, newN) != newN) { + if (index == (dir < 0 ? 0 : view.length - 1)) return null; + newN += dir * view[index - (dir < 0 ? 1 : 0)].size; + index += dir; + } + return {index: index, lineN: newN}; + } + + // Force the view to cover a given range, adding empty view element + // or clipping off existing ones as needed. + function adjustView(cm, from, to) { + var display = cm.display, view = display.view; + if (view.length == 0 || from >= display.viewTo || to <= display.viewFrom) { + display.view = buildViewArray(cm, from, to); + display.viewFrom = from; + } else { + if (display.viewFrom > from) + display.view = buildViewArray(cm, from, display.viewFrom).concat(display.view); + else if (display.viewFrom < from) + display.view = display.view.slice(findViewIndex(cm, from)); + display.viewFrom = from; + if (display.viewTo < to) + display.view = display.view.concat(buildViewArray(cm, display.viewTo, to)); + else if (display.viewTo > to) + display.view = display.view.slice(0, findViewIndex(cm, to)); + } + display.viewTo = to; + } + + // Count the number of lines in the view whose DOM representation is + // out of date (or nonexistent). + function countDirtyView(cm) { + var view = cm.display.view, dirty = 0; + for (var i = 0; i < view.length; i++) { + var lineView = view[i]; + if (!lineView.hidden && (!lineView.node || lineView.changes)) ++dirty; + } + return dirty; } // INPUT HANDLING + // Poll for input changes, using the normal rate of polling. This + // runs as long as the editor is focused. function slowPoll(cm) { if (cm.display.pollingFast) return; cm.display.poll.set(cm.options.pollInterval, function() { @@ -1471,6 +2178,9 @@ window.CodeMirror = (function() { }); } + // When an event has just come in that is likely to add or change + // something in the input textarea, we poll faster, to ensure that + // the change appears on the screen quickly. function fastPoll(cm) { var missed = false; cm.display.pollingFast = true; @@ -1482,53 +2192,72 @@ window.CodeMirror = (function() { cm.display.poll.set(20, p); } - // prevInput is a hack to work with IME. If we reset the textarea - // on every change, that breaks IME. So we look for changes - // compared to the previous content instead. (Modern browsers have - // events that indicate IME taking place, but these are not widely - // supported or compatible enough yet to rely on.) + // Read input from the textarea, and update the document to match. + // When something is selected, it is present in the textarea, and + // selected (unless it is huge, in which case a placeholder is + // used). When nothing is selected, the cursor sits after previously + // seen text (can be empty), which is stored in prevInput (we must + // not reset the textarea when typing, because that breaks IME). function readInput(cm) { - var input = cm.display.input, prevInput = cm.display.prevInput, doc = cm.doc, sel = doc.sel; + var input = cm.display.input, prevInput = cm.display.prevInput, doc = cm.doc; + // Since this is called a *lot*, try to bail out as cheaply as + // possible when it is clear that nothing happened. hasSelection + // will be the case when there is a lot of text in the textarea, + // in which case reading its value would be expensive. if (!cm.state.focused || hasSelection(input) || isReadOnly(cm) || cm.options.disableInput) return false; - if (cm.state.pasteIncoming && cm.state.fakedLastChar) { - input.value = input.value.substring(0, input.value.length - 1); - cm.state.fakedLastChar = false; - } var text = input.value; - if (text == prevInput && posEq(sel.from, sel.to)) return false; - if (ie && !ie_lt9 && cm.display.inputHasSelection === text) { - resetInput(cm, true); + // If nothing changed, bail. + if (text == prevInput && !cm.somethingSelected()) return false; + // Work around nonsensical selection resetting in IE9/10 + if (ie && !ie_upto8 && cm.display.inputHasSelection === text) { + resetInput(cm); return false; } var withOp = !cm.curOp; if (withOp) startOperation(cm); - sel.shift = false; + cm.display.shift = false; + + // Find the part of the input that is actually new var same = 0, l = Math.min(prevInput.length, text.length); while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) ++same; - var from = sel.from, to = sel.to; - var inserted = text.slice(same); - if (same < prevInput.length) - from = Pos(from.line, from.ch - (prevInput.length - same)); - else if (cm.state.overwrite && posEq(from, to) && !cm.state.pasteIncoming) - to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + inserted.length)); + var inserted = text.slice(same), textLines = splitLines(inserted); - var updateInput = cm.curOp.updateInput; - var changeEvent = {from: from, to: to, text: splitLines(inserted), - origin: cm.state.pasteIncoming ? "paste" : cm.state.cutIncoming ? "cut" : "+input"}; - makeChange(cm.doc, changeEvent, "end"); - cm.curOp.updateInput = updateInput; - signalLater(cm, "inputRead", cm, changeEvent); - if (inserted && !cm.state.pasteIncoming && cm.options.electricChars && - cm.options.smartIndent && sel.head.ch < 100) { - var electric = cm.getModeAt(sel.head).electricChars; - if (electric) for (var i = 0; i < electric.length; i++) - if (inserted.indexOf(electric.charAt(i)) > -1) { - indentLine(cm, sel.head.line, "smart"); - break; - } + // When pasing N lines into N selections, insert one line per selection + var multiPaste = cm.state.pasteIncoming && textLines.length > 1 && doc.sel.ranges.length == textLines.length; + + // Normal behavior is to insert the new text into every selection + for (var i = doc.sel.ranges.length - 1; i >= 0; i--) { + var range = doc.sel.ranges[i]; + var from = range.from(), to = range.to(); + // Handle deletion + if (same < prevInput.length) + from = Pos(from.line, from.ch - (prevInput.length - same)); + // Handle overwrite + else if (cm.state.overwrite && range.empty() && !cm.state.pasteIncoming) + to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + lst(textLines).length)); + var updateInput = cm.curOp.updateInput; + var changeEvent = {from: from, to: to, text: multiPaste ? [textLines[i]] : textLines, + origin: cm.state.pasteIncoming ? "paste" : cm.state.cutIncoming ? "cut" : "+input"}; + makeChange(cm.doc, changeEvent); + signalLater(cm, "inputRead", cm, changeEvent); + // When an 'electric' character is inserted, immediately trigger a reindent + if (inserted && !cm.state.pasteIncoming && cm.options.electricChars && + cm.options.smartIndent && range.head.ch < 100 && + (!i || doc.sel.ranges[i - 1].head.line != range.head.line)) { + var electric = cm.getModeAt(range.head).electricChars; + if (electric) for (var j = 0; j < electric.length; j++) + if (inserted.indexOf(electric.charAt(j)) > -1) { + indentLine(cm, range.head.line, "smart"); + break; + } + } } + ensureCursorVisible(cm); + cm.curOp.updateInput = updateInput; + cm.curOp.typing = true; + // Don't leave long text in the textarea, since it makes further polling slow if (text.length > 1000 || text.indexOf("\n") > -1) input.value = cm.display.prevInput = ""; else cm.display.prevInput = text; if (withOp) endOperation(cm); @@ -1536,56 +2265,68 @@ window.CodeMirror = (function() { return true; } - function resetInput(cm, user) { + // Reset the input to correspond to the selection (or to be empty, + // when not typing and nothing is selected) + function resetInput(cm, typing) { var minimal, selected, doc = cm.doc; - if (!posEq(doc.sel.from, doc.sel.to)) { + if (cm.somethingSelected()) { cm.display.prevInput = ""; + var range = doc.sel.primary(); minimal = hasCopyEvent && - (doc.sel.to.line - doc.sel.from.line > 100 || (selected = cm.getSelection()).length > 1000); + (range.to().line - range.from().line > 100 || (selected = cm.getSelection()).length > 1000); var content = minimal ? "-" : selected || cm.getSelection(); cm.display.input.value = content; if (cm.state.focused) selectInput(cm.display.input); - if (ie && !ie_lt9) cm.display.inputHasSelection = content; - } else if (user) { + if (ie && !ie_upto8) cm.display.inputHasSelection = content; + } else if (!typing) { cm.display.prevInput = cm.display.input.value = ""; - if (ie && !ie_lt9) cm.display.inputHasSelection = null; + if (ie && !ie_upto8) cm.display.inputHasSelection = null; } cm.display.inaccurateSelection = minimal; } function focusInput(cm) { - if (cm.options.readOnly != "nocursor" && (!mobile || document.activeElement != cm.display.input)) + if (cm.options.readOnly != "nocursor" && (!mobile || activeElt() != cm.display.input)) cm.display.input.focus(); } + function ensureFocus(cm) { + if (!cm.state.focused) { focusInput(cm); onFocus(cm); } + } + function isReadOnly(cm) { return cm.options.readOnly || cm.doc.cantEdit; } // EVENT HANDLERS + // Attach the necessary event handlers when initializing the editor function registerEventHandlers(cm) { var d = cm.display; on(d.scroller, "mousedown", operation(cm, onMouseDown)); - if (old_ie) + // Older IE's will not fire a second mousedown for a double click + if (ie_upto10) on(d.scroller, "dblclick", operation(cm, function(e) { if (signalDOMEvent(cm, e)) return; var pos = posFromMouse(cm, e); if (!pos || clickInGutter(cm, e) || eventInWidget(cm.display, e)) return; e_preventDefault(e); - var word = findWordAt(getLine(cm.doc, pos.line).text, pos); - extendSelection(cm.doc, word.from, word.to); + var word = findWordAt(cm.doc, pos); + extendSelection(cm.doc, word.anchor, word.head); })); else on(d.scroller, "dblclick", function(e) { signalDOMEvent(cm, e) || e_preventDefault(e); }); + // Prevent normal selection in the editor (we handle our own) on(d.lineSpace, "selectstart", function(e) { if (!eventInWidget(d, e)) e_preventDefault(e); }); - // Gecko browsers fire contextmenu *after* opening the menu, at + // Some browsers fire contextmenu *after* opening the menu, at // which point we can't mess with it anymore. Context menu is - // handled in onMouseDown for Gecko. - if (!captureMiddleClick) on(d.scroller, "contextmenu", function(e) {onContextMenu(cm, e);}); + // handled in onMouseDown for these browsers. + if (!captureRightClick) on(d.scroller, "contextmenu", function(e) {onContextMenu(cm, e);}); + // Sync scrolling between fake scrollbars and real scrollable + // area, ensure viewport is updated when scrolling. on(d.scroller, "scroll", function() { if (d.scroller.clientHeight) { setScrollTop(cm, d.scroller.scrollTop); @@ -1600,42 +2341,40 @@ window.CodeMirror = (function() { if (d.scroller.clientHeight) setScrollLeft(cm, d.scrollbarH.scrollLeft); }); + // Listen to wheel events in order to try and update the viewport on time. on(d.scroller, "mousewheel", function(e){onScrollWheel(cm, e);}); on(d.scroller, "DOMMouseScroll", function(e){onScrollWheel(cm, e);}); + // Prevent clicks in the scrollbars from killing focus function reFocus() { if (cm.state.focused) setTimeout(bind(focusInput, cm), 0); } on(d.scrollbarH, "mousedown", reFocus); on(d.scrollbarV, "mousedown", reFocus); // Prevent wrapper from ever scrolling on(d.wrapper, "scroll", function() { d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; }); + // When the window resizes, we need to refresh active editors. var resizeTimer; function onResize() { if (resizeTimer == null) resizeTimer = setTimeout(function() { resizeTimer = null; // Might be a text scaling operation, clear size caches. - d.cachedCharWidth = d.cachedTextHeight = knownScrollbarWidth = null; - clearCaches(cm); - runInOp(cm, bind(regChange, cm)); + d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = knownScrollbarWidth = null; + cm.setSize(); }, 100); } on(window, "resize", onResize); - // Above handler holds on to the editor and its data structures. - // Here we poll to unregister it when the editor is no longer in - // the document, so that it can be garbage-collected. + // The above handler holds on to the editor and its data + // structures. Here we poll to unregister it when the editor is no + // longer in the document, so that it can be garbage-collected. function unregister() { - for (var p = d.wrapper.parentNode; p && p != document.body; p = p.parentNode) {} - if (p) setTimeout(unregister, 5000); + if (contains(document.body, d.wrapper)) setTimeout(unregister, 5000); else off(window, "resize", onResize); } setTimeout(unregister, 5000); - on(d.input, "keyup", operation(cm, function(e) { - if (signalDOMEvent(cm, e) || cm.options.onKeyEvent && cm.options.onKeyEvent(cm, addStop(e))) return; - if (e.keyCode == 16) cm.doc.sel.shift = false; - })); + on(d.input, "keyup", operation(cm, onKeyUp)); on(d.input, "input", function() { - if (ie && !ie_lt9 && cm.display.inputHasSelection) cm.display.inputHasSelection = null; + if (ie && !ie_upto8 && cm.display.inputHasSelection) cm.display.inputHasSelection = null; fastPoll(cm); }); on(d.input, "keydown", operation(cm, onKeyDown)); @@ -1644,8 +2383,7 @@ window.CodeMirror = (function() { on(d.input, "blur", bind(onBlur, cm)); function drag_(e) { - if (signalDOMEvent(cm, e) || cm.options.onDragEvent && cm.options.onDragEvent(cm, addStop(e))) return; - e_stop(e); + if (!signalDOMEvent(cm, e)) e_stop(e); } if (cm.options.dragDrop) { on(d.scroller, "dragstart", function(e){onDragStart(cm, e);}); @@ -1655,20 +2393,11 @@ window.CodeMirror = (function() { } on(d.scroller, "paste", function(e) { if (eventInWidget(d, e)) return; + cm.state.pasteIncoming = true; focusInput(cm); fastPoll(cm); }); on(d.input, "paste", function() { - // Workaround for webkit bug https://bugs.webkit.org/show_bug.cgi?id=90206 - // Add a char to the end of textarea before paste occur so that - // selection doesn't span to the end of textarea. - if (webkit && !cm.state.fakedLastChar && !(new Date - cm.state.lastMiddleDown < 200)) { - var start = d.input.selectionStart, end = d.input.selectionEnd; - d.input.value += "$"; - d.input.selectionStart = start; - d.input.selectionEnd = end; - cm.state.fakedLastChar = true; - } cm.state.pasteIncoming = true; fastPoll(cm); }); @@ -1687,39 +2416,58 @@ window.CodeMirror = (function() { // Needed to handle Tab key in KHTML if (khtml) on(d.sizer, "mouseup", function() { - if (document.activeElement == d.input) d.input.blur(); + if (activeElt() == d.input) d.input.blur(); focusInput(cm); }); } + // MOUSE EVENTS + + // Return true when the given mouse event happened in a widget function eventInWidget(display, e) { for (var n = e_target(e); n != display.wrapper; n = n.parentNode) { if (!n || n.ignoreEvents || n.parentNode == display.sizer && n != display.mover) return true; } } - function posFromMouse(cm, e, liberal) { + // Given a mouse event, find the corresponding position. If liberal + // is false, it checks whether a gutter or scrollbar was clicked, + // and returns null if it was. forRect is used by rectangular + // selections, and tries to estimate a character position even for + // coordinates beyond the right of the text. + function posFromMouse(cm, e, liberal, forRect) { var display = cm.display; if (!liberal) { var target = e_target(e); - if (target == display.scrollbarH || target == display.scrollbarH.firstChild || - target == display.scrollbarV || target == display.scrollbarV.firstChild || + if (target == display.scrollbarH || target == display.scrollbarV || target == display.scrollbarFiller || target == display.gutterFiller) return null; } - var x, y, space = getRect(display.lineSpace); + var x, y, space = display.lineSpace.getBoundingClientRect(); // Fails unpredictably on IE[67] when mouse is dragged around quickly. - try { x = e.clientX; y = e.clientY; } catch (e) { return null; } - return coordsChar(cm, x - space.left, y - space.top); + try { x = e.clientX - space.left; y = e.clientY - space.top; } + catch (e) { return null; } + var coords = coordsChar(cm, x, y), line; + if (forRect && coords.xRel == 1 && (line = getLine(cm.doc, coords.line).text).length == coords.ch) { + var colDiff = countColumn(line, line.length, cm.options.tabSize) - line.length; + coords = Pos(coords.line, Math.round((x - paddingH(cm.display).left) / charWidth(cm.display)) - colDiff); + } + return coords; } - var lastClick, lastDoubleClick; + // A mouse down can be a single click, double click, triple click, + // start of selection drag, start of text drag, new cursor + // (ctrl-click), rectangle drag (alt-drag), or xwin + // middle-click-paste. Or it might be a click on something we should + // not interfere with, such as a scrollbar or widget. function onMouseDown(e) { if (signalDOMEvent(this, e)) return; - var cm = this, display = cm.display, doc = cm.doc, sel = doc.sel; - sel.shift = e.shiftKey; + var cm = this, display = cm.display; + display.shift = e.shiftKey; if (eventInWidget(display, e)) { if (!webkit) { + // Briefly turn off draggability, to allow widgets to do + // normal dragging things. display.scroller.draggable = false; setTimeout(function(){display.scroller.draggable = true;}, 100); } @@ -1727,92 +2475,168 @@ window.CodeMirror = (function() { } if (clickInGutter(cm, e)) return; var start = posFromMouse(cm, e); + window.focus(); switch (e_button(e)) { - case 3: - if (captureMiddleClick) onContextMenu.call(cm, cm, e); - return; + case 1: + if (start) + leftButtonDown(cm, e, start); + else if (e_target(e) == display.scroller) + e_preventDefault(e); + break; case 2: if (webkit) cm.state.lastMiddleDown = +new Date; if (start) extendSelection(cm.doc, start); setTimeout(bind(focusInput, cm), 20); e_preventDefault(e); - return; + break; + case 3: + if (captureRightClick) onContextMenu(cm, e); + break; } - // For button 1, if it was clicked inside the editor - // (posFromMouse returning non-null), we have to adjust the - // selection. - if (!start) {if (e_target(e) == display.scroller) e_preventDefault(e); return;} + } - if (!cm.state.focused) onFocus(cm); + var lastClick, lastDoubleClick; + function leftButtonDown(cm, e, start) { + setTimeout(bind(ensureFocus, cm), 0); - var now = +new Date, type = "single"; - if (lastDoubleClick && lastDoubleClick.time > now - 400 && posEq(lastDoubleClick.pos, start)) { + var now = +new Date, type; + if (lastDoubleClick && lastDoubleClick.time > now - 400 && cmp(lastDoubleClick.pos, start) == 0) { type = "triple"; - e_preventDefault(e); - setTimeout(bind(focusInput, cm), 20); - selectLine(cm, start.line); - } else if (lastClick && lastClick.time > now - 400 && posEq(lastClick.pos, start)) { + } else if (lastClick && lastClick.time > now - 400 && cmp(lastClick.pos, start) == 0) { type = "double"; lastDoubleClick = {time: now, pos: start}; - e_preventDefault(e); - var word = findWordAt(getLine(doc, start.line).text, start); - extendSelection(cm.doc, word.from, word.to); - } else { lastClick = {time: now, pos: start}; } - - var last = start; - if (cm.options.dragDrop && dragAndDrop && !isReadOnly(cm) && !posEq(sel.from, sel.to) && - !posLess(start, sel.from) && !posLess(sel.to, start) && type == "single") { - var dragEnd = operation(cm, function(e2) { - if (webkit) display.scroller.draggable = false; - cm.state.draggingText = false; - off(document, "mouseup", dragEnd); - off(display.scroller, "drop", dragEnd); - if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) { - e_preventDefault(e2); - extendSelection(cm.doc, start); - focusInput(cm); - // Work around unexplainable focus problem in IE9 (#2127) - if (old_ie && !ie_lt9) - setTimeout(function() {document.body.focus(); focusInput(cm);}, 20); - } - }); - // Let the drag handler handle this. - if (webkit) display.scroller.draggable = true; - cm.state.draggingText = dragEnd; - // IE's approach to draggable - if (display.scroller.dragDrop) display.scroller.dragDrop(); - on(document, "mouseup", dragEnd); - on(display.scroller, "drop", dragEnd); - return; + } else { + type = "single"; + lastClick = {time: now, pos: start}; } + + var sel = cm.doc.sel, addNew = mac ? e.metaKey : e.ctrlKey; + if (cm.options.dragDrop && dragAndDrop && !addNew && !isReadOnly(cm) && + type == "single" && sel.contains(start) > -1 && sel.somethingSelected()) + leftButtonStartDrag(cm, e, start); + else + leftButtonSelect(cm, e, start, type, addNew); + } + + // Start a text drag. When it ends, see if any dragging actually + // happen, and treat as a click if it didn't. + function leftButtonStartDrag(cm, e, start) { + var display = cm.display; + var dragEnd = operation(cm, function(e2) { + if (webkit) display.scroller.draggable = false; + cm.state.draggingText = false; + off(document, "mouseup", dragEnd); + off(display.scroller, "drop", dragEnd); + if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) { + e_preventDefault(e2); + extendSelection(cm.doc, start); + focusInput(cm); + // Work around unexplainable focus problem in IE9 (#2127) + if (ie_upto10 && !ie_upto8) + setTimeout(function() {document.body.focus(); focusInput(cm);}, 20); + } + }); + // Let the drag handler handle this. + if (webkit) display.scroller.draggable = true; + cm.state.draggingText = dragEnd; + // IE's approach to draggable + if (display.scroller.dragDrop) display.scroller.dragDrop(); + on(document, "mouseup", dragEnd); + on(display.scroller, "drop", dragEnd); + } + + // Normal selection, as opposed to text dragging. + function leftButtonSelect(cm, e, start, type, addNew) { + var display = cm.display, doc = cm.doc; e_preventDefault(e); - if (type == "single") extendSelection(cm.doc, clipPos(doc, start)); - var startstart = sel.from, startend = sel.to, lastPos = start; + var ourRange, ourIndex, startSel = doc.sel; + if (addNew) { + ourIndex = doc.sel.contains(start); + if (ourIndex > -1) + ourRange = doc.sel.ranges[ourIndex]; + else + ourRange = new Range(start, start); + } else { + ourRange = doc.sel.primary(); + } - function doSelect(cur) { - if (posEq(lastPos, cur)) return; - lastPos = cur; + if (e.altKey) { + type = "rect"; + if (!addNew) ourRange = new Range(start, start); + start = posFromMouse(cm, e, true, true); + ourIndex = -1; + } else if (type == "double") { + var word = findWordAt(doc, start); + if (cm.display.shift || doc.extend) + ourRange = extendRange(doc, ourRange, word.anchor, word.head); + else + ourRange = word; + } else if (type == "triple") { + var line = new Range(Pos(start.line, 0), clipPos(doc, Pos(start.line + 1, 0))); + if (cm.display.shift || doc.extend) + ourRange = extendRange(doc, ourRange, line.anchor, line.head); + else + ourRange = line; + } else { + ourRange = extendRange(doc, ourRange, start); + } - if (type == "single") { - extendSelection(cm.doc, clipPos(doc, start), cur); - return; - } + if (!addNew) { + ourIndex = 0; + setSelection(doc, new Selection([ourRange], 0), sel_mouse); + } else if (ourIndex > -1) { + replaceOneSelection(doc, ourIndex, ourRange, sel_mouse); + } else { + ourIndex = doc.sel.ranges.length; + setSelection(doc, normalizeSelection(doc.sel.ranges.concat([ourRange]), ourIndex), + {scroll: false, origin: "*mouse"}); + } - startstart = clipPos(doc, startstart); - startend = clipPos(doc, startend); - if (type == "double") { - var word = findWordAt(getLine(doc, cur.line).text, cur); - if (posLess(cur, startstart)) extendSelection(cm.doc, word.from, startend); - else extendSelection(cm.doc, startstart, word.to); - } else if (type == "triple") { - if (posLess(cur, startstart)) extendSelection(cm.doc, startend, clipPos(doc, Pos(cur.line, 0))); - else extendSelection(cm.doc, startstart, clipPos(doc, Pos(cur.line + 1, 0))); + var lastPos = start; + function extendTo(pos) { + if (cmp(lastPos, pos) == 0) return; + lastPos = pos; + + if (type == "rect") { + var ranges = [], tabSize = cm.options.tabSize; + var startCol = countColumn(getLine(doc, start.line).text, start.ch, tabSize); + var posCol = countColumn(getLine(doc, pos.line).text, pos.ch, tabSize); + var left = Math.min(startCol, posCol), right = Math.max(startCol, posCol); + for (var line = Math.min(start.line, pos.line), end = Math.min(cm.lastLine(), Math.max(start.line, pos.line)); + line <= end; line++) { + var text = getLine(doc, line).text, leftPos = findColumn(text, left, tabSize); + if (left == right) + ranges.push(new Range(Pos(line, leftPos), Pos(line, leftPos))); + else if (text.length > leftPos) + ranges.push(new Range(Pos(line, leftPos), Pos(line, findColumn(text, right, tabSize)))); + } + if (!ranges.length) ranges.push(new Range(start, start)); + setSelection(doc, normalizeSelection(startSel.ranges.slice(0, ourIndex).concat(ranges), ourIndex), sel_mouse); + } else { + var oldRange = ourRange; + var anchor = oldRange.anchor, head = pos; + if (type != "single") { + if (type == "double") + var range = findWordAt(doc, pos); + else + var range = new Range(Pos(pos.line, 0), clipPos(doc, Pos(pos.line + 1, 0))); + if (cmp(range.anchor, anchor) > 0) { + head = range.head; + anchor = minPos(oldRange.from(), range.anchor); + } else { + head = range.anchor; + anchor = maxPos(oldRange.to(), range.head); + } + } + var ranges = startSel.ranges.slice(0); + ranges[ourIndex] = new Range(clipPos(doc, anchor), head); + setSelection(doc, normalizeSelection(ranges, ourIndex), sel_mouse); } } - var editorSize = getRect(display.wrapper); + var editorSize = display.wrapper.getBoundingClientRect(); // Used to ensure timeout re-tries don't fire when another extend // happened in the meantime (clearTimeout isn't reliable -- at // least on Chrome, the timeouts still happen even when cleared, @@ -1821,12 +2645,11 @@ window.CodeMirror = (function() { function extend(e) { var curCount = ++counter; - var cur = posFromMouse(cm, e, true); + var cur = posFromMouse(cm, e, true, type == "rect"); if (!cur) return; - if (!posEq(cur, last)) { - if (!cm.state.focused) onFocus(cm); - last = cur; - doSelect(cur); + if (cmp(cur, lastPos) != 0) { + ensureFocus(cm); + extendTo(cur); var visible = visibleLines(display, doc); if (cur.line >= visible.to || cur.line < visible.from) setTimeout(operation(cm, function(){if (counter == curCount) extend(e);}), 150); @@ -1846,10 +2669,11 @@ window.CodeMirror = (function() { focusInput(cm); off(document, "mousemove", move); off(document, "mouseup", up); + doc.history.lastSelOrigin = null; } var move = operation(cm, function(e) { - if (!old_ie && !e_button(e)) done(e); + if ((ie && !ie_upto9) ? !e.buttons : !e_button(e)) done(e); else extend(e); }); var up = operation(cm, done); @@ -1857,21 +2681,23 @@ window.CodeMirror = (function() { on(document, "mouseup", up); } + // Determines whether an event happened in the gutter, and fires the + // handlers for the corresponding event. function gutterEvent(cm, e, type, prevent, signalfn) { try { var mX = e.clientX, mY = e.clientY; } catch(e) { return false; } - if (mX >= Math.floor(getRect(cm.display.gutters).right)) return false; + if (mX >= Math.floor(cm.display.gutters.getBoundingClientRect().right)) return false; if (prevent) e_preventDefault(e); var display = cm.display; - var lineBox = getRect(display.lineDiv); + var lineBox = display.lineDiv.getBoundingClientRect(); if (mY > lineBox.bottom || !hasHandler(cm, type)) return e_defaultPrevented(e); mY -= lineBox.top - display.viewOffset; for (var i = 0; i < cm.options.gutters.length; ++i) { var g = display.gutters.childNodes[i]; - if (g && getRect(g).right >= mX) { + if (g && g.getBoundingClientRect().right >= mX) { var line = lineAtHeight(cm.doc, mY); var gutter = cm.options.gutters[i]; signalfn(cm, type, cm, line, gutter, e); @@ -1880,11 +2706,6 @@ window.CodeMirror = (function() { } } - function contextMenuInGutter(cm, e) { - if (!hasHandler(cm, "gutterContextMenu")) return false; - return gutterEvent(cm, e, "gutterContextMenu", false, signal); - } - function clickInGutter(cm, e) { return gutterEvent(cm, e, "gutterClick", true, signalLater); } @@ -1895,12 +2716,14 @@ window.CodeMirror = (function() { function onDrop(e) { var cm = this; - if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e) || (cm.options.onDragEvent && cm.options.onDragEvent(cm, addStop(e)))) + if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) return; e_preventDefault(e); - if (ie) lastDrop = +new Date; + if (ie_upto10) lastDrop = +new Date; var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files; if (!pos || isReadOnly(cm)) return; + // Might be a file drop, in which case we simply extract the text + // and insert it. if (files && files.length && window.FileReader && window.File) { var n = files.length, text = Array(n), read = 0; var loadFile = function(file, i) { @@ -1909,15 +2732,17 @@ window.CodeMirror = (function() { text[i] = reader.result; if (++read == n) { pos = clipPos(cm.doc, pos); - makeChange(cm.doc, {from: pos, to: pos, text: splitLines(text.join("\n")), origin: "paste"}, "around"); + var change = {from: pos, to: pos, text: splitLines(text.join("\n")), origin: "paste"}; + makeChange(cm.doc, change); + setSelectionReplaceHistory(cm.doc, simpleSelection(pos, changeEnd(change))); } }; reader.readAsText(file); }; for (var i = 0; i < n; ++i) loadFile(files[i], i); - } else { + } else { // Normal drop // Don't do a replace if the drop happened inside of the selected text. - if (cm.state.draggingText && !(posLess(pos, cm.doc.sel.from) || posLess(cm.doc.sel.to, pos))) { + if (cm.state.draggingText && cm.doc.sel.contains(pos) > -1) { cm.state.draggingText(e); // Ensure the editor is re-focused setTimeout(bind(focusInput, cm), 20); @@ -1926,10 +2751,11 @@ window.CodeMirror = (function() { try { var text = e.dataTransfer.getData("Text"); if (text) { - var curFrom = cm.doc.sel.from, curTo = cm.doc.sel.to; - setSelection(cm.doc, pos, pos); - if (cm.state.draggingText) replaceRange(cm.doc, "", curFrom, curTo, "paste"); - cm.replaceSelection(text, null, "paste"); + var selected = cm.state.draggingText && cm.listSelections(); + setSelectionNoUndo(cm.doc, simpleSelection(pos, pos)); + if (selected) for (var i = 0; i < selected.length; ++i) + replaceRange(cm.doc, "", selected[i].anchor, selected[i].head, "drag"); + cm.replaceSelection(text, "around", "paste"); focusInput(cm); } } @@ -1938,37 +2764,42 @@ window.CodeMirror = (function() { } function onDragStart(cm, e) { - if (ie && (!cm.state.draggingText || +new Date - lastDrop < 100)) { e_stop(e); return; } + if (ie_upto10 && (!cm.state.draggingText || +new Date - lastDrop < 100)) { e_stop(e); return; } if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) return; - var txt = cm.getSelection(); - e.dataTransfer.setData("Text", txt); + e.dataTransfer.setData("Text", cm.getSelection()); // Use dummy image instead of default browsers image. // Recent Safari (~6.0.2) have a tendency to segfault when this happens, so we don't do it there. if (e.dataTransfer.setDragImage && !safari) { var img = elt("img", null, null, "position: fixed; left: 0; top: 0;"); img.src = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; - if (opera) { + if (presto) { img.width = img.height = 1; cm.display.wrapper.appendChild(img); // Force a relayout, or Opera won't use our image for some obscure reason img._top = img.offsetTop; } e.dataTransfer.setDragImage(img, 0, 0); - if (opera) img.parentNode.removeChild(img); + if (presto) img.parentNode.removeChild(img); } } + // SCROLL EVENTS + + // Sync the scrollable area and scrollbars, ensure the viewport + // covers the visible area. function setScrollTop(cm, val) { if (Math.abs(cm.doc.scrollTop - val) < 2) return; cm.doc.scrollTop = val; - if (!gecko) updateDisplay(cm, [], val); + if (!gecko) updateDisplay(cm, {top: val}); if (cm.display.scroller.scrollTop != val) cm.display.scroller.scrollTop = val; if (cm.display.scrollbarV.scrollTop != val) cm.display.scrollbarV.scrollTop = val; - if (gecko) updateDisplay(cm, []); + if (gecko) updateDisplay(cm); startWorker(cm, 100); } + // Sync scroller and scrollbar, ensure the gutter elements are + // aligned. function setScrollLeft(cm, val, isScroller) { if (isScroller ? val == cm.doc.scrollLeft : Math.abs(cm.doc.scrollLeft - val) < 2) return; val = Math.min(val, cm.display.scroller.scrollWidth - cm.display.scroller.clientWidth); @@ -1994,7 +2825,7 @@ window.CodeMirror = (function() { // know one. These don't have to be accurate -- the result of them // being wrong would just be a slight flicker on the first wheel // scroll (if it is large enough). - if (old_ie) wheelPixelsPerUnit = -.53; + if (ie) wheelPixelsPerUnit = -.53; else if (gecko) wheelPixelsPerUnit = 15; else if (chrome) wheelPixelsPerUnit = -.7; else if (safari) wheelPixelsPerUnit = -1/3; @@ -2015,10 +2846,12 @@ window.CodeMirror = (function() { // This hack (see related code in patchDisplay) makes sure the // element is kept around. if (dy && mac && webkit) { - for (var cur = e.target; cur != scroll; cur = cur.parentNode) { - if (cur.lineObj) { - cm.display.currentWheelTarget = cur; - break; + outer: for (var cur = e.target, view = display.view; cur != scroll; cur = cur.parentNode) { + for (var i = 0; i < view.length; i++) { + if (view[i].node == cur) { + cm.display.currentWheelTarget = cur; + break outer; + } } } } @@ -2029,7 +2862,7 @@ window.CodeMirror = (function() { // estimated pixels/delta value, we just handle horizontal // scrolling entirely here. It'll be slightly off from native, but // better than glitching out. - if (dx && !gecko && !opera && wheelPixelsPerUnit != null) { + if (dx && !gecko && !presto && wheelPixelsPerUnit != null) { if (dy) setScrollTop(cm, Math.max(0, Math.min(scroll.scrollTop + dy * wheelPixelsPerUnit, scroll.scrollHeight - scroll.clientHeight))); setScrollLeft(cm, Math.max(0, Math.min(scroll.scrollLeft + dx * wheelPixelsPerUnit, scroll.scrollWidth - scroll.clientWidth))); @@ -2038,12 +2871,14 @@ window.CodeMirror = (function() { return; } + // 'Project' the visible viewport to cover the area that is being + // scrolled into view (if we know enough to estimate it). if (dy && wheelPixelsPerUnit != null) { var pixels = dy * wheelPixelsPerUnit; var top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight; if (pixels < 0) top = Math.max(0, top + pixels - 50); else bot = Math.min(cm.doc.height, bot + pixels + 50); - updateDisplay(cm, [], {top: top, bottom: bot}); + updateDisplay(cm, {top: top, bottom: bot}); } if (wheelSamples < 20) { @@ -2067,6 +2902,9 @@ window.CodeMirror = (function() { } } + // KEY EVENTS + + // Run a handler that was bound to a key. function doHandleBinding(cm, bound, dropShift) { if (typeof bound == "string") { bound = commands[bound]; @@ -2075,18 +2913,19 @@ window.CodeMirror = (function() { // Ensure previous input has been read, so that the handler sees a // consistent view of the document if (cm.display.pollingFast && readInput(cm)) cm.display.pollingFast = false; - var doc = cm.doc, prevShift = doc.sel.shift, done = false; + var prevShift = cm.display.shift, done = false; try { if (isReadOnly(cm)) cm.state.suppressEdits = true; - if (dropShift) doc.sel.shift = false; + if (dropShift) cm.display.shift = false; done = bound(cm) != Pass; } finally { - doc.sel.shift = prevShift; + cm.display.shift = prevShift; cm.state.suppressEdits = false; } return done; } + // Collect the currently active keymaps. function allKeyMaps(cm) { var maps = cm.state.keyMaps.slice(0); if (cm.options.extraKeys) maps.push(cm.options.extraKeys); @@ -2095,8 +2934,9 @@ window.CodeMirror = (function() { } var maybeTransition; + // Handle a key from the keydown event. function handleKeyBinding(cm, e) { - // Handle auto keymap transitions + // Handle automatic keymap transitions var startMap = getKeyMap(cm.options.keyMap), next = startMap.auto; clearTimeout(maybeTransition); if (next && !isModifierKey(e)) maybeTransition = setTimeout(function() { @@ -2126,12 +2966,12 @@ window.CodeMirror = (function() { if (handled) { e_preventDefault(e); restartBlink(cm); - if (ie_lt9) { e.oldKeyCode = e.keyCode; e.keyCode = 0; } signalLater(cm, "keyHandled", cm, name, e); } return handled; } + // Handle a key from the keypress event function handleCharBinding(cm, e, ch) { var handled = lookupKey("'" + ch + "'", allKeyMaps(cm), function(b) { return doHandleBinding(cm, b, true); }); @@ -2146,34 +2986,40 @@ window.CodeMirror = (function() { var lastStoppedKey = null; function onKeyDown(e) { var cm = this; - if (!cm.state.focused) onFocus(cm); - if (signalDOMEvent(cm, e) || cm.options.onKeyEvent && cm.options.onKeyEvent(cm, addStop(e))) return; - if (old_ie && e.keyCode == 27) e.returnValue = false; - var code = e.keyCode; + ensureFocus(cm); + if (signalDOMEvent(cm, e)) return; // IE does strange things with escape. - cm.doc.sel.shift = code == 16 || e.shiftKey; - // First give onKeyEvent option a chance to handle this. + if (ie_upto10 && e.keyCode == 27) e.returnValue = false; + var code = e.keyCode; + cm.display.shift = code == 16 || e.shiftKey; var handled = handleKeyBinding(cm, e); - if (opera) { + if (presto) { lastStoppedKey = handled ? code : null; // Opera has no cut event... we try to at least catch the key combo if (!handled && code == 88 && !hasCopyEvent && (mac ? e.metaKey : e.ctrlKey)) - cm.replaceSelection(""); + cm.replaceSelection("", null, "cut"); } } + function onKeyUp(e) { + if (signalDOMEvent(this, e)) return; + if (e.keyCode == 16) this.doc.sel.shift = false; + } + function onKeyPress(e) { var cm = this; - if (signalDOMEvent(cm, e) || cm.options.onKeyEvent && cm.options.onKeyEvent(cm, addStop(e))) return; + if (signalDOMEvent(cm, e)) return; var keyCode = e.keyCode, charCode = e.charCode; - if (opera && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;} - if (((opera && (!e.which || e.which < 10)) || khtml) && handleKeyBinding(cm, e)) return; + if (presto && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;} + if (((presto && (!e.which || e.which < 10)) || khtml) && handleKeyBinding(cm, e)) return; var ch = String.fromCharCode(charCode == null ? keyCode : charCode); if (handleCharBinding(cm, e, ch)) return; - if (ie && !ie_lt9) cm.display.inputHasSelection = null; + if (ie && !ie_upto8) cm.display.inputHasSelection = null; fastPoll(cm); } + // FOCUS/BLUR EVENTS + function onFocus(cm) { if (cm.options.readOnly == "nocursor") return; if (!cm.state.focused) { @@ -2182,7 +3028,7 @@ window.CodeMirror = (function() { if (cm.display.wrapper.className.search(/\bCodeMirror-focused\b/) == -1) cm.display.wrapper.className += " CodeMirror-focused"; if (!cm.curOp) { - resetInput(cm, true); + resetInput(cm); if (webkit) setTimeout(bind(resetInput, cm, true), 0); // Issue #1730 } } @@ -2196,37 +3042,46 @@ window.CodeMirror = (function() { cm.display.wrapper.className = cm.display.wrapper.className.replace(" CodeMirror-focused", ""); } clearInterval(cm.display.blinker); - setTimeout(function() {if (!cm.state.focused) cm.doc.sel.shift = false;}, 150); + setTimeout(function() {if (!cm.state.focused) cm.display.shift = false;}, 150); } + // CONTEXT MENU HANDLING + var detectingSelectAll; + // To make the context menu work, we need to briefly unhide the + // textarea (making it as unobtrusive as possible) to let the + // right-click take effect on it. function onContextMenu(cm, e) { if (signalDOMEvent(cm, e, "contextmenu")) return; - var display = cm.display, sel = cm.doc.sel; + var display = cm.display; if (eventInWidget(display, e) || contextMenuInGutter(cm, e)) return; var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop; - if (!pos || opera) return; // Opera is difficult. + if (!pos || presto) return; // Opera is difficult. // Reset the current text selection only if the click is done outside of the selection // and 'resetSelectionOnContextMenu' option is true. var reset = cm.options.resetSelectionOnContextMenu; - if (reset && (posEq(sel.from, sel.to) || posLess(pos, sel.from) || !posLess(pos, sel.to))) - operation(cm, setSelection)(cm.doc, pos, pos); + if (reset && cm.doc.sel.contains(pos) == -1) + operation(cm, setSelection)(cm.doc, simpleSelection(pos), sel_dontScroll); var oldCSS = display.input.style.cssText; display.inputDiv.style.position = "absolute"; display.input.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.clientY - 5) + - "px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: transparent; outline: none;" + - "border-width: 0; outline: none; overflow: hidden; opacity: .05; -ms-opacity: .05; filter: alpha(opacity=5);"; + "px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: " + + (ie ? "rgba(255, 255, 255, .05)" : "transparent") + + "; outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);"; focusInput(cm); - resetInput(cm, true); + resetInput(cm); // Adds "Select all" to context menu in FF - if (posEq(sel.from, sel.to)) display.input.value = display.prevInput = " "; + if (!cm.somethingSelected()) display.input.value = display.prevInput = " "; + // Select-all will be greyed out if there's nothing to select, so + // this adds a zero-width space so that we can later check whether + // it got selected. function prepareSelectAllHack() { if (display.input.selectionStart != null) { - var extval = display.input.value = "\u200b" + (posEq(sel.from, sel.to) ? "" : display.input.value); + var extval = display.input.value = "\u200b" + (cm.somethingSelected() ? display.input.value : ""); display.prevInput = "\u200b"; display.input.selectionStart = 1; display.input.selectionEnd = extval.length; } @@ -2234,12 +3089,12 @@ window.CodeMirror = (function() { function rehide() { display.inputDiv.style.position = "relative"; display.input.style.cssText = oldCSS; - if (ie_lt9) display.scrollbarV.scrollTop = display.scroller.scrollTop = scrollPos; + if (ie_upto8) display.scrollbarV.scrollTop = display.scroller.scrollTop = scrollPos; slowPoll(cm); // Try to detect the user choosing select-all if (display.input.selectionStart != null) { - if (!old_ie || ie_lt9) prepareSelectAllHack(); + if (!ie || ie_upto8) prepareSelectAllHack(); clearTimeout(detectingSelectAll); var i = 0, poll = function(){ if (display.prevInput == "\u200b" && display.input.selectionStart == 0) @@ -2251,8 +3106,8 @@ window.CodeMirror = (function() { } } - if (old_ie && !ie_lt9) prepareSelectAllHack(); - if (captureMiddleClick) { + if (ie && !ie_upto8) prepareSelectAllHack(); + if (captureRightClick) { e_stop(e); var mouseup = function() { off(window, "mouseup", mouseup); @@ -2264,54 +3119,71 @@ window.CodeMirror = (function() { } } + function contextMenuInGutter(cm, e) { + if (!hasHandler(cm, "gutterContextMenu")) return false; + return gutterEvent(cm, e, "gutterContextMenu", false, signal); + } + // UPDATING + // Compute the position of the end of a change (its 'to' property + // refers to the pre-change end). var changeEnd = CodeMirror.changeEnd = function(change) { if (!change.text) return change.to; return Pos(change.from.line + change.text.length - 1, lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0)); }; - // Make sure a position will be valid after the given change. - function clipPostChange(doc, change, pos) { - if (!posLess(change.from, pos)) return clipPos(doc, pos); - var diff = (change.text.length - 1) - (change.to.line - change.from.line); - if (pos.line > change.to.line + diff) { - var preLine = pos.line - diff, lastLine = doc.first + doc.size - 1; - if (preLine > lastLine) return Pos(lastLine, getLine(doc, lastLine).text.length); - return clipToLen(pos, getLine(doc, preLine).text.length); + // Adjust a position to refer to the post-change position of the + // same text, or the end of the change if the change covers it. + function adjustForChange(pos, change) { + if (cmp(pos, change.from) < 0) return pos; + if (cmp(pos, change.to) <= 0) return changeEnd(change); + + var line = pos.line + change.text.length - (change.to.line - change.from.line) - 1, ch = pos.ch; + if (pos.line == change.to.line) ch += changeEnd(change).ch - change.to.ch; + return Pos(line, ch); + } + + function computeSelAfterChange(doc, change) { + var out = []; + for (var i = 0; i < doc.sel.ranges.length; i++) { + var range = doc.sel.ranges[i]; + out.push(new Range(adjustForChange(range.anchor, change), + adjustForChange(range.head, change))); } - if (pos.line == change.to.line + diff) - return clipToLen(pos, lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0) + - getLine(doc, change.to.line).text.length - change.to.ch); - var inside = pos.line - change.from.line; - return clipToLen(pos, change.text[inside].length + (inside ? 0 : change.from.ch)); + return normalizeSelection(out, doc.sel.primIndex); } - // Hint can be null|"end"|"start"|"around"|{anchor,head} - function computeSelAfterChange(doc, change, hint) { - if (hint && typeof hint == "object") // Assumed to be {anchor, head} object - return {anchor: clipPostChange(doc, change, hint.anchor), - head: clipPostChange(doc, change, hint.head)}; - - if (hint == "start") return {anchor: change.from, head: change.from}; - - var end = changeEnd(change); - if (hint == "around") return {anchor: change.from, head: end}; - if (hint == "end") return {anchor: end, head: end}; - - // hint is null, leave the selection alone as much as possible - var adjustPos = function(pos) { - if (posLess(pos, change.from)) return pos; - if (!posLess(change.to, pos)) return end; - - var line = pos.line + change.text.length - (change.to.line - change.from.line) - 1, ch = pos.ch; - if (pos.line == change.to.line) ch += end.ch - change.to.ch; - return Pos(line, ch); - }; - return {anchor: adjustPos(doc.sel.anchor), head: adjustPos(doc.sel.head)}; + function offsetPos(pos, old, nw) { + if (pos.line == old.line) + return Pos(nw.line, pos.ch - old.ch + nw.ch); + else + return Pos(nw.line + (pos.line - old.line), pos.ch); } + // Used by replaceSelections to allow moving the selection to the + // start or around the replaced test. Hint may be "start" or "around". + function computeReplacedSel(doc, changes, hint) { + var out = []; + var oldPrev = Pos(doc.first, 0), newPrev = oldPrev; + for (var i = 0; i < changes.length; i++) { + var change = changes[i]; + var from = offsetPos(change.from, oldPrev, newPrev); + var to = offsetPos(changeEnd(change), oldPrev, newPrev); + oldPrev = change.to; + newPrev = to; + if (hint == "around") { + var range = doc.sel.ranges[i], inv = cmp(range.head, range.anchor) < 0; + out[i] = new Range(inv ? to : from, inv ? from : to); + } else { + out[i] = new Range(from, from); + } + } + return new Selection(out, doc.sel.primIndex); + } + + // Allow "beforeChange" event handlers to influence a change function filterChange(doc, change, update) { var obj = { canceled: false, @@ -2334,11 +3206,11 @@ window.CodeMirror = (function() { return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin}; } - // Replace the range from from to to by the strings in replacement. - // change is a {from, to, text [, origin]} object - function makeChange(doc, change, selUpdate, ignoreReadOnly) { + // Apply a change to a document, and add it to the document's + // history, and propagating it to all linked documents. + function makeChange(doc, change, ignoreReadOnly) { if (doc.cm) { - if (!doc.cm.curOp) return operation(doc.cm, makeChange)(doc, change, selUpdate, ignoreReadOnly); + if (!doc.cm.curOp) return operation(doc.cm, makeChange)(doc, change, ignoreReadOnly); if (doc.cm.state.suppressEdits) return; } @@ -2351,19 +3223,17 @@ window.CodeMirror = (function() { // of read-only spans in its range. var split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to); if (split) { - for (var i = split.length - 1; i >= 1; --i) - makeChangeNoReadonly(doc, {from: split[i].from, to: split[i].to, text: [""]}); - if (split.length) - makeChangeNoReadonly(doc, {from: split[0].from, to: split[0].to, text: change.text}, selUpdate); + for (var i = split.length - 1; i >= 0; --i) + makeChangeInner(doc, {from: split[i].from, to: split[i].to, text: i ? [""] : change.text}); } else { - makeChangeNoReadonly(doc, change, selUpdate); + makeChangeInner(doc, change); } } - function makeChangeNoReadonly(doc, change, selUpdate) { - if (change.text.length == 1 && change.text[0] == "" && posEq(change.from, change.to)) return; - var selAfter = computeSelAfterChange(doc, change, selUpdate); - addToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN); + function makeChangeInner(doc, change) { + if (change.text.length == 1 && change.text[0] == "" && cmp(change.from, change.to) == 0) return; + var selAfter = computeSelAfterChange(doc, change); + addChangeToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN); makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change)); var rebased = []; @@ -2377,17 +3247,41 @@ window.CodeMirror = (function() { }); } - function makeChangeFromHistory(doc, type) { + // Revert a change stored in a document's history. + function makeChangeFromHistory(doc, type, allowSelectionOnly) { if (doc.cm && doc.cm.state.suppressEdits) return; - var hist = doc.history; - var event = (type == "undo" ? hist.done : hist.undone).pop(); - if (!event) return; + var hist = doc.history, event, selAfter = doc.sel; + var source = type == "undo" ? hist.done : hist.undone, dest = type == "undo" ? hist.undone : hist.done; - var anti = {changes: [], anchorBefore: event.anchorAfter, headBefore: event.headAfter, - anchorAfter: event.anchorBefore, headAfter: event.headBefore, - generation: hist.generation}; - (type == "undo" ? hist.undone : hist.done).push(anti); + // Verify that there is a useable event (so that ctrl-z won't + // needlessly clear selection events) + for (var i = 0; i < source.length; i++) { + event = source[i]; + if (allowSelectionOnly ? event.ranges && !event.equals(doc.sel) : !event.ranges) + break; + } + if (i == source.length) return; + hist.lastOrigin = hist.lastSelOrigin = null; + + for (;;) { + event = source.pop(); + if (event.ranges) { + pushSelectionToHistory(event, dest); + if (allowSelectionOnly && !event.equals(doc.sel)) { + setSelection(doc, event, {clearRedo: false}); + return; + } + selAfter = event; + } + else break; + } + + // Build up a reverse change object to add to the opposite history + // stack (redo when undoing, and vice versa). + var antiChanges = []; + pushSelectionToHistory(selAfter, dest); + dest.push({changes: antiChanges, generation: hist.generation}); hist.generation = event.generation || ++hist.maxGeneration; var filter = hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange"); @@ -2396,17 +3290,18 @@ window.CodeMirror = (function() { var change = event.changes[i]; change.origin = type; if (filter && !filterChange(doc, change, false)) { - (type == "undo" ? hist.done : hist.undone).length = 0; + source.length = 0; return; } - anti.changes.push(historyChangeFromChange(doc, change)); + antiChanges.push(historyChangeFromChange(doc, change)); - var after = i ? computeSelAfterChange(doc, change, null) - : {anchor: event.anchorBefore, head: event.headBefore}; + var after = i ? computeSelAfterChange(doc, change, null) : lst(source); makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change)); + if (doc.cm) ensureCursorVisible(doc.cm); var rebased = []; + // Propagate to the linked documents linkedDocs(doc, function(doc, sharedHist) { if (!sharedHist && indexOf(rebased, doc.history) == -1) { rebaseHist(doc.history, change); @@ -2417,14 +3312,19 @@ window.CodeMirror = (function() { } } + // Sub-views need their line numbers shifted when text is added + // above or below them in the parent document. function shiftDoc(doc, distance) { - function shiftPos(pos) {return Pos(pos.line + distance, pos.ch);} doc.first += distance; - if (doc.cm) regChange(doc.cm, doc.first, doc.first, distance); - doc.sel.head = shiftPos(doc.sel.head); doc.sel.anchor = shiftPos(doc.sel.anchor); - doc.sel.from = shiftPos(doc.sel.from); doc.sel.to = shiftPos(doc.sel.to); + doc.sel = new Selection(map(doc.sel.ranges, function(range) { + return new Range(Pos(range.anchor.line + distance, range.anchor.ch), + Pos(range.head.line + distance, range.head.ch)); + }), doc.sel.primIndex); + if (doc.cm) regChange(doc.cm, doc.first, doc.first - distance, distance); } + // More lower-level change function, handling only a single document + // (not linked ones). function makeChangeSingleDoc(doc, change, selAfter, spans) { if (doc.cm && !doc.cm.curOp) return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans); @@ -2451,16 +3351,19 @@ window.CodeMirror = (function() { change.removed = getBetween(doc, change.from, change.to); if (!selAfter) selAfter = computeSelAfterChange(doc, change, null); - if (doc.cm) makeChangeSingleDocInEditor(doc.cm, change, spans, selAfter); - else updateDoc(doc, change, spans, selAfter); + if (doc.cm) makeChangeSingleDocInEditor(doc.cm, change, spans); + else updateDoc(doc, change, spans); + setSelectionNoUndo(doc, selAfter, sel_dontScroll); } - function makeChangeSingleDocInEditor(cm, change, spans, selAfter) { + // Handle the interaction of a change to a document with the editor + // that this document is part of. + function makeChangeSingleDocInEditor(cm, change, spans) { var doc = cm.doc, display = cm.display, from = change.from, to = change.to; var recomputeMaxLength = false, checkWidthStart = from.line; if (!cm.options.lineWrapping) { - checkWidthStart = lineNo(visualLine(doc, getLine(doc, from.line))); + checkWidthStart = lineNo(visualLine(getLine(doc, from.line))); doc.iter(checkWidthStart, to.line + 1, function(line) { if (line == display.maxLine) { recomputeMaxLength = true; @@ -2469,14 +3372,14 @@ window.CodeMirror = (function() { }); } - if (!posLess(doc.sel.head, change.from) && !posLess(change.to, doc.sel.head)) + if (doc.sel.contains(change.from, change.to) > -1) cm.curOp.cursorActivity = true; - updateDoc(doc, change, spans, selAfter, estimateHeight(cm)); + updateDoc(doc, change, spans, estimateHeight(cm)); if (!cm.options.lineWrapping) { doc.iter(checkWidthStart, from.line + change.text.length, function(line) { - var len = lineLength(doc, line); + var len = lineLength(line); if (len > display.maxLineLength) { display.maxLine = line; display.maxLineLength = len; @@ -2493,184 +3396,38 @@ window.CodeMirror = (function() { var lendiff = change.text.length - (to.line - from.line) - 1; // Remember that these lines changed, for updating the display - regChange(cm, from.line, to.line + 1, lendiff); + if (from.line == to.line && change.text.length == 1 && !isWholeLineUpdate(cm.doc, change)) + regLineChange(cm, from.line, "text"); + else + regChange(cm, from.line, to.line + 1, lendiff); - if (hasHandler(cm, "change")) { - var changeObj = {from: from, to: to, - text: change.text, - removed: change.removed, - origin: change.origin}; - if (cm.curOp.textChanged) { - for (var cur = cm.curOp.textChanged; cur.next; cur = cur.next) {} - cur.next = changeObj; - } else cm.curOp.textChanged = changeObj; - } + if (hasHandler(cm, "change") || hasHandler(cm, "changes")) + (cm.curOp.changeObjs || (cm.curOp.changeObjs = [])).push({ + from: from, to: to, + text: change.text, + removed: change.removed, + origin: change.origin + }); } function replaceRange(doc, code, from, to, origin) { if (!to) to = from; - if (posLess(to, from)) { var tmp = to; to = from; from = tmp; } + if (cmp(to, from) < 0) { var tmp = to; to = from; from = tmp; } if (typeof code == "string") code = splitLines(code); - makeChange(doc, {from: from, to: to, text: code, origin: origin}, null); + makeChange(doc, {from: from, to: to, text: code, origin: origin}); } - // POSITION OBJECT + // SCROLLING THINGS INTO VIEW - function Pos(line, ch) { - if (!(this instanceof Pos)) return new Pos(line, ch); - this.line = line; this.ch = ch; - } - CodeMirror.Pos = Pos; - - function posEq(a, b) {return a.line == b.line && a.ch == b.ch;} - function posLess(a, b) {return a.line < b.line || (a.line == b.line && a.ch < b.ch);} - function cmp(a, b) {return a.line - b.line || a.ch - b.ch;} - function copyPos(x) {return Pos(x.line, x.ch);} - - // SELECTION - - function clipLine(doc, n) {return Math.max(doc.first, Math.min(n, doc.first + doc.size - 1));} - function clipPos(doc, pos) { - if (pos.line < doc.first) return Pos(doc.first, 0); - var last = doc.first + doc.size - 1; - if (pos.line > last) return Pos(last, getLine(doc, last).text.length); - return clipToLen(pos, getLine(doc, pos.line).text.length); - } - function clipToLen(pos, linelen) { - var ch = pos.ch; - if (ch == null || ch > linelen) return Pos(pos.line, linelen); - else if (ch < 0) return Pos(pos.line, 0); - else return pos; - } - function isLine(doc, l) {return l >= doc.first && l < doc.first + doc.size;} - - // If shift is held, this will move the selection anchor. Otherwise, - // it'll set the whole selection. - function extendSelection(doc, pos, other, bias) { - if (doc.sel.shift || doc.sel.extend) { - var anchor = doc.sel.anchor; - if (other) { - var posBefore = posLess(pos, anchor); - if (posBefore != posLess(other, anchor)) { - anchor = pos; - pos = other; - } else if (posBefore != posLess(pos, other)) { - pos = other; - } - } - setSelection(doc, anchor, pos, bias); - } else { - setSelection(doc, pos, other || pos, bias); - } - if (doc.cm) doc.cm.curOp.userSelChange = true; - } - - function filterSelectionChange(doc, anchor, head) { - var obj = {anchor: anchor, head: head}; - signal(doc, "beforeSelectionChange", doc, obj); - if (doc.cm) signal(doc.cm, "beforeSelectionChange", doc.cm, obj); - obj.anchor = clipPos(doc, obj.anchor); obj.head = clipPos(doc, obj.head); - return obj; - } - - // Update the selection. Last two args are only used by - // updateDoc, since they have to be expressed in the line - // numbers before the update. - function setSelection(doc, anchor, head, bias, checkAtomic) { - if (!checkAtomic && hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange")) { - var filtered = filterSelectionChange(doc, anchor, head); - head = filtered.head; - anchor = filtered.anchor; - } - - var sel = doc.sel; - sel.goalColumn = null; - if (bias == null) bias = posLess(head, sel.head) ? -1 : 1; - // Skip over atomic spans. - if (checkAtomic || !posEq(anchor, sel.anchor)) - anchor = skipAtomic(doc, anchor, bias, checkAtomic != "push"); - if (checkAtomic || !posEq(head, sel.head)) - head = skipAtomic(doc, head, bias, checkAtomic != "push"); - - if (posEq(sel.anchor, anchor) && posEq(sel.head, head)) return; - - sel.anchor = anchor; sel.head = head; - var inv = posLess(head, anchor); - sel.from = inv ? head : anchor; - sel.to = inv ? anchor : head; - - if (doc.cm) - doc.cm.curOp.updateInput = doc.cm.curOp.selectionChanged = - doc.cm.curOp.cursorActivity = true; - - signalLater(doc, "cursorActivity", doc); - } - - function reCheckSelection(cm) { - setSelection(cm.doc, cm.doc.sel.from, cm.doc.sel.to, null, "push"); - } - - function skipAtomic(doc, pos, bias, mayClear) { - var flipped = false, curPos = pos; - var dir = bias || 1; - doc.cantEdit = false; - search: for (;;) { - var line = getLine(doc, curPos.line); - if (line.markedSpans) { - for (var i = 0; i < line.markedSpans.length; ++i) { - var sp = line.markedSpans[i], m = sp.marker; - if ((sp.from == null || (m.inclusiveLeft ? sp.from <= curPos.ch : sp.from < curPos.ch)) && - (sp.to == null || (m.inclusiveRight ? sp.to >= curPos.ch : sp.to > curPos.ch))) { - if (mayClear) { - signal(m, "beforeCursorEnter"); - if (m.explicitlyCleared) { - if (!line.markedSpans) break; - else {--i; continue;} - } - } - if (!m.atomic) continue; - var newPos = m.find()[dir < 0 ? "from" : "to"]; - if (posEq(newPos, curPos)) { - newPos.ch += dir; - if (newPos.ch < 0) { - if (newPos.line > doc.first) newPos = clipPos(doc, Pos(newPos.line - 1)); - else newPos = null; - } else if (newPos.ch > line.text.length) { - if (newPos.line < doc.first + doc.size - 1) newPos = Pos(newPos.line + 1, 0); - else newPos = null; - } - if (!newPos) { - if (flipped) { - // Driven in a corner -- no valid cursor position found at all - // -- try again *with* clearing, if we didn't already - if (!mayClear) return skipAtomic(doc, pos, bias, true); - // Otherwise, turn off editing until further notice, and return the start of the doc - doc.cantEdit = true; - return Pos(doc.first, 0); - } - flipped = true; newPos = pos; dir = -dir; - } - } - curPos = newPos; - continue search; - } - } - } - return curPos; - } - } - - // SCROLLING - - function scrollCursorIntoView(cm) { - var coords = scrollPosIntoView(cm, cm.doc.sel.head, null, cm.options.cursorScrollMargin); - if (!cm.state.focused) return; - var display = cm.display, box = getRect(display.sizer), doScroll = null; + // If an editor sits on the top or bottom of the window, partially + // scrolled out of view, this ensures that the cursor is visible. + function maybeScrollWindow(cm, coords) { + var display = cm.display, box = display.sizer.getBoundingClientRect(), doScroll = null; if (coords.top + box.top < 0) doScroll = true; else if (coords.bottom + box.top > (window.innerHeight || document.documentElement.clientHeight)) doScroll = false; if (doScroll != null && !phantom) { var scrollNode = elt("div", "\u200b", null, "position: absolute; top: " + - (coords.top - display.viewOffset) + "px; height: " + + (coords.top - display.viewOffset - paddingTop(cm.display)) + "px; height: " + (coords.bottom - coords.top + scrollerCutOff) + "px; left: " + coords.left + "px; width: 2px;"); cm.display.lineSpace.appendChild(scrollNode); @@ -2679,6 +3436,9 @@ window.CodeMirror = (function() { } } + // Scroll a given position into view (immediately), verifying that + // it actually became visible (as line heights are accurately + // measured, the position of something may 'drift' during drawing). function scrollPosIntoView(cm, pos, end, margin) { if (margin == null) margin = 0; for (;;) { @@ -2701,16 +3461,22 @@ window.CodeMirror = (function() { } } + // Scroll a given set of coordinates into view (immediately). function scrollIntoView(cm, x1, y1, x2, y2) { var scrollPos = calculateScrollPos(cm, x1, y1, x2, y2); if (scrollPos.scrollTop != null) setScrollTop(cm, scrollPos.scrollTop); if (scrollPos.scrollLeft != null) setScrollLeft(cm, scrollPos.scrollLeft); } + // Calculate a new scroll position needed to scroll the given + // rectangle into view. Returns an object with scrollTop and + // scrollLeft properties. When these are undefined, the + // vertical/horizontal position does not need to be adjusted. function calculateScrollPos(cm, x1, y1, x2, y2) { var display = cm.display, snapMargin = textHeight(cm.display); if (y1 < 0) y1 = 0; - var screen = display.scroller.clientHeight - scrollerCutOff, screentop = display.scroller.scrollTop, result = {}; + var screentop = cm.curOp && cm.curOp.scrollTop != null ? cm.curOp.scrollTop : display.scroller.scrollTop; + var screen = display.scroller.clientHeight - scrollerCutOff, result = {}; var docBottom = cm.doc.height + paddingVert(display); var atTop = y1 < snapMargin, atBottom = y2 > docBottom - snapMargin; if (y1 < screentop) { @@ -2720,7 +3486,8 @@ window.CodeMirror = (function() { if (newTop != screentop) result.scrollTop = newTop; } - var screenw = display.scroller.clientWidth - scrollerCutOff, screenleft = display.scroller.scrollLeft; + var screenleft = cm.curOp && cm.curOp.scrollLeft != null ? cm.curOp.scrollLeft : display.scroller.scrollLeft; + var screenw = display.scroller.clientWidth - scrollerCutOff; x1 += display.gutters.offsetWidth; x2 += display.gutters.offsetWidth; var gutterw = display.gutters.offsetWidth; var atLeft = x1 < gutterw + 10; @@ -2733,30 +3500,65 @@ window.CodeMirror = (function() { return result; } - function updateScrollPos(cm, left, top) { - cm.curOp.updateScrollPos = {scrollLeft: left == null ? cm.doc.scrollLeft : left, - scrollTop: top == null ? cm.doc.scrollTop : top}; + // Store a relative adjustment to the scroll position in the current + // operation (to be applied when the operation finishes). + function addToScrollPos(cm, left, top) { + if (left != null || top != null) resolveScrollToPos(cm); + if (left != null) + cm.curOp.scrollLeft = (cm.curOp.scrollLeft == null ? cm.doc.scrollLeft : cm.curOp.scrollLeft) + left; + if (top != null) + cm.curOp.scrollTop = (cm.curOp.scrollTop == null ? cm.doc.scrollTop : cm.curOp.scrollTop) + top; } - function addToScrollPos(cm, left, top) { - var pos = cm.curOp.updateScrollPos || (cm.curOp.updateScrollPos = {scrollLeft: cm.doc.scrollLeft, scrollTop: cm.doc.scrollTop}); - var scroll = cm.display.scroller; - pos.scrollTop = Math.max(0, Math.min(scroll.scrollHeight - scroll.clientHeight, pos.scrollTop + top)); - pos.scrollLeft = Math.max(0, Math.min(scroll.scrollWidth - scroll.clientWidth, pos.scrollLeft + left)); + // Make sure that at the end of the operation the current cursor is + // shown. + function ensureCursorVisible(cm) { + resolveScrollToPos(cm); + var cur = cm.getCursor(), from = cur, to = cur; + if (!cm.options.lineWrapping) { + from = cur.ch ? Pos(cur.line, cur.ch - 1) : cur; + to = Pos(cur.line, cur.ch + 1); + } + cm.curOp.scrollToPos = {from: from, to: to, margin: cm.options.cursorScrollMargin, isCursor: true}; + } + + // When an operation has its scrollToPos property set, and another + // scroll action is applied before the end of the operation, this + // 'simulates' scrolling that position into view in a cheap way, so + // that the effect of intermediate scroll commands is not ignored. + function resolveScrollToPos(cm) { + var range = cm.curOp.scrollToPos; + if (range) { + cm.curOp.scrollToPos = null; + var from = estimateCoords(cm, range.from), to = estimateCoords(cm, range.to); + var sPos = calculateScrollPos(cm, Math.min(from.left, to.left), + Math.min(from.top, to.top) - range.margin, + Math.max(from.right, to.right), + Math.max(from.bottom, to.bottom) + range.margin); + cm.scrollTo(sPos.scrollLeft, sPos.scrollTop); + } } // API UTILITIES + // Indent the given line. The how parameter can be "smart", + // "add"/null, "subtract", or "prev". When aggressive is false + // (typically set to true for forced single-line indents), empty + // lines are not indented, and places where the mode returns Pass + // are left alone. function indentLine(cm, n, how, aggressive) { - var doc = cm.doc; + var doc = cm.doc, state; if (how == null) how = "add"; if (how == "smart") { + // Fall back to "prev" when the mode doesn't have an indentation + // method. if (!cm.doc.mode.indent) how = "prev"; - else var state = getStateBefore(cm, n); + else state = getStateBefore(cm, n); } var tabSize = cm.options.tabSize; var line = getLine(doc, n), curSpace = countColumn(line.text, null, tabSize); + if (line.stateAfter) line.stateAfter = null; var curSpaceString = line.text.match(/^\s*/)[0], indentation; if (!aggressive && !/\S/.test(line.text)) { indentation = 0; @@ -2785,23 +3587,70 @@ window.CodeMirror = (function() { for (var i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += "\t";} if (pos < indentation) indentString += spaceStr(indentation - pos); - if (indentString != curSpaceString) + if (indentString != curSpaceString) { replaceRange(cm.doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input"); - else if (doc.sel.head.line == n && doc.sel.head.ch < curSpaceString.length) - setSelection(doc, Pos(n, curSpaceString.length), Pos(n, curSpaceString.length), 1); + } else { + // Ensure that, if the cursor was in the whitespace at the start + // of the line, it is moved to the end of that space. + for (var i = 0; i < doc.sel.ranges.length; i++) { + var range = doc.sel.ranges[i]; + if (range.head.line == n && range.head.ch < curSpaceString.length) { + var pos = Pos(n, curSpaceString.length); + replaceOneSelection(doc, i, new Range(pos, pos)); + break; + } + } + } line.stateAfter = null; } - function changeLine(cm, handle, op) { + // Utility for applying a change to a line by handle or number, + // returning the number and optionally registering the line as + // changed. + function changeLine(cm, handle, changeType, op) { var no = handle, line = handle, doc = cm.doc; if (typeof handle == "number") line = getLine(doc, clipLine(doc, handle)); else no = lineNo(handle); if (no == null) return null; - if (op(line, no)) regChange(cm, no, no + 1); + if (op(line, no)) regLineChange(cm, no, changeType); else return null; return line; } + // Helper for deleting text near the selection(s), used to implement + // backspace, delete, and similar functionality. + function deleteNearSelection(cm, compute) { + var ranges = cm.doc.sel.ranges, kill = []; + // Build up a set of ranges to kill first, merging overlapping + // ranges. + for (var i = 0; i < ranges.length; i++) { + var toKill = compute(ranges[i]); + while (kill.length && cmp(toKill.from, lst(kill).to) <= 0) { + var replaced = kill.pop(); + if (cmp(replaced.from, toKill.from) < 0) { + toKill.from = replaced.from; + break; + } + } + kill.push(toKill); + } + // Next, remove those actual ranges. + runInOp(cm, function() { + for (var i = kill.length - 1; i >= 0; i--) + replaceRange(cm.doc, "", kill[i].from, kill[i].to, "+delete"); + ensureCursorVisible(cm); + }); + } + + // Used for horizontal relative motion. Dir is -1 or 1 (left or + // right), unit can be "char", "column" (like char, but doesn't + // cross line boundaries), "word" (across next word), or "group" (to + // the start of next group of word or non-word-non-whitespace + // chars). The visually param controls whether, in right-to-left + // text, direction 1 means to move towards the next index in the + // string, or towards the character to the right of the current + // position. The resulting position will have a hitSide=true + // property if it reached the end of the document. function findPosH(doc, pos, dir, unit, visually) { var line = pos.line, ch = pos.ch, origDir = dir; var lineObj = getLine(doc, line); @@ -2831,13 +3680,15 @@ window.CodeMirror = (function() { if (dir < 0 && !moveOnce(!first)) break; var cur = lineObj.text.charAt(ch) || "\n"; var type = isWordChar(cur) ? "w" - : !group ? null - : /\s/.test(cur) ? null + : group && cur == "\n" ? "n" + : !group || /\s/.test(cur) ? null : "p"; + if (group && !first && !type) type = "s"; if (sawType && sawType != type) { if (dir < 0) {dir = 1; moveOnce();} break; } + if (type) sawType = type; if (dir > 0 && !moveOnce(!first)) break; } @@ -2847,6 +3698,9 @@ window.CodeMirror = (function() { return result; } + // For relative vertical movement. Dir may be -1 or 1. Unit can be + // "page" or "line". The resulting position will have a hitSide=true + // property if it reached the end of the document. function findPosV(cm, pos, dir, unit) { var doc = cm.doc, x = pos.left, y; if (unit == "page") { @@ -2864,7 +3718,9 @@ window.CodeMirror = (function() { return target; } - function findWordAt(line, pos) { + // Find the word at the given position (as returned by coordsChar). + function findWordAt(doc, pos) { + var line = getLine(doc, pos.line).text; var start = pos.ch, end = pos.ch; if (line) { if ((pos.xRel < 0 || end == line.length) && start) --start; else ++end; @@ -2875,17 +3731,18 @@ window.CodeMirror = (function() { while (start > 0 && check(line.charAt(start - 1))) --start; while (end < line.length && check(line.charAt(end))) ++end; } - return {from: Pos(pos.line, start), to: Pos(pos.line, end)}; + return new Range(Pos(pos.line, start), Pos(pos.line, end)); } - function selectLine(cm, line) { - extendSelection(cm.doc, Pos(line, 0), clipPos(cm.doc, Pos(line + 1, 0))); - } + // EDITOR METHODS - // PROTOTYPE + // The publicly visible API. Note that methodOp(f) means + // 'wrap f in an operation, performed on its `this` parameter'. - // The publicly visible API. Note that operation(null, f) means - // 'wrap f in an operation, performed on its `this` parameter' + // This is not the complete set of editor methods. Most of the + // methods defined on the Doc type are also injected into + // CodeMirror.prototype, for backwards compatibility and + // convenience. CodeMirror.prototype = { constructor: CodeMirror, @@ -2914,14 +3771,14 @@ window.CodeMirror = (function() { } }, - addOverlay: operation(null, function(spec, options) { + addOverlay: methodOp(function(spec, options) { var mode = spec.token ? spec : CodeMirror.getMode(this.options, spec); if (mode.startState) throw new Error("Overlays may not be stateful."); this.state.overlays.push({mode: mode, modeSpec: spec, opaque: options && options.opaque}); this.state.modeGen++; regChange(this); }), - removeOverlay: operation(null, function(spec) { + removeOverlay: methodOp(function(spec) { var overlays = this.state.overlays; for (var i = 0; i < overlays.length; ++i) { var cur = overlays[i].modeSpec; @@ -2934,18 +3791,29 @@ window.CodeMirror = (function() { } }), - indentLine: operation(null, function(n, dir, aggressive) { + indentLine: methodOp(function(n, dir, aggressive) { if (typeof dir != "string" && typeof dir != "number") { if (dir == null) dir = this.options.smartIndent ? "smart" : "prev"; else dir = dir ? "add" : "subtract"; } if (isLine(this.doc, n)) indentLine(this, n, dir, aggressive); }), - indentSelection: operation(null, function(how) { - var sel = this.doc.sel; - if (posEq(sel.from, sel.to)) return indentLine(this, sel.from.line, how, true); - var e = sel.to.line - (sel.to.ch ? 0 : 1); - for (var i = sel.from.line; i <= e; ++i) indentLine(this, i, how); + indentSelection: methodOp(function(how) { + var ranges = this.doc.sel.ranges, end = -1; + for (var i = 0; i < ranges.length; i++) { + var range = ranges[i]; + if (!range.empty()) { + var start = Math.max(end, range.from().line); + var to = range.to(); + end = Math.min(this.lastLine(), to.line - (to.ch ? 0 : 1)) + 1; + for (var j = start; j < end; ++j) + indentLine(this, j, how); + } else if (range.head.line > end) { + indentLine(this, range.head.line, how, true); + end = range.head.line; + if (i == this.doc.sel.primIndex) ensureCursorVisible(this); + } + } }), // Fetch the parser token for a given character. Useful for hacks @@ -2963,7 +3831,6 @@ window.CodeMirror = (function() { return {start: stream.start, end: stream.pos, string: stream.current(), - className: style || null, // Deprecated, use 'type' instead type: style || null, state: state}; }, @@ -3022,10 +3889,10 @@ window.CodeMirror = (function() { }, cursorCoords: function(start, mode) { - var pos, sel = this.doc.sel; - if (start == null) pos = sel.head; + var pos, range = this.doc.sel.primary(); + if (start == null) pos = range.head; else if (typeof start == "object") pos = clipPos(this.doc, start); - else pos = start ? sel.from : sel.to; + else pos = start ? range.from() : range.to(); return cursorCoords(this, pos, mode || "page"); }, @@ -3047,15 +3914,15 @@ window.CodeMirror = (function() { if (line < this.doc.first) line = this.doc.first; else if (line > last) { line = last; end = true; } var lineObj = getLine(this.doc, line); - return intoCoordSystem(this, getLine(this.doc, line), {top: 0, left: 0}, mode || "page").top + - (end ? lineObj.height : 0); + return intoCoordSystem(this, lineObj, {top: 0, left: 0}, mode || "page").top + + (end ? this.doc.height - heightAtLine(lineObj) : 0); }, defaultTextHeight: function() { return textHeight(this.display); }, defaultCharWidth: function() { return charWidth(this.display); }, - setGutterMarker: operation(null, function(line, gutterID, value) { - return changeLine(this, line, function(line) { + setGutterMarker: methodOp(function(line, gutterID, value) { + return changeLine(this, line, "gutter", function(line) { var markers = line.gutterMarkers || (line.gutterMarkers = {}); markers[gutterID] = value; if (!value && isEmpty(markers)) line.gutterMarkers = null; @@ -3063,20 +3930,20 @@ window.CodeMirror = (function() { }); }), - clearGutter: operation(null, function(gutterID) { + clearGutter: methodOp(function(gutterID) { var cm = this, doc = cm.doc, i = doc.first; doc.iter(function(line) { if (line.gutterMarkers && line.gutterMarkers[gutterID]) { line.gutterMarkers[gutterID] = null; - regChange(cm, i, i + 1); + regLineChange(cm, i, "gutter"); if (isEmpty(line.gutterMarkers)) line.gutterMarkers = null; } ++i; }); }), - addLineClass: operation(null, function(handle, where, cls) { - return changeLine(this, handle, function(line) { + addLineClass: methodOp(function(handle, where, cls) { + return changeLine(this, handle, "class", function(line) { var prop = where == "text" ? "textClass" : where == "background" ? "bgClass" : "wrapClass"; if (!line[prop]) line[prop] = cls; else if (new RegExp("(?:^|\\s)" + cls + "(?:$|\\s)").test(line[prop])) return false; @@ -3085,8 +3952,8 @@ window.CodeMirror = (function() { }); }), - removeLineClass: operation(null, function(handle, where, cls) { - return changeLine(this, handle, function(line) { + removeLineClass: methodOp(function(handle, where, cls) { + return changeLine(this, handle, "class", function(line) { var prop = where == "text" ? "textClass" : where == "background" ? "bgClass" : "wrapClass"; var cur = line[prop]; if (!cur) return false; @@ -3101,7 +3968,7 @@ window.CodeMirror = (function() { }); }), - addLineWidget: operation(null, function(handle, node, options) { + addLineWidget: methodOp(function(handle, node, options) { return addLineWidget(this, handle, node, options); }), @@ -3122,7 +3989,7 @@ window.CodeMirror = (function() { widgets: line.widgets}; }, - getViewport: function() { return {from: this.display.showingFrom, to: this.display.showingTo};}, + getViewport: function() { return {from: this.display.viewFrom, to: this.display.viewTo};}, addWidget: function(pos, node, scroll, vert, horiz) { var display = this.display; @@ -3157,7 +4024,9 @@ window.CodeMirror = (function() { scrollIntoView(this, left, top, left + node.offsetWidth, top + node.offsetHeight); }, - triggerOnKeyDown: operation(null, onKeyDown), + triggerOnKeyDown: methodOp(onKeyDown), + triggerOnKeyPress: methodOp(onKeyPress), + triggerOnKeyUp: methodOp(onKeyUp), execCommand: function(cmd) { if (commands.hasOwnProperty(cmd)) @@ -3174,20 +4043,25 @@ window.CodeMirror = (function() { return cur; }, - moveH: operation(null, function(dir, unit) { - var sel = this.doc.sel, pos; - if (sel.shift || sel.extend || posEq(sel.from, sel.to)) - pos = findPosH(this.doc, sel.head, dir, unit, this.options.rtlMoveVisually); - else - pos = dir < 0 ? sel.from : sel.to; - extendSelection(this.doc, pos, pos, dir); + moveH: methodOp(function(dir, unit) { + var cm = this; + cm.extendSelectionsBy(function(range) { + if (cm.display.shift || cm.doc.extend || range.empty()) + return findPosH(cm.doc, range.head, dir, unit, cm.options.rtlMoveVisually); + else + return dir < 0 ? range.from() : range.to(); + }, sel_move); }), - deleteH: operation(null, function(dir, unit) { - var sel = this.doc.sel; - if (!posEq(sel.from, sel.to)) replaceRange(this.doc, "", sel.from, sel.to, "+delete"); - else replaceRange(this.doc, "", sel.from, findPosH(this.doc, sel.head, dir, unit, false), "+delete"); - this.curOp.userSelChange = true; + deleteH: methodOp(function(dir, unit) { + var sel = this.doc.sel, doc = this.doc; + if (sel.somethingSelected()) + doc.replaceSelection("", null, "+delete"); + else + deleteNearSelection(this, function(range) { + var other = findPosH(doc, range.head, dir, unit, false); + return dir < 0 ? {from: other, to: range.head} : {from: range.head, to: other}; + }); }), findPosV: function(from, amount, unit, goalColumn) { @@ -3203,32 +4077,39 @@ window.CodeMirror = (function() { return cur; }, - moveV: operation(null, function(dir, unit) { - var sel = this.doc.sel, target, goal; - if (sel.shift || sel.extend || posEq(sel.from, sel.to)) { - var pos = cursorCoords(this, sel.head, "div"); - if (sel.goalColumn != null) pos.left = sel.goalColumn; - target = findPosV(this, pos, dir, unit); - if (unit == "page") addToScrollPos(this, 0, charCoords(this, target, "div").top - pos.top); - goal = pos.left; - } else { - target = dir < 0 ? sel.from : sel.to; - } - extendSelection(this.doc, target, target, dir); - if (goal != null) sel.goalColumn = goal; + moveV: methodOp(function(dir, unit) { + var cm = this, doc = this.doc, goals = []; + var collapse = !cm.display.shift && !doc.extend && doc.sel.somethingSelected(); + doc.extendSelectionsBy(function(range) { + if (collapse) + return dir < 0 ? range.from() : range.to(); + var headPos = cursorCoords(cm, range.head, "div"); + if (range.goalColumn != null) headPos.left = range.goalColumn; + goals.push(headPos.left); + var pos = findPosV(cm, headPos, dir, unit); + if (unit == "page" && range == doc.sel.primary()) + addToScrollPos(cm, null, charCoords(cm, pos, "div").top - headPos.top); + return pos; + }, sel_move); + if (goals.length) for (var i = 0; i < doc.sel.ranges.length; i++) + doc.sel.ranges[i].goalColumn = goals[i]; }), toggleOverwrite: function(value) { if (value != null && value == this.state.overwrite) return; if (this.state.overwrite = !this.state.overwrite) - this.display.cursor.className += " CodeMirror-overwrite"; + this.display.cursorDiv.className += " CodeMirror-overwrite"; else - this.display.cursor.className = this.display.cursor.className.replace(" CodeMirror-overwrite", ""); - }, - hasFocus: function() { return this.state.focused; }, + this.display.cursorDiv.className = this.display.cursorDiv.className.replace(" CodeMirror-overwrite", ""); - scrollTo: operation(null, function(x, y) { - updateScrollPos(this, x, y); + signal(this, "overwriteToggle", this, this.state.overwrite); + }, + hasFocus: function() { return activeElt() == this.display.input; }, + + scrollTo: methodOp(function(x, y) { + if (x != null || y != null) resolveScrollToPos(this); + if (x != null) this.curOp.scrollLeft = x; + if (y != null) this.curOp.scrollTop = y; }), getScrollInfo: function() { var scroller = this.display.scroller, co = scrollerCutOff; @@ -3237,54 +4118,60 @@ window.CodeMirror = (function() { clientHeight: scroller.clientHeight - co, clientWidth: scroller.clientWidth - co}; }, - scrollIntoView: operation(null, function(range, margin) { - if (range == null) range = {from: this.doc.sel.head, to: null}; - else if (typeof range == "number") range = {from: Pos(range, 0), to: null}; - else if (range.from == null) range = {from: range, to: null}; - if (!range.to) range.to = range.from; - if (!margin) margin = 0; - - var coords = range; - if (range.from.line != null) { - this.curOp.scrollToPos = {from: range.from, to: range.to, margin: margin}; - coords = {from: cursorCoords(this, range.from), - to: cursorCoords(this, range.to)}; + scrollIntoView: methodOp(function(range, margin) { + if (range == null) { + range = {from: this.doc.sel.primary().head, to: null}; + if (margin == null) margin = this.options.cursorScrollMargin; + } else if (typeof range == "number") { + range = {from: Pos(range, 0), to: null}; + } else if (range.from == null) { + range = {from: range, to: null}; + } + if (!range.to) range.to = range.from; + range.margin = margin || 0; + + if (range.from.line != null) { + resolveScrollToPos(this); + this.curOp.scrollToPos = range; + } else { + var sPos = calculateScrollPos(this, Math.min(range.from.left, range.to.left), + Math.min(range.from.top, range.to.top) - range.margin, + Math.max(range.from.right, range.to.right), + Math.max(range.from.bottom, range.to.bottom) + range.margin); + this.scrollTo(sPos.scrollLeft, sPos.scrollTop); } - var sPos = calculateScrollPos(this, Math.min(coords.from.left, coords.to.left), - Math.min(coords.from.top, coords.to.top) - margin, - Math.max(coords.from.right, coords.to.right), - Math.max(coords.from.bottom, coords.to.bottom) + margin); - updateScrollPos(this, sPos.scrollLeft, sPos.scrollTop); }), - setSize: operation(null, function(width, height) { + setSize: methodOp(function(width, height) { function interpret(val) { return typeof val == "number" || /^\d+$/.test(String(val)) ? val + "px" : val; } if (width != null) this.display.wrapper.style.width = interpret(width); if (height != null) this.display.wrapper.style.height = interpret(height); - if (this.options.lineWrapping) - this.display.measureLineCache.length = this.display.measureLineCachePos = 0; + if (this.options.lineWrapping) clearLineMeasurementCache(this); this.curOp.forceUpdate = true; + signal(this, "refresh", this); }), operation: function(f){return runInOp(this, f);}, - refresh: operation(null, function() { - var badHeight = this.display.cachedTextHeight == null; - clearCaches(this); - updateScrollPos(this, this.doc.scrollLeft, this.doc.scrollTop); + refresh: methodOp(function() { + var oldHeight = this.display.cachedTextHeight; regChange(this); - if (badHeight) estimateLineHeights(this); + clearCaches(this); + this.scrollTo(this.doc.scrollLeft, this.doc.scrollTop); + if (oldHeight == null || Math.abs(oldHeight - textHeight(this.display)) > .5) + estimateLineHeights(this); + signal(this, "refresh", this); }), - swapDoc: operation(null, function(doc) { + swapDoc: methodOp(function(doc) { var old = this.doc; old.cm = null; attachDoc(this, doc); clearCaches(this); - resetInput(this, true); - updateScrollPos(this, doc.scrollLeft, doc.scrollTop); + resetInput(this); + this.scrollTo(doc.scrollLeft, doc.scrollTop); signalLater(this, "swapDoc", this, old); return old; }), @@ -3298,10 +4185,10 @@ window.CodeMirror = (function() { // OPTION DEFAULTS - var optionHandlers = CodeMirror.optionHandlers = {}; - // The default configuration options. var defaults = CodeMirror.defaults = {}; + // Functions to run when options are changed. + var optionHandlers = CodeMirror.optionHandlers = {}; function option(name, deflt, handle, notOnInit) { CodeMirror.defaults[name] = deflt; @@ -3309,6 +4196,7 @@ window.CodeMirror = (function() { notOnInit ? function(cm, val, old) {if (old != Init) handle(cm, val, old);} : handle; } + // Passed to option handlers when there is no old value. var Init = CodeMirror.Init = {toString: function(){return "CodeMirror.Init";}}; // These two are, on init, called from the constructor because they @@ -3345,9 +4233,6 @@ window.CodeMirror = (function() { option("keyMap", "default", keyMapChanged); option("extraKeys", null); - option("onKeyEvent", null); - option("onDragEvent", null); - option("lineWrapping", false, wrappingChanged, true); option("gutters", [], function(cm) { setGuttersForLineNumbers(cm.options); @@ -3375,10 +4260,10 @@ window.CodeMirror = (function() { cm.display.disabled = true; } else { cm.display.disabled = false; - if (!val) resetInput(cm, true); + if (!val) resetInput(cm); } }); - option("disableInput", false, function(cm, val) {if (!val) resetInput(cm, true);}, true); + option("disableInput", false, function(cm, val) {if (!val) resetInput(cm);}, true); option("dragDrop", true); option("cursorBlinkRate", 530); @@ -3389,11 +4274,10 @@ window.CodeMirror = (function() { option("flattenSpans", true, resetModeState, true); option("addModeClass", false, resetModeState, true); option("pollInterval", 100); - option("undoDepth", 40, function(cm, val){cm.doc.history.undoDepth = val;}); - option("historyEventDelay", 500); + option("undoDepth", 200, function(cm, val){cm.doc.history.undoDepth = val;}); + option("historyEventDelay", 1250); option("viewportMargin", 10, function(cm){cm.refresh();}, true); option("maxHighlightLength", 10000, resetModeState, true); - option("crudeMeasuringFrom", 10000); option("moveInputWithCursor", true, function(cm, val) { if (!val) cm.display.inputDiv.style.top = cm.display.inputDiv.style.left = 0; }); @@ -3408,6 +4292,9 @@ window.CodeMirror = (function() { // Known modes, by name and by MIME var modes = CodeMirror.modes = {}, mimeModes = CodeMirror.mimeModes = {}; + // Extra arguments are stored as the mode's dependencies, which is + // used by (legacy) mechanisms like loadmode.js to automatically + // load a mode. (Preferred mechanism is the require/define calls.) CodeMirror.defineMode = function(name, mode) { if (!CodeMirror.defaults.mode && name != "null") CodeMirror.defaults.mode = name; if (arguments.length > 2) { @@ -3421,11 +4308,14 @@ window.CodeMirror = (function() { mimeModes[mime] = spec; }; + // Given a MIME type, a {name, ...options} config object, or a name + // string, return a mode config object. CodeMirror.resolveMode = function(spec) { if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) { spec = mimeModes[spec]; } else if (spec && typeof spec.name == "string" && mimeModes.hasOwnProperty(spec.name)) { var found = mimeModes[spec.name]; + if (typeof found == "string") found = {name: found}; spec = createObj(found, spec); spec.name = found.name; } else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec)) { @@ -3435,6 +4325,8 @@ window.CodeMirror = (function() { else return spec || {name: "null"}; }; + // Given a mode spec (anything that resolveMode accepts), find and + // initialize an actual mode object. CodeMirror.getMode = function(options, spec) { var spec = CodeMirror.resolveMode(spec); var mfactory = modes[spec.name]; @@ -3456,11 +4348,14 @@ window.CodeMirror = (function() { return modeObj; }; + // Minimal default mode. CodeMirror.defineMode("null", function() { return {token: function(stream) {stream.skipToEnd();}}; }); CodeMirror.defineMIME("text/plain", "null"); + // This can be used to attach properties to mode objects from + // outside the actual mode definition. var modeExtensions = CodeMirror.modeExtensions = {}; CodeMirror.extendMode = function(mode, properties) { var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {}); @@ -3490,15 +4385,12 @@ window.CodeMirror = (function() { helpers[type]._global.push({pred: predicate, val: value}); }; - // UTILITIES - - CodeMirror.isWordChar = isWordChar; - // MODE STATE HANDLING - // Utility functions for working with state. Exported because modes - // sometimes need to do this. - function copyState(mode, state) { + // Utility functions for working with state. Exported because nested + // modes need to do this for their inner modes. + + var copyState = CodeMirror.copyState = function(mode, state) { if (state === true) return state; if (mode.copyState) return mode.copyState(state); var nstate = {}; @@ -3508,14 +4400,14 @@ window.CodeMirror = (function() { nstate[n] = val; } return nstate; - } - CodeMirror.copyState = copyState; + }; - function startState(mode, a1, a2) { + var startState = CodeMirror.startState = function(mode, a1, a2) { return mode.startState ? mode.startState(a1, a2) : true; - } - CodeMirror.startState = startState; + }; + // Given a mode and a state (for that mode), find the inner mode and + // state at the position that the state refers to. CodeMirror.innerMode = function(mode, state) { while (mode.innerMode) { var info = mode.innerMode(state); @@ -3528,49 +4420,73 @@ window.CodeMirror = (function() { // STANDARD COMMANDS + // Commands are parameter-less actions that can be performed on an + // editor, mostly used for keybindings. var commands = CodeMirror.commands = { - selectAll: function(cm) {cm.setSelection(Pos(cm.firstLine(), 0), Pos(cm.lastLine()));}, + selectAll: function(cm) {cm.setSelection(Pos(cm.firstLine(), 0), Pos(cm.lastLine()), sel_dontScroll);}, + singleSelection: function(cm) { + cm.setSelection(cm.getCursor("anchor"), cm.getCursor("head"), sel_dontScroll); + }, killLine: function(cm) { - var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to); - if (!sel && cm.getLine(from.line).length == from.ch) - cm.replaceRange("", from, Pos(from.line + 1, 0), "+delete"); - else cm.replaceRange("", from, sel ? to : Pos(from.line), "+delete"); + deleteNearSelection(cm, function(range) { + if (range.empty()) { + var len = getLine(cm.doc, range.head.line).text.length; + if (range.head.ch == len && range.head.line < cm.lastLine()) + return {from: range.head, to: Pos(range.head.line + 1, 0)}; + else + return {from: range.head, to: Pos(range.head.line, len)}; + } else { + return {from: range.from(), to: range.to()}; + } + }); }, deleteLine: function(cm) { - var l = cm.getCursor().line; - cm.replaceRange("", Pos(l, 0), Pos(l), "+delete"); + deleteNearSelection(cm, function(range) { + return {from: Pos(range.from().line, 0), + to: clipPos(cm.doc, Pos(range.to().line + 1, 0))}; + }); }, delLineLeft: function(cm) { - var cur = cm.getCursor(); - cm.replaceRange("", Pos(cur.line, 0), cur, "+delete"); + deleteNearSelection(cm, function(range) { + return {from: Pos(range.from().line, 0), to: range.from()}; + }); }, undo: function(cm) {cm.undo();}, redo: function(cm) {cm.redo();}, + undoSelection: function(cm) {cm.undoSelection();}, + redoSelection: function(cm) {cm.redoSelection();}, goDocStart: function(cm) {cm.extendSelection(Pos(cm.firstLine(), 0));}, goDocEnd: function(cm) {cm.extendSelection(Pos(cm.lastLine()));}, goLineStart: function(cm) { - cm.extendSelection(lineStart(cm, cm.getCursor().line)); + cm.extendSelectionsBy(function(range) { return lineStart(cm, range.head.line); }, sel_move); }, goLineStartSmart: function(cm) { - var cur = cm.getCursor(), start = lineStart(cm, cur.line); - var line = cm.getLineHandle(start.line); - var order = getOrder(line); - if (!order || order[0].level == 0) { - var firstNonWS = Math.max(0, line.text.search(/\S/)); - var inWS = cur.line == start.line && cur.ch <= firstNonWS && cur.ch; - cm.extendSelection(Pos(start.line, inWS ? 0 : firstNonWS)); - } else cm.extendSelection(start); + cm.extendSelectionsBy(function(range) { + var start = lineStart(cm, range.head.line); + var line = cm.getLineHandle(start.line); + var order = getOrder(line); + if (!order || order[0].level == 0) { + var firstNonWS = Math.max(0, line.text.search(/\S/)); + var inWS = range.head.line == start.line && range.head.ch <= firstNonWS && range.head.ch; + return Pos(start.line, inWS ? 0 : firstNonWS); + } + return start; + }, sel_move); }, goLineEnd: function(cm) { - cm.extendSelection(lineEnd(cm, cm.getCursor().line)); + cm.extendSelectionsBy(function(range) { return lineEnd(cm, range.head.line); }, sel_move); }, goLineRight: function(cm) { - var top = cm.charCoords(cm.getCursor(), "div").top + 5; - cm.extendSelection(cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div")); + cm.extendSelectionsBy(function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + return cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div"); + }, sel_move); }, goLineLeft: function(cm) { - var top = cm.charCoords(cm.getCursor(), "div").top + 5; - cm.extendSelection(cm.coordsChar({left: 0, top: top}, "div")); + cm.extendSelectionsBy(function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + return cm.coordsChar({left: 0, top: top}, "div"); + }, sel_move); }, goLineUp: function(cm) {cm.moveV(-1, "line");}, goLineDown: function(cm) {cm.moveV(1, "line");}, @@ -3593,24 +4509,32 @@ window.CodeMirror = (function() { indentAuto: function(cm) {cm.indentSelection("smart");}, indentMore: function(cm) {cm.indentSelection("add");}, indentLess: function(cm) {cm.indentSelection("subtract");}, - insertTab: function(cm) { - cm.replaceSelection("\t", "end", "+input"); - }, + insertTab: function(cm) {cm.replaceSelection("\t");}, defaultTab: function(cm) { if (cm.somethingSelected()) cm.indentSelection("add"); - else cm.replaceSelection("\t", "end", "+input"); + else cm.execCommand("insertTab"); }, transposeChars: function(cm) { - var cur = cm.getCursor(), line = cm.getLine(cur.line); - if (cur.ch > 0 && cur.ch < line.length - 1) - cm.replaceRange(line.charAt(cur.ch) + line.charAt(cur.ch - 1), - Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1)); + runInOp(cm, function() { + var ranges = cm.listSelections(); + for (var i = 0; i < ranges.length; i++) { + var cur = ranges[i].head, line = getLine(cm.doc, cur.line).text; + if (cur.ch > 0 && cur.ch < line.length - 1) + cm.replaceRange(line.charAt(cur.ch) + line.charAt(cur.ch - 1), + Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1)); + } + }); }, newlineAndIndent: function(cm) { - operation(cm, function() { - cm.replaceSelection("\n", "end", "+input"); - cm.indentLine(cm.getCursor().line, null, true); - })(); + runInOp(cm, function() { + var len = cm.listSelections().length; + for (var i = 0; i < len; i++) { + var range = cm.listSelections()[i]; + cm.replaceRange("\n", range.anchor, range.head, "+input"); + cm.indentLine(range.from().line + 1, null, true); + ensureCursorVisible(cm); + } + }); }, toggleOverwrite: function(cm) {cm.toggleOverwrite();} }; @@ -3623,17 +4547,20 @@ window.CodeMirror = (function() { "End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown", "Delete": "delCharAfter", "Backspace": "delCharBefore", "Shift-Backspace": "delCharBefore", "Tab": "defaultTab", "Shift-Tab": "indentAuto", - "Enter": "newlineAndIndent", "Insert": "toggleOverwrite" + "Enter": "newlineAndIndent", "Insert": "toggleOverwrite", + "Esc": "singleSelection" }; // Note that the save and find-related commands aren't defined by - // default. Unknown commands are simply ignored. + // default. User code or addons can define them. Unknown commands + // are simply ignored. keyMap.pcDefault = { "Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo", - "Ctrl-Home": "goDocStart", "Alt-Up": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Down": "goDocEnd", + "Ctrl-Home": "goDocStart", "Ctrl-Up": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Down": "goDocEnd", "Ctrl-Left": "goGroupLeft", "Ctrl-Right": "goGroupRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd", "Ctrl-Backspace": "delGroupBefore", "Ctrl-Delete": "delGroupAfter", "Ctrl-S": "save", "Ctrl-F": "find", "Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll", "Ctrl-[": "indentLess", "Ctrl-]": "indentMore", + "Ctrl-U": "undoSelection", "Shift-Ctrl-U": "redoSelection", "Alt-U": "redoSelection", fallthrough: "basic" }; keyMap.macDefault = { @@ -3643,15 +4570,17 @@ window.CodeMirror = (function() { "Ctrl-Alt-Backspace": "delGroupAfter", "Alt-Delete": "delGroupAfter", "Cmd-S": "save", "Cmd-F": "find", "Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll", "Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delLineLeft", + "Cmd-U": "undoSelection", "Shift-Cmd-U": "redoSelection", fallthrough: ["basic", "emacsy"] }; - keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault; + // Very basic readline/emacs-style bindings, which are standard on Mac. keyMap.emacsy = { "Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown", "Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd", "Ctrl-V": "goPageDown", "Shift-Ctrl-V": "goPageUp", "Ctrl-D": "delCharAfter", "Ctrl-H": "delCharBefore", "Alt-D": "delWordAfter", "Alt-Backspace": "delWordBefore", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars" }; + keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault; // KEYMAP DISPATCH @@ -3660,7 +4589,11 @@ window.CodeMirror = (function() { else return val; } - function lookupKey(name, maps, handle) { + // Given an array of keymaps and a key name, call handle on any + // bindings found, until that returns a truthy value, at which point + // we consider the key handled. Implements things like binding a key + // to false stopping further handling and keymap fallthrough. + var lookupKey = CodeMirror.lookupKey = function(name, maps, handle) { function lookup(map) { map = getKeyMap(map); var found = map[name]; @@ -3672,7 +4605,7 @@ window.CodeMirror = (function() { if (fallthrough == null) return false; if (Object.prototype.toString.call(fallthrough) != "[object Array]") return lookup(fallthrough); - for (var i = 0, e = fallthrough.length; i < e; ++i) { + for (var i = 0; i < fallthrough.length; ++i) { var done = lookup(fallthrough[i]); if (done) return done; } @@ -3683,13 +4616,18 @@ window.CodeMirror = (function() { var done = lookup(maps[i]); if (done) return done != "stop"; } - } - function isModifierKey(event) { + }; + + // Modifier key presses don't count as 'real' key presses for the + // purpose of keymap fallthrough. + var isModifierKey = CodeMirror.isModifierKey = function(event) { var name = keyNames[event.keyCode]; return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod"; - } - function keyName(event, noShift) { - if (opera && event.keyCode == 34 && event["char"]) return false; + }; + + // Look up the name of a key as indicated by an event object. + var keyName = CodeMirror.keyName = function(event, noShift) { + if (presto && event.keyCode == 34 && event["char"]) return false; var name = keyNames[event.keyCode]; if (name == null || event.altGraphKey) return false; if (event.altKey) name = "Alt-" + name; @@ -3697,10 +4635,7 @@ window.CodeMirror = (function() { if (flipCtrlCmd ? event.ctrlKey : event.metaKey) name = "Cmd-" + name; if (!noShift && event.shiftKey) name = "Shift-" + name; return name; - } - CodeMirror.lookupKey = lookupKey; - CodeMirror.isModifierKey = isModifierKey; - CodeMirror.keyName = keyName; + }; // FROMTEXTAREA @@ -3714,9 +4649,7 @@ window.CodeMirror = (function() { // Set autofocus to true if this textarea is focused, or if it has // autofocus and no other element is focused. if (options.autofocus == null) { - var hasFocus = document.body; - // doc.activeElement occasionally throws on IE - try { hasFocus = document.activeElement; } catch(e) {} + var hasFocus = activeElt(); options.autofocus = hasFocus == textarea || textarea.getAttribute("autofocus") != null && hasFocus == document.body; } @@ -3762,14 +4695,13 @@ window.CodeMirror = (function() { // Fed to the mode parsers, provides helper functions to make // parsers more succinct. - // The character stream used by a mode's parser. - function StringStream(string, tabSize) { + var StringStream = CodeMirror.StringStream = function(string, tabSize) { this.pos = this.start = 0; this.string = string; this.tabSize = tabSize || 8; this.lastColumnPos = this.lastColumnValue = 0; this.lineStart = 0; - } + }; StringStream.prototype = { eol: function() {return this.pos >= this.string.length;}, @@ -3834,18 +4766,27 @@ window.CodeMirror = (function() { finally { this.lineStart -= n; } } }; - CodeMirror.StringStream = StringStream; // TEXTMARKERS - function TextMarker(doc, type) { + // Created with markText and setBookmark methods. A TextMarker is a + // handle that can be used to clear or find a marked position in the + // document. Line objects hold arrays (markedSpans) containing + // {from, to, marker} object pointing to such marker objects, and + // indicating that such a marker is present on that line. Multiple + // lines may point to the same marker when it spans across lines. + // The spans will have null for their from/to properties when the + // marker continues beyond the start/end of the line. Markers have + // links back to the lines they currently touch. + + var TextMarker = CodeMirror.TextMarker = function(doc, type) { this.lines = []; this.type = type; this.doc = doc; - } - CodeMirror.TextMarker = TextMarker; + }; eventMixin(TextMarker); + // Clear the marker. TextMarker.prototype.clear = function() { if (this.explicitlyCleared) return; var cm = this.doc.cm, withOp = cm && !cm.curOp; @@ -3858,15 +4799,17 @@ window.CodeMirror = (function() { for (var i = 0; i < this.lines.length; ++i) { var line = this.lines[i]; var span = getMarkedSpanFor(line.markedSpans, this); - if (span.to != null) max = lineNo(line); + if (cm && !this.collapsed) regLineChange(cm, lineNo(line), "text"); + else if (cm) { + if (span.to != null) max = lineNo(line); + if (span.from != null) min = lineNo(line); + } line.markedSpans = removeMarkedSpan(line.markedSpans, span); - if (span.from != null) - min = lineNo(line); - else if (this.collapsed && !lineIsHidden(this.doc, line) && cm) + if (span.from == null && this.collapsed && !lineIsHidden(this.doc, line) && cm) updateLineHeight(line, textHeight(cm.display)); } if (cm && this.collapsed && !cm.options.lineWrapping) for (var i = 0; i < this.lines.length; ++i) { - var visual = visualLine(cm.doc, this.lines[i]), len = lineLength(cm.doc, visual); + var visual = visualLine(this.lines[i]), len = lineLength(visual); if (len > cm.display.maxLineLength) { cm.display.maxLine = visual; cm.display.maxLineLength = len; @@ -3874,46 +4817,61 @@ window.CodeMirror = (function() { } } - if (min != null && cm) regChange(cm, min, max + 1); + if (min != null && cm && this.collapsed) regChange(cm, min, max + 1); this.lines.length = 0; this.explicitlyCleared = true; if (this.atomic && this.doc.cantEdit) { this.doc.cantEdit = false; - if (cm) reCheckSelection(cm); + if (cm) reCheckSelection(cm.doc); } + if (cm) signalLater(cm, "markerCleared", cm, this); if (withOp) endOperation(cm); }; - TextMarker.prototype.find = function(bothSides) { + // Find the position of the marker in the document. Returns a {from, + // to} object by default. Side can be passed to get a specific side + // -- 0 (both), -1 (left), or 1 (right). When lineObj is true, the + // Pos objects returned contain a line object, rather than a line + // number (used to prevent looking up the same line twice). + TextMarker.prototype.find = function(side, lineObj) { + if (side == null && this.type == "bookmark") side = 1; var from, to; for (var i = 0; i < this.lines.length; ++i) { var line = this.lines[i]; var span = getMarkedSpanFor(line.markedSpans, this); - if (span.from != null || span.to != null) { - var found = lineNo(line); - if (span.from != null) from = Pos(found, span.from); - if (span.to != null) to = Pos(found, span.to); + if (span.from != null) { + from = Pos(lineObj ? line : lineNo(line), span.from); + if (side == -1) return from; + } + if (span.to != null) { + to = Pos(lineObj ? line : lineNo(line), span.to); + if (side == 1) return to; } } - if (this.type == "bookmark" && !bothSides) return from; return from && {from: from, to: to}; }; + // Signals that the marker's widget changed, and surrounding layout + // should be recomputed. TextMarker.prototype.changed = function() { - var pos = this.find(), cm = this.doc.cm; + var pos = this.find(-1, true), widget = this, cm = this.doc.cm; if (!pos || !cm) return; - if (this.type != "bookmark") pos = pos.from; - var line = getLine(this.doc, pos.line); - clearCachedMeasurement(cm, line); - if (pos.line >= cm.display.showingFrom && pos.line < cm.display.showingTo) { - for (var node = cm.display.lineDiv.firstChild; node; node = node.nextSibling) if (node.lineObj == line) { - if (node.offsetHeight != line.height) updateLineHeight(line, node.offsetHeight); - break; + runInOp(cm, function() { + var line = pos.line, lineN = lineNo(pos.line); + var view = findViewForLine(cm, lineN); + if (view) { + clearLineMeasurementCacheFor(view); + cm.curOp.selectionChanged = cm.curOp.forceUpdate = true; } - runInOp(cm, function() { - cm.curOp.selectionChanged = cm.curOp.forceUpdate = cm.curOp.updateMaxLine = true; - }); - } + cm.curOp.updateMaxLine = true; + if (!lineIsHidden(widget.doc, line) && widget.height != null) { + var oldHeight = widget.height; + widget.height = null; + var dHeight = widgetHeight(widget) - oldHeight; + if (dHeight) + updateLineHeight(line, line.height + dHeight); + } + }); }; TextMarker.prototype.attachLine = function(line) { @@ -3932,20 +4890,31 @@ window.CodeMirror = (function() { } }; + // Collapsed markers have unique ids, in order to be able to order + // them, which is needed for uniquely determining an outer marker + // when they overlap (they may nest, but not partially overlap). var nextMarkerId = 0; + // Create a marker, wire it up to the right lines, and function markText(doc, from, to, options, type) { + // Shared markers (across linked documents) are handled separately + // (markTextShared will call out to this again, once per + // document). if (options && options.shared) return markTextShared(doc, from, to, options, type); + // Ensure we are in an operation. if (doc.cm && !doc.cm.curOp) return operation(doc.cm, markText)(doc, from, to, options, type); - var marker = new TextMarker(doc, type); + var marker = new TextMarker(doc, type), diff = cmp(from, to); if (options) copyObj(options, marker); - if (posLess(to, from) || posEq(from, to) && marker.clearWhenEmpty !== false) + // Don't connect empty markers unless clearWhenEmpty is false + if (diff > 0 || diff == 0 && marker.clearWhenEmpty !== false) return marker; if (marker.replacedWith) { + // Showing up as a widget implies collapsed (widget replaces text) marker.collapsed = true; - marker.replacedWith = elt("span", [marker.replacedWith], "CodeMirror-widget"); - if (!options.handleMouseEvents) marker.replacedWith.ignoreEvents = true; + marker.widgetNode = elt("span", [marker.replacedWith], "CodeMirror-widget"); + if (!options.handleMouseEvents) marker.widgetNode.ignoreEvents = true; + if (options.insertLeft) marker.widgetNode.insertLeft = true; } if (marker.collapsed) { if (conflictingCollapsedRange(doc, from.line, from, to, marker) || @@ -3955,20 +4924,19 @@ window.CodeMirror = (function() { } if (marker.addToHistory) - addToHistory(doc, {from: from, to: to, origin: "markText"}, - {head: doc.sel.head, anchor: doc.sel.anchor}, NaN); + addChangeToHistory(doc, {from: from, to: to, origin: "markText"}, doc.sel, NaN); var curLine = from.line, cm = doc.cm, updateMaxLine; doc.iter(curLine, to.line + 1, function(line) { - if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(doc, line) == cm.display.maxLine) + if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(line) == cm.display.maxLine) updateMaxLine = true; - var span = {from: null, to: null, marker: marker}; - if (curLine == from.line) span.from = from.ch; - if (curLine == to.line) span.to = to.ch; if (marker.collapsed && curLine != from.line) updateLineHeight(line, 0); - addMarkedSpan(line, span); + addMarkedSpan(line, new MarkedSpan(marker, + curLine == from.line ? from.ch : null, + curLine == to.line ? to.ch : null)); ++curLine; }); + // lineIsHidden depends on the presence of the spans, so needs a second pass if (marker.collapsed) doc.iter(from.line, to.line + 1, function(line) { if (lineIsHidden(doc, line)) updateLineHeight(line, 0); }); @@ -3985,25 +4953,31 @@ window.CodeMirror = (function() { marker.atomic = true; } if (cm) { + // Sync editor state if (updateMaxLine) cm.curOp.updateMaxLine = true; - if (marker.className || marker.title || marker.startStyle || marker.endStyle || marker.collapsed) + if (marker.collapsed) regChange(cm, from.line, to.line + 1); - if (marker.atomic) reCheckSelection(cm); + else if (marker.className || marker.title || marker.startStyle || marker.endStyle) + for (var i = from.line; i <= to.line; i++) regLineChange(cm, i, "text"); + if (marker.atomic) reCheckSelection(cm.doc); + signalLater(cm, "markerAdded", cm, marker); } return marker; } // SHARED TEXTMARKERS - function SharedTextMarker(markers, primary) { + // A shared marker spans multiple linked documents. It is + // implemented as a meta-marker-object controlling multiple normal + // markers. + var SharedTextMarker = CodeMirror.SharedTextMarker = function(markers, primary) { this.markers = markers; this.primary = primary; for (var i = 0, me = this; i < markers.length; ++i) { markers[i].parent = this; on(markers[i], "clear", function(){me.clear();}); } - } - CodeMirror.SharedTextMarker = SharedTextMarker; + }; eventMixin(SharedTextMarker); SharedTextMarker.prototype.clear = function() { @@ -4013,17 +4987,17 @@ window.CodeMirror = (function() { this.markers[i].clear(); signalLater(this, "clear"); }; - SharedTextMarker.prototype.find = function() { - return this.primary.find(); + SharedTextMarker.prototype.find = function(side, lineObj) { + return this.primary.find(side, lineObj); }; function markTextShared(doc, from, to, options, type) { options = copyObj(options); options.shared = false; var markers = [markText(doc, from, to, options, type)], primary = markers[0]; - var widget = options.replacedWith; + var widget = options.widgetNode; linkedDocs(doc, function(doc) { - if (widget) options.replacedWith = widget.cloneNode(true); + if (widget) options.widgetNode = widget.cloneNode(true); markers.push(markText(doc, clipPos(doc, from), clipPos(doc, to), options, type)); for (var i = 0; i < doc.linked.length; ++i) if (doc.linked[i].isParent) return; @@ -4034,56 +5008,71 @@ window.CodeMirror = (function() { // TEXTMARKER SPANS + function MarkedSpan(marker, from, to) { + this.marker = marker; + this.from = from; this.to = to; + } + + // Search an array of spans for a span matching the given marker. function getMarkedSpanFor(spans, marker) { if (spans) for (var i = 0; i < spans.length; ++i) { var span = spans[i]; if (span.marker == marker) return span; } } + // Remove a span from an array, returning undefined if no spans are + // left (we don't store arrays for lines without spans). function removeMarkedSpan(spans, span) { for (var r, i = 0; i < spans.length; ++i) if (spans[i] != span) (r || (r = [])).push(spans[i]); return r; } + // Add a span to a line. function addMarkedSpan(line, span) { line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span]; span.marker.attachLine(line); } + // Used for the algorithm that adjusts markers for a change in the + // document. These functions cut an array of spans at a given + // character position, returning an array of remaining chunks (or + // undefined if nothing remains). function markedSpansBefore(old, startCh, isInsert) { if (old) for (var i = 0, nw; i < old.length; ++i) { var span = old[i], marker = span.marker; var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh); if (startsBefore || span.from == startCh && marker.type == "bookmark" && (!isInsert || !span.marker.insertLeft)) { var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh); - (nw || (nw = [])).push({from: span.from, - to: endsAfter ? null : span.to, - marker: marker}); + (nw || (nw = [])).push(new MarkedSpan(marker, span.from, endsAfter ? null : span.to)); } } return nw; } - function markedSpansAfter(old, endCh, isInsert) { if (old) for (var i = 0, nw; i < old.length; ++i) { var span = old[i], marker = span.marker; var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh); if (endsAfter || span.from == endCh && marker.type == "bookmark" && (!isInsert || span.marker.insertLeft)) { var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh); - (nw || (nw = [])).push({from: startsBefore ? null : span.from - endCh, - to: span.to == null ? null : span.to - endCh, - marker: marker}); + (nw || (nw = [])).push(new MarkedSpan(marker, startsBefore ? null : span.from - endCh, + span.to == null ? null : span.to - endCh)); } } return nw; } + // Given a change object, compute the new set of marker spans that + // cover the line in which the change took place. Removes spans + // entirely within the change, reconnects spans belonging to the + // same marker that appear on both sides of the change, and cuts off + // spans partially within the change. Returns an array of span + // arrays with one element for each line in (after) the change. function stretchSpansOverChange(doc, change) { var oldFirst = isLine(doc, change.from.line) && getLine(doc, change.from.line).markedSpans; var oldLast = isLine(doc, change.to.line) && getLine(doc, change.to.line).markedSpans; if (!oldFirst && !oldLast) return null; - var startCh = change.from.ch, endCh = change.to.ch, isInsert = posEq(change.from, change.to); + var startCh = change.from.ch, endCh = change.to.ch, isInsert = cmp(change.from, change.to) == 0; // Get the spans that 'stick out' on both sides var first = markedSpansBefore(oldFirst, startCh, isInsert); var last = markedSpansAfter(oldLast, endCh, isInsert); @@ -4129,7 +5118,7 @@ window.CodeMirror = (function() { if (gap > 0 && first) for (var i = 0; i < first.length; ++i) if (first[i].to == null) - (gapMarkers || (gapMarkers = [])).push({from: null, to: null, marker: first[i].marker}); + (gapMarkers || (gapMarkers = [])).push(new MarkedSpan(first[i].marker, null, null)); for (var i = 0; i < gap; ++i) newMarkers.push(gapMarkers); newMarkers.push(last); @@ -4137,6 +5126,8 @@ window.CodeMirror = (function() { return newMarkers; } + // Remove spans that are empty and don't have a clearWhenEmpty + // option of false. function clearEmptySpans(spans) { for (var i = 0; i < spans.length; ++i) { var span = spans[i]; @@ -4147,6 +5138,10 @@ window.CodeMirror = (function() { return spans; } + // Used for un/re-doing changes from the history. Combines the + // result of computing the existing spans with the set of spans that + // existed in the history (so that deleting around a span and then + // undoing brings back the span). function mergeOldSpans(doc, change) { var old = getOldSpans(doc, change); var stretched = stretchSpansOverChange(doc, change); @@ -4169,6 +5164,7 @@ window.CodeMirror = (function() { return old; } + // Used to 'clip' out readOnly ranges when making a change. function removeReadOnlyRanges(doc, from, to) { var markers = null; doc.iter(from.line, to.line + 1, function(line) { @@ -4181,14 +5177,14 @@ window.CodeMirror = (function() { if (!markers) return null; var parts = [{from: from, to: to}]; for (var i = 0; i < markers.length; ++i) { - var mk = markers[i], m = mk.find(); + var mk = markers[i], m = mk.find(0); for (var j = 0; j < parts.length; ++j) { var p = parts[j]; - if (posLess(p.to, m.from) || posLess(m.to, p.from)) continue; - var newParts = [j, 1]; - if (posLess(p.from, m.from) || !mk.inclusiveLeft && posEq(p.from, m.from)) + if (cmp(p.to, m.from) < 0 || cmp(p.from, m.to) > 0) continue; + var newParts = [j, 1], dfrom = cmp(p.from, m.from), dto = cmp(p.to, m.to); + if (dfrom < 0 || !mk.inclusiveLeft && !dfrom) newParts.push({from: p.from, to: m.from}); - if (posLess(m.to, p.to) || !mk.inclusiveRight && posEq(p.to, m.to)) + if (dto > 0 || !mk.inclusiveRight && !dto) newParts.push({from: m.to, to: p.to}); parts.splice.apply(parts, newParts); j += newParts.length - 1; @@ -4197,9 +5193,29 @@ window.CodeMirror = (function() { return parts; } + // Connect or disconnect spans from a line. + function detachMarkedSpans(line) { + var spans = line.markedSpans; + if (!spans) return; + for (var i = 0; i < spans.length; ++i) + spans[i].marker.detachLine(line); + line.markedSpans = null; + } + function attachMarkedSpans(line, spans) { + if (!spans) return; + for (var i = 0; i < spans.length; ++i) + spans[i].marker.attachLine(line); + line.markedSpans = spans; + } + + // Helpers used when computing which overlapping collapsed span + // counts as the larger one. function extraLeft(marker) { return marker.inclusiveLeft ? -1 : 0; } function extraRight(marker) { return marker.inclusiveRight ? 1 : 0; } + // Returns a number indicating which of two overlapping collapsed + // spans is larger (and thus includes the other). Falls back to + // comparing ids when the spans cover exactly the same range. function compareCollapsedMarkers(a, b) { var lenDiff = a.lines.length - b.lines.length; if (lenDiff != 0) return lenDiff; @@ -4211,6 +5227,8 @@ window.CodeMirror = (function() { return b.id - a.id; } + // Find out whether a line ends or starts in a collapsed span. If + // so, return the marker for that span. function collapsedSpanAtSide(line, start) { var sps = sawCollapsedSpans && line.markedSpans, found; if (sps) for (var sp, i = 0; i < sps.length; ++i) { @@ -4224,13 +5242,16 @@ window.CodeMirror = (function() { function collapsedSpanAtStart(line) { return collapsedSpanAtSide(line, true); } function collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false); } + // Test whether there exists a collapsed span that partially + // overlaps (covers the start or end, but not both) of a new span. + // Such overlap is not allowed. function conflictingCollapsedRange(doc, lineNo, from, to, marker) { var line = getLine(doc, lineNo); var sps = sawCollapsedSpans && line.markedSpans; if (sps) for (var i = 0; i < sps.length; ++i) { var sp = sps[i]; if (!sp.marker.collapsed) continue; - var found = sp.marker.find(true); + var found = sp.marker.find(0); var fromCmp = cmp(found.from, from) || extraLeft(sp.marker) - extraLeft(marker); var toCmp = cmp(found.to, to) || extraRight(sp.marker) - extraRight(marker); if (fromCmp >= 0 && toCmp <= 0 || fromCmp <= 0 && toCmp >= 0) continue; @@ -4240,57 +5261,80 @@ window.CodeMirror = (function() { } } - function visualLine(doc, line) { + // A visual line is a line as drawn on the screen. Folding, for + // example, can cause multiple logical lines to appear on the same + // visual line. This finds the start of the visual line that the + // given line is part of (usually that is the line itself). + function visualLine(line) { var merged; while (merged = collapsedSpanAtStart(line)) - line = getLine(doc, merged.find().from.line); + line = merged.find(-1, true).line; return line; } + // Returns an array of logical lines that continue the visual line + // started by the argument, or undefined if there are no such lines. + function visualLineContinued(line) { + var merged, lines; + while (merged = collapsedSpanAtEnd(line)) { + line = merged.find(1, true).line; + (lines || (lines = [])).push(line); + } + return lines; + } + + // Get the line number of the start of the visual line that the + // given line number is part of. + function visualLineNo(doc, lineN) { + var line = getLine(doc, lineN), vis = visualLine(line); + if (line == vis) return lineN; + return lineNo(vis); + } + // Get the line number of the start of the next visual line after + // the given line. + function visualLineEndNo(doc, lineN) { + if (lineN > doc.lastLine()) return lineN; + var line = getLine(doc, lineN), merged; + if (!lineIsHidden(doc, line)) return lineN; + while (merged = collapsedSpanAtEnd(line)) + line = merged.find(1, true).line; + return lineNo(line) + 1; + } + + // Compute whether a line is hidden. Lines count as hidden when they + // are part of a visual line that starts with another line, or when + // they are entirely covered by collapsed, non-widget span. function lineIsHidden(doc, line) { var sps = sawCollapsedSpans && line.markedSpans; if (sps) for (var sp, i = 0; i < sps.length; ++i) { sp = sps[i]; if (!sp.marker.collapsed) continue; if (sp.from == null) return true; - if (sp.marker.replacedWith) continue; + if (sp.marker.widgetNode) continue; if (sp.from == 0 && sp.marker.inclusiveLeft && lineIsHiddenInner(doc, line, sp)) return true; } } function lineIsHiddenInner(doc, line, span) { if (span.to == null) { - var end = span.marker.find().to, endLine = getLine(doc, end.line); - return lineIsHiddenInner(doc, endLine, getMarkedSpanFor(endLine.markedSpans, span.marker)); + var end = span.marker.find(1, true); + return lineIsHiddenInner(doc, end.line, getMarkedSpanFor(end.line.markedSpans, span.marker)); } if (span.marker.inclusiveRight && span.to == line.text.length) return true; for (var sp, i = 0; i < line.markedSpans.length; ++i) { sp = line.markedSpans[i]; - if (sp.marker.collapsed && !sp.marker.replacedWith && sp.from == span.to && + if (sp.marker.collapsed && !sp.marker.widgetNode && sp.from == span.to && (sp.to == null || sp.to != span.from) && (sp.marker.inclusiveLeft || span.marker.inclusiveRight) && lineIsHiddenInner(doc, line, sp)) return true; } } - function detachMarkedSpans(line) { - var spans = line.markedSpans; - if (!spans) return; - for (var i = 0; i < spans.length; ++i) - spans[i].marker.detachLine(line); - line.markedSpans = null; - } - - function attachMarkedSpans(line, spans) { - if (!spans) return; - for (var i = 0; i < spans.length; ++i) - spans[i].marker.attachLine(line); - line.markedSpans = spans; - } - // LINE WIDGETS + // Line widgets are block elements displayed above or below a line. + var LineWidget = CodeMirror.LineWidget = function(cm, node, options) { if (options) for (var opt in options) if (options.hasOwnProperty(opt)) this[opt] = options[opt]; @@ -4298,38 +5342,39 @@ window.CodeMirror = (function() { this.node = node; }; eventMixin(LineWidget); - function widgetOperation(f) { - return function() { - var withOp = !this.cm.curOp; - if (withOp) startOperation(this.cm); - try {var result = f.apply(this, arguments);} - finally {if (withOp) endOperation(this.cm);} - return result; - }; + + function adjustScrollWhenAboveVisible(cm, line, diff) { + if (heightAtLine(line) < ((cm.curOp && cm.curOp.scrollTop) || cm.doc.scrollTop)) + addToScrollPos(cm, null, diff); } - LineWidget.prototype.clear = widgetOperation(function() { - var ws = this.line.widgets, no = lineNo(this.line); + + LineWidget.prototype.clear = function() { + var cm = this.cm, ws = this.line.widgets, line = this.line, no = lineNo(line); if (no == null || !ws) return; for (var i = 0; i < ws.length; ++i) if (ws[i] == this) ws.splice(i--, 1); - if (!ws.length) this.line.widgets = null; - var aboveVisible = heightAtLine(this.cm, this.line) < this.cm.doc.scrollTop; - updateLineHeight(this.line, Math.max(0, this.line.height - widgetHeight(this))); - if (aboveVisible) addToScrollPos(this.cm, 0, -this.height); - regChange(this.cm, no, no + 1); - }); - LineWidget.prototype.changed = widgetOperation(function() { - var oldH = this.height; + if (!ws.length) line.widgets = null; + var height = widgetHeight(this); + runInOp(cm, function() { + adjustScrollWhenAboveVisible(cm, line, -height); + regLineChange(cm, no, "widget"); + updateLineHeight(line, Math.max(0, line.height - height)); + }); + }; + LineWidget.prototype.changed = function() { + var oldH = this.height, cm = this.cm, line = this.line; this.height = null; var diff = widgetHeight(this) - oldH; if (!diff) return; - updateLineHeight(this.line, this.line.height + diff); - var no = lineNo(this.line); - regChange(this.cm, no, no + 1); - }); + runInOp(cm, function() { + cm.curOp.forceUpdate = true; + adjustScrollWhenAboveVisible(cm, line, diff); + updateLineHeight(line, line.height + diff); + }); + }; function widgetHeight(widget) { if (widget.height != null) return widget.height; - if (!widget.node.parentNode || widget.node.parentNode.nodeType != 1) + if (!contains(document.body, widget.node)) removeChildrenAndAdd(widget.cm.display.measure, elt("div", [widget.node], null, "position: relative")); return widget.height = widget.node.offsetHeight; } @@ -4337,15 +5382,16 @@ window.CodeMirror = (function() { function addLineWidget(cm, handle, node, options) { var widget = new LineWidget(cm, node, options); if (widget.noHScroll) cm.display.alignWidgets = true; - changeLine(cm, handle, function(line) { + changeLine(cm, handle, "widget", function(line) { var widgets = line.widgets || (line.widgets = []); if (widget.insertAt == null) widgets.push(widget); else widgets.splice(Math.min(widgets.length - 1, Math.max(0, widget.insertAt)), 0, widget); widget.line = line; - if (!lineIsHidden(cm.doc, line) || widget.showIfHidden) { - var aboveVisible = heightAtLine(cm, line) < cm.doc.scrollTop; + if (!lineIsHidden(cm.doc, line)) { + var aboveVisible = heightAtLine(line) < cm.doc.scrollTop; updateLineHeight(line, line.height + widgetHeight(widget)); - if (aboveVisible) addToScrollPos(cm, 0, widget.height); + if (aboveVisible) addToScrollPos(cm, null, widget.height); + cm.curOp.forceUpdate = true; } return true; }); @@ -4364,6 +5410,9 @@ window.CodeMirror = (function() { eventMixin(Line); Line.prototype.lineNo = function() { return lineNo(this); }; + // Change the content (text, markers) of a line. Automatically + // invalidates cached information and tries to re-estimate the + // line's height. function updateLine(line, text, markedSpans, estimateHeight) { line.text = text; if (line.stateAfter) line.stateAfter = null; @@ -4375,14 +5424,13 @@ window.CodeMirror = (function() { if (estHeight != line.height) updateLineHeight(line, estHeight); } + // Detach a line from the document tree and its markers. function cleanUpLine(line) { line.parent = null; detachMarkedSpans(line); } - // Run the given mode's parser over a line, update the styles - // array, which contains alternating fragments of text and CSS - // classes. + // Run the given mode's parser over a line, calling f for each token. function runMode(cm, text, mode, state, f, forceToEnd) { var flattenSpans = mode.flattenSpans; if (flattenSpans == null) flattenSpans = cm.options.flattenSpans; @@ -4416,6 +5464,10 @@ window.CodeMirror = (function() { } } + // Compute a style array (an array starting with a mode generation + // -- for invalidation -- followed by pairs of end positions and + // style strings), which is used to highlight the tokens on the + // line. function highlightLine(cm, line, state, forceToEnd) { // A styles array always starts with a number identifying the // mode/overlays that it is based on (for easy invalidation). @@ -4461,7 +5513,8 @@ window.CodeMirror = (function() { } // Lightweight form of highlight -- proceed over this line and - // update state, but don't save a style array. + // update state, but don't save a style array. Used for lines that + // aren't currently visible. function processLine(cm, text, state, startAt) { var mode = cm.doc.mode; var stream = new StringStream(text, cm.options.tabSize); @@ -4473,11 +5526,14 @@ window.CodeMirror = (function() { } } + // Convert a style as returned by a mode (either null, or a string + // containing one or more styles) to a CSS style. This is cached, + // and also looks for line-wide styles. var styleToClassCache = {}, styleToClassCacheWithMode = {}; function interpretTokenStyle(style, builder) { if (!style) return null; for (;;) { - var lineClass = style.match(/(?:^|\s)line-(background-)?(\S+)/); + var lineClass = style.match(/(?:^|\s+)line-(background-)?(\S+)/); if (!lineClass) break; style = style.slice(0, lineClass.index) + style.slice(lineClass.index + lineClass[0].length); var prop = lineClass[1] ? "bgClass" : "textClass"; @@ -4486,59 +5542,54 @@ window.CodeMirror = (function() { else if (!(new RegExp("(?:^|\s)" + lineClass[2] + "(?:$|\s)")).test(builder[prop])) builder[prop] += " " + lineClass[2]; } + if (/^\s*$/.test(style)) return null; var cache = builder.cm.options.addModeClass ? styleToClassCacheWithMode : styleToClassCache; return cache[style] || - (cache[style] = "cm-" + style.replace(/ +/g, " cm-")); + (cache[style] = style.replace(/\S+/g, "cm-$&")); } - function buildLineContent(cm, realLine, measure, copyWidgets) { - var merged, line = realLine, empty = true; - while (merged = collapsedSpanAtStart(line)) - line = getLine(cm.doc, merged.find().from.line); + // Render the DOM representation of the text of a line. Also builds + // up a 'line map', which points at the DOM nodes that represent + // specific stretches of text, and is used by the measuring code. + // The returned object contains the DOM node, this map, and + // information about line-wide styles that were set by the mode. + function buildLineContent(cm, lineView) { + // The padding-right forces the element to have a 'border', which + // is needed on Webkit to be able to get line-level bounding + // rectangles for it (in measureChar). + var content = elt("span", null, null, webkit ? "padding-right: .1px" : null); + var builder = {pre: elt("pre", [content]), content: content, col: 0, pos: 0, cm: cm}; + lineView.measure = {}; - var builder = {pre: elt("pre"), col: 0, pos: 0, - measure: null, measuredSomething: false, cm: cm, - copyWidgets: copyWidgets}; - - do { - if (line.text) empty = false; - builder.measure = line == realLine && measure; + // Iterate over the logical lines that make up this visual line. + for (var i = 0; i <= (lineView.rest ? lineView.rest.length : 0); i++) { + var line = i ? lineView.rest[i - 1] : lineView.line, order; builder.pos = 0; - builder.addToken = builder.measure ? buildTokenMeasure : buildToken; - if ((old_ie || webkit) && cm.getOption("lineWrapping")) + builder.addToken = buildToken; + // Optionally wire in some hacks into the token-rendering + // algorithm, to deal with browser quirks. + if ((ie || webkit) && cm.getOption("lineWrapping")) builder.addToken = buildTokenSplitSpaces(builder.addToken); - var next = insertLineContent(line, builder, getLineStyles(cm, line)); - if (measure && line == realLine && !builder.measuredSomething) { - measure[0] = builder.pre.appendChild(zeroWidthElement(cm.display.measure)); - builder.measuredSomething = true; - } - if (next) line = getLine(cm.doc, next.to.line); - } while (next); + if (hasBadBidiRects(cm.display.measure) && (order = getOrder(line))) + builder.addToken = buildTokenBadBidi(builder.addToken, order); + builder.map = []; + insertLineContent(line, builder, getLineStyles(cm, line)); - if (measure && !builder.measuredSomething && !measure[0]) - measure[0] = builder.pre.appendChild(empty ? elt("span", "\u00a0") : zeroWidthElement(cm.display.measure)); - if (!builder.pre.firstChild && !lineIsHidden(cm.doc, realLine)) - builder.pre.appendChild(document.createTextNode("\u00a0")); + // Ensure at least a single node is present, for measuring. + if (builder.map.length == 0) + builder.map.push(0, 0, builder.content.appendChild(zeroWidthElement(cm.display.measure))); - var order; - // Work around problem with the reported dimensions of single-char - // direction spans on IE (issue #1129). See also the comment in - // cursorCoords. - if (measure && ie && (order = getOrder(line))) { - var l = order.length - 1; - if (order[l].from == order[l].to) --l; - var last = order[l], prev = order[l - 1]; - if (last.from + 1 == last.to && prev && last.level < prev.level) { - var span = measure[builder.pos - 1]; - if (span) span.parentNode.insertBefore(span.measureRight = zeroWidthElement(cm.display.measure), - span.nextSibling); + // Store the map and a cache object for the current logical line + if (i == 0) { + lineView.measure.map = builder.map; + lineView.measure.cache = {}; + } else { + (lineView.measure.maps || (lineView.measure.maps = [])).push(builder.map); + (lineView.measure.caches || (lineView.measure.caches = [])).push({}); } } - var textClass = builder.textClass ? builder.textClass + " " + (realLine.textClass || "") : realLine.textClass; - if (textClass) builder.pre.className = textClass; - - signal(cm, "renderLine", cm, realLine, builder.pre); + signal(cm, "renderLine", cm, lineView.line, builder.pre); return builder; } @@ -4548,12 +5599,17 @@ window.CodeMirror = (function() { return token; } + // Build up the DOM representation for a single token, and add it to + // the line map. Takes care to render special characters separately. function buildToken(builder, text, style, startStyle, endStyle, title) { if (!text) return; - var special = builder.cm.options.specialChars; + var special = builder.cm.options.specialChars, mustWrap = false; if (!special.test(text)) { builder.col += text.length; var content = document.createTextNode(text); + builder.map.push(builder.pos, builder.pos + text.length, content); + if (ie_upto8) mustWrap = true; + builder.pos += text.length; } else { var content = document.createDocumentFragment(), pos = 0; while (true) { @@ -4561,56 +5617,38 @@ window.CodeMirror = (function() { var m = special.exec(text); var skipped = m ? m.index - pos : text.length - pos; if (skipped) { - content.appendChild(document.createTextNode(text.slice(pos, pos + skipped))); + var txt = document.createTextNode(text.slice(pos, pos + skipped)); + if (ie_upto8) content.appendChild(elt("span", [txt])); + else content.appendChild(txt); + builder.map.push(builder.pos, builder.pos + skipped, txt); builder.col += skipped; + builder.pos += skipped; } if (!m) break; pos += skipped + 1; if (m[0] == "\t") { var tabSize = builder.cm.options.tabSize, tabWidth = tabSize - builder.col % tabSize; - content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab")); + var txt = content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab")); builder.col += tabWidth; } else { - var token = builder.cm.options.specialCharPlaceholder(m[0]); - content.appendChild(token); + var txt = builder.cm.options.specialCharPlaceholder(m[0]); + if (ie_upto8) content.appendChild(elt("span", [txt])); + else content.appendChild(txt); builder.col += 1; } + builder.map.push(builder.pos, builder.pos + 1, txt); + builder.pos++; } } - if (style || startStyle || endStyle || builder.measure) { + if (style || startStyle || endStyle || mustWrap) { var fullStyle = style || ""; if (startStyle) fullStyle += startStyle; if (endStyle) fullStyle += endStyle; var token = elt("span", [content], fullStyle); if (title) token.title = title; - return builder.pre.appendChild(token); + return builder.content.appendChild(token); } - builder.pre.appendChild(content); - } - - function buildTokenMeasure(builder, text, style, startStyle, endStyle) { - var wrapping = builder.cm.options.lineWrapping; - for (var i = 0; i < text.length; ++i) { - var start = i == 0, to = i + 1; - while (to < text.length && isExtendingChar(text.charAt(to))) ++to; - var ch = text.slice(i, to); - i = to - 1; - if (i && wrapping && spanAffectsWrapping(text, i)) - builder.pre.appendChild(elt("wbr")); - var old = builder.measure[builder.pos]; - var span = builder.measure[builder.pos] = - buildToken(builder, ch, style, - start && startStyle, i == text.length - 1 && endStyle); - if (old) span.leftSide = old.leftSide || old; - // In IE single-space nodes wrap differently than spaces - // embedded in larger text nodes, except when set to - // white-space: normal (issue #1268). - if (old_ie && wrapping && ch == " " && i && !/\s/.test(text.charAt(i - 1)) && - i < text.length - 1 && !/\s/.test(text.charAt(i + 1))) - span.style.whiteSpace = "normal"; - builder.pos += ch.length; - } - if (text.length) builder.measuredSomething = true; + builder.content.appendChild(content); } function buildTokenSplitSpaces(inner) { @@ -4621,29 +5659,36 @@ window.CodeMirror = (function() { return out; } return function(builder, text, style, startStyle, endStyle, title) { - return inner(builder, text.replace(/ {3,}/g, split), style, startStyle, endStyle, title); + inner(builder, text.replace(/ {3,}/g, split), style, startStyle, endStyle, title); + }; + } + + // Work around nonsense dimensions being reported for stretches of + // right-to-left text. + function buildTokenBadBidi(inner, order) { + return function(builder, text, style, startStyle, endStyle, title) { + style = style ? style + " cm-force-border" : "cm-force-border"; + var start = builder.pos, end = start + text.length; + for (;;) { + // Find the part that overlaps with the start of this text + for (var i = 0; i < order.length; i++) { + var part = order[i]; + if (part.to > start && part.from <= start) break; + } + if (part.to >= end) return inner(builder, text, style, startStyle, endStyle, title); + inner(builder, text.slice(0, part.to - start), style, startStyle, null, title); + startStyle = null; + text = text.slice(part.to - start); + start = part.to; + } }; } function buildCollapsedSpan(builder, size, marker, ignoreWidget) { - var widget = !ignoreWidget && marker.replacedWith; + var widget = !ignoreWidget && marker.widgetNode; if (widget) { - if (builder.copyWidgets) widget = widget.cloneNode(true); - builder.pre.appendChild(widget); - if (builder.measure) { - if (size) { - builder.measure[builder.pos] = widget; - } else { - var elt = zeroWidthElement(builder.cm.display.measure); - if (marker.type == "bookmark" && !marker.insertLeft) - builder.measure[builder.pos] = builder.pre.appendChild(elt); - else if (builder.measure[builder.pos]) - return; - else - builder.measure[builder.pos] = builder.pre.insertBefore(elt, widget); - } - builder.measuredSomething = true; - } + builder.map.push(builder.pos, builder.pos + size, widget); + builder.content.appendChild(widget); } builder.pos += size; } @@ -4678,12 +5723,12 @@ window.CodeMirror = (function() { } else if (sp.from > pos && nextChange > sp.from) { nextChange = sp.from; } - if (m.type == "bookmark" && sp.from == pos && m.replacedWith) foundBookmarks.push(m); + if (m.type == "bookmark" && sp.from == pos && m.widgetNode) foundBookmarks.push(m); } if (collapsed && (collapsed.from || 0) == pos) { - buildCollapsedSpan(builder, (collapsed.to == null ? len : collapsed.to) - pos, + buildCollapsedSpan(builder, (collapsed.to == null ? len + 1 : collapsed.to) - pos, collapsed.marker, collapsed.from == null); - if (collapsed.to == null) return collapsed.marker.find(); + if (collapsed.to == null) return; } if (!collapsed && foundBookmarks.length) for (var j = 0; j < foundBookmarks.length; ++j) buildCollapsedSpan(builder, 0, foundBookmarks[j]); @@ -4711,7 +5756,16 @@ window.CodeMirror = (function() { // DOCUMENT DATA STRUCTURE - function updateDoc(doc, change, markedSpans, selAfter, estimateHeight) { + // By default, updates that start and end at the beginning of a line + // are treated specially, in order to make the association of line + // widgets and marker elements with the text behave more intuitive. + function isWholeLineUpdate(doc, change) { + return change.from.ch == 0 && change.to.ch == 0 && lst(change.text) == "" && + (!doc.cm || doc.cm.options.wholeLineUpdateBefore); + } + + // Perform a change on the document data structure. + function updateDoc(doc, change, markedSpans, estimateHeight) { function spansFor(n) {return markedSpans ? markedSpans[n] : null;} function update(line, text, spans) { updateLine(line, text, spans, estimateHeight); @@ -4722,12 +5776,11 @@ window.CodeMirror = (function() { var firstLine = getLine(doc, from.line), lastLine = getLine(doc, to.line); var lastText = lst(text), lastSpans = spansFor(text.length - 1), nlines = to.line - from.line; - // First adjust the line structure - if (from.ch == 0 && to.ch == 0 && lastText == "" && - (!doc.cm || doc.cm.options.wholeLineUpdateBefore)) { + // Adjust the line structure + if (isWholeLineUpdate(doc, change)) { // This is a whole-line replace. Treated specially to make // sure line objects move the way they are supposed to. - for (var i = 0, e = text.length - 1, added = []; i < e; ++i) + for (var i = 0, added = []; i < text.length - 1; ++i) added.push(new Line(text[i], spansFor(i), estimateHeight)); update(lastLine, lastLine.text, lastSpans); if (nlines) doc.remove(from.line, nlines); @@ -4736,7 +5789,7 @@ window.CodeMirror = (function() { if (text.length == 1) { update(firstLine, firstLine.text.slice(0, from.ch) + lastText + firstLine.text.slice(to.ch), lastSpans); } else { - for (var added = [], i = 1, e = text.length - 1; i < e; ++i) + for (var added = [], i = 1; i < text.length - 1; ++i) added.push(new Line(text[i], spansFor(i), estimateHeight)); added.push(new Line(lastText + firstLine.text.slice(to.ch), lastSpans, estimateHeight)); update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); @@ -4748,20 +5801,32 @@ window.CodeMirror = (function() { } else { update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); update(lastLine, lastText + lastLine.text.slice(to.ch), lastSpans); - for (var i = 1, e = text.length - 1, added = []; i < e; ++i) + for (var i = 1, added = []; i < text.length - 1; ++i) added.push(new Line(text[i], spansFor(i), estimateHeight)); if (nlines > 1) doc.remove(from.line + 1, nlines - 1); doc.insert(from.line + 1, added); } signalLater(doc, "change", doc, change); - setSelection(doc, selAfter.anchor, selAfter.head, null, true); } + // The document is represented as a BTree consisting of leaves, with + // chunk of lines in them, and branches, with up to ten leaves or + // other branch nodes below them. The top node is always a branch + // node, and is the document object itself (meaning it has + // additional methods and properties). + // + // All nodes have parent links. The tree is used both to go from + // line numbers to line objects, and to go from objects to numbers. + // It also indexes by height, and is used to convert between height + // and line object, and to find the total height of the document. + // + // See also http://marijnhaverbeke.nl/blog/codemirror-line-tree.html + function LeafChunk(lines) { this.lines = lines; this.parent = null; - for (var i = 0, e = lines.length, height = 0; i < e; ++i) { + for (var i = 0, height = 0; i < lines.length; ++i) { lines[i].parent = this; height += lines[i].height; } @@ -4770,6 +5835,7 @@ window.CodeMirror = (function() { LeafChunk.prototype = { chunkSize: function() { return this.lines.length; }, + // Remove the n lines at offset 'at'. removeInner: function(at, n) { for (var i = at, e = at + n; i < e; ++i) { var line = this.lines[i]; @@ -4779,14 +5845,18 @@ window.CodeMirror = (function() { } this.lines.splice(at, n); }, + // Helper used to collapse a small branch into a single leaf. collapse: function(lines) { - lines.splice.apply(lines, [lines.length, 0].concat(this.lines)); + lines.push.apply(lines, this.lines); }, + // Insert the given array of lines at offset 'at', count them as + // having the given height. insertInner: function(at, lines, height) { this.height += height; this.lines = this.lines.slice(0, at).concat(lines).concat(this.lines.slice(at)); - for (var i = 0, e = lines.length; i < e; ++i) lines[i].parent = this; + for (var i = 0; i < lines.length; ++i) lines[i].parent = this; }, + // Used to iterate over a part of the tree. iterN: function(at, n, op) { for (var e = at + n; at < e; ++at) if (op(this.lines[at])) return true; @@ -4796,7 +5866,7 @@ window.CodeMirror = (function() { function BranchChunk(children) { this.children = children; var size = 0, height = 0; - for (var i = 0, e = children.length; i < e; ++i) { + for (var i = 0; i < children.length; ++i) { var ch = children[i]; size += ch.chunkSize(); height += ch.height; ch.parent = this; @@ -4821,7 +5891,10 @@ window.CodeMirror = (function() { at = 0; } else at -= sz; } - if (this.size - n < 25) { + // If the result is smaller than 25 lines, ensure that it is a + // single leaf node. + if (this.size - n < 25 && + (this.children.length > 1 || !(this.children[0] instanceof LeafChunk))) { var lines = []; this.collapse(lines); this.children = [new LeafChunk(lines)]; @@ -4829,12 +5902,12 @@ window.CodeMirror = (function() { } }, collapse: function(lines) { - for (var i = 0, e = this.children.length; i < e; ++i) this.children[i].collapse(lines); + for (var i = 0; i < this.children.length; ++i) this.children[i].collapse(lines); }, insertInner: function(at, lines, height) { this.size += lines.length; this.height += height; - for (var i = 0, e = this.children.length; i < e; ++i) { + for (var i = 0; i < this.children.length; ++i) { var child = this.children[i], sz = child.chunkSize(); if (at <= sz) { child.insertInner(at, lines, height); @@ -4853,6 +5926,7 @@ window.CodeMirror = (function() { at -= sz; } }, + // When a node has grown, check whether it should be split. maybeSpill: function() { if (this.children.length <= 10) return; var me = this; @@ -4875,7 +5949,7 @@ window.CodeMirror = (function() { me.parent.maybeSpill(); }, iterN: function(at, n, op) { - for (var i = 0, e = this.children.length; i < e; ++i) { + for (var i = 0; i < this.children.length; ++i) { var child = this.children[i], sz = child.chunkSize(); if (at < sz) { var used = Math.min(n, sz - at); @@ -4896,43 +5970,52 @@ window.CodeMirror = (function() { this.first = firstLine; this.scrollTop = this.scrollLeft = 0; this.cantEdit = false; - this.history = makeHistory(); this.cleanGeneration = 1; this.frontier = firstLine; var start = Pos(firstLine, 0); - this.sel = {from: start, to: start, head: start, anchor: start, shift: false, extend: false, goalColumn: null}; + this.sel = simpleSelection(start); + this.history = new History(null); this.id = ++nextDocId; this.modeOption = mode; if (typeof text == "string") text = splitLines(text); - updateDoc(this, {from: start, to: start, text: text}, null, {head: start, anchor: start}); + updateDoc(this, {from: start, to: start, text: text}); + setSelection(this, simpleSelection(start), sel_dontScroll); }; Doc.prototype = createObj(BranchChunk.prototype, { constructor: Doc, + // Iterate over the document. Supports two forms -- with only one + // argument, it calls that for each line in the document. With + // three, it iterates over the range given by the first two (with + // the second being non-inclusive). iter: function(from, to, op) { if (op) this.iterN(from - this.first, to - from, op); else this.iterN(this.first, this.first + this.size, from); }, + // Non-public interface for adding and removing lines. insert: function(at, lines) { var height = 0; - for (var i = 0, e = lines.length; i < e; ++i) height += lines[i].height; + for (var i = 0; i < lines.length; ++i) height += lines[i].height; this.insertInner(at - this.first, lines, height); }, remove: function(at, n) { this.removeInner(at - this.first, n); }, + // From here, the methods are part of the public interface. Most + // are also available from CodeMirror (editor) instances. + getValue: function(lineSep) { var lines = getLines(this, this.first, this.first + this.size); if (lineSep === false) return lines; return lines.join(lineSep || "\n"); }, - setValue: function(code) { + setValue: docMethodOp(function(code) { var top = Pos(this.first, 0), last = this.first + this.size - 1; makeChange(this, {from: top, to: Pos(last, getLine(this, last).text.length), - text: splitLines(code), origin: "setValue"}, - {head: top, anchor: top}, true); - }, + text: splitLines(code), origin: "setValue"}, true); + setSelection(this, simpleSelection(top)); + }), replaceRange: function(code, from, to, origin) { from = clipPos(this, from); to = to ? clipPos(this, to) : from; @@ -4945,21 +6028,13 @@ window.CodeMirror = (function() { }, getLine: function(line) {var l = this.getLineHandle(line); return l && l.text;}, - setLine: function(line, text) { - if (isLine(this, line)) - replaceRange(this, text, Pos(line, 0), clipPos(this, Pos(line))); - }, - removeLine: function(line) { - if (line) replaceRange(this, "", clipPos(this, Pos(line - 1)), clipPos(this, Pos(line))); - else replaceRange(this, "", Pos(0, 0), clipPos(this, Pos(1, 0))); - }, getLineHandle: function(line) {if (isLine(this, line)) return getLine(this, line);}, getLineNumber: function(line) {return lineNo(line);}, getLineHandleVisualStart: function(line) { if (typeof line == "number") line = getLine(this, line); - return visualLine(this, line); + return visualLine(line); }, lineCount: function() {return this.size;}, @@ -4969,41 +6044,96 @@ window.CodeMirror = (function() { clipPos: function(pos) {return clipPos(this, pos);}, getCursor: function(start) { - var sel = this.sel, pos; - if (start == null || start == "head") pos = sel.head; - else if (start == "anchor") pos = sel.anchor; - else if (start == "end" || start === false) pos = sel.to; - else pos = sel.from; - return copyPos(pos); + var range = this.sel.primary(), pos; + if (start == null || start == "head") pos = range.head; + else if (start == "anchor") pos = range.anchor; + else if (start == "end" || start == "to" || start === false) pos = range.to(); + else pos = range.from(); + return pos; }, - somethingSelected: function() {return !posEq(this.sel.head, this.sel.anchor);}, + listSelections: function() { return this.sel.ranges; }, + somethingSelected: function() {return this.sel.somethingSelected();}, - setCursor: docOperation(function(line, ch, extend) { - var pos = clipPos(this, typeof line == "number" ? Pos(line, ch || 0) : line); - if (extend) extendSelection(this, pos); - else setSelection(this, pos, pos); + setCursor: docMethodOp(function(line, ch, options) { + setSimpleSelection(this, clipPos(this, typeof line == "number" ? Pos(line, ch || 0) : line), null, options); }), - setSelection: docOperation(function(anchor, head, bias) { - setSelection(this, clipPos(this, anchor), clipPos(this, head || anchor), bias); + setSelection: docMethodOp(function(anchor, head, options) { + setSimpleSelection(this, clipPos(this, anchor), clipPos(this, head || anchor), options); }), - extendSelection: docOperation(function(from, to, bias) { - extendSelection(this, clipPos(this, from), to && clipPos(this, to), bias); + extendSelection: docMethodOp(function(head, other, options) { + extendSelection(this, clipPos(this, head), other && clipPos(this, other), options); + }), + extendSelections: docMethodOp(function(heads, options) { + extendSelections(this, clipPosArray(this, heads, options)); + }), + extendSelectionsBy: docMethodOp(function(f, options) { + extendSelections(this, map(this.sel.ranges, f), options); + }), + setSelections: docMethodOp(function(ranges, primary, options) { + if (!ranges.length) return; + for (var i = 0, out = []; i < ranges.length; i++) + out[i] = new Range(clipPos(this, ranges[i].anchor), + clipPos(this, ranges[i].head)); + if (primary == null) primary = Math.min(ranges.length - 1, this.sel.primIndex); + setSelection(this, normalizeSelection(out, primary), options); + }), + addSelection: docMethodOp(function(anchor, head, options) { + var ranges = this.sel.ranges.slice(0); + ranges.push(new Range(clipPos(this, anchor), clipPos(this, head || anchor))); + setSelection(this, normalizeSelection(ranges, ranges.length - 1), options); }), - getSelection: function(lineSep) {return this.getRange(this.sel.from, this.sel.to, lineSep);}, - replaceSelection: function(code, collapse, origin) { - makeChange(this, {from: this.sel.from, to: this.sel.to, text: splitLines(code), origin: origin}, collapse || "around"); + getSelection: function(lineSep) { + var ranges = this.sel.ranges, lines; + for (var i = 0; i < ranges.length; i++) { + var sel = getBetween(this, ranges[i].from(), ranges[i].to()); + lines = lines ? lines.concat(sel) : sel; + } + if (lineSep === false) return lines; + else return lines.join(lineSep || "\n"); }, - undo: docOperation(function() {makeChangeFromHistory(this, "undo");}), - redo: docOperation(function() {makeChangeFromHistory(this, "redo");}), + getSelections: function(lineSep) { + var parts = [], ranges = this.sel.ranges; + for (var i = 0; i < ranges.length; i++) { + var sel = getBetween(this, ranges[i].from(), ranges[i].to()); + if (lineSep !== false) sel = sel.join(lineSep || "\n"); + parts[i] = sel; + } + return parts; + }, + replaceSelection: docMethodOp(function(code, collapse, origin) { + var dup = []; + for (var i = 0; i < this.sel.ranges.length; i++) + dup[i] = code; + this.replaceSelections(dup, collapse, origin || "+input"); + }), + replaceSelections: function(code, collapse, origin) { + var changes = [], sel = this.sel; + for (var i = 0; i < sel.ranges.length; i++) { + var range = sel.ranges[i]; + changes[i] = {from: range.from(), to: range.to(), text: splitLines(code[i]), origin: origin}; + } + var newSel = collapse && collapse != "end" && computeReplacedSel(this, changes, collapse); + for (var i = changes.length - 1; i >= 0; i--) + makeChange(this, changes[i]); + if (newSel) setSelectionReplaceHistory(this, newSel); + else if (this.cm) ensureCursorVisible(this.cm); + }, + undo: docMethodOp(function() {makeChangeFromHistory(this, "undo");}), + redo: docMethodOp(function() {makeChangeFromHistory(this, "redo");}), + undoSelection: docMethodOp(function() {makeChangeFromHistory(this, "undo", true);}), + redoSelection: docMethodOp(function() {makeChangeFromHistory(this, "redo", true);}), - setExtending: function(val) {this.sel.extend = val;}, + setExtending: function(val) {this.extend = val;}, + getExtending: function() {return this.extend;}, historySize: function() { - var hist = this.history; - return {undo: hist.done.length, redo: hist.undone.length}; + var hist = this.history, done = 0, undone = 0; + for (var i = 0; i < hist.done.length; i++) if (!hist.done[i].ranges) ++done; + for (var i = 0; i < hist.undone.length; i++) if (!hist.undone[i].ranges) ++undone; + return {undo: done, redo: undone}; }, - clearHistory: function() {this.history = makeHistory(this.history.maxGeneration);}, + clearHistory: function() {this.history = new History(this.history.maxGeneration);}, markClean: function() { this.cleanGeneration = this.changeGeneration(true); @@ -5022,9 +6152,9 @@ window.CodeMirror = (function() { undone: copyHistoryArray(this.history.undone)}; }, setHistory: function(histData) { - var hist = this.history = makeHistory(this.history.maxGeneration); - hist.done = histData.done.slice(0); - hist.undone = histData.undone.slice(0); + var hist = this.history = new History(this.history.maxGeneration); + hist.done = copyHistoryArray(histData.done.slice(0), null, true); + hist.undone = copyHistoryArray(histData.undone.slice(0), null, true); }, markText: function(from, to, options) { @@ -5033,7 +6163,7 @@ window.CodeMirror = (function() { setBookmark: function(pos, options) { var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options), insertLeft: options && options.insertLeft, - clearWhenEmpty: false}; + clearWhenEmpty: false, shared: options && options.shared}; pos = clipPos(this, pos); return markText(this, pos, pos, realOpts, "bookmark"); }, @@ -5048,6 +6178,22 @@ window.CodeMirror = (function() { } return markers; }, + findMarks: function(from, to) { + from = clipPos(this, from); to = clipPos(this, to); + var found = [], lineNo = from.line; + this.iter(from.line, to.line + 1, function(line) { + var spans = line.markedSpans; + if (spans) for (var i = 0; i < spans.length; i++) { + var span = spans[i]; + if (!(lineNo == from.line && from.ch > span.to || + span.from == null && lineNo != from.line|| + lineNo == to.line && span.from > to.ch)) + found.push(span.marker.parent || span.marker); + } + ++lineNo; + }); + return found; + }, getAllMarks: function() { var markers = []; this.iter(function(line) { @@ -5081,8 +6227,8 @@ window.CodeMirror = (function() { copy: function(copyHistory) { var doc = new Doc(getLines(this, this.first, this.first + this.size), this.modeOption, this.first); doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft; - doc.sel = {from: this.sel.from, to: this.sel.to, head: this.sel.head, anchor: this.sel.anchor, - shift: this.sel.shift, extend: false, goalColumn: this.sel.goalColumn}; + doc.sel = this.sel; + doc.extend = false; if (copyHistory) { doc.history.undoDepth = this.history.undoDepth; doc.setHistory(this.getHistory()); @@ -5114,7 +6260,7 @@ window.CodeMirror = (function() { if (other.history == this.history) { var splitIds = [other.id]; linkedDocs(other, function(doc) {splitIds.push(doc.id);}, true); - other.history = makeHistory(); + other.history = new History(null); other.history.done = copyHistoryArray(this.history.done, splitIds); other.history.undone = copyHistoryArray(this.history.undone, splitIds); } @@ -5125,9 +6271,10 @@ window.CodeMirror = (function() { getEditor: function() {return this.cm;} }); + // Public alias. Doc.prototype.eachLine = Doc.prototype.iter; - // The Doc methods that should be available on CodeMirror instances + // Set up methods on CodeMirror's prototype to redirect to the editor's document. var dontDelegate = "iter insert remove copy getEditor".split(" "); for (var prop in Doc.prototype) if (Doc.prototype.hasOwnProperty(prop) && indexOf(dontDelegate, prop) < 0) CodeMirror.prototype[prop] = (function(method) { @@ -5136,6 +6283,7 @@ window.CodeMirror = (function() { eventMixin(Doc); + // Call f for all linked documents. function linkedDocs(doc, f, sharedHistOnly) { function propagate(doc, skip, sharedHist) { if (doc.linked) for (var i = 0; i < doc.linked.length; ++i) { @@ -5150,22 +6298,25 @@ window.CodeMirror = (function() { propagate(doc, null, true); } + // Attach a document to an editor. function attachDoc(cm, doc) { if (doc.cm) throw new Error("This document is already in use."); cm.doc = doc; doc.cm = cm; estimateLineHeights(cm); loadMode(cm); - if (!cm.options.lineWrapping) computeMaxLength(cm); + if (!cm.options.lineWrapping) findMaxLine(cm); cm.options.mode = doc.modeOption; regChange(cm); } // LINE UTILITIES - function getLine(chunk, n) { - n -= chunk.first; - while (!chunk.lines) { + // Find the line object corresponding to the given line number. + function getLine(doc, n) { + n -= doc.first; + if (n < 0 || n >= doc.size) throw new Error("There is no line " + (n + doc.first) + " in the document."); + for (var chunk = doc; !chunk.lines;) { for (var i = 0;; ++i) { var child = chunk.children[i], sz = child.chunkSize(); if (n < sz) { chunk = child; break; } @@ -5175,6 +6326,8 @@ window.CodeMirror = (function() { return chunk.lines[n]; } + // Get the part of a document between two positions, as an array of + // strings. function getBetween(doc, start, end) { var out = [], n = start.line; doc.iter(start.line, end.line + 1, function(line) { @@ -5186,17 +6339,22 @@ window.CodeMirror = (function() { }); return out; } + // Get the lines between from and to, as array of strings. function getLines(doc, from, to) { var out = []; doc.iter(from, to, function(line) { out.push(line.text); }); return out; } + // Update the height of a line, propagating the height change + // upwards to parent nodes. function updateLineHeight(line, height) { var diff = height - line.height; - for (var n = line; n; n = n.parent) n.height += diff; + if (diff) for (var n = line; n; n = n.parent) n.height += diff; } + // Given a line object, find its line number by walking up through + // its parent links. function lineNo(line) { if (line.parent == null) return null; var cur = line.parent, no = indexOf(cur.lines, line); @@ -5209,10 +6367,12 @@ window.CodeMirror = (function() { return no + cur.first; } + // Find the line at the given vertical position, using the height + // information in the document tree. function lineAtHeight(chunk, h) { var n = chunk.first; outer: do { - for (var i = 0, e = chunk.children.length; i < e; ++i) { + for (var i = 0; i < chunk.children.length; ++i) { var child = chunk.children[i], ch = child.height; if (h < ch) { chunk = child; continue outer; } h -= ch; @@ -5220,7 +6380,7 @@ window.CodeMirror = (function() { } return n; } while (!chunk.lines); - for (var i = 0, e = chunk.lines.length; i < e; ++i) { + for (var i = 0; i < chunk.lines.length; ++i) { var line = chunk.lines[i], lh = line.height; if (h < lh) break; h -= lh; @@ -5228,8 +6388,10 @@ window.CodeMirror = (function() { return n + i; } - function heightAtLine(cm, lineObj) { - lineObj = visualLine(cm.doc, lineObj); + + // Find the height above the given line. + function heightAtLine(lineObj) { + lineObj = visualLine(lineObj); var h = 0, chunk = lineObj.parent; for (var i = 0; i < chunk.lines.length; ++i) { @@ -5247,6 +6409,9 @@ window.CodeMirror = (function() { return h; } + // Get the bidi ordering for the given line (and cache it). Returns + // false for lines that are fully left-to-right, and an array of + // BidiSpan objects otherwise. function getOrder(line) { var order = line.order; if (order == null) order = line.order = bidiOrdering(line.text); @@ -5255,20 +6420,141 @@ window.CodeMirror = (function() { // HISTORY - function makeHistory(startGen) { - return { - // Arrays of history events. Doing something adds an event to - // done and clears undo. Undoing moves events from done to - // undone, redoing moves them in the other direction. - done: [], undone: [], undoDepth: Infinity, - // Used to track when changes can be merged into a single undo - // event - lastTime: 0, lastOp: null, lastOrigin: null, - // Used by the isClean() method - generation: startGen || 1, maxGeneration: startGen || 1 - }; + function History(startGen) { + // Arrays of change events and selections. Doing something adds an + // event to done and clears undo. Undoing moves events from done + // to undone, redoing moves them in the other direction. + this.done = []; this.undone = []; + this.undoDepth = Infinity; + // Used to track when changes can be merged into a single undo + // event + this.lastModTime = this.lastSelTime = 0; + this.lastOp = null; + this.lastOrigin = this.lastSelOrigin = null; + // Used by the isClean() method + this.generation = this.maxGeneration = startGen || 1; } + // Create a history change event from an updateDoc-style change + // object. + function historyChangeFromChange(doc, change) { + var histChange = {from: copyPos(change.from), to: changeEnd(change), text: getBetween(doc, change.from, change.to)}; + attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1); + linkedDocs(doc, function(doc) {attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1);}, true); + return histChange; + } + + // Pop all selection events off the end of a history array. Stop at + // a change event. + function clearSelectionEvents(array) { + while (array.length) { + var last = lst(array); + if (last.ranges) array.pop(); + else break; + } + } + + // Find the top change event in the history. Pop off selection + // events that are in the way. + function lastChangeEvent(hist, force) { + if (force) { + clearSelectionEvents(hist.done); + return lst(hist.done); + } else if (hist.done.length && !lst(hist.done).ranges) { + return lst(hist.done); + } else if (hist.done.length > 1 && !hist.done[hist.done.length - 2].ranges) { + hist.done.pop(); + return lst(hist.done); + } + } + + // Register a change in the history. Merges changes that are within + // a single operation, ore are close together with an origin that + // allows merging (starting with "+") into a single event. + function addChangeToHistory(doc, change, selAfter, opId) { + var hist = doc.history; + hist.undone.length = 0; + var time = +new Date, cur; + + if ((hist.lastOp == opId || + hist.lastOrigin == change.origin && change.origin && + ((change.origin.charAt(0) == "+" && doc.cm && hist.lastModTime > time - doc.cm.options.historyEventDelay) || + change.origin.charAt(0) == "*")) && + (cur = lastChangeEvent(hist, hist.lastOp == opId))) { + // Merge this change into the last event + var last = lst(cur.changes); + if (cmp(change.from, change.to) == 0 && cmp(change.from, last.to) == 0) { + // Optimized case for simple insertion -- don't want to add + // new changesets for every character typed + last.to = changeEnd(change); + } else { + // Add new sub-event + cur.changes.push(historyChangeFromChange(doc, change)); + } + } else { + // Can not be merged, start a new event. + var before = lst(hist.done); + if (!before || !before.ranges) + pushSelectionToHistory(doc.sel, hist.done); + cur = {changes: [historyChangeFromChange(doc, change)], + generation: hist.generation}; + hist.done.push(cur); + while (hist.done.length > hist.undoDepth) { + hist.done.shift(); + if (!hist.done[0].ranges) hist.done.shift(); + } + } + hist.done.push(selAfter); + hist.generation = ++hist.maxGeneration; + hist.lastModTime = hist.lastSelTime = time; + hist.lastOp = opId; + hist.lastOrigin = hist.lastSelOrigin = change.origin; + + if (!last) signal(doc, "historyAdded"); + } + + function selectionEventCanBeMerged(doc, origin, prev, sel) { + var ch = origin.charAt(0); + return ch == "*" || + ch == "+" && + prev.ranges.length == sel.ranges.length && + prev.somethingSelected() == sel.somethingSelected() && + new Date - doc.history.lastSelTime <= (doc.cm ? doc.cm.options.historyEventDelay : 500); + } + + // Called whenever the selection changes, sets the new selection as + // the pending selection in the history, and pushes the old pending + // selection into the 'done' array when it was significantly + // different (in number of selected ranges, emptiness, or time). + function addSelectionToHistory(doc, sel, opId, options) { + var hist = doc.history, origin = options && options.origin; + + // A new event is started when the previous origin does not match + // the current, or the origins don't allow matching. Origins + // starting with * are always merged, those starting with + are + // merged when similar and close together in time. + if (opId == hist.lastOp || + (origin && hist.lastSelOrigin == origin && + (hist.lastModTime == hist.lastSelTime && hist.lastOrigin == origin || + selectionEventCanBeMerged(doc, origin, lst(hist.done), sel)))) + hist.done[hist.done.length - 1] = sel; + else + pushSelectionToHistory(sel, hist.done); + + hist.lastSelTime = +new Date; + hist.lastSelOrigin = origin; + hist.lastOp = opId; + if (options && options.clearRedo !== false) + clearSelectionEvents(hist.undone); + } + + function pushSelectionToHistory(sel, dest) { + var top = lst(dest); + if (!(top && top.ranges && top.equals(sel))) + dest.push(sel); + } + + // Used to store marked span information in the history. function attachLocalSpans(doc, change, from, to) { var existing = change["spans_" + doc.id], n = 0; doc.iter(Math.max(doc.first, from), Math.min(doc.first + doc.size, to), function(line) { @@ -5278,51 +6564,8 @@ window.CodeMirror = (function() { }); } - function historyChangeFromChange(doc, change) { - var from = { line: change.from.line, ch: change.from.ch }; - var histChange = {from: from, to: changeEnd(change), text: getBetween(doc, change.from, change.to)}; - attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1); - linkedDocs(doc, function(doc) {attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1);}, true); - return histChange; - } - - function addToHistory(doc, change, selAfter, opId) { - var hist = doc.history; - hist.undone.length = 0; - var time = +new Date, cur = lst(hist.done); - - if (cur && - (hist.lastOp == opId || - hist.lastOrigin == change.origin && change.origin && - ((change.origin.charAt(0) == "+" && doc.cm && hist.lastTime > time - doc.cm.options.historyEventDelay) || - change.origin.charAt(0) == "*"))) { - // Merge this change into the last event - var last = lst(cur.changes); - if (posEq(change.from, change.to) && posEq(change.from, last.to)) { - // Optimized case for simple insertion -- don't want to add - // new changesets for every character typed - last.to = changeEnd(change); - } else { - // Add new sub-event - cur.changes.push(historyChangeFromChange(doc, change)); - } - cur.anchorAfter = selAfter.anchor; cur.headAfter = selAfter.head; - } else { - // Can not be merged, start a new event. - cur = {changes: [historyChangeFromChange(doc, change)], - generation: hist.generation, - anchorBefore: doc.sel.anchor, headBefore: doc.sel.head, - anchorAfter: selAfter.anchor, headAfter: selAfter.head}; - hist.done.push(cur); - while (hist.done.length > hist.undoDepth) - hist.done.shift(); - } - hist.generation = ++hist.maxGeneration; - hist.lastTime = time; - hist.lastOp = opId; - hist.lastOrigin = change.origin; - } - + // When un/re-doing restores text containing marked spans, those + // that have been explicitly cleared should not be restored. function removeClearedSpans(spans) { if (!spans) return null; for (var i = 0, out; i < spans.length; ++i) { @@ -5332,6 +6575,7 @@ window.CodeMirror = (function() { return !out ? spans : out.length ? out : null; } + // Retrieve and filter the old marked spans stored in a change event. function getOldSpans(doc, change) { var found = change["spans_" + doc.id]; if (!found) return null; @@ -5342,11 +6586,15 @@ window.CodeMirror = (function() { // Used both to provide a JSON-safe object in .getHistory, and, when // detaching a document, to split the history in two - function copyHistoryArray(events, newGroup) { + function copyHistoryArray(events, newGroup, instantiateSel) { for (var i = 0, copy = []; i < events.length; ++i) { - var event = events[i], changes = event.changes, newChanges = []; - copy.push({changes: newChanges, anchorBefore: event.anchorBefore, headBefore: event.headBefore, - anchorAfter: event.anchorAfter, headAfter: event.headAfter}); + var event = events[i]; + if (event.ranges) { + copy.push(instantiateSel ? Selection.prototype.deepCopy.call(event) : event); + continue; + } + var changes = event.changes, newChanges = []; + copy.push({changes: newChanges}); for (var j = 0; j < changes.length; ++j) { var change = changes[j], m; newChanges.push({from: change.from, to: change.to, text: change.text}); @@ -5363,7 +6611,7 @@ window.CodeMirror = (function() { // Rebasing/resetting history to deal with externally-sourced changes - function rebaseHistSel(pos, from, to, diff) { + function rebaseHistSelSingle(pos, from, to, diff) { if (to < pos.line) { pos.line += diff; } else if (from < pos.line) { @@ -5382,28 +6630,27 @@ window.CodeMirror = (function() { function rebaseHistArray(array, from, to, diff) { for (var i = 0; i < array.length; ++i) { var sub = array[i], ok = true; + if (sub.ranges) { + if (!sub.copied) { sub = array[i] = sub.deepCopy(); sub.copied = true; } + for (var j = 0; j < sub.ranges.length; j++) { + rebaseHistSelSingle(sub.ranges[j].anchor, from, to, diff); + rebaseHistSelSingle(sub.ranges[j].head, from, to, diff); + } + continue; + } for (var j = 0; j < sub.changes.length; ++j) { var cur = sub.changes[j]; - if (!sub.copied) { cur.from = copyPos(cur.from); cur.to = copyPos(cur.to); } if (to < cur.from.line) { - cur.from.line += diff; - cur.to.line += diff; + cur.from = Pos(cur.from.line + diff, cur.from.ch); + cur.to = Pos(cur.to.line + diff, cur.to.ch); } else if (from <= cur.to.line) { ok = false; break; } } - if (!sub.copied) { - sub.anchorBefore = copyPos(sub.anchorBefore); sub.headBefore = copyPos(sub.headBefore); - sub.anchorAfter = copyPos(sub.anchorAfter); sub.readAfter = copyPos(sub.headAfter); - sub.copied = true; - } if (!ok) { array.splice(0, i + 1); i = 0; - } else { - rebaseHistSel(sub.anchorBefore); rebaseHistSel(sub.headBefore); - rebaseHistSel(sub.anchorAfter); rebaseHistSel(sub.headAfter); } } } @@ -5414,30 +6661,23 @@ window.CodeMirror = (function() { rebaseHistArray(hist.undone, from, to, diff); } - // EVENT OPERATORS + // EVENT UTILITIES - function stopMethod() {e_stop(this);} - // Ensure an event has a stop method. - function addStop(event) { - if (!event.stop) event.stop = stopMethod; - return event; - } + // Due to the fact that we still support jurassic IE versions, some + // compatibility wrappers are needed. - function e_preventDefault(e) { + var e_preventDefault = CodeMirror.e_preventDefault = function(e) { if (e.preventDefault) e.preventDefault(); else e.returnValue = false; - } - function e_stopPropagation(e) { + }; + var e_stopPropagation = CodeMirror.e_stopPropagation = function(e) { if (e.stopPropagation) e.stopPropagation(); else e.cancelBubble = true; - } + }; function e_defaultPrevented(e) { return e.defaultPrevented != null ? e.defaultPrevented : e.returnValue == false; } - function e_stop(e) {e_preventDefault(e); e_stopPropagation(e);} - CodeMirror.e_stop = e_stop; - CodeMirror.e_preventDefault = e_preventDefault; - CodeMirror.e_stopPropagation = e_stopPropagation; + var e_stop = CodeMirror.e_stop = function(e) {e_preventDefault(e); e_stopPropagation(e);}; function e_target(e) {return e.target || e.srcElement;} function e_button(e) { @@ -5453,7 +6693,10 @@ window.CodeMirror = (function() { // EVENT HANDLING - function on(emitter, type, f) { + // Lightweight event framework. on/off also work on DOM nodes, + // registering native DOM handlers. + + var on = CodeMirror.on = function(emitter, type, f) { if (emitter.addEventListener) emitter.addEventListener(type, f, false); else if (emitter.attachEvent) @@ -5463,9 +6706,9 @@ window.CodeMirror = (function() { var arr = map[type] || (map[type] = []); arr.push(f); } - } + }; - function off(emitter, type, f) { + var off = CodeMirror.off = function(emitter, type, f) { if (emitter.removeEventListener) emitter.removeEventListener(type, f, false); else if (emitter.detachEvent) @@ -5476,15 +6719,22 @@ window.CodeMirror = (function() { for (var i = 0; i < arr.length; ++i) if (arr[i] == f) { arr.splice(i, 1); break; } } - } + }; - function signal(emitter, type /*, values...*/) { + var signal = CodeMirror.signal = function(emitter, type /*, values...*/) { var arr = emitter._handlers && emitter._handlers[type]; if (!arr) return; var args = Array.prototype.slice.call(arguments, 2); for (var i = 0; i < arr.length; ++i) arr[i].apply(null, args); - } + }; + // Often, we want to signal events at a point where we are in the + // middle of some work, but don't want the handler to start calling + // other methods on the editor, which might be in an inconsistent + // state or simply not expect any other events to happen. + // signalLater looks whether there are any handlers, and schedules + // them to be executed when the last operation ends, or, if no + // operation is active, when a timeout fires. var delayedCallbacks, delayedCallbackDepth = 0; function signalLater(emitter, type /*, values...*/) { var arr = emitter._handlers && emitter._handlers[type]; @@ -5500,11 +6750,6 @@ window.CodeMirror = (function() { delayedCallbacks.push(bnd(arr[i])); } - function signalDOMEvent(cm, e, override) { - signal(cm, override || e.type, cm, e); - return e_defaultPrevented(e) || e.codemirrorIgnore; - } - function fireDelayed() { --delayedCallbackDepth; var delayed = delayedCallbacks; @@ -5512,13 +6757,21 @@ window.CodeMirror = (function() { for (var i = 0; i < delayed.length; ++i) delayed[i](); } + // The DOM events that CodeMirror handles can be overridden by + // registering a (non-DOM) handler on the editor for the event name, + // and preventDefault-ing the event in that handler. + function signalDOMEvent(cm, e, override) { + signal(cm, override || e.type, cm, e); + return e_defaultPrevented(e) || e.codemirrorIgnore; + } + function hasHandler(emitter, type) { var arr = emitter._handlers && emitter._handlers[type]; return arr && arr.length > 0; } - CodeMirror.on = on; CodeMirror.off = off; CodeMirror.signal = signal; - + // Add on and off methods to a constructor's prototype, to make + // registering events on such objects more convenient. function eventMixin(ctor) { ctor.prototype.on = function(type, f) {on(this, type, f);}; ctor.prototype.off = function(type, f) {off(this, type, f);}; @@ -5533,23 +6786,47 @@ window.CodeMirror = (function() { // handling this'. var Pass = CodeMirror.Pass = {toString: function(){return "CodeMirror.Pass";}}; + // Reused option objects for setSelection & friends + var sel_dontScroll = {scroll: false}, sel_mouse = {origin: "*mouse"}, sel_move = {origin: "+move"}; + function Delayed() {this.id = null;} - Delayed.prototype = {set: function(ms, f) {clearTimeout(this.id); this.id = setTimeout(f, ms);}}; + Delayed.prototype.set = function(ms, f) { + clearTimeout(this.id); + this.id = setTimeout(f, ms); + }; // Counts the column offset in a string, taking tabs into account. // Used mostly to find indentation. - function countColumn(string, end, tabSize, startIndex, startValue) { + var countColumn = CodeMirror.countColumn = function(string, end, tabSize, startIndex, startValue) { if (end == null) { end = string.search(/[^\s\u00a0]/); if (end == -1) end = string.length; } - for (var i = startIndex || 0, n = startValue || 0; i < end; ++i) { - if (string.charAt(i) == "\t") n += tabSize - (n % tabSize); - else ++n; + for (var i = startIndex || 0, n = startValue || 0;;) { + var nextTab = string.indexOf("\t", i); + if (nextTab < 0 || nextTab >= end) + return n + (end - i); + n += nextTab - i; + n += tabSize - (n % tabSize); + i = nextTab + 1; + } + }; + + // The inverse of countColumn -- find the offset that corresponds to + // a particular column. + function findColumn(string, goal, tabSize) { + for (var pos = 0, col = 0;;) { + var nextTab = string.indexOf("\t", pos); + if (nextTab == -1) nextTab = string.length; + var skipped = nextTab - pos; + if (nextTab == string.length || col + skipped >= goal) + return pos + Math.min(skipped, goal - col); + col += nextTab - pos; + col += tabSize - (col % tabSize); + pos = nextTab + 1; + if (col >= goal) return pos; } - return n; } - CodeMirror.countColumn = countColumn; var spaceStrs = [""]; function spaceStr(n) { @@ -5560,31 +6837,37 @@ window.CodeMirror = (function() { function lst(arr) { return arr[arr.length-1]; } - function selectInput(node) { - if (ios) { // Mobile Safari apparently has a bug where select() is broken. - node.selectionStart = 0; - node.selectionEnd = node.value.length; - } else { - // Suppress mysterious IE10 errors - try { node.select(); } - catch(_e) {} - } - } + var selectInput = function(node) { node.select(); }; + if (ios) // Mobile Safari apparently has a bug where select() is broken. + selectInput = function(node) { node.selectionStart = 0; node.selectionEnd = node.value.length; }; + else if (ie) // Suppress mysterious IE10 errors + selectInput = function(node) { try { node.select(); } catch(_e) {} }; - 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; + function indexOf(array, elt) { + for (var i = 0; i < array.length; ++i) + if (array[i] == elt) return i; return -1; } + if ([].indexOf) indexOf = function(array, elt) { return array.indexOf(elt); }; + function map(array, f) { + var out = []; + for (var i = 0; i < array.length; i++) out[i] = f(array[i], i); + return out; + } + if ([].map) map = function(array, f) { return array.map(f); }; function createObj(base, props) { - function Obj() {} - Obj.prototype = base; - var inst = new Obj(); + var inst; + if (Object.create) { + inst = Object.create(base); + } else { + var ctor = function() {}; + ctor.prototype = base; + inst = new ctor(); + } if (props) copyObj(props, inst); return inst; - } + }; function copyObj(obj, target) { if (!target) target = {}; @@ -5592,27 +6875,27 @@ window.CodeMirror = (function() { return target; } - function emptyArray(size) { - for (var a = [], i = 0; i < size; ++i) a.push(undefined); - return a; - } - function bind(f) { var args = Array.prototype.slice.call(arguments, 1); return function(){return f.apply(null, args);}; } - var nonASCIISingleCaseWordChar = /[\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/; - function isWordChar(ch) { + var nonASCIISingleCaseWordChar = /[\u00df\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/; + var isWordChar = CodeMirror.isWordChar = function(ch) { return /\w/.test(ch) || ch > "\x80" && (ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch)); - } + }; function isEmpty(obj) { for (var n in obj) if (obj.hasOwnProperty(n) && obj[n]) return false; return true; } + // Extending unicode characters. A series of a non-extending char + + // any number of extending chars is treated as a single unit as far + // as editing and measuring is concerned. This is not fully correct, + // since some scripts/fonts/browsers also treat other configurations + // of code points as a group. var extendingChars = /[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/; function isExtendingChar(ch) { return ch.charCodeAt(0) >= 768 && extendingChars.test(ch); } @@ -5622,11 +6905,27 @@ window.CodeMirror = (function() { var e = document.createElement(tag); if (className) e.className = className; if (style) e.style.cssText = style; - if (typeof content == "string") setTextContent(e, content); + if (typeof content == "string") e.appendChild(document.createTextNode(content)); else if (content) for (var i = 0; i < content.length; ++i) e.appendChild(content[i]); return e; } + var range; + if (document.createRange) range = function(node, start, end) { + var r = document.createRange(); + r.setEnd(node, end); + r.setStart(node, start); + return r; + }; + else range = function(node, start, end) { + var r = document.body.createTextRange(); + r.moveToElementText(node.parentNode); + r.collapse(true); + r.moveEnd("character", end); + r.moveStart("character", start); + return r; + }; + function removeChildren(e) { for (var count = e.childNodes.length; count > 0; --count) e.removeChild(e.firstChild); @@ -5637,17 +6936,20 @@ window.CodeMirror = (function() { return removeChildren(parent).appendChild(e); } - function setTextContent(e, str) { - if (ie_lt9) { - e.innerHTML = ""; - e.appendChild(document.createTextNode(str)); - } else e.textContent = str; + function contains(parent, child) { + if (parent.contains) + return parent.contains(child); + while (child = child.parentNode) + if (child == parent) return true; } - function getRect(node) { - return node.getBoundingClientRect(); - } - CodeMirror.replaceGetRect = function(f) { getRect = f; }; + function activeElt() { return document.activeElement; } + // Older versions of IE throws unspecified error when touching + // document.activeElement in some cases (during loading, in iframe) + if (ie_upto10) activeElt = function() { + try { return document.activeElement; } + catch(e) { return document.body; } + }; // FEATURE DETECTION @@ -5655,41 +6957,11 @@ window.CodeMirror = (function() { var dragAndDrop = function() { // There is *some* kind of drag-and-drop support in IE6-8, but I // couldn't get it to work yet. - if (ie_lt9) return false; + if (ie_upto8) return false; var div = elt('div'); return "draggable" in div || "dragDrop" in div; }(); - // For a reason I have yet to figure out, some browsers disallow - // word wrapping between certain characters *only* if a new inline - // element is started between them. This makes it hard to reliably - // measure the position of things, since that requires inserting an - // extra span. This terribly fragile set of tests matches the - // character combinations that suffer from this phenomenon on the - // various browsers. - function spanAffectsWrapping() { return false; } - if (gecko) // Only for "$'" - spanAffectsWrapping = function(str, i) { - return str.charCodeAt(i - 1) == 36 && str.charCodeAt(i) == 39; - }; - else if (safari && !/Version\/([6-9]|\d\d)\b/.test(navigator.userAgent)) - spanAffectsWrapping = function(str, i) { - return /\-[^ \-?]|\?[^ !\'\"\),.\-\/:;\?\]\}]/.test(str.slice(i - 1, i + 1)); - }; - else if (webkit && /Chrome\/(?:29|[3-9]\d|\d\d\d)\./.test(navigator.userAgent)) - spanAffectsWrapping = function(str, i) { - var code = str.charCodeAt(i - 1); - return code >= 8208 && code <= 8212; - }; - else if (webkit) - spanAffectsWrapping = function(str, i) { - if (i > 1 && str.charCodeAt(i - 1) == 45) { - if (/\w/.test(str.charAt(i - 2)) && /[^\-?\.]/.test(str.charAt(i))) return true; - if (i > 2 && /[\d\.,]/.test(str.charAt(i - 2)) && /[\d\.,]/.test(str.charAt(i))) return false; - } - return /[~!#%&*)=+}\]\\|\"\.>,:;][({[<]|-[^\-?\.\u2010-\u201f\u2026]|\?[\w~`@#$%\^&*(_=+{[|><]|\u2026[\w~`@#$%\^&*(_=+{[><]/.test(str.slice(i - 1, i + 1)); - }; - var knownScrollbarWidth; function scrollbarWidth(measure) { if (knownScrollbarWidth != null) return knownScrollbarWidth; @@ -5706,15 +6978,26 @@ window.CodeMirror = (function() { var test = elt("span", "\u200b"); removeChildrenAndAdd(measure, elt("span", [test, document.createTextNode("x")])); if (measure.firstChild.offsetHeight != 0) - zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !ie_lt8; + zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !ie_upto7; } if (zwspSupported) return elt("span", "\u200b"); else return elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px"); } + // Feature-detect IE's crummy client rect reporting for bidi text + var badBidiRects; + function hasBadBidiRects(measure) { + if (badBidiRects != null) return badBidiRects; + var txt = removeChildrenAndAdd(measure, document.createTextNode("A\u062eA")); + var r0 = range(txt, 0, 1).getBoundingClientRect(); + if (r0.left == r0.right) return false; + var r1 = range(txt, 1, 2).getBoundingClientRect(); + return badBidiRects = (r1.right - r0.right < 3); + } + // See if "".split is the broken IE version, if so, provide an // alternative way to split lines. - var splitLines = "\n\nb".split(/\n/).length != 3 ? function(string) { + var splitLines = CodeMirror.splitLines = "\n\nb".split(/\n/).length != 3 ? function(string) { var pos = 0, result = [], l = string.length; while (pos <= l) { var nl = string.indexOf("\n", pos); @@ -5731,7 +7014,6 @@ window.CodeMirror = (function() { } return result; } : function(string){return string.split(/\r\n?|\n/);}; - CodeMirror.splitLines = splitLines; var hasSelection = window.getSelection ? function(te) { try { return te.selectionStart != te.selectionEnd; } @@ -5747,10 +7029,10 @@ window.CodeMirror = (function() { var e = elt("div"); if ("oncopy" in e) return true; e.setAttribute("oncopy", "return;"); - return typeof e.oncopy == 'function'; + return typeof e.oncopy == "function"; })(); - // KEY NAMING + // KEY NAMES var keyNames = {3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt", 19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", @@ -5796,19 +7078,21 @@ window.CodeMirror = (function() { function lineStart(cm, lineN) { var line = getLine(cm.doc, lineN); - var visual = visualLine(cm.doc, line); + var visual = visualLine(line); if (visual != line) lineN = lineNo(visual); var order = getOrder(visual); var ch = !order ? 0 : order[0].level % 2 ? lineRight(visual) : lineLeft(visual); return Pos(lineN, ch); } function lineEnd(cm, lineN) { - var merged, line; - while (merged = collapsedSpanAtEnd(line = getLine(cm.doc, lineN))) - lineN = merged.find().to.line; + var merged, line = getLine(cm.doc, lineN); + while (merged = collapsedSpanAtEnd(line)) { + line = merged.find(1, true).line; + lineN = null; + } var order = getOrder(line); var ch = !order ? line.text.length : order[0].level % 2 ? lineLeft(line) : lineRight(line); - return Pos(lineN, ch); + return Pos(lineN == null ? lineNo(line) : lineN, ch); } function compareBidiLevel(order, a, b) { @@ -5845,12 +7129,11 @@ window.CodeMirror = (function() { return pos; } - // This is somewhat involved. It is needed in order to move - // 'visually' through bi-directional text -- i.e., pressing left - // should make the cursor go left, even when in RTL text. The - // tricky part is the 'jumps', where RTL and LTR text touch each - // other. This often requires the cursor offset to move more than - // one unit, in order to visually move one unit. + // This is needed in order to move 'visually' through bi-directional + // text -- i.e., pressing left should make the cursor go left, even + // when in RTL text. The tricky part is the 'jumps', where RTL and + // LTR text touch each other. This often requires the cursor offset + // to move more than one unit, in order to visually move one unit. function moveVisually(line, start, dir, byUnit) { var bidi = getOrder(line); if (!bidi) return moveLogically(line, start, dir, byUnit); @@ -5905,14 +7188,16 @@ window.CodeMirror = (function() { // objects) in the order in which they occur visually. var bidiOrdering = (function() { // Character types for codepoints 0 to 0xff - var lowTypes = "bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLL"; + var lowTypes = "bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN"; // Character types for codepoints 0x600 to 0x6ff - var arabicTypes = "rrrrrrrrrrrr,rNNmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmrrrrrrrnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmNmmmmrrrrrrrrrrrrrrrrrr"; + var arabicTypes = "rrrrrrrrrrrr,rNNmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmrrrrrrrnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmNmmmm"; function charType(code) { - if (code <= 0xff) return lowTypes.charAt(code); + if (code <= 0xf7) return lowTypes.charAt(code); else if (0x590 <= code && code <= 0x5f4) return "R"; - else if (0x600 <= code && code <= 0x6ff) return arabicTypes.charAt(code - 0x600); - else if (0x700 <= code && code <= 0x8ac) return "r"; + else if (0x600 <= code && code <= 0x6ed) return arabicTypes.charAt(code - 0x600); + else if (0x6ee <= code && code <= 0x8ac) return "r"; + else if (0x2000 <= code && code <= 0x200b) return "w"; + else if (code == 0x200c) return "b"; else return "L"; } @@ -5921,6 +7206,11 @@ window.CodeMirror = (function() { // Browsers seem to always treat the boundaries of block elements as being L. var outerType = "L"; + function BidiSpan(level, from, to) { + this.level = level; + this.from = from; this.to = to; + } + return function(str) { if (!bidiRE.test(str)) return false; var len = str.length, types = []; @@ -6010,32 +7300,32 @@ window.CodeMirror = (function() { if (countsAsLeft.test(types[i])) { var start = i; for (++i; i < len && countsAsLeft.test(types[i]); ++i) {} - order.push({from: start, to: i, level: 0}); + order.push(new BidiSpan(0, start, i)); } else { var pos = i, at = order.length; for (++i; i < len && types[i] != "L"; ++i) {} for (var j = pos; j < i;) { if (countsAsNum.test(types[j])) { - if (pos < j) order.splice(at, 0, {from: pos, to: j, level: 1}); + if (pos < j) order.splice(at, 0, new BidiSpan(1, pos, j)); var nstart = j; for (++j; j < i && countsAsNum.test(types[j]); ++j) {} - order.splice(at, 0, {from: nstart, to: j, level: 2}); + order.splice(at, 0, new BidiSpan(2, nstart, j)); pos = j; } else ++j; } - if (pos < i) order.splice(at, 0, {from: pos, to: i, level: 1}); + if (pos < i) order.splice(at, 0, new BidiSpan(1, pos, i)); } } if (order[0].level == 1 && (m = str.match(/^\s+/))) { order[0].from = m[0].length; - order.unshift({from: 0, to: m[0].length, level: 0}); + order.unshift(new BidiSpan(0, 0, m[0].length)); } if (lst(order).level == 1 && (m = str.match(/\s+$/))) { lst(order).to -= m[0].length; - order.push({from: len - m[0].length, to: len, level: 0}); + order.push(new BidiSpan(0, len - m[0].length, len)); } if (order[0].level != lst(order).level) - order.push({from: len, to: len, level: order[0].level}); + order.push(new BidiSpan(order[0].level, len, len)); return order; }; @@ -6043,7 +7333,7 @@ window.CodeMirror = (function() { // THE END - CodeMirror.version = "3.21.0"; + CodeMirror.version = "4.0.3"; return CodeMirror; -})(); +}); diff --git a/applications/admin/static/codemirror/mode/css/css.js b/applications/admin/static/codemirror/mode/css/css.js index 8f6fe7df..f8fc5cea 100644 --- a/applications/admin/static/codemirror/mode/css/css.js +++ b/applications/admin/static/codemirror/mode/css/css.js @@ -1,6 +1,14 @@ -CodeMirror.defineMode("css", function(config, parserConfig) { - "use strict"; +(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.defineMode("css", function(config, parserConfig) { if (!parserConfig.propertyKeywords) parserConfig = CodeMirror.resolveMode("text/css"); var indentUnit = config.indentUnit, @@ -140,6 +148,8 @@ CodeMirror.defineMode("css", function(config, parserConfig) { return pushContext(state, stream, "media"); } else if (type == "@font-face") { return "font_face_before"; + } else if (/^@(-(moz|ms|o|webkit)-)?keyframes$/.test(type)) { + return "keyframes"; } else if (type && type.charAt(0) == "@") { return pushContext(state, stream, "at"); } else if (type == "hash") { @@ -264,6 +274,12 @@ CodeMirror.defineMode("css", function(config, parserConfig) { return "font_face"; }; + states.keyframes = function(type, stream, state) { + if (type == "word") { override = "variable"; return "keyframes"; } + if (type == "{") return pushContext(state, stream, "top"); + return pass(type, stream, state); + }; + states.at = function(type, stream, state) { if (type == ";") return popContext(state); if (type == "{" || type == "}") return popAndPass(type, stream, state); @@ -308,6 +324,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) { indent: function(state, textAfter) { var cx = state.context, ch = textAfter && textAfter.charAt(0); var indent = cx.indent; + if (cx.type == "prop" && ch == "}") cx = cx.prev; if (cx.prev && (ch == "}" && (cx.type == "block" || cx.type == "top" || cx.type == "interpolation" || cx.type == "font_face") || ch == ")" && (cx.type == "parens" || cx.type == "params" || cx.type == "media_parens") || @@ -325,7 +342,6 @@ CodeMirror.defineMode("css", function(config, parserConfig) { }; }); -(function() { function keySet(array) { var keys = {}; for (var i = 0; i < array.length; ++i) { @@ -353,10 +369,10 @@ CodeMirror.defineMode("css", function(config, parserConfig) { var propertyKeywords_ = [ "align-content", "align-items", "align-self", "alignment-adjust", "alignment-baseline", "anchor-point", "animation", "animation-delay", - "animation-direction", "animation-duration", "animation-iteration-count", - "animation-name", "animation-play-state", "animation-timing-function", - "appearance", "azimuth", "backface-visibility", "background", - "background-attachment", "background-clip", "background-color", + "animation-direction", "animation-duration", "animation-fill-mode", + "animation-iteration-count", "animation-name", "animation-play-state", + "animation-timing-function", "appearance", "azimuth", "backface-visibility", + "background", "background-attachment", "background-clip", "background-color", "background-image", "background-origin", "background-position", "background-repeat", "background-size", "baseline-shift", "binding", "bleed", "bookmark-label", "bookmark-level", "bookmark-state", @@ -387,10 +403,11 @@ CodeMirror.defineMode("css", function(config, parserConfig) { "font-stretch", "font-style", "font-synthesis", "font-variant", "font-variant-alternates", "font-variant-caps", "font-variant-east-asian", "font-variant-ligatures", "font-variant-numeric", "font-variant-position", - "font-weight", "grid-cell", "grid-column", "grid-column-align", - "grid-column-sizing", "grid-column-span", "grid-columns", "grid-flow", - "grid-row", "grid-row-align", "grid-row-sizing", "grid-row-span", - "grid-rows", "grid-template", "hanging-punctuation", "height", "hyphens", + "font-weight", "grid", "grid-area", "grid-auto-columns", "grid-auto-flow", + "grid-auto-position", "grid-auto-rows", "grid-column", "grid-column-end", + "grid-column-start", "grid-row", "grid-row-end", "grid-row-start", + "grid-template", "grid-template-areas", "grid-template-columns", + "grid-template-rows", "hanging-punctuation", "height", "hyphens", "icon", "image-orientation", "image-rendering", "image-resolution", "inline-box-align", "justify-content", "left", "letter-spacing", "line-break", "line-height", "line-stacking", "line-stacking-ruby", @@ -667,7 +684,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) { } }, "@": function(stream) { - if (stream.match(/^(charset|document|font-face|import|keyframes|media|namespace|page|supports)\b/, false)) return false; + if (stream.match(/^(charset|document|font-face|import|(-(moz|ms|o|webkit)-)?keyframes|media|namespace|page|supports)\b/, false)) return false; stream.eatWhile(/[\w\\\-]/); if (stream.match(/^\s*:/, false)) return ["variable-2", "variable-definition"]; @@ -680,4 +697,5 @@ CodeMirror.defineMode("css", function(config, parserConfig) { name: "css", helperType: "less" }); -})(); + +}); diff --git a/applications/admin/static/codemirror/mode/htmlembedded/htmlembedded.js b/applications/admin/static/codemirror/mode/htmlembedded/htmlembedded.js index c316cd34..3a07c343 100644 --- a/applications/admin/static/codemirror/mode/htmlembedded/htmlembedded.js +++ b/applications/admin/static/codemirror/mode/htmlembedded/htmlembedded.js @@ -1,3 +1,13 @@ +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror"), require("../htmlmixed/htmlmixed")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror", "../htmlmixed/htmlmixed"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + CodeMirror.defineMode("htmlembedded", function(config, parserConfig) { //config settings @@ -69,3 +79,5 @@ CodeMirror.defineMIME("application/x-ejs", { name: "htmlembedded", scriptingMode CodeMirror.defineMIME("application/x-aspx", { name: "htmlembedded", scriptingModeSpec:"text/x-csharp"}); CodeMirror.defineMIME("application/x-jsp", { name: "htmlembedded", scriptingModeSpec:"text/x-java"}); CodeMirror.defineMIME("application/x-erb", { name: "htmlembedded", scriptingModeSpec:"ruby"}); + +}); diff --git a/applications/admin/static/codemirror/mode/htmlmixed/htmlmixed.js b/applications/admin/static/codemirror/mode/htmlmixed/htmlmixed.js index e9eab3b9..d80ef9c6 100644 --- a/applications/admin/static/codemirror/mode/htmlmixed/htmlmixed.js +++ b/applications/admin/static/codemirror/mode/htmlmixed/htmlmixed.js @@ -1,5 +1,18 @@ +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror"), require("../xml/xml"), require("../javascript/javascript"), require("../css/css")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror", "../xml/xml", "../javascript/javascript", "../css/css"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + CodeMirror.defineMode("htmlmixed", function(config, parserConfig) { - var htmlMode = CodeMirror.getMode(config, {name: "xml", htmlMode: true}); + var htmlMode = CodeMirror.getMode(config, {name: "xml", + htmlMode: true, + multilineTagIndentFactor: parserConfig.multilineTagIndentFactor, + multilineTagIndentPastTag: parserConfig.multilineTagIndentPastTag}); var cssMode = CodeMirror.getMode(config, "css"); var scriptTypes = [], scriptTypesConf = parserConfig && parserConfig.scriptTypes; @@ -100,3 +113,5 @@ CodeMirror.defineMode("htmlmixed", function(config, parserConfig) { }, "xml", "javascript", "css"); CodeMirror.defineMIME("text/html", "htmlmixed"); + +}); diff --git a/applications/admin/static/codemirror/mode/javascript/javascript.js b/applications/admin/static/codemirror/mode/javascript/javascript.js index fbf574b4..15eccf7e 100644 --- a/applications/admin/static/codemirror/mode/javascript/javascript.js +++ b/applications/admin/static/codemirror/mode/javascript/javascript.js @@ -1,9 +1,20 @@ // TODO actually recognize syntax of TypeScript constructs +(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.defineMode("javascript", function(config, parserConfig) { var indentUnit = config.indentUnit; var statementIndent = parserConfig.statementIndent; - var jsonMode = parserConfig.json; + var jsonldMode = parserConfig.jsonld; + var jsonMode = parserConfig.json || jsonldMode; var isTS = parserConfig.typescript; // Tokenizer @@ -53,6 +64,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) { }(); var isOperatorChar = /[+\-*&%=<>!?|~^]/; + var isJsonldKeyword = /^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/; function readRegexp(stream) { var escaped = false, next, inSet = false; @@ -128,6 +140,10 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) { function tokenString(quote) { return function(stream, state) { var escaped = false, next; + if (jsonldMode && stream.peek() == "@" && stream.match(isJsonldKeyword)){ + state.tokenize = tokenBase; + return ret("jsonld-keyword", "meta"); + } while ((next = stream.next()) != null) { if (next == quote && !escaped) break; escaped = !escaped && next == "\\"; @@ -195,7 +211,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) { // Parser - var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, "regexp": true, "this": true}; + var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, "regexp": true, "this": true, "jsonld-keyword": true}; function JSLexical(indented, column, type, align, prev, info) { this.indented = indented; @@ -295,11 +311,12 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) { poplex.lex = true; function expect(wanted) { - return function(type) { + function exp(type) { if (type == wanted) return cont(); else if (wanted == ";") return pass(); - else return cont(arguments.callee); + else return cont(exp); }; + return exp; } function statement(type, value) { @@ -408,7 +425,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) { cx.marked = "property"; if (value == "get" || value == "set") return cont(getterSetter); } else if (type == "number" || type == "string") { - cx.marked = type + " property"; + cx.marked = jsonldMode ? "property" : (type + " property"); } else if (type == "[") { return cont(expression, expect("]"), afterprop); } @@ -565,7 +582,8 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) { context: parserConfig.localVars && {vars: parserConfig.localVars}, indented: 0 }; - if (parserConfig.globalVars) state.globalVars = parserConfig.globalVars; + if (parserConfig.globalVars && typeof parserConfig.globalVars == "object") + state.globalVars = parserConfig.globalVars; return state; }, @@ -616,6 +634,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) { fold: "brace", helperType: jsonMode ? "json" : "javascript", + jsonldMode: jsonldMode, jsonMode: jsonMode }; }); @@ -626,5 +645,8 @@ CodeMirror.defineMIME("application/javascript", "javascript"); CodeMirror.defineMIME("application/ecmascript", "javascript"); CodeMirror.defineMIME("application/json", {name: "javascript", json: true}); CodeMirror.defineMIME("application/x-json", {name: "javascript", json: true}); +CodeMirror.defineMIME("application/ld+json", {name: "javascript", jsonld: true}); CodeMirror.defineMIME("text/typescript", { name: "javascript", typescript: true }); CodeMirror.defineMIME("application/typescript", { name: "javascript", typescript: true }); + +}); diff --git a/applications/admin/static/codemirror/mode/meta.js b/applications/admin/static/codemirror/mode/meta.js index f37cea55..142d9595 100644 --- a/applications/admin/static/codemirror/mode/meta.js +++ b/applications/admin/static/codemirror/mode/meta.js @@ -1,3 +1,13 @@ +(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'}, {name: 'Asterisk', mime: 'text/x-asterisk', mode: 'asterisk'}, @@ -36,10 +46,11 @@ CodeMirror.modeInfo = [ {name: 'JavaScript', mime: 'text/javascript', mode: 'javascript'}, {name: 'JSON', mime: 'application/x-json', mode: 'javascript'}, {name: 'JSON', mime: 'application/json', mode: 'javascript'}, + {name: 'JSON-LD', mime: 'application/ld+json', mode: 'javascript'}, {name: 'TypeScript', mime: 'application/typescript', mode: 'javascript'}, {name: 'Jinja2', mime: null, mode: 'jinja2'}, {name: 'Julia', mime: 'text/x-julia', mode: 'julia'}, - {name: 'LESS', mime: 'text/x-less', mode: 'less'}, + {name: 'LESS', mime: 'text/x-less', mode: 'css'}, {name: 'LiveScript', mime: 'text/x-livescript', mode: 'livescript'}, {name: 'Lua', mime: 'text/x-lua', mode: 'lua'}, {name: 'Markdown (GitHub-flavour)', mime: 'text/x-markdown', mode: 'markdown'}, @@ -57,6 +68,7 @@ CodeMirror.modeInfo = [ {name: 'Plain Text', mime: 'text/plain', mode: 'null'}, {name: 'Properties files', mime: 'text/x-properties', mode: 'properties'}, {name: 'Python', mime: 'text/x-python', mode: 'python'}, + {name: 'Puppet', mime: 'text/x-puppet', mode: 'puppet'}, {name: 'Cython', mime: 'text/x-cython', mode: 'python'}, {name: 'R', mime: 'text/x-rsrc', mode: 'r'}, {name: 'reStructuredText', mime: 'text/x-rst', mode: 'rst'}, @@ -70,6 +82,7 @@ CodeMirror.modeInfo = [ {name: 'Smalltalk', mime: 'text/x-stsrc', mode: 'smalltalk'}, {name: 'Smarty', mime: 'text/x-smarty', mode: 'smarty'}, {name: 'SmartyMixed', mime: 'text/x-smarty', mode: 'smartymixed'}, + {name: 'Solr', mime: 'text/x-solr', mode: 'solr'}, {name: 'SPARQL', mime: 'application/x-sparql-query', mode: 'sparql'}, {name: 'SQL', mime: 'text/x-sql', mode: 'sql'}, {name: 'MariaDB', mime: 'text/x-mariadb', mode: 'sql'}, @@ -85,8 +98,9 @@ CodeMirror.modeInfo = [ {name: 'Velocity', mime: 'text/velocity', mode: 'velocity'}, {name: 'Verilog', mime: 'text/x-verilog', mode: 'verilog'}, {name: 'XML', mime: 'application/xml', mode: 'xml'}, - {name: 'HTML', mime: 'text/html', mode: 'xml'}, {name: 'XQuery', mime: 'application/xquery', mode: 'xquery'}, {name: 'YAML', mime: 'text/x-yaml', mode: 'yaml'}, {name: 'Z80', mime: 'text/x-z80', mode: 'z80'} ]; + +}); diff --git a/applications/admin/static/codemirror/mode/python/python.js b/applications/admin/static/codemirror/mode/python/python.js index 8bea5d19..f5530bcf 100644 --- a/applications/admin/static/codemirror/mode/python/python.js +++ b/applications/admin/static/codemirror/mode/python/python.js @@ -1,3 +1,14 @@ +(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.defineMode("python", function(conf, parserConf) { var ERRORCLASS = 'error'; @@ -151,6 +162,10 @@ CodeMirror.defineMode("python", function(conf, parserConf) { return 'builtin'; } + if (stream.match(/^(self|cls)\b/)) { + return "variable-2"; + } + if (stream.match(identifiers)) { if (state.lastToken == 'def' || state.lastToken == 'class') { return 'def'; @@ -361,14 +376,13 @@ CodeMirror.defineMode("python", function(conf, parserConf) { CodeMirror.defineMIME("text/x-python", "python"); -(function() { - "use strict"; - var words = function(str){return str.split(' ');}; +var words = function(str){return str.split(' ');}; - CodeMirror.defineMIME("text/x-cython", { - name: "python", - extra_keywords: words("by cdef cimport cpdef ctypedef enum except"+ - "extern gil include nogil property public"+ - "readonly struct union DEF IF ELIF ELSE") - }); -})(); +CodeMirror.defineMIME("text/x-cython", { + name: "python", + extra_keywords: words("by cdef cimport cpdef ctypedef enum except"+ + "extern gil include nogil property public"+ + "readonly struct union DEF IF ELIF ELSE") +}); + +}); diff --git a/applications/admin/static/codemirror/mode/xml/xml.js b/applications/admin/static/codemirror/mode/xml/xml.js index 96b51ff9..880b74d8 100644 --- a/applications/admin/static/codemirror/mode/xml/xml.js +++ b/applications/admin/static/codemirror/mode/xml/xml.js @@ -1,7 +1,18 @@ +(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.defineMode("xml", function(config, parserConfig) { var indentUnit = config.indentUnit; var multilineTagIndentFactor = parserConfig.multilineTagIndentFactor || 1; - var multilineTagIndentPastTag = parserConfig.multilineTagIndentPastTag || true; + var multilineTagIndentPastTag = parserConfig.multilineTagIndentPastTag; + if (multilineTagIndentPastTag == null) multilineTagIndentPastTag = true; var Kludges = parserConfig.htmlMode ? { autoSelfClosers: {'area': true, 'base': true, 'br': true, 'col': true, 'command': true, @@ -33,14 +44,16 @@ CodeMirror.defineMode("xml", function(config, parserConfig) { }, doNotIndent: {"pre": true}, allowUnquoted: true, - allowMissing: true + allowMissing: true, + caseFold: true } : { autoSelfClosers: {}, implicitlyClosed: {}, contextGrabbers: {}, doNotIndent: {}, allowUnquoted: false, - allowMissing: false + allowMissing: false, + caseFold: false }; var alignCDATA = parserConfig.alignCDATA; @@ -76,6 +89,7 @@ CodeMirror.defineMode("xml", function(config, parserConfig) { tagName = ""; var c; while ((c = stream.eat(/[^\s\u00a0=<>\"\'\/?]/))) tagName += c; + if (Kludges.caseFold) tagName = tagName.toLowerCase(); if (!tagName) return "tag error"; type = isClose ? "closeTag" : "openTag"; state.tokenize = inTag; @@ -188,7 +202,7 @@ CodeMirror.defineMode("xml", function(config, parserConfig) { if (!state.context) { return; } - parentTagName = state.context.tagName.toLowerCase(); + parentTagName = state.context.tagName; if (!Kludges.contextGrabbers.hasOwnProperty(parentTagName) || !Kludges.contextGrabbers[parentTagName].hasOwnProperty(nextTagName)) { return; @@ -206,7 +220,7 @@ CodeMirror.defineMode("xml", function(config, parserConfig) { var err = false; if (state.context) { if (state.context.tagName != tagName) { - if (Kludges.implicitlyClosed.hasOwnProperty(state.context.tagName.toLowerCase())) + if (Kludges.implicitlyClosed.hasOwnProperty(state.context.tagName)) popContext(state); err = !state.context || state.context.tagName != tagName; } @@ -219,6 +233,7 @@ CodeMirror.defineMode("xml", function(config, parserConfig) { return baseState; } } + function closeState(type, _stream, state) { if (type != "endTag") { setStyle = "error"; @@ -240,10 +255,10 @@ CodeMirror.defineMode("xml", function(config, parserConfig) { var tagName = state.tagName, tagStart = state.tagStart; state.tagName = state.tagStart = null; if (type == "selfcloseTag" || - Kludges.autoSelfClosers.hasOwnProperty(tagName.toLowerCase())) { - maybePopContext(state, tagName.toLowerCase()); + Kludges.autoSelfClosers.hasOwnProperty(tagName)) { + maybePopContext(state, tagName); } else { - maybePopContext(state, tagName.toLowerCase()); + maybePopContext(state, tagName); state.context = new Context(state, tagName, tagStart == state.indented); } return baseState; @@ -330,3 +345,5 @@ CodeMirror.defineMIME("text/xml", "xml"); CodeMirror.defineMIME("application/xml", "xml"); if (!CodeMirror.mimeModes.hasOwnProperty("text/html")) CodeMirror.defineMIME("text/html", {name: "xml", htmlMode: true}); + +}); diff --git a/applications/admin/static/codemirror/theme/mdn-like.css b/applications/admin/static/codemirror/theme/mdn-like.css new file mode 100644 index 00000000..c12cb1f9 --- /dev/null +++ b/applications/admin/static/codemirror/theme/mdn-like.css @@ -0,0 +1,44 @@ +/* + MDN-LIKE Theme - Mozilla + Ported to CodeMirror by Peter Kroon + Report bugs/issues here: https://github.com/marijnh/CodeMirror/issues + GitHub: @peterkroon + + The mdn-like theme is inspired on the displayed code examples at: https://developer.mozilla.org/en-US/docs/Web/CSS/animation + +*/ +.cm-s-mdn-like.CodeMirror { color: #999; font-family: monospace; background-color: #fff; } +.cm-s-mdn-like .CodeMirror-selected { background: #cfc !important; } + +.cm-s-mdn-like .CodeMirror-gutters { background: #f8f8f8; border-left: 6px solid rgba(0,83,159,0.65); color: #333; } +.cm-s-mdn-like .CodeMirror-linenumber { color: #aaa; margin-left: 3px; } +div.cm-s-mdn-like .CodeMirror-cursor { border-left: 2px solid #222; } + +.cm-s-mdn-like .cm-keyword { color: #6262FF; } +.cm-s-mdn-like .cm-atom { color: #F90; } +.cm-s-mdn-like .cm-number { color: #ca7841; } +.cm-s-mdn-like .cm-def { color: #8DA6CE; } +.cm-s-mdn-like span.cm-variable-2, .cm-s-mdn-like span.cm-tag { color: #690; } +.cm-s-mdn-like span.cm-variable-3, .cm-s-mdn-like span.cm-def { color: #07a; } + +.cm-s-mdn-like .cm-variable { color: #07a; } +.cm-s-mdn-like .cm-property { color: #905; } +.cm-s-mdn-like .cm-qualifier { color: #690; } + +.cm-s-mdn-like .cm-operator { color: #cda869; } +.cm-s-mdn-like .cm-comment { color:#777; font-weight:normal; } +.cm-s-mdn-like .cm-string { color:#07a; font-style:italic; } +.cm-s-mdn-like .cm-string-2 { color:#bd6b18; } /*?*/ +.cm-s-mdn-like .cm-meta { color: #000; } /*?*/ +.cm-s-mdn-like .cm-builtin { color: #9B7536; } /*?*/ +.cm-s-mdn-like .cm-tag { color: #997643; } +.cm-s-mdn-like .cm-attribute { color: #d6bb6d; } /*?*/ +.cm-s-mdn-like .cm-header { color: #FF6400; } +.cm-s-mdn-like .cm-hr { color: #AEAEAE; } +.cm-s-mdn-like .cm-link { color:#ad9361; font-style:italic; text-decoration:none; } +.cm-s-mdn-like .cm-error { border-bottom: 1px solid red; } + +div.cm-s-mdn-like .CodeMirror-activeline-background {background: #efefff;} +div.cm-s-mdn-like span.CodeMirror-matchingbracket {outline:1px solid grey; color: inherit;} + +.cm-s-mdn-like.CodeMirror { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFcAAAAyCAYAAAAp8UeFAAAHvklEQVR42s2b63bcNgyEQZCSHCdt2vd/0tWF7I+Q6XgMXiTtuvU5Pl57ZQKkKHzEAOtF5KeIJBGJ8uvL599FRFREZhFx8DeXv8trn68RuGaC8TRfo3SNp9dlDDHedyLyTUTeRWStXKPZrjtpZxaRw5hPqozRs1N8/enzIiQRWcCgy4MUA0f+XWliDhyL8Lfyvx7ei/Ae3iQFHyw7U/59pQVIMEEPEz0G7XiwdRjzSfC3UTtz9vchIntxvry5iMgfIhJoEflOz2CQr3F5h/HfeFe+GTdLaKcu9L8LTeQb/R/7GgbsfKedyNdoHsN31uRPWrfZ5wsj/NzzRQHuToIdU3ahwnsKPxXCjJITuOsi7XLc7SG/v5GdALs7wf8JjTFiB5+QvTEfRyGOfX3Lrx8wxyQi3sNq46O7QahQiCsRFgqddjBouVEHOKDgXAQHD9gJCr5sMKkEdjwsarG/ww3BMHBU7OBjXnzdyY7SfCxf5/z6ATccrwlKuwC/jhznnPF4CgVzhhVf4xp2EixcBActO75iZ8/fM9zAs2OMzKdslgXWJ9XG8PQoOAMA5fGcsvORgv0doBXyHrCwfLJAOwo71QLNkb8n2Pl6EWiR7OCibtkPaz4Kc/0NNAze2gju3zOwekALDaCFPI5vjPFmgGY5AZqyGEvH1x7QfIb8YtxMnA/b+QQ0aQDAwc6JMFg8CbQZ4qoYEEHbRwNojuK3EHwd7VALSgq+MNDKzfT58T8qdpADrgW0GmgcAS1lhzztJmkAzcPNOQbsWEALBDSlMKUG0Eq4CLAQWvEVQ9WU57gZJwZtgPO3r9oBTQ9WO8TjqXINx8R0EYpiZEUWOF3FxkbJkgU9B2f41YBrIj5ZfsQa0M5kTgiAAqM3ShXLgu8XMqcrQBvJ0CL5pnTsfMB13oB8athpAq2XOQmcGmoACCLydx7nToa23ATaSIY2ichfOdPTGxlasXMLaL0MLZAOwAKIM+y8CmicobGdCcbbK9DzN+yYGVoNNI5iUKTMyYOjPse4A8SM1MmcXgU0toOq1yO/v8FOxlASyc7TgeYaAMBJHcY1CcCwGI/TK4AmDbDyKYBBtFUkRwto8gygiQEaByFgJ00BH2M8JWwQS1nafDXQCidWyOI8AcjDCSjCLk8ngObuAm3JAHAdubAmOaK06V8MNEsKPJOhobSprwQa6gD7DclRQdqcwL4zxqgBrQcabUiBLclRDKAlWp+etPkBaNMA0AKlrHwTdEByZAA4GM+SNluSY6wAzcMNewxmgig5Ks0nkrSpBvSaQHMdKTBAnLojOdYyGpQ254602ZILPdTD1hdlggdIm74jbTp8vDwF5ZYUeLWGJpWsh6XNyXgcYwVoJQTEhhTYkxzZjiU5npU2TaB979TQehlaAVq4kaGpiPwwwLkYUuBbQwocyQTv1tA0+1UFWoJF3iv1oq+qoSk8EQdJmwHkziIF7oOZk14EGitibAdjLYYK78H5vZOhtWpoI0ATGHs0Q8OMb4Ey+2bU2UYztCtA0wFAs7TplGLRVQCcqaFdGSPCeTI1QNIC52iWNzof6Uib7xjEp07mNNoUYmVosVItHrHzRlLgBn9LFyRHaQCtVUMbtTNhoXWiTOO9k/V8BdAc1Oq0ArSQs6/5SU0hckNy9NnXqQY0PGYo5dWJ7nINaN6o958FWin27aBaWRka1r5myvLOAm0j30eBJqCxHLReVclxhxOEN2JfDWjxBtAC7MIH1fVaGdoOp4qJYDgKtKPSFNID2gSnGldrCqkFZ+5UeQXQBIRrSwocbdZYQT/2LwRahBPBXoHrB8nxaGROST62DKUbQOMMzZIC9abkuELfQzQALWTnDNAm8KHWFOJgJ5+SHIvTPcmx1xQyZRhNL5Qci689aXMEaN/uNIWkEwDAvFpOZmgsBaaGnbs1NPa1Jm32gBZAIh1pCtG7TSH4aE0y1uVY4uqoFPisGlpP2rSA5qTecWn5agK6BzSpgAyD+wFaqhnYoSZ1Vwr8CmlTQbrcO3ZaX0NAEyMbYaAlyquFoLKK3SPby9CeVUPThrSJmkCAE0CrKUQadi4DrdSlWhmah0YL9z9vClH59YGbHx1J8VZTyAjQepJjmXwAKTDQI3omc3p1U4gDUf6RfcdYfrUp5ClAi2J3Ba6UOXGo+K+bQrjjssitG2SJzshaLwMtXgRagUNpYYoVkMSBLM+9GGiJZMvduG6DRZ4qc04DMPtQQxOjEtACmhO7K1AbNbQDEggZyJwscFpAGwENhoBeUwh3bWolhe8BTYVKxQEWrSUn/uhcM5KhvUu/+eQu0Lzhi+VrK0PrZZNDQKs9cpYUuFYgMVpD4/NxenJTiMCNqdUEUf1qZWjppLT5qSkkUZbCwkbZMSuVnu80hfSkzRbQeqCZSAh6huR4VtoM2gHAlLf72smuWgE+VV7XpE25Ab2WFDgyhnSuKbs4GuGzCjR+tIoUuMFg3kgcWKLTwRqanJQ2W00hAsenfaApRC42hbCvK1SlE0HtE9BGgneJO+ELamitD1YjjOYnNYVcraGhtKkW0EqVVeDx733I2NH581k1NNxNLG0i0IJ8/NjVaOZ0tYZ2Vtr0Xv7tPV3hkWp9EFkgS/J0vosngTaSoaG06WHi+xObQkaAdlbanP8B2+2l0f90LmUAAAAASUVORK5CYII=); } diff --git a/applications/admin/static/codemirror/theme/solarized.css b/applications/admin/static/codemirror/theme/solarized.css index af30d621..9c2e9148 100644 --- a/applications/admin/static/codemirror/theme/solarized.css +++ b/applications/admin/static/codemirror/theme/solarized.css @@ -92,6 +92,7 @@ http://ethanschoonover.com/solarized/img/solarized-palette.png .cm-s-solarized .cm-tab:before { content: "➤"; /*visualize tab character*/ color: #586e75; + position:absolute; } .cm-s-solarized .cm-error, .cm-s-solarized .cm-invalidchar {