summaryrefslogtreecommitdiffstats
path: root/devtools/client/sourceeditor
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/sourceeditor')
-rw-r--r--devtools/client/sourceeditor/.eslintrc.js15
-rw-r--r--devtools/client/sourceeditor/autocomplete.js405
-rw-r--r--devtools/client/sourceeditor/codemirror/LICENSE23
-rw-r--r--devtools/client/sourceeditor/codemirror/README114
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/comment/comment.js203
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/comment/continuecomment.js85
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/dialog/dialog.css32
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/dialog/dialog.js157
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/edit/closebrackets.js195
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/edit/closetag.js169
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/edit/continuelist.js51
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/edit/matchbrackets.js120
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/edit/matchtags.js66
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/edit/trailingspace.js27
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/fold/brace-fold.js105
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/fold/comment-fold.js59
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/fold/foldcode.js150
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/fold/foldgutter.css20
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/fold/foldgutter.js146
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/fold/indent-fold.js44
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/fold/markdown-fold.js49
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/fold/xml-fold.js182
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/hint/show-hint.js437
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/search/match-highlighter.js146
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/search/search.js246
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/search/searchcursor.js189
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/selection/active-line.js74
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/selection/mark-selection.js118
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/tern/tern.css87
-rw-r--r--devtools/client/sourceeditor/codemirror/addon/tern/tern.js701
-rw-r--r--devtools/client/sourceeditor/codemirror/codemirror.bundle.js20152
-rw-r--r--devtools/client/sourceeditor/codemirror/keymap/emacs.js412
-rw-r--r--devtools/client/sourceeditor/codemirror/keymap/sublime.js580
-rw-r--r--devtools/client/sourceeditor/codemirror/keymap/vim.js5065
-rw-r--r--devtools/client/sourceeditor/codemirror/lib/codemirror.css347
-rw-r--r--devtools/client/sourceeditor/codemirror/lib/codemirror.js8922
-rw-r--r--devtools/client/sourceeditor/codemirror/mode/clike/clike.js786
-rw-r--r--devtools/client/sourceeditor/codemirror/mode/css/css.js825
-rw-r--r--devtools/client/sourceeditor/codemirror/mode/htmlmixed/htmlmixed.js152
-rw-r--r--devtools/client/sourceeditor/codemirror/mode/javascript/javascript.js748
-rw-r--r--devtools/client/sourceeditor/codemirror/mode/wasm/wasm.js203
-rw-r--r--devtools/client/sourceeditor/codemirror/mode/xml/xml.js394
-rw-r--r--devtools/client/sourceeditor/codemirror/mozilla.css263
-rw-r--r--devtools/client/sourceeditor/css-autocompleter.js1214
-rw-r--r--devtools/client/sourceeditor/debugger.js336
-rw-r--r--devtools/client/sourceeditor/editor.js1410
-rw-r--r--devtools/client/sourceeditor/moz.build18
-rw-r--r--devtools/client/sourceeditor/tern/README13
-rw-r--r--devtools/client/sourceeditor/tern/browser.js2921
-rwxr-xr-xdevtools/client/sourceeditor/tern/comment.js87
-rwxr-xr-xdevtools/client/sourceeditor/tern/condense.js304
-rwxr-xr-xdevtools/client/sourceeditor/tern/def.js656
-rw-r--r--devtools/client/sourceeditor/tern/ecma5.js950
-rwxr-xr-xdevtools/client/sourceeditor/tern/infer.js2119
-rw-r--r--devtools/client/sourceeditor/tern/moz.build18
-rwxr-xr-xdevtools/client/sourceeditor/tern/signal.js51
-rwxr-xr-xdevtools/client/sourceeditor/tern/tern.js1056
-rw-r--r--devtools/client/sourceeditor/tern/tests/unit/head_tern.js3
-rw-r--r--devtools/client/sourceeditor/tern/tests/unit/test_autocompletion.js26
-rw-r--r--devtools/client/sourceeditor/tern/tests/unit/test_import_tern.js16
-rw-r--r--devtools/client/sourceeditor/tern/tests/unit/xpcshell.ini8
-rw-r--r--devtools/client/sourceeditor/test/.eslintrc.js6
-rw-r--r--devtools/client/sourceeditor/test/browser.ini48
-rw-r--r--devtools/client/sourceeditor/test/browser_codemirror.js18
-rw-r--r--devtools/client/sourceeditor/test/browser_css_autocompletion.js145
-rw-r--r--devtools/client/sourceeditor/test/browser_css_getInfo.js176
-rw-r--r--devtools/client/sourceeditor/test/browser_css_statemachine.js109
-rw-r--r--devtools/client/sourceeditor/test/browser_detectindent.js102
-rw-r--r--devtools/client/sourceeditor/test/browser_editor_addons.js34
-rw-r--r--devtools/client/sourceeditor/test/browser_editor_autocomplete_basic.js59
-rw-r--r--devtools/client/sourceeditor/test/browser_editor_autocomplete_events.js126
-rw-r--r--devtools/client/sourceeditor/test/browser_editor_autocomplete_js.js45
-rw-r--r--devtools/client/sourceeditor/test/browser_editor_basic.js62
-rw-r--r--devtools/client/sourceeditor/test/browser_editor_cursor.js44
-rw-r--r--devtools/client/sourceeditor/test/browser_editor_find_again.js215
-rw-r--r--devtools/client/sourceeditor/test/browser_editor_goto_line.js131
-rw-r--r--devtools/client/sourceeditor/test/browser_editor_history.js32
-rw-r--r--devtools/client/sourceeditor/test/browser_editor_markers.js39
-rw-r--r--devtools/client/sourceeditor/test/browser_editor_movelines.js63
-rw-r--r--devtools/client/sourceeditor/test/browser_editor_prefs.js121
-rw-r--r--devtools/client/sourceeditor/test/browser_editor_script_injection.js27
-rw-r--r--devtools/client/sourceeditor/test/browser_vimemacs.js17
-rw-r--r--devtools/client/sourceeditor/test/cm_mode_ruby.js285
-rw-r--r--devtools/client/sourceeditor/test/cm_script_injection_test.js8
-rw-r--r--devtools/client/sourceeditor/test/codemirror/codemirror.html210
-rw-r--r--devtools/client/sourceeditor/test/codemirror/comment_test.js100
-rw-r--r--devtools/client/sourceeditor/test/codemirror/doc_test.js371
-rw-r--r--devtools/client/sourceeditor/test/codemirror/driver.js138
-rw-r--r--devtools/client/sourceeditor/test/codemirror/emacs_test.js147
-rw-r--r--devtools/client/sourceeditor/test/codemirror/mode/javascript/test.js210
-rw-r--r--devtools/client/sourceeditor/test/codemirror/mode_test.css23
-rw-r--r--devtools/client/sourceeditor/test/codemirror/mode_test.js192
-rw-r--r--devtools/client/sourceeditor/test/codemirror/multi_test.js285
-rw-r--r--devtools/client/sourceeditor/test/codemirror/search_test.js62
-rw-r--r--devtools/client/sourceeditor/test/codemirror/sublime_test.js307
-rw-r--r--devtools/client/sourceeditor/test/codemirror/test.js2151
-rw-r--r--devtools/client/sourceeditor/test/codemirror/vim_test.js4011
-rw-r--r--devtools/client/sourceeditor/test/codemirror/vimemacs.html212
-rw-r--r--devtools/client/sourceeditor/test/css_autocompletion_tests.json39
-rw-r--r--devtools/client/sourceeditor/test/css_statemachine_testcases.css121
-rw-r--r--devtools/client/sourceeditor/test/css_statemachine_tests.json84
-rw-r--r--devtools/client/sourceeditor/test/head.js163
-rw-r--r--devtools/client/sourceeditor/test/helper_codemirror_runner.js38
103 files changed, 65246 insertions, 0 deletions
diff --git a/devtools/client/sourceeditor/.eslintrc.js b/devtools/client/sourceeditor/.eslintrc.js
new file mode 100644
index 000000000..e705169c7
--- /dev/null
+++ b/devtools/client/sourceeditor/.eslintrc.js
@@ -0,0 +1,15 @@
+"use strict";
+
+module.exports = {
+ // Extend from the devtools eslintrc.
+ "extends": "../../.eslintrc.js",
+
+ "rules": {
+ // The inspector is being migrated to HTML and cleaned of
+ // chrome-privileged code, so this rule disallows requiring chrome
+ // code. Some files here disable this rule still. The
+ // goal is to enable the rule globally on all files.
+ /* eslint-disable max-len */
+ "mozilla/reject-some-requires": ["error", "^(chrome|chrome:.*|resource:.*|devtools/server/.*|.*\\.jsm)$"],
+ },
+};
diff --git a/devtools/client/sourceeditor/autocomplete.js b/devtools/client/sourceeditor/autocomplete.js
new file mode 100644
index 000000000..357f25ed1
--- /dev/null
+++ b/devtools/client/sourceeditor/autocomplete.js
@@ -0,0 +1,405 @@
+/* vim:set ts=2 sw=2 sts=2 et tw=80:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const CSSCompleter = require("devtools/client/sourceeditor/css-autocompleter");
+const { AutocompletePopup } = require("devtools/client/shared/autocomplete-popup");
+const {KeyCodes} = require("devtools/client/shared/keycodes");
+
+const CM_TERN_SCRIPTS = [
+ "chrome://devtools/content/sourceeditor/codemirror/addon/tern/tern.js",
+ "chrome://devtools/content/sourceeditor/codemirror/addon/hint/show-hint.js"
+];
+
+const autocompleteMap = new WeakMap();
+
+/**
+ * Prepares an editor instance for autocompletion.
+ */
+function initializeAutoCompletion(ctx, options = {}) {
+ let { cm, ed, Editor } = ctx;
+ if (autocompleteMap.has(ed)) {
+ return;
+ }
+
+ let win = ed.container.contentWindow.wrappedJSObject;
+ let { CodeMirror, document } = win;
+
+ let completer = null;
+ let autocompleteKey = "Ctrl-" +
+ Editor.keyFor("autocompletion", { noaccel: true });
+ if (ed.config.mode == Editor.modes.js) {
+ let defs = [
+ require("./tern/browser"),
+ require("./tern/ecma5"),
+ ];
+
+ CM_TERN_SCRIPTS.forEach(ed.loadScript, ed);
+ win.tern = require("./tern/tern");
+ cm.tern = new CodeMirror.TernServer({
+ defs: defs,
+ typeTip: function (data) {
+ let tip = document.createElement("span");
+ tip.className = "CodeMirror-Tern-information";
+ let tipType = document.createElement("strong");
+ let tipText = document.createTextNode(data.type ||
+ cm.l10n("autocompletion.notFound"));
+ tipType.appendChild(tipText);
+ tip.appendChild(tipType);
+
+ if (data.doc) {
+ tip.appendChild(document.createTextNode(" — " + data.doc));
+ }
+
+ if (data.url) {
+ tip.appendChild(document.createTextNode(" "));
+ let docLink = document.createElement("a");
+ docLink.textContent = "[" + cm.l10n("autocompletion.docsLink") + "]";
+ docLink.href = data.url;
+ docLink.className = "theme-link";
+ docLink.setAttribute("target", "_blank");
+ tip.appendChild(docLink);
+ }
+
+ return tip;
+ }
+ });
+
+ let keyMap = {};
+ let updateArgHintsCallback = cm.tern.updateArgHints.bind(cm.tern, cm);
+ cm.on("cursorActivity", updateArgHintsCallback);
+
+ keyMap[autocompleteKey] = cmArg => {
+ cmArg.tern.getHint(cmArg, data => {
+ CodeMirror.on(data, "shown", () => ed.emit("before-suggest"));
+ CodeMirror.on(data, "close", () => ed.emit("after-suggest"));
+ CodeMirror.on(data, "select", () => ed.emit("suggestion-entered"));
+ CodeMirror.showHint(cmArg, (cmIgnore, cb) => cb(data), { async: true });
+ });
+ };
+
+ keyMap[Editor.keyFor("showInformation2", { noaccel: true })] = cmArg => {
+ cmArg.tern.showType(cmArg, null, () => {
+ ed.emit("show-information");
+ });
+ };
+ cm.addKeyMap(keyMap);
+
+ let destroyTern = function () {
+ ed.off("destroy", destroyTern);
+ cm.off("cursorActivity", updateArgHintsCallback);
+ cm.removeKeyMap(keyMap);
+ win.tern = cm.tern = null;
+ autocompleteMap.delete(ed);
+ };
+
+ ed.on("destroy", destroyTern);
+
+ autocompleteMap.set(ed, {
+ destroy: destroyTern
+ });
+
+ // TODO: Integrate tern autocompletion with this autocomplete API.
+ return;
+ } else if (ed.config.mode == Editor.modes.css) {
+ completer = new CSSCompleter({walker: options.walker,
+ cssProperties: options.cssProperties});
+ }
+
+ function insertSelectedPopupItem() {
+ let autocompleteState = autocompleteMap.get(ed);
+ if (!popup || !popup.isOpen || !autocompleteState) {
+ return false;
+ }
+
+ if (!autocompleteState.suggestionInsertedOnce && popup.selectedItem) {
+ autocompleteMap.get(ed).insertingSuggestion = true;
+ insertPopupItem(ed, popup.selectedItem);
+ }
+
+ popup.once("popup-closed", () => {
+ // This event is used in tests.
+ ed.emit("popup-hidden");
+ });
+ popup.hidePopup();
+ return true;
+ }
+
+ // Give each popup a new name to avoid sharing the elements.
+
+ let popup = new AutocompletePopup(win.parent.document, {
+ position: "bottom",
+ theme: "auto",
+ autoSelect: true,
+ onClick: insertSelectedPopupItem
+ });
+
+ let cycle = reverse => {
+ if (popup && popup.isOpen) {
+ cycleSuggestions(ed, reverse == true);
+ return null;
+ }
+
+ return CodeMirror.Pass;
+ };
+
+ let keyMap = {
+ "Tab": cycle,
+ "Down": cycle,
+ "Shift-Tab": cycle.bind(null, true),
+ "Up": cycle.bind(null, true),
+ "Enter": () => {
+ let wasHandled = insertSelectedPopupItem();
+ return wasHandled ? true : CodeMirror.Pass;
+ }
+ };
+
+ let autoCompleteCallback = autoComplete.bind(null, ctx);
+ let keypressCallback = onEditorKeypress.bind(null, ctx);
+ keyMap[autocompleteKey] = autoCompleteCallback;
+ cm.addKeyMap(keyMap);
+
+ cm.on("keydown", keypressCallback);
+ ed.on("change", autoCompleteCallback);
+ ed.on("destroy", destroy);
+
+ function destroy() {
+ ed.off("destroy", destroy);
+ cm.off("keydown", keypressCallback);
+ ed.off("change", autoCompleteCallback);
+ cm.removeKeyMap(keyMap);
+ popup.destroy();
+ keyMap = popup = completer = null;
+ autocompleteMap.delete(ed);
+ }
+
+ autocompleteMap.set(ed, {
+ popup: popup,
+ completer: completer,
+ keyMap: keyMap,
+ destroy: destroy,
+ insertingSuggestion: false,
+ suggestionInsertedOnce: false
+ });
+}
+
+/**
+ * Destroy autocompletion on an editor instance.
+ */
+function destroyAutoCompletion(ctx) {
+ let { ed } = ctx;
+ if (!autocompleteMap.has(ed)) {
+ return;
+ }
+
+ let {destroy} = autocompleteMap.get(ed);
+ destroy();
+}
+
+/**
+ * Provides suggestions to autocomplete the current token/word being typed.
+ */
+function autoComplete({ ed, cm }) {
+ let autocompleteOpts = autocompleteMap.get(ed);
+ let { completer, popup } = autocompleteOpts;
+ if (!completer || autocompleteOpts.insertingSuggestion ||
+ autocompleteOpts.doNotAutocomplete) {
+ autocompleteOpts.insertingSuggestion = false;
+ return;
+ }
+ let cur = ed.getCursor();
+ completer.complete(cm.getRange({line: 0, ch: 0}, cur), cur).then(suggestions => {
+ if (!suggestions || !suggestions.length || suggestions[0].preLabel == null) {
+ autocompleteOpts.suggestionInsertedOnce = false;
+ popup.once("popup-closed", () => {
+ // This event is used in tests.
+ ed.emit("after-suggest");
+ });
+ popup.hidePopup();
+ return;
+ }
+ // The cursor is at the end of the currently entered part of the token,
+ // like "backgr|" but we need to open the popup at the beginning of the
+ // character "b". Thus we need to calculate the width of the entered part
+ // of the token ("backgr" here). 4 comes from the popup's left padding.
+
+ let cursorElement = cm.display.cursorDiv.querySelector(".CodeMirror-cursor");
+ let left = suggestions[0].preLabel.length * cm.defaultCharWidth() + 4;
+ popup.hidePopup();
+ popup.setItems(suggestions);
+
+ popup.once("popup-opened", () => {
+ // This event is used in tests.
+ ed.emit("after-suggest");
+ });
+ popup.openPopup(cursorElement, -1 * left, 0);
+ autocompleteOpts.suggestionInsertedOnce = false;
+ }).then(null, e => console.error(e));
+}
+
+/**
+ * Inserts a popup item into the current cursor location
+ * in the editor.
+ */
+function insertPopupItem(ed, popupItem) {
+ let {preLabel, text} = popupItem;
+ let cur = ed.getCursor();
+ let textBeforeCursor = ed.getText(cur.line).substring(0, cur.ch);
+ let backwardsTextBeforeCursor = textBeforeCursor.split("").reverse().join("");
+ let backwardsPreLabel = preLabel.split("").reverse().join("");
+
+ // If there is additional text in the preLabel vs the line, then
+ // just insert the entire autocomplete text. An example:
+ // if you type 'a' and select '#about' from the autocomplete menu,
+ // then the final text needs to the end up as '#about'.
+ if (backwardsPreLabel.indexOf(backwardsTextBeforeCursor) === 0) {
+ ed.replaceText(text, {line: cur.line, ch: 0}, cur);
+ } else {
+ ed.replaceText(text.slice(preLabel.length), cur, cur);
+ }
+}
+
+/**
+ * Cycles through provided suggestions by the popup in a top to bottom manner
+ * when `reverse` is not true. Opposite otherwise.
+ */
+function cycleSuggestions(ed, reverse) {
+ let autocompleteOpts = autocompleteMap.get(ed);
+ let { popup } = autocompleteOpts;
+ let cur = ed.getCursor();
+ autocompleteOpts.insertingSuggestion = true;
+ if (!autocompleteOpts.suggestionInsertedOnce) {
+ autocompleteOpts.suggestionInsertedOnce = true;
+ let firstItem;
+ if (reverse) {
+ firstItem = popup.getItemAtIndex(popup.itemCount - 1);
+ popup.selectPreviousItem();
+ } else {
+ firstItem = popup.getItemAtIndex(0);
+ if (firstItem.label == firstItem.preLabel && popup.itemCount > 1) {
+ firstItem = popup.getItemAtIndex(1);
+ popup.selectNextItem();
+ }
+ }
+ if (popup.itemCount == 1) {
+ popup.hidePopup();
+ }
+ insertPopupItem(ed, firstItem);
+ } else {
+ let fromCur = {
+ line: cur.line,
+ ch: cur.ch - popup.selectedItem.text.length
+ };
+ if (reverse) {
+ popup.selectPreviousItem();
+ } else {
+ popup.selectNextItem();
+ }
+ ed.replaceText(popup.selectedItem.text, fromCur, cur);
+ }
+ // This event is used in tests.
+ ed.emit("suggestion-entered");
+}
+
+/**
+ * onkeydown handler for the editor instance to prevent autocompleting on some
+ * keypresses.
+ */
+function onEditorKeypress({ ed, Editor }, cm, event) {
+ let autocompleteOpts = autocompleteMap.get(ed);
+
+ // Do not try to autocomplete with multiple selections.
+ if (ed.hasMultipleSelections()) {
+ autocompleteOpts.doNotAutocomplete = true;
+ autocompleteOpts.popup.hidePopup();
+ return;
+ }
+
+ if ((event.ctrlKey || event.metaKey) && event.keyCode == KeyCodes.DOM_VK_SPACE) {
+ // When Ctrl/Cmd + Space is pressed, two simultaneous keypresses are emitted
+ // first one for just the Ctrl/Cmd and second one for combo. The first one
+ // leave the autocompleteOpts.doNotAutocomplete as true, so we have to make
+ // it false
+ autocompleteOpts.doNotAutocomplete = false;
+ return;
+ }
+
+ if (event.ctrlKey || event.metaKey || event.altKey) {
+ autocompleteOpts.doNotAutocomplete = true;
+ autocompleteOpts.popup.hidePopup();
+ return;
+ }
+
+ switch (event.keyCode) {
+ case KeyCodes.DOM_VK_RETURN:
+ autocompleteOpts.doNotAutocomplete = true;
+ break;
+ case KeyCodes.DOM_VK_ESCAPE:
+ if (autocompleteOpts.popup.isOpen) {
+ event.preventDefault();
+ }
+ break;
+ case KeyCodes.DOM_VK_LEFT:
+ case KeyCodes.DOM_VK_RIGHT:
+ case KeyCodes.DOM_VK_HOME:
+ case KeyCodes.DOM_VK_END:
+ autocompleteOpts.doNotAutocomplete = true;
+ autocompleteOpts.popup.hidePopup();
+ break;
+ case KeyCodes.DOM_VK_BACK_SPACE:
+ case KeyCodes.DOM_VK_DELETE:
+ if (ed.config.mode == Editor.modes.css) {
+ autocompleteOpts.completer.invalidateCache(ed.getCursor().line);
+ }
+ autocompleteOpts.doNotAutocomplete = true;
+ autocompleteOpts.popup.hidePopup();
+ break;
+ default:
+ autocompleteOpts.doNotAutocomplete = false;
+ }
+}
+
+/**
+ * Returns the private popup. This method is used by tests to test the feature.
+ */
+function getPopup({ ed }) {
+ if (autocompleteMap.has(ed)) {
+ return autocompleteMap.get(ed).popup;
+ }
+
+ return null;
+}
+
+/**
+ * Returns contextual information about the token covered by the caret if the
+ * implementation of completer supports it.
+ */
+function getInfoAt({ ed }, caret) {
+ if (autocompleteMap.has(ed)) {
+ let completer = autocompleteMap.get(ed).completer;
+ if (completer && completer.getInfoAt) {
+ return completer.getInfoAt(ed.getText(), caret);
+ }
+ }
+
+ return null;
+}
+
+/**
+ * Returns whether autocompletion is enabled for this editor.
+ * Used for testing
+ */
+function isAutocompletionEnabled({ ed }) {
+ return autocompleteMap.has(ed);
+}
+
+// Export functions
+
+module.exports.initializeAutoCompletion = initializeAutoCompletion;
+module.exports.destroyAutoCompletion = destroyAutoCompletion;
+module.exports.getAutocompletionPopup = getPopup;
+module.exports.getInfoAt = getInfoAt;
+module.exports.isAutocompletionEnabled = isAutocompletionEnabled;
diff --git a/devtools/client/sourceeditor/codemirror/LICENSE b/devtools/client/sourceeditor/codemirror/LICENSE
new file mode 100644
index 000000000..4db615dd2
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/LICENSE
@@ -0,0 +1,23 @@
+Copyright (C) 2015 by Marijn Haverbeke <marijnh@gmail.com> and others
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+Please note that some subdirectories of the CodeMirror distribution
+include their own LICENSE files, and are released under different
+licences.
diff --git a/devtools/client/sourceeditor/codemirror/README b/devtools/client/sourceeditor/codemirror/README
new file mode 100644
index 000000000..38ab10e9c
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/README
@@ -0,0 +1,114 @@
+This is the CodeMirror editor packaged for the Mozilla Project. CodeMirror
+is a JavaScript component that provides a code editor in the browser. When
+a mode is available for the language you are coding in, it will color your
+code, and optionally help with indentation.
+
+# Upgrade
+
+Currently used version is 5.16.0. To upgrade: download a new version of
+CodeMirror from the project's page [1] and replace all JavaScript and
+CSS files inside the codemirror directory [2].
+
+Then to recreate codemirror.bundle.js:
+ > cd devtools/client
+ > npm install
+ > webpack
+
+To confirm the functionality run mochitests for the following components:
+
+ * sourceeditor
+ * scratchpad
+ * debugger
+ * styleditor
+ * netmonitor
+
+The sourceeditor component contains imported CodeMirror tests [3].
+
+ * Some tests were commented out because we don't use that functionality
+ within Firefox (for example Ruby editing mode). Be careful when updating
+ files test/codemirror.html and test/vimemacs.html; they were modified to
+ co-exist with Mozilla's testing infrastructure. Basically, vimemacs.html
+ is a copy of codemirror.html but only with VIM and Emacs mode tests
+ enabled.
+ * In cm_comment_test.js comment out fallbackToBlock and fallbackToLine
+ tests.
+ * The search addon (search.js) was slightly modified to make search
+ UI localizable (see patch below).
+
+Other than that, we don't have any Mozilla-specific patches applied to
+CodeMirror itself.
+
+# Addons
+
+To install a new CodeMirror addon add it to the codemirror directory,
+jar.mn [4] file and editor.js [5]. Also, add it to the License section
+below.
+
+# License
+
+The following files in this directory and devtools/client/sourceeditor/test/codemirror/
+are licensed according to the contents in the LICENSE file.
+
+# Localization patches
+
+diff --git a/devtools/client/sourceeditor/codemirror/addon/search/search.js b/devtools/client/sourceeditor/codemirror/addon/search/search.js
+--- a/devtools/client/sourceeditor/codemirror/addon/search/search.js
++++ b/devtools/client/sourceeditor/codemirror/addon/search/search.js
+@@ -92,32 +92,47 @@
+ } else {
+ query = parseString(query)
+ }
+ if (typeof query == "string" ? query == "" : query.test(""))
+ query = /x^/;
+ return query;
+ }
+
+- var queryDialog =
+- 'Search: <input type="text" style="width: 10em" class="CodeMirror-search-field"/> <span style="color: #888" class="CodeMirror-search-hint">(Use /re/ syntax for regexp search)</span>';
++ var queryDialog;
+
+ function startSearch(cm, state, query) {
+ state.queryText = query;
+ state.query = parseQuery(query);
+ cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query));
+ state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query));
+ cm.addOverlay(state.overlay);
+ if (cm.showMatchesOnScrollbar) {
+ if (state.annotate) { state.annotate.clear(); state.annotate = null; }
+ state.annotate = cm.showMatchesOnScrollbar(state.query, queryCaseInsensitive(state.query));
+ }
+ }
+
+ function doSearch(cm, rev, persistent) {
++ if (!queryDialog) {
++ let doc = cm.getWrapperElement().ownerDocument;
++ let inp = doc.createElement("input");
++
++ inp.type = "search";
++ inp.placeholder = cm.l10n("findCmd.promptMessage");
++ inp.style.marginInlineStart = "1em";
++ inp.style.marginInlineEnd = "1em";
++ inp.style.flexGrow = "1";
++ inp.addEventListener("focus", () => inp.select());
++
++ queryDialog = doc.createElement("div");
++ queryDialog.appendChild(inp);
++ queryDialog.style.display = "flex";
++ }
++
+ var state = getSearchState(cm);
+ if (state.query) return findNext(cm, rev);
+ var q = cm.getSelection() || state.lastQuery;
+ if (persistent && cm.openDialog) {
+ var hiding = null
+ persistentDialog(cm, queryDialog, q, function(query, event) {
+ CodeMirror.e_stop(event);
+ if (!query) return;
+
+# Footnotes
+
+[1] http://codemirror.net
+[2] devtools/client/sourceeditor/codemirror
+[3] devtools/client/sourceeditor/test/browser_codemirror.js
+[4] devtools/client/jar.mn
+[5] devtools/client/sourceeditor/editor.js
diff --git a/devtools/client/sourceeditor/codemirror/addon/comment/comment.js b/devtools/client/sourceeditor/codemirror/addon/comment/comment.js
new file mode 100644
index 000000000..2c4f975d0
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/comment/comment.js
@@ -0,0 +1,203 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ "use strict";
+
+ var noOptions = {};
+ var nonWS = /[^\s\u00a0]/;
+ var Pos = CodeMirror.Pos;
+
+ function firstNonWS(str) {
+ var found = str.search(nonWS);
+ return found == -1 ? 0 : found;
+ }
+
+ CodeMirror.commands.toggleComment = function(cm) {
+ cm.toggleComment();
+ };
+
+ CodeMirror.defineExtension("toggleComment", function(options) {
+ if (!options) options = noOptions;
+ var cm = this;
+ var minLine = Infinity, ranges = this.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, options)) mode = "un";
+ else { cm.lineComment(from, to, options); mode = "line"; }
+ } else if (mode == "un") {
+ cm.uncomment(from, to, options);
+ } else {
+ cm.lineComment(from, to, options);
+ }
+ }
+ });
+
+ // Rough heuristic to try and detect lines that are part of multi-line string
+ function probablyInsideString(cm, pos, line) {
+ return /\bstring\b/.test(cm.getTokenTypeAt(Pos(pos.line, 0))) && !/^[\'\"`]/.test(line)
+ }
+
+ CodeMirror.defineExtension("lineComment", function(from, to, options) {
+ if (!options) options = noOptions;
+ var self = this, mode = self.getModeAt(from);
+ var firstLine = self.getLine(from.line);
+ if (firstLine == null || probablyInsideString(self, from, firstLine)) return;
+
+ var commentString = options.lineComment || mode.lineComment;
+ if (!commentString) {
+ if (options.blockCommentStart || mode.blockCommentStart) {
+ options.fullLines = true;
+ self.blockComment(from, to, options);
+ }
+ return;
+ }
+
+ var end = Math.min(to.ch != 0 || to.line == from.line ? to.line + 1 : to.line, self.lastLine() + 1);
+ var pad = options.padding == null ? " " : options.padding;
+ var blankLines = options.commentBlankLines || from.line == to.line;
+
+ self.operation(function() {
+ if (options.indent) {
+ var baseString = null;
+ for (var i = from.line; i < end; ++i) {
+ var line = self.getLine(i);
+ var whitespace = line.slice(0, firstNonWS(line));
+ if (baseString == null || baseString.length > whitespace.length) {
+ baseString = whitespace;
+ }
+ }
+ for (var i = from.line; i < end; ++i) {
+ var line = self.getLine(i), cut = baseString.length;
+ if (!blankLines && !nonWS.test(line)) continue;
+ if (line.slice(0, cut) != baseString) cut = firstNonWS(line);
+ self.replaceRange(baseString + commentString + pad, Pos(i, 0), Pos(i, cut));
+ }
+ } else {
+ for (var i = from.line; i < end; ++i) {
+ if (blankLines || nonWS.test(self.getLine(i)))
+ self.replaceRange(commentString + pad, Pos(i, 0));
+ }
+ }
+ });
+ });
+
+ CodeMirror.defineExtension("blockComment", function(from, to, options) {
+ if (!options) options = noOptions;
+ var self = this, mode = self.getModeAt(from);
+ var startString = options.blockCommentStart || mode.blockCommentStart;
+ var endString = options.blockCommentEnd || mode.blockCommentEnd;
+ if (!startString || !endString) {
+ if ((options.lineComment || mode.lineComment) && options.fullLines != false)
+ self.lineComment(from, to, options);
+ return;
+ }
+
+ var end = Math.min(to.line, self.lastLine());
+ if (end != from.line && to.ch == 0 && nonWS.test(self.getLine(end))) --end;
+
+ var pad = options.padding == null ? " " : options.padding;
+ if (from.line > end) return;
+
+ self.operation(function() {
+ if (options.fullLines != false) {
+ var lastLineHasText = nonWS.test(self.getLine(end));
+ self.replaceRange(pad + endString, Pos(end));
+ self.replaceRange(startString + pad, Pos(from.line, 0));
+ var lead = options.blockCommentLead || mode.blockCommentLead;
+ if (lead != null) for (var i = from.line + 1; i <= end; ++i)
+ if (i != end || lastLineHasText)
+ self.replaceRange(lead + pad, Pos(i, 0));
+ } else {
+ self.replaceRange(endString, to);
+ self.replaceRange(startString, from);
+ }
+ });
+ });
+
+ CodeMirror.defineExtension("uncomment", function(from, to, options) {
+ if (!options) options = noOptions;
+ var self = this, mode = self.getModeAt(from);
+ var end = Math.min(to.ch != 0 || to.line == from.line ? to.line : to.line - 1, self.lastLine()), start = Math.min(from.line, end);
+
+ // Try finding line comments
+ var lineString = options.lineComment || mode.lineComment, lines = [];
+ var pad = options.padding == null ? " " : options.padding, didSomething;
+ lineComment: {
+ if (!lineString) break lineComment;
+ for (var i = start; i <= end; ++i) {
+ var line = self.getLine(i);
+ var found = line.indexOf(lineString);
+ if (found > -1 && !/comment/.test(self.getTokenTypeAt(Pos(i, found + 1)))) found = -1;
+ if (found == -1 && (i != end || i == start) && nonWS.test(line)) break lineComment;
+ if (found > -1 && nonWS.test(line.slice(0, found))) break lineComment;
+ lines.push(line);
+ }
+ self.operation(function() {
+ for (var i = start; i <= end; ++i) {
+ var line = lines[i - start];
+ var pos = line.indexOf(lineString), endPos = pos + lineString.length;
+ if (pos < 0) continue;
+ if (line.slice(endPos, endPos + pad.length) == pad) endPos += pad.length;
+ didSomething = true;
+ self.replaceRange("", Pos(i, pos), Pos(i, endPos));
+ }
+ });
+ if (didSomething) return true;
+ }
+
+ // Try block comments
+ var startString = options.blockCommentStart || mode.blockCommentStart;
+ var endString = options.blockCommentEnd || mode.blockCommentEnd;
+ if (!startString || !endString) return false;
+ var lead = options.blockCommentLead || mode.blockCommentLead;
+ var startLine = self.getLine(start), endLine = end == start ? startLine : self.getLine(end);
+ var open = startLine.indexOf(startString), close = endLine.lastIndexOf(endString);
+ if (close == -1 && start != end) {
+ endLine = self.getLine(--end);
+ close = endLine.lastIndexOf(endString);
+ }
+ if (open == -1 || close == -1 ||
+ !/comment/.test(self.getTokenTypeAt(Pos(start, open + 1))) ||
+ !/comment/.test(self.getTokenTypeAt(Pos(end, close + 1))))
+ return false;
+
+ // Avoid killing block comments completely outside the selection.
+ // Positions of the last startString before the start of the selection, and the first endString after it.
+ var lastStart = startLine.lastIndexOf(startString, from.ch);
+ var firstEnd = lastStart == -1 ? -1 : startLine.slice(0, from.ch).indexOf(endString, lastStart + startString.length);
+ if (lastStart != -1 && firstEnd != -1 && firstEnd + endString.length != from.ch) return false;
+ // Positions of the first endString after the end of the selection, and the last startString before it.
+ firstEnd = endLine.indexOf(endString, to.ch);
+ var almostLastStart = endLine.slice(to.ch).lastIndexOf(startString, firstEnd - to.ch);
+ lastStart = (firstEnd == -1 || almostLastStart == -1) ? -1 : to.ch + almostLastStart;
+ if (firstEnd != -1 && lastStart != -1 && lastStart != to.ch) return false;
+
+ self.operation(function() {
+ self.replaceRange("", Pos(end, close - (pad && endLine.slice(close - pad.length, close) == pad ? pad.length : 0)),
+ Pos(end, close + endString.length));
+ var openEnd = open + startString.length;
+ if (pad && startLine.slice(openEnd, openEnd + pad.length) == pad) openEnd += pad.length;
+ self.replaceRange("", Pos(start, open), Pos(start, openEnd));
+ if (lead) for (var i = start + 1; i <= end; ++i) {
+ var line = self.getLine(i), found = line.indexOf(lead);
+ if (found == -1 || nonWS.test(line.slice(0, found))) continue;
+ var foundEnd = found + lead.length;
+ if (pad && line.slice(foundEnd, foundEnd + pad.length) == pad) foundEnd += pad.length;
+ self.replaceRange("", Pos(i, found), Pos(i, foundEnd));
+ }
+ });
+ return true;
+ });
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/comment/continuecomment.js b/devtools/client/sourceeditor/codemirror/addon/comment/continuecomment.js
new file mode 100644
index 000000000..b11d51e6c
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/comment/continuecomment.js
@@ -0,0 +1,85 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ var modes = ["clike", "css", "javascript"];
+
+ for (var i = 0; i < modes.length; ++i)
+ CodeMirror.extendMode(modes[i], {blockCommentContinue: " * "});
+
+ function continueComment(cm) {
+ if (cm.getOption("disableInput")) return CodeMirror.Pass;
+ var ranges = cm.listSelections(), mode, inserts = [];
+ for (var i = 0; i < ranges.length; i++) {
+ var pos = ranges[i].head, token = cm.getTokenAt(pos);
+ if (token.type != "comment") return CodeMirror.Pass;
+ var modeHere = CodeMirror.innerMode(cm.getMode(), token.state).mode;
+ if (!mode) mode = modeHere;
+ else if (mode != modeHere) return CodeMirror.Pass;
+
+ var insert = null;
+ if (mode.blockCommentStart && mode.blockCommentContinue) {
+ var end = token.string.indexOf(mode.blockCommentEnd);
+ var full = cm.getRange(CodeMirror.Pos(pos.line, 0), CodeMirror.Pos(pos.line, token.end)), found;
+ if (end != -1 && end == token.string.length - mode.blockCommentEnd.length && pos.ch >= end) {
+ // Comment ended, don't continue it
+ } else if (token.string.indexOf(mode.blockCommentStart) == 0) {
+ insert = full.slice(0, token.start);
+ if (!/^\s*$/.test(insert)) {
+ insert = "";
+ for (var j = 0; j < token.start; ++j) insert += " ";
+ }
+ } else if ((found = full.indexOf(mode.blockCommentContinue)) != -1 &&
+ found + mode.blockCommentContinue.length > token.start &&
+ /^\s*$/.test(full.slice(0, found))) {
+ insert = full.slice(0, found);
+ }
+ if (insert != null) insert += mode.blockCommentContinue;
+ }
+ if (insert == null && mode.lineComment && continueLineCommentEnabled(cm)) {
+ var line = cm.getLine(pos.line), found = line.indexOf(mode.lineComment);
+ if (found > -1) {
+ insert = line.slice(0, found);
+ if (/\S/.test(insert)) insert = null;
+ else insert += mode.lineComment + line.slice(found + mode.lineComment.length).match(/^\s*/)[0];
+ }
+ }
+ if (insert == null) return CodeMirror.Pass;
+ inserts[i] = "\n" + insert;
+ }
+
+ cm.operation(function() {
+ for (var i = ranges.length - 1; i >= 0; i--)
+ cm.replaceRange(inserts[i], ranges[i].from(), ranges[i].to(), "+insert");
+ });
+ }
+
+ function continueLineCommentEnabled(cm) {
+ var opt = cm.getOption("continueComments");
+ if (opt && typeof opt == "object")
+ return opt.continueLineComment !== false;
+ return true;
+ }
+
+ CodeMirror.defineOption("continueComments", null, function(cm, val, prev) {
+ if (prev && prev != CodeMirror.Init)
+ cm.removeKeyMap("continueComment");
+ if (val) {
+ var key = "Enter";
+ if (typeof val == "string")
+ key = val;
+ else if (typeof val == "object" && val.key)
+ key = val.key;
+ var map = {name: "continueComment"};
+ map[key] = continueComment;
+ cm.addKeyMap(map);
+ }
+ });
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/dialog/dialog.css b/devtools/client/sourceeditor/codemirror/addon/dialog/dialog.css
new file mode 100644
index 000000000..677c07838
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/dialog/dialog.css
@@ -0,0 +1,32 @@
+.CodeMirror-dialog {
+ position: absolute;
+ left: 0; right: 0;
+ background: inherit;
+ z-index: 15;
+ padding: .1em .8em;
+ overflow: hidden;
+ color: inherit;
+}
+
+.CodeMirror-dialog-top {
+ border-bottom: 1px solid #eee;
+ top: 0;
+}
+
+.CodeMirror-dialog-bottom {
+ border-top: 1px solid #eee;
+ bottom: 0;
+}
+
+.CodeMirror-dialog input {
+ border: none;
+ outline: none;
+ background: transparent;
+ width: 20em;
+ color: inherit;
+ font-family: monospace;
+}
+
+.CodeMirror-dialog button {
+ font-size: 70%;
+}
diff --git a/devtools/client/sourceeditor/codemirror/addon/dialog/dialog.js b/devtools/client/sourceeditor/codemirror/addon/dialog/dialog.js
new file mode 100644
index 000000000..f10bb5bf1
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/dialog/dialog.js
@@ -0,0 +1,157 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+// Open simple dialogs on top of an editor. Relies on dialog.css.
+
+(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;
+ dialog = wrap.appendChild(document.createElement("div"));
+ if (bottom)
+ dialog.className = "CodeMirror-dialog CodeMirror-dialog-bottom";
+ else
+ dialog.className = "CodeMirror-dialog CodeMirror-dialog-top";
+
+ if (typeof template == "string") {
+ dialog.innerHTML = template;
+ } else { // Assuming it's a detached DOM element.
+ dialog.appendChild(template);
+ }
+ return dialog;
+ }
+
+ function closeNotification(cm, newVal) {
+ if (cm.state.currentNotificationClose)
+ cm.state.currentNotificationClose();
+ cm.state.currentNotificationClose = newVal;
+ }
+
+ CodeMirror.defineExtension("openDialog", function(template, callback, options) {
+ if (!options) options = {};
+
+ closeNotification(this, null);
+
+ var dialog = dialogDiv(this, template, options.bottom);
+ var closed = false, me = this;
+ function close(newVal) {
+ if (typeof newVal == 'string') {
+ inp.value = newVal;
+ } else {
+ if (closed) return;
+ closed = true;
+ dialog.parentNode.removeChild(dialog);
+ me.focus();
+
+ if (options.onClose) options.onClose(dialog);
+ }
+ }
+
+ var inp = dialog.getElementsByTagName("input")[0], button;
+ if (inp) {
+ inp.focus();
+
+ if (options.value) {
+ inp.value = options.value;
+ if (options.selectValueOnOpen !== false) {
+ inp.select();
+ }
+ }
+
+ if (options.onInput)
+ CodeMirror.on(inp, "input", function(e) { options.onInput(e, inp.value, close);});
+ if (options.onKeyUp)
+ CodeMirror.on(inp, "keyup", function(e) {options.onKeyUp(e, inp.value, close);});
+
+ CodeMirror.on(inp, "keydown", function(e) {
+ if (options && options.onKeyDown && options.onKeyDown(e, inp.value, close)) { return; }
+ if (e.keyCode == 27 || (options.closeOnEnter !== false && e.keyCode == 13)) {
+ inp.blur();
+ CodeMirror.e_stop(e);
+ close();
+ }
+ if (e.keyCode == 13) callback(inp.value, e);
+ });
+
+ if (options.closeOnBlur !== false) CodeMirror.on(inp, "blur", close);
+ } else if (button = dialog.getElementsByTagName("button")[0]) {
+ CodeMirror.on(button, "click", function() {
+ close();
+ me.focus();
+ });
+
+ if (options.closeOnBlur !== false) CodeMirror.on(button, "blur", close);
+
+ button.focus();
+ }
+ return close;
+ });
+
+ CodeMirror.defineExtension("openConfirm", function(template, callbacks, options) {
+ closeNotification(this, null);
+ var dialog = dialogDiv(this, template, options && options.bottom);
+ var buttons = dialog.getElementsByTagName("button");
+ var closed = false, me = this, blurring = 1;
+ function close() {
+ if (closed) return;
+ closed = true;
+ dialog.parentNode.removeChild(dialog);
+ me.focus();
+ }
+ buttons[0].focus();
+ for (var i = 0; i < buttons.length; ++i) {
+ var b = buttons[i];
+ (function(callback) {
+ CodeMirror.on(b, "click", function(e) {
+ CodeMirror.e_preventDefault(e);
+ close();
+ if (callback) callback(me);
+ });
+ })(callbacks[i]);
+ CodeMirror.on(b, "blur", function() {
+ --blurring;
+ setTimeout(function() { if (blurring <= 0) close(); }, 200);
+ });
+ CodeMirror.on(b, "focus", function() { ++blurring; });
+ }
+ });
+
+ /*
+ * openNotification
+ * Opens a notification, that can be closed with an optional timer
+ * (default 5000ms timer) and always closes on click.
+ *
+ * If a notification is opened while another is opened, it will close the
+ * currently opened one and open the new one immediately.
+ */
+ CodeMirror.defineExtension("openNotification", function(template, options) {
+ closeNotification(this, close);
+ var dialog = dialogDiv(this, template, options && options.bottom);
+ var closed = false, doneTimer;
+ var duration = options && typeof options.duration !== "undefined" ? options.duration : 5000;
+
+ function close() {
+ if (closed) return;
+ closed = true;
+ clearTimeout(doneTimer);
+ dialog.parentNode.removeChild(dialog);
+ }
+
+ CodeMirror.on(dialog, 'click', function(e) {
+ CodeMirror.e_preventDefault(e);
+ close();
+ });
+
+ if (duration)
+ doneTimer = setTimeout(close, duration);
+
+ return close;
+ });
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/edit/closebrackets.js b/devtools/client/sourceeditor/codemirror/addon/edit/closebrackets.js
new file mode 100644
index 000000000..af7fce2a8
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/edit/closebrackets.js
@@ -0,0 +1,195 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ var defaults = {
+ pairs: "()[]{}''\"\"",
+ triples: "",
+ explode: "[]{}"
+ };
+
+ var Pos = CodeMirror.Pos;
+
+ CodeMirror.defineOption("autoCloseBrackets", false, function(cm, val, old) {
+ if (old && old != CodeMirror.Init) {
+ cm.removeKeyMap(keyMap);
+ cm.state.closeBrackets = null;
+ }
+ if (val) {
+ cm.state.closeBrackets = val;
+ cm.addKeyMap(keyMap);
+ }
+ });
+
+ function getOption(conf, name) {
+ if (name == "pairs" && typeof conf == "string") return conf;
+ if (typeof conf == "object" && conf[name] != null) return conf[name];
+ return defaults[name];
+ }
+
+ var bind = defaults.pairs + "`";
+ var keyMap = {Backspace: handleBackspace, Enter: handleEnter};
+ for (var i = 0; i < bind.length; i++)
+ keyMap["'" + bind.charAt(i) + "'"] = handler(bind.charAt(i));
+
+ function handler(ch) {
+ return function(cm) { return handleChar(cm, ch); };
+ }
+
+ function getConfig(cm) {
+ var deflt = cm.state.closeBrackets;
+ if (!deflt) return null;
+ var mode = cm.getModeAt(cm.getCursor());
+ return mode.closeBrackets || deflt;
+ }
+
+ function handleBackspace(cm) {
+ var conf = getConfig(cm);
+ if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass;
+
+ var pairs = getOption(conf, "pairs");
+ var ranges = cm.listSelections();
+ for (var i = 0; i < ranges.length; i++) {
+ if (!ranges[i].empty()) return CodeMirror.Pass;
+ var around = charsAround(cm, ranges[i].head);
+ if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass;
+ }
+ for (var i = ranges.length - 1; i >= 0; i--) {
+ var cur = ranges[i].head;
+ cm.replaceRange("", Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1), "+delete");
+ }
+ }
+
+ function handleEnter(cm) {
+ var conf = getConfig(cm);
+ var explode = conf && getOption(conf, "explode");
+ if (!explode || cm.getOption("disableInput")) return CodeMirror.Pass;
+
+ var ranges = cm.listSelections();
+ for (var i = 0; i < ranges.length; i++) {
+ if (!ranges[i].empty()) return CodeMirror.Pass;
+ var around = charsAround(cm, ranges[i].head);
+ if (!around || explode.indexOf(around) % 2 != 0) return CodeMirror.Pass;
+ }
+ cm.operation(function() {
+ 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);
+ }
+ });
+ }
+
+ function contractSelection(sel) {
+ var inverted = CodeMirror.cmpPos(sel.anchor, sel.head) > 0;
+ return {anchor: new Pos(sel.anchor.line, sel.anchor.ch + (inverted ? -1 : 1)),
+ head: new Pos(sel.head.line, sel.head.ch + (inverted ? 1 : -1))};
+ }
+
+ function handleChar(cm, ch) {
+ var conf = getConfig(cm);
+ if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass;
+
+ var pairs = getOption(conf, "pairs");
+ var pos = pairs.indexOf(ch);
+ if (pos == -1) return CodeMirror.Pass;
+ var triples = getOption(conf, "triples");
+
+ var identical = pairs.charAt(pos + 1) == ch;
+ var ranges = cm.listSelections();
+ var opening = pos % 2 == 0;
+
+ var type;
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i], cur = range.head, curType;
+ var next = cm.getRange(cur, Pos(cur.line, cur.ch + 1));
+ if (opening && !range.empty()) {
+ curType = "surround";
+ } else if ((identical || !opening) && next == ch) {
+ if (triples.indexOf(ch) >= 0 && cm.getRange(cur, Pos(cur.line, cur.ch + 3)) == ch + ch + ch)
+ curType = "skipThree";
+ else
+ curType = "skip";
+ } else if (identical && cur.ch > 1 && triples.indexOf(ch) >= 0 &&
+ cm.getRange(Pos(cur.line, cur.ch - 2), cur) == ch + ch &&
+ (cur.ch <= 2 || cm.getRange(Pos(cur.line, cur.ch - 3), Pos(cur.line, cur.ch - 2)) != ch)) {
+ curType = "addFour";
+ } else if (identical) {
+ if (!CodeMirror.isWordChar(next) && enteringString(cm, cur, ch)) curType = "both";
+ else return CodeMirror.Pass;
+ } else if (opening && (cm.getLine(cur.line).length == cur.ch ||
+ isClosingBracket(next, pairs) ||
+ /\s/.test(next))) {
+ curType = "both";
+ } else {
+ return CodeMirror.Pass;
+ }
+ if (!type) type = curType;
+ else if (type != curType) return CodeMirror.Pass;
+ }
+
+ var left = pos % 2 ? pairs.charAt(pos - 1) : ch;
+ var right = pos % 2 ? ch : pairs.charAt(pos + 1);
+ cm.operation(function() {
+ if (type == "skip") {
+ cm.execCommand("goCharRight");
+ } else if (type == "skipThree") {
+ for (var i = 0; i < 3; i++)
+ cm.execCommand("goCharRight");
+ } else if (type == "surround") {
+ var sels = cm.getSelections();
+ for (var i = 0; i < sels.length; i++)
+ sels[i] = left + sels[i] + right;
+ cm.replaceSelections(sels, "around");
+ sels = cm.listSelections().slice();
+ for (var i = 0; i < sels.length; i++)
+ sels[i] = contractSelection(sels[i]);
+ cm.setSelections(sels);
+ } else if (type == "both") {
+ cm.replaceSelection(left + right, null);
+ cm.triggerElectric(left + right);
+ cm.execCommand("goCharLeft");
+ } else if (type == "addFour") {
+ cm.replaceSelection(left + left + left + left, "before");
+ cm.execCommand("goCharRight");
+ }
+ });
+ }
+
+ function isClosingBracket(ch, pairs) {
+ var pos = pairs.lastIndexOf(ch);
+ return pos > -1 && pos % 2 == 1;
+ }
+
+ function charsAround(cm, pos) {
+ var str = cm.getRange(Pos(pos.line, pos.ch - 1),
+ Pos(pos.line, pos.ch + 1));
+ return str.length == 2 ? str : null;
+ }
+
+ // Project the token type that will exists after the given char is
+ // typed, and use it to determine whether it would cause the start
+ // of a string token.
+ function enteringString(cm, pos, ch) {
+ var line = cm.getLine(pos.line);
+ var token = cm.getTokenAt(pos);
+ if (/\bstring2?\b/.test(token.type)) return false;
+ var stream = new CodeMirror.StringStream(line.slice(0, pos.ch) + ch + line.slice(pos.ch), 4);
+ stream.pos = stream.start = token.start;
+ for (;;) {
+ var type1 = cm.getMode().token(stream, token.state);
+ if (stream.pos >= pos.ch + 1) return /\bstring2?\b/.test(type1);
+ stream.start = stream.pos;
+ }
+ }
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/edit/closetag.js b/devtools/client/sourceeditor/codemirror/addon/edit/closetag.js
new file mode 100644
index 000000000..a518da3ec
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/edit/closetag.js
@@ -0,0 +1,169 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+/**
+ * Tag-closer extension for CodeMirror.
+ *
+ * This extension adds an "autoCloseTags" option that can be set to
+ * either true to get the default behavior, or an object to further
+ * configure its behavior.
+ *
+ * These are supported options:
+ *
+ * `whenClosing` (default true)
+ * Whether to autoclose when the '/' of a closing tag is typed.
+ * `whenOpening` (default true)
+ * Whether to autoclose the tag when the final '>' of an opening
+ * tag is typed.
+ * `dontCloseTags` (default is empty tags for HTML, none for XML)
+ * An array of tag names that should not be autoclosed.
+ * `indentTags` (default is block tags for HTML, none for XML)
+ * An array of tag names that should, when opened, cause a
+ * blank line to be added inside the tag, and the blank line and
+ * closing line to be indented.
+ *
+ * See demos/closetag.html for a usage example.
+ */
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"), require("../fold/xml-fold"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror", "../fold/xml-fold"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ CodeMirror.defineOption("autoCloseTags", false, function(cm, val, old) {
+ if (old != CodeMirror.Init && old)
+ cm.removeKeyMap("autoCloseTags");
+ if (!val) return;
+ var map = {name: "autoCloseTags"};
+ if (typeof val != "object" || val.whenClosing)
+ map["'/'"] = function(cm) { return autoCloseSlash(cm); };
+ if (typeof val != "object" || val.whenOpening)
+ map["'>'"] = function(cm) { return autoCloseGT(cm); };
+ cm.addKeyMap(map);
+ });
+
+ var htmlDontClose = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param",
+ "source", "track", "wbr"];
+ var htmlIndent = ["applet", "blockquote", "body", "button", "div", "dl", "fieldset", "form", "frameset", "h1", "h2", "h3", "h4",
+ "h5", "h6", "head", "html", "iframe", "layer", "legend", "object", "ol", "p", "select", "table", "ul"];
+
+ function autoCloseGT(cm) {
+ if (cm.getOption("disableInput")) return CodeMirror.Pass;
+ var ranges = cm.listSelections(), replacements = [];
+ 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 tagName = state.tagName;
+ if (tok.end > pos.ch) tagName = tagName.slice(0, tagName.length - tok.end + pos.ch);
+ var lowerTagName = tagName.toLowerCase();
+ // Don't process the '>' at the end of an end-tag or self-closing tag
+ if (!tagName ||
+ tok.type == "string" && (tok.end != pos.ch || !/[\"\']/.test(tok.string.charAt(tok.string.length - 1)) || tok.string.length == 1) ||
+ tok.type == "tag" && state.type == "closeTag" ||
+ tok.string.indexOf("/") == (tok.string.length - 1) || // match something like <someTagName />
+ dontCloseTags && indexOf(dontCloseTags, lowerTagName) > -1 ||
+ closingTagExists(cm, tagName, pos, state, true))
+ return CodeMirror.Pass;
+
+ var indent = indentTags && indexOf(indentTags, lowerTagName) > -1;
+ replacements[i] = {indent: indent,
+ text: ">" + (indent ? "\n\n" : "") + "</" + tagName + ">",
+ newPos: indent ? CodeMirror.Pos(pos.line + 1, 0) : CodeMirror.Pos(pos.line, pos.ch + 1)};
+ }
+
+ 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 autoCloseCurrent(cm, typingSlash) {
+ var ranges = cm.listSelections(), replacements = [];
+ var head = typingSlash ? "/" : "</";
+ for (var i = 0; i < ranges.length; i++) {
+ if (!ranges[i].empty()) return CodeMirror.Pass;
+ var pos = ranges[i].head, tok = cm.getTokenAt(pos);
+ var inner = CodeMirror.innerMode(cm.getMode(), tok.state), state = inner.state;
+ if (typingSlash && (tok.type == "string" || tok.string.charAt(0) != "<" ||
+ tok.start != pos.ch - 1))
+ return CodeMirror.Pass;
+ // Kludge to get around the fact that we are not in XML mode
+ // when completing in JS/CSS snippet in htmlmixed mode. Does not
+ // work for other XML embedded languages (there is no general
+ // way to go from a mixed mode to its current XML state).
+ var replacement;
+ if (inner.mode.name != "xml") {
+ if (cm.getMode().name == "htmlmixed" && inner.mode.name == "javascript")
+ replacement = head + "script";
+ else if (cm.getMode().name == "htmlmixed" && inner.mode.name == "css")
+ replacement = head + "style";
+ else
+ return CodeMirror.Pass;
+ } else {
+ if (!state.context || !state.context.tagName ||
+ closingTagExists(cm, state.context.tagName, pos, state))
+ return CodeMirror.Pass;
+ replacement = head + state.context.tagName;
+ }
+ if (cm.getLine(pos.line).charAt(tok.end) != ">") replacement += ">";
+ replacements[i] = replacement;
+ }
+ cm.replaceSelections(replacements);
+ ranges = cm.listSelections();
+ for (var i = 0; i < ranges.length; i++)
+ if (i == ranges.length - 1 || ranges[i].head.line < ranges[i + 1].head.line)
+ cm.indentLine(ranges[i].head.line);
+ }
+
+ function autoCloseSlash(cm) {
+ if (cm.getOption("disableInput")) return CodeMirror.Pass;
+ return autoCloseCurrent(cm, true);
+ }
+
+ CodeMirror.commands.closeTag = function(cm) { return autoCloseCurrent(cm); };
+
+ function indexOf(collection, elt) {
+ if (collection.indexOf) return collection.indexOf(elt);
+ for (var i = 0, e = collection.length; i < e; ++i)
+ if (collection[i] == elt) return i;
+ return -1;
+ }
+
+ // If xml-fold is loaded, we use its functionality to try and verify
+ // whether a given tag is actually unclosed.
+ function closingTagExists(cm, tagName, pos, state, newTag) {
+ if (!CodeMirror.scanForClosingTag) return false;
+ var end = Math.min(cm.lastLine() + 1, pos.line + 500);
+ var nextClose = CodeMirror.scanForClosingTag(cm, pos, null, end);
+ if (!nextClose || nextClose.tag != tagName) return false;
+ var cx = state.context;
+ // If the immediate wrapping context contains onCx instances of
+ // the same tag, a closing tag only exists if there are at least
+ // that many closing tags of that type following.
+ for (var onCx = newTag ? 1 : 0; cx && cx.tagName == tagName; cx = cx.prev) ++onCx;
+ pos = nextClose.to;
+ for (var i = 1; i < onCx; i++) {
+ var next = CodeMirror.scanForClosingTag(cm, pos, null, end);
+ if (!next || next.tag != tagName) return false;
+ pos = next.to;
+ }
+ return true;
+ }
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/edit/continuelist.js b/devtools/client/sourceeditor/codemirror/addon/edit/continuelist.js
new file mode 100644
index 000000000..df5179fe4
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/edit/continuelist.js
@@ -0,0 +1,51 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ "use strict";
+
+ var listRE = /^(\s*)(>[> ]*|[*+-]\s|(\d+)([.)]))(\s*)/,
+ emptyListRE = /^(\s*)(>[> ]*|[*+-]|(\d+)[.)])(\s*)$/,
+ unorderedListRE = /[*+-]\s/;
+
+ 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;
+ var eolState = cm.getStateAfter(pos.line);
+ var inList = eolState.list !== false;
+ var inQuote = eolState.quote !== 0;
+
+ var line = cm.getLine(pos.line), match = listRE.exec(line);
+ if (!ranges[i].empty() || (!inList && !inQuote) || !match) {
+ cm.execCommand("newlineAndIndent");
+ return;
+ }
+ if (emptyListRE.test(line)) {
+ cm.replaceRange("", {
+ line: pos.line, ch: 0
+ }, {
+ line: pos.line, ch: pos.ch + 1
+ });
+ replacements[i] = "\n";
+ } else {
+ var indent = match[1], after = match[5];
+ var bullet = unorderedListRE.test(match[2]) || match[2].indexOf(">") >= 0
+ ? match[2]
+ : (parseInt(match[3], 10) + 1) + match[4];
+
+ replacements[i] = "\n" + indent + bullet + after;
+ }
+ }
+
+ cm.replaceSelections(replacements);
+ };
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/edit/matchbrackets.js b/devtools/client/sourceeditor/codemirror/addon/edit/matchbrackets.js
new file mode 100644
index 000000000..70e1ae18c
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/edit/matchbrackets.js
@@ -0,0 +1,120 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ var ie_lt8 = /MSIE \d/.test(navigator.userAgent) &&
+ (document.documentMode == null || document.documentMode < 8);
+
+ var Pos = CodeMirror.Pos;
+
+ var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<"};
+
+ function findMatchingBracket(cm, where, 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 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 found = scanForBracket(cm, Pos(where.line, pos + (dir > 0 ? 1 : 0)), dir, style || null, config);
+ if (found == null) return null;
+ return {from: Pos(where.line, pos), to: found && found.pos,
+ match: found && found.ch == match.charAt(0), forward: dir > 0};
+ }
+
+ // bracketRegex is used to specify which type of bracket to scan
+ // should be a regexp, e.g. /[[\]]/
+ //
+ // Note: If "where" is on an open bracket, then this bracket is ignored.
+ //
+ // Returns false when no bracket was found, null when it reached
+ // maxScanLines and gave up
+ function scanForBracket(cm, where, dir, style, config) {
+ var maxScanLen = (config && config.maxScanLineLength) || 10000;
+ var maxScanLines = (config && config.maxScanLines) || 1000;
+
+ var stack = [];
+ var re = config && config.bracketRegex ? config.bracketRegex : /[(){}[\]]/;
+ var lineEnd = dir > 0 ? Math.min(where.line + maxScanLines, cm.lastLine() + 1)
+ : Math.max(cm.firstLine() - 1, where.line - maxScanLines);
+ for (var lineNo = where.line; lineNo != lineEnd; lineNo += dir) {
+ var line = cm.getLine(lineNo);
+ if (!line) continue;
+ var pos = dir > 0 ? 0 : line.length - 1, end = dir > 0 ? line.length : -1;
+ if (line.length > maxScanLen) continue;
+ if (lineNo == where.line) pos = where.ch - (dir < 0 ? 1 : 0);
+ for (; pos != end; pos += dir) {
+ var ch = line.charAt(pos);
+ if (re.test(ch) && (style === undefined || cm.getTokenTypeAt(Pos(lineNo, pos + 1)) == style)) {
+ var match = matching[ch];
+ if ((match.charAt(1) == ">") == (dir > 0)) stack.push(ch);
+ else if (!stack.length) return {pos: Pos(lineNo, pos), ch: ch};
+ else stack.pop();
+ }
+ }
+ }
+ return lineNo - dir == (dir > 0 ? cm.lastLine() : cm.firstLine()) ? false : null;
+ }
+
+ function matchBrackets(cm, autoclear, config) {
+ // Disable brace matching in long lines, since it'll cause hugely slow updates
+ var maxHighlightLen = cm.state.matchBrackets.maxHighlightLineLength || 1000;
+ var marks = [], ranges = cm.listSelections();
+ for (var i = 0; i < ranges.length; i++) {
+ var match = ranges[i].empty() && findMatchingBracket(cm, ranges[i].head, false, config);
+ if (match && cm.getLine(match.from.line).length <= maxHighlightLen) {
+ var style = match.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket";
+ marks.push(cm.markText(match.from, Pos(match.from.line, match.from.ch + 1), {className: style}));
+ if (match.to && cm.getLine(match.to.line).length <= maxHighlightLen)
+ marks.push(cm.markText(match.to, Pos(match.to.line, match.to.ch + 1), {className: style}));
+ }
+ }
+
+ if (marks.length) {
+ // Kludge to work around the IE bug from issue #1193, where text
+ // input stops going to the textare whever this fires.
+ if (ie_lt8 && cm.state.focused) cm.focus();
+
+ var clear = function() {
+ cm.operation(function() {
+ for (var i = 0; i < marks.length; i++) marks[i].clear();
+ });
+ };
+ if (autoclear) setTimeout(clear, 800);
+ else return clear;
+ }
+ }
+
+ var currentlyHighlighted = null;
+ function doMatchBrackets(cm) {
+ cm.operation(function() {
+ if (currentlyHighlighted) {currentlyHighlighted(); currentlyHighlighted = null;}
+ currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets);
+ });
+ }
+
+ CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) {
+ if (old && old != CodeMirror.Init)
+ cm.off("cursorActivity", doMatchBrackets);
+ if (val) {
+ cm.state.matchBrackets = typeof val == "object" ? val : {};
+ cm.on("cursorActivity", doMatchBrackets);
+ }
+ });
+
+ CodeMirror.defineExtension("matchBrackets", function() {matchBrackets(this, true);});
+ CodeMirror.defineExtension("findMatchingBracket", function(pos, strict, config){
+ return findMatchingBracket(this, pos, strict, config);
+ });
+ CodeMirror.defineExtension("scanForBracket", function(pos, dir, style, config){
+ return scanForBracket(this, pos, dir, style, config);
+ });
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/edit/matchtags.js b/devtools/client/sourceeditor/codemirror/addon/edit/matchtags.js
new file mode 100644
index 000000000..fb1911a8d
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/edit/matchtags.js
@@ -0,0 +1,66 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"), require("../fold/xml-fold"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror", "../fold/xml-fold"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ "use strict";
+
+ CodeMirror.defineOption("matchTags", false, function(cm, val, old) {
+ if (old && old != CodeMirror.Init) {
+ cm.off("cursorActivity", doMatchTags);
+ cm.off("viewportChange", maybeUpdateMatch);
+ clear(cm);
+ }
+ if (val) {
+ cm.state.matchBothTags = typeof val == "object" && val.bothTags;
+ cm.on("cursorActivity", doMatchTags);
+ cm.on("viewportChange", maybeUpdateMatch);
+ doMatchTags(cm);
+ }
+ });
+
+ function clear(cm) {
+ if (cm.state.tagHit) cm.state.tagHit.clear();
+ if (cm.state.tagOther) cm.state.tagOther.clear();
+ cm.state.tagHit = cm.state.tagOther = null;
+ }
+
+ function doMatchTags(cm) {
+ cm.state.failedTagMatch = false;
+ cm.operation(function() {
+ clear(cm);
+ if (cm.somethingSelected()) return;
+ var cur = cm.getCursor(), range = cm.getViewport();
+ range.from = Math.min(range.from, cur.line); range.to = Math.max(cur.line + 1, range.to);
+ var match = CodeMirror.findMatchingTag(cm, cur, range);
+ if (!match) return;
+ if (cm.state.matchBothTags) {
+ var hit = match.at == "open" ? match.open : match.close;
+ if (hit) cm.state.tagHit = cm.markText(hit.from, hit.to, {className: "CodeMirror-matchingtag"});
+ }
+ var other = match.at == "close" ? match.open : match.close;
+ if (other)
+ cm.state.tagOther = cm.markText(other.from, other.to, {className: "CodeMirror-matchingtag"});
+ else
+ cm.state.failedTagMatch = true;
+ });
+ }
+
+ function maybeUpdateMatch(cm) {
+ if (cm.state.failedTagMatch) doMatchTags(cm);
+ }
+
+ CodeMirror.commands.toMatchingTag = function(cm) {
+ var found = CodeMirror.findMatchingTag(cm, cm.getCursor());
+ if (found) {
+ var other = found.at == "close" ? found.open : found.close;
+ if (other) cm.extendSelection(other.to, other.from);
+ }
+ };
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/edit/trailingspace.js b/devtools/client/sourceeditor/codemirror/addon/edit/trailingspace.js
new file mode 100644
index 000000000..fa7b56be5
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/edit/trailingspace.js
@@ -0,0 +1,27 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ CodeMirror.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/devtools/client/sourceeditor/codemirror/addon/fold/brace-fold.js b/devtools/client/sourceeditor/codemirror/addon/fold/brace-fold.js
new file mode 100644
index 000000000..13c0f0cd8
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/fold/brace-fold.js
@@ -0,0 +1,105 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+"use strict";
+
+CodeMirror.registerHelper("fold", "brace", function(cm, start) {
+ var line = start.line, lineText = cm.getLine(line);
+ var tokenType;
+
+ function findOpening(openCh) {
+ for (var at = start.ch, pass = 0;;) {
+ var found = at <= 0 ? -1 : lineText.lastIndexOf(openCh, at - 1);
+ if (found == -1) {
+ if (pass == 1) break;
+ pass = 1;
+ at = lineText.length;
+ continue;
+ }
+ if (pass == 1 && found < start.ch) break;
+ tokenType = cm.getTokenTypeAt(CodeMirror.Pos(line, found + 1));
+ if (!/^(comment|string)/.test(tokenType)) return found + 1;
+ at = found - 1;
+ }
+ }
+
+ var startToken = "{", endToken = "}", startCh = findOpening("{");
+ if (startCh == null) {
+ startToken = "[", endToken = "]";
+ startCh = findOpening("[");
+ }
+
+ if (startCh == null) return;
+ var count = 1, lastLine = cm.lastLine(), end, endCh;
+ outer: for (var i = line; i <= lastLine; ++i) {
+ var text = cm.getLine(i), pos = i == line ? startCh : 0;
+ for (;;) {
+ var nextOpen = text.indexOf(startToken, pos), nextClose = text.indexOf(endToken, pos);
+ if (nextOpen < 0) nextOpen = text.length;
+ if (nextClose < 0) nextClose = text.length;
+ pos = Math.min(nextOpen, nextClose);
+ if (pos == text.length) break;
+ if (cm.getTokenTypeAt(CodeMirror.Pos(i, pos + 1)) == tokenType) {
+ if (pos == nextOpen) ++count;
+ else if (!--count) { end = i; endCh = pos; break outer; }
+ }
+ ++pos;
+ }
+ }
+ if (end == null || line == end && endCh == startCh) return;
+ return {from: CodeMirror.Pos(line, startCh),
+ to: CodeMirror.Pos(end, endCh)};
+});
+
+CodeMirror.registerHelper("fold", "import", function(cm, start) {
+ function hasImport(line) {
+ if (line < cm.firstLine() || line > cm.lastLine()) return null;
+ var start = cm.getTokenAt(CodeMirror.Pos(line, 1));
+ if (!/\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1));
+ if (start.type != "keyword" || start.string != "import") return null;
+ // Now find closing semicolon, return its position
+ for (var i = line, e = Math.min(cm.lastLine(), line + 10); i <= e; ++i) {
+ var text = cm.getLine(i), semi = text.indexOf(";");
+ if (semi != -1) return {startCh: start.end, end: CodeMirror.Pos(i, semi)};
+ }
+ }
+
+ var startLine = start.line, has = hasImport(startLine), prev;
+ if (!has || hasImport(startLine - 1) || ((prev = hasImport(startLine - 2)) && prev.end.line == startLine - 1))
+ return null;
+ for (var end = has.end;;) {
+ var next = hasImport(end.line + 1);
+ if (next == null) break;
+ end = next.end;
+ }
+ return {from: cm.clipPos(CodeMirror.Pos(startLine, has.startCh + 1)), to: end};
+});
+
+CodeMirror.registerHelper("fold", "include", function(cm, start) {
+ function hasInclude(line) {
+ if (line < cm.firstLine() || line > cm.lastLine()) return null;
+ var start = cm.getTokenAt(CodeMirror.Pos(line, 1));
+ if (!/\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1));
+ if (start.type == "meta" && start.string.slice(0, 8) == "#include") return start.start + 8;
+ }
+
+ var startLine = start.line, has = hasInclude(startLine);
+ if (has == null || hasInclude(startLine - 1) != null) return null;
+ for (var end = startLine;;) {
+ var next = hasInclude(end + 1);
+ if (next == null) break;
+ ++end;
+ }
+ return {from: CodeMirror.Pos(startLine, has + 1),
+ to: cm.clipPos(CodeMirror.Pos(end))};
+});
+
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/fold/comment-fold.js b/devtools/client/sourceeditor/codemirror/addon/fold/comment-fold.js
new file mode 100644
index 000000000..e8d800eb5
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/fold/comment-fold.js
@@ -0,0 +1,59 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+"use strict";
+
+CodeMirror.registerGlobalHelper("fold", "comment", function(mode) {
+ return mode.blockCommentStart && mode.blockCommentEnd;
+}, function(cm, start) {
+ var mode = cm.getModeAt(start), startToken = mode.blockCommentStart, endToken = mode.blockCommentEnd;
+ if (!startToken || !endToken) return;
+ var line = start.line, lineText = cm.getLine(line);
+
+ var startCh;
+ for (var at = start.ch, pass = 0;;) {
+ var found = at <= 0 ? -1 : lineText.lastIndexOf(startToken, at - 1);
+ if (found == -1) {
+ if (pass == 1) return;
+ pass = 1;
+ at = lineText.length;
+ continue;
+ }
+ if (pass == 1 && found < start.ch) return;
+ if (/comment/.test(cm.getTokenTypeAt(CodeMirror.Pos(line, found + 1))) &&
+ (found == 0 || lineText.slice(found - endToken.length, found) == endToken ||
+ !/comment/.test(cm.getTokenTypeAt(CodeMirror.Pos(line, found))))) {
+ startCh = found + startToken.length;
+ break;
+ }
+ at = found - 1;
+ }
+
+ var depth = 1, lastLine = cm.lastLine(), end, endCh;
+ outer: for (var i = line; i <= lastLine; ++i) {
+ var text = cm.getLine(i), pos = i == line ? startCh : 0;
+ for (;;) {
+ var nextOpen = text.indexOf(startToken, pos), nextClose = text.indexOf(endToken, pos);
+ if (nextOpen < 0) nextOpen = text.length;
+ if (nextClose < 0) nextClose = text.length;
+ pos = Math.min(nextOpen, nextClose);
+ if (pos == text.length) break;
+ if (pos == nextOpen) ++depth;
+ else if (!--depth) { end = i; endCh = pos; break outer; }
+ ++pos;
+ }
+ }
+ if (end == null || line == end && endCh == startCh) return;
+ return {from: CodeMirror.Pos(line, startCh),
+ to: CodeMirror.Pos(end, endCh)};
+});
+
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/fold/foldcode.js b/devtools/client/sourceeditor/codemirror/addon/fold/foldcode.js
new file mode 100644
index 000000000..78b36c464
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/fold/foldcode.js
@@ -0,0 +1,150 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ "use strict";
+
+ function doFold(cm, pos, options, force) {
+ if (options && options.call) {
+ var finder = options;
+ options = null;
+ } else {
+ var finder = getOption(cm, options, "rangeFinder");
+ }
+ if (typeof pos == "number") pos = CodeMirror.Pos(pos, 0);
+ var minSize = getOption(cm, options, "minFoldSize");
+
+ function getRange(allowFolded) {
+ var range = finder(cm, pos);
+ if (!range || range.to.line - range.from.line < minSize) return null;
+ var marks = cm.findMarksAt(range.from);
+ for (var i = 0; i < marks.length; ++i) {
+ if (marks[i].__isFold && force !== "fold") {
+ if (!allowFolded) return null;
+ range.cleared = true;
+ marks[i].clear();
+ }
+ }
+ return range;
+ }
+
+ var range = getRange(true);
+ if (getOption(cm, options, "scanUp")) while (!range && pos.line > cm.firstLine()) {
+ pos = CodeMirror.Pos(pos.line - 1, 0);
+ range = getRange(false);
+ }
+ if (!range || range.cleared || force === "unfold") return;
+
+ var myWidget = makeWidget(cm, options);
+ CodeMirror.on(myWidget, "mousedown", function(e) {
+ myRange.clear();
+ CodeMirror.e_preventDefault(e);
+ });
+ var myRange = cm.markText(range.from, range.to, {
+ replacedWith: myWidget,
+ clearOnEnter: getOption(cm, options, "clearOnEnter"),
+ __isFold: true
+ });
+ myRange.on("clear", function(from, to) {
+ CodeMirror.signal(cm, "unfold", cm, from, to);
+ });
+ CodeMirror.signal(cm, "fold", cm, range.from, range.to);
+ }
+
+ function makeWidget(cm, options) {
+ var widget = getOption(cm, options, "widget");
+ if (typeof widget == "string") {
+ var text = document.createTextNode(widget);
+ widget = document.createElement("span");
+ widget.appendChild(text);
+ widget.className = "CodeMirror-foldmarker";
+ }
+ return widget;
+ }
+
+ // Clumsy backwards-compatible interface
+ CodeMirror.newFoldFunction = function(rangeFinder, widget) {
+ return function(cm, pos) { doFold(cm, pos, {rangeFinder: rangeFinder, widget: widget}); };
+ };
+
+ // New-style interface
+ CodeMirror.defineExtension("foldCode", function(pos, options, force) {
+ doFold(this, pos, options, force);
+ });
+
+ CodeMirror.defineExtension("isFolded", function(pos) {
+ var marks = this.findMarksAt(pos);
+ for (var i = 0; i < marks.length; ++i)
+ if (marks[i].__isFold) return true;
+ });
+
+ CodeMirror.commands.toggleFold = function(cm) {
+ cm.foldCode(cm.getCursor());
+ };
+ CodeMirror.commands.fold = function(cm) {
+ cm.foldCode(cm.getCursor(), null, "fold");
+ };
+ CodeMirror.commands.unfold = function(cm) {
+ cm.foldCode(cm.getCursor(), null, "unfold");
+ };
+ CodeMirror.commands.foldAll = function(cm) {
+ cm.operation(function() {
+ for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++)
+ cm.foldCode(CodeMirror.Pos(i, 0), null, "fold");
+ });
+ };
+ CodeMirror.commands.unfoldAll = function(cm) {
+ cm.operation(function() {
+ for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++)
+ cm.foldCode(CodeMirror.Pos(i, 0), null, "unfold");
+ });
+ };
+
+ CodeMirror.registerHelper("fold", "combine", function() {
+ var funcs = Array.prototype.slice.call(arguments, 0);
+ return function(cm, start) {
+ for (var i = 0; i < funcs.length; ++i) {
+ var found = funcs[i](cm, start);
+ if (found) return found;
+ }
+ };
+ });
+
+ CodeMirror.registerHelper("fold", "auto", function(cm, start) {
+ var helpers = cm.getHelpers(start, "fold");
+ for (var i = 0; i < helpers.length; i++) {
+ var cur = helpers[i](cm, start);
+ if (cur) return cur;
+ }
+ });
+
+ var defaultOptions = {
+ rangeFinder: CodeMirror.fold.auto,
+ widget: "\u2194",
+ minFoldSize: 0,
+ scanUp: false,
+ clearOnEnter: true
+ };
+
+ CodeMirror.defineOption("foldOptions", null);
+
+ function getOption(cm, options, name) {
+ if (options && options[name] !== undefined)
+ return options[name];
+ var editorOptions = cm.options.foldOptions;
+ if (editorOptions && editorOptions[name] !== undefined)
+ return editorOptions[name];
+ return defaultOptions[name];
+ }
+
+ CodeMirror.defineExtension("foldOption", function(options, name) {
+ return getOption(this, options, name);
+ });
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/fold/foldgutter.css b/devtools/client/sourceeditor/codemirror/addon/fold/foldgutter.css
new file mode 100644
index 000000000..ad19ae2d3
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/fold/foldgutter.css
@@ -0,0 +1,20 @@
+.CodeMirror-foldmarker {
+ color: blue;
+ text-shadow: #b9f 1px 1px 2px, #b9f -1px -1px 2px, #b9f 1px -1px 2px, #b9f -1px 1px 2px;
+ font-family: arial;
+ line-height: .3;
+ cursor: pointer;
+}
+.CodeMirror-foldgutter {
+ width: .7em;
+}
+.CodeMirror-foldgutter-open,
+.CodeMirror-foldgutter-folded {
+ cursor: pointer;
+}
+.CodeMirror-foldgutter-open:after {
+ content: "\25BE";
+}
+.CodeMirror-foldgutter-folded:after {
+ content: "\25B8";
+}
diff --git a/devtools/client/sourceeditor/codemirror/addon/fold/foldgutter.js b/devtools/client/sourceeditor/codemirror/addon/fold/foldgutter.js
new file mode 100644
index 000000000..9d3232656
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/fold/foldgutter.js
@@ -0,0 +1,146 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"), require("./foldcode"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror", "./foldcode"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ "use strict";
+
+ CodeMirror.defineOption("foldGutter", false, function(cm, val, old) {
+ if (old && old != CodeMirror.Init) {
+ cm.clearGutter(cm.state.foldGutter.options.gutter);
+ cm.state.foldGutter = null;
+ cm.off("gutterClick", onGutterClick);
+ cm.off("change", onChange);
+ cm.off("viewportChange", onViewportChange);
+ cm.off("fold", onFold);
+ cm.off("unfold", onFold);
+ cm.off("swapDoc", onChange);
+ }
+ if (val) {
+ cm.state.foldGutter = new State(parseOptions(val));
+ updateInViewport(cm);
+ cm.on("gutterClick", onGutterClick);
+ cm.on("change", onChange);
+ cm.on("viewportChange", onViewportChange);
+ cm.on("fold", onFold);
+ cm.on("unfold", onFold);
+ cm.on("swapDoc", onChange);
+ }
+ });
+
+ var Pos = CodeMirror.Pos;
+
+ function State(options) {
+ this.options = options;
+ this.from = this.to = 0;
+ }
+
+ function parseOptions(opts) {
+ if (opts === true) opts = {};
+ if (opts.gutter == null) opts.gutter = "CodeMirror-foldgutter";
+ if (opts.indicatorOpen == null) opts.indicatorOpen = "CodeMirror-foldgutter-open";
+ if (opts.indicatorFolded == null) opts.indicatorFolded = "CodeMirror-foldgutter-folded";
+ return opts;
+ }
+
+ function isFolded(cm, line) {
+ var marks = cm.findMarks(Pos(line, 0), Pos(line + 1, 0));
+ for (var i = 0; i < marks.length; ++i)
+ if (marks[i].__isFold && marks[i].find().from.line == line) return marks[i];
+ }
+
+ function marker(spec) {
+ if (typeof spec == "string") {
+ var elt = document.createElement("div");
+ elt.className = spec + " CodeMirror-guttermarker-subtle";
+ return elt;
+ } else {
+ return spec.cloneNode(true);
+ }
+ }
+
+ function updateFoldInfo(cm, from, to) {
+ var opts = cm.state.foldGutter.options, cur = from;
+ var minSize = cm.foldOption(opts, "minFoldSize");
+ var func = cm.foldOption(opts, "rangeFinder");
+ cm.eachLine(from, to, function(line) {
+ var mark = null;
+ if (isFolded(cm, cur)) {
+ mark = marker(opts.indicatorFolded);
+ } else {
+ var pos = Pos(cur, 0);
+ var range = func && func(cm, pos);
+ if (range && range.to.line - range.from.line >= minSize)
+ mark = marker(opts.indicatorOpen);
+ }
+ cm.setGutterMarker(line, opts.gutter, mark);
+ ++cur;
+ });
+ }
+
+ function updateInViewport(cm) {
+ var vp = cm.getViewport(), state = cm.state.foldGutter;
+ if (!state) return;
+ cm.operation(function() {
+ updateFoldInfo(cm, vp.from, vp.to);
+ });
+ state.from = vp.from; state.to = vp.to;
+ }
+
+ function onGutterClick(cm, line, gutter) {
+ var state = cm.state.foldGutter;
+ if (!state) return;
+ var opts = state.options;
+ if (gutter != opts.gutter) return;
+ var folded = isFolded(cm, line);
+ if (folded) folded.clear();
+ else cm.foldCode(Pos(line, 0), opts.rangeFinder);
+ }
+
+ function onChange(cm) {
+ var state = cm.state.foldGutter;
+ if (!state) return;
+ var opts = state.options;
+ state.from = state.to = 0;
+ clearTimeout(state.changeUpdate);
+ state.changeUpdate = setTimeout(function() { updateInViewport(cm); }, opts.foldOnChangeTimeSpan || 600);
+ }
+
+ function onViewportChange(cm) {
+ var state = cm.state.foldGutter;
+ if (!state) return;
+ var opts = state.options;
+ clearTimeout(state.changeUpdate);
+ state.changeUpdate = setTimeout(function() {
+ var vp = cm.getViewport();
+ if (state.from == state.to || vp.from - state.to > 20 || state.from - vp.to > 20) {
+ updateInViewport(cm);
+ } else {
+ cm.operation(function() {
+ if (vp.from < state.from) {
+ updateFoldInfo(cm, vp.from, state.from);
+ state.from = vp.from;
+ }
+ if (vp.to > state.to) {
+ updateFoldInfo(cm, state.to, vp.to);
+ state.to = vp.to;
+ }
+ });
+ }
+ }, opts.updateViewportTimeSpan || 400);
+ }
+
+ function onFold(cm, from) {
+ var state = cm.state.foldGutter;
+ if (!state) return;
+ var line = from.line;
+ if (line >= state.from && line < state.to)
+ updateFoldInfo(cm, line, line + 1);
+ }
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/fold/indent-fold.js b/devtools/client/sourceeditor/codemirror/addon/fold/indent-fold.js
new file mode 100644
index 000000000..e29f15e9d
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/fold/indent-fold.js
@@ -0,0 +1,44 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+"use strict";
+
+CodeMirror.registerHelper("fold", "indent", function(cm, start) {
+ var tabSize = cm.getOption("tabSize"), firstLine = cm.getLine(start.line);
+ if (!/\S/.test(firstLine)) return;
+ var getIndent = function(line) {
+ return CodeMirror.countColumn(line, null, tabSize);
+ };
+ var myIndent = getIndent(firstLine);
+ var lastLineInFold = null;
+ // Go through lines until we find a line that definitely doesn't belong in
+ // the block we're folding, or to the end.
+ for (var i = start.line + 1, end = cm.lastLine(); i <= end; ++i) {
+ var curLine = cm.getLine(i);
+ var curIndent = getIndent(curLine);
+ if (curIndent > myIndent) {
+ // Lines with a greater indent are considered part of the block.
+ lastLineInFold = i;
+ } else if (!/\S/.test(curLine)) {
+ // Empty lines might be breaks within the block we're trying to fold.
+ } else {
+ // A non-empty line at an indent equal to or less than ours marks the
+ // start of another block.
+ break;
+ }
+ }
+ if (lastLineInFold) return {
+ from: CodeMirror.Pos(start.line, firstLine.length),
+ to: CodeMirror.Pos(lastLineInFold, cm.getLine(lastLineInFold).length)
+ };
+});
+
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/fold/markdown-fold.js b/devtools/client/sourceeditor/codemirror/addon/fold/markdown-fold.js
new file mode 100644
index 000000000..ce84c946c
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/fold/markdown-fold.js
@@ -0,0 +1,49 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+"use strict";
+
+CodeMirror.registerHelper("fold", "markdown", function(cm, start) {
+ var maxDepth = 100;
+
+ function isHeader(lineNo) {
+ var tokentype = cm.getTokenTypeAt(CodeMirror.Pos(lineNo, 0));
+ return tokentype && /\bheader\b/.test(tokentype);
+ }
+
+ function headerLevel(lineNo, line, nextLine) {
+ var match = line && line.match(/^#+/);
+ if (match && isHeader(lineNo)) return match[0].length;
+ match = nextLine && nextLine.match(/^[=\-]+\s*$/);
+ if (match && isHeader(lineNo + 1)) return nextLine[0] == "=" ? 1 : 2;
+ return maxDepth;
+ }
+
+ var firstLine = cm.getLine(start.line), nextLine = cm.getLine(start.line + 1);
+ var level = headerLevel(start.line, firstLine, nextLine);
+ if (level === maxDepth) return undefined;
+
+ var lastLineNo = cm.lastLine();
+ var end = start.line, nextNextLine = cm.getLine(end + 2);
+ while (end < lastLineNo) {
+ if (headerLevel(end + 1, nextLine, nextNextLine) <= level) break;
+ ++end;
+ nextLine = nextNextLine;
+ nextNextLine = cm.getLine(end + 2);
+ }
+
+ return {
+ from: CodeMirror.Pos(start.line, firstLine.length),
+ to: CodeMirror.Pos(end, cm.getLine(end).length)
+ };
+});
+
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/fold/xml-fold.js b/devtools/client/sourceeditor/codemirror/addon/fold/xml-fold.js
new file mode 100644
index 000000000..f8c67b897
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/fold/xml-fold.js
@@ -0,0 +1,182 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ "use strict";
+
+ var Pos = CodeMirror.Pos;
+ function cmp(a, b) { return a.line - b.line || a.ch - b.ch; }
+
+ var nameStartChar = "A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD";
+ var nameChar = nameStartChar + "\-\:\.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040";
+ var xmlTagStart = new RegExp("<(/?)([" + nameStartChar + "][" + nameChar + "]*)", "g");
+
+ function Iter(cm, line, ch, range) {
+ this.line = line; this.ch = ch;
+ this.cm = cm; this.text = cm.getLine(line);
+ this.min = range ? range.from : cm.firstLine();
+ this.max = range ? range.to - 1 : cm.lastLine();
+ }
+
+ function tagAt(iter, ch) {
+ var type = iter.cm.getTokenTypeAt(Pos(iter.line, ch));
+ return type && /\btag\b/.test(type);
+ }
+
+ function nextLine(iter) {
+ if (iter.line >= iter.max) return;
+ iter.ch = 0;
+ iter.text = iter.cm.getLine(++iter.line);
+ return true;
+ }
+ function prevLine(iter) {
+ if (iter.line <= iter.min) return;
+ iter.text = iter.cm.getLine(--iter.line);
+ iter.ch = iter.text.length;
+ return true;
+ }
+
+ function toTagEnd(iter) {
+ for (;;) {
+ var gt = iter.text.indexOf(">", iter.ch);
+ if (gt == -1) { if (nextLine(iter)) continue; else return; }
+ if (!tagAt(iter, gt + 1)) { iter.ch = gt + 1; continue; }
+ var lastSlash = iter.text.lastIndexOf("/", gt);
+ var selfClose = lastSlash > -1 && !/\S/.test(iter.text.slice(lastSlash + 1, gt));
+ iter.ch = gt + 1;
+ return selfClose ? "selfClose" : "regular";
+ }
+ }
+ function toTagStart(iter) {
+ for (;;) {
+ var lt = iter.ch ? iter.text.lastIndexOf("<", iter.ch - 1) : -1;
+ if (lt == -1) { if (prevLine(iter)) continue; else return; }
+ if (!tagAt(iter, lt + 1)) { iter.ch = lt; continue; }
+ xmlTagStart.lastIndex = lt;
+ iter.ch = lt;
+ var match = xmlTagStart.exec(iter.text);
+ if (match && match.index == lt) return match;
+ }
+ }
+
+ function toNextTag(iter) {
+ for (;;) {
+ xmlTagStart.lastIndex = iter.ch;
+ var found = xmlTagStart.exec(iter.text);
+ if (!found) { if (nextLine(iter)) continue; else return; }
+ if (!tagAt(iter, found.index + 1)) { iter.ch = found.index + 1; continue; }
+ iter.ch = found.index + found[0].length;
+ return found;
+ }
+ }
+ function toPrevTag(iter) {
+ for (;;) {
+ var gt = iter.ch ? iter.text.lastIndexOf(">", iter.ch - 1) : -1;
+ if (gt == -1) { if (prevLine(iter)) continue; else return; }
+ if (!tagAt(iter, gt + 1)) { iter.ch = gt; continue; }
+ var lastSlash = iter.text.lastIndexOf("/", gt);
+ var selfClose = lastSlash > -1 && !/\S/.test(iter.text.slice(lastSlash + 1, gt));
+ iter.ch = gt + 1;
+ return selfClose ? "selfClose" : "regular";
+ }
+ }
+
+ function findMatchingClose(iter, tag) {
+ var stack = [];
+ for (;;) {
+ var next = toNextTag(iter), end, startLine = iter.line, startCh = iter.ch - (next ? next[0].length : 0);
+ if (!next || !(end = toTagEnd(iter))) return;
+ if (end == "selfClose") continue;
+ if (next[1]) { // closing tag
+ for (var i = stack.length - 1; i >= 0; --i) if (stack[i] == next[2]) {
+ stack.length = i;
+ break;
+ }
+ if (i < 0 && (!tag || tag == next[2])) return {
+ tag: next[2],
+ from: Pos(startLine, startCh),
+ to: Pos(iter.line, iter.ch)
+ };
+ } else { // opening tag
+ stack.push(next[2]);
+ }
+ }
+ }
+ function findMatchingOpen(iter, tag) {
+ var stack = [];
+ for (;;) {
+ var prev = toPrevTag(iter);
+ if (!prev) return;
+ if (prev == "selfClose") { toTagStart(iter); continue; }
+ var endLine = iter.line, endCh = iter.ch;
+ var start = toTagStart(iter);
+ if (!start) return;
+ if (start[1]) { // closing tag
+ stack.push(start[2]);
+ } else { // opening tag
+ for (var i = stack.length - 1; i >= 0; --i) if (stack[i] == start[2]) {
+ stack.length = i;
+ break;
+ }
+ if (i < 0 && (!tag || tag == start[2])) return {
+ tag: start[2],
+ from: Pos(iter.line, iter.ch),
+ to: Pos(endLine, endCh)
+ };
+ }
+ }
+ }
+
+ CodeMirror.registerHelper("fold", "xml", function(cm, start) {
+ var iter = new Iter(cm, start.line, 0);
+ for (;;) {
+ var openTag = toNextTag(iter), end;
+ if (!openTag || iter.line != start.line || !(end = toTagEnd(iter))) return;
+ if (!openTag[1] && end != "selfClose") {
+ var startPos = Pos(iter.line, iter.ch);
+ var endPos = findMatchingClose(iter, openTag[2]);
+ return endPos && {from: startPos, to: endPos.from};
+ }
+ }
+ });
+ CodeMirror.findMatchingTag = function(cm, pos, range) {
+ var iter = new Iter(cm, pos.line, pos.ch, range);
+ if (iter.text.indexOf(">") == -1 && iter.text.indexOf("<") == -1) return;
+ var end = toTagEnd(iter), to = end && Pos(iter.line, iter.ch);
+ var start = end && toTagStart(iter);
+ if (!end || !start || cmp(iter, pos) > 0) return;
+ var here = {from: Pos(iter.line, iter.ch), to: to, tag: start[2]};
+ if (end == "selfClose") return {open: here, close: null, at: "open"};
+
+ if (start[1]) { // closing tag
+ return {open: findMatchingOpen(iter, start[2]), close: here, at: "close"};
+ } else { // opening tag
+ iter = new Iter(cm, to.line, to.ch, range);
+ return {open: here, close: findMatchingClose(iter, start[2]), at: "open"};
+ }
+ };
+
+ CodeMirror.findEnclosingTag = function(cm, pos, range) {
+ var iter = new Iter(cm, pos.line, pos.ch, range);
+ for (;;) {
+ var open = findMatchingOpen(iter);
+ if (!open) break;
+ var forward = new Iter(cm, pos.line, pos.ch, range);
+ var close = findMatchingClose(forward, open.tag);
+ if (close) return {open: open, close: close};
+ }
+ };
+
+ // Used by addon/edit/closetag.js
+ CodeMirror.scanForClosingTag = function(cm, pos, name, end) {
+ var iter = new Iter(cm, pos.line, pos.ch, end ? {from: 0, to: end} : null);
+ return findMatchingClose(iter, name);
+ };
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/hint/show-hint.js b/devtools/client/sourceeditor/codemirror/addon/hint/show-hint.js
new file mode 100644
index 000000000..64ec9289c
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/hint/show-hint.js
@@ -0,0 +1,437 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ "use strict";
+
+ var HINT_ELEMENT_CLASS = "CodeMirror-hint";
+ var ACTIVE_HINT_ELEMENT_CLASS = "CodeMirror-hint-active";
+
+ // This is the old interface, kept around for now to stay
+ // backwards-compatible.
+ CodeMirror.showHint = function(cm, getHints, options) {
+ if (!getHints) return cm.showHint(options);
+ if (options && options.async) getHints.async = true;
+ var newOpts = {hint: getHints};
+ if (options) for (var prop in options) newOpts[prop] = options[prop];
+ return cm.showHint(newOpts);
+ };
+
+ CodeMirror.defineExtension("showHint", function(options) {
+ options = parseOptions(this, this.getCursor("start"), options);
+ var selections = this.listSelections()
+ if (selections.length > 1) return;
+ // By default, don't allow completion when something is selected.
+ // A hint function can have a `supportsSelection` property to
+ // indicate that it can handle selections.
+ if (this.somethingSelected()) {
+ if (!options.hint.supportsSelection) return;
+ // Don't try with cross-line selections
+ for (var i = 0; i < selections.length; i++)
+ if (selections[i].head.line != selections[i].anchor.line) return;
+ }
+
+ if (this.state.completionActive) this.state.completionActive.close();
+ var completion = this.state.completionActive = new Completion(this, options);
+ if (!completion.options.hint) return;
+
+ CodeMirror.signal(this, "startCompletion", this);
+ completion.update(true);
+ });
+
+ function Completion(cm, options) {
+ this.cm = cm;
+ this.options = options;
+ this.widget = null;
+ this.debounce = 0;
+ this.tick = 0;
+ this.startPos = this.cm.getCursor("start");
+ this.startLen = this.cm.getLine(this.startPos.line).length - this.cm.getSelection().length;
+
+ var self = this;
+ cm.on("cursorActivity", this.activityFunc = function() { self.cursorActivity(); });
+ }
+
+ var requestAnimationFrame = window.requestAnimationFrame || function(fn) {
+ return setTimeout(fn, 1000/60);
+ };
+ var cancelAnimationFrame = window.cancelAnimationFrame || clearTimeout;
+
+ Completion.prototype = {
+ close: function() {
+ if (!this.active()) return;
+ this.cm.state.completionActive = null;
+ this.tick = null;
+ this.cm.off("cursorActivity", this.activityFunc);
+
+ if (this.widget && this.data) CodeMirror.signal(this.data, "close");
+ if (this.widget) this.widget.close();
+ CodeMirror.signal(this.cm, "endCompletion", this.cm);
+ },
+
+ active: function() {
+ return this.cm.state.completionActive == this;
+ },
+
+ pick: function(data, i) {
+ var completion = data.list[i];
+ if (completion.hint) completion.hint(this.cm, data, completion);
+ else this.cm.replaceRange(getText(completion), completion.from || data.from,
+ completion.to || data.to, "complete");
+ CodeMirror.signal(data, "pick", completion);
+ this.close();
+ },
+
+ cursorActivity: function() {
+ if (this.debounce) {
+ cancelAnimationFrame(this.debounce);
+ this.debounce = 0;
+ }
+
+ var pos = this.cm.getCursor(), line = this.cm.getLine(pos.line);
+ if (pos.line != this.startPos.line || line.length - pos.ch != this.startLen - this.startPos.ch ||
+ pos.ch < this.startPos.ch || this.cm.somethingSelected() ||
+ (pos.ch && this.options.closeCharacters.test(line.charAt(pos.ch - 1)))) {
+ this.close();
+ } else {
+ var self = this;
+ this.debounce = requestAnimationFrame(function() {self.update();});
+ if (this.widget) this.widget.disable();
+ }
+ },
+
+ update: function(first) {
+ if (this.tick == null) return
+ var self = this, myTick = ++this.tick
+ fetchHints(this.options.hint, this.cm, this.options, function(data) {
+ if (self.tick == myTick) self.finishUpdate(data, first)
+ })
+ },
+
+ finishUpdate: function(data, first) {
+ if (this.data) CodeMirror.signal(this.data, "update");
+
+ var picked = (this.widget && this.widget.picked) || (first && this.options.completeSingle);
+ if (this.widget) this.widget.close();
+
+ if (data && this.data && isNewCompletion(this.data, data)) return;
+ this.data = data;
+
+ if (data && data.list.length) {
+ if (picked && data.list.length == 1) {
+ this.pick(data, 0);
+ } else {
+ this.widget = new Widget(this, data);
+ CodeMirror.signal(data, "shown");
+ }
+ }
+ }
+ };
+
+ function isNewCompletion(old, nw) {
+ var moved = CodeMirror.cmpPos(nw.from, old.from)
+ return moved > 0 && old.to.ch - old.from.ch != nw.to.ch - nw.from.ch
+ }
+
+ function parseOptions(cm, pos, options) {
+ var editor = cm.options.hintOptions;
+ var out = {};
+ for (var prop in defaultOptions) out[prop] = defaultOptions[prop];
+ if (editor) for (var prop in editor)
+ if (editor[prop] !== undefined) out[prop] = editor[prop];
+ if (options) for (var prop in options)
+ if (options[prop] !== undefined) out[prop] = options[prop];
+ if (out.hint.resolve) out.hint = out.hint.resolve(cm, pos)
+ return out;
+ }
+
+ function getText(completion) {
+ if (typeof completion == "string") return completion;
+ else return completion.text;
+ }
+
+ function buildKeyMap(completion, handle) {
+ var baseMap = {
+ Up: function() {handle.moveFocus(-1);},
+ Down: function() {handle.moveFocus(1);},
+ PageUp: function() {handle.moveFocus(-handle.menuSize() + 1, true);},
+ PageDown: function() {handle.moveFocus(handle.menuSize() - 1, true);},
+ Home: function() {handle.setFocus(0);},
+ End: function() {handle.setFocus(handle.length - 1);},
+ Enter: handle.pick,
+ Tab: handle.pick,
+ Esc: handle.close
+ };
+ var custom = completion.options.customKeys;
+ var ourMap = custom ? {} : baseMap;
+ function addBinding(key, val) {
+ var bound;
+ if (typeof val != "string")
+ bound = function(cm) { return val(cm, handle); };
+ // This mechanism is deprecated
+ else if (baseMap.hasOwnProperty(val))
+ bound = baseMap[val];
+ else
+ bound = val;
+ ourMap[key] = bound;
+ }
+ if (custom)
+ for (var key in custom) if (custom.hasOwnProperty(key))
+ addBinding(key, custom[key]);
+ var extra = completion.options.extraKeys;
+ if (extra)
+ for (var key in extra) if (extra.hasOwnProperty(key))
+ addBinding(key, extra[key]);
+ return ourMap;
+ }
+
+ function getHintElement(hintsElement, el) {
+ while (el && el != hintsElement) {
+ if (el.nodeName.toUpperCase() === "LI" && el.parentNode == hintsElement) return el;
+ el = el.parentNode;
+ }
+ }
+
+ function Widget(completion, data) {
+ this.completion = completion;
+ this.data = data;
+ this.picked = false;
+ var widget = this, cm = completion.cm;
+
+ var hints = this.hints = document.createElement("ul");
+ hints.className = "CodeMirror-hints";
+ this.selectedHint = data.selectedHint || 0;
+
+ var completions = data.list;
+ for (var i = 0; i < completions.length; ++i) {
+ var elt = hints.appendChild(document.createElement("li")), cur = completions[i];
+ var className = HINT_ELEMENT_CLASS + (i != this.selectedHint ? "" : " " + ACTIVE_HINT_ELEMENT_CLASS);
+ if (cur.className != null) className = cur.className + " " + className;
+ elt.className = className;
+ if (cur.render) cur.render(elt, data, cur);
+ else elt.appendChild(document.createTextNode(cur.displayText || getText(cur)));
+ elt.hintId = i;
+ }
+
+ var pos = cm.cursorCoords(completion.options.alignWithWord ? data.from : null);
+ var left = pos.left, top = pos.bottom, below = true;
+ hints.style.left = left + "px";
+ hints.style.top = top + "px";
+ // If we're at the edge of the screen, then we want the menu to appear on the left of the cursor.
+ var winW = window.innerWidth || Math.max(document.body.offsetWidth, document.documentElement.offsetWidth);
+ var winH = window.innerHeight || Math.max(document.body.offsetHeight, document.documentElement.offsetHeight);
+ (completion.options.container || document.body).appendChild(hints);
+ var box = hints.getBoundingClientRect(), overlapY = box.bottom - winH;
+ var scrolls = hints.scrollHeight > hints.clientHeight + 1
+ if (overlapY > 0) {
+ var height = box.bottom - box.top, curTop = pos.top - (pos.bottom - box.top);
+ if (curTop - height > 0) { // Fits above cursor
+ hints.style.top = (top = pos.top - 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.right - winW;
+ if (overlapX > 0) {
+ if (box.right - box.left > winW) {
+ hints.style.width = (winW - 5) + "px";
+ overlapX -= (box.right - box.left) - winW;
+ }
+ hints.style.left = (left = pos.left - overlapX) + "px";
+ }
+ if (scrolls) for (var node = hints.firstChild; node; node = node.nextSibling)
+ node.style.paddingRight = cm.display.nativeBarWidth + "px"
+
+ cm.addKeyMap(this.keyMap = buildKeyMap(completion, {
+ moveFocus: function(n, avoidWrap) { widget.changeActive(widget.selectedHint + n, avoidWrap); },
+ setFocus: function(n) { widget.changeActive(n); },
+ menuSize: function() { return widget.screenAmount(); },
+ length: completions.length,
+ close: function() { completion.close(); },
+ pick: function() { widget.pick(); },
+ data: data
+ }));
+
+ if (completion.options.closeOnUnfocus) {
+ var closingOnBlur;
+ cm.on("blur", this.onBlur = function() { closingOnBlur = setTimeout(function() { completion.close(); }, 100); });
+ cm.on("focus", this.onFocus = function() { clearTimeout(closingOnBlur); });
+ }
+
+ var startScroll = cm.getScrollInfo();
+ cm.on("scroll", this.onScroll = function() {
+ var curScroll = cm.getScrollInfo(), editor = cm.getWrapperElement().getBoundingClientRect();
+ var newTop = top + startScroll.top - curScroll.top;
+ var point = newTop - (window.pageYOffset || (document.documentElement || document.body).scrollTop);
+ if (!below) point += hints.offsetHeight;
+ if (point <= editor.top || point >= editor.bottom) return completion.close();
+ hints.style.top = newTop + "px";
+ hints.style.left = (left + startScroll.left - curScroll.left) + "px";
+ });
+
+ CodeMirror.on(hints, "dblclick", function(e) {
+ var t = getHintElement(hints, e.target || e.srcElement);
+ if (t && t.hintId != null) {widget.changeActive(t.hintId); widget.pick();}
+ });
+
+ CodeMirror.on(hints, "click", function(e) {
+ var t = getHintElement(hints, e.target || e.srcElement);
+ if (t && t.hintId != null) {
+ widget.changeActive(t.hintId);
+ if (completion.options.completeOnSingleClick) widget.pick();
+ }
+ });
+
+ CodeMirror.on(hints, "mousedown", function() {
+ setTimeout(function(){cm.focus();}, 20);
+ });
+
+ CodeMirror.signal(data, "select", completions[0], hints.firstChild);
+ return true;
+ }
+
+ Widget.prototype = {
+ close: function() {
+ if (this.completion.widget != this) return;
+ this.completion.widget = null;
+ this.hints.parentNode.removeChild(this.hints);
+ this.completion.cm.removeKeyMap(this.keyMap);
+
+ var cm = this.completion.cm;
+ if (this.completion.options.closeOnUnfocus) {
+ cm.off("blur", this.onBlur);
+ cm.off("focus", this.onFocus);
+ }
+ cm.off("scroll", this.onScroll);
+ },
+
+ disable: function() {
+ this.completion.cm.removeKeyMap(this.keyMap);
+ var widget = this;
+ this.keyMap = {Enter: function() { widget.picked = true; }};
+ this.completion.cm.addKeyMap(this.keyMap);
+ },
+
+ pick: function() {
+ this.completion.pick(this.data, this.selectedHint);
+ },
+
+ changeActive: function(i, avoidWrap) {
+ if (i >= this.data.list.length)
+ i = avoidWrap ? this.data.list.length - 1 : 0;
+ else if (i < 0)
+ i = avoidWrap ? 0 : this.data.list.length - 1;
+ if (this.selectedHint == i) return;
+ var node = this.hints.childNodes[this.selectedHint];
+ node.className = node.className.replace(" " + ACTIVE_HINT_ELEMENT_CLASS, "");
+ node = this.hints.childNodes[this.selectedHint = i];
+ node.className += " " + ACTIVE_HINT_ELEMENT_CLASS;
+ if (node.offsetTop < this.hints.scrollTop)
+ this.hints.scrollTop = node.offsetTop - 3;
+ else if (node.offsetTop + node.offsetHeight > this.hints.scrollTop + this.hints.clientHeight)
+ this.hints.scrollTop = node.offsetTop + node.offsetHeight - this.hints.clientHeight + 3;
+ CodeMirror.signal(this.data, "select", this.data.list[this.selectedHint], node);
+ },
+
+ screenAmount: function() {
+ return Math.floor(this.hints.clientHeight / this.hints.firstChild.offsetHeight) || 1;
+ }
+ };
+
+ function applicableHelpers(cm, helpers) {
+ if (!cm.somethingSelected()) return helpers
+ var result = []
+ for (var i = 0; i < helpers.length; i++)
+ if (helpers[i].supportsSelection) result.push(helpers[i])
+ return result
+ }
+
+ function fetchHints(hint, cm, options, callback) {
+ if (hint.async) {
+ hint(cm, callback, options)
+ } else {
+ var result = hint(cm, options)
+ if (result && result.then) result.then(callback)
+ else callback(result)
+ }
+ }
+
+ function resolveAutoHints(cm, pos) {
+ var helpers = cm.getHelpers(pos, "hint"), words
+ if (helpers.length) {
+ var resolved = function(cm, callback, options) {
+ var app = applicableHelpers(cm, helpers);
+ function run(i) {
+ if (i == app.length) return callback(null)
+ fetchHints(app[i], cm, options, function(result) {
+ if (result && result.list.length > 0) callback(result)
+ else run(i + 1)
+ })
+ }
+ run(0)
+ }
+ resolved.async = true
+ resolved.supportsSelection = true
+ return resolved
+ } else if (words = cm.getHelper(cm.getCursor(), "hintWords")) {
+ return function(cm) { return CodeMirror.hint.fromList(cm, {words: words}) }
+ } else if (CodeMirror.hint.anyword) {
+ return function(cm, options) { return CodeMirror.hint.anyword(cm, options) }
+ } else {
+ return function() {}
+ }
+ }
+
+ CodeMirror.registerHelper("hint", "auto", {
+ resolve: resolveAutoHints
+ });
+
+ CodeMirror.registerHelper("hint", "fromList", function(cm, options) {
+ var cur = cm.getCursor(), token = cm.getTokenAt(cur);
+ var to = CodeMirror.Pos(cur.line, token.end);
+ if (token.string && /\w/.test(token.string[token.string.length - 1])) {
+ var term = token.string, from = CodeMirror.Pos(cur.line, token.start);
+ } else {
+ var term = "", from = to;
+ }
+ var found = [];
+ for (var i = 0; i < options.words.length; i++) {
+ var word = options.words[i];
+ if (word.slice(0, term.length) == term)
+ found.push(word);
+ }
+
+ if (found.length) return {list: found, from: from, to: to};
+ });
+
+ CodeMirror.commands.autocomplete = CodeMirror.showHint;
+
+ var defaultOptions = {
+ hint: CodeMirror.hint.auto,
+ completeSingle: true,
+ alignWithWord: true,
+ closeCharacters: /[\s()\[\]{};:>,]/,
+ closeOnUnfocus: true,
+ completeOnSingleClick: true,
+ container: null,
+ customKeys: null,
+ extraKeys: null
+ };
+
+ CodeMirror.defineOption("hintOptions", null);
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/search/match-highlighter.js b/devtools/client/sourceeditor/codemirror/addon/search/match-highlighter.js
new file mode 100644
index 000000000..2c2914a9d
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/search/match-highlighter.js
@@ -0,0 +1,146 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+// Highlighting text that matches the selection
+//
+// Defines an option highlightSelectionMatches, which, when enabled,
+// will style strings that match the selection throughout the
+// document.
+//
+// The option can be set to true to simply enable it, or to a
+// {minChars, style, wordsOnly, showToken, delay} object to explicitly
+// configure it. minChars is the minimum amount of characters that should be
+// selected for the behavior to occur, and style is the token style to
+// apply to the matches. This will be prefixed by "cm-" to create an
+// actual CSS class name. If wordsOnly is enabled, the matches will be
+// highlighted only if the selected text is a word. showToken, when enabled,
+// will cause the current token to be highlighted when nothing is selected.
+// delay is used to specify how much time to wait, in milliseconds, before
+// highlighting the matches. If annotateScrollbar is enabled, the occurences
+// will be highlighted on the scrollbar via the matchesonscrollbar addon.
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"), require("./matchesonscrollbar"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror", "./matchesonscrollbar"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ "use strict";
+
+ var defaults = {
+ style: "matchhighlight",
+ minChars: 2,
+ delay: 100,
+ wordsOnly: false,
+ annotateScrollbar: false,
+ showToken: false,
+ trim: true
+ }
+
+ function State(options) {
+ this.options = {}
+ for (var name in defaults)
+ this.options[name] = (options && options.hasOwnProperty(name) ? options : defaults)[name]
+ this.overlay = this.timeout = null;
+ this.matchesonscroll = null;
+ }
+
+ CodeMirror.defineOption("highlightSelectionMatches", false, function(cm, val, old) {
+ if (old && old != CodeMirror.Init) {
+ removeOverlay(cm);
+ clearTimeout(cm.state.matchHighlighter.timeout);
+ cm.state.matchHighlighter = null;
+ cm.off("cursorActivity", cursorActivity);
+ }
+ if (val) {
+ cm.state.matchHighlighter = new State(val);
+ highlightMatches(cm);
+ cm.on("cursorActivity", cursorActivity);
+ }
+ });
+
+ function cursorActivity(cm) {
+ var state = cm.state.matchHighlighter;
+ clearTimeout(state.timeout);
+ state.timeout = setTimeout(function() {highlightMatches(cm);}, state.options.delay);
+ }
+
+ function addOverlay(cm, query, hasBoundary, style) {
+ var state = cm.state.matchHighlighter;
+ cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style));
+ if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) {
+ var searchFor = hasBoundary ? new RegExp("\\b" + query + "\\b") : query;
+ state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, true,
+ {className: "CodeMirror-selection-highlight-scrollbar"});
+ }
+ }
+
+ function removeOverlay(cm) {
+ var state = cm.state.matchHighlighter;
+ if (state.overlay) {
+ cm.removeOverlay(state.overlay);
+ state.overlay = null;
+ if (state.matchesonscroll) {
+ state.matchesonscroll.clear();
+ state.matchesonscroll = null;
+ }
+ }
+ }
+
+ function highlightMatches(cm) {
+ cm.operation(function() {
+ var state = cm.state.matchHighlighter;
+ removeOverlay(cm);
+ if (!cm.somethingSelected() && state.options.showToken) {
+ var re = state.options.showToken === true ? /[\w$]/ : state.options.showToken;
+ var cur = cm.getCursor(), line = cm.getLine(cur.line), start = cur.ch, end = start;
+ while (start && re.test(line.charAt(start - 1))) --start;
+ while (end < line.length && re.test(line.charAt(end))) ++end;
+ if (start < end)
+ addOverlay(cm, line.slice(start, end), re, state.options.style);
+ return;
+ }
+ var from = cm.getCursor("from"), to = cm.getCursor("to");
+ if (from.line != to.line) return;
+ if (state.options.wordsOnly && !isWord(cm, from, to)) return;
+ var selection = cm.getRange(from, to)
+ if (state.options.trim) selection = selection.replace(/^\s+|\s+$/g, "")
+ if (selection.length >= state.options.minChars)
+ addOverlay(cm, selection, false, state.options.style);
+ });
+ }
+
+ function isWord(cm, from, to) {
+ var str = cm.getRange(from, to);
+ if (str.match(/^\w+$/) !== null) {
+ if (from.ch > 0) {
+ var pos = {line: from.line, ch: from.ch - 1};
+ var chr = cm.getRange(pos, from);
+ if (chr.match(/\W/) === null) return false;
+ }
+ if (to.ch < cm.getLine(from.line).length) {
+ var pos = {line: to.line, ch: to.ch + 1};
+ var chr = cm.getRange(to, pos);
+ if (chr.match(/\W/) === null) return false;
+ }
+ return true;
+ } else return false;
+ }
+
+ function boundariesAround(stream, re) {
+ return (!stream.start || !re.test(stream.string.charAt(stream.start - 1))) &&
+ (stream.pos == stream.string.length || !re.test(stream.string.charAt(stream.pos)));
+ }
+
+ function makeOverlay(query, hasBoundary, style) {
+ return {token: function(stream) {
+ if (stream.match(query) &&
+ (!hasBoundary || boundariesAround(stream, hasBoundary)))
+ return style;
+ stream.next();
+ stream.skipTo(query.charAt(0)) || stream.skipToEnd();
+ }};
+ }
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/search/search.js b/devtools/client/sourceeditor/codemirror/addon/search/search.js
new file mode 100644
index 000000000..198531821
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/search/search.js
@@ -0,0 +1,246 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+// Define search commands. Depends on dialog.js or another
+// implementation of the openDialog method.
+
+// Replace works a little oddly -- it will do the replace on the next
+// Ctrl-G (or whatever is bound to findNext) press. You prevent a
+// replace by making sure the match is no longer selected when hitting
+// Ctrl-G.
+
+(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) {
+ if (typeof query == "string")
+ query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"), caseInsensitive ? "gi" : "g");
+ else if (!query.global)
+ query = new RegExp(query.source, query.ignoreCase ? "gi" : "g");
+
+ return {token: function(stream) {
+ query.lastIndex = stream.pos;
+ var match = query.exec(stream.string);
+ if (match && match.index == stream.pos) {
+ stream.pos += match[0].length || 1;
+ return "searching";
+ } else if (match) {
+ stream.pos = match.index;
+ } else {
+ stream.skipToEnd();
+ }
+ }};
+ }
+
+ function SearchState() {
+ this.posFrom = this.posTo = this.lastQuery = this.query = null;
+ this.overlay = null;
+ }
+
+ function getSearchState(cm) {
+ return cm.state.search || (cm.state.search = new SearchState());
+ }
+
+ function queryCaseInsensitive(query) {
+ return typeof query == "string" && query == query.toLowerCase();
+ }
+
+ function getSearchCursor(cm, query, pos) {
+ // Heuristic: if the query string is all lowercase, do a case insensitive search.
+ return cm.getSearchCursor(query, pos, queryCaseInsensitive(query));
+ }
+
+ function persistentDialog(cm, text, deflt, f) {
+ cm.openDialog(text, f, {
+ value: deflt,
+ selectValueOnOpen: true,
+ closeOnEnter: false,
+ onClose: function() { clearSearch(cm); }
+ });
+ }
+
+ function dialog(cm, text, shortText, deflt, f) {
+ if (cm.openDialog) cm.openDialog(text, f, {value: deflt, selectValueOnOpen: true});
+ else f(prompt(shortText, deflt));
+ }
+
+ function confirmDialog(cm, text, shortText, fs) {
+ if (cm.openConfirm) cm.openConfirm(text, fs);
+ else if (confirm(shortText)) fs[0]();
+ }
+
+ function parseString(string) {
+ return string.replace(/\\(.)/g, function(_, ch) {
+ if (ch == "n") return "\n"
+ if (ch == "r") return "\r"
+ return ch
+ })
+ }
+
+ function parseQuery(query) {
+ var isRE = query.match(/^\/(.*)\/([a-z]*)$/);
+ if (isRE) {
+ try { query = new RegExp(isRE[1], isRE[2].indexOf("i") == -1 ? "" : "i"); }
+ catch(e) {} // Not a regular expression after all, do a string search
+ } else {
+ query = parseString(query)
+ }
+ if (typeof query == "string" ? query == "" : query.test(""))
+ query = /x^/;
+ return query;
+ }
+
+ var queryDialog;
+
+ function startSearch(cm, state, query) {
+ state.queryText = query;
+ state.query = parseQuery(query);
+ cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query));
+ state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query));
+ cm.addOverlay(state.overlay);
+ if (cm.showMatchesOnScrollbar) {
+ if (state.annotate) { state.annotate.clear(); state.annotate = null; }
+ state.annotate = cm.showMatchesOnScrollbar(state.query, queryCaseInsensitive(state.query));
+ }
+ }
+
+ function doSearch(cm, rev, persistent) {
+ if (!queryDialog) {
+ let doc = cm.getWrapperElement().ownerDocument;
+ let inp = doc.createElement("input");
+
+ inp.type = "search";
+ inp.placeholder = cm.l10n("findCmd.promptMessage");
+ inp.style.marginInlineStart = "1em";
+ inp.style.marginInlineEnd = "1em";
+ inp.style.flexGrow = "1";
+ inp.addEventListener("focus", () => inp.select());
+
+ queryDialog = doc.createElement("div");
+ queryDialog.appendChild(inp);
+ queryDialog.style.display = "flex";
+ }
+
+ var state = getSearchState(cm);
+ if (state.query) return findNext(cm, rev);
+ var q = cm.getSelection() || state.lastQuery;
+ if (persistent && cm.openDialog) {
+ var hiding = null
+ persistentDialog(cm, queryDialog, q, function(query, event) {
+ CodeMirror.e_stop(event);
+ if (!query) return;
+ if (query != state.queryText) {
+ startSearch(cm, state, query);
+ state.posFrom = state.posTo = cm.getCursor();
+ }
+ if (hiding) hiding.style.opacity = 1
+ findNext(cm, event.shiftKey, function(_, to) {
+ var dialog
+ if (to.line < 3 && document.querySelector &&
+ (dialog = cm.display.wrapper.querySelector(".CodeMirror-dialog")) &&
+ dialog.getBoundingClientRect().bottom - 4 > cm.cursorCoords(to, "window").top)
+ (hiding = dialog).style.opacity = .4
+ })
+ });
+ } else {
+ dialog(cm, queryDialog, "Search for:", q, function(query) {
+ if (query && !state.query) cm.operation(function() {
+ startSearch(cm, state, query);
+ state.posFrom = state.posTo = cm.getCursor();
+ findNext(cm, rev);
+ });
+ });
+ }
+ }
+
+ function findNext(cm, rev, callback) {cm.operation(function() {
+ var state = getSearchState(cm);
+ var cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo);
+ if (!cursor.find(rev)) {
+ cursor = getSearchCursor(cm, state.query, rev ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0));
+ if (!cursor.find(rev)) return;
+ }
+ cm.setSelection(cursor.from(), cursor.to());
+ cm.scrollIntoView({from: cursor.from(), to: cursor.to()}, 20);
+ state.posFrom = cursor.from(); state.posTo = cursor.to();
+ if (callback) callback(cursor.from(), cursor.to())
+ });}
+
+ function clearSearch(cm) {cm.operation(function() {
+ var state = getSearchState(cm);
+ state.lastQuery = state.query;
+ if (!state.query) return;
+ state.query = state.queryText = null;
+ cm.removeOverlay(state.overlay);
+ if (state.annotate) { state.annotate.clear(); state.annotate = null; }
+ });}
+
+ var replaceQueryDialog =
+ ' <input type="text" style="width: 10em" class="CodeMirror-search-field"/> <span style="color: #888" class="CodeMirror-search-hint">(Use /re/ syntax for regexp search)</span>';
+ var replacementQueryDialog = 'With: <input type="text" style="width: 10em" class="CodeMirror-search-field"/>';
+ var doReplaceConfirm = "Replace? <button>Yes</button> <button>No</button> <button>All</button> <button>Stop</button>";
+
+ function replaceAll(cm, query, text) {
+ cm.operation(function() {
+ 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)/g, function(_, i) {return match[i];}));
+ } else cursor.replace(text);
+ }
+ });
+ }
+
+ function replace(cm, all) {
+ if (cm.getOption("readOnly")) return;
+ var query = cm.getSelection() || getSearchState(cm).lastQuery;
+ var dialogText = all ? "Replace all:" : "Replace:"
+ dialog(cm, dialogText + replaceQueryDialog, dialogText, query, function(query) {
+ if (!query) return;
+ query = parseQuery(query);
+ dialog(cm, replacementQueryDialog, "Replace with:", "", function(text) {
+ text = parseString(text)
+ if (all) {
+ replaceAll(cm, query, text)
+ } else {
+ clearSearch(cm);
+ var cursor = getSearchCursor(cm, query, cm.getCursor("from"));
+ var advance = function() {
+ var start = cursor.from(), match;
+ if (!(match = cursor.findNext())) {
+ cursor = getSearchCursor(cm, query);
+ if (!(match = cursor.findNext()) ||
+ (start && cursor.from().line == start.line && cursor.from().ch == start.ch)) return;
+ }
+ cm.setSelection(cursor.from(), cursor.to());
+ cm.scrollIntoView({from: cursor.from(), to: cursor.to()});
+ confirmDialog(cm, doReplaceConfirm, "Replace?",
+ [function() {doReplace(match);}, advance,
+ function() {replaceAll(cm, query, text)}]);
+ };
+ var doReplace = function(match) {
+ cursor.replace(typeof query == "string" ? text :
+ text.replace(/\$(\d)/g, function(_, i) {return match[i];}));
+ advance();
+ };
+ advance();
+ }
+ });
+ });
+ }
+
+ CodeMirror.commands.find = function(cm) {clearSearch(cm); doSearch(cm);};
+ CodeMirror.commands.findPersistent = function(cm) {clearSearch(cm); doSearch(cm, false, true);};
+ CodeMirror.commands.findNext = doSearch;
+ CodeMirror.commands.findPrev = function(cm) {doSearch(cm, true);};
+ CodeMirror.commands.clearSearch = clearSearch;
+ CodeMirror.commands.replace = replace;
+ CodeMirror.commands.replaceAll = function(cm) {replace(cm, true);};
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/search/searchcursor.js b/devtools/client/sourceeditor/codemirror/addon/search/searchcursor.js
new file mode 100644
index 000000000..b70242ee4
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/search/searchcursor.js
@@ -0,0 +1,189 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ "use strict";
+ var Pos = CodeMirror.Pos;
+
+ function SearchCursor(doc, query, pos, caseFold) {
+ this.atOccurrence = false; this.doc = doc;
+ if (caseFold == null && typeof query == "string") caseFold = false;
+
+ pos = pos ? doc.clipPos(pos) : Pos(0, 0);
+ this.pos = {from: pos, to: pos};
+
+ // The matches method is filled in based on the type of query.
+ // It takes a position and a direction, and returns an object
+ // describing the next occurrence of the query, or null if no
+ // more matches were found.
+ if (typeof query != "string") { // Regexp match
+ if (!query.global) query = new RegExp(query.source, query.ignoreCase ? "ig" : "g");
+ this.matches = function(reverse, pos) {
+ if (reverse) {
+ query.lastIndex = 0;
+ var line = doc.getLine(pos.line).slice(0, pos.ch), cutOff = 0, match, start;
+ for (;;) {
+ query.lastIndex = cutOff;
+ var newMatch = query.exec(line);
+ if (!newMatch) break;
+ match = newMatch;
+ start = match.index;
+ cutOff = match.index + (match[0].length || 1);
+ if (cutOff == line.length) break;
+ }
+ var matchLen = (match && match[0].length) || 0;
+ if (!matchLen) {
+ if (start == 0 && line.length == 0) {match = undefined;}
+ else if (start != doc.getLine(pos.line).length) {
+ matchLen++;
+ }
+ }
+ } else {
+ query.lastIndex = pos.ch;
+ var line = doc.getLine(pos.line), match = query.exec(line);
+ var matchLen = (match && match[0].length) || 0;
+ var start = match && match.index;
+ if (start + matchLen != line.length && !matchLen) matchLen = 1;
+ }
+ if (match && matchLen)
+ return {from: Pos(pos.line, start),
+ to: Pos(pos.line, start + matchLen),
+ match: match};
+ };
+ } else { // String query
+ var origQuery = query;
+ if (caseFold) query = query.toLowerCase();
+ var fold = caseFold ? function(str){return str.toLowerCase();} : function(str){return str;};
+ var target = query.split("\n");
+ // Different methods for single-line and multi-line queries
+ if (target.length == 1) {
+ if (!query.length) {
+ // Empty string would match anything and never progress, so
+ // we define it to match nothing instead.
+ this.matches = function() {};
+ } else {
+ this.matches = function(reverse, pos) {
+ if (reverse) {
+ var orig = doc.getLine(pos.line).slice(0, pos.ch), line = fold(orig);
+ var match = line.lastIndexOf(query);
+ if (match > -1) {
+ match = adjustPos(orig, line, match);
+ return {from: Pos(pos.line, match), to: Pos(pos.line, match + origQuery.length)};
+ }
+ } else {
+ var orig = doc.getLine(pos.line).slice(pos.ch), line = fold(orig);
+ var match = line.indexOf(query);
+ if (match > -1) {
+ match = adjustPos(orig, line, match) + pos.ch;
+ return {from: Pos(pos.line, match), to: Pos(pos.line, match + origQuery.length)};
+ }
+ }
+ };
+ }
+ } else {
+ var origTarget = origQuery.split("\n");
+ this.matches = function(reverse, pos) {
+ var last = target.length - 1;
+ if (reverse) {
+ if (pos.line - (target.length - 1) < doc.firstLine()) return;
+ if (fold(doc.getLine(pos.line).slice(0, origTarget[last].length)) != target[target.length - 1]) return;
+ var to = Pos(pos.line, origTarget[last].length);
+ for (var ln = pos.line - 1, i = last - 1; i >= 1; --i, --ln)
+ if (target[i] != fold(doc.getLine(ln))) return;
+ var line = doc.getLine(ln), cut = line.length - origTarget[0].length;
+ if (fold(line.slice(cut)) != target[0]) return;
+ return {from: Pos(ln, cut), to: to};
+ } else {
+ if (pos.line + (target.length - 1) > doc.lastLine()) return;
+ var line = doc.getLine(pos.line), cut = line.length - origTarget[0].length;
+ if (fold(line.slice(cut)) != target[0]) return;
+ var from = Pos(pos.line, cut);
+ for (var ln = pos.line + 1, i = 1; i < last; ++i, ++ln)
+ if (target[i] != fold(doc.getLine(ln))) return;
+ if (fold(doc.getLine(ln).slice(0, origTarget[last].length)) != target[last]) return;
+ return {from: from, to: Pos(ln, origTarget[last].length)};
+ }
+ };
+ }
+ }
+ }
+
+ SearchCursor.prototype = {
+ findNext: function() {return this.find(false);},
+ findPrevious: function() {return this.find(true);},
+
+ find: function(reverse) {
+ var self = this, pos = this.doc.clipPos(reverse ? this.pos.from : this.pos.to);
+ function savePosAndFail(line) {
+ var pos = Pos(line, 0);
+ self.pos = {from: pos, to: pos};
+ self.atOccurrence = false;
+ return false;
+ }
+
+ for (;;) {
+ if (this.pos = this.matches(reverse, pos)) {
+ this.atOccurrence = true;
+ return this.pos.match || true;
+ }
+ if (reverse) {
+ if (!pos.line) return savePosAndFail(0);
+ pos = Pos(pos.line-1, this.doc.getLine(pos.line-1).length);
+ }
+ else {
+ var maxLine = this.doc.lineCount();
+ if (pos.line == maxLine - 1) return savePosAndFail(maxLine);
+ pos = Pos(pos.line + 1, 0);
+ }
+ }
+ },
+
+ from: function() {if (this.atOccurrence) return this.pos.from;},
+ to: function() {if (this.atOccurrence) return this.pos.to;},
+
+ replace: function(newText, origin) {
+ if (!this.atOccurrence) return;
+ var lines = CodeMirror.splitLines(newText);
+ this.doc.replaceRange(lines, this.pos.from, this.pos.to, origin);
+ this.pos.to = Pos(this.pos.from.line + lines.length - 1,
+ lines[lines.length - 1].length + (lines.length == 1 ? this.pos.from.ch : 0));
+ }
+ };
+
+ // Maps a position in a case-folded line back to a position in the original line
+ // (compensating for codepoints increasing in number during folding)
+ function adjustPos(orig, folded, pos) {
+ if (orig.length == folded.length) return pos;
+ for (var pos1 = Math.min(pos, orig.length);;) {
+ var len1 = orig.slice(0, pos1).toLowerCase().length;
+ if (len1 < pos) ++pos1;
+ else if (len1 > pos) --pos1;
+ else return pos1;
+ }
+ }
+
+ CodeMirror.defineExtension("getSearchCursor", function(query, pos, caseFold) {
+ return new SearchCursor(this.doc, query, pos, caseFold);
+ });
+ CodeMirror.defineDocExtension("getSearchCursor", function(query, pos, caseFold) {
+ return new SearchCursor(this, query, pos, caseFold);
+ });
+
+ CodeMirror.defineExtension("selectMatches", function(query, caseFold) {
+ var ranges = [];
+ var cur = this.getSearchCursor(query, this.getCursor("from"), caseFold);
+ while (cur.findNext()) {
+ if (CodeMirror.cmpPos(cur.to(), this.getCursor("to")) > 0) break;
+ ranges.push({anchor: cur.from(), head: cur.to()});
+ }
+ if (ranges.length)
+ this.setSelections(ranges, 0);
+ });
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/selection/active-line.js b/devtools/client/sourceeditor/codemirror/addon/selection/active-line.js
new file mode 100644
index 000000000..b0b3f61af
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/selection/active-line.js
@@ -0,0 +1,74 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+// Because sometimes you need to style the cursor's line.
+//
+// Adds an option 'styleActiveLine' which, when enabled, gives the
+// active line's wrapping <div> the CSS class "CodeMirror-activeline",
+// and gives its background <div> the class "CodeMirror-activeline-background".
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ "use strict";
+ var WRAP_CLASS = "CodeMirror-activeline";
+ var BACK_CLASS = "CodeMirror-activeline-background";
+ var GUTT_CLASS = "CodeMirror-activeline-gutter";
+
+ CodeMirror.defineOption("styleActiveLine", false, function(cm, val, old) {
+ var prev = old && old != CodeMirror.Init;
+ if (val && !prev) {
+ cm.state.activeLines = [];
+ updateActiveLines(cm, cm.listSelections());
+ cm.on("beforeSelectionChange", selectionChange);
+ } else if (!val && prev) {
+ cm.off("beforeSelectionChange", selectionChange);
+ clearActiveLines(cm);
+ delete cm.state.activeLines;
+ }
+ });
+
+ function clearActiveLines(cm) {
+ for (var i = 0; i < cm.state.activeLines.length; i++) {
+ cm.removeLineClass(cm.state.activeLines[i], "wrap", WRAP_CLASS);
+ cm.removeLineClass(cm.state.activeLines[i], "background", BACK_CLASS);
+ cm.removeLineClass(cm.state.activeLines[i], "gutter", GUTT_CLASS);
+ }
+ }
+
+ function sameArray(a, b) {
+ if (a.length != b.length) return false;
+ for (var i = 0; i < a.length; i++)
+ if (a[i] != b[i]) return false;
+ return true;
+ }
+
+ function updateActiveLines(cm, ranges) {
+ var active = [];
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i];
+ if (!range.empty()) continue;
+ var line = cm.getLineHandleVisualStart(range.head.line);
+ if (active[active.length - 1] != line) active.push(line);
+ }
+ if (sameArray(cm.state.activeLines, active)) return;
+ cm.operation(function() {
+ clearActiveLines(cm);
+ for (var i = 0; i < active.length; i++) {
+ cm.addLineClass(active[i], "wrap", WRAP_CLASS);
+ cm.addLineClass(active[i], "background", BACK_CLASS);
+ cm.addLineClass(active[i], "gutter", GUTT_CLASS);
+ }
+ cm.state.activeLines = active;
+ });
+ }
+
+ function selectionChange(cm, sel) {
+ updateActiveLines(cm, sel.ranges);
+ }
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/selection/mark-selection.js b/devtools/client/sourceeditor/codemirror/addon/selection/mark-selection.js
new file mode 100644
index 000000000..5c42d21eb
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/selection/mark-selection.js
@@ -0,0 +1,118 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+// Because sometimes you need to mark the selected *text*.
+//
+// Adds an option 'styleSelectedText' which, when enabled, gives
+// selected text the CSS class given as option value, or
+// "CodeMirror-selectedtext" when the value is not a string.
+
+(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) {
+ var prev = old && old != CodeMirror.Init;
+ if (val && !prev) {
+ cm.state.markedSelection = [];
+ cm.state.markedSelectionStyle = typeof val == "string" ? val : "CodeMirror-selectedtext";
+ reset(cm);
+ cm.on("cursorActivity", onCursorActivity);
+ cm.on("change", onChange);
+ } else if (!val && prev) {
+ cm.off("cursorActivity", onCursorActivity);
+ cm.off("change", onChange);
+ clear(cm);
+ cm.state.markedSelection = cm.state.markedSelectionStyle = null;
+ }
+ });
+
+ function onCursorActivity(cm) {
+ cm.operation(function() { update(cm); });
+ }
+
+ function onChange(cm) {
+ if (cm.state.markedSelection.length)
+ cm.operation(function() { clear(cm); });
+ }
+
+ var CHUNK_SIZE = 8;
+ var Pos = CodeMirror.Pos;
+ var cmp = CodeMirror.cmpPos;
+
+ function coverRange(cm, from, to, addAt) {
+ if (cmp(from, to) == 0) return;
+ var array = cm.state.markedSelection;
+ var cls = cm.state.markedSelectionStyle;
+ for (var line = from.line;;) {
+ var start = line == from.line ? from : Pos(line, 0);
+ var endLine = line + CHUNK_SIZE, atEnd = endLine >= to.line;
+ var end = atEnd ? to : Pos(endLine, 0);
+ var mark = cm.markText(start, end, {className: cls});
+ if (addAt == null) array.push(mark);
+ else array.splice(addAt++, 0, mark);
+ if (atEnd) break;
+ line = endLine;
+ }
+ }
+
+ function clear(cm) {
+ var array = cm.state.markedSelection;
+ for (var i = 0; i < array.length; ++i) array[i].clear();
+ array.length = 0;
+ }
+
+ function reset(cm) {
+ clear(cm);
+ 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");
+
+ var array = cm.state.markedSelection;
+ if (!array.length) return coverRange(cm, from, to);
+
+ var coverStart = array[0].find(), coverEnd = array[array.length - 1].find();
+ if (!coverStart || !coverEnd || to.line - from.line < CHUNK_SIZE ||
+ cmp(from, coverEnd.to) >= 0 || cmp(to, coverStart.from) <= 0)
+ return reset(cm);
+
+ while (cmp(from, coverStart.from) > 0) {
+ array.shift().clear();
+ coverStart = array[0].find();
+ }
+ if (cmp(from, coverStart.from) < 0) {
+ if (coverStart.to.line - from.line < CHUNK_SIZE) {
+ array.shift().clear();
+ coverRange(cm, from, coverStart.to, 0);
+ } else {
+ coverRange(cm, from, coverStart.from, 0);
+ }
+ }
+
+ while (cmp(to, coverEnd.to) < 0) {
+ array.pop().clear();
+ coverEnd = array[array.length - 1].find();
+ }
+ if (cmp(to, coverEnd.to) > 0) {
+ if (to.line - coverEnd.from.line < CHUNK_SIZE) {
+ array.pop().clear();
+ coverRange(cm, coverEnd.from, to);
+ } else {
+ coverRange(cm, coverEnd.to, to);
+ }
+ }
+ }
+});
diff --git a/devtools/client/sourceeditor/codemirror/addon/tern/tern.css b/devtools/client/sourceeditor/codemirror/addon/tern/tern.css
new file mode 100644
index 000000000..c4b8a2f77
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/tern/tern.css
@@ -0,0 +1,87 @@
+.CodeMirror-Tern-completion {
+ padding-left: 22px;
+ position: relative;
+ line-height: 1.5;
+}
+.CodeMirror-Tern-completion:before {
+ position: absolute;
+ left: 2px;
+ bottom: 2px;
+ border-radius: 50%;
+ font-size: 12px;
+ font-weight: bold;
+ height: 15px;
+ width: 15px;
+ line-height: 16px;
+ text-align: center;
+ color: white;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.CodeMirror-Tern-completion-unknown:before {
+ content: "?";
+ background: #4bb;
+}
+.CodeMirror-Tern-completion-object:before {
+ content: "O";
+ background: #77c;
+}
+.CodeMirror-Tern-completion-fn:before {
+ content: "F";
+ background: #7c7;
+}
+.CodeMirror-Tern-completion-array:before {
+ content: "A";
+ background: #c66;
+}
+.CodeMirror-Tern-completion-number:before {
+ content: "1";
+ background: #999;
+}
+.CodeMirror-Tern-completion-string:before {
+ content: "S";
+ background: #999;
+}
+.CodeMirror-Tern-completion-bool:before {
+ content: "B";
+ background: #999;
+}
+
+.CodeMirror-Tern-completion-guess {
+ color: #999;
+}
+
+.CodeMirror-Tern-tooltip {
+ border: 1px solid silver;
+ border-radius: 3px;
+ color: #444;
+ padding: 2px 5px;
+ font-size: 90%;
+ font-family: monospace;
+ background-color: white;
+ white-space: pre-wrap;
+
+ max-width: 40em;
+ position: absolute;
+ z-index: 10;
+ -webkit-box-shadow: 2px 3px 5px rgba(0,0,0,.2);
+ -moz-box-shadow: 2px 3px 5px rgba(0,0,0,.2);
+ box-shadow: 2px 3px 5px rgba(0,0,0,.2);
+
+ transition: opacity 1s;
+ -moz-transition: opacity 1s;
+ -webkit-transition: opacity 1s;
+ -o-transition: opacity 1s;
+ -ms-transition: opacity 1s;
+}
+
+.CodeMirror-Tern-hint-doc {
+ max-width: 25em;
+ margin-top: -3px;
+}
+
+.CodeMirror-Tern-fname { color: black; }
+.CodeMirror-Tern-farg { color: #70a; }
+.CodeMirror-Tern-farg-current { text-decoration: underline; }
+.CodeMirror-Tern-type { color: #07c; }
+.CodeMirror-Tern-fhint-guess { opacity: .7; }
diff --git a/devtools/client/sourceeditor/codemirror/addon/tern/tern.js b/devtools/client/sourceeditor/codemirror/addon/tern/tern.js
new file mode 100644
index 000000000..efdf2ed62
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/addon/tern/tern.js
@@ -0,0 +1,701 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+// Glue code between CodeMirror and Tern.
+//
+// Create a CodeMirror.TernServer to wrap an actual Tern server,
+// register open documents (CodeMirror.Doc instances) with it, and
+// call its methods to activate the assisting functions that Tern
+// provides.
+//
+// Options supported (all optional):
+// * defs: An array of JSON definition data structures.
+// * plugins: An object mapping plugin names to configuration
+// options.
+// * getFile: A function(name, c) that can be used to access files in
+// the project that haven't been loaded yet. Simply do c(null) to
+// indicate that a file is not available.
+// * fileFilter: A function(value, docName, doc) that will be applied
+// to documents before passing them on to Tern.
+// * switchToDoc: A function(name, doc) that should, when providing a
+// multi-file view, switch the view or focus to the named file.
+// * showError: A function(editor, message) that can be used to
+// override the way errors are displayed.
+// * completionTip: Customize the content in tooltips for completions.
+// Is passed a single argument—the completion's data as returned by
+// Tern—and may return a string, DOM node, or null to indicate that
+// no tip should be shown. By default the docstring is shown.
+// * typeTip: Like completionTip, but for the tooltips shown for type
+// queries.
+// * responseFilter: A function(doc, query, request, error, data) that
+// will be applied to the Tern responses before treating them
+//
+//
+// It is possible to run the Tern server in a web worker by specifying
+// these additional options:
+// * useWorker: Set to true to enable web worker mode. You'll probably
+// want to feature detect the actual value you use here, for example
+// !!window.Worker.
+// * workerScript: The main script of the worker. Point this to
+// wherever you are hosting worker.js from this directory.
+// * workerDeps: An array of paths pointing (relative to workerScript)
+// to the Acorn and Tern libraries and any Tern plugins you want to
+// load. Or, if you minified those into a single script and included
+// them in the workerScript, simply leave this undefined.
+
+(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: tern
+
+ CodeMirror.TernServer = function(options) {
+ var self = this;
+ this.options = options || {};
+ var plugins = this.options.plugins || (this.options.plugins = {});
+ if (!plugins.doc_comment) plugins.doc_comment = true;
+ this.docs = Object.create(null);
+ if (this.options.useWorker) {
+ this.server = new WorkerServer(this);
+ } else {
+ this.server = new tern.Server({
+ getFile: function(name, c) { return getFile(self, name, c); },
+ async: true,
+ defs: this.options.defs || [],
+ plugins: plugins
+ });
+ }
+ this.trackChange = function(doc, change) { trackChange(self, doc, change); };
+
+ this.cachedArgHints = null;
+ this.activeArgHints = null;
+ this.jumpStack = [];
+
+ this.getHint = function(cm, c) { return hint(self, cm, c); };
+ this.getHint.async = true;
+ };
+
+ CodeMirror.TernServer.prototype = {
+ addDoc: function(name, doc) {
+ var data = {doc: doc, name: name, changed: null};
+ this.server.addFile(name, docValue(this, data));
+ CodeMirror.on(doc, "change", this.trackChange);
+ return this.docs[name] = data;
+ },
+
+ delDoc: function(id) {
+ var found = resolveDoc(this, id);
+ if (!found) return;
+ CodeMirror.off(found.doc, "change", this.trackChange);
+ delete this.docs[found.name];
+ this.server.delFile(found.name);
+ },
+
+ hideDoc: function(id) {
+ closeArgHints(this);
+ var found = resolveDoc(this, id);
+ if (found && found.changed) sendDoc(this, found);
+ },
+
+ complete: function(cm) {
+ cm.showHint({hint: this.getHint});
+ },
+
+ showType: function(cm, pos, c) { showContextInfo(this, cm, pos, "type", c); },
+
+ showDocs: function(cm, pos, c) { showContextInfo(this, cm, pos, "documentation", c); },
+
+ updateArgHints: function(cm) { updateArgHints(this, cm); },
+
+ jumpToDef: function(cm) { jumpToDef(this, cm); },
+
+ jumpBack: function(cm) { jumpBack(this, cm); },
+
+ rename: function(cm) { rename(this, cm); },
+
+ selectName: function(cm) { selectName(this, cm); },
+
+ request: function (cm, query, c, pos) {
+ var self = this;
+ var doc = findDoc(this, cm.getDoc());
+ var request = buildRequest(this, doc, query, pos);
+ var extraOptions = request.query && this.options.queryOptions && this.options.queryOptions[request.query.type]
+ if (extraOptions) for (var prop in extraOptions) request.query[prop] = extraOptions[prop];
+
+ this.server.request(request, function (error, data) {
+ if (!error && self.options.responseFilter)
+ data = self.options.responseFilter(doc, query, request, error, data);
+ c(error, data);
+ });
+ },
+
+ destroy: function () {
+ closeArgHints(this)
+ if (this.worker) {
+ this.worker.terminate();
+ this.worker = null;
+ }
+ }
+ };
+
+ var Pos = CodeMirror.Pos;
+ var cls = "CodeMirror-Tern-";
+ var bigDoc = 250;
+
+ function getFile(ts, name, c) {
+ var buf = ts.docs[name];
+ if (buf)
+ c(docValue(ts, buf));
+ else if (ts.options.getFile)
+ ts.options.getFile(name, c);
+ else
+ c(null);
+ }
+
+ function findDoc(ts, doc, name) {
+ for (var n in ts.docs) {
+ var cur = ts.docs[n];
+ if (cur.doc == doc) return cur;
+ }
+ if (!name) for (var i = 0;; ++i) {
+ n = "[doc" + (i || "") + "]";
+ if (!ts.docs[n]) { name = n; break; }
+ }
+ return ts.addDoc(name, doc);
+ }
+
+ function resolveDoc(ts, id) {
+ if (typeof id == "string") return ts.docs[id];
+ if (id instanceof CodeMirror) id = id.getDoc();
+ if (id instanceof CodeMirror.Doc) return findDoc(ts, id);
+ }
+
+ function trackChange(ts, doc, change) {
+ var data = findDoc(ts, doc);
+
+ var argHints = ts.cachedArgHints;
+ if (argHints && argHints.doc == doc && cmpPos(argHints.start, change.to) >= 0)
+ ts.cachedArgHints = null;
+
+ var changed = data.changed;
+ if (changed == null)
+ data.changed = changed = {from: change.from.line, to: change.from.line};
+ var end = change.from.line + (change.text.length - 1);
+ if (change.from.line < changed.to) changed.to = changed.to - (change.to.line - end);
+ if (end >= changed.to) changed.to = end + 1;
+ if (changed.from > change.from.line) changed.from = change.from.line;
+
+ if (doc.lineCount() > bigDoc && change.to - changed.from > 100) setTimeout(function() {
+ if (data.changed && data.changed.to - data.changed.from > 100) sendDoc(ts, data);
+ }, 200);
+ }
+
+ function sendDoc(ts, doc) {
+ ts.server.request({files: [{type: "full", name: doc.name, text: docValue(ts, doc)}]}, function(error) {
+ if (error) window.console.error(error);
+ else doc.changed = null;
+ });
+ }
+
+ // Completion
+
+ function hint(ts, cm, c) {
+ ts.request(cm, {type: "completions", types: true, docs: true, urls: true}, function(error, data) {
+ if (error) return showError(ts, cm, error);
+ var completions = [], after = "";
+ var from = data.start, to = data.end;
+ if (cm.getRange(Pos(from.line, from.ch - 2), from) == "[\"" &&
+ cm.getRange(to, Pos(to.line, to.ch + 2)) != "\"]")
+ after = "\"]";
+
+ for (var i = 0; i < data.completions.length; ++i) {
+ var completion = data.completions[i], className = typeToIcon(completion.type);
+ if (data.guess) className += " " + cls + "guess";
+ completions.push({text: completion.name + after,
+ displayText: completion.displayName || completion.name,
+ className: className,
+ data: completion});
+ }
+
+ var obj = {from: from, to: to, list: completions};
+ var tooltip = null;
+ CodeMirror.on(obj, "close", function() { remove(tooltip); });
+ CodeMirror.on(obj, "update", function() { remove(tooltip); });
+ CodeMirror.on(obj, "select", function(cur, node) {
+ remove(tooltip);
+ var content = ts.options.completionTip ? ts.options.completionTip(cur.data) : cur.data.doc;
+ if (content) {
+ tooltip = makeTooltip(node.parentNode.getBoundingClientRect().right + window.pageXOffset,
+ node.getBoundingClientRect().top + window.pageYOffset, content);
+ tooltip.className += " " + cls + "hint-doc";
+ }
+ });
+ c(obj);
+ });
+ }
+
+ function typeToIcon(type) {
+ var suffix;
+ if (type == "?") suffix = "unknown";
+ else if (type == "number" || type == "string" || type == "bool") suffix = type;
+ else if (/^fn\(/.test(type)) suffix = "fn";
+ else if (/^\[/.test(type)) suffix = "array";
+ else suffix = "object";
+ return cls + "completion " + cls + "completion-" + suffix;
+ }
+
+ // Type queries
+
+ function showContextInfo(ts, cm, pos, queryName, c) {
+ ts.request(cm, queryName, function(error, data) {
+ if (error) return showError(ts, cm, error);
+ if (ts.options.typeTip) {
+ var tip = ts.options.typeTip(data);
+ } else {
+ var tip = elt("span", null, elt("strong", null, data.type || "not found"));
+ if (data.doc)
+ tip.appendChild(document.createTextNode(" — " + data.doc));
+ if (data.url) {
+ tip.appendChild(document.createTextNode(" "));
+ var child = tip.appendChild(elt("a", null, "[docs]"));
+ child.href = data.url;
+ child.target = "_blank";
+ }
+ }
+ tempTooltip(cm, tip, ts);
+ if (c) c();
+ }, pos);
+ }
+
+ // Maintaining argument hints
+
+ function updateArgHints(ts, cm) {
+ closeArgHints(ts);
+
+ if (cm.somethingSelected()) return;
+ var state = cm.getTokenAt(cm.getCursor()).state;
+ var inner = CodeMirror.innerMode(cm.getMode(), state);
+ if (inner.mode.name != "javascript") return;
+ var lex = inner.state.lexical;
+ if (lex.info != "call") return;
+
+ var ch, argPos = lex.pos || 0, tabSize = cm.getOption("tabSize");
+ for (var line = cm.getCursor().line, e = Math.max(0, line - 9), found = false; line >= e; --line) {
+ var str = cm.getLine(line), extra = 0;
+ for (var pos = 0;;) {
+ var tab = str.indexOf("\t", pos);
+ if (tab == -1) break;
+ extra += tabSize - (tab + extra) % tabSize - 1;
+ pos = tab + 1;
+ }
+ ch = lex.column - extra;
+ if (str.charAt(ch) == "(") {found = true; break;}
+ }
+ if (!found) return;
+
+ var start = Pos(line, ch);
+ var cache = ts.cachedArgHints;
+ if (cache && cache.doc == cm.getDoc() && cmpPos(start, cache.start) == 0)
+ return showArgHints(ts, cm, argPos);
+
+ ts.request(cm, {type: "type", preferFunction: true, end: start}, function(error, data) {
+ if (error || !data.type || !(/^fn\(/).test(data.type)) return;
+ ts.cachedArgHints = {
+ start: start,
+ type: parseFnType(data.type),
+ name: data.exprName || data.name || "fn",
+ guess: data.guess,
+ doc: cm.getDoc()
+ };
+ showArgHints(ts, cm, argPos);
+ });
+ }
+
+ function showArgHints(ts, cm, pos) {
+ closeArgHints(ts);
+
+ var cache = ts.cachedArgHints, tp = cache.type;
+ var tip = elt("span", cache.guess ? cls + "fhint-guess" : null,
+ elt("span", cls + "fname", cache.name), "(");
+ for (var i = 0; i < tp.args.length; ++i) {
+ if (i) tip.appendChild(document.createTextNode(", "));
+ var arg = tp.args[i];
+ tip.appendChild(elt("span", cls + "farg" + (i == pos ? " " + cls + "farg-current" : ""), arg.name || "?"));
+ if (arg.type != "?") {
+ tip.appendChild(document.createTextNode(":\u00a0"));
+ tip.appendChild(elt("span", cls + "type", arg.type));
+ }
+ }
+ tip.appendChild(document.createTextNode(tp.rettype ? ") ->\u00a0" : ")"));
+ if (tp.rettype) tip.appendChild(elt("span", cls + "type", tp.rettype));
+ var place = cm.cursorCoords(null, "page");
+ ts.activeArgHints = makeTooltip(place.right + 1, place.bottom, tip);
+ }
+
+ function parseFnType(text) {
+ var args = [], pos = 3;
+
+ function skipMatching(upto) {
+ var depth = 0, start = pos;
+ for (;;) {
+ var next = text.charAt(pos);
+ if (upto.test(next) && !depth) return text.slice(start, pos);
+ if (/[{\[\(]/.test(next)) ++depth;
+ else if (/[}\]\)]/.test(next)) --depth;
+ ++pos;
+ }
+ }
+
+ // Parse arguments
+ if (text.charAt(pos) != ")") for (;;) {
+ var name = text.slice(pos).match(/^([^, \(\[\{]+): /);
+ if (name) {
+ pos += name[0].length;
+ name = name[1];
+ }
+ args.push({name: name, type: skipMatching(/[\),]/)});
+ if (text.charAt(pos) == ")") break;
+ pos += 2;
+ }
+
+ var rettype = text.slice(pos).match(/^\) -> (.*)$/);
+
+ return {args: args, rettype: rettype && rettype[1]};
+ }
+
+ // Moving to the definition of something
+
+ function jumpToDef(ts, cm) {
+ function inner(varName) {
+ var req = {type: "definition", variable: varName || null};
+ var doc = findDoc(ts, cm.getDoc());
+ ts.server.request(buildRequest(ts, doc, req), function(error, data) {
+ if (error) return showError(ts, cm, error);
+ if (!data.file && data.url) { window.open(data.url); return; }
+
+ if (data.file) {
+ var localDoc = ts.docs[data.file], found;
+ if (localDoc && (found = findContext(localDoc.doc, data))) {
+ ts.jumpStack.push({file: doc.name,
+ start: cm.getCursor("from"),
+ end: cm.getCursor("to")});
+ moveTo(ts, doc, localDoc, found.start, found.end);
+ return;
+ }
+ }
+ showError(ts, cm, "Could not find a definition.");
+ });
+ }
+
+ if (!atInterestingExpression(cm))
+ dialog(cm, "Jump to variable", function(name) { if (name) inner(name); });
+ else
+ inner();
+ }
+
+ function jumpBack(ts, cm) {
+ var pos = ts.jumpStack.pop(), doc = pos && ts.docs[pos.file];
+ if (!doc) return;
+ moveTo(ts, findDoc(ts, cm.getDoc()), doc, pos.start, pos.end);
+ }
+
+ function moveTo(ts, curDoc, doc, start, end) {
+ doc.doc.setSelection(start, end);
+ if (curDoc != doc && ts.options.switchToDoc) {
+ closeArgHints(ts);
+ ts.options.switchToDoc(doc.name, doc.doc);
+ }
+ }
+
+ // The {line,ch} representation of positions makes this rather awkward.
+ function findContext(doc, data) {
+ var before = data.context.slice(0, data.contextOffset).split("\n");
+ var startLine = data.start.line - (before.length - 1);
+ var start = Pos(startLine, (before.length == 1 ? data.start.ch : doc.getLine(startLine).length) - before[0].length);
+
+ var text = doc.getLine(startLine).slice(start.ch);
+ for (var cur = startLine + 1; cur < doc.lineCount() && text.length < data.context.length; ++cur)
+ text += "\n" + doc.getLine(cur);
+ if (text.slice(0, data.context.length) == data.context) return data;
+
+ var cursor = doc.getSearchCursor(data.context, 0, false);
+ var nearest, nearestDist = Infinity;
+ while (cursor.findNext()) {
+ var from = cursor.from(), dist = Math.abs(from.line - start.line) * 10000;
+ if (!dist) dist = Math.abs(from.ch - start.ch);
+ if (dist < nearestDist) { nearest = from; nearestDist = dist; }
+ }
+ if (!nearest) return null;
+
+ if (before.length == 1)
+ nearest.ch += before[0].length;
+ else
+ nearest = Pos(nearest.line + (before.length - 1), before[before.length - 1].length);
+ if (data.start.line == data.end.line)
+ var end = Pos(nearest.line, nearest.ch + (data.end.ch - data.start.ch));
+ else
+ var end = Pos(nearest.line + (data.end.line - data.start.line), data.end.ch);
+ return {start: nearest, end: end};
+ }
+
+ function atInterestingExpression(cm) {
+ var pos = cm.getCursor("end"), tok = cm.getTokenAt(pos);
+ if (tok.start < pos.ch && tok.type == "comment") return false;
+ return /[\w)\]]/.test(cm.getLine(pos.line).slice(Math.max(pos.ch - 1, 0), pos.ch + 1));
+ }
+
+ // Variable renaming
+
+ function rename(ts, cm) {
+ var token = cm.getTokenAt(cm.getCursor());
+ if (!/\w/.test(token.string)) return showError(ts, cm, "Not at a variable");
+ dialog(cm, "New name for " + token.string, function(newName) {
+ ts.request(cm, {type: "rename", newName: newName, fullDocs: true}, function(error, data) {
+ if (error) return showError(ts, cm, error);
+ applyChanges(ts, data.changes);
+ });
+ });
+ }
+
+ function selectName(ts, cm) {
+ var name = findDoc(ts, cm.doc).name;
+ ts.request(cm, {type: "refs"}, function(error, data) {
+ if (error) return showError(ts, cm, error);
+ var ranges = [], cur = 0;
+ var curPos = cm.getCursor();
+ for (var i = 0; i < data.refs.length; i++) {
+ var ref = data.refs[i];
+ if (ref.file == name) {
+ ranges.push({anchor: ref.start, head: ref.end});
+ if (cmpPos(curPos, ref.start) >= 0 && cmpPos(curPos, ref.end) <= 0)
+ cur = ranges.length - 1;
+ }
+ }
+ cm.setSelections(ranges, cur);
+ });
+ }
+
+ var nextChangeOrig = 0;
+ function applyChanges(ts, changes) {
+ var perFile = Object.create(null);
+ for (var i = 0; i < changes.length; ++i) {
+ var ch = changes[i];
+ (perFile[ch.file] || (perFile[ch.file] = [])).push(ch);
+ }
+ for (var file in perFile) {
+ var known = ts.docs[file], chs = perFile[file];;
+ if (!known) continue;
+ chs.sort(function(a, b) { return cmpPos(b.start, a.start); });
+ var origin = "*rename" + (++nextChangeOrig);
+ for (var i = 0; i < chs.length; ++i) {
+ var ch = chs[i];
+ known.doc.replaceRange(ch.text, ch.start, ch.end, origin);
+ }
+ }
+ }
+
+ // Generic request-building helper
+
+ function buildRequest(ts, doc, query, pos) {
+ var files = [], offsetLines = 0, allowFragments = !query.fullDocs;
+ if (!allowFragments) delete query.fullDocs;
+ if (typeof query == "string") query = {type: query};
+ query.lineCharPositions = true;
+ if (query.end == null) {
+ query.end = pos || doc.doc.getCursor("end");
+ if (doc.doc.somethingSelected())
+ query.start = doc.doc.getCursor("start");
+ }
+ var startPos = query.start || query.end;
+
+ if (doc.changed) {
+ if (doc.doc.lineCount() > bigDoc && allowFragments !== false &&
+ doc.changed.to - doc.changed.from < 100 &&
+ doc.changed.from <= startPos.line && doc.changed.to > query.end.line) {
+ files.push(getFragmentAround(doc, startPos, query.end));
+ query.file = "#0";
+ var offsetLines = files[0].offsetLines;
+ if (query.start != null) query.start = Pos(query.start.line - -offsetLines, query.start.ch);
+ query.end = Pos(query.end.line - offsetLines, query.end.ch);
+ } else {
+ files.push({type: "full",
+ name: doc.name,
+ text: docValue(ts, doc)});
+ query.file = doc.name;
+ doc.changed = null;
+ }
+ } else {
+ query.file = doc.name;
+ }
+ for (var name in ts.docs) {
+ var cur = ts.docs[name];
+ if (cur.changed && cur != doc) {
+ files.push({type: "full", name: cur.name, text: docValue(ts, cur)});
+ cur.changed = null;
+ }
+ }
+
+ return {query: query, files: files};
+ }
+
+ function getFragmentAround(data, start, end) {
+ var doc = data.doc;
+ var minIndent = null, minLine = null, endLine, tabSize = 4;
+ for (var p = start.line - 1, min = Math.max(0, p - 50); p >= min; --p) {
+ var line = doc.getLine(p), fn = line.search(/\bfunction\b/);
+ if (fn < 0) continue;
+ var indent = CodeMirror.countColumn(line, null, tabSize);
+ if (minIndent != null && minIndent <= indent) continue;
+ minIndent = indent;
+ minLine = p;
+ }
+ if (minLine == null) minLine = min;
+ var max = Math.min(doc.lastLine(), end.line + 20);
+ if (minIndent == null || minIndent == CodeMirror.countColumn(doc.getLine(start.line), null, tabSize))
+ endLine = max;
+ else for (endLine = end.line + 1; endLine < max; ++endLine) {
+ var indent = CodeMirror.countColumn(doc.getLine(endLine), null, tabSize);
+ if (indent <= minIndent) break;
+ }
+ var from = Pos(minLine, 0);
+
+ return {type: "part",
+ name: data.name,
+ offsetLines: from.line,
+ text: doc.getRange(from, Pos(endLine, 0))};
+ }
+
+ // Generic utilities
+
+ var cmpPos = CodeMirror.cmpPos;
+
+ function elt(tagname, cls /*, ... elts*/) {
+ var e = document.createElement(tagname);
+ if (cls) e.className = cls;
+ for (var i = 2; i < arguments.length; ++i) {
+ var elt = arguments[i];
+ if (typeof elt == "string") elt = document.createTextNode(elt);
+ e.appendChild(elt);
+ }
+ return e;
+ }
+
+ function dialog(cm, text, f) {
+ if (cm.openDialog)
+ cm.openDialog(text + ": <input type=text>", f);
+ else
+ f(prompt(text, ""));
+ }
+
+ // Tooltips
+
+ function tempTooltip(cm, content, ts) {
+ if (cm.state.ternTooltip) remove(cm.state.ternTooltip);
+ var where = cm.cursorCoords();
+ var tip = cm.state.ternTooltip = makeTooltip(where.right + 1, where.bottom, content);
+ function maybeClear() {
+ old = true;
+ if (!mouseOnTip) clear();
+ }
+ function clear() {
+ cm.state.ternTooltip = null;
+ if (!tip.parentNode) return;
+ cm.off("cursorActivity", clear);
+ cm.off('blur', clear);
+ cm.off('scroll', clear);
+ fadeOut(tip);
+ }
+ var mouseOnTip = false, old = false;
+ CodeMirror.on(tip, "mousemove", function() { mouseOnTip = true; });
+ CodeMirror.on(tip, "mouseout", function(e) {
+ if (!CodeMirror.contains(tip, e.relatedTarget || e.toElement)) {
+ if (old) clear();
+ else mouseOnTip = false;
+ }
+ });
+ setTimeout(maybeClear, ts.options.hintDelay ? ts.options.hintDelay : 1700);
+ cm.on("cursorActivity", clear);
+ cm.on('blur', clear);
+ cm.on('scroll', clear);
+ }
+
+ function makeTooltip(x, y, content) {
+ var node = elt("div", cls + "tooltip", content);
+ node.style.left = x + "px";
+ node.style.top = y + "px";
+ document.body.appendChild(node);
+ return node;
+ }
+
+ function remove(node) {
+ var p = node && node.parentNode;
+ if (p) p.removeChild(node);
+ }
+
+ function fadeOut(tooltip) {
+ tooltip.style.opacity = "0";
+ setTimeout(function() { remove(tooltip); }, 1100);
+ }
+
+ function showError(ts, cm, msg) {
+ if (ts.options.showError)
+ ts.options.showError(cm, msg);
+ else
+ tempTooltip(cm, String(msg), ts);
+ }
+
+ function closeArgHints(ts) {
+ if (ts.activeArgHints) { remove(ts.activeArgHints); ts.activeArgHints = null; }
+ }
+
+ function docValue(ts, doc) {
+ var val = doc.doc.getValue();
+ if (ts.options.fileFilter) val = ts.options.fileFilter(val, doc.name, doc.doc);
+ return val;
+ }
+
+ // Worker wrapper
+
+ function WorkerServer(ts) {
+ var worker = ts.worker = new Worker(ts.options.workerScript);
+ worker.postMessage({type: "init",
+ defs: ts.options.defs,
+ plugins: ts.options.plugins,
+ scripts: ts.options.workerDeps});
+ var msgId = 0, pending = {};
+
+ function send(data, c) {
+ if (c) {
+ data.id = ++msgId;
+ pending[msgId] = c;
+ }
+ worker.postMessage(data);
+ }
+ worker.onmessage = function(e) {
+ var data = e.data;
+ if (data.type == "getFile") {
+ getFile(ts, data.name, function(err, text) {
+ send({type: "getFile", err: String(err), text: text, id: data.id});
+ });
+ } else if (data.type == "debug") {
+ window.console.log(data.message);
+ } else if (data.id && pending[data.id]) {
+ pending[data.id](data.err, data.body);
+ delete pending[data.id];
+ }
+ };
+ worker.onerror = function(e) {
+ for (var id in pending) pending[id](e);
+ pending = {};
+ };
+
+ this.addFile = function(name, text) { send({type: "add", name: name, text: text}); };
+ this.delFile = function(name) { send({type: "del", name: name}); };
+ this.request = function(body, c) { send({type: "req", body: body}, c); };
+ }
+});
diff --git a/devtools/client/sourceeditor/codemirror/codemirror.bundle.js b/devtools/client/sourceeditor/codemirror/codemirror.bundle.js
new file mode 100644
index 000000000..e01abc925
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/codemirror.bundle.js
@@ -0,0 +1,20152 @@
+var CodeMirror =
+/******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId])
+/******/ return installedModules[moduleId].exports;
+
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ exports: {},
+/******/ id: moduleId,
+/******/ loaded: false
+/******/ };
+
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+
+/******/ // Flag the module as loaded
+/******/ module.loaded = true;
+
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+
+
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ function(module, exports, __webpack_require__) {
+
+ __webpack_require__(1);
+ __webpack_require__(3);
+ __webpack_require__(4);
+ __webpack_require__(5);
+ __webpack_require__(6);
+ __webpack_require__(7);
+ __webpack_require__(8);
+ __webpack_require__(9);
+ __webpack_require__(10);
+ __webpack_require__(11);
+ __webpack_require__(12);
+ __webpack_require__(13);
+ __webpack_require__(14);
+ __webpack_require__(15);
+ __webpack_require__(16);
+ __webpack_require__(17);
+ __webpack_require__(18);
+ __webpack_require__(19);
+ __webpack_require__(20);
+ __webpack_require__(21);
+ __webpack_require__(22);
+ __webpack_require__(23);
+ module.exports = __webpack_require__(2);
+
+
+/***/ },
+/* 1 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ // Open simple dialogs on top of an editor. Relies on dialog.css.
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2));
+ 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;
+ dialog = wrap.appendChild(document.createElement("div"));
+ if (bottom)
+ dialog.className = "CodeMirror-dialog CodeMirror-dialog-bottom";
+ else
+ dialog.className = "CodeMirror-dialog CodeMirror-dialog-top";
+
+ if (typeof template == "string") {
+ dialog.innerHTML = template;
+ } else { // Assuming it's a detached DOM element.
+ dialog.appendChild(template);
+ }
+ return dialog;
+ }
+
+ function closeNotification(cm, newVal) {
+ if (cm.state.currentNotificationClose)
+ cm.state.currentNotificationClose();
+ cm.state.currentNotificationClose = newVal;
+ }
+
+ CodeMirror.defineExtension("openDialog", function(template, callback, options) {
+ if (!options) options = {};
+
+ closeNotification(this, null);
+
+ var dialog = dialogDiv(this, template, options.bottom);
+ var closed = false, me = this;
+ function close(newVal) {
+ if (typeof newVal == 'string') {
+ inp.value = newVal;
+ } else {
+ if (closed) return;
+ closed = true;
+ dialog.parentNode.removeChild(dialog);
+ me.focus();
+
+ if (options.onClose) options.onClose(dialog);
+ }
+ }
+
+ var inp = dialog.getElementsByTagName("input")[0], button;
+ if (inp) {
+ inp.focus();
+
+ if (options.value) {
+ inp.value = options.value;
+ if (options.selectValueOnOpen !== false) {
+ inp.select();
+ }
+ }
+
+ if (options.onInput)
+ CodeMirror.on(inp, "input", function(e) { options.onInput(e, inp.value, close);});
+ if (options.onKeyUp)
+ CodeMirror.on(inp, "keyup", function(e) {options.onKeyUp(e, inp.value, close);});
+
+ CodeMirror.on(inp, "keydown", function(e) {
+ if (options && options.onKeyDown && options.onKeyDown(e, inp.value, close)) { return; }
+ if (e.keyCode == 27 || (options.closeOnEnter !== false && e.keyCode == 13)) {
+ inp.blur();
+ CodeMirror.e_stop(e);
+ close();
+ }
+ if (e.keyCode == 13) callback(inp.value, e);
+ });
+
+ if (options.closeOnBlur !== false) CodeMirror.on(inp, "blur", close);
+ } else if (button = dialog.getElementsByTagName("button")[0]) {
+ CodeMirror.on(button, "click", function() {
+ close();
+ me.focus();
+ });
+
+ if (options.closeOnBlur !== false) CodeMirror.on(button, "blur", close);
+
+ button.focus();
+ }
+ return close;
+ });
+
+ CodeMirror.defineExtension("openConfirm", function(template, callbacks, options) {
+ closeNotification(this, null);
+ var dialog = dialogDiv(this, template, options && options.bottom);
+ var buttons = dialog.getElementsByTagName("button");
+ var closed = false, me = this, blurring = 1;
+ function close() {
+ if (closed) return;
+ closed = true;
+ dialog.parentNode.removeChild(dialog);
+ me.focus();
+ }
+ buttons[0].focus();
+ for (var i = 0; i < buttons.length; ++i) {
+ var b = buttons[i];
+ (function(callback) {
+ CodeMirror.on(b, "click", function(e) {
+ CodeMirror.e_preventDefault(e);
+ close();
+ if (callback) callback(me);
+ });
+ })(callbacks[i]);
+ CodeMirror.on(b, "blur", function() {
+ --blurring;
+ setTimeout(function() { if (blurring <= 0) close(); }, 200);
+ });
+ CodeMirror.on(b, "focus", function() { ++blurring; });
+ }
+ });
+
+ /*
+ * openNotification
+ * Opens a notification, that can be closed with an optional timer
+ * (default 5000ms timer) and always closes on click.
+ *
+ * If a notification is opened while another is opened, it will close the
+ * currently opened one and open the new one immediately.
+ */
+ CodeMirror.defineExtension("openNotification", function(template, options) {
+ closeNotification(this, close);
+ var dialog = dialogDiv(this, template, options && options.bottom);
+ var closed = false, doneTimer;
+ var duration = options && typeof options.duration !== "undefined" ? options.duration : 5000;
+
+ function close() {
+ if (closed) return;
+ closed = true;
+ clearTimeout(doneTimer);
+ dialog.parentNode.removeChild(dialog);
+ }
+
+ CodeMirror.on(dialog, 'click', function(e) {
+ CodeMirror.e_preventDefault(e);
+ close();
+ });
+
+ if (duration)
+ doneTimer = setTimeout(close, duration);
+
+ return close;
+ });
+ });
+
+
+/***/ },
+/* 2 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ // This is CodeMirror (http://codemirror.net), a code editor
+ // implemented in JavaScript on top of the browser's DOM.
+ //
+ // You can find some technical background for some of the code below
+ // at http://marijnhaverbeke.nl/blog/#cm-internals .
+
+ (function(mod) {
+ if (true) // CommonJS
+ module.exports = mod();
+ else if (typeof define == "function" && define.amd) // AMD
+ return define([], mod);
+ else // Plain browser env
+ (this || window).CodeMirror = mod();
+ })(function() {
+ "use strict";
+
+ // BROWSER SNIFFING
+
+ // Kludges for bugs and behavior differences that can't be feature
+ // detected are enabled based on userAgent etc sniffing.
+ var userAgent = navigator.userAgent;
+ var platform = navigator.platform;
+
+ var gecko = /gecko\/\d/i.test(userAgent);
+ var ie_upto10 = /MSIE \d/.test(userAgent);
+ var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(userAgent);
+ var ie = ie_upto10 || ie_11up;
+ var ie_version = ie && (ie_upto10 ? document.documentMode || 6 : ie_11up[1]);
+ var webkit = /WebKit\//.test(userAgent);
+ var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(userAgent);
+ var chrome = /Chrome\//.test(userAgent);
+ var presto = /Opera\//.test(userAgent);
+ var safari = /Apple Computer/.test(navigator.vendor);
+ var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(userAgent);
+ var phantom = /PhantomJS/.test(userAgent);
+
+ var ios = /AppleWebKit/.test(userAgent) && /Mobile\/\w+/.test(userAgent);
+ // This is woefully incomplete. Suggestions for alternative methods welcome.
+ var mobile = ios || /Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(userAgent);
+ var mac = ios || /Mac/.test(platform);
+ var chromeOS = /\bCrOS\b/.test(userAgent);
+ var windows = /win/i.test(platform);
+
+ var presto_version = presto && 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 || presto && (presto_version == null || presto_version < 12.11));
+ var captureRightClick = gecko || (ie && ie_version >= 9);
+
+ // Optimize some code when these features are not used.
+ var sawReadOnlySpans = false, sawCollapsedSpans = false;
+
+ // 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 ? copyObj(options) : {};
+ // Determine effective options based on given values and defaults.
+ copyObj(defaults, options, false);
+ setGuttersForLineNumbers(options);
+
+ var doc = options.value;
+ if (typeof doc == "string") doc = new Doc(doc, options.mode, null, options.lineSeparator);
+ this.doc = doc;
+
+ var input = new CodeMirror.inputStyles[options.inputStyle](this);
+ var display = this.display = new Display(place, doc, input);
+ display.wrapper.CodeMirror = this;
+ updateGutters(this);
+ themeChanged(this);
+ if (options.lineWrapping)
+ this.display.wrapper.className += " CodeMirror-wrap";
+ if (options.autofocus && !mobile) display.input.focus();
+ initScrollbars(this);
+
+ 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,
+ delayingBlurEvent: 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 input.poll
+ selectingText: false,
+ draggingText: false,
+ highlight: new Delayed(), // stores highlight worker timeout
+ keySeq: null, // Unfinished key sequence
+ specialChars: null
+ };
+
+ var cm = this;
+
+ // Override magic textarea content restore that IE sometimes does
+ // on our hidden textarea on reload
+ if (ie && ie_version < 11) setTimeout(function() { cm.display.input.reset(true); }, 20);
+
+ registerEventHandlers(this);
+ ensureGlobalHandlers();
+
+ startOperation(this);
+ this.curOp.forceUpdate = true;
+ attachDoc(this, doc);
+
+ if ((options.autofocus && !mobile) || cm.hasFocus())
+ setTimeout(bind(onFocus, this), 20);
+ else
+ onBlur(this);
+
+ for (var opt in optionHandlers) if (optionHandlers.hasOwnProperty(opt))
+ optionHandlers[opt](this, options[opt], Init);
+ maybeUpdateLineNumberWidth(this);
+ if (options.finishInit) options.finishInit(this);
+ for (var i = 0; i < initHooks.length; ++i) initHooks[i](this);
+ endOperation(this);
+ // Suppress optimizelegibility in Webkit, since it breaks text
+ // measuring on line wrapping boundaries.
+ if (webkit && options.lineWrapping &&
+ getComputedStyle(display.lineDiv).textRendering == "optimizelegibility")
+ display.lineDiv.style.textRendering = "auto";
+ }
+
+ // DISPLAY CONSTRUCTOR
+
+ // The display handles the DOM integration, both for input reading
+ // and content drawing. It holds references to DOM nodes and
+ // display-related state.
+
+ function Display(place, doc, input) {
+ var d = this;
+ this.input = input;
+
+ // Covers bottom-right square when both scrollbars are present.
+ d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler");
+ d.scrollbarFiller.setAttribute("cm-not-content", "true");
+ // Covers bottom of gutter when coverGutterNextToScrollbar is on
+ // and h scrollbar is present.
+ d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler");
+ d.gutterFiller.setAttribute("cm-not-content", "true");
+ // 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");
+ 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.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 document, allowing scrolling.
+ d.sizer = elt("div", [d.mover], "CodeMirror-sizer");
+ d.sizerWidth = null;
+ // 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: " + scrollerGap + "px; width: 1px;");
+ // Will contain the gutters, if any.
+ d.gutters = elt("div", null, "CodeMirror-gutters");
+ d.lineGutter = null;
+ // 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.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror");
+
+ // Work around IE7 z-index bug (not perfect, hence IE7 not really being supported)
+ if (ie && ie_version < 8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; }
+ if (!webkit && !(gecko && mobile)) d.scroller.draggable = true;
+
+ if (place) {
+ 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;
+ d.reportedViewFrom = d.reportedViewTo = doc.first;
+ // Information about the rendered lines.
+ d.view = [];
+ d.renderedView = null;
+ // 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.lastWrapHeight = d.lastWrapWidth = 0;
+ d.updateLineNumbers = null;
+
+ d.nativeBarWidth = d.barHeight = d.barWidth = 0;
+ d.scrollbarsClipped = false;
+
+ // 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;
+ // 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;
+
+ d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null;
+
+ // Tracks the maximum line length so that the horizontal scrollbar
+ // can be kept static when scrolling.
+ d.maxLine = null;
+ d.maxLineLength = 0;
+ d.maxLineChanged = false;
+
+ // Used for measuring wheel scrolling granularity
+ d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null;
+
+ // True when shift is held down.
+ d.shift = false;
+
+ // Used to track whether anything happened since the context menu
+ // was opened.
+ d.selForContextMenu = null;
+
+ d.activeTouch = null;
+
+ input.init(d);
+ }
+
+ // STATE UPDATES
+
+ // Used to get the editor into a consistent state again when options change.
+
+ function loadMode(cm) {
+ cm.doc.mode = CodeMirror.getMode(cm.options, cm.doc.modeOption);
+ resetModeState(cm);
+ }
+
+ function resetModeState(cm) {
+ cm.doc.iter(function(line) {
+ if (line.stateAfter) line.stateAfter = null;
+ if (line.styles) line.styles = null;
+ });
+ cm.doc.frontier = cm.doc.first;
+ startWorker(cm, 100);
+ cm.state.modeGen++;
+ if (cm.curOp) regChange(cm);
+ }
+
+ function wrappingChanged(cm) {
+ if (cm.options.lineWrapping) {
+ addClass(cm.display.wrapper, "CodeMirror-wrap");
+ cm.display.sizer.style.minWidth = "";
+ cm.display.sizerWidth = null;
+ } else {
+ rmClass(cm.display.wrapper, "CodeMirror-wrap");
+ findMaxLine(cm);
+ }
+ estimateLineHeights(cm);
+ regChange(cm);
+ clearCaches(cm);
+ 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;
+
+ 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 widgetsHeight + th;
+ };
+ }
+
+ function estimateLineHeights(cm) {
+ var doc = cm.doc, est = estimateHeight(cm);
+ doc.iter(function(line) {
+ var estHeight = est(line);
+ if (estHeight != line.height) updateLineHeight(line, estHeight);
+ });
+ }
+
+ function themeChanged(cm) {
+ cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-s-\S+/g, "") +
+ cm.options.theme.replace(/(^|\s)\s*/g, " cm-s-");
+ clearCaches(cm);
+ }
+
+ function guttersChanged(cm) {
+ updateGutters(cm);
+ regChange(cm);
+ 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);
+ for (var i = 0; i < specs.length; ++i) {
+ var gutterClass = specs[i];
+ var gElt = gutters.appendChild(elt("div", null, "CodeMirror-gutter " + gutterClass));
+ if (gutterClass == "CodeMirror-linenumbers") {
+ cm.display.lineGutter = gElt;
+ gElt.style.width = (cm.display.lineNumWidth || 1) + "px";
+ }
+ }
+ gutters.style.display = i ? "" : "none";
+ updateGutterSpace(cm);
+ }
+
+ function updateGutterSpace(cm) {
+ var width = cm.display.gutters.offsetWidth;
+ cm.display.sizer.style.marginLeft = width + "px";
+ }
+
+ // 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(0, true);
+ cur = found.from.line;
+ len += found.from.ch - found.to.ch;
+ }
+ cur = line;
+ while (merged = collapsedSpanAtEnd(cur)) {
+ var found = merged.find(0, true);
+ len -= cur.text.length - found.from.ch;
+ cur = found.to.line;
+ len += cur.text.length - found.to.ch;
+ }
+ return len;
+ }
+
+ // 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(d.maxLine);
+ d.maxLineChanged = true;
+ doc.iter(function(line) {
+ var len = lineLength(line);
+ if (len > d.maxLineLength) {
+ d.maxLineLength = len;
+ d.maxLine = line;
+ }
+ });
+ }
+
+ // Make sure the gutters options contains the element
+ // "CodeMirror-linenumbers" when the lineNumbers option is true.
+ function setGuttersForLineNumbers(options) {
+ var found = indexOf(options.gutters, "CodeMirror-linenumbers");
+ if (found == -1 && options.lineNumbers) {
+ options.gutters = options.gutters.concat(["CodeMirror-linenumbers"]);
+ } else if (found > -1 && !options.lineNumbers) {
+ options.gutters = options.gutters.slice(0);
+ options.gutters.splice(found, 1);
+ }
+ }
+
+ // SCROLLBARS
+
+ // Prepare DOM reads needed to update the scrollbars. Done in one
+ // shot to minimize update/measure roundtrips.
+ function measureForScrollbars(cm) {
+ var d = cm.display, gutterW = d.gutters.offsetWidth;
+ var docH = Math.round(cm.doc.height + paddingVert(cm.display));
+ return {
+ clientHeight: d.scroller.clientHeight,
+ viewHeight: d.wrapper.clientHeight,
+ scrollWidth: d.scroller.scrollWidth, clientWidth: d.scroller.clientWidth,
+ viewWidth: d.wrapper.clientWidth,
+ barLeft: cm.options.fixedGutter ? gutterW : 0,
+ docHeight: docH,
+ scrollHeight: docH + scrollGap(cm) + d.barHeight,
+ nativeBarWidth: d.nativeBarWidth,
+ gutterWidth: gutterW
+ };
+ }
+
+ function NativeScrollbars(place, scroll, cm) {
+ this.cm = cm;
+ var vert = this.vert = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar");
+ var horiz = this.horiz = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar");
+ place(vert); place(horiz);
+
+ on(vert, "scroll", function() {
+ if (vert.clientHeight) scroll(vert.scrollTop, "vertical");
+ });
+ on(horiz, "scroll", function() {
+ if (horiz.clientWidth) scroll(horiz.scrollLeft, "horizontal");
+ });
+
+ this.checkedZeroWidth = false;
+ // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8).
+ if (ie && ie_version < 8) this.horiz.style.minHeight = this.vert.style.minWidth = "18px";
+ }
+
+ NativeScrollbars.prototype = copyObj({
+ update: function(measure) {
+ var needsH = measure.scrollWidth > measure.clientWidth + 1;
+ var needsV = measure.scrollHeight > measure.clientHeight + 1;
+ var sWidth = measure.nativeBarWidth;
+
+ if (needsV) {
+ this.vert.style.display = "block";
+ this.vert.style.bottom = needsH ? sWidth + "px" : "0";
+ var totalHeight = measure.viewHeight - (needsH ? sWidth : 0);
+ // A bug in IE8 can cause this value to be negative, so guard it.
+ this.vert.firstChild.style.height =
+ Math.max(0, measure.scrollHeight - measure.clientHeight + totalHeight) + "px";
+ } else {
+ this.vert.style.display = "";
+ this.vert.firstChild.style.height = "0";
+ }
+
+ if (needsH) {
+ this.horiz.style.display = "block";
+ this.horiz.style.right = needsV ? sWidth + "px" : "0";
+ this.horiz.style.left = measure.barLeft + "px";
+ var totalWidth = measure.viewWidth - measure.barLeft - (needsV ? sWidth : 0);
+ this.horiz.firstChild.style.width =
+ (measure.scrollWidth - measure.clientWidth + totalWidth) + "px";
+ } else {
+ this.horiz.style.display = "";
+ this.horiz.firstChild.style.width = "0";
+ }
+
+ if (!this.checkedZeroWidth && measure.clientHeight > 0) {
+ if (sWidth == 0) this.zeroWidthHack();
+ this.checkedZeroWidth = true;
+ }
+
+ return {right: needsV ? sWidth : 0, bottom: needsH ? sWidth : 0};
+ },
+ setScrollLeft: function(pos) {
+ if (this.horiz.scrollLeft != pos) this.horiz.scrollLeft = pos;
+ if (this.disableHoriz) this.enableZeroWidthBar(this.horiz, this.disableHoriz);
+ },
+ setScrollTop: function(pos) {
+ if (this.vert.scrollTop != pos) this.vert.scrollTop = pos;
+ if (this.disableVert) this.enableZeroWidthBar(this.vert, this.disableVert);
+ },
+ zeroWidthHack: function() {
+ var w = mac && !mac_geMountainLion ? "12px" : "18px";
+ this.horiz.style.height = this.vert.style.width = w;
+ this.horiz.style.pointerEvents = this.vert.style.pointerEvents = "none";
+ this.disableHoriz = new Delayed;
+ this.disableVert = new Delayed;
+ },
+ enableZeroWidthBar: function(bar, delay) {
+ bar.style.pointerEvents = "auto";
+ function maybeDisable() {
+ // To find out whether the scrollbar is still visible, we
+ // check whether the element under the pixel in the bottom
+ // left corner of the scrollbar box is the scrollbar box
+ // itself (when the bar is still visible) or its filler child
+ // (when the bar is hidden). If it is still visible, we keep
+ // it enabled, if it's hidden, we disable pointer events.
+ var box = bar.getBoundingClientRect();
+ var elt = document.elementFromPoint(box.left + 1, box.bottom - 1);
+ if (elt != bar) bar.style.pointerEvents = "none";
+ else delay.set(1000, maybeDisable);
+ }
+ delay.set(1000, maybeDisable);
+ },
+ clear: function() {
+ var parent = this.horiz.parentNode;
+ parent.removeChild(this.horiz);
+ parent.removeChild(this.vert);
+ }
+ }, NativeScrollbars.prototype);
+
+ function NullScrollbars() {}
+
+ NullScrollbars.prototype = copyObj({
+ update: function() { return {bottom: 0, right: 0}; },
+ setScrollLeft: function() {},
+ setScrollTop: function() {},
+ clear: function() {}
+ }, NullScrollbars.prototype);
+
+ CodeMirror.scrollbarModel = {"native": NativeScrollbars, "null": NullScrollbars};
+
+ function initScrollbars(cm) {
+ if (cm.display.scrollbars) {
+ cm.display.scrollbars.clear();
+ if (cm.display.scrollbars.addClass)
+ rmClass(cm.display.wrapper, cm.display.scrollbars.addClass);
+ }
+
+ cm.display.scrollbars = new CodeMirror.scrollbarModel[cm.options.scrollbarStyle](function(node) {
+ cm.display.wrapper.insertBefore(node, cm.display.scrollbarFiller);
+ // Prevent clicks in the scrollbars from killing focus
+ on(node, "mousedown", function() {
+ if (cm.state.focused) setTimeout(function() { cm.display.input.focus(); }, 0);
+ });
+ node.setAttribute("cm-not-content", "true");
+ }, function(pos, axis) {
+ if (axis == "horizontal") setScrollLeft(cm, pos);
+ else setScrollTop(cm, pos);
+ }, cm);
+ if (cm.display.scrollbars.addClass)
+ addClass(cm.display.wrapper, cm.display.scrollbars.addClass);
+ }
+
+ function updateScrollbars(cm, measure) {
+ if (!measure) measure = measureForScrollbars(cm);
+ var startWidth = cm.display.barWidth, startHeight = cm.display.barHeight;
+ updateScrollbarsInner(cm, measure);
+ for (var i = 0; i < 4 && startWidth != cm.display.barWidth || startHeight != cm.display.barHeight; i++) {
+ if (startWidth != cm.display.barWidth && cm.options.lineWrapping)
+ updateHeightsInViewport(cm);
+ updateScrollbarsInner(cm, measureForScrollbars(cm));
+ startWidth = cm.display.barWidth; startHeight = cm.display.barHeight;
+ }
+ }
+
+ // Re-synchronize the fake scrollbars with the actual size of the
+ // content.
+ function updateScrollbarsInner(cm, measure) {
+ var d = cm.display;
+ var sizes = d.scrollbars.update(measure);
+
+ d.sizer.style.paddingRight = (d.barWidth = sizes.right) + "px";
+ d.sizer.style.paddingBottom = (d.barHeight = sizes.bottom) + "px";
+ d.heightForcer.style.borderBottom = sizes.bottom + "px solid transparent"
+
+ if (sizes.right && sizes.bottom) {
+ d.scrollbarFiller.style.display = "block";
+ d.scrollbarFiller.style.height = sizes.bottom + "px";
+ d.scrollbarFiller.style.width = sizes.right + "px";
+ } else d.scrollbarFiller.style.display = "";
+ if (sizes.bottom && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) {
+ d.gutterFiller.style.display = "block";
+ d.gutterFiller.style.height = sizes.bottom + "px";
+ d.gutterFiller.style.width = measure.gutterWidth + "px";
+ } else d.gutterFiller.style.display = "";
+ }
+
+ // 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 = viewport && viewport.top != null ? Math.max(0, viewport.top) : display.scroller.scrollTop;
+ top = Math.floor(top - paddingTop(display));
+ 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) {
+ from = ensureFrom;
+ to = lineAtHeight(doc, heightAtLine(getLine(doc, ensureFrom)) + display.wrapper.clientHeight);
+ } else if (Math.min(ensureTo, doc.lastLine()) >= to) {
+ from = lineAtHeight(doc, heightAtLine(getLine(doc, ensureTo)) - display.wrapper.clientHeight);
+ to = ensureTo;
+ }
+ }
+ return {from: from, to: Math.max(to, from + 1)};
+ }
+
+ // LINE NUMBERS
+
+ // Re-align line numbers and gutter marks to compensate for
+ // horizontal scrolling.
+ function alignHorizontally(cm) {
+ 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, 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;
+ if (last.length != display.lineNumChars) {
+ var test = display.measure.appendChild(elt("div", [elt("div", last)],
+ "CodeMirror-linenumber CodeMirror-gutter-elt"));
+ var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW;
+ display.lineGutter.style.width = "";
+ display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding) + 1;
+ display.lineNumWidth = display.lineNumInnerWidth + padding;
+ display.lineNumChars = display.lineNumInnerWidth ? last.length : -1;
+ display.lineGutter.style.width = display.lineNumWidth + "px";
+ updateGutterSpace(cm);
+ return true;
+ }
+ return false;
+ }
+
+ 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 display.scroller.getBoundingClientRect().left - display.sizer.getBoundingClientRect().left;
+ }
+
+ // DISPLAY DRAWING
+
+ function DisplayUpdate(cm, viewport, force) {
+ var display = cm.display;
+
+ this.viewport = viewport;
+ // Store some values that we'll need later (but don't want to force a relayout for)
+ this.visible = visibleLines(display, cm.doc, viewport);
+ this.editorIsHidden = !display.wrapper.offsetWidth;
+ this.wrapperHeight = display.wrapper.clientHeight;
+ this.wrapperWidth = display.wrapper.clientWidth;
+ this.oldDisplayWidth = displayWidth(cm);
+ this.force = force;
+ this.dims = getDimensions(cm);
+ this.events = [];
+ }
+
+ DisplayUpdate.prototype.signal = function(emitter, type) {
+ if (hasHandler(emitter, type))
+ this.events.push(arguments);
+ };
+ DisplayUpdate.prototype.finish = function() {
+ for (var i = 0; i < this.events.length; i++)
+ signal.apply(null, this.events[i]);
+ };
+
+ function maybeClipScrollbars(cm) {
+ var display = cm.display;
+ if (!display.scrollbarsClipped && display.scroller.offsetWidth) {
+ display.nativeBarWidth = display.scroller.offsetWidth - display.scroller.clientWidth;
+ display.heightForcer.style.height = scrollGap(cm) + "px";
+ display.sizer.style.marginBottom = -display.nativeBarWidth + "px";
+ display.sizer.style.borderRightWidth = scrollGap(cm) + "px";
+ display.scrollbarsClipped = true;
+ }
+ }
+
+ // Does the actual updating of the line display. Bails out
+ // (returning false) when there is nothing to be done and forced is
+ // false.
+ function updateDisplayIfNeeded(cm, update) {
+ var display = cm.display, doc = cm.doc;
+
+ if (update.editorIsHidden) {
+ resetView(cm);
+ return false;
+ }
+
+ // Bail out if the visible area is already rendered and nothing changed.
+ if (!update.force &&
+ update.visible.from >= display.viewFrom && update.visible.to <= display.viewTo &&
+ (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo) &&
+ display.renderedView == display.view && countDirtyView(cm) == 0)
+ return false;
+
+ if (maybeUpdateLineNumberWidth(cm)) {
+ resetView(cm);
+ update.dims = getDimensions(cm);
+ }
+
+ // Compute a suitable new viewport (from & to)
+ var end = doc.first + doc.size;
+ var from = Math.max(update.visible.from - cm.options.viewportMargin, doc.first);
+ var to = Math.min(end, update.visible.to + cm.options.viewportMargin);
+ 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 = visualLineNo(cm.doc, from);
+ to = visualLineEndNo(cm.doc, to);
+ }
+
+ var different = from != display.viewFrom || to != display.viewTo ||
+ display.lastWrapHeight != update.wrapperHeight || display.lastWrapWidth != update.wrapperWidth;
+ 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 && !update.force && display.renderedView == display.view &&
+ (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo))
+ return false;
+
+ // 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, update.dims);
+ if (toUpdate > 4) display.lineDiv.style.display = "";
+ display.renderedView = display.view;
+ // 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 and height.
+ removeChildren(display.cursorDiv);
+ removeChildren(display.selectionDiv);
+ display.gutters.style.height = display.sizer.style.minHeight = 0;
+
+ if (different) {
+ display.lastWrapHeight = update.wrapperHeight;
+ display.lastWrapWidth = update.wrapperWidth;
+ startWorker(cm, 400);
+ }
+
+ display.updateLineNumbers = null;
+
+ return true;
+ }
+
+ function postUpdateDisplay(cm, update) {
+ var viewport = update.viewport;
+
+ for (var first = true;; first = false) {
+ if (!first || !cm.options.lineWrapping || update.oldDisplayWidth == displayWidth(cm)) {
+ // Clip forced viewport to actual scrollable area.
+ if (viewport && viewport.top != null)
+ viewport = {top: Math.min(cm.doc.height + paddingVert(cm.display) - displayHeight(cm), viewport.top)};
+ // Updated line heights might result in the drawn area not
+ // actually covering the viewport. Keep looping until it does.
+ update.visible = visibleLines(cm.display, cm.doc, viewport);
+ if (update.visible.from >= cm.display.viewFrom && update.visible.to <= cm.display.viewTo)
+ break;
+ }
+ if (!updateDisplayIfNeeded(cm, update)) break;
+ updateHeightsInViewport(cm);
+ var barMeasure = measureForScrollbars(cm);
+ updateSelection(cm);
+ updateScrollbars(cm, barMeasure);
+ setDocumentHeight(cm, barMeasure);
+ }
+
+ update.signal(cm, "update", cm);
+ if (cm.display.viewFrom != cm.display.reportedViewFrom || cm.display.viewTo != cm.display.reportedViewTo) {
+ update.signal(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo);
+ cm.display.reportedViewFrom = cm.display.viewFrom; cm.display.reportedViewTo = cm.display.viewTo;
+ }
+ }
+
+ function updateDisplaySimple(cm, viewport) {
+ var update = new DisplayUpdate(cm, viewport);
+ if (updateDisplayIfNeeded(cm, update)) {
+ updateHeightsInViewport(cm);
+ postUpdateDisplay(cm, update);
+ var barMeasure = measureForScrollbars(cm);
+ updateSelection(cm);
+ updateScrollbars(cm, barMeasure);
+ setDocumentHeight(cm, barMeasure);
+ update.finish();
+ }
+ }
+
+ function setDocumentHeight(cm, measure) {
+ cm.display.sizer.style.minHeight = measure.docHeight + "px";
+ cm.display.heightForcer.style.top = measure.docHeight + "px";
+ cm.display.gutters.style.height = (measure.docHeight + cm.display.barHeight + scrollGap(cm)) + "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 i = 0; i < display.view.length; i++) {
+ var cur = display.view[i], height;
+ if (cur.hidden) continue;
+ if (ie && ie_version < 8) {
+ var bot = cur.node.offsetTop + cur.node.offsetHeight;
+ height = bot - prevBottom;
+ prevBottom = bot;
+ } else {
+ var box = cur.node.getBoundingClientRect();
+ height = box.bottom - box.top;
+ }
+ var diff = cur.line.height - height;
+ if (height < 2) height = textHeight(display);
+ if (diff > .001 || diff < -.001) {
+ updateLineHeight(cur.line, height);
+ updateWidgetHeight(cur.line);
+ if (cur.rest) for (var j = 0; j < cur.rest.length; j++)
+ updateWidgetHeight(cur.rest[j]);
+ }
+ }
+ }
+
+ // 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.parentNode.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 = {};
+ var gutterLeft = d.gutters.clientLeft;
+ for (var n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) {
+ left[cm.options.gutters[i]] = n.offsetLeft + n.clientLeft + gutterLeft;
+ width[cm.options.gutters[i]] = n.clientWidth;
+ }
+ return {fixedPos: compensateForHScroll(d),
+ gutterTotalWidth: d.gutters.offsetWidth,
+ gutterLeft: left,
+ gutterWidth: width,
+ wrapperWidth: d.wrapper.clientWidth};
+ }
+
+ // 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;
+ var container = display.lineDiv, cur = container.firstChild;
+
+ function rm(node) {
+ var next = node.nextSibling;
+ // Works around a throw-scroll bug in OS X Webkit
+ if (webkit && mac && cm.display.currentWheelTarget == node)
+ node.style.display = "none";
+ else
+ node.parentNode.removeChild(node);
+ return next;
+ }
+
+ 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 || lineView.node.parentNode != container) { // 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);
+ }
+ if (updateNumber) {
+ removeChildren(lineView.lineNumber);
+ lineView.lineNumber.appendChild(document.createTextNode(lineNumberFor(cm.options, lineN)));
+ }
+ cur = lineView.node.nextSibling;
+ }
+ lineN += lineView.size;
+ }
+ while (cur) cur = rm(cur);
+ }
+
+ // 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(cm, lineView, dims);
+ }
+ 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 && ie_version < 8) lineView.node.style.zIndex = 2;
+ }
+ 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;
+ }
+ if (lineView.gutterBackground) {
+ lineView.node.removeChild(lineView.gutterBackground);
+ lineView.gutterBackground = null;
+ }
+ if (lineView.line.gutterClass) {
+ var wrap = ensureLineWrapped(lineView);
+ lineView.gutterBackground = elt("div", null, "CodeMirror-gutter-background " + lineView.line.gutterClass,
+ "left: " + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) +
+ "px; width: " + dims.gutterTotalWidth + "px");
+ wrap.insertBefore(lineView.gutterBackground, lineView.text);
+ }
+ var markers = lineView.line.gutterMarkers;
+ if (cm.options.lineNumbers || markers) {
+ var wrap = ensureLineWrapped(lineView);
+ var gutterWrap = lineView.gutter = elt("div", null, "CodeMirror-gutter-wrapper", "left: " +
+ (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px");
+ cm.display.input.setUneditable(gutterWrap);
+ wrap.insertBefore(gutterWrap, lineView.text);
+ if (lineView.line.gutterClass)
+ gutterWrap.className += " " + lineView.line.gutterClass;
+ if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"]))
+ lineView.lineNumber = gutterWrap.appendChild(
+ elt("div", lineNumberFor(cm.options, lineN),
+ "CodeMirror-linenumber CodeMirror-gutter-elt",
+ "left: " + dims.gutterLeft["CodeMirror-linenumbers"] + "px; width: "
+ + 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"));
+ }
+ }
+ }
+
+ function updateLineWidgets(cm, 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(cm, 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(cm, 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(cm, lineView, dims) {
+ insertLineWidgetsFor(cm, lineView.line, lineView, dims, true);
+ if (lineView.rest) for (var i = 0; i < lineView.rest.length; i++)
+ insertLineWidgetsFor(cm, lineView.rest[i], lineView, dims, false);
+ }
+
+ function insertLineWidgetsFor(cm, 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.setAttribute("cm-ignore-events", "true");
+ positionLineWidget(widget, node, lineView, dims);
+ cm.display.input.setUneditable(node);
+ if (allowAbove && widget.above)
+ wrap.insertBefore(node, lineView.gutter || lineView.text);
+ else
+ wrap.appendChild(node);
+ signalLater(widget, "redraw");
+ }
+ }
+
+ function positionLineWidget(widget, node, lineView, dims) {
+ if (widget.noHScroll) {
+ (lineView.alignable || (lineView.alignable = [])).push(node);
+ var width = dims.wrapperWidth;
+ node.style.left = dims.fixedPos + "px";
+ if (!widget.coverGutter) {
+ width -= dims.gutterTotalWidth;
+ node.style.paddingLeft = dims.gutterTotalWidth + "px";
+ }
+ node.style.width = width + "px";
+ }
+ if (widget.coverGutter) {
+ node.style.zIndex = 5;
+ node.style.position = "relative";
+ if (!widget.noHScroll) node.style.marginLeft = -dims.gutterTotalWidth + "px";
+ }
+ }
+
+ // 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; }
+
+ // INPUT HANDLING
+
+ function ensureFocus(cm) {
+ if (!cm.state.focused) { cm.display.input.focus(); onFocus(cm); }
+ }
+
+ // This will be set to a {lineWise: bool, text: [string]} object, so
+ // that, when pasting, we know what kind of selections the copied
+ // text was made out of.
+ var lastCopied = null;
+
+ function applyTextInput(cm, inserted, deleted, sel, origin) {
+ var doc = cm.doc;
+ cm.display.shift = false;
+ if (!sel) sel = doc.sel;
+
+ var paste = cm.state.pasteIncoming || origin == "paste";
+ var textLines = doc.splitLines(inserted), multiPaste = null
+ // When pasing N lines into N selections, insert one line per selection
+ if (paste && sel.ranges.length > 1) {
+ if (lastCopied && lastCopied.text.join("\n") == inserted) {
+ if (sel.ranges.length % lastCopied.text.length == 0) {
+ multiPaste = [];
+ for (var i = 0; i < lastCopied.text.length; i++)
+ multiPaste.push(doc.splitLines(lastCopied.text[i]));
+ }
+ } else if (textLines.length == sel.ranges.length) {
+ multiPaste = map(textLines, function(l) { return [l]; });
+ }
+ }
+
+ // Normal behavior is to insert the new text into every selection
+ for (var i = sel.ranges.length - 1; i >= 0; i--) {
+ var range = sel.ranges[i];
+ var from = range.from(), to = range.to();
+ if (range.empty()) {
+ if (deleted && deleted > 0) // Handle deletion
+ from = Pos(from.line, from.ch - deleted);
+ else if (cm.state.overwrite && !paste) // Handle overwrite
+ to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + lst(textLines).length));
+ else if (lastCopied && lastCopied.lineWise && lastCopied.text.join("\n") == inserted)
+ from = to = Pos(from.line, 0)
+ }
+ var updateInput = cm.curOp.updateInput;
+ var changeEvent = {from: from, to: to, text: multiPaste ? multiPaste[i % multiPaste.length] : textLines,
+ origin: origin || (paste ? "paste" : cm.state.cutIncoming ? "cut" : "+input")};
+ makeChange(cm.doc, changeEvent);
+ signalLater(cm, "inputRead", cm, changeEvent);
+ }
+ if (inserted && !paste)
+ triggerElectric(cm, inserted);
+
+ ensureCursorVisible(cm);
+ cm.curOp.updateInput = updateInput;
+ cm.curOp.typing = true;
+ cm.state.pasteIncoming = cm.state.cutIncoming = false;
+ }
+
+ function handlePaste(e, cm) {
+ var pasted = e.clipboardData && e.clipboardData.getData("text/plain");
+ if (pasted) {
+ e.preventDefault();
+ if (!cm.isReadOnly() && !cm.options.disableInput)
+ runInOp(cm, function() { applyTextInput(cm, pasted, 0, null, "paste"); });
+ return true;
+ }
+ }
+
+ function triggerElectric(cm, inserted) {
+ // When an 'electric' character is inserted, immediately trigger a reindent
+ if (!cm.options.electricChars || !cm.options.smartIndent) return;
+ var sel = cm.doc.sel;
+
+ for (var i = sel.ranges.length - 1; i >= 0; i--) {
+ var range = sel.ranges[i];
+ if (range.head.ch > 100 || (i && sel.ranges[i - 1].head.line == range.head.line)) continue;
+ var mode = cm.getModeAt(range.head);
+ var indented = false;
+ if (mode.electricChars) {
+ for (var j = 0; j < mode.electricChars.length; j++)
+ if (inserted.indexOf(mode.electricChars.charAt(j)) > -1) {
+ indented = indentLine(cm, range.head.line, "smart");
+ break;
+ }
+ } else if (mode.electricInput) {
+ if (mode.electricInput.test(getLine(cm.doc, range.head.line).text.slice(0, range.head.ch)))
+ indented = indentLine(cm, range.head.line, "smart");
+ }
+ if (indented) signalLater(cm, "electricInput", cm, range.head.line);
+ }
+ }
+
+ function copyableRanges(cm) {
+ var text = [], ranges = [];
+ for (var i = 0; i < cm.doc.sel.ranges.length; i++) {
+ var line = cm.doc.sel.ranges[i].head.line;
+ var lineRange = {anchor: Pos(line, 0), head: Pos(line + 1, 0)};
+ ranges.push(lineRange);
+ text.push(cm.getRange(lineRange.anchor, lineRange.head));
+ }
+ return {text: text, ranges: ranges};
+ }
+
+ function disableBrowserMagic(field) {
+ field.setAttribute("autocorrect", "off");
+ field.setAttribute("autocapitalize", "off");
+ field.setAttribute("spellcheck", "false");
+ }
+
+ // TEXTAREA INPUT STYLE
+
+ function TextareaInput(cm) {
+ this.cm = cm;
+ // See input.poll and input.reset
+ this.prevInput = "";
+
+ // Flag that indicates whether we expect input to appear real soon
+ // now (after some event like 'keypress' or 'input') and are
+ // polling intensively.
+ this.pollingFast = false;
+ // Self-resetting timeout for the poller
+ this.polling = new Delayed();
+ // Tracks when input.reset has punted to just putting a short
+ // string into the textarea instead of the full selection.
+ this.inaccurateSelection = false;
+ // Used to work around IE issue with selection being forgotten when focus moves away from textarea
+ this.hasSelection = false;
+ this.composing = null;
+ };
+
+ function hiddenTextarea() {
+ var te = elt("textarea", null, null, "position: absolute; padding: 0; width: 1px; height: 1em; outline: none");
+ var div = elt("div", [te], null, "overflow: hidden; position: relative; width: 3px; height: 0px;");
+ // 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) te.style.width = "1000px";
+ else te.setAttribute("wrap", "off");
+ // If border: 0; -- iOS fails to open keyboard (issue #1287)
+ if (ios) te.style.border = "1px solid black";
+ disableBrowserMagic(te);
+ return div;
+ }
+
+ TextareaInput.prototype = copyObj({
+ init: function(display) {
+ var input = this, cm = this.cm;
+
+ // Wraps and hides input textarea
+ var div = this.wrapper = hiddenTextarea();
+ // The semihidden textarea that is focused when the editor is
+ // focused, and receives input.
+ var te = this.textarea = div.firstChild;
+ display.wrapper.insertBefore(div, display.wrapper.firstChild);
+
+ // Needed to hide big blue blinking cursor on Mobile Safari (doesn't seem to work in iOS 8 anymore)
+ if (ios) te.style.width = "0px";
+
+ on(te, "input", function() {
+ if (ie && ie_version >= 9 && input.hasSelection) input.hasSelection = null;
+ input.poll();
+ });
+
+ on(te, "paste", function(e) {
+ if (signalDOMEvent(cm, e) || handlePaste(e, cm)) return
+
+ cm.state.pasteIncoming = true;
+ input.fastPoll();
+ });
+
+ function prepareCopyCut(e) {
+ if (signalDOMEvent(cm, e)) return
+ if (cm.somethingSelected()) {
+ lastCopied = {lineWise: false, text: cm.getSelections()};
+ if (input.inaccurateSelection) {
+ input.prevInput = "";
+ input.inaccurateSelection = false;
+ te.value = lastCopied.text.join("\n");
+ selectInput(te);
+ }
+ } else if (!cm.options.lineWiseCopyCut) {
+ return;
+ } else {
+ var ranges = copyableRanges(cm);
+ lastCopied = {lineWise: true, text: ranges.text};
+ if (e.type == "cut") {
+ cm.setSelections(ranges.ranges, null, sel_dontScroll);
+ } else {
+ input.prevInput = "";
+ te.value = ranges.text.join("\n");
+ selectInput(te);
+ }
+ }
+ if (e.type == "cut") cm.state.cutIncoming = true;
+ }
+ on(te, "cut", prepareCopyCut);
+ on(te, "copy", prepareCopyCut);
+
+ on(display.scroller, "paste", function(e) {
+ if (eventInWidget(display, e) || signalDOMEvent(cm, e)) return;
+ cm.state.pasteIncoming = true;
+ input.focus();
+ });
+
+ // Prevent normal selection in the editor (we handle our own)
+ on(display.lineSpace, "selectstart", function(e) {
+ if (!eventInWidget(display, e)) e_preventDefault(e);
+ });
+
+ on(te, "compositionstart", function() {
+ var start = cm.getCursor("from");
+ if (input.composing) input.composing.range.clear()
+ input.composing = {
+ start: start,
+ range: cm.markText(start, cm.getCursor("to"), {className: "CodeMirror-composing"})
+ };
+ });
+ on(te, "compositionend", function() {
+ if (input.composing) {
+ input.poll();
+ input.composing.range.clear();
+ input.composing = null;
+ }
+ });
+ },
+
+ prepareSelection: function() {
+ // Redraw the selection and/or cursor
+ var cm = this.cm, display = cm.display, doc = cm.doc;
+ var result = prepareSelection(cm);
+
+ // 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();
+ result.teTop = Math.max(0, Math.min(display.wrapper.clientHeight - 10,
+ headPos.top + lineOff.top - wrapOff.top));
+ result.teLeft = Math.max(0, Math.min(display.wrapper.clientWidth - 10,
+ headPos.left + lineOff.left - wrapOff.left));
+ }
+
+ return result;
+ },
+
+ showSelection: function(drawn) {
+ var cm = this.cm, display = cm.display;
+ removeChildrenAndAdd(display.cursorDiv, drawn.cursors);
+ removeChildrenAndAdd(display.selectionDiv, drawn.selection);
+ if (drawn.teTop != null) {
+ this.wrapper.style.top = drawn.teTop + "px";
+ this.wrapper.style.left = drawn.teLeft + "px";
+ }
+ },
+
+ // Reset the input to correspond to the selection (or to be empty,
+ // when not typing and nothing is selected)
+ reset: function(typing) {
+ if (this.contextMenuPending) return;
+ var minimal, selected, cm = this.cm, doc = cm.doc;
+ if (cm.somethingSelected()) {
+ this.prevInput = "";
+ var range = doc.sel.primary();
+ minimal = hasCopyEvent &&
+ (range.to().line - range.from().line > 100 || (selected = cm.getSelection()).length > 1000);
+ var content = minimal ? "-" : selected || cm.getSelection();
+ this.textarea.value = content;
+ if (cm.state.focused) selectInput(this.textarea);
+ if (ie && ie_version >= 9) this.hasSelection = content;
+ } else if (!typing) {
+ this.prevInput = this.textarea.value = "";
+ if (ie && ie_version >= 9) this.hasSelection = null;
+ }
+ this.inaccurateSelection = minimal;
+ },
+
+ getField: function() { return this.textarea; },
+
+ supportsTouch: function() { return false; },
+
+ focus: function() {
+ if (this.cm.options.readOnly != "nocursor" && (!mobile || activeElt() != this.textarea)) {
+ try { this.textarea.focus(); }
+ catch (e) {} // IE8 will throw if the textarea is display: none or not in DOM
+ }
+ },
+
+ blur: function() { this.textarea.blur(); },
+
+ resetPosition: function() {
+ this.wrapper.style.top = this.wrapper.style.left = 0;
+ },
+
+ receivedFocus: function() { this.slowPoll(); },
+
+ // Poll for input changes, using the normal rate of polling. This
+ // runs as long as the editor is focused.
+ slowPoll: function() {
+ var input = this;
+ if (input.pollingFast) return;
+ input.polling.set(this.cm.options.pollInterval, function() {
+ input.poll();
+ if (input.cm.state.focused) input.slowPoll();
+ });
+ },
+
+ // 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.
+ fastPoll: function() {
+ var missed = false, input = this;
+ input.pollingFast = true;
+ function p() {
+ var changed = input.poll();
+ if (!changed && !missed) {missed = true; input.polling.set(60, p);}
+ else {input.pollingFast = false; input.slowPoll();}
+ }
+ input.polling.set(20, p);
+ },
+
+ // 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).
+ poll: function() {
+ var cm = this.cm, input = this.textarea, prevInput = this.prevInput;
+ // 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 (this.contextMenuPending || !cm.state.focused ||
+ (hasSelection(input) && !prevInput && !this.composing) ||
+ cm.isReadOnly() || cm.options.disableInput || cm.state.keySeq)
+ return false;
+
+ var text = input.value;
+ // If nothing changed, bail.
+ if (text == prevInput && !cm.somethingSelected()) return false;
+ // Work around nonsensical selection resetting in IE9/10, and
+ // inexplicable appearance of private area unicode characters on
+ // some key combos in Mac (#2689).
+ if (ie && ie_version >= 9 && this.hasSelection === text ||
+ mac && /[\uf700-\uf7ff]/.test(text)) {
+ cm.display.input.reset();
+ return false;
+ }
+
+ if (cm.doc.sel == cm.display.selForContextMenu) {
+ var first = text.charCodeAt(0);
+ if (first == 0x200b && !prevInput) prevInput = "\u200b";
+ if (first == 0x21da) { this.reset(); return this.cm.execCommand("undo"); }
+ }
+ // 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 self = this;
+ runInOp(cm, function() {
+ applyTextInput(cm, text.slice(same), prevInput.length - same,
+ null, self.composing ? "*compose" : null);
+
+ // Don't leave long text in the textarea, since it makes further polling slow
+ if (text.length > 1000 || text.indexOf("\n") > -1) input.value = self.prevInput = "";
+ else self.prevInput = text;
+
+ if (self.composing) {
+ self.composing.range.clear();
+ self.composing.range = cm.markText(self.composing.start, cm.getCursor("to"),
+ {className: "CodeMirror-composing"});
+ }
+ });
+ return true;
+ },
+
+ ensurePolled: function() {
+ if (this.pollingFast && this.poll()) this.pollingFast = false;
+ },
+
+ onKeyPress: function() {
+ if (ie && ie_version >= 9) this.hasSelection = null;
+ this.fastPoll();
+ },
+
+ onContextMenu: function(e) {
+ var input = this, cm = input.cm, display = cm.display, te = input.textarea;
+ var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop;
+ 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 && cm.doc.sel.contains(pos) == -1)
+ operation(cm, setSelection)(cm.doc, simpleSelection(pos), sel_dontScroll);
+
+ var oldCSS = te.style.cssText, oldWrapperCSS = input.wrapper.style.cssText;
+ input.wrapper.style.cssText = "position: absolute"
+ var wrapperBox = input.wrapper.getBoundingClientRect()
+ te.style.cssText = "position: absolute; width: 30px; height: 30px; top: " + (e.clientY - wrapperBox.top - 5) +
+ "px; left: " + (e.clientX - wrapperBox.left - 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);";
+ if (webkit) var oldScrollY = window.scrollY; // Work around Chrome issue (#2712)
+ display.input.focus();
+ if (webkit) window.scrollTo(null, oldScrollY);
+ display.input.reset();
+ // Adds "Select all" to context menu in FF
+ if (!cm.somethingSelected()) te.value = input.prevInput = " ";
+ input.contextMenuPending = true;
+ display.selForContextMenu = cm.doc.sel;
+ clearTimeout(display.detectingSelectAll);
+
+ // 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 (te.selectionStart != null) {
+ var selected = cm.somethingSelected();
+ var extval = "\u200b" + (selected ? te.value : "");
+ te.value = "\u21da"; // Used to catch context-menu undo
+ te.value = extval;
+ input.prevInput = selected ? "" : "\u200b";
+ te.selectionStart = 1; te.selectionEnd = extval.length;
+ // Re-set this, in case some other handler touched the
+ // selection in the meantime.
+ display.selForContextMenu = cm.doc.sel;
+ }
+ }
+ function rehide() {
+ input.contextMenuPending = false;
+ input.wrapper.style.cssText = oldWrapperCSS
+ te.style.cssText = oldCSS;
+ if (ie && ie_version < 9) display.scrollbars.setScrollTop(display.scroller.scrollTop = scrollPos);
+
+ // Try to detect the user choosing select-all
+ if (te.selectionStart != null) {
+ if (!ie || (ie && ie_version < 9)) prepareSelectAllHack();
+ var i = 0, poll = function() {
+ if (display.selForContextMenu == cm.doc.sel && te.selectionStart == 0 &&
+ te.selectionEnd > 0 && input.prevInput == "\u200b")
+ operation(cm, commands.selectAll)(cm);
+ else if (i++ < 10) display.detectingSelectAll = setTimeout(poll, 500);
+ else display.input.reset();
+ };
+ display.detectingSelectAll = setTimeout(poll, 200);
+ }
+ }
+
+ if (ie && ie_version >= 9) prepareSelectAllHack();
+ if (captureRightClick) {
+ e_stop(e);
+ var mouseup = function() {
+ off(window, "mouseup", mouseup);
+ setTimeout(rehide, 20);
+ };
+ on(window, "mouseup", mouseup);
+ } else {
+ setTimeout(rehide, 50);
+ }
+ },
+
+ readOnlyChanged: function(val) {
+ if (!val) this.reset();
+ },
+
+ setUneditable: nothing,
+
+ needsContentAttribute: false
+ }, TextareaInput.prototype);
+
+ // CONTENTEDITABLE INPUT STYLE
+
+ function ContentEditableInput(cm) {
+ this.cm = cm;
+ this.lastAnchorNode = this.lastAnchorOffset = this.lastFocusNode = this.lastFocusOffset = null;
+ this.polling = new Delayed();
+ this.gracePeriod = false;
+ }
+
+ ContentEditableInput.prototype = copyObj({
+ init: function(display) {
+ var input = this, cm = input.cm;
+ var div = input.div = display.lineDiv;
+ disableBrowserMagic(div);
+
+ on(div, "paste", function(e) {
+ if (!signalDOMEvent(cm, e)) handlePaste(e, cm);
+ })
+
+ on(div, "compositionstart", function(e) {
+ var data = e.data;
+ input.composing = {sel: cm.doc.sel, data: data, startData: data};
+ if (!data) return;
+ var prim = cm.doc.sel.primary();
+ var line = cm.getLine(prim.head.line);
+ var found = line.indexOf(data, Math.max(0, prim.head.ch - data.length));
+ if (found > -1 && found <= prim.head.ch)
+ input.composing.sel = simpleSelection(Pos(prim.head.line, found),
+ Pos(prim.head.line, found + data.length));
+ });
+ on(div, "compositionupdate", function(e) {
+ input.composing.data = e.data;
+ });
+ on(div, "compositionend", function(e) {
+ var ours = input.composing;
+ if (!ours) return;
+ if (e.data != ours.startData && !/\u200b/.test(e.data))
+ ours.data = e.data;
+ // Need a small delay to prevent other code (input event,
+ // selection polling) from doing damage when fired right after
+ // compositionend.
+ setTimeout(function() {
+ if (!ours.handled)
+ input.applyComposition(ours);
+ if (input.composing == ours)
+ input.composing = null;
+ }, 50);
+ });
+
+ on(div, "touchstart", function() {
+ input.forceCompositionEnd();
+ });
+
+ on(div, "input", function() {
+ if (input.composing) return;
+ if (cm.isReadOnly() || !input.pollContent())
+ runInOp(input.cm, function() {regChange(cm);});
+ });
+
+ function onCopyCut(e) {
+ if (signalDOMEvent(cm, e)) return
+ if (cm.somethingSelected()) {
+ lastCopied = {lineWise: false, text: cm.getSelections()};
+ if (e.type == "cut") cm.replaceSelection("", null, "cut");
+ } else if (!cm.options.lineWiseCopyCut) {
+ return;
+ } else {
+ var ranges = copyableRanges(cm);
+ lastCopied = {lineWise: true, text: ranges.text};
+ if (e.type == "cut") {
+ cm.operation(function() {
+ cm.setSelections(ranges.ranges, 0, sel_dontScroll);
+ cm.replaceSelection("", null, "cut");
+ });
+ }
+ }
+ // iOS exposes the clipboard API, but seems to discard content inserted into it
+ if (e.clipboardData && !ios) {
+ e.preventDefault();
+ e.clipboardData.clearData();
+ e.clipboardData.setData("text/plain", lastCopied.text.join("\n"));
+ } else {
+ // Old-fashioned briefly-focus-a-textarea hack
+ var kludge = hiddenTextarea(), te = kludge.firstChild;
+ cm.display.lineSpace.insertBefore(kludge, cm.display.lineSpace.firstChild);
+ te.value = lastCopied.text.join("\n");
+ var hadFocus = document.activeElement;
+ selectInput(te);
+ setTimeout(function() {
+ cm.display.lineSpace.removeChild(kludge);
+ hadFocus.focus();
+ }, 50);
+ }
+ }
+ on(div, "copy", onCopyCut);
+ on(div, "cut", onCopyCut);
+ },
+
+ prepareSelection: function() {
+ var result = prepareSelection(this.cm, false);
+ result.focus = this.cm.state.focused;
+ return result;
+ },
+
+ showSelection: function(info, takeFocus) {
+ if (!info || !this.cm.display.view.length) return;
+ if (info.focus || takeFocus) this.showPrimarySelection();
+ this.showMultipleSelections(info);
+ },
+
+ showPrimarySelection: function() {
+ var sel = window.getSelection(), prim = this.cm.doc.sel.primary();
+ var curAnchor = domToPos(this.cm, sel.anchorNode, sel.anchorOffset);
+ var curFocus = domToPos(this.cm, sel.focusNode, sel.focusOffset);
+ if (curAnchor && !curAnchor.bad && curFocus && !curFocus.bad &&
+ cmp(minPos(curAnchor, curFocus), prim.from()) == 0 &&
+ cmp(maxPos(curAnchor, curFocus), prim.to()) == 0)
+ return;
+
+ var start = posToDOM(this.cm, prim.from());
+ var end = posToDOM(this.cm, prim.to());
+ if (!start && !end) return;
+
+ var view = this.cm.display.view;
+ var old = sel.rangeCount && sel.getRangeAt(0);
+ if (!start) {
+ start = {node: view[0].measure.map[2], offset: 0};
+ } else if (!end) { // FIXME dangerously hacky
+ var measure = view[view.length - 1].measure;
+ var map = measure.maps ? measure.maps[measure.maps.length - 1] : measure.map;
+ end = {node: map[map.length - 1], offset: map[map.length - 2] - map[map.length - 3]};
+ }
+
+ try { var rng = range(start.node, start.offset, end.offset, end.node); }
+ catch(e) {} // Our model of the DOM might be outdated, in which case the range we try to set can be impossible
+ if (rng) {
+ if (!gecko && this.cm.state.focused) {
+ sel.collapse(start.node, start.offset);
+ if (!rng.collapsed) sel.addRange(rng);
+ } else {
+ sel.removeAllRanges();
+ sel.addRange(rng);
+ }
+ if (old && sel.anchorNode == null) sel.addRange(old);
+ else if (gecko) this.startGracePeriod();
+ }
+ this.rememberSelection();
+ },
+
+ startGracePeriod: function() {
+ var input = this;
+ clearTimeout(this.gracePeriod);
+ this.gracePeriod = setTimeout(function() {
+ input.gracePeriod = false;
+ if (input.selectionChanged())
+ input.cm.operation(function() { input.cm.curOp.selectionChanged = true; });
+ }, 20);
+ },
+
+ showMultipleSelections: function(info) {
+ removeChildrenAndAdd(this.cm.display.cursorDiv, info.cursors);
+ removeChildrenAndAdd(this.cm.display.selectionDiv, info.selection);
+ },
+
+ rememberSelection: function() {
+ var sel = window.getSelection();
+ this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset;
+ this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset;
+ },
+
+ selectionInEditor: function() {
+ var sel = window.getSelection();
+ if (!sel.rangeCount) return false;
+ var node = sel.getRangeAt(0).commonAncestorContainer;
+ return contains(this.div, node);
+ },
+
+ focus: function() {
+ if (this.cm.options.readOnly != "nocursor") this.div.focus();
+ },
+ blur: function() { this.div.blur(); },
+ getField: function() { return this.div; },
+
+ supportsTouch: function() { return true; },
+
+ receivedFocus: function() {
+ var input = this;
+ if (this.selectionInEditor())
+ this.pollSelection();
+ else
+ runInOp(this.cm, function() { input.cm.curOp.selectionChanged = true; });
+
+ function poll() {
+ if (input.cm.state.focused) {
+ input.pollSelection();
+ input.polling.set(input.cm.options.pollInterval, poll);
+ }
+ }
+ this.polling.set(this.cm.options.pollInterval, poll);
+ },
+
+ selectionChanged: function() {
+ var sel = window.getSelection();
+ return sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset ||
+ sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset;
+ },
+
+ pollSelection: function() {
+ if (!this.composing && !this.gracePeriod && this.selectionChanged()) {
+ var sel = window.getSelection(), cm = this.cm;
+ this.rememberSelection();
+ var anchor = domToPos(cm, sel.anchorNode, sel.anchorOffset);
+ var head = domToPos(cm, sel.focusNode, sel.focusOffset);
+ if (anchor && head) runInOp(cm, function() {
+ setSelection(cm.doc, simpleSelection(anchor, head), sel_dontScroll);
+ if (anchor.bad || head.bad) cm.curOp.selectionChanged = true;
+ });
+ }
+ },
+
+ pollContent: function() {
+ var cm = this.cm, display = cm.display, sel = cm.doc.sel.primary();
+ var from = sel.from(), to = sel.to();
+ if (from.line < display.viewFrom || to.line > display.viewTo - 1) return false;
+
+ var fromIndex;
+ if (from.line == display.viewFrom || (fromIndex = findViewIndex(cm, from.line)) == 0) {
+ var fromLine = lineNo(display.view[0].line);
+ var fromNode = display.view[0].node;
+ } else {
+ var fromLine = lineNo(display.view[fromIndex].line);
+ var fromNode = display.view[fromIndex - 1].node.nextSibling;
+ }
+ var toIndex = findViewIndex(cm, to.line);
+ if (toIndex == display.view.length - 1) {
+ var toLine = display.viewTo - 1;
+ var toNode = display.lineDiv.lastChild;
+ } else {
+ var toLine = lineNo(display.view[toIndex + 1].line) - 1;
+ var toNode = display.view[toIndex + 1].node.previousSibling;
+ }
+
+ var newText = cm.doc.splitLines(domTextBetween(cm, fromNode, toNode, fromLine, toLine));
+ var oldText = getBetween(cm.doc, Pos(fromLine, 0), Pos(toLine, getLine(cm.doc, toLine).text.length));
+ while (newText.length > 1 && oldText.length > 1) {
+ if (lst(newText) == lst(oldText)) { newText.pop(); oldText.pop(); toLine--; }
+ else if (newText[0] == oldText[0]) { newText.shift(); oldText.shift(); fromLine++; }
+ else break;
+ }
+
+ var cutFront = 0, cutEnd = 0;
+ var newTop = newText[0], oldTop = oldText[0], maxCutFront = Math.min(newTop.length, oldTop.length);
+ while (cutFront < maxCutFront && newTop.charCodeAt(cutFront) == oldTop.charCodeAt(cutFront))
+ ++cutFront;
+ var newBot = lst(newText), oldBot = lst(oldText);
+ var maxCutEnd = Math.min(newBot.length - (newText.length == 1 ? cutFront : 0),
+ oldBot.length - (oldText.length == 1 ? cutFront : 0));
+ while (cutEnd < maxCutEnd &&
+ newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1))
+ ++cutEnd;
+
+ newText[newText.length - 1] = newBot.slice(0, newBot.length - cutEnd);
+ newText[0] = newText[0].slice(cutFront);
+
+ var chFrom = Pos(fromLine, cutFront);
+ var chTo = Pos(toLine, oldText.length ? lst(oldText).length - cutEnd : 0);
+ if (newText.length > 1 || newText[0] || cmp(chFrom, chTo)) {
+ replaceRange(cm.doc, newText, chFrom, chTo, "+input");
+ return true;
+ }
+ },
+
+ ensurePolled: function() {
+ this.forceCompositionEnd();
+ },
+ reset: function() {
+ this.forceCompositionEnd();
+ },
+ forceCompositionEnd: function() {
+ if (!this.composing || this.composing.handled) return;
+ this.applyComposition(this.composing);
+ this.composing.handled = true;
+ this.div.blur();
+ this.div.focus();
+ },
+ applyComposition: function(composing) {
+ if (this.cm.isReadOnly())
+ operation(this.cm, regChange)(this.cm)
+ else if (composing.data && composing.data != composing.startData)
+ operation(this.cm, applyTextInput)(this.cm, composing.data, 0, composing.sel);
+ },
+
+ setUneditable: function(node) {
+ node.contentEditable = "false"
+ },
+
+ onKeyPress: function(e) {
+ e.preventDefault();
+ if (!this.cm.isReadOnly())
+ operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0);
+ },
+
+ readOnlyChanged: function(val) {
+ this.div.contentEditable = String(val != "nocursor")
+ },
+
+ onContextMenu: nothing,
+ resetPosition: nothing,
+
+ needsContentAttribute: true
+ }, ContentEditableInput.prototype);
+
+ function posToDOM(cm, pos) {
+ var view = findViewForLine(cm, pos.line);
+ if (!view || view.hidden) return null;
+ var line = getLine(cm.doc, pos.line);
+ var info = mapFromLineView(view, line, pos.line);
+
+ var order = getOrder(line), side = "left";
+ if (order) {
+ var partPos = getBidiPartAt(order, pos.ch);
+ side = partPos % 2 ? "right" : "left";
+ }
+ var result = nodeAndOffsetInLineMap(info.map, pos.ch, side);
+ result.offset = result.collapse == "right" ? result.end : result.start;
+ return result;
+ }
+
+ function badPos(pos, bad) { if (bad) pos.bad = true; return pos; }
+
+ function domToPos(cm, node, offset) {
+ var lineNode;
+ if (node == cm.display.lineDiv) {
+ lineNode = cm.display.lineDiv.childNodes[offset];
+ if (!lineNode) return badPos(cm.clipPos(Pos(cm.display.viewTo - 1)), true);
+ node = null; offset = 0;
+ } else {
+ for (lineNode = node;; lineNode = lineNode.parentNode) {
+ if (!lineNode || lineNode == cm.display.lineDiv) return null;
+ if (lineNode.parentNode && lineNode.parentNode == cm.display.lineDiv) break;
+ }
+ }
+ for (var i = 0; i < cm.display.view.length; i++) {
+ var lineView = cm.display.view[i];
+ if (lineView.node == lineNode)
+ return locateNodeInLineView(lineView, node, offset);
+ }
+ }
+
+ function locateNodeInLineView(lineView, node, offset) {
+ var wrapper = lineView.text.firstChild, bad = false;
+ if (!node || !contains(wrapper, node)) return badPos(Pos(lineNo(lineView.line), 0), true);
+ if (node == wrapper) {
+ bad = true;
+ node = wrapper.childNodes[offset];
+ offset = 0;
+ if (!node) {
+ var line = lineView.rest ? lst(lineView.rest) : lineView.line;
+ return badPos(Pos(lineNo(line), line.text.length), bad);
+ }
+ }
+
+ var textNode = node.nodeType == 3 ? node : null, topNode = node;
+ if (!textNode && node.childNodes.length == 1 && node.firstChild.nodeType == 3) {
+ textNode = node.firstChild;
+ if (offset) offset = textNode.nodeValue.length;
+ }
+ while (topNode.parentNode != wrapper) topNode = topNode.parentNode;
+ var measure = lineView.measure, maps = measure.maps;
+
+ function find(textNode, topNode, offset) {
+ for (var i = -1; i < (maps ? maps.length : 0); i++) {
+ var map = i < 0 ? measure.map : maps[i];
+ for (var j = 0; j < map.length; j += 3) {
+ var curNode = map[j + 2];
+ if (curNode == textNode || curNode == topNode) {
+ var line = lineNo(i < 0 ? lineView.line : lineView.rest[i]);
+ var ch = map[j] + offset;
+ if (offset < 0 || curNode != textNode) ch = map[j + (offset ? 1 : 0)];
+ return Pos(line, ch);
+ }
+ }
+ }
+ }
+ var found = find(textNode, topNode, offset);
+ if (found) return badPos(found, bad);
+
+ // FIXME this is all really shaky. might handle the few cases it needs to handle, but likely to cause problems
+ for (var after = topNode.nextSibling, dist = textNode ? textNode.nodeValue.length - offset : 0; after; after = after.nextSibling) {
+ found = find(after, after.firstChild, 0);
+ if (found)
+ return badPos(Pos(found.line, found.ch - dist), bad);
+ else
+ dist += after.textContent.length;
+ }
+ for (var before = topNode.previousSibling, dist = offset; before; before = before.previousSibling) {
+ found = find(before, before.firstChild, -1);
+ if (found)
+ return badPos(Pos(found.line, found.ch + dist), bad);
+ else
+ dist += after.textContent.length;
+ }
+ }
+
+ function domTextBetween(cm, from, to, fromLine, toLine) {
+ var text = "", closing = false, lineSep = cm.doc.lineSeparator();
+ function recognizeMarker(id) { return function(marker) { return marker.id == id; }; }
+ function walk(node) {
+ if (node.nodeType == 1) {
+ var cmText = node.getAttribute("cm-text");
+ if (cmText != null) {
+ if (cmText == "") cmText = node.textContent.replace(/\u200b/g, "");
+ text += cmText;
+ return;
+ }
+ var markerID = node.getAttribute("cm-marker"), range;
+ if (markerID) {
+ var found = cm.findMarks(Pos(fromLine, 0), Pos(toLine + 1, 0), recognizeMarker(+markerID));
+ if (found.length && (range = found[0].find()))
+ text += getBetween(cm.doc, range.from, range.to).join(lineSep);
+ return;
+ }
+ if (node.getAttribute("contenteditable") == "false") return;
+ for (var i = 0; i < node.childNodes.length; i++)
+ walk(node.childNodes[i]);
+ if (/^(pre|div|p)$/i.test(node.nodeName))
+ closing = true;
+ } else if (node.nodeType == 3) {
+ var val = node.nodeValue;
+ if (!val) return;
+ if (closing) {
+ text += lineSep;
+ closing = false;
+ }
+ text += val;
+ }
+ }
+ for (;;) {
+ walk(from);
+ if (from == to) break;
+ from = from.nextSibling;
+ }
+ return text;
+ }
+
+ CodeMirror.inputStyles = {"textarea": TextareaInput, "contenteditable": ContentEditableInput};
+
+ // SELECTION / CURSOR
+
+ // 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;
+ }
+
+ 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);
+ }
+ }
+
+ // 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);
+ }
+
+ // 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, options) {
+ 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));
+ },
+ origin: options && options.origin
+ };
+ 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, options);
+
+ var bias = options && options.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 = true;
+ signalCursorActivity(doc.cm);
+ }
+ 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 old = sel.ranges.length == doc.sel.ranges.length && doc.sel.ranges[i];
+ var newAnchor = skipAtomic(doc, range.anchor, old && old.anchor, bias, mayClear);
+ var newHead = skipAtomic(doc, range.head, old && old.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;
+ }
+
+ function skipAtomicInner(doc, pos, oldPos, dir, mayClear) {
+ var line = getLine(doc, pos.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 <= pos.ch : sp.from < pos.ch)) &&
+ (sp.to == null || (m.inclusiveRight ? sp.to >= pos.ch : sp.to > pos.ch))) {
+ if (mayClear) {
+ signal(m, "beforeCursorEnter");
+ if (m.explicitlyCleared) {
+ if (!line.markedSpans) break;
+ else {--i; continue;}
+ }
+ }
+ if (!m.atomic) continue;
+
+ if (oldPos) {
+ var near = m.find(dir < 0 ? 1 : -1), diff;
+ if (dir < 0 ? m.inclusiveRight : m.inclusiveLeft)
+ near = movePos(doc, near, -dir, near && near.line == pos.line ? line : null);
+ if (near && near.line == pos.line && (diff = cmp(near, oldPos)) && (dir < 0 ? diff < 0 : diff > 0))
+ return skipAtomicInner(doc, near, pos, dir, mayClear);
+ }
+
+ var far = m.find(dir < 0 ? -1 : 1);
+ if (dir < 0 ? m.inclusiveLeft : m.inclusiveRight)
+ far = movePos(doc, far, dir, far.line == pos.line ? line : null);
+ return far ? skipAtomicInner(doc, far, pos, dir, mayClear) : null;
+ }
+ }
+ return pos;
+ }
+
+ // Ensure a given position is not inside an atomic range.
+ function skipAtomic(doc, pos, oldPos, bias, mayClear) {
+ var dir = bias || 1;
+ var found = skipAtomicInner(doc, pos, oldPos, dir, mayClear) ||
+ (!mayClear && skipAtomicInner(doc, pos, oldPos, dir, true)) ||
+ skipAtomicInner(doc, pos, oldPos, -dir, mayClear) ||
+ (!mayClear && skipAtomicInner(doc, pos, oldPos, -dir, true));
+ if (!found) {
+ doc.cantEdit = true;
+ return Pos(doc.first, 0);
+ }
+ return found;
+ }
+
+ function movePos(doc, pos, dir, line) {
+ if (dir < 0 && pos.ch == 0) {
+ if (pos.line > doc.first) return clipPos(doc, Pos(pos.line - 1));
+ else return null;
+ } else if (dir > 0 && pos.ch == (line || getLine(doc, pos.line)).text.length) {
+ if (pos.line < doc.first + doc.size - 1) return Pos(pos.line + 1, 0);
+ else return null;
+ } else {
+ return new Pos(pos.line, pos.ch + dir);
+ }
+ }
+
+ // SELECTION DRAWING
+
+ function updateSelection(cm) {
+ cm.display.input.showSelection(cm.display.input.prepareSelection());
+ }
+
+ function prepareSelection(cm, primary) {
+ var doc = cm.doc, result = {};
+ var curFragment = result.cursors = document.createDocumentFragment();
+ var selFragment = result.selection = document.createDocumentFragment();
+
+ for (var i = 0; i < doc.sel.ranges.length; i++) {
+ if (primary === false && i == doc.sel.primIndex) continue;
+ var range = doc.sel.ranges[i];
+ if (range.from().line >= cm.display.viewTo || range.to().line < cm.display.viewFrom) continue;
+ var collapsed = range.empty();
+ if (collapsed || cm.options.showCursorWhenSelecting)
+ drawSelectionCursor(cm, range.head, curFragment);
+ if (!collapsed)
+ drawSelectionRange(cm, range, selFragment);
+ }
+ return result;
+ }
+
+ // Draws a cursor for the given range
+ function drawSelectionCursor(cm, head, output) {
+ var pos = cursorCoords(cm, head, "div", null, null, !cm.options.singleCursorHeightPerLine);
+
+ 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 drawSelectionRange(cm, range, output) {
+ var display = cm.display, doc = cm.doc;
+ var fragment = document.createDocumentFragment();
+ var padding = paddingH(cm.display), leftSide = padding.left;
+ var rightSide = Math.max(display.sizerWidth, displayWidth(cm) - display.sizer.offsetLeft) - padding.right;
+
+ function add(left, top, width, bottom) {
+ if (top < 0) top = 0;
+ top = Math.round(top);
+ bottom = Math.round(bottom);
+ fragment.appendChild(elt("div", null, "CodeMirror-selected", "position: absolute; left: " + left +
+ "px; top: " + top + "px; width: " + (width == null ? rightSide - left : width) +
+ "px; height: " + (bottom - top) + "px"));
+ }
+
+ function drawForLine(line, fromArg, toArg) {
+ var lineObj = getLine(doc, line);
+ var lineLen = lineObj.text.length;
+ var start, end;
+ function coords(ch, bias) {
+ return charCoords(cm, Pos(line, ch), "div", lineObj, bias);
+ }
+
+ iterateBidiSections(getOrder(lineObj), fromArg || 0, toArg == null ? lineLen : toArg, function(from, to, dir) {
+ var leftPos = coords(from, "left"), rightPos, left, right;
+ if (from == to) {
+ rightPos = leftPos;
+ left = right = leftPos.left;
+ } else {
+ rightPos = coords(to - 1, "right");
+ if (dir == "rtl") { var tmp = leftPos; leftPos = rightPos; rightPos = tmp; }
+ left = leftPos.left;
+ right = rightPos.right;
+ }
+ 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 = leftSide;
+ if (leftPos.bottom < rightPos.top) add(left, leftPos.bottom, null, rightPos.top);
+ }
+ 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 < leftSide + 1) left = leftSide;
+ add(left, rightPos.top, right - left, rightPos.bottom);
+ });
+ return {start: start, end: end};
+ }
+
+ var sFrom = range.from(), sTo = range.to();
+ if (sFrom.line == sTo.line) {
+ drawForLine(sFrom.line, sFrom.ch, sTo.ch);
+ } else {
+ 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(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(leftSide, leftEnd.bottom, null, rightStart.top);
+ }
+
+ output.appendChild(fragment);
+ }
+
+ // Cursor-blinking
+ function restartBlink(cm) {
+ if (!cm.state.focused) return;
+ var display = cm.display;
+ clearInterval(display.blinker);
+ var on = true;
+ display.cursorDiv.style.visibility = "";
+ if (cm.options.cursorBlinkRate > 0)
+ display.blinker = setInterval(function() {
+ display.cursorDiv.style.visibility = (on = !on) ? "" : "hidden";
+ }, cm.options.cursorBlinkRate);
+ else if (cm.options.cursorBlinkRate < 0)
+ display.cursorDiv.style.visibility = "hidden";
+ }
+
+ // HIGHLIGHT WORKER
+
+ function startWorker(cm, time) {
+ 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.viewTo) return;
+ var end = +new Date + cm.options.workTime;
+ var state = copyState(doc.mode, getStateBefore(cm, doc.frontier));
+ var changedLines = [];
+
+ 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, tooLong = line.text.length > cm.options.maxHighlightLength;
+ var highlighted = highlightLine(cm, line, tooLong ? copyState(doc.mode, state) : state, true);
+ line.styles = highlighted.styles;
+ var oldCls = line.styleClasses, newCls = highlighted.classes;
+ if (newCls) line.styleClasses = newCls;
+ else if (oldCls) line.styleClasses = null;
+ var ischange = !oldStyles || oldStyles.length != line.styles.length ||
+ oldCls != newCls && (!oldCls || !newCls || oldCls.bgClass != newCls.bgClass || oldCls.textClass != newCls.textClass);
+ for (var i = 0; !ischange && i < oldStyles.length; ++i) ischange = oldStyles[i] != line.styles[i];
+ if (ischange) changedLines.push(doc.frontier);
+ line.stateAfter = tooLong ? state : copyState(doc.mode, state);
+ } else {
+ if (line.text.length <= cm.options.maxHighlightLength)
+ processLine(cm, line.text, state);
+ line.stateAfter = doc.frontier % 5 == 0 ? copyState(doc.mode, state) : null;
+ }
+ ++doc.frontier;
+ if (+new Date > end) {
+ startWorker(cm, cm.options.workDelay);
+ return true;
+ }
+ });
+ if (changedLines.length) runInOp(cm, function() {
+ for (var i = 0; i < changedLines.length; i++)
+ regLineChange(cm, changedLines[i], "text");
+ });
+ }
+
+ // Finds the line to start with when starting a parse. Tries to
+ // find a line with a stateAfter, so that it can start with a
+ // valid state. If that fails, it returns the line with the
+ // smallest indentation, which tends to need the least context to
+ // parse correctly.
+ function findStartLine(cm, n, precise) {
+ var minindent, minline, doc = cm.doc;
+ var lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100);
+ for (var search = n; search > lim; --search) {
+ if (search <= doc.first) return doc.first;
+ var line = getLine(doc, search - 1);
+ if (line.stateAfter && (!precise || search <= doc.frontier)) return search;
+ var indented = countColumn(line.text, null, cm.options.tabSize);
+ if (minline == null || minindent > indented) {
+ minline = search - 1;
+ minindent = indented;
+ }
+ }
+ return minline;
+ }
+
+ function getStateBefore(cm, n, precise) {
+ var doc = cm.doc, display = cm.display;
+ if (!doc.mode.startState) return true;
+ var pos = findStartLine(cm, n, precise), state = pos > doc.first && getLine(doc, pos-1).stateAfter;
+ if (!state) state = startState(doc.mode);
+ 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.viewFrom && pos < display.viewTo;
+ line.stateAfter = save ? copyState(doc.mode, state) : null;
+ ++pos;
+ });
+ if (precise) doc.frontier = pos;
+ return state;
+ }
+
+ // POSITION MEASUREMENT
+
+ function paddingTop(display) {return display.lineSpace.offsetTop;}
+ function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight;}
+ 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;
+ var data = {left: parseInt(style.paddingLeft), right: parseInt(style.paddingRight)};
+ if (!isNaN(data.left) && !isNaN(data.right)) display.cachedPaddingH = data;
+ return data;
+ }
+
+ function scrollGap(cm) { return scrollerGap - cm.display.nativeBarWidth; }
+ function displayWidth(cm) {
+ return cm.display.scroller.clientWidth - scrollGap(cm) - cm.display.barWidth;
+ }
+ function displayHeight(cm) {
+ return cm.display.scroller.clientHeight - scrollGap(cm) - cm.display.barHeight;
+ }
+
+ // 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 && displayWidth(cm);
+ 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);
+ }
+ }
+ heights.push(rect.bottom - rect.top);
+ }
+ }
+
+ // 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));
+ cm.curOp.forceUpdate = true;
+ }
+ 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, varHeight) {
+ 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;
+ }
+ found = measureCharInner(cm, prepared, ch, bias);
+ if (!found.bogus) prepared.cache[key] = found;
+ }
+ return {left: found.left, right: found.right,
+ top: varHeight ? found.rtop : found.top,
+ bottom: varHeight ? found.rbottom : found.bottom};
+ }
+
+ var nullRect = {left: 0, right: 0, top: 0, bottom: 0};
+
+ function nodeAndOffsetInLineMap(map, ch, bias) {
+ 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;
+ }
+ }
+ return {node: node, start: start, end: end, collapse: collapse, coverStart: mStart, coverEnd: mEnd};
+ }
+
+ function measureCharInner(cm, prepared, ch, bias) {
+ var place = nodeAndOffsetInLineMap(prepared.map, ch, bias);
+ var node = place.node, start = place.start, end = place.end, collapse = place.collapse;
+
+ var rect;
+ if (node.nodeType == 3) { // If it is a text node, use a range to retrieve the coordinates.
+ for (var i = 0; i < 4; i++) { // Retry a maximum of 4 times when nonsense rectangles are returned
+ while (start && isExtendingChar(prepared.line.text.charAt(place.coverStart + start))) --start;
+ while (place.coverStart + end < place.coverEnd && isExtendingChar(prepared.line.text.charAt(place.coverStart + end))) ++end;
+ if (ie && ie_version < 9 && start == 0 && end == place.coverEnd - place.coverStart) {
+ 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() || nullRect;
+ }
+ if (rect.left || rect.right || start == 0) break;
+ end = start;
+ start = start - 1;
+ collapse = "right";
+ }
+ if (ie && ie_version < 11) rect = maybeUpdateRectForZooming(cm.display.measure, rect);
+ } 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 && ie_version < 9 && !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 rtop = rect.top - prepared.rect.top, rbot = rect.bottom - prepared.rect.top;
+ var mid = (rtop + rbot) / 2;
+ var heights = prepared.view.measure.heights;
+ for (var i = 0; i < heights.length - 1; i++)
+ if (mid < heights[i]) break;
+ var 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;
+ if (!cm.options.singleCursorHeightPerLine) { result.rtop = rtop; result.rbottom = rbot; }
+
+ return result;
+ }
+
+ // Work around problem with bounding client rects on ranges being
+ // returned incorrectly when zoomed on IE10 and below.
+ function maybeUpdateRectForZooming(measure, rect) {
+ if (!window.screen || screen.logicalXDPI == null ||
+ screen.logicalXDPI == screen.deviceXDPI || !hasBadZoomedRects(measure))
+ return rect;
+ var scaleX = screen.logicalXDPI / screen.deviceXDPI;
+ var scaleY = screen.logicalYDPI / screen.deviceYDPI;
+ return {left: rect.left * scaleX, right: rect.right * scaleX,
+ top: rect.top * scaleY, bottom: rect.bottom * scaleY};
+ }
+
+ 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] = {};
+ }
+ }
+
+ 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) {
+ clearLineMeasurementCache(cm);
+ cm.display.cachedCharWidth = cm.display.cachedTextHeight = cm.display.cachedPaddingH = null;
+ if (!cm.options.lineWrapping) cm.display.maxLineChanged = true;
+ cm.display.lineNumChars = null;
+ }
+
+ function pageScrollX() { return window.pageXOffset || (document.documentElement || document.body).scrollLeft; }
+ function pageScrollY() { return window.pageYOffset || (document.documentElement || document.body).scrollTop; }
+
+ // 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), "window",
+ // 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]);
+ rect.top += size; rect.bottom += size;
+ }
+ if (context == "line") return rect;
+ if (!context) context = "local";
+ var yOff = heightAtLine(lineObj);
+ if (context == "local") yOff += paddingTop(cm.display);
+ else yOff -= cm.display.viewOffset;
+ if (context == "page" || context == "window") {
+ 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;
+ }
+ rect.top += yOff; rect.bottom += yOff;
+ return rect;
+ }
+
+ // 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;
+ // First move into "page" coordinate system
+ if (context == "page") {
+ left -= pageScrollX();
+ top -= pageScrollY();
+ } else if (context == "local" || !context) {
+ var localBox = cm.display.sizer.getBoundingClientRect();
+ left += localBox.left;
+ top += localBox.top;
+ }
+
+ 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, bias), context);
+ }
+
+ // 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, varHeight) {
+ lineObj = lineObj || getLine(cm.doc, pos.line);
+ if (!preparedMeasure) preparedMeasure = prepareMeasureForLine(cm, lineObj);
+ function get(ch, right) {
+ var m = measureCharPrepared(cm, preparedMeasure, ch, right ? "right" : "left", varHeight);
+ if (right) m.left = m.right; else m.right = m.left;
+ return intoCoordSystem(cm, lineObj, m, context);
+ }
+ function getBidi(ch, partPos) {
+ var part = order[partPos], right = part.level % 2;
+ if (ch == bidiLeft(part) && partPos && part.level < order[partPos - 1].level) {
+ part = order[--partPos];
+ ch = bidiRight(part) - (part.level % 2 ? 0 : 1);
+ right = true;
+ } else if (ch == bidiRight(part) && partPos < order.length - 1 && part.level < order[partPos + 1].level) {
+ part = order[++partPos];
+ ch = bidiLeft(part) - part.level % 2;
+ right = false;
+ }
+ if (right && ch == part.to && ch > part.from) return get(ch - 1);
+ return get(ch, right);
+ }
+ var order = getOrder(lineObj), ch = pos.ch;
+ if (!order) return get(ch);
+ var partPos = getBidiPartAt(order, ch);
+ var val = getBidi(ch, partPos);
+ if (bidiOther != null) val.other = getBidi(ch, bidiOther);
+ 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 = Pos(line, ch);
+ pos.xRel = xRel;
+ if (outside) pos.outside = true;
+ return pos;
+ }
+
+ // 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 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 found = coordsCharInner(cm, lineObj, lineN, x, y);
+ var merged = collapsedSpanAtEnd(lineObj);
+ var mergedPos = merged && merged.find(0, true);
+ if (merged && (found.ch > mergedPos.from.ch || found.ch == mergedPos.from.ch && found.xRel > 0))
+ lineN = lineNo(lineObj = mergedPos.to.line);
+ else
+ return found;
+ }
+ }
+
+ function coordsCharInner(cm, lineObj, lineNo, x, y) {
+ var innerOff = y - heightAtLine(lineObj);
+ var wrongLine = false, adjust = 2 * cm.display.wrapper.clientWidth;
+ var preparedMeasure = prepareMeasureForLine(cm, lineObj);
+
+ function getX(ch) {
+ 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;
+ else wrongLine = false;
+ return sp.left;
+ }
+
+ var bidi = getOrder(lineObj), dist = lineObj.text.length;
+ var from = lineLeft(lineObj), to = lineRight(lineObj);
+ var fromX = getX(from), fromOutside = wrongLine, toX = getX(to), toOutside = wrongLine;
+
+ if (x > toX) return PosWithInfo(lineNo, to, toOutside, 1);
+ // Do a binary search between these bounds.
+ for (;;) {
+ if (bidi ? to == from || to == moveVisually(lineObj, from, 1) : to - from <= 1) {
+ var ch = x < fromX || x - fromX <= toX - x ? from : to;
+ var outside = ch == from ? fromOutside : toOutside
+ var xDiff = x - (ch == from ? fromX : toX);
+ // This is a kludge to handle the case where the coordinates
+ // are after a line-wrapped line. We should replace it with a
+ // more general handling of cursor positions around line
+ // breaks. (Issue #4078)
+ if (toOutside && !bidi && !/\s/.test(lineObj.text.charAt(ch)) && xDiff > 0 &&
+ ch < lineObj.text.length && preparedMeasure.view.measure.heights.length > 1) {
+ var charSize = measureCharPrepared(cm, preparedMeasure, ch, "right");
+ if (innerOff <= charSize.bottom && innerOff >= charSize.top && Math.abs(x - charSize.right) < xDiff) {
+ outside = false
+ ch++
+ xDiff = x - charSize.right
+ }
+ }
+ while (isExtendingChar(lineObj.text.charAt(ch))) ++ch;
+ var pos = PosWithInfo(lineNo, ch, outside, xDiff < -1 ? -1 : xDiff > 1 ? 1 : 0);
+ return pos;
+ }
+ var step = Math.ceil(dist / 2), middle = from + step;
+ if (bidi) {
+ middle = from;
+ for (var i = 0; i < step; ++i) middle = moveVisually(lineObj, middle, 1);
+ }
+ var middleX = getX(middle);
+ if (middleX > x) {to = middle; toX = middleX; if (toOutside = wrongLine) toX += 1000; dist = step;}
+ else {from = middle; fromX = middleX; fromOutside = wrongLine; dist -= step;}
+ }
+ }
+
+ var measureText;
+ // Compute the default text height.
+ function textHeight(display) {
+ if (display.cachedTextHeight != null) return display.cachedTextHeight;
+ if (measureText == null) {
+ measureText = elt("pre");
+ // Measure a bunch of lines, for browsers that compute
+ // fractional heights.
+ for (var i = 0; i < 49; ++i) {
+ measureText.appendChild(document.createTextNode("x"));
+ measureText.appendChild(elt("br"));
+ }
+ measureText.appendChild(document.createTextNode("x"));
+ }
+ removeChildrenAndAdd(display.measure, measureText);
+ var height = measureText.offsetHeight / 50;
+ if (height > 3) display.cachedTextHeight = height;
+ removeChildren(display.measure);
+ return height || 1;
+ }
+
+ // Compute the default character width.
+ function charWidth(display) {
+ if (display.cachedCharWidth != null) return display.cachedCharWidth;
+ var anchor = elt("span", "xxxxxxxxxx");
+ var pre = elt("pre", [anchor]);
+ removeChildrenAndAdd(display.measure, pre);
+ 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 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 operationGroup = null;
+
+ var nextOpId = 0;
+ // Start a new operation.
+ function startOperation(cm) {
+ cm.curOp = {
+ cm: cm,
+ 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
+ cursorActivityHandlers: null, // Set of handlers to fire cursorActivity on
+ cursorActivityCalled: 0, // Tracks which cursorActivity handlers have been called already
+ 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
+ focus: false,
+ id: ++nextOpId // Unique ID
+ };
+ if (operationGroup) {
+ operationGroup.ops.push(cm.curOp);
+ } else {
+ cm.curOp.ownsGroup = operationGroup = {
+ ops: [cm.curOp],
+ delayedCallbacks: []
+ };
+ }
+ }
+
+ function fireCallbacksForOps(group) {
+ // Calls delayed callbacks and cursorActivity handlers until no
+ // new ones appear
+ var callbacks = group.delayedCallbacks, i = 0;
+ do {
+ for (; i < callbacks.length; i++)
+ callbacks[i].call(null);
+ for (var j = 0; j < group.ops.length; j++) {
+ var op = group.ops[j];
+ if (op.cursorActivityHandlers)
+ while (op.cursorActivityCalled < op.cursorActivityHandlers.length)
+ op.cursorActivityHandlers[op.cursorActivityCalled++].call(null, op.cm);
+ }
+ } while (i < callbacks.length);
+ }
+
+ // Finish an operation, updating the display and signalling delayed events
+ function endOperation(cm) {
+ var op = cm.curOp, group = op.ownsGroup;
+ if (!group) return;
+
+ try { fireCallbacksForOps(group); }
+ finally {
+ operationGroup = null;
+ for (var i = 0; i < group.ops.length; i++)
+ group.ops[i].cm.curOp = null;
+ endOperations(group);
+ }
+ }
+
+ // The DOM updates done when an operation finishes are batched so
+ // that the minimum number of relayouts are required.
+ function endOperations(group) {
+ var ops = group.ops;
+ for (var i = 0; i < ops.length; i++) // Read DOM
+ endOperation_R1(ops[i]);
+ for (var i = 0; i < ops.length; i++) // Write DOM (maybe)
+ endOperation_W1(ops[i]);
+ for (var i = 0; i < ops.length; i++) // Read DOM
+ endOperation_R2(ops[i]);
+ for (var i = 0; i < ops.length; i++) // Write DOM (maybe)
+ endOperation_W2(ops[i]);
+ for (var i = 0; i < ops.length; i++) // Read DOM
+ endOperation_finish(ops[i]);
+ }
+
+ function endOperation_R1(op) {
+ var cm = op.cm, display = cm.display;
+ maybeClipScrollbars(cm);
+ if (op.updateMaxLine) findMaxLine(cm);
+
+ op.mustUpdate = 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;
+ op.update = op.mustUpdate &&
+ new DisplayUpdate(cm, op.mustUpdate && {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate);
+ }
+
+ function endOperation_W1(op) {
+ op.updatedDisplay = op.mustUpdate && updateDisplayIfNeeded(op.cm, op.update);
+ }
+
+ function endOperation_R2(op) {
+ var cm = op.cm, display = cm.display;
+ if (op.updatedDisplay) updateHeightsInViewport(cm);
+
+ op.barMeasure = measureForScrollbars(cm);
+
+ // If the max line changed since it was last measured, measure it,
+ // and ensure the document's width matches it.
+ // updateDisplay_W2 will use these properties to do the actual resizing
+ if (display.maxLineChanged && !cm.options.lineWrapping) {
+ op.adjustWidthTo = measureChar(cm, display.maxLine, display.maxLine.text.length).left + 3;
+ cm.display.sizerWidth = op.adjustWidthTo;
+ op.barMeasure.scrollWidth =
+ Math.max(display.scroller.clientWidth, display.sizer.offsetLeft + op.adjustWidthTo + scrollGap(cm) + cm.display.barWidth);
+ op.maxScrollLeft = Math.max(0, display.sizer.offsetLeft + op.adjustWidthTo - displayWidth(cm));
+ }
+
+ if (op.updatedDisplay || op.selectionChanged)
+ op.preparedSelection = display.input.prepareSelection(op.focus);
+ }
+
+ function endOperation_W2(op) {
+ var cm = op.cm;
+
+ if (op.adjustWidthTo != null) {
+ cm.display.sizer.style.minWidth = op.adjustWidthTo + "px";
+ if (op.maxScrollLeft < cm.doc.scrollLeft)
+ setScrollLeft(cm, Math.min(cm.display.scroller.scrollLeft, op.maxScrollLeft), true);
+ cm.display.maxLineChanged = false;
+ }
+
+ var takeFocus = op.focus && op.focus == activeElt() && (!document.hasFocus || document.hasFocus())
+ if (op.preparedSelection)
+ cm.display.input.showSelection(op.preparedSelection, takeFocus);
+ if (op.updatedDisplay || op.startHeight != cm.doc.height)
+ updateScrollbars(cm, op.barMeasure);
+ if (op.updatedDisplay)
+ setDocumentHeight(cm, op.barMeasure);
+
+ if (op.selectionChanged) restartBlink(cm);
+
+ if (cm.state.focused && op.updateInput)
+ cm.display.input.reset(op.typing);
+ if (takeFocus) ensureFocus(op.cm);
+ }
+
+ function endOperation_finish(op) {
+ var cm = op.cm, display = cm.display, doc = cm.doc;
+
+ if (op.updatedDisplay) postUpdateDisplay(cm, op.update);
+
+ // Abort mouse wheel delta measurement, when scrolling explicitly
+ if (display.wheelStartX != null && (op.scrollTop != null || op.scrollLeft != null || op.scrollToPos))
+ display.wheelStartX = display.wheelStartY = null;
+
+ // Propagate the scroll position to the actual DOM scroller
+ if (op.scrollTop != null && (display.scroller.scrollTop != op.scrollTop || op.forceScroll)) {
+ doc.scrollTop = Math.max(0, Math.min(display.scroller.scrollHeight - display.scroller.clientHeight, op.scrollTop));
+ display.scrollbars.setScrollTop(doc.scrollTop);
+ display.scroller.scrollTop = doc.scrollTop;
+ }
+ if (op.scrollLeft != null && (display.scroller.scrollLeft != op.scrollLeft || op.forceScroll)) {
+ doc.scrollLeft = Math.max(0, Math.min(display.scroller.scrollWidth - display.scroller.clientWidth, op.scrollLeft));
+ display.scrollbars.setScrollLeft(doc.scrollLeft);
+ display.scroller.scrollLeft = doc.scrollLeft;
+ alignHorizontally(cm);
+ }
+ // If we need to scroll a specific position into view, do so.
+ if (op.scrollToPos) {
+ var coords = scrollPosIntoView(cm, clipPos(doc, op.scrollToPos.from),
+ clipPos(doc, op.scrollToPos.to), op.scrollToPos.margin);
+ if (op.scrollToPos.isCursor && cm.state.focused) maybeScrollWindow(cm, coords);
+ }
+
+ // 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");
+ if (unhidden) for (var i = 0; i < unhidden.length; ++i)
+ if (unhidden[i].lines.length) signal(unhidden[i], "unhide");
+
+ if (display.wrapper.offsetHeight)
+ doc.scrollTop = cm.display.scroller.scrollTop;
+
+ // Fire change events, and delayed event handlers
+ if (op.changeObjs)
+ signal(cm, "changes", cm, op.changeObjs);
+ if (op.update)
+ op.update.finish();
+ }
+
+ // Run the given function in an operation
+ function runInOp(cm, f) {
+ 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;
+ 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 || newN == cm.doc.first + cm.doc.size)
+ 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;
+ }
+
+ // 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));
+ // Older IE's will not fire a second mousedown for a double click
+ if (ie && ie_version < 11)
+ 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 = cm.findWordAt(pos);
+ extendSelection(cm.doc, word.anchor, word.head);
+ }));
+ else
+ on(d.scroller, "dblclick", function(e) { signalDOMEvent(cm, e) || e_preventDefault(e); });
+ // 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 these browsers.
+ if (!captureRightClick) on(d.scroller, "contextmenu", function(e) {onContextMenu(cm, e);});
+
+ // Used to suppress mouse event handling when a touch happens
+ var touchFinished, prevTouch = {end: 0};
+ function finishTouch() {
+ if (d.activeTouch) {
+ touchFinished = setTimeout(function() {d.activeTouch = null;}, 1000);
+ prevTouch = d.activeTouch;
+ prevTouch.end = +new Date;
+ }
+ };
+ function isMouseLikeTouchEvent(e) {
+ if (e.touches.length != 1) return false;
+ var touch = e.touches[0];
+ return touch.radiusX <= 1 && touch.radiusY <= 1;
+ }
+ function farAway(touch, other) {
+ if (other.left == null) return true;
+ var dx = other.left - touch.left, dy = other.top - touch.top;
+ return dx * dx + dy * dy > 20 * 20;
+ }
+ on(d.scroller, "touchstart", function(e) {
+ if (!signalDOMEvent(cm, e) && !isMouseLikeTouchEvent(e)) {
+ clearTimeout(touchFinished);
+ var now = +new Date;
+ d.activeTouch = {start: now, moved: false,
+ prev: now - prevTouch.end <= 300 ? prevTouch : null};
+ if (e.touches.length == 1) {
+ d.activeTouch.left = e.touches[0].pageX;
+ d.activeTouch.top = e.touches[0].pageY;
+ }
+ }
+ });
+ on(d.scroller, "touchmove", function() {
+ if (d.activeTouch) d.activeTouch.moved = true;
+ });
+ on(d.scroller, "touchend", function(e) {
+ var touch = d.activeTouch;
+ if (touch && !eventInWidget(d, e) && touch.left != null &&
+ !touch.moved && new Date - touch.start < 300) {
+ var pos = cm.coordsChar(d.activeTouch, "page"), range;
+ if (!touch.prev || farAway(touch, touch.prev)) // Single tap
+ range = new Range(pos, pos);
+ else if (!touch.prev.prev || farAway(touch, touch.prev.prev)) // Double tap
+ range = cm.findWordAt(pos);
+ else // Triple tap
+ range = new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0)));
+ cm.setSelection(range.anchor, range.head);
+ cm.focus();
+ e_preventDefault(e);
+ }
+ finishTouch();
+ });
+ on(d.scroller, "touchcancel", finishTouch);
+
+ // 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);
+ setScrollLeft(cm, d.scroller.scrollLeft, true);
+ signal(cm, "scroll", cm);
+ }
+ });
+
+ // 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 wrapper from ever scrolling
+ on(d.wrapper, "scroll", function() { d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; });
+
+ d.dragFunctions = {
+ enter: function(e) {if (!signalDOMEvent(cm, e)) e_stop(e);},
+ over: function(e) {if (!signalDOMEvent(cm, e)) { onDragOver(cm, e); e_stop(e); }},
+ start: function(e){onDragStart(cm, e);},
+ drop: operation(cm, onDrop),
+ leave: function(e) {if (!signalDOMEvent(cm, e)) { clearDragCursor(cm); }}
+ };
+
+ var inp = d.input.getField();
+ on(inp, "keyup", function(e) { onKeyUp.call(cm, e); });
+ on(inp, "keydown", operation(cm, onKeyDown));
+ on(inp, "keypress", operation(cm, onKeyPress));
+ on(inp, "focus", bind(onFocus, cm));
+ on(inp, "blur", bind(onBlur, cm));
+ }
+
+ function dragDropChanged(cm, value, old) {
+ var wasOn = old && old != CodeMirror.Init;
+ if (!value != !wasOn) {
+ var funcs = cm.display.dragFunctions;
+ var toggle = value ? on : off;
+ toggle(cm.display.scroller, "dragstart", funcs.start);
+ toggle(cm.display.scroller, "dragenter", funcs.enter);
+ toggle(cm.display.scroller, "dragover", funcs.over);
+ toggle(cm.display.scroller, "dragleave", funcs.leave);
+ toggle(cm.display.scroller, "drop", funcs.drop);
+ }
+ }
+
+ // Called when the window resizes
+ function onResize(cm) {
+ var d = cm.display;
+ if (d.lastWrapHeight == d.wrapper.clientHeight && d.lastWrapWidth == d.wrapper.clientWidth)
+ return;
+ // Might be a text scaling operation, clear size caches.
+ d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null;
+ d.scrollbarsClipped = false;
+ cm.setSize();
+ }
+
+ // 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.nodeType == 1 && n.getAttribute("cm-ignore-events") == "true") ||
+ (n.parentNode == display.sizer && n != display.mover))
+ return true;
+ }
+ }
+
+ // 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 && e_target(e).getAttribute("cm-not-content") == "true") return null;
+
+ var x, y, space = display.lineSpace.getBoundingClientRect();
+ // Fails unpredictably on IE[67] when mouse is dragged around quickly.
+ 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.max(0, Math.round((x - paddingH(cm.display).left) / charWidth(cm.display)) - colDiff));
+ }
+ return coords;
+ }
+
+ // 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) {
+ var cm = this, display = cm.display;
+ if (signalDOMEvent(cm, e) || display.activeTouch && display.input.supportsTouch()) return;
+ 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);
+ }
+ return;
+ }
+ if (clickInGutter(cm, e)) return;
+ var start = posFromMouse(cm, e);
+ window.focus();
+
+ switch (e_button(e)) {
+ case 1:
+ // #3261: make sure, that we're not starting a second selection
+ if (cm.state.selectingText)
+ cm.state.selectingText(e);
+ else 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(function() {display.input.focus();}, 20);
+ e_preventDefault(e);
+ break;
+ case 3:
+ if (captureRightClick) onContextMenu(cm, e);
+ else delayBlurEvent(cm);
+ break;
+ }
+ }
+
+ var lastClick, lastDoubleClick;
+ function leftButtonDown(cm, e, start) {
+ if (ie) setTimeout(bind(ensureFocus, cm), 0);
+ else cm.curOp.focus = activeElt();
+
+ var now = +new Date, type;
+ if (lastDoubleClick && lastDoubleClick.time > now - 400 && cmp(lastDoubleClick.pos, start) == 0) {
+ type = "triple";
+ } else if (lastClick && lastClick.time > now - 400 && cmp(lastClick.pos, start) == 0) {
+ type = "double";
+ lastDoubleClick = {time: now, pos: start};
+ } else {
+ type = "single";
+ lastClick = {time: now, pos: start};
+ }
+
+ var sel = cm.doc.sel, modifier = mac ? e.metaKey : e.ctrlKey, contained;
+ if (cm.options.dragDrop && dragAndDrop && !cm.isReadOnly() &&
+ type == "single" && (contained = sel.contains(start)) > -1 &&
+ (cmp((contained = sel.ranges[contained]).from(), start) < 0 || start.xRel > 0) &&
+ (cmp(contained.to(), start) > 0 || start.xRel < 0))
+ leftButtonStartDrag(cm, e, start, modifier);
+ else
+ leftButtonSelect(cm, e, start, type, modifier);
+ }
+
+ // 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, modifier) {
+ var display = cm.display, startTime = +new Date;
+ 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);
+ if (!modifier && +new Date - 200 < startTime)
+ extendSelection(cm.doc, start);
+ // Work around unexplainable focus problem in IE9 (#2127) and Chrome (#3081)
+ if (webkit || ie && ie_version == 9)
+ setTimeout(function() {document.body.focus(); display.input.focus();}, 20);
+ else
+ display.input.focus();
+ }
+ });
+ // Let the drag handler handle this.
+ if (webkit) display.scroller.draggable = true;
+ cm.state.draggingText = dragEnd;
+ dragEnd.copy = mac ? e.altKey : e.ctrlKey
+ // 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);
+
+ var ourRange, ourIndex, startSel = doc.sel, ranges = startSel.ranges;
+ if (addNew && !e.shiftKey) {
+ ourIndex = doc.sel.contains(start);
+ if (ourIndex > -1)
+ ourRange = ranges[ourIndex];
+ else
+ ourRange = new Range(start, start);
+ } else {
+ ourRange = doc.sel.primary();
+ ourIndex = doc.sel.primIndex;
+ }
+
+ if (chromeOS ? e.shiftKey && e.metaKey : 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 = cm.findWordAt(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 (!addNew) {
+ ourIndex = 0;
+ setSelection(doc, new Selection([ourRange], 0), sel_mouse);
+ startSel = doc.sel;
+ } else if (ourIndex == -1) {
+ ourIndex = ranges.length;
+ setSelection(doc, normalizeSelection(ranges.concat([ourRange]), ourIndex),
+ {scroll: false, origin: "*mouse"});
+ } else if (ranges.length > 1 && ranges[ourIndex].empty() && type == "single" && !e.shiftKey) {
+ setSelection(doc, normalizeSelection(ranges.slice(0, ourIndex).concat(ranges.slice(ourIndex + 1)), 0),
+ {scroll: false, origin: "*mouse"});
+ startSel = doc.sel;
+ } else {
+ replaceOneSelection(doc, ourIndex, ourRange, sel_mouse);
+ }
+
+ 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),
+ {origin: "*mouse", scroll: false});
+ cm.scrollIntoView(pos);
+ } else {
+ var oldRange = ourRange;
+ var anchor = oldRange.anchor, head = pos;
+ if (type != "single") {
+ if (type == "double")
+ var range = cm.findWordAt(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 = 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,
+ // if the clear happens after their scheduled firing time).
+ var counter = 0;
+
+ function extend(e) {
+ var curCount = ++counter;
+ var cur = posFromMouse(cm, e, true, type == "rect");
+ if (!cur) return;
+ if (cmp(cur, lastPos) != 0) {
+ cm.curOp.focus = activeElt();
+ 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);
+ } else {
+ var outside = e.clientY < editorSize.top ? -20 : e.clientY > editorSize.bottom ? 20 : 0;
+ if (outside) setTimeout(operation(cm, function() {
+ if (counter != curCount) return;
+ display.scroller.scrollTop += outside;
+ extend(e);
+ }), 50);
+ }
+ }
+
+ function done(e) {
+ cm.state.selectingText = false;
+ counter = Infinity;
+ e_preventDefault(e);
+ display.input.focus();
+ off(document, "mousemove", move);
+ off(document, "mouseup", up);
+ doc.history.lastSelOrigin = null;
+ }
+
+ var move = operation(cm, function(e) {
+ if (!e_button(e)) done(e);
+ else extend(e);
+ });
+ var up = operation(cm, done);
+ cm.state.selectingText = up;
+ on(document, "mousemove", move);
+ 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) {
+ try { var mX = e.clientX, mY = e.clientY; }
+ catch(e) { return false; }
+ if (mX >= Math.floor(cm.display.gutters.getBoundingClientRect().right)) return false;
+ if (prevent) e_preventDefault(e);
+
+ var display = cm.display;
+ 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 && g.getBoundingClientRect().right >= mX) {
+ var line = lineAtHeight(cm.doc, mY);
+ var gutter = cm.options.gutters[i];
+ signal(cm, type, cm, line, gutter, e);
+ return e_defaultPrevented(e);
+ }
+ }
+ }
+
+ function clickInGutter(cm, e) {
+ return gutterEvent(cm, e, "gutterClick", true);
+ }
+
+ // Kludge to work around strange IE behavior where it'll sometimes
+ // re-fire a series of drag-related events right after the drop (#1551)
+ var lastDrop = 0;
+
+ function onDrop(e) {
+ var cm = this;
+ clearDragCursor(cm);
+ if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e))
+ return;
+ e_preventDefault(e);
+ if (ie) lastDrop = +new Date;
+ var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files;
+ if (!pos || cm.isReadOnly()) 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) {
+ if (cm.options.allowDropFileTypes &&
+ indexOf(cm.options.allowDropFileTypes, file.type) == -1)
+ return;
+
+ var reader = new FileReader;
+ reader.onload = operation(cm, function() {
+ var content = reader.result;
+ if (/[\x00-\x08\x0e-\x1f]{2}/.test(content)) content = "";
+ text[i] = content;
+ if (++read == n) {
+ pos = clipPos(cm.doc, pos);
+ var change = {from: pos, to: pos,
+ text: cm.doc.splitLines(text.join(cm.doc.lineSeparator())),
+ 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 { // Normal drop
+ // Don't do a replace if the drop happened inside of the selected text.
+ if (cm.state.draggingText && cm.doc.sel.contains(pos) > -1) {
+ cm.state.draggingText(e);
+ // Ensure the editor is re-focused
+ setTimeout(function() {cm.display.input.focus();}, 20);
+ return;
+ }
+ try {
+ var text = e.dataTransfer.getData("Text");
+ if (text) {
+ if (cm.state.draggingText && !cm.state.draggingText.copy)
+ var selected = 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");
+ cm.display.input.focus();
+ }
+ }
+ catch(e){}
+ }
+ }
+
+ function onDragStart(cm, e) {
+ if (ie && (!cm.state.draggingText || +new Date - lastDrop < 100)) { e_stop(e); return; }
+ if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) return;
+
+ e.dataTransfer.setData("Text", cm.getSelection());
+ e.dataTransfer.effectAllowed = "copyMove"
+
+ // 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 (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 (presto) img.parentNode.removeChild(img);
+ }
+ }
+
+ function onDragOver(cm, e) {
+ var pos = posFromMouse(cm, e);
+ if (!pos) return;
+ var frag = document.createDocumentFragment();
+ drawSelectionCursor(cm, pos, frag);
+ if (!cm.display.dragCursor) {
+ cm.display.dragCursor = elt("div", null, "CodeMirror-cursors CodeMirror-dragcursors");
+ cm.display.lineSpace.insertBefore(cm.display.dragCursor, cm.display.cursorDiv);
+ }
+ removeChildrenAndAdd(cm.display.dragCursor, frag);
+ }
+
+ function clearDragCursor(cm) {
+ if (cm.display.dragCursor) {
+ cm.display.lineSpace.removeChild(cm.display.dragCursor);
+ cm.display.dragCursor = null;
+ }
+ }
+
+ // 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) updateDisplaySimple(cm, {top: val});
+ if (cm.display.scroller.scrollTop != val) cm.display.scroller.scrollTop = val;
+ cm.display.scrollbars.setScrollTop(val);
+ if (gecko) updateDisplaySimple(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);
+ cm.doc.scrollLeft = val;
+ alignHorizontally(cm);
+ if (cm.display.scroller.scrollLeft != val) cm.display.scroller.scrollLeft = val;
+ cm.display.scrollbars.setScrollLeft(val);
+ }
+
+ // Since the delta values reported on mouse wheel events are
+ // unstandardized between browsers and even browser versions, and
+ // generally horribly unpredictable, this code starts by measuring
+ // the scroll effect that the first few mouse wheel events have,
+ // and, from that, detects the way it can convert deltas to pixel
+ // offsets afterwards.
+ //
+ // The reason we want to know the amount a wheel event will scroll
+ // is that it gives us a chance to update the display before the
+ // actual scrolling happens, reducing flickering.
+
+ var wheelSamples = 0, wheelPixelsPerUnit = null;
+ // Fill in a browser-detected starting value on browsers where we
+ // 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 (ie) wheelPixelsPerUnit = -.53;
+ else if (gecko) wheelPixelsPerUnit = 15;
+ else if (chrome) wheelPixelsPerUnit = -.7;
+ else if (safari) wheelPixelsPerUnit = -1/3;
+
+ var wheelEventDelta = function(e) {
+ var dx = e.wheelDeltaX, dy = e.wheelDeltaY;
+ if (dx == null && e.detail && e.axis == e.HORIZONTAL_AXIS) dx = e.detail;
+ if (dy == null && e.detail && e.axis == e.VERTICAL_AXIS) dy = e.detail;
+ else if (dy == null) dy = e.wheelDelta;
+ return {x: dx, y: dy};
+ };
+ CodeMirror.wheelEventPixels = function(e) {
+ var delta = wheelEventDelta(e);
+ delta.x *= wheelPixelsPerUnit;
+ delta.y *= wheelPixelsPerUnit;
+ return delta;
+ };
+
+ function onScrollWheel(cm, e) {
+ var delta = wheelEventDelta(e), dx = delta.x, dy = delta.y;
+
+ var display = cm.display, scroll = display.scroller;
+ // Quit if there's nothing to scroll here
+ var canScrollX = scroll.scrollWidth > scroll.clientWidth;
+ var canScrollY = scroll.scrollHeight > scroll.clientHeight;
+ if (!(dx && canScrollX || dy && canScrollY)) return;
+
+ // Webkit browsers on OS X abort momentum scrolls when the target
+ // of the scroll event is removed from the scrollable element.
+ // This hack (see related code in patchDisplay) makes sure the
+ // element is kept around.
+ if (dy && mac && webkit) {
+ 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;
+ }
+ }
+ }
+ }
+
+ // On some browsers, horizontal scrolling will cause redraws to
+ // happen before the gutter has been realigned, causing it to
+ // wriggle around in a most unseemly way. When we have an
+ // 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 && !presto && wheelPixelsPerUnit != null) {
+ if (dy && canScrollY)
+ 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)));
+ // Only prevent default scrolling if vertical scrolling is
+ // actually possible. Otherwise, it causes vertical scroll
+ // jitter on OSX trackpads when deltaX is small and deltaY
+ // is large (issue #3579)
+ if (!dy || (dy && canScrollY))
+ e_preventDefault(e);
+ display.wheelStartX = null; // Abort measurement, if in progress
+ 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);
+ updateDisplaySimple(cm, {top: top, bottom: bot});
+ }
+
+ if (wheelSamples < 20) {
+ if (display.wheelStartX == null) {
+ display.wheelStartX = scroll.scrollLeft; display.wheelStartY = scroll.scrollTop;
+ display.wheelDX = dx; display.wheelDY = dy;
+ setTimeout(function() {
+ if (display.wheelStartX == null) return;
+ var movedX = scroll.scrollLeft - display.wheelStartX;
+ var movedY = scroll.scrollTop - display.wheelStartY;
+ var sample = (movedY && display.wheelDY && movedY / display.wheelDY) ||
+ (movedX && display.wheelDX && movedX / display.wheelDX);
+ display.wheelStartX = display.wheelStartY = null;
+ if (!sample) return;
+ wheelPixelsPerUnit = (wheelPixelsPerUnit * wheelSamples + sample) / (wheelSamples + 1);
+ ++wheelSamples;
+ }, 200);
+ } else {
+ display.wheelDX += dx; display.wheelDY += dy;
+ }
+ }
+ }
+
+ // KEY EVENTS
+
+ // Run a handler that was bound to a key.
+ function doHandleBinding(cm, bound, dropShift) {
+ if (typeof bound == "string") {
+ bound = commands[bound];
+ if (!bound) return false;
+ }
+ // Ensure previous input has been read, so that the handler sees a
+ // consistent view of the document
+ cm.display.input.ensurePolled();
+ var prevShift = cm.display.shift, done = false;
+ try {
+ if (cm.isReadOnly()) cm.state.suppressEdits = true;
+ if (dropShift) cm.display.shift = false;
+ done = bound(cm) != Pass;
+ } finally {
+ cm.display.shift = prevShift;
+ cm.state.suppressEdits = false;
+ }
+ return done;
+ }
+
+ function lookupKeyForEditor(cm, name, handle) {
+ for (var i = 0; i < cm.state.keyMaps.length; i++) {
+ var result = lookupKey(name, cm.state.keyMaps[i], handle, cm);
+ if (result) return result;
+ }
+ return (cm.options.extraKeys && lookupKey(name, cm.options.extraKeys, handle, cm))
+ || lookupKey(name, cm.options.keyMap, handle, cm);
+ }
+
+ var stopSeq = new Delayed;
+ function dispatchKey(cm, name, e, handle) {
+ var seq = cm.state.keySeq;
+ if (seq) {
+ if (isModifierKey(name)) return "handled";
+ stopSeq.set(50, function() {
+ if (cm.state.keySeq == seq) {
+ cm.state.keySeq = null;
+ cm.display.input.reset();
+ }
+ });
+ name = seq + " " + name;
+ }
+ var result = lookupKeyForEditor(cm, name, handle);
+
+ if (result == "multi")
+ cm.state.keySeq = name;
+ if (result == "handled")
+ signalLater(cm, "keyHandled", cm, name, e);
+
+ if (result == "handled" || result == "multi") {
+ e_preventDefault(e);
+ restartBlink(cm);
+ }
+
+ if (seq && !result && /\'$/.test(name)) {
+ e_preventDefault(e);
+ return true;
+ }
+ return !!result;
+ }
+
+ // Handle a key from the keydown event.
+ function handleKeyBinding(cm, e) {
+ var name = keyName(e, true);
+ if (!name) return false;
+
+ if (e.shiftKey && !cm.state.keySeq) {
+ // First try to resolve full name (including 'Shift-'). Failing
+ // that, see if there is a cursor-motion command (starting with
+ // 'go') bound to the keyname without 'Shift-'.
+ return dispatchKey(cm, "Shift-" + name, e, function(b) {return doHandleBinding(cm, b, true);})
+ || dispatchKey(cm, name, e, function(b) {
+ if (typeof b == "string" ? /^go[A-Z]/.test(b) : b.motion)
+ return doHandleBinding(cm, b);
+ });
+ } else {
+ return dispatchKey(cm, name, e, function(b) { return doHandleBinding(cm, b); });
+ }
+ }
+
+ // Handle a key from the keypress event
+ function handleCharBinding(cm, e, ch) {
+ return dispatchKey(cm, "'" + ch + "'", e,
+ function(b) { return doHandleBinding(cm, b, true); });
+ }
+
+ var lastStoppedKey = null;
+ function onKeyDown(e) {
+ var cm = this;
+ cm.curOp.focus = activeElt();
+ if (signalDOMEvent(cm, e)) return;
+ // IE does strange things with escape.
+ if (ie && ie_version < 11 && e.keyCode == 27) e.returnValue = false;
+ var code = e.keyCode;
+ cm.display.shift = code == 16 || e.shiftKey;
+ var handled = handleKeyBinding(cm, e);
+ 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("", null, "cut");
+ }
+
+ // Turn mouse into crosshair when Alt is held on Mac.
+ if (code == 18 && !/\bCodeMirror-crosshair\b/.test(cm.display.lineDiv.className))
+ showCrossHair(cm);
+ }
+
+ function showCrossHair(cm) {
+ var lineDiv = cm.display.lineDiv;
+ addClass(lineDiv, "CodeMirror-crosshair");
+
+ function up(e) {
+ if (e.keyCode == 18 || !e.altKey) {
+ rmClass(lineDiv, "CodeMirror-crosshair");
+ off(document, "keyup", up);
+ off(document, "mouseover", up);
+ }
+ }
+ on(document, "keyup", up);
+ on(document, "mouseover", up);
+ }
+
+ function onKeyUp(e) {
+ if (e.keyCode == 16) this.doc.sel.shift = false;
+ signalDOMEvent(this, e);
+ }
+
+ function onKeyPress(e) {
+ var cm = this;
+ if (eventInWidget(cm.display, e) || signalDOMEvent(cm, e) || e.ctrlKey && !e.altKey || mac && e.metaKey) return;
+ var keyCode = e.keyCode, charCode = e.charCode;
+ if (presto && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;}
+ if ((presto && (!e.which || e.which < 10)) && handleKeyBinding(cm, e)) return;
+ var ch = String.fromCharCode(charCode == null ? keyCode : charCode);
+ if (handleCharBinding(cm, e, ch)) return;
+ cm.display.input.onKeyPress(e);
+ }
+
+ // FOCUS/BLUR EVENTS
+
+ function delayBlurEvent(cm) {
+ cm.state.delayingBlurEvent = true;
+ setTimeout(function() {
+ if (cm.state.delayingBlurEvent) {
+ cm.state.delayingBlurEvent = false;
+ onBlur(cm);
+ }
+ }, 100);
+ }
+
+ function onFocus(cm) {
+ if (cm.state.delayingBlurEvent) cm.state.delayingBlurEvent = false;
+
+ if (cm.options.readOnly == "nocursor") return;
+ if (!cm.state.focused) {
+ signal(cm, "focus", cm);
+ cm.state.focused = true;
+ addClass(cm.display.wrapper, "CodeMirror-focused");
+ // This test prevents this from firing when a context
+ // menu is closed (since the input reset would kill the
+ // select-all detection hack)
+ if (!cm.curOp && cm.display.selForContextMenu != cm.doc.sel) {
+ cm.display.input.reset();
+ if (webkit) setTimeout(function() { cm.display.input.reset(true); }, 20); // Issue #1730
+ }
+ cm.display.input.receivedFocus();
+ }
+ restartBlink(cm);
+ }
+ function onBlur(cm) {
+ if (cm.state.delayingBlurEvent) return;
+
+ if (cm.state.focused) {
+ signal(cm, "blur", cm);
+ cm.state.focused = false;
+ rmClass(cm.display.wrapper, "CodeMirror-focused");
+ }
+ clearInterval(cm.display.blinker);
+ setTimeout(function() {if (!cm.state.focused) cm.display.shift = false;}, 150);
+ }
+
+ // CONTEXT MENU HANDLING
+
+ // 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 (eventInWidget(cm.display, e) || contextMenuInGutter(cm, e)) return;
+ if (signalDOMEvent(cm, e, "contextmenu")) return;
+ cm.display.input.onContextMenu(e);
+ }
+
+ function contextMenuInGutter(cm, e) {
+ if (!hasHandler(cm, "gutterContextMenu")) return false;
+ return gutterEvent(cm, e, "gutterContextMenu", false);
+ }
+
+ // 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));
+ };
+
+ // 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)));
+ }
+ return normalizeSelection(out, doc.sel.primIndex);
+ }
+
+ 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,
+ from: change.from,
+ to: change.to,
+ text: change.text,
+ origin: change.origin,
+ cancel: function() { this.canceled = true; }
+ };
+ if (update) obj.update = function(from, to, text, origin) {
+ if (from) this.from = clipPos(doc, from);
+ if (to) this.to = clipPos(doc, to);
+ if (text) this.text = text;
+ if (origin !== undefined) this.origin = origin;
+ };
+ signal(doc, "beforeChange", doc, obj);
+ if (doc.cm) signal(doc.cm, "beforeChange", doc.cm, obj);
+
+ if (obj.canceled) return null;
+ return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin};
+ }
+
+ // 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, ignoreReadOnly);
+ if (doc.cm.state.suppressEdits) return;
+ }
+
+ if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) {
+ change = filterChange(doc, change, true);
+ if (!change) return;
+ }
+
+ // Possibly split or suppress the update based on the presence
+ // 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 >= 0; --i)
+ makeChangeInner(doc, {from: split[i].from, to: split[i].to, text: i ? [""] : change.text});
+ } else {
+ makeChangeInner(doc, change);
+ }
+ }
+
+ 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 = [];
+
+ linkedDocs(doc, function(doc, sharedHist) {
+ if (!sharedHist && indexOf(rebased, doc.history) == -1) {
+ rebaseHist(doc.history, change);
+ rebased.push(doc.history);
+ }
+ makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change));
+ });
+ }
+
+ // 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, event, selAfter = doc.sel;
+ var source = type == "undo" ? hist.done : hist.undone, dest = type == "undo" ? hist.undone : hist.done;
+
+ // 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");
+
+ for (var i = event.changes.length - 1; i >= 0; --i) {
+ var change = event.changes[i];
+ change.origin = type;
+ if (filter && !filterChange(doc, change, false)) {
+ source.length = 0;
+ return;
+ }
+
+ antiChanges.push(historyChangeFromChange(doc, change));
+
+ var after = i ? computeSelAfterChange(doc, change) : lst(source);
+ makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change));
+ if (!i && doc.cm) doc.cm.scrollIntoView({from: change.from, to: changeEnd(change)});
+ var rebased = [];
+
+ // Propagate to the linked documents
+ linkedDocs(doc, function(doc, sharedHist) {
+ if (!sharedHist && indexOf(rebased, doc.history) == -1) {
+ rebaseHist(doc.history, change);
+ rebased.push(doc.history);
+ }
+ makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change));
+ });
+ }
+ }
+
+ // Sub-views need their line numbers shifted when text is added
+ // above or below them in the parent document.
+ function shiftDoc(doc, distance) {
+ if (distance == 0) return;
+ doc.first += distance;
+ 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);
+ for (var d = doc.cm.display, l = d.viewFrom; l < d.viewTo; l++)
+ regLineChange(doc.cm, l, "gutter");
+ }
+ }
+
+ // 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);
+
+ if (change.to.line < doc.first) {
+ shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line));
+ return;
+ }
+ if (change.from.line > doc.lastLine()) return;
+
+ // Clip the change to the size of this doc
+ if (change.from.line < doc.first) {
+ var shift = change.text.length - 1 - (doc.first - change.from.line);
+ shiftDoc(doc, shift);
+ change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch),
+ text: [lst(change.text)], origin: change.origin};
+ }
+ var last = doc.lastLine();
+ if (change.to.line > last) {
+ change = {from: change.from, to: Pos(last, getLine(doc, last).text.length),
+ text: [change.text[0]], origin: change.origin};
+ }
+
+ change.removed = getBetween(doc, change.from, change.to);
+
+ if (!selAfter) selAfter = computeSelAfterChange(doc, change);
+ if (doc.cm) makeChangeSingleDocInEditor(doc.cm, change, spans);
+ else updateDoc(doc, change, spans);
+ setSelectionNoUndo(doc, selAfter, sel_dontScroll);
+ }
+
+ // 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(getLine(doc, from.line)));
+ doc.iter(checkWidthStart, to.line + 1, function(line) {
+ if (line == display.maxLine) {
+ recomputeMaxLength = true;
+ return true;
+ }
+ });
+ }
+
+ if (doc.sel.contains(change.from, change.to) > -1)
+ signalCursorActivity(cm);
+
+ updateDoc(doc, change, spans, estimateHeight(cm));
+
+ if (!cm.options.lineWrapping) {
+ doc.iter(checkWidthStart, from.line + change.text.length, function(line) {
+ var len = lineLength(line);
+ if (len > display.maxLineLength) {
+ display.maxLine = line;
+ display.maxLineLength = len;
+ display.maxLineChanged = true;
+ recomputeMaxLength = false;
+ }
+ });
+ if (recomputeMaxLength) cm.curOp.updateMaxLine = true;
+ }
+
+ // Adjust frontier, schedule worker
+ doc.frontier = Math.min(doc.frontier, from.line);
+ startWorker(cm, 400);
+
+ var lendiff = change.text.length - (to.line - from.line) - 1;
+ // Remember that these lines changed, for updating the display
+ if (change.full)
+ regChange(cm);
+ else 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);
+
+ var changesHandler = hasHandler(cm, "changes"), changeHandler = hasHandler(cm, "change");
+ if (changeHandler || changesHandler) {
+ var obj = {
+ from: from, to: to,
+ text: change.text,
+ removed: change.removed,
+ origin: change.origin
+ };
+ if (changeHandler) signalLater(cm, "change", cm, obj);
+ if (changesHandler) (cm.curOp.changeObjs || (cm.curOp.changeObjs = [])).push(obj);
+ }
+ cm.display.selForContextMenu = null;
+ }
+
+ function replaceRange(doc, code, from, to, origin) {
+ if (!to) to = from;
+ if (cmp(to, from) < 0) { var tmp = to; to = from; from = tmp; }
+ if (typeof code == "string") code = doc.splitLines(code);
+ makeChange(doc, {from: from, to: to, text: code, origin: origin});
+ }
+
+ // SCROLLING THINGS INTO VIEW
+
+ // 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) {
+ if (signalDOMEvent(cm, "scrollCursorIntoView")) return;
+
+ 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 - paddingTop(cm.display)) + "px; height: " +
+ (coords.bottom - coords.top + scrollGap(cm) + display.barHeight) + "px; left: " +
+ coords.left + "px; width: 2px;");
+ cm.display.lineSpace.appendChild(scrollNode);
+ scrollNode.scrollIntoView(doScroll);
+ cm.display.lineSpace.removeChild(scrollNode);
+ }
+ }
+
+ // 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 (var limit = 0; limit < 5; limit++) {
+ var changed = false, coords = cursorCoords(cm, pos);
+ var endCoords = !end || end == pos ? coords : cursorCoords(cm, end);
+ var scrollPos = calculateScrollPos(cm, Math.min(coords.left, endCoords.left),
+ Math.min(coords.top, endCoords.top) - margin,
+ Math.max(coords.left, endCoords.left),
+ Math.max(coords.bottom, endCoords.bottom) + margin);
+ var startTop = cm.doc.scrollTop, startLeft = cm.doc.scrollLeft;
+ if (scrollPos.scrollTop != null) {
+ setScrollTop(cm, scrollPos.scrollTop);
+ if (Math.abs(cm.doc.scrollTop - startTop) > 1) changed = true;
+ }
+ if (scrollPos.scrollLeft != null) {
+ setScrollLeft(cm, scrollPos.scrollLeft);
+ if (Math.abs(cm.doc.scrollLeft - startLeft) > 1) changed = true;
+ }
+ if (!changed) break;
+ }
+ return coords;
+ }
+
+ // 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 screentop = cm.curOp && cm.curOp.scrollTop != null ? cm.curOp.scrollTop : display.scroller.scrollTop;
+ var screen = displayHeight(cm), result = {};
+ if (y2 - y1 > screen) y2 = y1 + screen;
+ var docBottom = cm.doc.height + paddingVert(display);
+ var atTop = y1 < snapMargin, atBottom = y2 > docBottom - snapMargin;
+ if (y1 < screentop) {
+ result.scrollTop = atTop ? 0 : y1;
+ } else if (y2 > screentop + screen) {
+ var newTop = Math.min(y1, (atBottom ? docBottom : y2) - screen);
+ if (newTop != screentop) result.scrollTop = newTop;
+ }
+
+ var screenleft = cm.curOp && cm.curOp.scrollLeft != null ? cm.curOp.scrollLeft : display.scroller.scrollLeft;
+ var screenw = displayWidth(cm) - (cm.options.fixedGutter ? display.gutters.offsetWidth : 0);
+ var tooWide = x2 - x1 > screenw;
+ if (tooWide) x2 = x1 + screenw;
+ if (x1 < 10)
+ result.scrollLeft = 0;
+ else if (x1 < screenleft)
+ result.scrollLeft = Math.max(0, x1 - (tooWide ? 0 : 10));
+ else if (x2 > screenw + screenleft - 3)
+ result.scrollLeft = x2 + (tooWide ? 0 : 10) - screenw;
+ return result;
+ }
+
+ // 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;
+ }
+
+ // 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, state;
+ if (how == null) how = "add";
+ if (how == "smart") {
+ // Fall back to "prev" when the mode doesn't have an indentation
+ // method.
+ if (!doc.mode.indent) how = "prev";
+ 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;
+ how = "not";
+ } else if (how == "smart") {
+ indentation = doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text);
+ if (indentation == Pass || indentation > 150) {
+ if (!aggressive) return;
+ how = "prev";
+ }
+ }
+ if (how == "prev") {
+ if (n > doc.first) indentation = countColumn(getLine(doc, n-1).text, null, tabSize);
+ else indentation = 0;
+ } else if (how == "add") {
+ indentation = curSpace + cm.options.indentUnit;
+ } else if (how == "subtract") {
+ indentation = curSpace - cm.options.indentUnit;
+ } else if (typeof how == "number") {
+ indentation = curSpace + how;
+ }
+ indentation = Math.max(0, indentation);
+
+ var indentString = "", pos = 0;
+ if (cm.options.indentWithTabs)
+ for (var i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += "\t";}
+ if (pos < indentation) indentString += spaceStr(indentation - pos);
+
+ if (indentString != curSpaceString) {
+ replaceRange(doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input");
+ line.stateAfter = null;
+ return true;
+ } 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;
+ }
+ }
+ }
+ }
+
+ // Utility for applying a change to a line by handle or number,
+ // returning the number and optionally registering the line as
+ // changed.
+ function changeLine(doc, handle, changeType, op) {
+ var no = handle, line = handle;
+ if (typeof handle == "number") line = getLine(doc, clipLine(doc, handle));
+ else no = lineNo(handle);
+ if (no == null) return null;
+ if (op(line, no) && doc.cm) regLineChange(doc.cm, no, changeType);
+ 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);
+ function findNextLine() {
+ var l = line + dir;
+ if (l < doc.first || l >= doc.first + doc.size) return false
+ line = l;
+ return lineObj = getLine(doc, l);
+ }
+ function moveOnce(boundToLine) {
+ var next = (visually ? moveVisually : moveLogically)(lineObj, ch, dir, true);
+ if (next == null) {
+ if (!boundToLine && findNextLine()) {
+ if (visually) ch = (dir < 0 ? lineRight : lineLeft)(lineObj);
+ else ch = dir < 0 ? lineObj.text.length : 0;
+ } else return false
+ } else ch = next;
+ return true;
+ }
+
+ if (unit == "char") {
+ moveOnce()
+ } else if (unit == "column") {
+ moveOnce(true)
+ } else if (unit == "word" || unit == "group") {
+ var sawType = null, group = unit == "group";
+ var helper = doc.cm && doc.cm.getHelper(pos, "wordChars");
+ for (var first = true;; first = false) {
+ if (dir < 0 && !moveOnce(!first)) break;
+ var cur = lineObj.text.charAt(ch) || "\n";
+ var type = isWordChar(cur, helper) ? "w"
+ : 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;
+ }
+ }
+ var result = skipAtomic(doc, Pos(line, ch), pos, origDir, true);
+ if (!cmp(pos, result)) result.hitSide = true;
+ 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") {
+ var pageSize = Math.min(cm.display.wrapper.clientHeight, window.innerHeight || document.documentElement.clientHeight);
+ y = pos.top + dir * (pageSize - (dir < 0 ? 1.5 : .5) * textHeight(cm.display));
+ } else if (unit == "line") {
+ y = dir > 0 ? pos.bottom + 3 : pos.top - 3;
+ }
+ for (;;) {
+ var target = coordsChar(cm, x, y);
+ if (!target.outside) break;
+ if (dir < 0 ? y <= 0 : y >= doc.height) { target.hitSide = true; break; }
+ y += dir * 5;
+ }
+ return target;
+ }
+
+ // EDITOR METHODS
+
+ // The publicly visible API. Note that methodOp(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,
+ focus: function(){window.focus(); this.display.input.focus();},
+
+ setOption: function(option, value) {
+ var options = this.options, old = options[option];
+ if (options[option] == value && option != "mode") return;
+ options[option] = value;
+ if (optionHandlers.hasOwnProperty(option))
+ operation(this, optionHandlers[option])(this, value, old);
+ },
+
+ getOption: function(option) {return this.options[option];},
+ getDoc: function() {return this.doc;},
+
+ addKeyMap: function(map, bottom) {
+ this.state.keyMaps[bottom ? "push" : "unshift"](getKeyMap(map));
+ },
+ removeKeyMap: function(map) {
+ var maps = this.state.keyMaps;
+ for (var i = 0; i < maps.length; ++i)
+ if (maps[i] == map || maps[i].name == map) {
+ maps.splice(i, 1);
+ return true;
+ }
+ },
+
+ 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: methodOp(function(spec) {
+ var overlays = this.state.overlays;
+ for (var i = 0; i < overlays.length; ++i) {
+ var cur = overlays[i].modeSpec;
+ if (cur == spec || typeof spec == "string" && cur.name == spec) {
+ overlays.splice(i, 1);
+ this.state.modeGen++;
+ regChange(this);
+ return;
+ }
+ }
+ }),
+
+ 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: 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 from = range.from(), to = range.to();
+ var start = Math.max(end, from.line);
+ end = Math.min(this.lastLine(), to.line - (to.ch ? 0 : 1)) + 1;
+ for (var j = start; j < end; ++j)
+ indentLine(this, j, how);
+ var newRanges = this.doc.sel.ranges;
+ if (from.ch == 0 && ranges.length == newRanges.length && newRanges[i].from().ch > 0)
+ replaceOneSelection(this.doc, i, new Range(from, newRanges[i].to()), sel_dontScroll);
+ } 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
+ // that want to inspect the mode state (say, for completion).
+ getTokenAt: function(pos, precise) {
+ return takeToken(this, pos, precise);
+ },
+
+ getLineTokens: function(line, precise) {
+ return takeToken(this, Pos(line), precise, true);
+ },
+
+ getTokenTypeAt: function(pos) {
+ pos = clipPos(this.doc, pos);
+ var styles = getLineStyles(this, getLine(this.doc, pos.line));
+ var before = 0, after = (styles.length - 1) / 2, ch = pos.ch;
+ var type;
+ if (ch == 0) type = styles[2];
+ else for (;;) {
+ var mid = (before + after) >> 1;
+ if ((mid ? styles[mid * 2 - 1] : 0) >= ch) after = mid;
+ else if (styles[mid * 2 + 1] < ch) before = mid + 1;
+ else { type = styles[mid * 2 + 2]; break; }
+ }
+ var cut = type ? type.indexOf("cm-overlay ") : -1;
+ return cut < 0 ? type : cut == 0 ? null : type.slice(0, cut - 1);
+ },
+
+ getModeAt: function(pos) {
+ var mode = this.doc.mode;
+ if (!mode.innerMode) return mode;
+ return CodeMirror.innerMode(mode, this.getTokenAt(pos).state).mode;
+ },
+
+ getHelper: function(pos, type) {
+ return this.getHelpers(pos, type)[0];
+ },
+
+ getHelpers: function(pos, type) {
+ var found = [];
+ if (!helpers.hasOwnProperty(type)) return found;
+ var help = helpers[type], mode = this.getModeAt(pos);
+ if (typeof mode[type] == "string") {
+ if (help[mode[type]]) found.push(help[mode[type]]);
+ } else if (mode[type]) {
+ for (var i = 0; i < mode[type].length; i++) {
+ var val = help[mode[type][i]];
+ if (val) found.push(val);
+ }
+ } else if (mode.helperType && help[mode.helperType]) {
+ found.push(help[mode.helperType]);
+ } else if (help[mode.name]) {
+ found.push(help[mode.name]);
+ }
+ for (var i = 0; i < help._global.length; i++) {
+ var cur = help._global[i];
+ if (cur.pred(mode, this) && indexOf(found, cur.val) == -1)
+ found.push(cur.val);
+ }
+ return found;
+ },
+
+ getStateAfter: function(line, precise) {
+ var doc = this.doc;
+ line = clipLine(doc, line == null ? doc.first + doc.size - 1: line);
+ return getStateBefore(this, line + 1, precise);
+ },
+
+ cursorCoords: function(start, mode) {
+ 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 ? range.from() : range.to();
+ return cursorCoords(this, pos, mode || "page");
+ },
+
+ charCoords: function(pos, mode) {
+ return charCoords(this, clipPos(this.doc, pos), mode || "page");
+ },
+
+ coordsChar: function(coords, mode) {
+ coords = fromCoordSystem(this, coords, mode || "page");
+ return coordsChar(this, coords.left, coords.top);
+ },
+
+ lineAtHeight: function(height, mode) {
+ height = fromCoordSystem(this, {top: height, left: 0}, mode || "page").top;
+ return lineAtHeight(this.doc, height + this.display.viewOffset);
+ },
+ heightAtLine: function(line, mode) {
+ var end = false, lineObj;
+ if (typeof line == "number") {
+ var last = this.doc.first + this.doc.size - 1;
+ if (line < this.doc.first) line = this.doc.first;
+ else if (line > last) { line = last; end = true; }
+ lineObj = getLine(this.doc, line);
+ } else {
+ lineObj = line;
+ }
+ 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: methodOp(function(line, gutterID, value) {
+ return changeLine(this.doc, line, "gutter", function(line) {
+ var markers = line.gutterMarkers || (line.gutterMarkers = {});
+ markers[gutterID] = value;
+ if (!value && isEmpty(markers)) line.gutterMarkers = null;
+ return true;
+ });
+ }),
+
+ 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;
+ regLineChange(cm, i, "gutter");
+ if (isEmpty(line.gutterMarkers)) line.gutterMarkers = null;
+ }
+ ++i;
+ });
+ }),
+
+ lineInfo: function(line) {
+ if (typeof line == "number") {
+ if (!isLine(this.doc, line)) return null;
+ var n = line;
+ line = getLine(this.doc, line);
+ if (!line) return null;
+ } else {
+ var n = lineNo(line);
+ if (n == null) return null;
+ }
+ return {line: n, handle: line, text: line.text, gutterMarkers: line.gutterMarkers,
+ textClass: line.textClass, bgClass: line.bgClass, wrapClass: line.wrapClass,
+ widgets: line.widgets};
+ },
+
+ getViewport: function() { return {from: this.display.viewFrom, to: this.display.viewTo};},
+
+ addWidget: function(pos, node, scroll, vert, horiz) {
+ var display = this.display;
+ pos = cursorCoords(this, clipPos(this.doc, pos));
+ var top = pos.bottom, left = pos.left;
+ node.style.position = "absolute";
+ node.setAttribute("cm-ignore-events", "true");
+ this.display.input.setUneditable(node);
+ display.sizer.appendChild(node);
+ if (vert == "over") {
+ top = pos.top;
+ } else if (vert == "above" || vert == "near") {
+ var vspace = Math.max(display.wrapper.clientHeight, this.doc.height),
+ hspace = Math.max(display.sizer.clientWidth, display.lineSpace.clientWidth);
+ // Default to positioning above (if specified and possible); otherwise default to positioning below
+ if ((vert == 'above' || pos.bottom + node.offsetHeight > vspace) && pos.top > node.offsetHeight)
+ top = pos.top - node.offsetHeight;
+ else if (pos.bottom + node.offsetHeight <= vspace)
+ top = pos.bottom;
+ if (left + node.offsetWidth > hspace)
+ left = hspace - node.offsetWidth;
+ }
+ node.style.top = top + "px";
+ node.style.left = node.style.right = "";
+ if (horiz == "right") {
+ left = display.sizer.clientWidth - node.offsetWidth;
+ node.style.right = "0px";
+ } else {
+ if (horiz == "left") left = 0;
+ else if (horiz == "middle") left = (display.sizer.clientWidth - node.offsetWidth) / 2;
+ node.style.left = left + "px";
+ }
+ if (scroll)
+ scrollIntoView(this, left, top, left + node.offsetWidth, top + node.offsetHeight);
+ },
+
+ triggerOnKeyDown: methodOp(onKeyDown),
+ triggerOnKeyPress: methodOp(onKeyPress),
+ triggerOnKeyUp: onKeyUp,
+
+ execCommand: function(cmd) {
+ if (commands.hasOwnProperty(cmd))
+ return commands[cmd].call(null, this);
+ },
+
+ triggerElectric: methodOp(function(text) { triggerElectric(this, text); }),
+
+ findPosH: function(from, amount, unit, visually) {
+ var dir = 1;
+ if (amount < 0) { dir = -1; amount = -amount; }
+ for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) {
+ cur = findPosH(this.doc, cur, dir, unit, visually);
+ if (cur.hitSide) break;
+ }
+ return cur;
+ },
+
+ 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: 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) {
+ var dir = 1, x = goalColumn;
+ if (amount < 0) { dir = -1; amount = -amount; }
+ for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) {
+ var coords = cursorCoords(this, cur, "div");
+ if (x == null) x = coords.left;
+ else coords.left = x;
+ cur = findPosV(this, coords, dir, unit);
+ if (cur.hitSide) break;
+ }
+ return cur;
+ },
+
+ 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];
+ }),
+
+ // Find the word at the given position (as returned by coordsChar).
+ findWordAt: function(pos) {
+ var doc = this.doc, line = getLine(doc, pos.line).text;
+ var start = pos.ch, end = pos.ch;
+ if (line) {
+ var helper = this.getHelper(pos, "wordChars");
+ if ((pos.xRel < 0 || end == line.length) && start) --start; else ++end;
+ var startChar = line.charAt(start);
+ var check = isWordChar(startChar, helper)
+ ? function(ch) { return isWordChar(ch, helper); }
+ : /\s/.test(startChar) ? function(ch) {return /\s/.test(ch);}
+ : function(ch) {return !/\s/.test(ch) && !isWordChar(ch);};
+ while (start > 0 && check(line.charAt(start - 1))) --start;
+ while (end < line.length && check(line.charAt(end))) ++end;
+ }
+ return new Range(Pos(pos.line, start), Pos(pos.line, end));
+ },
+
+ toggleOverwrite: function(value) {
+ if (value != null && value == this.state.overwrite) return;
+ if (this.state.overwrite = !this.state.overwrite)
+ addClass(this.display.cursorDiv, "CodeMirror-overwrite");
+ else
+ rmClass(this.display.cursorDiv, "CodeMirror-overwrite");
+
+ signal(this, "overwriteToggle", this, this.state.overwrite);
+ },
+ hasFocus: function() { return this.display.input.getField() == activeElt(); },
+ isReadOnly: function() { return !!(this.options.readOnly || this.doc.cantEdit); },
+
+ 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;
+ return {left: scroller.scrollLeft, top: scroller.scrollTop,
+ height: scroller.scrollHeight - scrollGap(this) - this.display.barHeight,
+ width: scroller.scrollWidth - scrollGap(this) - this.display.barWidth,
+ clientHeight: displayHeight(this), clientWidth: displayWidth(this)};
+ },
+
+ 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);
+ }
+ }),
+
+ setSize: methodOp(function(width, height) {
+ var cm = this;
+ function interpret(val) {
+ return typeof val == "number" || /^\d+$/.test(String(val)) ? val + "px" : val;
+ }
+ if (width != null) cm.display.wrapper.style.width = interpret(width);
+ if (height != null) cm.display.wrapper.style.height = interpret(height);
+ if (cm.options.lineWrapping) clearLineMeasurementCache(this);
+ var lineNo = cm.display.viewFrom;
+ cm.doc.iter(lineNo, cm.display.viewTo, function(line) {
+ if (line.widgets) for (var i = 0; i < line.widgets.length; i++)
+ if (line.widgets[i].noHScroll) { regLineChange(cm, lineNo, "widget"); break; }
+ ++lineNo;
+ });
+ cm.curOp.forceUpdate = true;
+ signal(cm, "refresh", this);
+ }),
+
+ operation: function(f){return runInOp(this, f);},
+
+ refresh: methodOp(function() {
+ var oldHeight = this.display.cachedTextHeight;
+ regChange(this);
+ this.curOp.forceUpdate = true;
+ clearCaches(this);
+ this.scrollTo(this.doc.scrollLeft, this.doc.scrollTop);
+ updateGutterSpace(this);
+ if (oldHeight == null || Math.abs(oldHeight - textHeight(this.display)) > .5)
+ estimateLineHeights(this);
+ signal(this, "refresh", this);
+ }),
+
+ swapDoc: methodOp(function(doc) {
+ var old = this.doc;
+ old.cm = null;
+ attachDoc(this, doc);
+ clearCaches(this);
+ this.display.input.reset();
+ this.scrollTo(doc.scrollLeft, doc.scrollTop);
+ this.curOp.forceScroll = true;
+ signalLater(this, "swapDoc", this, old);
+ return old;
+ }),
+
+ getInputField: function(){return this.display.input.getField();},
+ getWrapperElement: function(){return this.display.wrapper;},
+ getScrollerElement: function(){return this.display.scroller;},
+ getGutterElement: function(){return this.display.gutters;}
+ };
+ eventMixin(CodeMirror);
+
+ // OPTION DEFAULTS
+
+ // 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;
+ if (handle) optionHandlers[name] =
+ 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
+ // have to be initialized before the editor can start at all.
+ option("value", "", function(cm, val) {
+ cm.setValue(val);
+ }, true);
+ option("mode", null, function(cm, val) {
+ cm.doc.modeOption = val;
+ loadMode(cm);
+ }, true);
+
+ option("indentUnit", 2, loadMode, true);
+ option("indentWithTabs", false);
+ option("smartIndent", true);
+ option("tabSize", 4, function(cm) {
+ resetModeState(cm);
+ clearCaches(cm);
+ regChange(cm);
+ }, true);
+ option("lineSeparator", null, function(cm, val) {
+ cm.doc.lineSep = val;
+ if (!val) return;
+ var newBreaks = [], lineNo = cm.doc.first;
+ cm.doc.iter(function(line) {
+ for (var pos = 0;;) {
+ var found = line.text.indexOf(val, pos);
+ if (found == -1) break;
+ pos = found + val.length;
+ newBreaks.push(Pos(lineNo, found));
+ }
+ lineNo++;
+ });
+ for (var i = newBreaks.length - 1; i >= 0; i--)
+ replaceRange(cm.doc, val, newBreaks[i], Pos(newBreaks[i].line, newBreaks[i].ch + val.length))
+ });
+ option("specialChars", /[\u0000-\u001f\u007f\u00ad\u200b-\u200f\u2028\u2029\ufeff]/g, function(cm, val, old) {
+ cm.state.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g");
+ if (old != CodeMirror.Init) cm.refresh();
+ });
+ option("specialCharPlaceholder", defaultSpecialCharPlaceholder, function(cm) {cm.refresh();}, true);
+ option("electricChars", true);
+ option("inputStyle", mobile ? "contenteditable" : "textarea", function() {
+ throw new Error("inputStyle can not (yet) be changed in a running editor"); // FIXME
+ }, true);
+ option("rtlMoveVisually", !windows);
+ option("wholeLineUpdateBefore", true);
+
+ option("theme", "default", function(cm) {
+ themeChanged(cm);
+ guttersChanged(cm);
+ }, true);
+ option("keyMap", "default", function(cm, val, old) {
+ var next = getKeyMap(val);
+ var prev = old != CodeMirror.Init && getKeyMap(old);
+ if (prev && prev.detach) prev.detach(cm, next);
+ if (next.attach) next.attach(cm, prev || null);
+ });
+ option("extraKeys", null);
+
+ option("lineWrapping", false, wrappingChanged, true);
+ option("gutters", [], function(cm) {
+ setGuttersForLineNumbers(cm.options);
+ guttersChanged(cm);
+ }, true);
+ option("fixedGutter", true, function(cm, val) {
+ cm.display.gutters.style.left = val ? compensateForHScroll(cm.display) + "px" : "0";
+ cm.refresh();
+ }, true);
+ option("coverGutterNextToScrollbar", false, function(cm) {updateScrollbars(cm);}, true);
+ option("scrollbarStyle", "native", function(cm) {
+ initScrollbars(cm);
+ updateScrollbars(cm);
+ cm.display.scrollbars.setScrollTop(cm.doc.scrollTop);
+ cm.display.scrollbars.setScrollLeft(cm.doc.scrollLeft);
+ }, true);
+ option("lineNumbers", false, function(cm) {
+ setGuttersForLineNumbers(cm.options);
+ guttersChanged(cm);
+ }, true);
+ option("firstLineNumber", 1, guttersChanged, true);
+ option("lineNumberFormatter", function(integer) {return integer;}, guttersChanged, true);
+ option("showCursorWhenSelecting", false, updateSelection, true);
+
+ option("resetSelectionOnContextMenu", true);
+ option("lineWiseCopyCut", true);
+
+ option("readOnly", false, function(cm, val) {
+ if (val == "nocursor") {
+ onBlur(cm);
+ cm.display.input.blur();
+ cm.display.disabled = true;
+ } else {
+ cm.display.disabled = false;
+ }
+ cm.display.input.readOnlyChanged(val)
+ });
+ option("disableInput", false, function(cm, val) {if (!val) cm.display.input.reset();}, true);
+ option("dragDrop", true, dragDropChanged);
+ option("allowDropFileTypes", null);
+
+ option("cursorBlinkRate", 530);
+ option("cursorScrollMargin", 0);
+ option("cursorHeight", 1, updateSelection, true);
+ option("singleCursorHeightPerLine", true, updateSelection, true);
+ option("workTime", 100);
+ option("workDelay", 100);
+ option("flattenSpans", true, resetModeState, true);
+ option("addModeClass", false, resetModeState, true);
+ option("pollInterval", 100);
+ 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("moveInputWithCursor", true, function(cm, val) {
+ if (!val) cm.display.input.resetPosition();
+ });
+
+ option("tabindex", null, function(cm, val) {
+ cm.display.input.getField().tabIndex = val || "";
+ });
+ option("autofocus", null);
+
+ // MODE DEFINITION AND QUERYING
+
+ // 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)
+ mode.dependencies = Array.prototype.slice.call(arguments, 2);
+ modes[name] = mode;
+ };
+
+ CodeMirror.defineMIME = function(mime, spec) {
+ 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)) {
+ return CodeMirror.resolveMode("application/xml");
+ }
+ if (typeof spec == "string") return {name: spec};
+ 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];
+ if (!mfactory) return CodeMirror.getMode(options, "text/plain");
+ var modeObj = mfactory(options, spec);
+ if (modeExtensions.hasOwnProperty(spec.name)) {
+ var exts = modeExtensions[spec.name];
+ for (var prop in exts) {
+ if (!exts.hasOwnProperty(prop)) continue;
+ if (modeObj.hasOwnProperty(prop)) modeObj["_" + prop] = modeObj[prop];
+ modeObj[prop] = exts[prop];
+ }
+ }
+ modeObj.name = spec.name;
+ if (spec.helperType) modeObj.helperType = spec.helperType;
+ if (spec.modeProps) for (var prop in spec.modeProps)
+ modeObj[prop] = spec.modeProps[prop];
+
+ 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] = {});
+ copyObj(properties, exts);
+ };
+
+ // EXTENSIONS
+
+ CodeMirror.defineExtension = function(name, func) {
+ CodeMirror.prototype[name] = func;
+ };
+ CodeMirror.defineDocExtension = function(name, func) {
+ Doc.prototype[name] = func;
+ };
+ CodeMirror.defineOption = option;
+
+ var initHooks = [];
+ CodeMirror.defineInitHook = function(f) {initHooks.push(f);};
+
+ var helpers = CodeMirror.helpers = {};
+ CodeMirror.registerHelper = function(type, name, value) {
+ if (!helpers.hasOwnProperty(type)) helpers[type] = CodeMirror[type] = {_global: []};
+ helpers[type][name] = value;
+ };
+ CodeMirror.registerGlobalHelper = function(type, name, predicate, value) {
+ CodeMirror.registerHelper(type, name, value);
+ helpers[type]._global.push({pred: predicate, val: value});
+ };
+
+ // MODE STATE HANDLING
+
+ // 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 = {};
+ for (var n in state) {
+ var val = state[n];
+ if (val instanceof Array) val = val.concat([]);
+ nstate[n] = val;
+ }
+ return nstate;
+ };
+
+ var startState = CodeMirror.startState = function(mode, a1, a2) {
+ return mode.startState ? mode.startState(a1, a2) : true;
+ };
+
+ // 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);
+ if (!info || info.mode == mode) break;
+ state = info.state;
+ mode = info.mode;
+ }
+ return info || {mode: mode, state: state};
+ };
+
+ // 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()), sel_dontScroll);},
+ singleSelection: function(cm) {
+ cm.setSelection(cm.getCursor("anchor"), cm.getCursor("head"), sel_dontScroll);
+ },
+ killLine: function(cm) {
+ 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) {
+ deleteNearSelection(cm, function(range) {
+ return {from: Pos(range.from().line, 0),
+ to: clipPos(cm.doc, Pos(range.to().line + 1, 0))};
+ });
+ },
+ delLineLeft: function(cm) {
+ deleteNearSelection(cm, function(range) {
+ return {from: Pos(range.from().line, 0), to: range.from()};
+ });
+ },
+ delWrappedLineLeft: function(cm) {
+ deleteNearSelection(cm, function(range) {
+ var top = cm.charCoords(range.head, "div").top + 5;
+ var leftPos = cm.coordsChar({left: 0, top: top}, "div");
+ return {from: leftPos, to: range.from()};
+ });
+ },
+ delWrappedLineRight: function(cm) {
+ deleteNearSelection(cm, function(range) {
+ var top = cm.charCoords(range.head, "div").top + 5;
+ var rightPos = cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div");
+ return {from: range.from(), to: rightPos };
+ });
+ },
+ 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.extendSelectionsBy(function(range) { return lineStart(cm, range.head.line); },
+ {origin: "+move", bias: 1});
+ },
+ goLineStartSmart: function(cm) {
+ cm.extendSelectionsBy(function(range) {
+ return lineStartSmart(cm, range.head);
+ }, {origin: "+move", bias: 1});
+ },
+ goLineEnd: function(cm) {
+ cm.extendSelectionsBy(function(range) { return lineEnd(cm, range.head.line); },
+ {origin: "+move", bias: -1});
+ },
+ goLineRight: function(cm) {
+ 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) {
+ cm.extendSelectionsBy(function(range) {
+ var top = cm.charCoords(range.head, "div").top + 5;
+ return cm.coordsChar({left: 0, top: top}, "div");
+ }, sel_move);
+ },
+ goLineLeftSmart: function(cm) {
+ cm.extendSelectionsBy(function(range) {
+ var top = cm.charCoords(range.head, "div").top + 5;
+ var pos = cm.coordsChar({left: 0, top: top}, "div");
+ if (pos.ch < cm.getLine(pos.line).search(/\S/)) return lineStartSmart(cm, range.head);
+ return pos;
+ }, sel_move);
+ },
+ goLineUp: function(cm) {cm.moveV(-1, "line");},
+ goLineDown: function(cm) {cm.moveV(1, "line");},
+ goPageUp: function(cm) {cm.moveV(-1, "page");},
+ goPageDown: function(cm) {cm.moveV(1, "page");},
+ goCharLeft: function(cm) {cm.moveH(-1, "char");},
+ goCharRight: function(cm) {cm.moveH(1, "char");},
+ goColumnLeft: function(cm) {cm.moveH(-1, "column");},
+ goColumnRight: function(cm) {cm.moveH(1, "column");},
+ goWordLeft: function(cm) {cm.moveH(-1, "word");},
+ goGroupRight: function(cm) {cm.moveH(1, "group");},
+ goGroupLeft: function(cm) {cm.moveH(-1, "group");},
+ goWordRight: function(cm) {cm.moveH(1, "word");},
+ delCharBefore: function(cm) {cm.deleteH(-1, "char");},
+ delCharAfter: function(cm) {cm.deleteH(1, "char");},
+ delWordBefore: function(cm) {cm.deleteH(-1, "word");},
+ delWordAfter: function(cm) {cm.deleteH(1, "word");},
+ delGroupBefore: function(cm) {cm.deleteH(-1, "group");},
+ delGroupAfter: function(cm) {cm.deleteH(1, "group");},
+ indentAuto: function(cm) {cm.indentSelection("smart");},
+ indentMore: function(cm) {cm.indentSelection("add");},
+ indentLess: function(cm) {cm.indentSelection("subtract");},
+ insertTab: function(cm) {cm.replaceSelection("\t");},
+ insertSoftTab: function(cm) {
+ var spaces = [], ranges = cm.listSelections(), tabSize = cm.options.tabSize;
+ for (var i = 0; i < ranges.length; i++) {
+ var pos = ranges[i].from();
+ var col = countColumn(cm.getLine(pos.line), pos.ch, tabSize);
+ spaces.push(spaceStr(tabSize - col % tabSize));
+ }
+ cm.replaceSelections(spaces);
+ },
+ defaultTab: function(cm) {
+ if (cm.somethingSelected()) cm.indentSelection("add");
+ else cm.execCommand("insertTab");
+ },
+ transposeChars: function(cm) {
+ runInOp(cm, function() {
+ var ranges = cm.listSelections(), newSel = [];
+ for (var i = 0; i < ranges.length; i++) {
+ var cur = ranges[i].head, line = getLine(cm.doc, cur.line).text;
+ if (line) {
+ if (cur.ch == line.length) cur = new Pos(cur.line, cur.ch - 1);
+ if (cur.ch > 0) {
+ cur = new Pos(cur.line, cur.ch + 1);
+ cm.replaceRange(line.charAt(cur.ch - 1) + line.charAt(cur.ch - 2),
+ Pos(cur.line, cur.ch - 2), cur, "+transpose");
+ } else if (cur.line > cm.doc.first) {
+ var prev = getLine(cm.doc, cur.line - 1).text;
+ if (prev)
+ cm.replaceRange(line.charAt(0) + cm.doc.lineSeparator() +
+ prev.charAt(prev.length - 1),
+ Pos(cur.line - 1, prev.length - 1), Pos(cur.line, 1), "+transpose");
+ }
+ }
+ newSel.push(new Range(cur, cur));
+ }
+ cm.setSelections(newSel);
+ });
+ },
+ newlineAndIndent: function(cm) {
+ runInOp(cm, function() {
+ var len = cm.listSelections().length;
+ for (var i = 0; i < len; i++) {
+ var range = cm.listSelections()[i];
+ cm.replaceRange(cm.doc.lineSeparator(), range.anchor, range.head, "+input");
+ cm.indentLine(range.from().line + 1, null, true);
+ }
+ ensureCursorVisible(cm);
+ });
+ },
+ openLine: function(cm) {cm.replaceSelection("\n", "start")},
+ toggleOverwrite: function(cm) {cm.toggleOverwrite();}
+ };
+
+
+ // STANDARD KEYMAPS
+
+ var keyMap = CodeMirror.keyMap = {};
+
+ keyMap.basic = {
+ "Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown",
+ "End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown",
+ "Delete": "delCharAfter", "Backspace": "delCharBefore", "Shift-Backspace": "delCharBefore",
+ "Tab": "defaultTab", "Shift-Tab": "indentAuto",
+ "Enter": "newlineAndIndent", "Insert": "toggleOverwrite",
+ "Esc": "singleSelection"
+ };
+ // Note that the save and find-related commands aren't defined by
+ // 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", "Ctrl-End": "goDocEnd", "Ctrl-Up": "goLineUp", "Ctrl-Down": "goLineDown",
+ "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"
+ };
+ // 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",
+ "Ctrl-O": "openLine"
+ };
+ keyMap.macDefault = {
+ "Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo",
+ "Cmd-Home": "goDocStart", "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goGroupLeft",
+ "Alt-Right": "goGroupRight", "Cmd-Left": "goLineLeft", "Cmd-Right": "goLineRight", "Alt-Backspace": "delGroupBefore",
+ "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": "delWrappedLineLeft", "Cmd-Delete": "delWrappedLineRight",
+ "Cmd-U": "undoSelection", "Shift-Cmd-U": "redoSelection", "Ctrl-Up": "goDocStart", "Ctrl-Down": "goDocEnd",
+ fallthrough: ["basic", "emacsy"]
+ };
+ keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault;
+
+ // KEYMAP DISPATCH
+
+ function normalizeKeyName(name) {
+ var parts = name.split(/-(?!$)/), name = parts[parts.length - 1];
+ var alt, ctrl, shift, cmd;
+ for (var i = 0; i < parts.length - 1; i++) {
+ var mod = parts[i];
+ if (/^(cmd|meta|m)$/i.test(mod)) cmd = true;
+ else if (/^a(lt)?$/i.test(mod)) alt = true;
+ else if (/^(c|ctrl|control)$/i.test(mod)) ctrl = true;
+ else if (/^s(hift)$/i.test(mod)) shift = true;
+ else throw new Error("Unrecognized modifier name: " + mod);
+ }
+ if (alt) name = "Alt-" + name;
+ if (ctrl) name = "Ctrl-" + name;
+ if (cmd) name = "Cmd-" + name;
+ if (shift) name = "Shift-" + name;
+ return name;
+ }
+
+ // This is a kludge to keep keymaps mostly working as raw objects
+ // (backwards compatibility) while at the same time support features
+ // like normalization and multi-stroke key bindings. It compiles a
+ // new normalized keymap, and then updates the old object to reflect
+ // this.
+ CodeMirror.normalizeKeyMap = function(keymap) {
+ var copy = {};
+ for (var keyname in keymap) if (keymap.hasOwnProperty(keyname)) {
+ var value = keymap[keyname];
+ if (/^(name|fallthrough|(de|at)tach)$/.test(keyname)) continue;
+ if (value == "...") { delete keymap[keyname]; continue; }
+
+ var keys = map(keyname.split(" "), normalizeKeyName);
+ for (var i = 0; i < keys.length; i++) {
+ var val, name;
+ if (i == keys.length - 1) {
+ name = keys.join(" ");
+ val = value;
+ } else {
+ name = keys.slice(0, i + 1).join(" ");
+ val = "...";
+ }
+ var prev = copy[name];
+ if (!prev) copy[name] = val;
+ else if (prev != val) throw new Error("Inconsistent bindings for " + name);
+ }
+ delete keymap[keyname];
+ }
+ for (var prop in copy) keymap[prop] = copy[prop];
+ return keymap;
+ };
+
+ var lookupKey = CodeMirror.lookupKey = function(key, map, handle, context) {
+ map = getKeyMap(map);
+ var found = map.call ? map.call(key, context) : map[key];
+ if (found === false) return "nothing";
+ if (found === "...") return "multi";
+ if (found != null && handle(found)) return "handled";
+
+ if (map.fallthrough) {
+ if (Object.prototype.toString.call(map.fallthrough) != "[object Array]")
+ return lookupKey(key, map.fallthrough, handle, context);
+ for (var i = 0; i < map.fallthrough.length; i++) {
+ var result = lookupKey(key, map.fallthrough[i], handle, context);
+ if (result) return result;
+ }
+ }
+ };
+
+ // Modifier key presses don't count as 'real' key presses for the
+ // purpose of keymap fallthrough.
+ var isModifierKey = CodeMirror.isModifierKey = function(value) {
+ var name = typeof value == "string" ? value : keyNames[value.keyCode];
+ return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod";
+ };
+
+ // 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 base = keyNames[event.keyCode], name = base;
+ if (name == null || event.altGraphKey) return false;
+ if (event.altKey && base != "Alt") name = "Alt-" + name;
+ if ((flipCtrlCmd ? event.metaKey : event.ctrlKey) && base != "Ctrl") name = "Ctrl-" + name;
+ if ((flipCtrlCmd ? event.ctrlKey : event.metaKey) && base != "Cmd") name = "Cmd-" + name;
+ if (!noShift && event.shiftKey && base != "Shift") name = "Shift-" + name;
+ return name;
+ };
+
+ function getKeyMap(val) {
+ return typeof val == "string" ? keyMap[val] : val;
+ }
+
+ // FROMTEXTAREA
+
+ CodeMirror.fromTextArea = function(textarea, options) {
+ options = options ? copyObj(options) : {};
+ options.value = textarea.value;
+ if (!options.tabindex && textarea.tabIndex)
+ options.tabindex = textarea.tabIndex;
+ if (!options.placeholder && textarea.placeholder)
+ options.placeholder = textarea.placeholder;
+ // 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 = activeElt();
+ options.autofocus = hasFocus == textarea ||
+ textarea.getAttribute("autofocus") != null && hasFocus == document.body;
+ }
+
+ function save() {textarea.value = cm.getValue();}
+ if (textarea.form) {
+ on(textarea.form, "submit", save);
+ // Deplorable hack to make the submit method do the right thing.
+ if (!options.leaveSubmitMethodAlone) {
+ var form = textarea.form, realSubmit = form.submit;
+ try {
+ var wrappedSubmit = form.submit = function() {
+ save();
+ form.submit = realSubmit;
+ form.submit();
+ form.submit = wrappedSubmit;
+ };
+ } catch(e) {}
+ }
+ }
+
+ options.finishInit = function(cm) {
+ cm.save = save;
+ cm.getTextArea = function() { return textarea; };
+ cm.toTextArea = function() {
+ cm.toTextArea = isNaN; // Prevent this from being ran twice
+ save();
+ textarea.parentNode.removeChild(cm.getWrapperElement());
+ textarea.style.display = "";
+ if (textarea.form) {
+ off(textarea.form, "submit", save);
+ if (typeof textarea.form.submit == "function")
+ textarea.form.submit = realSubmit;
+ }
+ };
+ };
+
+ textarea.style.display = "none";
+ var cm = CodeMirror(function(node) {
+ textarea.parentNode.insertBefore(node, textarea.nextSibling);
+ }, options);
+ return cm;
+ };
+
+ // STRING STREAM
+
+ // Fed to the mode parsers, provides helper functions to make
+ // parsers more succinct.
+
+ 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;},
+ sol: function() {return this.pos == this.lineStart;},
+ peek: function() {return this.string.charAt(this.pos) || undefined;},
+ next: function() {
+ if (this.pos < this.string.length)
+ return this.string.charAt(this.pos++);
+ },
+ eat: function(match) {
+ var ch = this.string.charAt(this.pos);
+ if (typeof match == "string") var ok = ch == match;
+ else var ok = ch && (match.test ? match.test(ch) : match(ch));
+ if (ok) {++this.pos; return ch;}
+ },
+ eatWhile: function(match) {
+ var start = this.pos;
+ while (this.eat(match)){}
+ return this.pos > start;
+ },
+ eatSpace: function() {
+ var start = this.pos;
+ while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) ++this.pos;
+ return this.pos > start;
+ },
+ skipToEnd: function() {this.pos = this.string.length;},
+ skipTo: function(ch) {
+ var found = this.string.indexOf(ch, this.pos);
+ if (found > -1) {this.pos = found; return true;}
+ },
+ backUp: function(n) {this.pos -= n;},
+ column: function() {
+ if (this.lastColumnPos < this.start) {
+ this.lastColumnValue = countColumn(this.string, this.start, this.tabSize, this.lastColumnPos, this.lastColumnValue);
+ this.lastColumnPos = this.start;
+ }
+ return this.lastColumnValue - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0);
+ },
+ indentation: function() {
+ return countColumn(this.string, null, this.tabSize) -
+ (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0);
+ },
+ match: function(pattern, consume, caseInsensitive) {
+ if (typeof pattern == "string") {
+ var cased = function(str) {return caseInsensitive ? str.toLowerCase() : str;};
+ var substr = this.string.substr(this.pos, pattern.length);
+ if (cased(substr) == cased(pattern)) {
+ if (consume !== false) this.pos += pattern.length;
+ return true;
+ }
+ } else {
+ var match = this.string.slice(this.pos).match(pattern);
+ if (match && match.index > 0) return null;
+ if (match && consume !== false) this.pos += match[0].length;
+ return match;
+ }
+ },
+ current: function(){return this.string.slice(this.start, this.pos);},
+ hideFirstChars: function(n, inner) {
+ this.lineStart += n;
+ try { return inner(); }
+ finally { this.lineStart -= n; }
+ }
+ };
+
+ // TEXTMARKERS
+
+ // 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 nextMarkerId = 0;
+
+ var TextMarker = CodeMirror.TextMarker = function(doc, type) {
+ this.lines = [];
+ this.type = type;
+ this.doc = doc;
+ this.id = ++nextMarkerId;
+ };
+ eventMixin(TextMarker);
+
+ // Clear the marker.
+ TextMarker.prototype.clear = function() {
+ if (this.explicitlyCleared) return;
+ var cm = this.doc.cm, withOp = cm && !cm.curOp;
+ if (withOp) startOperation(cm);
+ if (hasHandler(this, "clear")) {
+ var found = this.find();
+ if (found) signalLater(this, "clear", found.from, found.to);
+ }
+ var min = null, max = null;
+ for (var i = 0; i < this.lines.length; ++i) {
+ var line = this.lines[i];
+ var span = getMarkedSpanFor(line.markedSpans, this);
+ 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 && 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(this.lines[i]), len = lineLength(visual);
+ if (len > cm.display.maxLineLength) {
+ cm.display.maxLine = visual;
+ cm.display.maxLineLength = len;
+ cm.display.maxLineChanged = true;
+ }
+ }
+
+ 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.doc);
+ }
+ if (cm) signalLater(cm, "markerCleared", cm, this);
+ if (withOp) endOperation(cm);
+ if (this.parent) this.parent.clear();
+ };
+
+ // 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) {
+ 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;
+ }
+ }
+ 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(-1, true), widget = this, cm = this.doc.cm;
+ if (!pos || !cm) return;
+ 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;
+ }
+ 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) {
+ if (!this.lines.length && this.doc.cm) {
+ var op = this.doc.cm.curOp;
+ if (!op.maybeHiddenMarkers || indexOf(op.maybeHiddenMarkers, this) == -1)
+ (op.maybeUnhiddenMarkers || (op.maybeUnhiddenMarkers = [])).push(this);
+ }
+ this.lines.push(line);
+ };
+ TextMarker.prototype.detachLine = function(line) {
+ this.lines.splice(indexOf(this.lines, line), 1);
+ if (!this.lines.length && this.doc.cm) {
+ var op = this.doc.cm.curOp;
+ (op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this);
+ }
+ };
+
+ // 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), diff = cmp(from, to);
+ if (options) copyObj(options, marker, 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.widgetNode = elt("span", [marker.replacedWith], "CodeMirror-widget");
+ if (!options.handleMouseEvents) marker.widgetNode.setAttribute("cm-ignore-events", "true");
+ if (options.insertLeft) marker.widgetNode.insertLeft = true;
+ }
+ if (marker.collapsed) {
+ if (conflictingCollapsedRange(doc, from.line, from, to, marker) ||
+ from.line != to.line && conflictingCollapsedRange(doc, to.line, from, to, marker))
+ throw new Error("Inserting collapsed marker partially overlapping an existing one");
+ sawCollapsedSpans = true;
+ }
+
+ if (marker.addToHistory)
+ 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(line) == cm.display.maxLine)
+ updateMaxLine = true;
+ if (marker.collapsed && curLine != from.line) updateLineHeight(line, 0);
+ 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);
+ });
+
+ if (marker.clearOnEnter) on(marker, "beforeCursorEnter", function() { marker.clear(); });
+
+ if (marker.readOnly) {
+ sawReadOnlySpans = true;
+ if (doc.history.done.length || doc.history.undone.length)
+ doc.clearHistory();
+ }
+ if (marker.collapsed) {
+ marker.id = ++nextMarkerId;
+ marker.atomic = true;
+ }
+ if (cm) {
+ // Sync editor state
+ if (updateMaxLine) cm.curOp.updateMaxLine = true;
+ if (marker.collapsed)
+ regChange(cm, from.line, to.line + 1);
+ else if (marker.className || marker.title || marker.startStyle || marker.endStyle || marker.css)
+ 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
+
+ // 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; i < markers.length; ++i)
+ markers[i].parent = this;
+ };
+ eventMixin(SharedTextMarker);
+
+ SharedTextMarker.prototype.clear = function() {
+ if (this.explicitlyCleared) return;
+ this.explicitlyCleared = true;
+ for (var i = 0; i < this.markers.length; ++i)
+ this.markers[i].clear();
+ signalLater(this, "clear");
+ };
+ 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.widgetNode;
+ linkedDocs(doc, function(doc) {
+ 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;
+ primary = lst(markers);
+ });
+ return new SharedTextMarker(markers, primary);
+ }
+
+ function findSharedMarkers(doc) {
+ return doc.findMarks(Pos(doc.first, 0), doc.clipPos(Pos(doc.lastLine())),
+ function(m) { return m.parent; });
+ }
+
+ function copySharedMarkers(doc, markers) {
+ for (var i = 0; i < markers.length; i++) {
+ var marker = markers[i], pos = marker.find();
+ var mFrom = doc.clipPos(pos.from), mTo = doc.clipPos(pos.to);
+ if (cmp(mFrom, mTo)) {
+ var subMark = markText(doc, mFrom, mTo, marker.primary, marker.primary.type);
+ marker.markers.push(subMark);
+ subMark.parent = marker;
+ }
+ }
+ }
+
+ function detachSharedMarkers(markers) {
+ for (var i = 0; i < markers.length; i++) {
+ var marker = markers[i], linked = [marker.primary.doc];;
+ linkedDocs(marker.primary.doc, function(d) { linked.push(d); });
+ for (var j = 0; j < marker.markers.length; j++) {
+ var subMarker = marker.markers[j];
+ if (indexOf(linked, subMarker.doc) == -1) {
+ subMarker.parent = null;
+ marker.markers.splice(j--, 1);
+ }
+ }
+ }
+ }
+
+ // 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(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(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) {
+ if (change.full) return null;
+ 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 = 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);
+
+ // Next, merge those two ends
+ var sameLine = change.text.length == 1, offset = lst(change.text).length + (sameLine ? startCh : 0);
+ if (first) {
+ // Fix up .to properties of first
+ for (var i = 0; i < first.length; ++i) {
+ var span = first[i];
+ if (span.to == null) {
+ var found = getMarkedSpanFor(last, span.marker);
+ if (!found) span.to = startCh;
+ else if (sameLine) span.to = found.to == null ? null : found.to + offset;
+ }
+ }
+ }
+ if (last) {
+ // Fix up .from in last (or move them into first in case of sameLine)
+ for (var i = 0; i < last.length; ++i) {
+ var span = last[i];
+ if (span.to != null) span.to += offset;
+ if (span.from == null) {
+ var found = getMarkedSpanFor(first, span.marker);
+ if (!found) {
+ span.from = offset;
+ if (sameLine) (first || (first = [])).push(span);
+ }
+ } else {
+ span.from += offset;
+ if (sameLine) (first || (first = [])).push(span);
+ }
+ }
+ }
+ // Make sure we didn't create any zero-length spans
+ if (first) first = clearEmptySpans(first);
+ if (last && last != first) last = clearEmptySpans(last);
+
+ var newMarkers = [first];
+ if (!sameLine) {
+ // Fill gap with whole-line-spans
+ var gap = change.text.length - 2, gapMarkers;
+ if (gap > 0 && first)
+ for (var i = 0; i < first.length; ++i)
+ if (first[i].to == null)
+ (gapMarkers || (gapMarkers = [])).push(new MarkedSpan(first[i].marker, null, null));
+ for (var i = 0; i < gap; ++i)
+ newMarkers.push(gapMarkers);
+ newMarkers.push(last);
+ }
+ 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];
+ if (span.from != null && span.from == span.to && span.marker.clearWhenEmpty !== false)
+ spans.splice(i--, 1);
+ }
+ if (!spans.length) return null;
+ 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);
+ if (!old) return stretched;
+ if (!stretched) return old;
+
+ for (var i = 0; i < old.length; ++i) {
+ var oldCur = old[i], stretchCur = stretched[i];
+ if (oldCur && stretchCur) {
+ spans: for (var j = 0; j < stretchCur.length; ++j) {
+ var span = stretchCur[j];
+ for (var k = 0; k < oldCur.length; ++k)
+ if (oldCur[k].marker == span.marker) continue spans;
+ oldCur.push(span);
+ }
+ } else if (stretchCur) {
+ old[i] = stretchCur;
+ }
+ }
+ 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) {
+ if (line.markedSpans) for (var i = 0; i < line.markedSpans.length; ++i) {
+ var mark = line.markedSpans[i].marker;
+ if (mark.readOnly && (!markers || indexOf(markers, mark) == -1))
+ (markers || (markers = [])).push(mark);
+ }
+ });
+ 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(0);
+ for (var j = 0; j < parts.length; ++j) {
+ var p = parts[j];
+ 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 (dto > 0 || !mk.inclusiveRight && !dto)
+ newParts.push({from: m.to, to: p.to});
+ parts.splice.apply(parts, newParts);
+ j += newParts.length - 1;
+ }
+ }
+ 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;
+ var aPos = a.find(), bPos = b.find();
+ var fromCmp = cmp(aPos.from, bPos.from) || extraLeft(a) - extraLeft(b);
+ if (fromCmp) return -fromCmp;
+ var toCmp = cmp(aPos.to, bPos.to) || extraRight(a) - extraRight(b);
+ if (toCmp) return toCmp;
+ 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) {
+ sp = sps[i];
+ if (sp.marker.collapsed && (start ? sp.from : sp.to) == null &&
+ (!found || compareCollapsedMarkers(found, sp.marker) < 0))
+ found = sp.marker;
+ }
+ return found;
+ }
+ 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(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;
+ if (fromCmp <= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.to, from) >= 0 : cmp(found.to, from) > 0) ||
+ fromCmp >= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.from, to) <= 0 : cmp(found.from, to) < 0))
+ return true;
+ }
+ }
+
+ // 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 = 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.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(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.widgetNode && sp.from == span.to &&
+ (sp.to == null || sp.to != span.from) &&
+ (sp.marker.inclusiveLeft || span.marker.inclusiveRight) &&
+ lineIsHiddenInner(doc, line, sp)) return true;
+ }
+ }
+
+ // LINE WIDGETS
+
+ // Line widgets are block elements displayed above or below a line.
+
+ var LineWidget = CodeMirror.LineWidget = function(doc, node, options) {
+ if (options) for (var opt in options) if (options.hasOwnProperty(opt))
+ this[opt] = options[opt];
+ this.doc = doc;
+ this.node = node;
+ };
+ eventMixin(LineWidget);
+
+ function adjustScrollWhenAboveVisible(cm, line, diff) {
+ if (heightAtLine(line) < ((cm.curOp && cm.curOp.scrollTop) || cm.doc.scrollTop))
+ addToScrollPos(cm, null, diff);
+ }
+
+ LineWidget.prototype.clear = function() {
+ var cm = this.doc.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) line.widgets = null;
+ var height = widgetHeight(this);
+ updateLineHeight(line, Math.max(0, line.height - height));
+ if (cm) runInOp(cm, function() {
+ adjustScrollWhenAboveVisible(cm, line, -height);
+ regLineChange(cm, no, "widget");
+ });
+ };
+ LineWidget.prototype.changed = function() {
+ var oldH = this.height, cm = this.doc.cm, line = this.line;
+ this.height = null;
+ var diff = widgetHeight(this) - oldH;
+ if (!diff) return;
+ updateLineHeight(line, line.height + diff);
+ if (cm) runInOp(cm, function() {
+ cm.curOp.forceUpdate = true;
+ adjustScrollWhenAboveVisible(cm, line, diff);
+ });
+ };
+
+ function widgetHeight(widget) {
+ if (widget.height != null) return widget.height;
+ var cm = widget.doc.cm;
+ if (!cm) return 0;
+ if (!contains(document.body, widget.node)) {
+ var parentStyle = "position: relative;";
+ if (widget.coverGutter)
+ parentStyle += "margin-left: -" + cm.display.gutters.offsetWidth + "px;";
+ if (widget.noHScroll)
+ parentStyle += "width: " + cm.display.wrapper.clientWidth + "px;";
+ removeChildrenAndAdd(cm.display.measure, elt("div", [widget.node], null, parentStyle));
+ }
+ return widget.height = widget.node.parentNode.offsetHeight;
+ }
+
+ function addLineWidget(doc, handle, node, options) {
+ var widget = new LineWidget(doc, node, options);
+ var cm = doc.cm;
+ if (cm && widget.noHScroll) cm.display.alignWidgets = true;
+ changeLine(doc, 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 (cm && !lineIsHidden(doc, line)) {
+ var aboveVisible = heightAtLine(line) < doc.scrollTop;
+ updateLineHeight(line, line.height + widgetHeight(widget));
+ if (aboveVisible) addToScrollPos(cm, null, widget.height);
+ cm.curOp.forceUpdate = true;
+ }
+ return true;
+ });
+ return widget;
+ }
+
+ // LINE DATA STRUCTURE
+
+ // Line objects. These hold state related to a line, including
+ // highlighting info (the styles array).
+ var Line = CodeMirror.Line = function(text, markedSpans, estimateHeight) {
+ this.text = text;
+ attachMarkedSpans(this, markedSpans);
+ this.height = estimateHeight ? estimateHeight(this) : 1;
+ };
+ 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;
+ if (line.styles) line.styles = null;
+ if (line.order != null) line.order = null;
+ detachMarkedSpans(line);
+ attachMarkedSpans(line, markedSpans);
+ var estHeight = estimateHeight ? estimateHeight(line) : 1;
+ 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);
+ }
+
+ function extractLineClasses(type, output) {
+ if (type) for (;;) {
+ var lineClass = type.match(/(?:^|\s+)line-(background-)?(\S+)/);
+ if (!lineClass) break;
+ type = type.slice(0, lineClass.index) + type.slice(lineClass.index + lineClass[0].length);
+ var prop = lineClass[1] ? "bgClass" : "textClass";
+ if (output[prop] == null)
+ output[prop] = lineClass[2];
+ else if (!(new RegExp("(?:^|\s)" + lineClass[2] + "(?:$|\s)")).test(output[prop]))
+ output[prop] += " " + lineClass[2];
+ }
+ return type;
+ }
+
+ function callBlankLine(mode, state) {
+ if (mode.blankLine) return mode.blankLine(state);
+ if (!mode.innerMode) return;
+ var inner = CodeMirror.innerMode(mode, state);
+ if (inner.mode.blankLine) return inner.mode.blankLine(inner.state);
+ }
+
+ function readToken(mode, stream, state, inner) {
+ for (var i = 0; i < 10; i++) {
+ if (inner) inner[0] = CodeMirror.innerMode(mode, state).mode;
+ var style = mode.token(stream, state);
+ if (stream.pos > stream.start) return style;
+ }
+ throw new Error("Mode " + mode.name + " failed to advance stream.");
+ }
+
+ // Utility for getTokenAt and getLineTokens
+ function takeToken(cm, pos, precise, asArray) {
+ function getObj(copy) {
+ return {start: stream.start, end: stream.pos,
+ string: stream.current(),
+ type: style || null,
+ state: copy ? copyState(doc.mode, state) : state};
+ }
+
+ var doc = cm.doc, mode = doc.mode, style;
+ pos = clipPos(doc, pos);
+ var line = getLine(doc, pos.line), state = getStateBefore(cm, pos.line, precise);
+ var stream = new StringStream(line.text, cm.options.tabSize), tokens;
+ if (asArray) tokens = [];
+ while ((asArray || stream.pos < pos.ch) && !stream.eol()) {
+ stream.start = stream.pos;
+ style = readToken(mode, stream, state);
+ if (asArray) tokens.push(getObj(true));
+ }
+ return asArray ? tokens : getObj();
+ }
+
+ // Run the given mode's parser over a line, calling f for each token.
+ function runMode(cm, text, mode, state, f, lineClasses, forceToEnd) {
+ var flattenSpans = mode.flattenSpans;
+ if (flattenSpans == null) flattenSpans = cm.options.flattenSpans;
+ var curStart = 0, curStyle = null;
+ var stream = new StringStream(text, cm.options.tabSize), style;
+ var inner = cm.options.addModeClass && [null];
+ if (text == "") extractLineClasses(callBlankLine(mode, state), lineClasses);
+ while (!stream.eol()) {
+ if (stream.pos > cm.options.maxHighlightLength) {
+ flattenSpans = false;
+ if (forceToEnd) processLine(cm, text, state, stream.pos);
+ stream.pos = text.length;
+ style = null;
+ } else {
+ style = extractLineClasses(readToken(mode, stream, state, inner), lineClasses);
+ }
+ if (inner) {
+ var mName = inner[0].name;
+ if (mName) style = "m-" + (style ? mName + " " + style : mName);
+ }
+ if (!flattenSpans || curStyle != style) {
+ while (curStart < stream.start) {
+ curStart = Math.min(stream.start, curStart + 50000);
+ f(curStart, curStyle);
+ }
+ curStyle = style;
+ }
+ stream.start = stream.pos;
+ }
+ while (curStart < stream.pos) {
+ // Webkit seems to refuse to render text nodes longer than 57444 characters
+ var pos = Math.min(stream.pos, curStart + 50000);
+ f(pos, curStyle);
+ curStart = pos;
+ }
+ }
+
+ // 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).
+ var st = [cm.state.modeGen], lineClasses = {};
+ // Compute the base array of styles
+ runMode(cm, line.text, cm.doc.mode, state, function(end, style) {
+ st.push(end, style);
+ }, lineClasses, forceToEnd);
+
+ // Run overlays, adjust style array.
+ for (var o = 0; o < cm.state.overlays.length; ++o) {
+ var overlay = cm.state.overlays[o], i = 1, at = 0;
+ runMode(cm, line.text, overlay.mode, true, function(end, style) {
+ var start = i;
+ // Ensure there's a token end at the current position, and that i points at it
+ while (at < end) {
+ var i_end = st[i];
+ if (i_end > end)
+ st.splice(i, 1, end, st[i+1], i_end);
+ i += 2;
+ at = Math.min(end, i_end);
+ }
+ if (!style) return;
+ if (overlay.opaque) {
+ st.splice(start, i - start, end, "cm-overlay " + style);
+ i = start + 2;
+ } else {
+ for (; start < i; start += 2) {
+ var cur = st[start+1];
+ st[start+1] = (cur ? cur + " " : "") + "cm-overlay " + style;
+ }
+ }
+ }, lineClasses);
+ }
+
+ return {styles: st, classes: lineClasses.bgClass || lineClasses.textClass ? lineClasses : null};
+ }
+
+ function getLineStyles(cm, line, updateFrontier) {
+ if (!line.styles || line.styles[0] != cm.state.modeGen) {
+ var state = getStateBefore(cm, lineNo(line));
+ var result = highlightLine(cm, line, line.text.length > cm.options.maxHighlightLength ? copyState(cm.doc.mode, state) : state);
+ line.stateAfter = state;
+ line.styles = result.styles;
+ if (result.classes) line.styleClasses = result.classes;
+ else if (line.styleClasses) line.styleClasses = null;
+ if (updateFrontier === cm.doc.frontier) cm.doc.frontier++;
+ }
+ return line.styles;
+ }
+
+ // Lightweight form of highlight -- proceed over this line and
+ // 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);
+ stream.start = stream.pos = startAt || 0;
+ if (text == "") callBlankLine(mode, state);
+ while (!stream.eol()) {
+ readToken(mode, stream, state);
+ stream.start = stream.pos;
+ }
+ }
+
+ // 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, options) {
+ if (!style || /^\s*$/.test(style)) return null;
+ var cache = options.addModeClass ? styleToClassCacheWithMode : styleToClassCache;
+ return cache[style] ||
+ (cache[style] = style.replace(/\S+/g, "cm-$&"));
+ }
+
+ // 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], "CodeMirror-line"), content: content,
+ col: 0, pos: 0, cm: cm,
+ splitSpaces: (ie || webkit) && cm.getOption("lineWrapping")};
+ lineView.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 = buildToken;
+ // Optionally wire in some hacks into the token-rendering
+ // algorithm, to deal with browser quirks.
+ if (hasBadBidiRects(cm.display.measure) && (order = getOrder(line)))
+ builder.addToken = buildTokenBadBidi(builder.addToken, order);
+ builder.map = [];
+ var allowFrontierUpdate = lineView != cm.display.externalMeasured && lineNo(line);
+ insertLineContent(line, builder, getLineStyles(cm, line, allowFrontierUpdate));
+ if (line.styleClasses) {
+ if (line.styleClasses.bgClass)
+ builder.bgClass = joinClasses(line.styleClasses.bgClass, builder.bgClass || "");
+ if (line.styleClasses.textClass)
+ builder.textClass = joinClasses(line.styleClasses.textClass, builder.textClass || "");
+ }
+
+ // 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)));
+
+ // 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({});
+ }
+ }
+
+ // See issue #2901
+ if (webkit) {
+ var last = builder.content.lastChild
+ if (/\bcm-tab\b/.test(last.className) || (last.querySelector && last.querySelector(".cm-tab")))
+ builder.content.className = "cm-tab-wrap-hack";
+ }
+
+ signal(cm, "renderLine", cm, lineView.line, builder.pre);
+ if (builder.pre.className)
+ builder.textClass = joinClasses(builder.pre.className, builder.textClass || "");
+
+ return builder;
+ }
+
+ function defaultSpecialCharPlaceholder(ch) {
+ var token = elt("span", "\u2022", "cm-invalidchar");
+ token.title = "\\u" + ch.charCodeAt(0).toString(16);
+ token.setAttribute("aria-label", token.title);
+ 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, css) {
+ if (!text) return;
+ var displayText = builder.splitSpaces ? text.replace(/ {3,}/g, splitSpaces) : text;
+ var special = builder.cm.state.specialChars, mustWrap = false;
+ if (!special.test(text)) {
+ builder.col += text.length;
+ var content = document.createTextNode(displayText);
+ builder.map.push(builder.pos, builder.pos + text.length, content);
+ if (ie && ie_version < 9) mustWrap = true;
+ builder.pos += text.length;
+ } else {
+ var content = document.createDocumentFragment(), pos = 0;
+ while (true) {
+ special.lastIndex = pos;
+ var m = special.exec(text);
+ var skipped = m ? m.index - pos : text.length - pos;
+ if (skipped) {
+ var txt = document.createTextNode(displayText.slice(pos, pos + skipped));
+ if (ie && ie_version < 9) 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;
+ var txt = content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab"));
+ txt.setAttribute("role", "presentation");
+ txt.setAttribute("cm-text", "\t");
+ builder.col += tabWidth;
+ } else if (m[0] == "\r" || m[0] == "\n") {
+ var txt = content.appendChild(elt("span", m[0] == "\r" ? "\u240d" : "\u2424", "cm-invalidchar"));
+ txt.setAttribute("cm-text", m[0]);
+ builder.col += 1;
+ } else {
+ var txt = builder.cm.options.specialCharPlaceholder(m[0]);
+ txt.setAttribute("cm-text", m[0]);
+ if (ie && ie_version < 9) 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 || mustWrap || css) {
+ var fullStyle = style || "";
+ if (startStyle) fullStyle += startStyle;
+ if (endStyle) fullStyle += endStyle;
+ var token = elt("span", [content], fullStyle, css);
+ if (title) token.title = title;
+ return builder.content.appendChild(token);
+ }
+ builder.content.appendChild(content);
+ }
+
+ function splitSpaces(old) {
+ var out = " ";
+ for (var i = 0; i < old.length - 2; ++i) out += i % 2 ? " " : "\u00a0";
+ out += " ";
+ return out;
+ }
+
+ // 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, css) {
+ 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, css);
+ inner(builder, text.slice(0, part.to - start), style, startStyle, null, title, css);
+ startStyle = null;
+ text = text.slice(part.to - start);
+ start = part.to;
+ }
+ };
+ }
+
+ function buildCollapsedSpan(builder, size, marker, ignoreWidget) {
+ var widget = !ignoreWidget && marker.widgetNode;
+ if (widget) builder.map.push(builder.pos, builder.pos + size, widget);
+ if (!ignoreWidget && builder.cm.display.input.needsContentAttribute) {
+ if (!widget)
+ widget = builder.content.appendChild(document.createElement("span"));
+ widget.setAttribute("cm-marker", marker.id);
+ }
+ if (widget) {
+ builder.cm.display.input.setUneditable(widget);
+ builder.content.appendChild(widget);
+ }
+ builder.pos += size;
+ }
+
+ // Outputs a number of spans to make up a line, taking highlighting
+ // and marked text into account.
+ function insertLineContent(line, builder, styles) {
+ var spans = line.markedSpans, allText = line.text, at = 0;
+ if (!spans) {
+ for (var i = 1; i < styles.length; i+=2)
+ builder.addToken(builder, allText.slice(at, at = styles[i]), interpretTokenStyle(styles[i+1], builder.cm.options));
+ return;
+ }
+
+ var len = allText.length, pos = 0, i = 1, text = "", style, css;
+ var nextChange = 0, spanStyle, spanEndStyle, spanStartStyle, title, collapsed;
+ for (;;) {
+ if (nextChange == pos) { // Update current marker set
+ spanStyle = spanEndStyle = spanStartStyle = title = css = "";
+ collapsed = null; nextChange = Infinity;
+ var foundBookmarks = [], endStyles
+ for (var j = 0; j < spans.length; ++j) {
+ var sp = spans[j], m = sp.marker;
+ if (m.type == "bookmark" && sp.from == pos && m.widgetNode) {
+ foundBookmarks.push(m);
+ } else if (sp.from <= pos && (sp.to == null || sp.to > pos || m.collapsed && sp.to == pos && sp.from == pos)) {
+ if (sp.to != null && sp.to != pos && nextChange > sp.to) {
+ nextChange = sp.to;
+ spanEndStyle = "";
+ }
+ if (m.className) spanStyle += " " + m.className;
+ if (m.css) css = (css ? css + ";" : "") + m.css;
+ if (m.startStyle && sp.from == pos) spanStartStyle += " " + m.startStyle;
+ if (m.endStyle && sp.to == nextChange) (endStyles || (endStyles = [])).push(m.endStyle, sp.to)
+ if (m.title && !title) title = m.title;
+ if (m.collapsed && (!collapsed || compareCollapsedMarkers(collapsed.marker, m) < 0))
+ collapsed = sp;
+ } else if (sp.from > pos && nextChange > sp.from) {
+ nextChange = sp.from;
+ }
+ }
+ if (endStyles) for (var j = 0; j < endStyles.length; j += 2)
+ if (endStyles[j + 1] == nextChange) spanEndStyle += " " + endStyles[j]
+
+ if (!collapsed || collapsed.from == pos) for (var j = 0; j < foundBookmarks.length; ++j)
+ buildCollapsedSpan(builder, 0, foundBookmarks[j]);
+ if (collapsed && (collapsed.from || 0) == pos) {
+ buildCollapsedSpan(builder, (collapsed.to == null ? len + 1 : collapsed.to) - pos,
+ collapsed.marker, collapsed.from == null);
+ if (collapsed.to == null) return;
+ if (collapsed.to == pos) collapsed = false;
+ }
+ }
+ if (pos >= len) break;
+
+ var upto = Math.min(len, nextChange);
+ while (true) {
+ if (text) {
+ var end = pos + text.length;
+ if (!collapsed) {
+ var tokenText = end > upto ? text.slice(0, upto - pos) : text;
+ builder.addToken(builder, tokenText, style ? style + spanStyle : spanStyle,
+ spanStartStyle, pos + tokenText.length == nextChange ? spanEndStyle : "", title, css);
+ }
+ if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;}
+ pos = end;
+ spanStartStyle = "";
+ }
+ text = allText.slice(at, at = styles[i++]);
+ style = interpretTokenStyle(styles[i++], builder.cm.options);
+ }
+ }
+ }
+
+ // DOCUMENT DATA STRUCTURE
+
+ // 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);
+ signalLater(line, "change", line, change);
+ }
+ function linesFor(start, end) {
+ for (var i = start, result = []; i < end; ++i)
+ result.push(new Line(text[i], spansFor(i), estimateHeight));
+ return result;
+ }
+
+ var from = change.from, to = change.to, text = change.text;
+ 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;
+
+ // Adjust the line structure
+ if (change.full) {
+ doc.insert(0, linesFor(0, text.length));
+ doc.remove(text.length, doc.size - text.length);
+ } else if (isWholeLineUpdate(doc, change)) {
+ // This is a whole-line replace. Treated specially to make
+ // sure line objects move the way they are supposed to.
+ var added = linesFor(0, text.length - 1);
+ update(lastLine, lastLine.text, lastSpans);
+ if (nlines) doc.remove(from.line, nlines);
+ if (added.length) doc.insert(from.line, added);
+ } else if (firstLine == lastLine) {
+ if (text.length == 1) {
+ update(firstLine, firstLine.text.slice(0, from.ch) + lastText + firstLine.text.slice(to.ch), lastSpans);
+ } else {
+ var added = linesFor(1, text.length - 1);
+ added.push(new Line(lastText + firstLine.text.slice(to.ch), lastSpans, estimateHeight));
+ update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0));
+ doc.insert(from.line + 1, added);
+ }
+ } else if (text.length == 1) {
+ update(firstLine, firstLine.text.slice(0, from.ch) + text[0] + lastLine.text.slice(to.ch), spansFor(0));
+ doc.remove(from.line + 1, nlines);
+ } else {
+ update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0));
+ update(lastLine, lastText + lastLine.text.slice(to.ch), lastSpans);
+ var added = linesFor(1, text.length - 1);
+ if (nlines > 1) doc.remove(from.line + 1, nlines - 1);
+ doc.insert(from.line + 1, added);
+ }
+
+ signalLater(doc, "change", doc, change);
+ }
+
+ // 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, height = 0; i < lines.length; ++i) {
+ lines[i].parent = this;
+ height += lines[i].height;
+ }
+ this.height = height;
+ }
+
+ 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];
+ this.height -= line.height;
+ cleanUpLine(line);
+ signalLater(line, "delete");
+ }
+ this.lines.splice(at, n);
+ },
+ // Helper used to collapse a small branch into a single leaf.
+ collapse: function(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; 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;
+ }
+ };
+
+ function BranchChunk(children) {
+ this.children = children;
+ var size = 0, height = 0;
+ for (var i = 0; i < children.length; ++i) {
+ var ch = children[i];
+ size += ch.chunkSize(); height += ch.height;
+ ch.parent = this;
+ }
+ this.size = size;
+ this.height = height;
+ this.parent = null;
+ }
+
+ BranchChunk.prototype = {
+ chunkSize: function() { return this.size; },
+ removeInner: function(at, n) {
+ this.size -= n;
+ for (var i = 0; i < this.children.length; ++i) {
+ var child = this.children[i], sz = child.chunkSize();
+ if (at < sz) {
+ var rm = Math.min(n, sz - at), oldHeight = child.height;
+ child.removeInner(at, rm);
+ this.height -= oldHeight - child.height;
+ if (sz == rm) { this.children.splice(i--, 1); child.parent = null; }
+ if ((n -= rm) == 0) break;
+ at = 0;
+ } else at -= sz;
+ }
+ // 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)];
+ this.children[0].parent = this;
+ }
+ },
+ collapse: function(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; i < this.children.length; ++i) {
+ var child = this.children[i], sz = child.chunkSize();
+ if (at <= sz) {
+ child.insertInner(at, lines, height);
+ if (child.lines && child.lines.length > 50) {
+ // To avoid memory thrashing when child.lines is huge (e.g. first view of a large file), it's never spliced.
+ // Instead, small slices are taken. They're taken in order because sequential memory accesses are fastest.
+ var remaining = child.lines.length % 25 + 25
+ for (var pos = remaining; pos < child.lines.length;) {
+ var leaf = new LeafChunk(child.lines.slice(pos, pos += 25));
+ child.height -= leaf.height;
+ this.children.splice(++i, 0, leaf);
+ leaf.parent = this;
+ }
+ child.lines = child.lines.slice(0, remaining);
+ this.maybeSpill();
+ }
+ break;
+ }
+ at -= sz;
+ }
+ },
+ // When a node has grown, check whether it should be split.
+ maybeSpill: function() {
+ if (this.children.length <= 10) return;
+ var me = this;
+ do {
+ var spilled = me.children.splice(me.children.length - 5, 5);
+ var sibling = new BranchChunk(spilled);
+ if (!me.parent) { // Become the parent node
+ var copy = new BranchChunk(me.children);
+ copy.parent = me;
+ me.children = [copy, sibling];
+ me = copy;
+ } else {
+ me.size -= sibling.size;
+ me.height -= sibling.height;
+ var myIndex = indexOf(me.parent.children, me);
+ me.parent.children.splice(myIndex + 1, 0, sibling);
+ }
+ sibling.parent = me.parent;
+ } while (me.children.length > 10);
+ me.parent.maybeSpill();
+ },
+ iterN: function(at, n, op) {
+ 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);
+ if (child.iterN(at, used, op)) return true;
+ if ((n -= used) == 0) break;
+ at = 0;
+ } else at -= sz;
+ }
+ }
+ };
+
+ var nextDocId = 0;
+ var Doc = CodeMirror.Doc = function(text, mode, firstLine, lineSep) {
+ if (!(this instanceof Doc)) return new Doc(text, mode, firstLine, lineSep);
+ if (firstLine == null) firstLine = 0;
+
+ BranchChunk.call(this, [new LeafChunk([new Line("", null)])]);
+ this.first = firstLine;
+ this.scrollTop = this.scrollLeft = 0;
+ this.cantEdit = false;
+ this.cleanGeneration = 1;
+ this.frontier = firstLine;
+ var start = Pos(firstLine, 0);
+ this.sel = simpleSelection(start);
+ this.history = new History(null);
+ this.id = ++nextDocId;
+ this.modeOption = mode;
+ this.lineSep = lineSep;
+ this.extend = false;
+
+ if (typeof text == "string") text = this.splitLines(text);
+ 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; 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 || this.lineSeparator());
+ },
+ 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: this.splitLines(code), origin: "setValue", full: true}, true);
+ setSelection(this, simpleSelection(top));
+ }),
+ replaceRange: function(code, from, to, origin) {
+ from = clipPos(this, from);
+ to = to ? clipPos(this, to) : from;
+ replaceRange(this, code, from, to, origin);
+ },
+ getRange: function(from, to, lineSep) {
+ var lines = getBetween(this, clipPos(this, from), clipPos(this, to));
+ if (lineSep === false) return lines;
+ return lines.join(lineSep || this.lineSeparator());
+ },
+
+ getLine: function(line) {var l = this.getLineHandle(line); return l && l.text;},
+
+ 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(line);
+ },
+
+ lineCount: function() {return this.size;},
+ firstLine: function() {return this.first;},
+ lastLine: function() {return this.first + this.size - 1;},
+
+ clipPos: function(pos) {return clipPos(this, pos);},
+
+ getCursor: function(start) {
+ 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;
+ },
+ listSelections: function() { return this.sel.ranges; },
+ somethingSelected: function() {return this.sel.somethingSelected();},
+
+ setCursor: docMethodOp(function(line, ch, options) {
+ setSimpleSelection(this, clipPos(this, typeof line == "number" ? Pos(line, ch || 0) : line), null, options);
+ }),
+ setSelection: docMethodOp(function(anchor, head, options) {
+ setSimpleSelection(this, clipPos(this, anchor), clipPos(this, head || anchor), options);
+ }),
+ 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) {
+ var heads = map(this.sel.ranges, f);
+ extendSelections(this, clipPosArray(this, heads), 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) {
+ 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 || this.lineSeparator());
+ },
+ 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 || this.lineSeparator());
+ parts[i] = sel;
+ }
+ return parts;
+ },
+ replaceSelection: 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: docMethodOp(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: this.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.extend = val;},
+ getExtending: function() {return this.extend;},
+
+ historySize: function() {
+ 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 = new History(this.history.maxGeneration);},
+
+ markClean: function() {
+ this.cleanGeneration = this.changeGeneration(true);
+ },
+ changeGeneration: function(forceSplit) {
+ if (forceSplit)
+ this.history.lastOp = this.history.lastSelOp = this.history.lastOrigin = null;
+ return this.history.generation;
+ },
+ isClean: function (gen) {
+ return this.history.generation == (gen || this.cleanGeneration);
+ },
+
+ getHistory: function() {
+ return {done: copyHistoryArray(this.history.done),
+ undone: copyHistoryArray(this.history.undone)};
+ },
+ setHistory: function(histData) {
+ 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);
+ },
+
+ addLineClass: docMethodOp(function(handle, where, cls) {
+ return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function(line) {
+ var prop = where == "text" ? "textClass"
+ : where == "background" ? "bgClass"
+ : where == "gutter" ? "gutterClass" : "wrapClass";
+ if (!line[prop]) line[prop] = cls;
+ else if (classTest(cls).test(line[prop])) return false;
+ else line[prop] += " " + cls;
+ return true;
+ });
+ }),
+ removeLineClass: docMethodOp(function(handle, where, cls) {
+ return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function(line) {
+ var prop = where == "text" ? "textClass"
+ : where == "background" ? "bgClass"
+ : where == "gutter" ? "gutterClass" : "wrapClass";
+ var cur = line[prop];
+ if (!cur) return false;
+ else if (cls == null) line[prop] = null;
+ else {
+ var found = cur.match(classTest(cls));
+ if (!found) return false;
+ var end = found.index + found[0].length;
+ line[prop] = cur.slice(0, found.index) + (!found.index || end == cur.length ? "" : " ") + cur.slice(end) || null;
+ }
+ return true;
+ });
+ }),
+
+ addLineWidget: docMethodOp(function(handle, node, options) {
+ return addLineWidget(this, handle, node, options);
+ }),
+ removeLineWidget: function(widget) { widget.clear(); },
+
+ markText: function(from, to, options) {
+ return markText(this, clipPos(this, from), clipPos(this, to), options, options && options.type || "range");
+ },
+ setBookmark: function(pos, options) {
+ var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options),
+ insertLeft: options && options.insertLeft,
+ clearWhenEmpty: false, shared: options && options.shared,
+ handleMouseEvents: options && options.handleMouseEvents};
+ pos = clipPos(this, pos);
+ return markText(this, pos, pos, realOpts, "bookmark");
+ },
+ findMarksAt: function(pos) {
+ pos = clipPos(this, pos);
+ var markers = [], spans = getLine(this, pos.line).markedSpans;
+ if (spans) for (var i = 0; i < spans.length; ++i) {
+ var span = spans[i];
+ if ((span.from == null || span.from <= pos.ch) &&
+ (span.to == null || span.to >= pos.ch))
+ markers.push(span.marker.parent || span.marker);
+ }
+ return markers;
+ },
+ findMarks: function(from, to, filter) {
+ 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 (!(span.to != null && lineNo == from.line && from.ch >= span.to ||
+ span.from == null && lineNo != from.line ||
+ span.from != null && lineNo == to.line && span.from >= to.ch) &&
+ (!filter || filter(span.marker)))
+ found.push(span.marker.parent || span.marker);
+ }
+ ++lineNo;
+ });
+ return found;
+ },
+ getAllMarks: function() {
+ var markers = [];
+ this.iter(function(line) {
+ var sps = line.markedSpans;
+ if (sps) for (var i = 0; i < sps.length; ++i)
+ if (sps[i].from != null) markers.push(sps[i].marker);
+ });
+ return markers;
+ },
+
+ posFromIndex: function(off) {
+ var ch, lineNo = this.first, sepSize = this.lineSeparator().length;
+ this.iter(function(line) {
+ var sz = line.text.length + sepSize;
+ if (sz > off) { ch = off; return true; }
+ off -= sz;
+ ++lineNo;
+ });
+ return clipPos(this, Pos(lineNo, ch));
+ },
+ indexFromPos: function (coords) {
+ coords = clipPos(this, coords);
+ var index = coords.ch;
+ if (coords.line < this.first || coords.ch < 0) return 0;
+ var sepSize = this.lineSeparator().length;
+ this.iter(this.first, coords.line, function (line) {
+ index += line.text.length + sepSize;
+ });
+ return index;
+ },
+
+ copy: function(copyHistory) {
+ var doc = new Doc(getLines(this, this.first, this.first + this.size),
+ this.modeOption, this.first, this.lineSep);
+ doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft;
+ doc.sel = this.sel;
+ doc.extend = false;
+ if (copyHistory) {
+ doc.history.undoDepth = this.history.undoDepth;
+ doc.setHistory(this.getHistory());
+ }
+ return doc;
+ },
+
+ linkedDoc: function(options) {
+ if (!options) options = {};
+ var from = this.first, to = this.first + this.size;
+ if (options.from != null && options.from > from) from = options.from;
+ if (options.to != null && options.to < to) to = options.to;
+ var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from, this.lineSep);
+ if (options.sharedHist) copy.history = this.history;
+ (this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist});
+ copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}];
+ copySharedMarkers(copy, findSharedMarkers(this));
+ return copy;
+ },
+ unlinkDoc: function(other) {
+ if (other instanceof CodeMirror) other = other.doc;
+ if (this.linked) for (var i = 0; i < this.linked.length; ++i) {
+ var link = this.linked[i];
+ if (link.doc != other) continue;
+ this.linked.splice(i, 1);
+ other.unlinkDoc(this);
+ detachSharedMarkers(findSharedMarkers(this));
+ break;
+ }
+ // If the histories were shared, split them again
+ if (other.history == this.history) {
+ var splitIds = [other.id];
+ linkedDocs(other, function(doc) {splitIds.push(doc.id);}, true);
+ other.history = new History(null);
+ other.history.done = copyHistoryArray(this.history.done, splitIds);
+ other.history.undone = copyHistoryArray(this.history.undone, splitIds);
+ }
+ },
+ iterLinkedDocs: function(f) {linkedDocs(this, f);},
+
+ getMode: function() {return this.mode;},
+ getEditor: function() {return this.cm;},
+
+ splitLines: function(str) {
+ if (this.lineSep) return str.split(this.lineSep);
+ return splitLinesAuto(str);
+ },
+ lineSeparator: function() { return this.lineSep || "\n"; }
+ });
+
+ // Public alias.
+ Doc.prototype.eachLine = Doc.prototype.iter;
+
+ // Set up methods on CodeMirror's prototype to redirect to the editor's document.
+ var dontDelegate = "iter insert remove copy getEditor constructor".split(" ");
+ for (var prop in Doc.prototype) if (Doc.prototype.hasOwnProperty(prop) && indexOf(dontDelegate, prop) < 0)
+ CodeMirror.prototype[prop] = (function(method) {
+ return function() {return method.apply(this.doc, arguments);};
+ })(Doc.prototype[prop]);
+
+ 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) {
+ var rel = doc.linked[i];
+ if (rel.doc == skip) continue;
+ var shared = sharedHist && rel.sharedHist;
+ if (sharedHistOnly && !shared) continue;
+ f(rel.doc, shared);
+ propagate(rel.doc, doc, shared);
+ }
+ }
+ 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) findMaxLine(cm);
+ cm.options.mode = doc.modeOption;
+ regChange(cm);
+ }
+
+ // LINE UTILITIES
+
+ // 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; }
+ n -= sz;
+ }
+ }
+ 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) {
+ var text = line.text;
+ if (n == end.line) text = text.slice(0, end.ch);
+ if (n == start.line) text = text.slice(start.ch);
+ out.push(text);
+ ++n;
+ });
+ 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;
+ 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);
+ for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) {
+ for (var i = 0;; ++i) {
+ if (chunk.children[i] == cur) break;
+ no += chunk.children[i].chunkSize();
+ }
+ }
+ 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; i < chunk.children.length; ++i) {
+ var child = chunk.children[i], ch = child.height;
+ if (h < ch) { chunk = child; continue outer; }
+ h -= ch;
+ n += child.chunkSize();
+ }
+ return n;
+ } while (!chunk.lines);
+ for (var i = 0; i < chunk.lines.length; ++i) {
+ var line = chunk.lines[i], lh = line.height;
+ if (h < lh) break;
+ h -= lh;
+ }
+ return n + i;
+ }
+
+
+ // 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) {
+ var line = chunk.lines[i];
+ if (line == lineObj) break;
+ else h += line.height;
+ }
+ for (var p = chunk.parent; p; chunk = p, p = chunk.parent) {
+ for (var i = 0; i < p.children.length; ++i) {
+ var cur = p.children[i];
+ if (cur == chunk) break;
+ else h += cur.height;
+ }
+ }
+ 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);
+ return order;
+ }
+
+ // HISTORY
+
+ 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 = this.lastSelOp = 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 = hist.lastSelOp = 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.lastSelOp ||
+ (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.lastSelOp = 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) {
+ if (line.markedSpans)
+ (existing || (existing = change["spans_" + doc.id] = {}))[n] = line.markedSpans;
+ ++n;
+ });
+ }
+
+ // 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) {
+ if (spans[i].marker.explicitlyCleared) { if (!out) out = spans.slice(0, i); }
+ else if (out) out.push(spans[i]);
+ }
+ 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;
+ for (var i = 0, nw = []; i < change.text.length; ++i)
+ nw.push(removeClearedSpans(found[i]));
+ return nw;
+ }
+
+ // 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, instantiateSel) {
+ for (var i = 0, copy = []; i < events.length; ++i) {
+ 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});
+ if (newGroup) for (var prop in change) if (m = prop.match(/^spans_(\d+)$/)) {
+ if (indexOf(newGroup, Number(m[1])) > -1) {
+ lst(newChanges)[prop] = change[prop];
+ delete change[prop];
+ }
+ }
+ }
+ }
+ return copy;
+ }
+
+ // Rebasing/resetting history to deal with externally-sourced changes
+
+ function rebaseHistSelSingle(pos, from, to, diff) {
+ if (to < pos.line) {
+ pos.line += diff;
+ } else if (from < pos.line) {
+ pos.line = from;
+ pos.ch = 0;
+ }
+ }
+
+ // Tries to rebase an array of history events given a change in the
+ // document. If the change touches the same lines as the event, the
+ // event, and everything 'behind' it, is discarded. If the change is
+ // before the event, the event's positions are updated. Uses a
+ // copy-on-write scheme for the positions, to avoid having to
+ // reallocate them all on every rebase, but also avoid problems with
+ // shared position objects being unsafely updated.
+ 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 (to < cur.from.line) {
+ 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 (!ok) {
+ array.splice(0, i + 1);
+ i = 0;
+ }
+ }
+ }
+
+ function rebaseHist(hist, change) {
+ var from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1;
+ rebaseHistArray(hist.done, from, to, diff);
+ rebaseHistArray(hist.undone, from, to, diff);
+ }
+
+ // EVENT UTILITIES
+
+ // Due to the fact that we still support jurassic IE versions, some
+ // compatibility wrappers are needed.
+
+ var e_preventDefault = CodeMirror.e_preventDefault = function(e) {
+ if (e.preventDefault) e.preventDefault();
+ else e.returnValue = false;
+ };
+ 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;
+ }
+ 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) {
+ var b = e.which;
+ if (b == null) {
+ if (e.button & 1) b = 1;
+ else if (e.button & 2) b = 3;
+ else if (e.button & 4) b = 2;
+ }
+ if (mac && e.ctrlKey && b == 1) b = 3;
+ return b;
+ }
+
+ // EVENT HANDLING
+
+ // 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)
+ emitter.attachEvent("on" + type, f);
+ else {
+ var map = emitter._handlers || (emitter._handlers = {});
+ var arr = map[type] || (map[type] = []);
+ arr.push(f);
+ }
+ };
+
+ var noHandlers = []
+ function getHandlers(emitter, type, copy) {
+ var arr = emitter._handlers && emitter._handlers[type]
+ if (copy) return arr && arr.length > 0 ? arr.slice() : noHandlers
+ else return arr || noHandlers
+ }
+
+ var off = CodeMirror.off = function(emitter, type, f) {
+ if (emitter.removeEventListener)
+ emitter.removeEventListener(type, f, false);
+ else if (emitter.detachEvent)
+ emitter.detachEvent("on" + type, f);
+ else {
+ var handlers = getHandlers(emitter, type, false)
+ for (var i = 0; i < handlers.length; ++i)
+ if (handlers[i] == f) { handlers.splice(i, 1); break; }
+ }
+ };
+
+ var signal = CodeMirror.signal = function(emitter, type /*, values...*/) {
+ var handlers = getHandlers(emitter, type, true)
+ if (!handlers.length) return;
+ var args = Array.prototype.slice.call(arguments, 2);
+ for (var i = 0; i < handlers.length; ++i) handlers[i].apply(null, args);
+ };
+
+ var orphanDelayedCallbacks = null;
+
+ // 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.
+ function signalLater(emitter, type /*, values...*/) {
+ var arr = getHandlers(emitter, type, false)
+ if (!arr.length) return;
+ var args = Array.prototype.slice.call(arguments, 2), list;
+ if (operationGroup) {
+ list = operationGroup.delayedCallbacks;
+ } else if (orphanDelayedCallbacks) {
+ list = orphanDelayedCallbacks;
+ } else {
+ list = orphanDelayedCallbacks = [];
+ setTimeout(fireOrphanDelayed, 0);
+ }
+ function bnd(f) {return function(){f.apply(null, args);};};
+ for (var i = 0; i < arr.length; ++i)
+ list.push(bnd(arr[i]));
+ }
+
+ function fireOrphanDelayed() {
+ var delayed = orphanDelayedCallbacks;
+ orphanDelayedCallbacks = null;
+ 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) {
+ if (typeof e == "string")
+ e = {type: e, preventDefault: function() { this.defaultPrevented = true; }};
+ signal(cm, override || e.type, cm, e);
+ return e_defaultPrevented(e) || e.codemirrorIgnore;
+ }
+
+ function signalCursorActivity(cm) {
+ var arr = cm._handlers && cm._handlers.cursorActivity;
+ if (!arr) return;
+ var set = cm.curOp.cursorActivityHandlers || (cm.curOp.cursorActivityHandlers = []);
+ for (var i = 0; i < arr.length; ++i) if (indexOf(set, arr[i]) == -1)
+ set.push(arr[i]);
+ }
+
+ function hasHandler(emitter, type) {
+ return getHandlers(emitter, type).length > 0
+ }
+
+ // 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);};
+ }
+
+ // MISC UTILITIES
+
+ // Number of pixels added to scroller and sizer to hide scrollbar
+ var scrollerGap = 30;
+
+ // Returned or thrown by various protocols to signal 'I'm not
+ // 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);
+ };
+
+ // Counts the column offset in a string, taking tabs into account.
+ // Used mostly to find indentation.
+ 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;;) {
+ 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.
+ var findColumn = CodeMirror.findColumn = function(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;
+ }
+ }
+
+ var spaceStrs = [""];
+ function spaceStr(n) {
+ while (spaceStrs.length <= n)
+ spaceStrs.push(lst(spaceStrs) + " ");
+ return spaceStrs[n];
+ }
+
+ function lst(arr) { return arr[arr.length-1]; }
+
+ 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(array, elt) {
+ for (var i = 0; i < array.length; ++i)
+ if (array[i] == elt) return i;
+ return -1;
+ }
+ function map(array, f) {
+ var out = [];
+ for (var i = 0; i < array.length; i++) out[i] = f(array[i], i);
+ return out;
+ }
+
+ function nothing() {}
+
+ function createObj(base, props) {
+ var inst;
+ if (Object.create) {
+ inst = Object.create(base);
+ } else {
+ nothing.prototype = base;
+ inst = new nothing();
+ }
+ if (props) copyObj(props, inst);
+ return inst;
+ };
+
+ function copyObj(obj, target, overwrite) {
+ if (!target) target = {};
+ for (var prop in obj)
+ if (obj.hasOwnProperty(prop) && (overwrite !== false || !target.hasOwnProperty(prop)))
+ target[prop] = obj[prop];
+ return target;
+ }
+
+ function bind(f) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ return function(){return f.apply(null, args);};
+ }
+
+ var nonASCIISingleCaseWordChar = /[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/;
+ var isWordCharBasic = CodeMirror.isWordChar = function(ch) {
+ return /\w/.test(ch) || ch > "\x80" &&
+ (ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch));
+ };
+ function isWordChar(ch, helper) {
+ if (!helper) return isWordCharBasic(ch);
+ if (helper.source.indexOf("\\w") > -1 && isWordCharBasic(ch)) return true;
+ return helper.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); }
+
+ // DOM UTILITIES
+
+ function elt(tag, content, className, style) {
+ var e = document.createElement(tag);
+ if (className) e.className = className;
+ if (style) e.style.cssText = style;
+ 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, endNode) {
+ var r = document.createRange();
+ r.setEnd(endNode || node, end);
+ r.setStart(node, start);
+ return r;
+ };
+ else range = function(node, start, end) {
+ var r = document.body.createTextRange();
+ try { r.moveToElementText(node.parentNode); }
+ catch(e) { return r; }
+ 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);
+ return e;
+ }
+
+ function removeChildrenAndAdd(parent, e) {
+ return removeChildren(parent).appendChild(e);
+ }
+
+ var contains = CodeMirror.contains = function(parent, child) {
+ if (child.nodeType == 3) // Android browser always returns false when child is a textnode
+ child = child.parentNode;
+ if (parent.contains)
+ return parent.contains(child);
+ do {
+ if (child.nodeType == 11) child = child.host;
+ if (child == parent) return true;
+ } while (child = child.parentNode);
+ };
+
+ function activeElt() {
+ var activeElement = document.activeElement;
+ while (activeElement && activeElement.root && activeElement.root.activeElement)
+ activeElement = activeElement.root.activeElement;
+ return activeElement;
+ }
+ // Older versions of IE throws unspecified error when touching
+ // document.activeElement in some cases (during loading, in iframe)
+ if (ie && ie_version < 11) activeElt = function() {
+ try { return document.activeElement; }
+ catch(e) { return document.body; }
+ };
+
+ function classTest(cls) { return new RegExp("(^|\\s)" + cls + "(?:$|\\s)\\s*"); }
+ var rmClass = CodeMirror.rmClass = function(node, cls) {
+ var current = node.className;
+ var match = classTest(cls).exec(current);
+ if (match) {
+ var after = current.slice(match.index + match[0].length);
+ node.className = current.slice(0, match.index) + (after ? match[1] + after : "");
+ }
+ };
+ var addClass = CodeMirror.addClass = function(node, cls) {
+ var current = node.className;
+ if (!classTest(cls).test(current)) node.className += (current ? " " : "") + cls;
+ };
+ function joinClasses(a, b) {
+ var as = a.split(" ");
+ for (var i = 0; i < as.length; i++)
+ if (as[i] && !classTest(as[i]).test(b)) b += " " + as[i];
+ return b;
+ }
+
+ // WINDOW-WIDE EVENTS
+
+ // These must be handled carefully, because naively registering a
+ // handler for each editor will cause the editors to never be
+ // garbage collected.
+
+ function forEachCodeMirror(f) {
+ if (!document.body.getElementsByClassName) return;
+ var byClass = document.body.getElementsByClassName("CodeMirror");
+ for (var i = 0; i < byClass.length; i++) {
+ var cm = byClass[i].CodeMirror;
+ if (cm) f(cm);
+ }
+ }
+
+ var globalsRegistered = false;
+ function ensureGlobalHandlers() {
+ if (globalsRegistered) return;
+ registerGlobalHandlers();
+ globalsRegistered = true;
+ }
+ function registerGlobalHandlers() {
+ // When the window resizes, we need to refresh active editors.
+ var resizeTimer;
+ on(window, "resize", function() {
+ if (resizeTimer == null) resizeTimer = setTimeout(function() {
+ resizeTimer = null;
+ forEachCodeMirror(onResize);
+ }, 100);
+ });
+ // When the window loses focus, we want to show the editor as blurred
+ on(window, "blur", function() {
+ forEachCodeMirror(onBlur);
+ });
+ }
+
+ // FEATURE DETECTION
+
+ // Detect drag-and-drop
+ 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 && ie_version < 9) return false;
+ var div = elt('div');
+ return "draggable" in div || "dragDrop" in div;
+ }();
+
+ var zwspSupported;
+ function zeroWidthElement(measure) {
+ if (zwspSupported == null) {
+ 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 && ie_version < 8);
+ }
+ var node = zwspSupported ? elt("span", "\u200b") :
+ elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px");
+ node.setAttribute("cm-text", "");
+ return node;
+ }
+
+ // 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 || r0.left == r0.right) return false; // Safari returns null in some cases (#2780)
+ 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 splitLinesAuto = 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);
+ if (nl == -1) nl = string.length;
+ var line = string.slice(pos, string.charAt(nl - 1) == "\r" ? nl - 1 : nl);
+ var rt = line.indexOf("\r");
+ if (rt != -1) {
+ result.push(line.slice(0, rt));
+ pos += rt + 1;
+ } else {
+ result.push(line);
+ pos = nl + 1;
+ }
+ }
+ return result;
+ } : function(string){return string.split(/\r\n?|\n/);};
+
+ var hasSelection = window.getSelection ? function(te) {
+ try { return te.selectionStart != te.selectionEnd; }
+ catch(e) { return false; }
+ } : function(te) {
+ try {var range = te.ownerDocument.selection.createRange();}
+ catch(e) {}
+ if (!range || range.parentElement() != te) return false;
+ return range.compareEndPoints("StartToEnd", range) != 0;
+ };
+
+ var hasCopyEvent = (function() {
+ var e = elt("div");
+ if ("oncopy" in e) return true;
+ e.setAttribute("oncopy", "return;");
+ return typeof e.oncopy == "function";
+ })();
+
+ var badZoomedRects = null;
+ function hasBadZoomedRects(measure) {
+ if (badZoomedRects != null) return badZoomedRects;
+ var node = removeChildrenAndAdd(measure, elt("span", "x"));
+ var normal = node.getBoundingClientRect();
+ var fromRange = range(node, 0, 1).getBoundingClientRect();
+ return badZoomedRects = Math.abs(normal.left - fromRange.left) > 1;
+ }
+
+ // KEY NAMES
+
+ var keyNames = CodeMirror.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",
+ 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert",
+ 46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod",
+ 106: "*", 107: "=", 109: "-", 110: ".", 111: "/", 127: "Delete",
+ 173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\",
+ 221: "]", 222: "'", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete",
+ 63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert"
+ };
+ (function() {
+ // Number keys
+ for (var i = 0; i < 10; i++) keyNames[i + 48] = keyNames[i + 96] = String(i);
+ // Alphabetic keys
+ for (var i = 65; i <= 90; i++) keyNames[i] = String.fromCharCode(i);
+ // Function keys
+ for (var i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i;
+ })();
+
+ // BIDI HELPERS
+
+ function iterateBidiSections(order, from, to, f) {
+ if (!order) return f(from, to, "ltr");
+ var found = false;
+ for (var i = 0; i < order.length; ++i) {
+ var part = order[i];
+ if (part.from < to && part.to > from || from == to && part.to == from) {
+ f(Math.max(part.from, from), Math.min(part.to, to), part.level == 1 ? "rtl" : "ltr");
+ found = true;
+ }
+ }
+ if (!found) f(from, to, "ltr");
+ }
+
+ function bidiLeft(part) { return part.level % 2 ? part.to : part.from; }
+ function bidiRight(part) { return part.level % 2 ? part.from : part.to; }
+
+ function lineLeft(line) { var order = getOrder(line); return order ? bidiLeft(order[0]) : 0; }
+ function lineRight(line) {
+ var order = getOrder(line);
+ if (!order) return line.text.length;
+ return bidiRight(lst(order));
+ }
+
+ function lineStart(cm, lineN) {
+ var line = getLine(cm.doc, lineN);
+ 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 = 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 == null ? lineNo(line) : lineN, ch);
+ }
+ function lineStartSmart(cm, pos) {
+ var start = lineStart(cm, pos.line);
+ var line = getLine(cm.doc, start.line);
+ var order = getOrder(line);
+ if (!order || order[0].level == 0) {
+ var firstNonWS = Math.max(0, line.text.search(/\S/));
+ var inWS = pos.line == start.line && pos.ch <= firstNonWS && pos.ch;
+ return Pos(start.line, inWS ? 0 : firstNonWS);
+ }
+ return start;
+ }
+
+ function compareBidiLevel(order, a, b) {
+ var linedir = order[0].level;
+ if (a == linedir) return true;
+ if (b == linedir) return false;
+ return a < b;
+ }
+ var bidiOther;
+ function getBidiPartAt(order, pos) {
+ bidiOther = null;
+ for (var i = 0, found; i < order.length; ++i) {
+ var cur = order[i];
+ if (cur.from < pos && cur.to > pos) return i;
+ if ((cur.from == pos || cur.to == pos)) {
+ if (found == null) {
+ found = i;
+ } else if (compareBidiLevel(order, cur.level, order[found].level)) {
+ if (cur.from != cur.to) bidiOther = found;
+ return i;
+ } else {
+ if (cur.from != cur.to) bidiOther = i;
+ return found;
+ }
+ }
+ }
+ return found;
+ }
+
+ function moveInLine(line, pos, dir, byUnit) {
+ if (!byUnit) return pos + dir;
+ do pos += dir;
+ while (pos > 0 && isExtendingChar(line.text.charAt(pos)));
+ return pos;
+ }
+
+ // 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);
+ var pos = getBidiPartAt(bidi, start), part = bidi[pos];
+ var target = moveInLine(line, start, part.level % 2 ? -dir : dir, byUnit);
+
+ for (;;) {
+ if (target > part.from && target < part.to) return target;
+ if (target == part.from || target == part.to) {
+ if (getBidiPartAt(bidi, target) == pos) return target;
+ part = bidi[pos += dir];
+ return (dir > 0) == part.level % 2 ? part.to : part.from;
+ } else {
+ part = bidi[pos += dir];
+ if (!part) return null;
+ if ((dir > 0) == part.level % 2)
+ target = moveInLine(line, part.to, -1, byUnit);
+ else
+ target = moveInLine(line, part.from, 1, byUnit);
+ }
+ }
+ }
+
+ function moveLogically(line, start, dir, byUnit) {
+ var target = start + dir;
+ if (byUnit) while (target > 0 && isExtendingChar(line.text.charAt(target))) target += dir;
+ return target < 0 || target > line.text.length ? null : target;
+ }
+
+ // Bidirectional ordering algorithm
+ // See http://unicode.org/reports/tr9/tr9-13.html for the algorithm
+ // that this (partially) implements.
+
+ // One-char codes used for character types:
+ // L (L): Left-to-Right
+ // R (R): Right-to-Left
+ // r (AL): Right-to-Left Arabic
+ // 1 (EN): European Number
+ // + (ES): European Number Separator
+ // % (ET): European Number Terminator
+ // n (AN): Arabic Number
+ // , (CS): Common Number Separator
+ // m (NSM): Non-Spacing Mark
+ // b (BN): Boundary Neutral
+ // s (B): Paragraph Separator
+ // t (S): Segment Separator
+ // w (WS): Whitespace
+ // N (ON): Other Neutrals
+
+ // Returns null if characters are ordered as they appear
+ // (left-to-right), or an array of sections ({from, to, level}
+ // 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%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN";
+ // Character types for codepoints 0x600 to 0x6ff
+ var arabicTypes = "rrrrrrrrrrrr,rNNmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmrrrrrrrnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmNmmmm";
+ function charType(code) {
+ if (code <= 0xf7) return lowTypes.charAt(code);
+ else if (0x590 <= code && code <= 0x5f4) 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";
+ }
+
+ var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/;
+ var isNeutral = /[stwN]/, isStrong = /[LRr]/, countsAsLeft = /[Lb1n]/, countsAsNum = /[1n]/;
+ // 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 = [];
+ for (var i = 0, type; i < len; ++i)
+ types.push(type = charType(str.charCodeAt(i)));
+
+ // W1. Examine each non-spacing mark (NSM) in the level run, and
+ // change the type of the NSM to the type of the previous
+ // character. If the NSM is at the start of the level run, it will
+ // get the type of sor.
+ for (var i = 0, prev = outerType; i < len; ++i) {
+ var type = types[i];
+ if (type == "m") types[i] = prev;
+ else prev = type;
+ }
+
+ // W2. Search backwards from each instance of a European number
+ // until the first strong type (R, L, AL, or sor) is found. If an
+ // AL is found, change the type of the European number to Arabic
+ // number.
+ // W3. Change all ALs to R.
+ for (var i = 0, cur = outerType; i < len; ++i) {
+ var type = types[i];
+ if (type == "1" && cur == "r") types[i] = "n";
+ else if (isStrong.test(type)) { cur = type; if (type == "r") types[i] = "R"; }
+ }
+
+ // W4. A single European separator between two European numbers
+ // changes to a European number. A single common separator between
+ // two numbers of the same type changes to that type.
+ for (var i = 1, prev = types[0]; i < len - 1; ++i) {
+ var type = types[i];
+ if (type == "+" && prev == "1" && types[i+1] == "1") types[i] = "1";
+ else if (type == "," && prev == types[i+1] &&
+ (prev == "1" || prev == "n")) types[i] = prev;
+ prev = type;
+ }
+
+ // W5. A sequence of European terminators adjacent to European
+ // numbers changes to all European numbers.
+ // W6. Otherwise, separators and terminators change to Other
+ // Neutral.
+ for (var i = 0; i < len; ++i) {
+ var type = types[i];
+ if (type == ",") types[i] = "N";
+ else if (type == "%") {
+ for (var end = i + 1; end < len && types[end] == "%"; ++end) {}
+ var replace = (i && types[i-1] == "!") || (end < len && types[end] == "1") ? "1" : "N";
+ for (var j = i; j < end; ++j) types[j] = replace;
+ i = end - 1;
+ }
+ }
+
+ // W7. Search backwards from each instance of a European number
+ // until the first strong type (R, L, or sor) is found. If an L is
+ // found, then change the type of the European number to L.
+ for (var i = 0, cur = outerType; i < len; ++i) {
+ var type = types[i];
+ if (cur == "L" && type == "1") types[i] = "L";
+ else if (isStrong.test(type)) cur = type;
+ }
+
+ // N1. A sequence of neutrals takes the direction of the
+ // surrounding strong text if the text on both sides has the same
+ // direction. European and Arabic numbers act as if they were R in
+ // terms of their influence on neutrals. Start-of-level-run (sor)
+ // and end-of-level-run (eor) are used at level run boundaries.
+ // N2. Any remaining neutrals take the embedding direction.
+ for (var i = 0; i < len; ++i) {
+ if (isNeutral.test(types[i])) {
+ for (var end = i + 1; end < len && isNeutral.test(types[end]); ++end) {}
+ var before = (i ? types[i-1] : outerType) == "L";
+ var after = (end < len ? types[end] : outerType) == "L";
+ var replace = before || after ? "L" : "R";
+ for (var j = i; j < end; ++j) types[j] = replace;
+ i = end - 1;
+ }
+ }
+
+ // Here we depart from the documented algorithm, in order to avoid
+ // building up an actual levels array. Since there are only three
+ // levels (0, 1, 2) in an implementation that doesn't take
+ // explicit embedding into account, we can build up the order on
+ // the fly, without following the level-based algorithm.
+ var order = [], m;
+ for (var i = 0; i < len;) {
+ if (countsAsLeft.test(types[i])) {
+ var start = i;
+ for (++i; i < len && countsAsLeft.test(types[i]); ++i) {}
+ 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, new BidiSpan(1, pos, j));
+ var nstart = j;
+ for (++j; j < i && countsAsNum.test(types[j]); ++j) {}
+ order.splice(at, 0, new BidiSpan(2, nstart, j));
+ pos = j;
+ } else ++j;
+ }
+ 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(new BidiSpan(0, 0, m[0].length));
+ }
+ if (lst(order).level == 1 && (m = str.match(/\s+$/))) {
+ lst(order).to -= m[0].length;
+ order.push(new BidiSpan(0, len - m[0].length, len));
+ }
+ if (order[0].level == 2)
+ order.unshift(new BidiSpan(1, order[0].to, order[0].to));
+ if (order[0].level != lst(order).level)
+ order.push(new BidiSpan(order[0].level, len, len));
+
+ return order;
+ };
+ })();
+
+ // THE END
+
+ CodeMirror.version = "5.16.0";
+
+ return CodeMirror;
+ });
+
+
+/***/ },
+/* 3 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2));
+ 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) {
+ this.atOccurrence = false; this.doc = doc;
+ if (caseFold == null && typeof query == "string") caseFold = false;
+
+ pos = pos ? doc.clipPos(pos) : Pos(0, 0);
+ this.pos = {from: pos, to: pos};
+
+ // The matches method is filled in based on the type of query.
+ // It takes a position and a direction, and returns an object
+ // describing the next occurrence of the query, or null if no
+ // more matches were found.
+ if (typeof query != "string") { // Regexp match
+ if (!query.global) query = new RegExp(query.source, query.ignoreCase ? "ig" : "g");
+ this.matches = function(reverse, pos) {
+ if (reverse) {
+ query.lastIndex = 0;
+ var line = doc.getLine(pos.line).slice(0, pos.ch), cutOff = 0, match, start;
+ for (;;) {
+ query.lastIndex = cutOff;
+ var newMatch = query.exec(line);
+ if (!newMatch) break;
+ match = newMatch;
+ start = match.index;
+ cutOff = match.index + (match[0].length || 1);
+ if (cutOff == line.length) break;
+ }
+ var matchLen = (match && match[0].length) || 0;
+ if (!matchLen) {
+ if (start == 0 && line.length == 0) {match = undefined;}
+ else if (start != doc.getLine(pos.line).length) {
+ matchLen++;
+ }
+ }
+ } else {
+ query.lastIndex = pos.ch;
+ var line = doc.getLine(pos.line), match = query.exec(line);
+ var matchLen = (match && match[0].length) || 0;
+ var start = match && match.index;
+ if (start + matchLen != line.length && !matchLen) matchLen = 1;
+ }
+ if (match && matchLen)
+ return {from: Pos(pos.line, start),
+ to: Pos(pos.line, start + matchLen),
+ match: match};
+ };
+ } else { // String query
+ var origQuery = query;
+ if (caseFold) query = query.toLowerCase();
+ var fold = caseFold ? function(str){return str.toLowerCase();} : function(str){return str;};
+ var target = query.split("\n");
+ // Different methods for single-line and multi-line queries
+ if (target.length == 1) {
+ if (!query.length) {
+ // Empty string would match anything and never progress, so
+ // we define it to match nothing instead.
+ this.matches = function() {};
+ } else {
+ this.matches = function(reverse, pos) {
+ if (reverse) {
+ var orig = doc.getLine(pos.line).slice(0, pos.ch), line = fold(orig);
+ var match = line.lastIndexOf(query);
+ if (match > -1) {
+ match = adjustPos(orig, line, match);
+ return {from: Pos(pos.line, match), to: Pos(pos.line, match + origQuery.length)};
+ }
+ } else {
+ var orig = doc.getLine(pos.line).slice(pos.ch), line = fold(orig);
+ var match = line.indexOf(query);
+ if (match > -1) {
+ match = adjustPos(orig, line, match) + pos.ch;
+ return {from: Pos(pos.line, match), to: Pos(pos.line, match + origQuery.length)};
+ }
+ }
+ };
+ }
+ } else {
+ var origTarget = origQuery.split("\n");
+ this.matches = function(reverse, pos) {
+ var last = target.length - 1;
+ if (reverse) {
+ if (pos.line - (target.length - 1) < doc.firstLine()) return;
+ if (fold(doc.getLine(pos.line).slice(0, origTarget[last].length)) != target[target.length - 1]) return;
+ var to = Pos(pos.line, origTarget[last].length);
+ for (var ln = pos.line - 1, i = last - 1; i >= 1; --i, --ln)
+ if (target[i] != fold(doc.getLine(ln))) return;
+ var line = doc.getLine(ln), cut = line.length - origTarget[0].length;
+ if (fold(line.slice(cut)) != target[0]) return;
+ return {from: Pos(ln, cut), to: to};
+ } else {
+ if (pos.line + (target.length - 1) > doc.lastLine()) return;
+ var line = doc.getLine(pos.line), cut = line.length - origTarget[0].length;
+ if (fold(line.slice(cut)) != target[0]) return;
+ var from = Pos(pos.line, cut);
+ for (var ln = pos.line + 1, i = 1; i < last; ++i, ++ln)
+ if (target[i] != fold(doc.getLine(ln))) return;
+ if (fold(doc.getLine(ln).slice(0, origTarget[last].length)) != target[last]) return;
+ return {from: from, to: Pos(ln, origTarget[last].length)};
+ }
+ };
+ }
+ }
+ }
+
+ SearchCursor.prototype = {
+ findNext: function() {return this.find(false);},
+ findPrevious: function() {return this.find(true);},
+
+ find: function(reverse) {
+ var self = this, pos = this.doc.clipPos(reverse ? this.pos.from : this.pos.to);
+ function savePosAndFail(line) {
+ var pos = Pos(line, 0);
+ self.pos = {from: pos, to: pos};
+ self.atOccurrence = false;
+ return false;
+ }
+
+ for (;;) {
+ if (this.pos = this.matches(reverse, pos)) {
+ this.atOccurrence = true;
+ return this.pos.match || true;
+ }
+ if (reverse) {
+ if (!pos.line) return savePosAndFail(0);
+ pos = Pos(pos.line-1, this.doc.getLine(pos.line-1).length);
+ }
+ else {
+ var maxLine = this.doc.lineCount();
+ if (pos.line == maxLine - 1) return savePosAndFail(maxLine);
+ pos = Pos(pos.line + 1, 0);
+ }
+ }
+ },
+
+ from: function() {if (this.atOccurrence) return this.pos.from;},
+ to: function() {if (this.atOccurrence) return this.pos.to;},
+
+ replace: function(newText, origin) {
+ if (!this.atOccurrence) return;
+ var lines = CodeMirror.splitLines(newText);
+ this.doc.replaceRange(lines, this.pos.from, this.pos.to, origin);
+ this.pos.to = Pos(this.pos.from.line + lines.length - 1,
+ lines[lines.length - 1].length + (lines.length == 1 ? this.pos.from.ch : 0));
+ }
+ };
+
+ // Maps a position in a case-folded line back to a position in the original line
+ // (compensating for codepoints increasing in number during folding)
+ function adjustPos(orig, folded, pos) {
+ if (orig.length == folded.length) return pos;
+ for (var pos1 = Math.min(pos, orig.length);;) {
+ var len1 = orig.slice(0, pos1).toLowerCase().length;
+ if (len1 < pos) ++pos1;
+ else if (len1 > pos) --pos1;
+ else return pos1;
+ }
+ }
+
+ CodeMirror.defineExtension("getSearchCursor", function(query, pos, caseFold) {
+ return new SearchCursor(this.doc, query, pos, caseFold);
+ });
+ CodeMirror.defineDocExtension("getSearchCursor", function(query, pos, caseFold) {
+ return new SearchCursor(this, query, pos, caseFold);
+ });
+
+ CodeMirror.defineExtension("selectMatches", function(query, caseFold) {
+ var ranges = [];
+ var cur = this.getSearchCursor(query, this.getCursor("from"), caseFold);
+ while (cur.findNext()) {
+ if (CodeMirror.cmpPos(cur.to(), this.getCursor("to")) > 0) break;
+ ranges.push({anchor: cur.from(), head: cur.to()});
+ }
+ if (ranges.length)
+ this.setSelections(ranges, 0);
+ });
+ });
+
+
+/***/ },
+/* 4 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ // Define search commands. Depends on dialog.js or another
+ // implementation of the openDialog method.
+
+ // Replace works a little oddly -- it will do the replace on the next
+ // Ctrl-G (or whatever is bound to findNext) press. You prevent a
+ // replace by making sure the match is no longer selected when hitting
+ // Ctrl-G.
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2), __webpack_require__(3), __webpack_require__(1));
+ 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) {
+ if (typeof query == "string")
+ query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"), caseInsensitive ? "gi" : "g");
+ else if (!query.global)
+ query = new RegExp(query.source, query.ignoreCase ? "gi" : "g");
+
+ return {token: function(stream) {
+ query.lastIndex = stream.pos;
+ var match = query.exec(stream.string);
+ if (match && match.index == stream.pos) {
+ stream.pos += match[0].length || 1;
+ return "searching";
+ } else if (match) {
+ stream.pos = match.index;
+ } else {
+ stream.skipToEnd();
+ }
+ }};
+ }
+
+ function SearchState() {
+ this.posFrom = this.posTo = this.lastQuery = this.query = null;
+ this.overlay = null;
+ }
+
+ function getSearchState(cm) {
+ return cm.state.search || (cm.state.search = new SearchState());
+ }
+
+ function queryCaseInsensitive(query) {
+ return typeof query == "string" && query == query.toLowerCase();
+ }
+
+ function getSearchCursor(cm, query, pos) {
+ // Heuristic: if the query string is all lowercase, do a case insensitive search.
+ return cm.getSearchCursor(query, pos, queryCaseInsensitive(query));
+ }
+
+ function persistentDialog(cm, text, deflt, f) {
+ cm.openDialog(text, f, {
+ value: deflt,
+ selectValueOnOpen: true,
+ closeOnEnter: false,
+ onClose: function() { clearSearch(cm); }
+ });
+ }
+
+ function dialog(cm, text, shortText, deflt, f) {
+ if (cm.openDialog) cm.openDialog(text, f, {value: deflt, selectValueOnOpen: true});
+ else f(prompt(shortText, deflt));
+ }
+
+ function confirmDialog(cm, text, shortText, fs) {
+ if (cm.openConfirm) cm.openConfirm(text, fs);
+ else if (confirm(shortText)) fs[0]();
+ }
+
+ function parseString(string) {
+ return string.replace(/\\(.)/g, function(_, ch) {
+ if (ch == "n") return "\n"
+ if (ch == "r") return "\r"
+ return ch
+ })
+ }
+
+ function parseQuery(query) {
+ var isRE = query.match(/^\/(.*)\/([a-z]*)$/);
+ if (isRE) {
+ try { query = new RegExp(isRE[1], isRE[2].indexOf("i") == -1 ? "" : "i"); }
+ catch(e) {} // Not a regular expression after all, do a string search
+ } else {
+ query = parseString(query)
+ }
+ if (typeof query == "string" ? query == "" : query.test(""))
+ query = /x^/;
+ return query;
+ }
+
+ var queryDialog;
+
+ function startSearch(cm, state, query) {
+ state.queryText = query;
+ state.query = parseQuery(query);
+ cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query));
+ state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query));
+ cm.addOverlay(state.overlay);
+ if (cm.showMatchesOnScrollbar) {
+ if (state.annotate) { state.annotate.clear(); state.annotate = null; }
+ state.annotate = cm.showMatchesOnScrollbar(state.query, queryCaseInsensitive(state.query));
+ }
+ }
+
+ function doSearch(cm, rev, persistent) {
+ if (!queryDialog) {
+ let doc = cm.getWrapperElement().ownerDocument;
+ let inp = doc.createElement("input");
+
+ inp.type = "search";
+ inp.placeholder = cm.l10n("findCmd.promptMessage");
+ inp.style.marginInlineStart = "1em";
+ inp.style.marginInlineEnd = "1em";
+ inp.style.flexGrow = "1";
+ inp.addEventListener("focus", () => inp.select());
+
+ queryDialog = doc.createElement("div");
+ queryDialog.appendChild(inp);
+ queryDialog.style.display = "flex";
+ }
+
+ var state = getSearchState(cm);
+ if (state.query) return findNext(cm, rev);
+ var q = cm.getSelection() || state.lastQuery;
+ if (persistent && cm.openDialog) {
+ var hiding = null
+ persistentDialog(cm, queryDialog, q, function(query, event) {
+ CodeMirror.e_stop(event);
+ if (!query) return;
+ if (query != state.queryText) {
+ startSearch(cm, state, query);
+ state.posFrom = state.posTo = cm.getCursor();
+ }
+ if (hiding) hiding.style.opacity = 1
+ findNext(cm, event.shiftKey, function(_, to) {
+ var dialog
+ if (to.line < 3 && document.querySelector &&
+ (dialog = cm.display.wrapper.querySelector(".CodeMirror-dialog")) &&
+ dialog.getBoundingClientRect().bottom - 4 > cm.cursorCoords(to, "window").top)
+ (hiding = dialog).style.opacity = .4
+ })
+ });
+ } else {
+ dialog(cm, queryDialog, "Search for:", q, function(query) {
+ if (query && !state.query) cm.operation(function() {
+ startSearch(cm, state, query);
+ state.posFrom = state.posTo = cm.getCursor();
+ findNext(cm, rev);
+ });
+ });
+ }
+ }
+
+ function findNext(cm, rev, callback) {cm.operation(function() {
+ var state = getSearchState(cm);
+ var cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo);
+ if (!cursor.find(rev)) {
+ cursor = getSearchCursor(cm, state.query, rev ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0));
+ if (!cursor.find(rev)) return;
+ }
+ cm.setSelection(cursor.from(), cursor.to());
+ cm.scrollIntoView({from: cursor.from(), to: cursor.to()}, 20);
+ state.posFrom = cursor.from(); state.posTo = cursor.to();
+ if (callback) callback(cursor.from(), cursor.to())
+ });}
+
+ function clearSearch(cm) {cm.operation(function() {
+ var state = getSearchState(cm);
+ state.lastQuery = state.query;
+ if (!state.query) return;
+ state.query = state.queryText = null;
+ cm.removeOverlay(state.overlay);
+ if (state.annotate) { state.annotate.clear(); state.annotate = null; }
+ });}
+
+ var replaceQueryDialog =
+ ' <input type="text" style="width: 10em" class="CodeMirror-search-field"/> <span style="color: #888" class="CodeMirror-search-hint">(Use /re/ syntax for regexp search)</span>';
+ var replacementQueryDialog = 'With: <input type="text" style="width: 10em" class="CodeMirror-search-field"/>';
+ var doReplaceConfirm = "Replace? <button>Yes</button> <button>No</button> <button>All</button> <button>Stop</button>";
+
+ function replaceAll(cm, query, text) {
+ cm.operation(function() {
+ 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)/g, function(_, i) {return match[i];}));
+ } else cursor.replace(text);
+ }
+ });
+ }
+
+ function replace(cm, all) {
+ if (cm.getOption("readOnly")) return;
+ var query = cm.getSelection() || getSearchState(cm).lastQuery;
+ var dialogText = all ? "Replace all:" : "Replace:"
+ dialog(cm, dialogText + replaceQueryDialog, dialogText, query, function(query) {
+ if (!query) return;
+ query = parseQuery(query);
+ dialog(cm, replacementQueryDialog, "Replace with:", "", function(text) {
+ text = parseString(text)
+ if (all) {
+ replaceAll(cm, query, text)
+ } else {
+ clearSearch(cm);
+ var cursor = getSearchCursor(cm, query, cm.getCursor("from"));
+ var advance = function() {
+ var start = cursor.from(), match;
+ if (!(match = cursor.findNext())) {
+ cursor = getSearchCursor(cm, query);
+ if (!(match = cursor.findNext()) ||
+ (start && cursor.from().line == start.line && cursor.from().ch == start.ch)) return;
+ }
+ cm.setSelection(cursor.from(), cursor.to());
+ cm.scrollIntoView({from: cursor.from(), to: cursor.to()});
+ confirmDialog(cm, doReplaceConfirm, "Replace?",
+ [function() {doReplace(match);}, advance,
+ function() {replaceAll(cm, query, text)}]);
+ };
+ var doReplace = function(match) {
+ cursor.replace(typeof query == "string" ? text :
+ text.replace(/\$(\d)/g, function(_, i) {return match[i];}));
+ advance();
+ };
+ advance();
+ }
+ });
+ });
+ }
+
+ CodeMirror.commands.find = function(cm) {clearSearch(cm); doSearch(cm);};
+ CodeMirror.commands.findPersistent = function(cm) {clearSearch(cm); doSearch(cm, false, true);};
+ CodeMirror.commands.findNext = doSearch;
+ CodeMirror.commands.findPrev = function(cm) {doSearch(cm, true);};
+ CodeMirror.commands.clearSearch = clearSearch;
+ CodeMirror.commands.replace = replace;
+ CodeMirror.commands.replaceAll = function(cm) {replace(cm, true);};
+ });
+
+
+/***/ },
+/* 5 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2));
+ 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, 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 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 found = scanForBracket(cm, Pos(where.line, pos + (dir > 0 ? 1 : 0)), dir, style || null, config);
+ if (found == null) return null;
+ return {from: Pos(where.line, pos), to: found && found.pos,
+ match: found && found.ch == match.charAt(0), forward: dir > 0};
+ }
+
+ // bracketRegex is used to specify which type of bracket to scan
+ // should be a regexp, e.g. /[[\]]/
+ //
+ // Note: If "where" is on an open bracket, then this bracket is ignored.
+ //
+ // Returns false when no bracket was found, null when it reached
+ // maxScanLines and gave up
+ function scanForBracket(cm, where, dir, style, config) {
+ var maxScanLen = (config && config.maxScanLineLength) || 10000;
+ var maxScanLines = (config && config.maxScanLines) || 1000;
+
+ var stack = [];
+ var re = config && config.bracketRegex ? config.bracketRegex : /[(){}[\]]/;
+ var lineEnd = dir > 0 ? Math.min(where.line + maxScanLines, cm.lastLine() + 1)
+ : Math.max(cm.firstLine() - 1, where.line - maxScanLines);
+ for (var lineNo = where.line; lineNo != lineEnd; lineNo += dir) {
+ var line = cm.getLine(lineNo);
+ if (!line) continue;
+ var pos = dir > 0 ? 0 : line.length - 1, end = dir > 0 ? line.length : -1;
+ if (line.length > maxScanLen) continue;
+ if (lineNo == where.line) pos = where.ch - (dir < 0 ? 1 : 0);
+ for (; pos != end; pos += dir) {
+ var ch = line.charAt(pos);
+ if (re.test(ch) && (style === undefined || cm.getTokenTypeAt(Pos(lineNo, pos + 1)) == style)) {
+ var match = matching[ch];
+ if ((match.charAt(1) == ">") == (dir > 0)) stack.push(ch);
+ else if (!stack.length) return {pos: Pos(lineNo, pos), ch: ch};
+ else stack.pop();
+ }
+ }
+ }
+ return lineNo - dir == (dir > 0 ? cm.lastLine() : cm.firstLine()) ? false : null;
+ }
+
+ function matchBrackets(cm, autoclear, config) {
+ // Disable brace matching in long lines, since it'll cause hugely slow updates
+ var maxHighlightLen = cm.state.matchBrackets.maxHighlightLineLength || 1000;
+ var marks = [], ranges = cm.listSelections();
+ for (var i = 0; i < ranges.length; i++) {
+ var match = ranges[i].empty() && findMatchingBracket(cm, ranges[i].head, false, config);
+ if (match && cm.getLine(match.from.line).length <= maxHighlightLen) {
+ var style = match.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket";
+ marks.push(cm.markText(match.from, Pos(match.from.line, match.from.ch + 1), {className: style}));
+ if (match.to && cm.getLine(match.to.line).length <= maxHighlightLen)
+ marks.push(cm.markText(match.to, Pos(match.to.line, match.to.ch + 1), {className: style}));
+ }
+ }
+
+ if (marks.length) {
+ // Kludge to work around the IE bug from issue #1193, where text
+ // input stops going to the textare whever this fires.
+ if (ie_lt8 && cm.state.focused) cm.focus();
+
+ var clear = function() {
+ cm.operation(function() {
+ for (var i = 0; i < marks.length; i++) marks[i].clear();
+ });
+ };
+ if (autoclear) setTimeout(clear, 800);
+ else return clear;
+ }
+ }
+
+ var currentlyHighlighted = null;
+ function doMatchBrackets(cm) {
+ cm.operation(function() {
+ if (currentlyHighlighted) {currentlyHighlighted(); currentlyHighlighted = null;}
+ currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets);
+ });
+ }
+
+ CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) {
+ if (old && old != CodeMirror.Init)
+ cm.off("cursorActivity", doMatchBrackets);
+ if (val) {
+ cm.state.matchBrackets = typeof val == "object" ? val : {};
+ cm.on("cursorActivity", doMatchBrackets);
+ }
+ });
+
+ CodeMirror.defineExtension("matchBrackets", function() {matchBrackets(this, true);});
+ CodeMirror.defineExtension("findMatchingBracket", function(pos, strict, config){
+ return findMatchingBracket(this, pos, strict, config);
+ });
+ CodeMirror.defineExtension("scanForBracket", function(pos, dir, style, config){
+ return scanForBracket(this, pos, dir, style, config);
+ });
+ });
+
+
+/***/ },
+/* 6 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ var defaults = {
+ pairs: "()[]{}''\"\"",
+ triples: "",
+ explode: "[]{}"
+ };
+
+ var Pos = CodeMirror.Pos;
+
+ CodeMirror.defineOption("autoCloseBrackets", false, function(cm, val, old) {
+ if (old && old != CodeMirror.Init) {
+ cm.removeKeyMap(keyMap);
+ cm.state.closeBrackets = null;
+ }
+ if (val) {
+ cm.state.closeBrackets = val;
+ cm.addKeyMap(keyMap);
+ }
+ });
+
+ function getOption(conf, name) {
+ if (name == "pairs" && typeof conf == "string") return conf;
+ if (typeof conf == "object" && conf[name] != null) return conf[name];
+ return defaults[name];
+ }
+
+ var bind = defaults.pairs + "`";
+ var keyMap = {Backspace: handleBackspace, Enter: handleEnter};
+ for (var i = 0; i < bind.length; i++)
+ keyMap["'" + bind.charAt(i) + "'"] = handler(bind.charAt(i));
+
+ function handler(ch) {
+ return function(cm) { return handleChar(cm, ch); };
+ }
+
+ function getConfig(cm) {
+ var deflt = cm.state.closeBrackets;
+ if (!deflt) return null;
+ var mode = cm.getModeAt(cm.getCursor());
+ return mode.closeBrackets || deflt;
+ }
+
+ function handleBackspace(cm) {
+ var conf = getConfig(cm);
+ if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass;
+
+ var pairs = getOption(conf, "pairs");
+ var ranges = cm.listSelections();
+ for (var i = 0; i < ranges.length; i++) {
+ if (!ranges[i].empty()) return CodeMirror.Pass;
+ var around = charsAround(cm, ranges[i].head);
+ if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass;
+ }
+ for (var i = ranges.length - 1; i >= 0; i--) {
+ var cur = ranges[i].head;
+ cm.replaceRange("", Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1), "+delete");
+ }
+ }
+
+ function handleEnter(cm) {
+ var conf = getConfig(cm);
+ var explode = conf && getOption(conf, "explode");
+ if (!explode || cm.getOption("disableInput")) return CodeMirror.Pass;
+
+ var ranges = cm.listSelections();
+ for (var i = 0; i < ranges.length; i++) {
+ if (!ranges[i].empty()) return CodeMirror.Pass;
+ var around = charsAround(cm, ranges[i].head);
+ if (!around || explode.indexOf(around) % 2 != 0) return CodeMirror.Pass;
+ }
+ cm.operation(function() {
+ 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);
+ }
+ });
+ }
+
+ function contractSelection(sel) {
+ var inverted = CodeMirror.cmpPos(sel.anchor, sel.head) > 0;
+ return {anchor: new Pos(sel.anchor.line, sel.anchor.ch + (inverted ? -1 : 1)),
+ head: new Pos(sel.head.line, sel.head.ch + (inverted ? 1 : -1))};
+ }
+
+ function handleChar(cm, ch) {
+ var conf = getConfig(cm);
+ if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass;
+
+ var pairs = getOption(conf, "pairs");
+ var pos = pairs.indexOf(ch);
+ if (pos == -1) return CodeMirror.Pass;
+ var triples = getOption(conf, "triples");
+
+ var identical = pairs.charAt(pos + 1) == ch;
+ var ranges = cm.listSelections();
+ var opening = pos % 2 == 0;
+
+ var type;
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i], cur = range.head, curType;
+ var next = cm.getRange(cur, Pos(cur.line, cur.ch + 1));
+ if (opening && !range.empty()) {
+ curType = "surround";
+ } else if ((identical || !opening) && next == ch) {
+ if (triples.indexOf(ch) >= 0 && cm.getRange(cur, Pos(cur.line, cur.ch + 3)) == ch + ch + ch)
+ curType = "skipThree";
+ else
+ curType = "skip";
+ } else if (identical && cur.ch > 1 && triples.indexOf(ch) >= 0 &&
+ cm.getRange(Pos(cur.line, cur.ch - 2), cur) == ch + ch &&
+ (cur.ch <= 2 || cm.getRange(Pos(cur.line, cur.ch - 3), Pos(cur.line, cur.ch - 2)) != ch)) {
+ curType = "addFour";
+ } else if (identical) {
+ if (!CodeMirror.isWordChar(next) && enteringString(cm, cur, ch)) curType = "both";
+ else return CodeMirror.Pass;
+ } else if (opening && (cm.getLine(cur.line).length == cur.ch ||
+ isClosingBracket(next, pairs) ||
+ /\s/.test(next))) {
+ curType = "both";
+ } else {
+ return CodeMirror.Pass;
+ }
+ if (!type) type = curType;
+ else if (type != curType) return CodeMirror.Pass;
+ }
+
+ var left = pos % 2 ? pairs.charAt(pos - 1) : ch;
+ var right = pos % 2 ? ch : pairs.charAt(pos + 1);
+ cm.operation(function() {
+ if (type == "skip") {
+ cm.execCommand("goCharRight");
+ } else if (type == "skipThree") {
+ for (var i = 0; i < 3; i++)
+ cm.execCommand("goCharRight");
+ } else if (type == "surround") {
+ var sels = cm.getSelections();
+ for (var i = 0; i < sels.length; i++)
+ sels[i] = left + sels[i] + right;
+ cm.replaceSelections(sels, "around");
+ sels = cm.listSelections().slice();
+ for (var i = 0; i < sels.length; i++)
+ sels[i] = contractSelection(sels[i]);
+ cm.setSelections(sels);
+ } else if (type == "both") {
+ cm.replaceSelection(left + right, null);
+ cm.triggerElectric(left + right);
+ cm.execCommand("goCharLeft");
+ } else if (type == "addFour") {
+ cm.replaceSelection(left + left + left + left, "before");
+ cm.execCommand("goCharRight");
+ }
+ });
+ }
+
+ function isClosingBracket(ch, pairs) {
+ var pos = pairs.lastIndexOf(ch);
+ return pos > -1 && pos % 2 == 1;
+ }
+
+ function charsAround(cm, pos) {
+ var str = cm.getRange(Pos(pos.line, pos.ch - 1),
+ Pos(pos.line, pos.ch + 1));
+ return str.length == 2 ? str : null;
+ }
+
+ // Project the token type that will exists after the given char is
+ // typed, and use it to determine whether it would cause the start
+ // of a string token.
+ function enteringString(cm, pos, ch) {
+ var line = cm.getLine(pos.line);
+ var token = cm.getTokenAt(pos);
+ if (/\bstring2?\b/.test(token.type)) return false;
+ var stream = new CodeMirror.StringStream(line.slice(0, pos.ch) + ch + line.slice(pos.ch), 4);
+ stream.pos = stream.start = token.start;
+ for (;;) {
+ var type1 = cm.getMode().token(stream, token.state);
+ if (stream.pos >= pos.ch + 1) return /\bstring2?\b/.test(type1);
+ stream.start = stream.pos;
+ }
+ }
+ });
+
+
+/***/ },
+/* 7 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ "use strict";
+
+ var noOptions = {};
+ var nonWS = /[^\s\u00a0]/;
+ var Pos = CodeMirror.Pos;
+
+ function firstNonWS(str) {
+ var found = str.search(nonWS);
+ return found == -1 ? 0 : found;
+ }
+
+ CodeMirror.commands.toggleComment = function(cm) {
+ cm.toggleComment();
+ };
+
+ CodeMirror.defineExtension("toggleComment", function(options) {
+ if (!options) options = noOptions;
+ var cm = this;
+ var minLine = Infinity, ranges = this.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, options)) mode = "un";
+ else { cm.lineComment(from, to, options); mode = "line"; }
+ } else if (mode == "un") {
+ cm.uncomment(from, to, options);
+ } else {
+ cm.lineComment(from, to, options);
+ }
+ }
+ });
+
+ // Rough heuristic to try and detect lines that are part of multi-line string
+ function probablyInsideString(cm, pos, line) {
+ return /\bstring\b/.test(cm.getTokenTypeAt(Pos(pos.line, 0))) && !/^[\'\"`]/.test(line)
+ }
+
+ CodeMirror.defineExtension("lineComment", function(from, to, options) {
+ if (!options) options = noOptions;
+ var self = this, mode = self.getModeAt(from);
+ var firstLine = self.getLine(from.line);
+ if (firstLine == null || probablyInsideString(self, from, firstLine)) return;
+
+ var commentString = options.lineComment || mode.lineComment;
+ if (!commentString) {
+ if (options.blockCommentStart || mode.blockCommentStart) {
+ options.fullLines = true;
+ self.blockComment(from, to, options);
+ }
+ return;
+ }
+
+ var end = Math.min(to.ch != 0 || to.line == from.line ? to.line + 1 : to.line, self.lastLine() + 1);
+ var pad = options.padding == null ? " " : options.padding;
+ var blankLines = options.commentBlankLines || from.line == to.line;
+
+ self.operation(function() {
+ if (options.indent) {
+ var baseString = null;
+ for (var i = from.line; i < end; ++i) {
+ var line = self.getLine(i);
+ var whitespace = line.slice(0, firstNonWS(line));
+ if (baseString == null || baseString.length > whitespace.length) {
+ baseString = whitespace;
+ }
+ }
+ for (var i = from.line; i < end; ++i) {
+ var line = self.getLine(i), cut = baseString.length;
+ if (!blankLines && !nonWS.test(line)) continue;
+ if (line.slice(0, cut) != baseString) cut = firstNonWS(line);
+ self.replaceRange(baseString + commentString + pad, Pos(i, 0), Pos(i, cut));
+ }
+ } else {
+ for (var i = from.line; i < end; ++i) {
+ if (blankLines || nonWS.test(self.getLine(i)))
+ self.replaceRange(commentString + pad, Pos(i, 0));
+ }
+ }
+ });
+ });
+
+ CodeMirror.defineExtension("blockComment", function(from, to, options) {
+ if (!options) options = noOptions;
+ var self = this, mode = self.getModeAt(from);
+ var startString = options.blockCommentStart || mode.blockCommentStart;
+ var endString = options.blockCommentEnd || mode.blockCommentEnd;
+ if (!startString || !endString) {
+ if ((options.lineComment || mode.lineComment) && options.fullLines != false)
+ self.lineComment(from, to, options);
+ return;
+ }
+
+ var end = Math.min(to.line, self.lastLine());
+ if (end != from.line && to.ch == 0 && nonWS.test(self.getLine(end))) --end;
+
+ var pad = options.padding == null ? " " : options.padding;
+ if (from.line > end) return;
+
+ self.operation(function() {
+ if (options.fullLines != false) {
+ var lastLineHasText = nonWS.test(self.getLine(end));
+ self.replaceRange(pad + endString, Pos(end));
+ self.replaceRange(startString + pad, Pos(from.line, 0));
+ var lead = options.blockCommentLead || mode.blockCommentLead;
+ if (lead != null) for (var i = from.line + 1; i <= end; ++i)
+ if (i != end || lastLineHasText)
+ self.replaceRange(lead + pad, Pos(i, 0));
+ } else {
+ self.replaceRange(endString, to);
+ self.replaceRange(startString, from);
+ }
+ });
+ });
+
+ CodeMirror.defineExtension("uncomment", function(from, to, options) {
+ if (!options) options = noOptions;
+ var self = this, mode = self.getModeAt(from);
+ var end = Math.min(to.ch != 0 || to.line == from.line ? to.line : to.line - 1, self.lastLine()), start = Math.min(from.line, end);
+
+ // Try finding line comments
+ var lineString = options.lineComment || mode.lineComment, lines = [];
+ var pad = options.padding == null ? " " : options.padding, didSomething;
+ lineComment: {
+ if (!lineString) break lineComment;
+ for (var i = start; i <= end; ++i) {
+ var line = self.getLine(i);
+ var found = line.indexOf(lineString);
+ if (found > -1 && !/comment/.test(self.getTokenTypeAt(Pos(i, found + 1)))) found = -1;
+ if (found == -1 && (i != end || i == start) && nonWS.test(line)) break lineComment;
+ if (found > -1 && nonWS.test(line.slice(0, found))) break lineComment;
+ lines.push(line);
+ }
+ self.operation(function() {
+ for (var i = start; i <= end; ++i) {
+ var line = lines[i - start];
+ var pos = line.indexOf(lineString), endPos = pos + lineString.length;
+ if (pos < 0) continue;
+ if (line.slice(endPos, endPos + pad.length) == pad) endPos += pad.length;
+ didSomething = true;
+ self.replaceRange("", Pos(i, pos), Pos(i, endPos));
+ }
+ });
+ if (didSomething) return true;
+ }
+
+ // Try block comments
+ var startString = options.blockCommentStart || mode.blockCommentStart;
+ var endString = options.blockCommentEnd || mode.blockCommentEnd;
+ if (!startString || !endString) return false;
+ var lead = options.blockCommentLead || mode.blockCommentLead;
+ var startLine = self.getLine(start), endLine = end == start ? startLine : self.getLine(end);
+ var open = startLine.indexOf(startString), close = endLine.lastIndexOf(endString);
+ if (close == -1 && start != end) {
+ endLine = self.getLine(--end);
+ close = endLine.lastIndexOf(endString);
+ }
+ if (open == -1 || close == -1 ||
+ !/comment/.test(self.getTokenTypeAt(Pos(start, open + 1))) ||
+ !/comment/.test(self.getTokenTypeAt(Pos(end, close + 1))))
+ return false;
+
+ // Avoid killing block comments completely outside the selection.
+ // Positions of the last startString before the start of the selection, and the first endString after it.
+ var lastStart = startLine.lastIndexOf(startString, from.ch);
+ var firstEnd = lastStart == -1 ? -1 : startLine.slice(0, from.ch).indexOf(endString, lastStart + startString.length);
+ if (lastStart != -1 && firstEnd != -1 && firstEnd + endString.length != from.ch) return false;
+ // Positions of the first endString after the end of the selection, and the last startString before it.
+ firstEnd = endLine.indexOf(endString, to.ch);
+ var almostLastStart = endLine.slice(to.ch).lastIndexOf(startString, firstEnd - to.ch);
+ lastStart = (firstEnd == -1 || almostLastStart == -1) ? -1 : to.ch + almostLastStart;
+ if (firstEnd != -1 && lastStart != -1 && lastStart != to.ch) return false;
+
+ self.operation(function() {
+ self.replaceRange("", Pos(end, close - (pad && endLine.slice(close - pad.length, close) == pad ? pad.length : 0)),
+ Pos(end, close + endString.length));
+ var openEnd = open + startString.length;
+ if (pad && startLine.slice(openEnd, openEnd + pad.length) == pad) openEnd += pad.length;
+ self.replaceRange("", Pos(start, open), Pos(start, openEnd));
+ if (lead) for (var i = start + 1; i <= end; ++i) {
+ var line = self.getLine(i), found = line.indexOf(lead);
+ if (found == -1 || nonWS.test(line.slice(0, found))) continue;
+ var foundEnd = found + lead.length;
+ if (pad && line.slice(foundEnd, foundEnd + pad.length) == pad) foundEnd += pad.length;
+ self.replaceRange("", Pos(i, found), Pos(i, foundEnd));
+ }
+ });
+ return true;
+ });
+ });
+
+
+/***/ },
+/* 8 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ // TODO actually recognize syntax of TypeScript constructs
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ "use strict";
+
+ function expressionAllowed(stream, state, backUp) {
+ return /^(?:operator|sof|keyword c|case|new|[\[{}\(,;:]|=>)$/.test(state.lastType) ||
+ (state.lastType == "quasi" && /\{\s*$/.test(stream.string.slice(0, stream.pos - (backUp || 0))))
+ }
+
+ CodeMirror.defineMode("javascript", function(config, parserConfig) {
+ var indentUnit = config.indentUnit;
+ var statementIndent = parserConfig.statementIndent;
+ var jsonldMode = parserConfig.jsonld;
+ var jsonMode = parserConfig.json || jsonldMode;
+ var isTS = parserConfig.typescript;
+ var wordRE = parserConfig.wordCharacters || /[\w$\xa1-\uffff]/;
+
+ // Tokenizer
+
+ var keywords = function(){
+ function kw(type) {return {type: type, style: "keyword"};}
+ var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c");
+ var operator = kw("operator"), atom = {type: "atom", style: "atom"};
+
+ var jsKeywords = {
+ "if": kw("if"), "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B,
+ "return": C, "break": C, "continue": C, "new": kw("new"), "delete": C, "throw": C, "debugger": C,
+ "var": kw("var"), "const": kw("var"), "let": kw("var"),
+ "function": kw("function"), "catch": kw("catch"),
+ "for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"),
+ "in": operator, "typeof": operator, "instanceof": operator,
+ "true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom,
+ "this": kw("this"), "class": kw("class"), "super": kw("atom"),
+ "yield": C, "export": kw("export"), "import": kw("import"), "extends": C,
+ "await": C, "async": kw("async")
+ };
+
+ // Extend the 'normal' keywords with the TypeScript language extensions
+ if (isTS) {
+ var type = {type: "variable", style: "variable-3"};
+ var tsKeywords = {
+ // object-like things
+ "interface": kw("class"),
+ "implements": C,
+ "namespace": C,
+ "module": kw("module"),
+ "enum": kw("module"),
+
+ // scope modifiers
+ "public": kw("modifier"),
+ "private": kw("modifier"),
+ "protected": kw("modifier"),
+ "abstract": kw("modifier"),
+
+ // operators
+ "as": operator,
+
+ // types
+ "string": type, "number": type, "boolean": type, "any": type
+ };
+
+ for (var attr in tsKeywords) {
+ jsKeywords[attr] = tsKeywords[attr];
+ }
+ }
+
+ return jsKeywords;
+ }();
+
+ 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;
+ while ((next = stream.next()) != null) {
+ if (!escaped) {
+ if (next == "/" && !inSet) return;
+ if (next == "[") inSet = true;
+ else if (inSet && next == "]") inSet = false;
+ }
+ escaped = !escaped && next == "\\";
+ }
+ }
+
+ // Used as scratch variables to communicate multiple values without
+ // consing up tons of objects.
+ var type, content;
+ function ret(tp, style, cont) {
+ type = tp; content = cont;
+ return style;
+ }
+ function tokenBase(stream, state) {
+ var ch = stream.next();
+ if (ch == '"' || ch == "'") {
+ state.tokenize = tokenString(ch);
+ return state.tokenize(stream, state);
+ } else if (ch == "." && stream.match(/^\d+(?:[eE][+\-]?\d+)?/)) {
+ return ret("number", "number");
+ } else if (ch == "." && stream.match("..")) {
+ return ret("spread", "meta");
+ } else if (/[\[\]{}\(\),;\:\.]/.test(ch)) {
+ return ret(ch);
+ } else if (ch == "=" && stream.eat(">")) {
+ return ret("=>", "operator");
+ } else if (ch == "0" && stream.eat(/x/i)) {
+ stream.eatWhile(/[\da-f]/i);
+ return ret("number", "number");
+ } else if (ch == "0" && stream.eat(/o/i)) {
+ stream.eatWhile(/[0-7]/i);
+ return ret("number", "number");
+ } else if (ch == "0" && stream.eat(/b/i)) {
+ stream.eatWhile(/[01]/i);
+ return ret("number", "number");
+ } else if (/\d/.test(ch)) {
+ stream.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/);
+ return ret("number", "number");
+ } else if (ch == "/") {
+ if (stream.eat("*")) {
+ state.tokenize = tokenComment;
+ return tokenComment(stream, state);
+ } else if (stream.eat("/")) {
+ stream.skipToEnd();
+ return ret("comment", "comment");
+ } else if (expressionAllowed(stream, state, 1)) {
+ readRegexp(stream);
+ stream.match(/^\b(([gimyu])(?![gimyu]*\2))+\b/);
+ return ret("regexp", "string-2");
+ } else {
+ stream.eatWhile(isOperatorChar);
+ return ret("operator", "operator", stream.current());
+ }
+ } else if (ch == "`") {
+ state.tokenize = tokenQuasi;
+ return tokenQuasi(stream, state);
+ } else if (ch == "#") {
+ stream.skipToEnd();
+ return ret("error", "error");
+ } else if (isOperatorChar.test(ch)) {
+ stream.eatWhile(isOperatorChar);
+ return ret("operator", "operator", stream.current());
+ } else if (wordRE.test(ch)) {
+ stream.eatWhile(wordRE);
+ var word = stream.current(), known = keywords.propertyIsEnumerable(word) && keywords[word];
+ return (known && state.lastType != ".") ? ret(known.type, known.style, word) :
+ ret("variable", "variable", word);
+ }
+ }
+
+ 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 == "\\";
+ }
+ if (!escaped) state.tokenize = tokenBase;
+ return ret("string", "string");
+ };
+ }
+
+ function tokenComment(stream, state) {
+ var maybeEnd = false, ch;
+ while (ch = stream.next()) {
+ if (ch == "/" && maybeEnd) {
+ state.tokenize = tokenBase;
+ break;
+ }
+ maybeEnd = (ch == "*");
+ }
+ return ret("comment", "comment");
+ }
+
+ function tokenQuasi(stream, state) {
+ var escaped = false, next;
+ while ((next = stream.next()) != null) {
+ if (!escaped && (next == "`" || next == "$" && stream.eat("{"))) {
+ state.tokenize = tokenBase;
+ break;
+ }
+ escaped = !escaped && next == "\\";
+ }
+ return ret("quasi", "string-2", stream.current());
+ }
+
+ var brackets = "([{}])";
+ // This is a crude lookahead trick to try and notice that we're
+ // parsing the argument patterns for a fat-arrow function before we
+ // actually hit the arrow token. It only works if the arrow is on
+ // the same line as the arguments and there's no strange noise
+ // (comments) in between. Fallback is to only notice when we hit the
+ // arrow, and not declare the arguments as locals for the arrow
+ // body.
+ function findFatArrow(stream, state) {
+ if (state.fatArrowAt) state.fatArrowAt = null;
+ var arrow = stream.string.indexOf("=>", stream.start);
+ if (arrow < 0) return;
+
+ var depth = 0, sawSomething = false;
+ for (var pos = arrow - 1; pos >= 0; --pos) {
+ var ch = stream.string.charAt(pos);
+ var bracket = brackets.indexOf(ch);
+ if (bracket >= 0 && bracket < 3) {
+ if (!depth) { ++pos; break; }
+ if (--depth == 0) break;
+ } else if (bracket >= 3 && bracket < 6) {
+ ++depth;
+ } else if (wordRE.test(ch)) {
+ sawSomething = true;
+ } else if (/["'\/]/.test(ch)) {
+ return;
+ } else if (sawSomething && !depth) {
+ ++pos;
+ break;
+ }
+ }
+ if (sawSomething && !depth) state.fatArrowAt = pos;
+ }
+
+ // Parser
+
+ 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;
+ this.column = column;
+ this.type = type;
+ this.prev = prev;
+ this.info = info;
+ if (align != null) this.align = align;
+ }
+
+ function inScope(state, varname) {
+ for (var v = state.localVars; v; v = v.next)
+ if (v.name == varname) return true;
+ for (var cx = state.context; cx; cx = cx.prev) {
+ for (var v = cx.vars; v; v = v.next)
+ if (v.name == varname) return true;
+ }
+ }
+
+ function parseJS(state, style, type, content, stream) {
+ var cc = state.cc;
+ // Communicate our context to the combinators.
+ // (Less wasteful than consing up a hundred closures on every call.)
+ cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; cx.style = style;
+
+ if (!state.lexical.hasOwnProperty("align"))
+ state.lexical.align = true;
+
+ while(true) {
+ var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement;
+ if (combinator(type, content)) {
+ while(cc.length && cc[cc.length - 1].lex)
+ cc.pop()();
+ if (cx.marked) return cx.marked;
+ if (type == "variable" && inScope(state, content)) return "variable-2";
+ return style;
+ }
+ }
+ }
+
+ // Combinator utils
+
+ var cx = {state: null, column: null, marked: null, cc: null};
+ function pass() {
+ for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]);
+ }
+ function cont() {
+ pass.apply(null, arguments);
+ return true;
+ }
+ function register(varname) {
+ function inList(list) {
+ for (var v = list; v; v = v.next)
+ if (v.name == varname) return true;
+ return false;
+ }
+ var state = cx.state;
+ cx.marked = "def";
+ if (state.context) {
+ if (inList(state.localVars)) return;
+ state.localVars = {name: varname, next: state.localVars};
+ } else {
+ if (inList(state.globalVars)) return;
+ if (parserConfig.globalVars)
+ state.globalVars = {name: varname, next: state.globalVars};
+ }
+ }
+
+ // Combinators
+
+ var defaultVars = {name: "this", next: {name: "arguments"}};
+ function pushcontext() {
+ cx.state.context = {prev: cx.state.context, vars: cx.state.localVars};
+ cx.state.localVars = defaultVars;
+ }
+ function popcontext() {
+ cx.state.localVars = cx.state.context.vars;
+ cx.state.context = cx.state.context.prev;
+ }
+ function pushlex(type, info) {
+ var result = function() {
+ var state = cx.state, indent = state.indented;
+ if (state.lexical.type == "stat") indent = state.lexical.indented;
+ else for (var outer = state.lexical; outer && outer.type == ")" && outer.align; outer = outer.prev)
+ indent = outer.indented;
+ state.lexical = new JSLexical(indent, cx.stream.column(), type, null, state.lexical, info);
+ };
+ result.lex = true;
+ return result;
+ }
+ function poplex() {
+ var state = cx.state;
+ if (state.lexical.prev) {
+ if (state.lexical.type == ")")
+ state.indented = state.lexical.indented;
+ state.lexical = state.lexical.prev;
+ }
+ }
+ poplex.lex = true;
+
+ function expect(wanted) {
+ function exp(type) {
+ if (type == wanted) return cont();
+ else if (wanted == ";") return pass();
+ else return cont(exp);
+ };
+ return exp;
+ }
+
+ function statement(type, value) {
+ if (type == "var") return cont(pushlex("vardef", value.length), vardef, expect(";"), poplex);
+ if (type == "keyword a") return cont(pushlex("form"), expression, statement, poplex);
+ if (type == "keyword b") return cont(pushlex("form"), statement, poplex);
+ if (type == "{") return cont(pushlex("}"), block, poplex);
+ if (type == ";") return cont();
+ if (type == "if") {
+ if (cx.state.lexical.info == "else" && cx.state.cc[cx.state.cc.length - 1] == poplex)
+ cx.state.cc.pop()();
+ return cont(pushlex("form"), expression, statement, poplex, maybeelse);
+ }
+ if (type == "function") return cont(functiondef);
+ if (type == "for") return cont(pushlex("form"), forspec, statement, poplex);
+ if (type == "variable") return cont(pushlex("stat"), maybelabel);
+ if (type == "switch") return cont(pushlex("form"), expression, pushlex("}", "switch"), expect("{"),
+ block, poplex, poplex);
+ if (type == "case") return cont(expression, expect(":"));
+ if (type == "default") return cont(expect(":"));
+ if (type == "catch") return cont(pushlex("form"), pushcontext, expect("("), funarg, expect(")"),
+ statement, poplex, popcontext);
+ if (type == "class") return cont(pushlex("form"), className, poplex);
+ if (type == "export") return cont(pushlex("stat"), afterExport, poplex);
+ if (type == "import") return cont(pushlex("stat"), afterImport, poplex);
+ if (type == "module") return cont(pushlex("form"), pattern, pushlex("}"), expect("{"), block, poplex, poplex)
+ if (type == "async") return cont(statement)
+ return pass(pushlex("stat"), expression, expect(";"), poplex);
+ }
+ function expression(type) {
+ return expressionInner(type, false);
+ }
+ function expressionNoComma(type) {
+ return expressionInner(type, true);
+ }
+ function expressionInner(type, noComma) {
+ if (cx.state.fatArrowAt == cx.stream.start) {
+ var body = noComma ? arrowBodyNoComma : arrowBody;
+ if (type == "(") return cont(pushcontext, pushlex(")"), commasep(pattern, ")"), poplex, expect("=>"), body, popcontext);
+ else if (type == "variable") return pass(pushcontext, pattern, expect("=>"), body, popcontext);
+ }
+
+ var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma;
+ if (atomicTypes.hasOwnProperty(type)) return cont(maybeop);
+ if (type == "function") return cont(functiondef, maybeop);
+ if (type == "keyword c") return cont(noComma ? maybeexpressionNoComma : maybeexpression);
+ if (type == "(") return cont(pushlex(")"), maybeexpression, comprehension, expect(")"), poplex, maybeop);
+ if (type == "operator" || type == "spread") return cont(noComma ? expressionNoComma : expression);
+ if (type == "[") return cont(pushlex("]"), arrayLiteral, poplex, maybeop);
+ if (type == "{") return contCommasep(objprop, "}", null, maybeop);
+ if (type == "quasi") return pass(quasi, maybeop);
+ if (type == "new") return cont(maybeTarget(noComma));
+ return cont();
+ }
+ function maybeexpression(type) {
+ if (type.match(/[;\}\)\],]/)) return pass();
+ return pass(expression);
+ }
+ function maybeexpressionNoComma(type) {
+ if (type.match(/[;\}\)\],]/)) return pass();
+ return pass(expressionNoComma);
+ }
+
+ function maybeoperatorComma(type, value) {
+ if (type == ",") return cont(expression);
+ return maybeoperatorNoComma(type, value, false);
+ }
+ function maybeoperatorNoComma(type, value, noComma) {
+ var me = noComma == false ? maybeoperatorComma : maybeoperatorNoComma;
+ var expr = noComma == false ? expression : expressionNoComma;
+ if (type == "=>") return cont(pushcontext, noComma ? arrowBodyNoComma : arrowBody, popcontext);
+ if (type == "operator") {
+ if (/\+\+|--/.test(value)) return cont(me);
+ if (value == "?") return cont(expression, expect(":"), expr);
+ return cont(expr);
+ }
+ if (type == "quasi") { return pass(quasi, me); }
+ if (type == ";") return;
+ if (type == "(") return contCommasep(expressionNoComma, ")", "call", me);
+ if (type == ".") return cont(property, me);
+ if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me);
+ }
+ function quasi(type, value) {
+ if (type != "quasi") return pass();
+ if (value.slice(value.length - 2) != "${") return cont(quasi);
+ return cont(expression, continueQuasi);
+ }
+ function continueQuasi(type) {
+ if (type == "}") {
+ cx.marked = "string-2";
+ cx.state.tokenize = tokenQuasi;
+ return cont(quasi);
+ }
+ }
+ function arrowBody(type) {
+ findFatArrow(cx.stream, cx.state);
+ return pass(type == "{" ? statement : expression);
+ }
+ function arrowBodyNoComma(type) {
+ findFatArrow(cx.stream, cx.state);
+ return pass(type == "{" ? statement : expressionNoComma);
+ }
+ function maybeTarget(noComma) {
+ return function(type) {
+ if (type == ".") return cont(noComma ? targetNoComma : target);
+ else return pass(noComma ? expressionNoComma : expression);
+ };
+ }
+ function target(_, value) {
+ if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorComma); }
+ }
+ function targetNoComma(_, value) {
+ if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorNoComma); }
+ }
+ function maybelabel(type) {
+ if (type == ":") return cont(poplex, statement);
+ return pass(maybeoperatorComma, expect(";"), poplex);
+ }
+ function property(type) {
+ if (type == "variable") {cx.marked = "property"; return cont();}
+ }
+ function objprop(type, value) {
+ if (type == "variable" || cx.style == "keyword") {
+ cx.marked = "property";
+ if (value == "get" || value == "set") return cont(getterSetter);
+ return cont(afterprop);
+ } else if (type == "number" || type == "string") {
+ cx.marked = jsonldMode ? "property" : (cx.style + " property");
+ return cont(afterprop);
+ } else if (type == "jsonld-keyword") {
+ return cont(afterprop);
+ } else if (type == "modifier") {
+ return cont(objprop)
+ } else if (type == "[") {
+ return cont(expression, expect("]"), afterprop);
+ } else if (type == "spread") {
+ return cont(expression);
+ }
+ }
+ function getterSetter(type) {
+ if (type != "variable") return pass(afterprop);
+ cx.marked = "property";
+ return cont(functiondef);
+ }
+ function afterprop(type) {
+ if (type == ":") return cont(expressionNoComma);
+ if (type == "(") return pass(functiondef);
+ }
+ function commasep(what, end) {
+ function proceed(type, value) {
+ if (type == ",") {
+ var lex = cx.state.lexical;
+ if (lex.info == "call") lex.pos = (lex.pos || 0) + 1;
+ return cont(what, proceed);
+ }
+ if (type == end || value == end) return cont();
+ return cont(expect(end));
+ }
+ return function(type, value) {
+ if (type == end || value == end) return cont();
+ return pass(what, proceed);
+ };
+ }
+ function contCommasep(what, end, info) {
+ for (var i = 3; i < arguments.length; i++)
+ cx.cc.push(arguments[i]);
+ return cont(pushlex(end, info), commasep(what, end), poplex);
+ }
+ function block(type) {
+ if (type == "}") return cont();
+ return pass(statement, block);
+ }
+ function maybetype(type) {
+ if (isTS && type == ":") return cont(typeexpr);
+ }
+ function maybedefault(_, value) {
+ if (value == "=") return cont(expressionNoComma);
+ }
+ function typeexpr(type) {
+ if (type == "variable") {cx.marked = "variable-3"; return cont(afterType);}
+ }
+ function afterType(type, value) {
+ if (value == "<") return cont(commasep(typeexpr, ">"), afterType)
+ if (type == "[") return cont(expect("]"), afterType)
+ }
+ function vardef() {
+ return pass(pattern, maybetype, maybeAssign, vardefCont);
+ }
+ function pattern(type, value) {
+ if (type == "modifier") return cont(pattern)
+ if (type == "variable") { register(value); return cont(); }
+ if (type == "spread") return cont(pattern);
+ if (type == "[") return contCommasep(pattern, "]");
+ if (type == "{") return contCommasep(proppattern, "}");
+ }
+ function proppattern(type, value) {
+ if (type == "variable" && !cx.stream.match(/^\s*:/, false)) {
+ register(value);
+ return cont(maybeAssign);
+ }
+ if (type == "variable") cx.marked = "property";
+ if (type == "spread") return cont(pattern);
+ if (type == "}") return pass();
+ return cont(expect(":"), pattern, maybeAssign);
+ }
+ function maybeAssign(_type, value) {
+ if (value == "=") return cont(expressionNoComma);
+ }
+ function vardefCont(type) {
+ if (type == ",") return cont(vardef);
+ }
+ function maybeelse(type, value) {
+ if (type == "keyword b" && value == "else") return cont(pushlex("form", "else"), statement, poplex);
+ }
+ function forspec(type) {
+ if (type == "(") return cont(pushlex(")"), forspec1, expect(")"), poplex);
+ }
+ function forspec1(type) {
+ if (type == "var") return cont(vardef, expect(";"), forspec2);
+ if (type == ";") return cont(forspec2);
+ if (type == "variable") return cont(formaybeinof);
+ return pass(expression, expect(";"), forspec2);
+ }
+ function formaybeinof(_type, value) {
+ if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression); }
+ return cont(maybeoperatorComma, forspec2);
+ }
+ function forspec2(type, value) {
+ if (type == ";") return cont(forspec3);
+ if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression); }
+ return pass(expression, expect(";"), forspec3);
+ }
+ function forspec3(type) {
+ if (type != ")") cont(expression);
+ }
+ function functiondef(type, value) {
+ if (value == "*") {cx.marked = "keyword"; return cont(functiondef);}
+ if (type == "variable") {register(value); return cont(functiondef);}
+ if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, maybetype, statement, popcontext);
+ }
+ function funarg(type) {
+ if (type == "spread") return cont(funarg);
+ return pass(pattern, maybetype, maybedefault);
+ }
+ function className(type, value) {
+ if (type == "variable") {register(value); return cont(classNameAfter);}
+ }
+ function classNameAfter(type, value) {
+ if (value == "extends") return cont(expression, classNameAfter);
+ if (type == "{") return cont(pushlex("}"), classBody, poplex);
+ }
+ function classBody(type, value) {
+ if (type == "variable" || cx.style == "keyword") {
+ if (value == "static") {
+ cx.marked = "keyword";
+ return cont(classBody);
+ }
+ cx.marked = "property";
+ if (value == "get" || value == "set") return cont(classGetterSetter, functiondef, classBody);
+ return cont(functiondef, classBody);
+ }
+ if (value == "*") {
+ cx.marked = "keyword";
+ return cont(classBody);
+ }
+ if (type == ";") return cont(classBody);
+ if (type == "}") return cont();
+ }
+ function classGetterSetter(type) {
+ if (type != "variable") return pass();
+ cx.marked = "property";
+ return cont();
+ }
+ function afterExport(_type, value) {
+ if (value == "*") { cx.marked = "keyword"; return cont(maybeFrom, expect(";")); }
+ if (value == "default") { cx.marked = "keyword"; return cont(expression, expect(";")); }
+ return pass(statement);
+ }
+ function afterImport(type) {
+ if (type == "string") return cont();
+ return pass(importSpec, maybeFrom);
+ }
+ function importSpec(type, value) {
+ if (type == "{") return contCommasep(importSpec, "}");
+ if (type == "variable") register(value);
+ if (value == "*") cx.marked = "keyword";
+ return cont(maybeAs);
+ }
+ function maybeAs(_type, value) {
+ if (value == "as") { cx.marked = "keyword"; return cont(importSpec); }
+ }
+ function maybeFrom(_type, value) {
+ if (value == "from") { cx.marked = "keyword"; return cont(expression); }
+ }
+ function arrayLiteral(type) {
+ if (type == "]") return cont();
+ return pass(expressionNoComma, maybeArrayComprehension);
+ }
+ function maybeArrayComprehension(type) {
+ if (type == "for") return pass(comprehension, expect("]"));
+ if (type == ",") return cont(commasep(maybeexpressionNoComma, "]"));
+ return pass(commasep(expressionNoComma, "]"));
+ }
+ function comprehension(type) {
+ if (type == "for") return cont(forspec, comprehension);
+ if (type == "if") return cont(expression, comprehension);
+ }
+
+ function isContinuedStatement(state, textAfter) {
+ return state.lastType == "operator" || state.lastType == "," ||
+ isOperatorChar.test(textAfter.charAt(0)) ||
+ /[,.]/.test(textAfter.charAt(0));
+ }
+
+ // Interface
+
+ return {
+ startState: function(basecolumn) {
+ var state = {
+ tokenize: tokenBase,
+ lastType: "sof",
+ cc: [],
+ lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false),
+ localVars: parserConfig.localVars,
+ context: parserConfig.localVars && {vars: parserConfig.localVars},
+ indented: basecolumn || 0
+ };
+ if (parserConfig.globalVars && typeof parserConfig.globalVars == "object")
+ state.globalVars = parserConfig.globalVars;
+ return state;
+ },
+
+ token: function(stream, state) {
+ if (stream.sol()) {
+ if (!state.lexical.hasOwnProperty("align"))
+ state.lexical.align = false;
+ state.indented = stream.indentation();
+ findFatArrow(stream, state);
+ }
+ if (state.tokenize != tokenComment && stream.eatSpace()) return null;
+ var style = state.tokenize(stream, state);
+ if (type == "comment") return style;
+ state.lastType = type == "operator" && (content == "++" || content == "--") ? "incdec" : type;
+ return parseJS(state, style, type, content, stream);
+ },
+
+ indent: function(state, textAfter) {
+ if (state.tokenize == tokenComment) return CodeMirror.Pass;
+ if (state.tokenize != tokenBase) return 0;
+ var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical;
+ // Kludge to prevent 'maybelse' from blocking lexical scope pops
+ if (!/^\s*else\b/.test(textAfter)) for (var i = state.cc.length - 1; i >= 0; --i) {
+ var c = state.cc[i];
+ if (c == poplex) lexical = lexical.prev;
+ else if (c != maybeelse) break;
+ }
+ if (lexical.type == "stat" && firstChar == "}") lexical = lexical.prev;
+ if (statementIndent && lexical.type == ")" && lexical.prev.type == "stat")
+ lexical = lexical.prev;
+ var type = lexical.type, closing = firstChar == type;
+
+ if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info + 1 : 0);
+ else if (type == "form" && firstChar == "{") return lexical.indented;
+ else if (type == "form") return lexical.indented + indentUnit;
+ else if (type == "stat")
+ return lexical.indented + (isContinuedStatement(state, textAfter) ? statementIndent || indentUnit : 0);
+ else if (lexical.info == "switch" && !closing && parserConfig.doubleIndentSwitch != false)
+ return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit);
+ else if (lexical.align) return lexical.column + (closing ? 0 : 1);
+ else return lexical.indented + (closing ? 0 : indentUnit);
+ },
+
+ electricInput: /^\s*(?:case .*?:|default:|\{|\})$/,
+ blockCommentStart: jsonMode ? null : "/*",
+ blockCommentEnd: jsonMode ? null : "*/",
+ lineComment: jsonMode ? null : "//",
+ fold: "brace",
+ closeBrackets: "()[]{}''\"\"``",
+
+ helperType: jsonMode ? "json" : "javascript",
+ jsonldMode: jsonldMode,
+ jsonMode: jsonMode,
+
+ expressionAllowed: expressionAllowed,
+ skipExpression: function(state) {
+ var top = state.cc[state.cc.length - 1]
+ if (top == expression || top == expressionNoComma) state.cc.pop()
+ }
+ };
+ });
+
+ CodeMirror.registerHelper("wordChars", "javascript", /[\w$]/);
+
+ CodeMirror.defineMIME("text/javascript", "javascript");
+ CodeMirror.defineMIME("text/ecmascript", "javascript");
+ CodeMirror.defineMIME("application/javascript", "javascript");
+ CodeMirror.defineMIME("application/x-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 });
+
+ });
+
+
+/***/ },
+/* 9 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ "use strict";
+
+ var htmlConfig = {
+ autoSelfClosers: {'area': true, 'base': true, 'br': true, 'col': true, 'command': true,
+ 'embed': true, 'frame': true, 'hr': true, 'img': true, 'input': true,
+ 'keygen': true, 'link': true, 'meta': true, 'param': true, 'source': true,
+ 'track': true, 'wbr': true, 'menuitem': true},
+ implicitlyClosed: {'dd': true, 'li': true, 'optgroup': true, 'option': true, 'p': true,
+ 'rp': true, 'rt': true, 'tbody': true, 'td': true, 'tfoot': true,
+ 'th': true, 'tr': true},
+ contextGrabbers: {
+ 'dd': {'dd': true, 'dt': true},
+ 'dt': {'dd': true, 'dt': true},
+ 'li': {'li': true},
+ 'option': {'option': true, 'optgroup': true},
+ 'optgroup': {'optgroup': true},
+ 'p': {'address': true, 'article': true, 'aside': true, 'blockquote': true, 'dir': true,
+ 'div': true, 'dl': true, 'fieldset': true, 'footer': true, 'form': true,
+ 'h1': true, 'h2': true, 'h3': true, 'h4': true, 'h5': true, 'h6': true,
+ 'header': true, 'hgroup': true, 'hr': true, 'menu': true, 'nav': true, 'ol': true,
+ 'p': true, 'pre': true, 'section': true, 'table': true, 'ul': true},
+ 'rp': {'rp': true, 'rt': true},
+ 'rt': {'rp': true, 'rt': true},
+ 'tbody': {'tbody': true, 'tfoot': true},
+ 'td': {'td': true, 'th': true},
+ 'tfoot': {'tbody': true},
+ 'th': {'td': true, 'th': true},
+ 'thead': {'tbody': true, 'tfoot': true},
+ 'tr': {'tr': true}
+ },
+ doNotIndent: {"pre": true},
+ allowUnquoted: true,
+ allowMissing: true,
+ caseFold: true
+ }
+
+ var xmlConfig = {
+ autoSelfClosers: {},
+ implicitlyClosed: {},
+ contextGrabbers: {},
+ doNotIndent: {},
+ allowUnquoted: false,
+ allowMissing: false,
+ caseFold: false
+ }
+
+ CodeMirror.defineMode("xml", function(editorConf, config_) {
+ var indentUnit = editorConf.indentUnit
+ var config = {}
+ var defaults = config_.htmlMode ? htmlConfig : xmlConfig
+ for (var prop in defaults) config[prop] = defaults[prop]
+ for (var prop in config_) config[prop] = config_[prop]
+
+ // Return variables for tokenizers
+ var type, setStyle;
+
+ function inText(stream, state) {
+ function chain(parser) {
+ state.tokenize = parser;
+ return parser(stream, state);
+ }
+
+ var ch = stream.next();
+ if (ch == "<") {
+ if (stream.eat("!")) {
+ if (stream.eat("[")) {
+ if (stream.match("CDATA[")) return chain(inBlock("atom", "]]>"));
+ else return null;
+ } else if (stream.match("--")) {
+ return chain(inBlock("comment", "-->"));
+ } else if (stream.match("DOCTYPE", true, true)) {
+ stream.eatWhile(/[\w\._\-]/);
+ return chain(doctype(1));
+ } else {
+ return null;
+ }
+ } else if (stream.eat("?")) {
+ stream.eatWhile(/[\w\._\-]/);
+ state.tokenize = inBlock("meta", "?>");
+ return "meta";
+ } else {
+ type = stream.eat("/") ? "closeTag" : "openTag";
+ state.tokenize = inTag;
+ return "tag bracket";
+ }
+ } else if (ch == "&") {
+ var ok;
+ if (stream.eat("#")) {
+ if (stream.eat("x")) {
+ ok = stream.eatWhile(/[a-fA-F\d]/) && stream.eat(";");
+ } else {
+ ok = stream.eatWhile(/[\d]/) && stream.eat(";");
+ }
+ } else {
+ ok = stream.eatWhile(/[\w\.\-:]/) && stream.eat(";");
+ }
+ return ok ? "atom" : "error";
+ } else {
+ stream.eatWhile(/[^&<]/);
+ return null;
+ }
+ }
+ inText.isInText = true;
+
+ function inTag(stream, state) {
+ var ch = stream.next();
+ if (ch == ">" || (ch == "/" && stream.eat(">"))) {
+ state.tokenize = inText;
+ type = ch == ">" ? "endTag" : "selfcloseTag";
+ return "tag bracket";
+ } else if (ch == "=") {
+ type = "equals";
+ return null;
+ } else if (ch == "<") {
+ state.tokenize = inText;
+ state.state = baseState;
+ state.tagName = state.tagStart = null;
+ var next = state.tokenize(stream, state);
+ return next ? next + " tag error" : "tag error";
+ } else if (/[\'\"]/.test(ch)) {
+ state.tokenize = inAttribute(ch);
+ state.stringStartCol = stream.column();
+ return state.tokenize(stream, state);
+ } else {
+ stream.match(/^[^\s\u00a0=<>\"\']*[^\s\u00a0=<>\"\'\/]/);
+ return "word";
+ }
+ }
+
+ function inAttribute(quote) {
+ var closure = function(stream, state) {
+ while (!stream.eol()) {
+ if (stream.next() == quote) {
+ state.tokenize = inTag;
+ break;
+ }
+ }
+ return "string";
+ };
+ closure.isInAttribute = true;
+ return closure;
+ }
+
+ function inBlock(style, terminator) {
+ return function(stream, state) {
+ while (!stream.eol()) {
+ if (stream.match(terminator)) {
+ state.tokenize = inText;
+ break;
+ }
+ stream.next();
+ }
+ return style;
+ };
+ }
+ function doctype(depth) {
+ return function(stream, state) {
+ var ch;
+ while ((ch = stream.next()) != null) {
+ if (ch == "<") {
+ state.tokenize = doctype(depth + 1);
+ return state.tokenize(stream, state);
+ } else if (ch == ">") {
+ if (depth == 1) {
+ state.tokenize = inText;
+ break;
+ } else {
+ state.tokenize = doctype(depth - 1);
+ return state.tokenize(stream, state);
+ }
+ }
+ }
+ return "meta";
+ };
+ }
+
+ function Context(state, tagName, startOfLine) {
+ this.prev = state.context;
+ this.tagName = tagName;
+ this.indent = state.indented;
+ this.startOfLine = startOfLine;
+ if (config.doNotIndent.hasOwnProperty(tagName) || (state.context && state.context.noIndent))
+ this.noIndent = true;
+ }
+ function popContext(state) {
+ if (state.context) state.context = state.context.prev;
+ }
+ function maybePopContext(state, nextTagName) {
+ var parentTagName;
+ while (true) {
+ if (!state.context) {
+ return;
+ }
+ parentTagName = state.context.tagName;
+ if (!config.contextGrabbers.hasOwnProperty(parentTagName) ||
+ !config.contextGrabbers[parentTagName].hasOwnProperty(nextTagName)) {
+ return;
+ }
+ popContext(state);
+ }
+ }
+
+ function baseState(type, stream, state) {
+ if (type == "openTag") {
+ state.tagStart = stream.column();
+ return tagNameState;
+ } else if (type == "closeTag") {
+ return closeTagNameState;
+ } else {
+ return baseState;
+ }
+ }
+ function tagNameState(type, stream, state) {
+ if (type == "word") {
+ state.tagName = stream.current();
+ setStyle = "tag";
+ return attrState;
+ } else {
+ setStyle = "error";
+ return tagNameState;
+ }
+ }
+ function closeTagNameState(type, stream, state) {
+ if (type == "word") {
+ var tagName = stream.current();
+ if (state.context && state.context.tagName != tagName &&
+ config.implicitlyClosed.hasOwnProperty(state.context.tagName))
+ popContext(state);
+ if ((state.context && state.context.tagName == tagName) || config.matchClosing === false) {
+ setStyle = "tag";
+ return closeState;
+ } else {
+ setStyle = "tag error";
+ return closeStateErr;
+ }
+ } else {
+ setStyle = "error";
+ return closeStateErr;
+ }
+ }
+
+ function closeState(type, _stream, state) {
+ if (type != "endTag") {
+ setStyle = "error";
+ return closeState;
+ }
+ popContext(state);
+ return baseState;
+ }
+ function closeStateErr(type, stream, state) {
+ setStyle = "error";
+ return closeState(type, stream, state);
+ }
+
+ function attrState(type, _stream, state) {
+ if (type == "word") {
+ setStyle = "attribute";
+ return attrEqState;
+ } else if (type == "endTag" || type == "selfcloseTag") {
+ var tagName = state.tagName, tagStart = state.tagStart;
+ state.tagName = state.tagStart = null;
+ if (type == "selfcloseTag" ||
+ config.autoSelfClosers.hasOwnProperty(tagName)) {
+ maybePopContext(state, tagName);
+ } else {
+ maybePopContext(state, tagName);
+ state.context = new Context(state, tagName, tagStart == state.indented);
+ }
+ return baseState;
+ }
+ setStyle = "error";
+ return attrState;
+ }
+ function attrEqState(type, stream, state) {
+ if (type == "equals") return attrValueState;
+ if (!config.allowMissing) setStyle = "error";
+ return attrState(type, stream, state);
+ }
+ function attrValueState(type, stream, state) {
+ if (type == "string") return attrContinuedState;
+ if (type == "word" && config.allowUnquoted) {setStyle = "string"; return attrState;}
+ setStyle = "error";
+ return attrState(type, stream, state);
+ }
+ function attrContinuedState(type, stream, state) {
+ if (type == "string") return attrContinuedState;
+ return attrState(type, stream, state);
+ }
+
+ return {
+ startState: function(baseIndent) {
+ var state = {tokenize: inText,
+ state: baseState,
+ indented: baseIndent || 0,
+ tagName: null, tagStart: null,
+ context: null}
+ if (baseIndent != null) state.baseIndent = baseIndent
+ return state
+ },
+
+ token: function(stream, state) {
+ if (!state.tagName && stream.sol())
+ state.indented = stream.indentation();
+
+ if (stream.eatSpace()) return null;
+ type = null;
+ var style = state.tokenize(stream, state);
+ if ((style || type) && style != "comment") {
+ setStyle = null;
+ state.state = state.state(type || style, stream, state);
+ if (setStyle)
+ style = setStyle == "error" ? style + " error" : setStyle;
+ }
+ return style;
+ },
+
+ indent: function(state, textAfter, fullLine) {
+ var context = state.context;
+ // Indent multi-line strings (e.g. css).
+ if (state.tokenize.isInAttribute) {
+ if (state.tagStart == state.indented)
+ return state.stringStartCol + 1;
+ else
+ return state.indented + indentUnit;
+ }
+ if (context && context.noIndent) return CodeMirror.Pass;
+ if (state.tokenize != inTag && state.tokenize != inText)
+ return fullLine ? fullLine.match(/^(\s*)/)[0].length : 0;
+ // Indent the starts of attribute names.
+ if (state.tagName) {
+ if (config.multilineTagIndentPastTag !== false)
+ return state.tagStart + state.tagName.length + 2;
+ else
+ return state.tagStart + indentUnit * (config.multilineTagIndentFactor || 1);
+ }
+ if (config.alignCDATA && /<!\[CDATA\[/.test(textAfter)) return 0;
+ var tagAfter = textAfter && /^<(\/)?([\w_:\.-]*)/.exec(textAfter);
+ if (tagAfter && tagAfter[1]) { // Closing tag spotted
+ while (context) {
+ if (context.tagName == tagAfter[2]) {
+ context = context.prev;
+ break;
+ } else if (config.implicitlyClosed.hasOwnProperty(context.tagName)) {
+ context = context.prev;
+ } else {
+ break;
+ }
+ }
+ } else if (tagAfter) { // Opening tag spotted
+ while (context) {
+ var grabbers = config.contextGrabbers[context.tagName];
+ if (grabbers && grabbers.hasOwnProperty(tagAfter[2]))
+ context = context.prev;
+ else
+ break;
+ }
+ }
+ while (context && context.prev && !context.startOfLine)
+ context = context.prev;
+ if (context) return context.indent + indentUnit;
+ else return state.baseIndent || 0;
+ },
+
+ electricInput: /<\/[\s\w:]+>$/,
+ blockCommentStart: "<!--",
+ blockCommentEnd: "-->",
+
+ configuration: config.htmlMode ? "html" : "xml",
+ helperType: config.htmlMode ? "html" : "xml",
+
+ skipAttribute: function(state) {
+ if (state.state == attrValueState)
+ state.state = attrState
+ }
+ };
+ });
+
+ CodeMirror.defineMIME("text/xml", "xml");
+ CodeMirror.defineMIME("application/xml", "xml");
+ if (!CodeMirror.mimeModes.hasOwnProperty("text/html"))
+ CodeMirror.defineMIME("text/html", {name: "xml", htmlMode: true});
+
+ });
+
+
+/***/ },
+/* 10 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2));
+ 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) {
+ var inline = parserConfig.inline
+ if (!parserConfig.propertyKeywords) parserConfig = CodeMirror.resolveMode("text/css");
+
+ var indentUnit = config.indentUnit,
+ tokenHooks = parserConfig.tokenHooks,
+ documentTypes = parserConfig.documentTypes || {},
+ mediaTypes = parserConfig.mediaTypes || {},
+ mediaFeatures = parserConfig.mediaFeatures || {},
+ mediaValueKeywords = parserConfig.mediaValueKeywords || {},
+ propertyKeywords = parserConfig.propertyKeywords || {},
+ nonStandardPropertyKeywords = parserConfig.nonStandardPropertyKeywords || {},
+ fontProperties = parserConfig.fontProperties || {},
+ counterDescriptors = parserConfig.counterDescriptors || {},
+ colorKeywords = parserConfig.colorKeywords || {},
+ valueKeywords = parserConfig.valueKeywords || {},
+ allowNested = parserConfig.allowNested,
+ supportsAtComponent = parserConfig.supportsAtComponent === true;
+
+ var type, override;
+ function ret(style, tp) { type = tp; return style; }
+
+ // Tokenizers
+
+ function tokenBase(stream, state) {
+ var ch = stream.next();
+ if (tokenHooks[ch]) {
+ var result = tokenHooks[ch](stream, state);
+ if (result !== false) return result;
+ }
+ if (ch == "@") {
+ stream.eatWhile(/[\w\\\-]/);
+ return ret("def", stream.current());
+ } else if (ch == "=" || (ch == "~" || ch == "|") && stream.eat("=")) {
+ return ret(null, "compare");
+ } else if (ch == "\"" || ch == "'") {
+ state.tokenize = tokenString(ch);
+ return state.tokenize(stream, state);
+ } else if (ch == "#") {
+ stream.eatWhile(/[\w\\\-]/);
+ return ret("atom", "hash");
+ } else if (ch == "!") {
+ stream.match(/^\s*\w*/);
+ return ret("keyword", "important");
+ } else if (/\d/.test(ch) || ch == "." && stream.eat(/\d/)) {
+ stream.eatWhile(/[\w.%]/);
+ return ret("number", "unit");
+ } else if (ch === "-") {
+ if (/[\d.]/.test(stream.peek())) {
+ stream.eatWhile(/[\w.%]/);
+ return ret("number", "unit");
+ } else if (stream.match(/^-[\w\\\-]+/)) {
+ stream.eatWhile(/[\w\\\-]/);
+ if (stream.match(/^\s*:/, false))
+ return ret("variable-2", "variable-definition");
+ return ret("variable-2", "variable");
+ } else if (stream.match(/^\w+-/)) {
+ return ret("meta", "meta");
+ }
+ } else if (/[,+>*\/]/.test(ch)) {
+ return ret(null, "select-op");
+ } else if (ch == "." && stream.match(/^-?[_a-z][_a-z0-9-]*/i)) {
+ return ret("qualifier", "qualifier");
+ } else if (/[:;{}\[\]\(\)]/.test(ch)) {
+ return ret(null, ch);
+ } else if ((ch == "u" && stream.match(/rl(-prefix)?\(/)) ||
+ (ch == "d" && stream.match("omain(")) ||
+ (ch == "r" && stream.match("egexp("))) {
+ stream.backUp(1);
+ state.tokenize = tokenParenthesized;
+ return ret("property", "word");
+ } else if (/[\w\\\-]/.test(ch)) {
+ stream.eatWhile(/[\w\\\-]/);
+ return ret("property", "word");
+ } else {
+ return ret(null, null);
+ }
+ }
+
+ function tokenString(quote) {
+ return function(stream, state) {
+ var escaped = false, ch;
+ while ((ch = stream.next()) != null) {
+ if (ch == quote && !escaped) {
+ if (quote == ")") stream.backUp(1);
+ break;
+ }
+ escaped = !escaped && ch == "\\";
+ }
+ if (ch == quote || !escaped && quote != ")") state.tokenize = null;
+ return ret("string", "string");
+ };
+ }
+
+ function tokenParenthesized(stream, state) {
+ stream.next(); // Must be '('
+ if (!stream.match(/\s*[\"\')]/, false))
+ state.tokenize = tokenString(")");
+ else
+ state.tokenize = null;
+ return ret(null, "(");
+ }
+
+ // Context management
+
+ function Context(type, indent, prev) {
+ this.type = type;
+ this.indent = indent;
+ this.prev = prev;
+ }
+
+ function pushContext(state, stream, type, indent) {
+ state.context = new Context(type, stream.indentation() + (indent === false ? 0 : indentUnit), state.context);
+ return type;
+ }
+
+ function popContext(state) {
+ if (state.context.prev)
+ state.context = state.context.prev;
+ return state.context.type;
+ }
+
+ function pass(type, stream, state) {
+ return states[state.context.type](type, stream, state);
+ }
+ function popAndPass(type, stream, state, n) {
+ for (var i = n || 1; i > 0; i--)
+ state.context = state.context.prev;
+ return pass(type, stream, state);
+ }
+
+ // Parser
+
+ function wordAsValue(stream) {
+ var word = stream.current().toLowerCase();
+ if (valueKeywords.hasOwnProperty(word))
+ override = "atom";
+ else if (colorKeywords.hasOwnProperty(word))
+ override = "keyword";
+ else
+ override = "variable";
+ }
+
+ var states = {};
+
+ states.top = function(type, stream, state) {
+ if (type == "{") {
+ return pushContext(state, stream, "block");
+ } else if (type == "}" && state.context.prev) {
+ return popContext(state);
+ } else if (supportsAtComponent && /@component/.test(type)) {
+ return pushContext(state, stream, "atComponentBlock");
+ } else if (/^@(-moz-)?document$/.test(type)) {
+ return pushContext(state, stream, "documentTypes");
+ } else if (/^@(media|supports|(-moz-)?document|import)$/.test(type)) {
+ return pushContext(state, stream, "atBlock");
+ } else if (/^@(font-face|counter-style)/.test(type)) {
+ state.stateArg = type;
+ return "restricted_atBlock_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") {
+ override = "builtin";
+ } else if (type == "word") {
+ override = "tag";
+ } else if (type == "variable-definition") {
+ return "maybeprop";
+ } else if (type == "interpolation") {
+ return pushContext(state, stream, "interpolation");
+ } else if (type == ":") {
+ return "pseudo";
+ } else if (allowNested && type == "(") {
+ return pushContext(state, stream, "parens");
+ }
+ return state.context.type;
+ };
+
+ states.block = function(type, stream, state) {
+ if (type == "word") {
+ var word = stream.current().toLowerCase();
+ if (propertyKeywords.hasOwnProperty(word)) {
+ override = "property";
+ return "maybeprop";
+ } else if (nonStandardPropertyKeywords.hasOwnProperty(word)) {
+ override = "string-2";
+ return "maybeprop";
+ } else if (allowNested) {
+ override = stream.match(/^\s*:(?:\s|$)/, false) ? "property" : "tag";
+ return "block";
+ } else {
+ override += " error";
+ return "maybeprop";
+ }
+ } else if (type == "meta") {
+ return "block";
+ } else if (!allowNested && (type == "hash" || type == "qualifier")) {
+ override = "error";
+ return "block";
+ } else {
+ return states.top(type, stream, state);
+ }
+ };
+
+ states.maybeprop = function(type, stream, state) {
+ if (type == ":") return pushContext(state, stream, "prop");
+ return pass(type, stream, state);
+ };
+
+ states.prop = function(type, stream, state) {
+ if (type == ";") return popContext(state);
+ if (type == "{" && allowNested) return pushContext(state, stream, "propBlock");
+ if (type == "}" || type == "{") return popAndPass(type, stream, state);
+ if (type == "(") return pushContext(state, stream, "parens");
+
+ if (type == "hash" && !/^#([0-9a-fA-f]{3,4}|[0-9a-fA-f]{6}|[0-9a-fA-f]{8})$/.test(stream.current())) {
+ override += " error";
+ } else if (type == "word") {
+ wordAsValue(stream);
+ } else if (type == "interpolation") {
+ return pushContext(state, stream, "interpolation");
+ }
+ return "prop";
+ };
+
+ states.propBlock = function(type, _stream, state) {
+ if (type == "}") return popContext(state);
+ if (type == "word") { override = "property"; return "maybeprop"; }
+ return state.context.type;
+ };
+
+ states.parens = function(type, stream, state) {
+ if (type == "{" || type == "}") return popAndPass(type, stream, state);
+ if (type == ")") return popContext(state);
+ if (type == "(") return pushContext(state, stream, "parens");
+ if (type == "interpolation") return pushContext(state, stream, "interpolation");
+ if (type == "word") wordAsValue(stream);
+ return "parens";
+ };
+
+ states.pseudo = function(type, stream, state) {
+ if (type == "word") {
+ override = "variable-3";
+ return state.context.type;
+ }
+ return pass(type, stream, state);
+ };
+
+ states.documentTypes = function(type, stream, state) {
+ if (type == "word" && documentTypes.hasOwnProperty(stream.current())) {
+ override = "tag";
+ return state.context.type;
+ } else {
+ return states.atBlock(type, stream, state);
+ }
+ };
+
+ states.atBlock = function(type, stream, state) {
+ if (type == "(") return pushContext(state, stream, "atBlock_parens");
+ if (type == "}" || type == ";") return popAndPass(type, stream, state);
+ if (type == "{") return popContext(state) && pushContext(state, stream, allowNested ? "block" : "top");
+
+ if (type == "interpolation") return pushContext(state, stream, "interpolation");
+
+ if (type == "word") {
+ var word = stream.current().toLowerCase();
+ if (word == "only" || word == "not" || word == "and" || word == "or")
+ override = "keyword";
+ else if (mediaTypes.hasOwnProperty(word))
+ override = "attribute";
+ else if (mediaFeatures.hasOwnProperty(word))
+ override = "property";
+ else if (mediaValueKeywords.hasOwnProperty(word))
+ override = "keyword";
+ else if (propertyKeywords.hasOwnProperty(word))
+ override = "property";
+ else if (nonStandardPropertyKeywords.hasOwnProperty(word))
+ override = "string-2";
+ else if (valueKeywords.hasOwnProperty(word))
+ override = "atom";
+ else if (colorKeywords.hasOwnProperty(word))
+ override = "keyword";
+ else
+ override = "error";
+ }
+ return state.context.type;
+ };
+
+ states.atComponentBlock = function(type, stream, state) {
+ if (type == "}")
+ return popAndPass(type, stream, state);
+ if (type == "{")
+ return popContext(state) && pushContext(state, stream, allowNested ? "block" : "top", false);
+ if (type == "word")
+ override = "error";
+ return state.context.type;
+ };
+
+ states.atBlock_parens = function(type, stream, state) {
+ if (type == ")") return popContext(state);
+ if (type == "{" || type == "}") return popAndPass(type, stream, state, 2);
+ return states.atBlock(type, stream, state);
+ };
+
+ states.restricted_atBlock_before = function(type, stream, state) {
+ if (type == "{")
+ return pushContext(state, stream, "restricted_atBlock");
+ if (type == "word" && state.stateArg == "@counter-style") {
+ override = "variable";
+ return "restricted_atBlock_before";
+ }
+ return pass(type, stream, state);
+ };
+
+ states.restricted_atBlock = function(type, stream, state) {
+ if (type == "}") {
+ state.stateArg = null;
+ return popContext(state);
+ }
+ if (type == "word") {
+ if ((state.stateArg == "@font-face" && !fontProperties.hasOwnProperty(stream.current().toLowerCase())) ||
+ (state.stateArg == "@counter-style" && !counterDescriptors.hasOwnProperty(stream.current().toLowerCase())))
+ override = "error";
+ else
+ override = "property";
+ return "maybeprop";
+ }
+ return "restricted_atBlock";
+ };
+
+ 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);
+ if (type == "word") override = "tag";
+ else if (type == "hash") override = "builtin";
+ return "at";
+ };
+
+ states.interpolation = function(type, stream, state) {
+ if (type == "}") return popContext(state);
+ if (type == "{" || type == ";") return popAndPass(type, stream, state);
+ if (type == "word") override = "variable";
+ else if (type != "variable" && type != "(" && type != ")") override = "error";
+ return "interpolation";
+ };
+
+ return {
+ startState: function(base) {
+ return {tokenize: null,
+ state: inline ? "block" : "top",
+ stateArg: null,
+ context: new Context(inline ? "block" : "top", base || 0, null)};
+ },
+
+ token: function(stream, state) {
+ if (!state.tokenize && stream.eatSpace()) return null;
+ var style = (state.tokenize || tokenBase)(stream, state);
+ if (style && typeof style == "object") {
+ type = style[1];
+ style = style[0];
+ }
+ override = style;
+ state.state = states[state.state](type, stream, state);
+ return override;
+ },
+
+ indent: function(state, textAfter) {
+ var cx = state.context, ch = textAfter && textAfter.charAt(0);
+ var indent = cx.indent;
+ if (cx.type == "prop" && (ch == "}" || ch == ")")) cx = cx.prev;
+ if (cx.prev) {
+ if (ch == "}" && (cx.type == "block" || cx.type == "top" ||
+ cx.type == "interpolation" || cx.type == "restricted_atBlock")) {
+ // Resume indentation from parent context.
+ cx = cx.prev;
+ indent = cx.indent;
+ } else if (ch == ")" && (cx.type == "parens" || cx.type == "atBlock_parens") ||
+ ch == "{" && (cx.type == "at" || cx.type == "atBlock")) {
+ // Dedent relative to current context.
+ indent = Math.max(0, cx.indent - indentUnit);
+ cx = cx.prev;
+ }
+ }
+ return indent;
+ },
+
+ electricChars: "}",
+ blockCommentStart: "/*",
+ blockCommentEnd: "*/",
+ fold: "brace"
+ };
+ });
+
+ function keySet(array) {
+ var keys = {};
+ for (var i = 0; i < array.length; ++i) {
+ keys[array[i]] = true;
+ }
+ return keys;
+ }
+
+ var documentTypes_ = [
+ "domain", "regexp", "url", "url-prefix"
+ ], documentTypes = keySet(documentTypes_);
+
+ var mediaTypes_ = [
+ "all", "aural", "braille", "handheld", "print", "projection", "screen",
+ "tty", "tv", "embossed"
+ ], mediaTypes = keySet(mediaTypes_);
+
+ var mediaFeatures_ = [
+ "width", "min-width", "max-width", "height", "min-height", "max-height",
+ "device-width", "min-device-width", "max-device-width", "device-height",
+ "min-device-height", "max-device-height", "aspect-ratio",
+ "min-aspect-ratio", "max-aspect-ratio", "device-aspect-ratio",
+ "min-device-aspect-ratio", "max-device-aspect-ratio", "color", "min-color",
+ "max-color", "color-index", "min-color-index", "max-color-index",
+ "monochrome", "min-monochrome", "max-monochrome", "resolution",
+ "min-resolution", "max-resolution", "scan", "grid", "orientation",
+ "device-pixel-ratio", "min-device-pixel-ratio", "max-device-pixel-ratio",
+ "pointer", "any-pointer", "hover", "any-hover"
+ ], mediaFeatures = keySet(mediaFeatures_);
+
+ var mediaValueKeywords_ = [
+ "landscape", "portrait", "none", "coarse", "fine", "on-demand", "hover",
+ "interlace", "progressive"
+ ], mediaValueKeywords = keySet(mediaValueKeywords_);
+
+ var propertyKeywords_ = [
+ "align-content", "align-items", "align-self", "alignment-adjust",
+ "alignment-baseline", "anchor-point", "animation", "animation-delay",
+ "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-blend-mode", "background-clip",
+ "background-color", "background-image", "background-origin", "background-position",
+ "background-repeat", "background-size", "baseline-shift", "binding",
+ "bleed", "bookmark-label", "bookmark-level", "bookmark-state",
+ "bookmark-target", "border", "border-bottom", "border-bottom-color",
+ "border-bottom-left-radius", "border-bottom-right-radius",
+ "border-bottom-style", "border-bottom-width", "border-collapse",
+ "border-color", "border-image", "border-image-outset",
+ "border-image-repeat", "border-image-slice", "border-image-source",
+ "border-image-width", "border-left", "border-left-color",
+ "border-left-style", "border-left-width", "border-radius", "border-right",
+ "border-right-color", "border-right-style", "border-right-width",
+ "border-spacing", "border-style", "border-top", "border-top-color",
+ "border-top-left-radius", "border-top-right-radius", "border-top-style",
+ "border-top-width", "border-width", "bottom", "box-decoration-break",
+ "box-shadow", "box-sizing", "break-after", "break-before", "break-inside",
+ "caption-side", "clear", "clip", "color", "color-profile", "column-count",
+ "column-fill", "column-gap", "column-rule", "column-rule-color",
+ "column-rule-style", "column-rule-width", "column-span", "column-width",
+ "columns", "content", "counter-increment", "counter-reset", "crop", "cue",
+ "cue-after", "cue-before", "cursor", "direction", "display",
+ "dominant-baseline", "drop-initial-after-adjust",
+ "drop-initial-after-align", "drop-initial-before-adjust",
+ "drop-initial-before-align", "drop-initial-size", "drop-initial-value",
+ "elevation", "empty-cells", "fit", "fit-position", "flex", "flex-basis",
+ "flex-direction", "flex-flow", "flex-grow", "flex-shrink", "flex-wrap",
+ "float", "float-offset", "flow-from", "flow-into", "font", "font-feature-settings",
+ "font-family", "font-kerning", "font-language-override", "font-size", "font-size-adjust",
+ "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", "grid-area", "grid-auto-columns", "grid-auto-flow",
+ "grid-auto-rows", "grid-column", "grid-column-end", "grid-column-gap",
+ "grid-column-start", "grid-gap", "grid-row", "grid-row-end", "grid-row-gap",
+ "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",
+ "line-stacking-shift", "line-stacking-strategy", "list-style",
+ "list-style-image", "list-style-position", "list-style-type", "margin",
+ "margin-bottom", "margin-left", "margin-right", "margin-top",
+ "marks", "marquee-direction", "marquee-loop",
+ "marquee-play-count", "marquee-speed", "marquee-style", "max-height",
+ "max-width", "min-height", "min-width", "move-to", "nav-down", "nav-index",
+ "nav-left", "nav-right", "nav-up", "object-fit", "object-position",
+ "opacity", "order", "orphans", "outline",
+ "outline-color", "outline-offset", "outline-style", "outline-width",
+ "overflow", "overflow-style", "overflow-wrap", "overflow-x", "overflow-y",
+ "padding", "padding-bottom", "padding-left", "padding-right", "padding-top",
+ "page", "page-break-after", "page-break-before", "page-break-inside",
+ "page-policy", "pause", "pause-after", "pause-before", "perspective",
+ "perspective-origin", "pitch", "pitch-range", "play-during", "position",
+ "presentation-level", "punctuation-trim", "quotes", "region-break-after",
+ "region-break-before", "region-break-inside", "region-fragment",
+ "rendering-intent", "resize", "rest", "rest-after", "rest-before", "richness",
+ "right", "rotation", "rotation-point", "ruby-align", "ruby-overhang",
+ "ruby-position", "ruby-span", "shape-image-threshold", "shape-inside", "shape-margin",
+ "shape-outside", "size", "speak", "speak-as", "speak-header",
+ "speak-numeral", "speak-punctuation", "speech-rate", "stress", "string-set",
+ "tab-size", "table-layout", "target", "target-name", "target-new",
+ "target-position", "text-align", "text-align-last", "text-decoration",
+ "text-decoration-color", "text-decoration-line", "text-decoration-skip",
+ "text-decoration-style", "text-emphasis", "text-emphasis-color",
+ "text-emphasis-position", "text-emphasis-style", "text-height",
+ "text-indent", "text-justify", "text-outline", "text-overflow", "text-shadow",
+ "text-size-adjust", "text-space-collapse", "text-transform", "text-underline-position",
+ "text-wrap", "top", "transform", "transform-origin", "transform-style",
+ "transition", "transition-delay", "transition-duration",
+ "transition-property", "transition-timing-function", "unicode-bidi",
+ "vertical-align", "visibility", "voice-balance", "voice-duration",
+ "voice-family", "voice-pitch", "voice-range", "voice-rate", "voice-stress",
+ "voice-volume", "volume", "white-space", "widows", "width", "word-break",
+ "word-spacing", "word-wrap", "z-index",
+ // SVG-specific
+ "clip-path", "clip-rule", "mask", "enable-background", "filter", "flood-color",
+ "flood-opacity", "lighting-color", "stop-color", "stop-opacity", "pointer-events",
+ "color-interpolation", "color-interpolation-filters",
+ "color-rendering", "fill", "fill-opacity", "fill-rule", "image-rendering",
+ "marker", "marker-end", "marker-mid", "marker-start", "shape-rendering", "stroke",
+ "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin",
+ "stroke-miterlimit", "stroke-opacity", "stroke-width", "text-rendering",
+ "baseline-shift", "dominant-baseline", "glyph-orientation-horizontal",
+ "glyph-orientation-vertical", "text-anchor", "writing-mode"
+ ], propertyKeywords = keySet(propertyKeywords_);
+
+ var nonStandardPropertyKeywords_ = [
+ "scrollbar-arrow-color", "scrollbar-base-color", "scrollbar-dark-shadow-color",
+ "scrollbar-face-color", "scrollbar-highlight-color", "scrollbar-shadow-color",
+ "scrollbar-3d-light-color", "scrollbar-track-color", "shape-inside",
+ "searchfield-cancel-button", "searchfield-decoration", "searchfield-results-button",
+ "searchfield-results-decoration", "zoom"
+ ], nonStandardPropertyKeywords = keySet(nonStandardPropertyKeywords_);
+
+ var fontProperties_ = [
+ "font-family", "src", "unicode-range", "font-variant", "font-feature-settings",
+ "font-stretch", "font-weight", "font-style"
+ ], fontProperties = keySet(fontProperties_);
+
+ var counterDescriptors_ = [
+ "additive-symbols", "fallback", "negative", "pad", "prefix", "range",
+ "speak-as", "suffix", "symbols", "system"
+ ], counterDescriptors = keySet(counterDescriptors_);
+
+ var colorKeywords_ = [
+ "aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", "beige",
+ "bisque", "black", "blanchedalmond", "blue", "blueviolet", "brown",
+ "burlywood", "cadetblue", "chartreuse", "chocolate", "coral", "cornflowerblue",
+ "cornsilk", "crimson", "cyan", "darkblue", "darkcyan", "darkgoldenrod",
+ "darkgray", "darkgreen", "darkkhaki", "darkmagenta", "darkolivegreen",
+ "darkorange", "darkorchid", "darkred", "darksalmon", "darkseagreen",
+ "darkslateblue", "darkslategray", "darkturquoise", "darkviolet",
+ "deeppink", "deepskyblue", "dimgray", "dodgerblue", "firebrick",
+ "floralwhite", "forestgreen", "fuchsia", "gainsboro", "ghostwhite",
+ "gold", "goldenrod", "gray", "grey", "green", "greenyellow", "honeydew",
+ "hotpink", "indianred", "indigo", "ivory", "khaki", "lavender",
+ "lavenderblush", "lawngreen", "lemonchiffon", "lightblue", "lightcoral",
+ "lightcyan", "lightgoldenrodyellow", "lightgray", "lightgreen", "lightpink",
+ "lightsalmon", "lightseagreen", "lightskyblue", "lightslategray",
+ "lightsteelblue", "lightyellow", "lime", "limegreen", "linen", "magenta",
+ "maroon", "mediumaquamarine", "mediumblue", "mediumorchid", "mediumpurple",
+ "mediumseagreen", "mediumslateblue", "mediumspringgreen", "mediumturquoise",
+ "mediumvioletred", "midnightblue", "mintcream", "mistyrose", "moccasin",
+ "navajowhite", "navy", "oldlace", "olive", "olivedrab", "orange", "orangered",
+ "orchid", "palegoldenrod", "palegreen", "paleturquoise", "palevioletred",
+ "papayawhip", "peachpuff", "peru", "pink", "plum", "powderblue",
+ "purple", "rebeccapurple", "red", "rosybrown", "royalblue", "saddlebrown",
+ "salmon", "sandybrown", "seagreen", "seashell", "sienna", "silver", "skyblue",
+ "slateblue", "slategray", "snow", "springgreen", "steelblue", "tan",
+ "teal", "thistle", "tomato", "turquoise", "violet", "wheat", "white",
+ "whitesmoke", "yellow", "yellowgreen"
+ ], colorKeywords = keySet(colorKeywords_);
+
+ var valueKeywords_ = [
+ "above", "absolute", "activeborder", "additive", "activecaption", "afar",
+ "after-white-space", "ahead", "alias", "all", "all-scroll", "alphabetic", "alternate",
+ "always", "amharic", "amharic-abegede", "antialiased", "appworkspace",
+ "arabic-indic", "armenian", "asterisks", "attr", "auto", "avoid", "avoid-column", "avoid-page",
+ "avoid-region", "background", "backwards", "baseline", "below", "bidi-override", "binary",
+ "bengali", "blink", "block", "block-axis", "bold", "bolder", "border", "border-box",
+ "both", "bottom", "break", "break-all", "break-word", "bullets", "button", "button-bevel",
+ "buttonface", "buttonhighlight", "buttonshadow", "buttontext", "calc", "cambodian",
+ "capitalize", "caps-lock-indicator", "caption", "captiontext", "caret",
+ "cell", "center", "checkbox", "circle", "cjk-decimal", "cjk-earthly-branch",
+ "cjk-heavenly-stem", "cjk-ideographic", "clear", "clip", "close-quote",
+ "col-resize", "collapse", "color", "color-burn", "color-dodge", "column", "column-reverse",
+ "compact", "condensed", "contain", "content",
+ "content-box", "context-menu", "continuous", "copy", "counter", "counters", "cover", "crop",
+ "cross", "crosshair", "currentcolor", "cursive", "cyclic", "darken", "dashed", "decimal",
+ "decimal-leading-zero", "default", "default-button", "dense", "destination-atop",
+ "destination-in", "destination-out", "destination-over", "devanagari", "difference",
+ "disc", "discard", "disclosure-closed", "disclosure-open", "document",
+ "dot-dash", "dot-dot-dash",
+ "dotted", "double", "down", "e-resize", "ease", "ease-in", "ease-in-out", "ease-out",
+ "element", "ellipse", "ellipsis", "embed", "end", "ethiopic", "ethiopic-abegede",
+ "ethiopic-abegede-am-et", "ethiopic-abegede-gez", "ethiopic-abegede-ti-er",
+ "ethiopic-abegede-ti-et", "ethiopic-halehame-aa-er",
+ "ethiopic-halehame-aa-et", "ethiopic-halehame-am-et",
+ "ethiopic-halehame-gez", "ethiopic-halehame-om-et",
+ "ethiopic-halehame-sid-et", "ethiopic-halehame-so-et",
+ "ethiopic-halehame-ti-er", "ethiopic-halehame-ti-et", "ethiopic-halehame-tig",
+ "ethiopic-numeric", "ew-resize", "exclusion", "expanded", "extends", "extra-condensed",
+ "extra-expanded", "fantasy", "fast", "fill", "fixed", "flat", "flex", "flex-end", "flex-start", "footnotes",
+ "forwards", "from", "geometricPrecision", "georgian", "graytext", "grid", "groove",
+ "gujarati", "gurmukhi", "hand", "hangul", "hangul-consonant", "hard-light", "hebrew",
+ "help", "hidden", "hide", "higher", "highlight", "highlighttext",
+ "hiragana", "hiragana-iroha", "horizontal", "hsl", "hsla", "hue", "icon", "ignore",
+ "inactiveborder", "inactivecaption", "inactivecaptiontext", "infinite",
+ "infobackground", "infotext", "inherit", "initial", "inline", "inline-axis",
+ "inline-block", "inline-flex", "inline-grid", "inline-table", "inset", "inside", "intrinsic", "invert",
+ "italic", "japanese-formal", "japanese-informal", "justify", "kannada",
+ "katakana", "katakana-iroha", "keep-all", "khmer",
+ "korean-hangul-formal", "korean-hanja-formal", "korean-hanja-informal",
+ "landscape", "lao", "large", "larger", "left", "level", "lighter", "lighten",
+ "line-through", "linear", "linear-gradient", "lines", "list-item", "listbox", "listitem",
+ "local", "logical", "loud", "lower", "lower-alpha", "lower-armenian",
+ "lower-greek", "lower-hexadecimal", "lower-latin", "lower-norwegian",
+ "lower-roman", "lowercase", "ltr", "luminosity", "malayalam", "match", "matrix", "matrix3d",
+ "media-controls-background", "media-current-time-display",
+ "media-fullscreen-button", "media-mute-button", "media-play-button",
+ "media-return-to-realtime-button", "media-rewind-button",
+ "media-seek-back-button", "media-seek-forward-button", "media-slider",
+ "media-sliderthumb", "media-time-remaining-display", "media-volume-slider",
+ "media-volume-slider-container", "media-volume-sliderthumb", "medium",
+ "menu", "menulist", "menulist-button", "menulist-text",
+ "menulist-textfield", "menutext", "message-box", "middle", "min-intrinsic",
+ "mix", "mongolian", "monospace", "move", "multiple", "multiply", "myanmar", "n-resize",
+ "narrower", "ne-resize", "nesw-resize", "no-close-quote", "no-drop",
+ "no-open-quote", "no-repeat", "none", "normal", "not-allowed", "nowrap",
+ "ns-resize", "numbers", "numeric", "nw-resize", "nwse-resize", "oblique", "octal", "open-quote",
+ "optimizeLegibility", "optimizeSpeed", "oriya", "oromo", "outset",
+ "outside", "outside-shape", "overlay", "overline", "padding", "padding-box",
+ "painted", "page", "paused", "persian", "perspective", "plus-darker", "plus-lighter",
+ "pointer", "polygon", "portrait", "pre", "pre-line", "pre-wrap", "preserve-3d",
+ "progress", "push-button", "radial-gradient", "radio", "read-only",
+ "read-write", "read-write-plaintext-only", "rectangle", "region",
+ "relative", "repeat", "repeating-linear-gradient",
+ "repeating-radial-gradient", "repeat-x", "repeat-y", "reset", "reverse",
+ "rgb", "rgba", "ridge", "right", "rotate", "rotate3d", "rotateX", "rotateY",
+ "rotateZ", "round", "row", "row-resize", "row-reverse", "rtl", "run-in", "running",
+ "s-resize", "sans-serif", "saturation", "scale", "scale3d", "scaleX", "scaleY", "scaleZ", "screen",
+ "scroll", "scrollbar", "se-resize", "searchfield",
+ "searchfield-cancel-button", "searchfield-decoration",
+ "searchfield-results-button", "searchfield-results-decoration",
+ "semi-condensed", "semi-expanded", "separate", "serif", "show", "sidama",
+ "simp-chinese-formal", "simp-chinese-informal", "single",
+ "skew", "skewX", "skewY", "skip-white-space", "slide", "slider-horizontal",
+ "slider-vertical", "sliderthumb-horizontal", "sliderthumb-vertical", "slow",
+ "small", "small-caps", "small-caption", "smaller", "soft-light", "solid", "somali",
+ "source-atop", "source-in", "source-out", "source-over", "space", "space-around", "space-between", "spell-out", "square",
+ "square-button", "start", "static", "status-bar", "stretch", "stroke", "sub",
+ "subpixel-antialiased", "super", "sw-resize", "symbolic", "symbols", "table",
+ "table-caption", "table-cell", "table-column", "table-column-group",
+ "table-footer-group", "table-header-group", "table-row", "table-row-group",
+ "tamil",
+ "telugu", "text", "text-bottom", "text-top", "textarea", "textfield", "thai",
+ "thick", "thin", "threeddarkshadow", "threedface", "threedhighlight",
+ "threedlightshadow", "threedshadow", "tibetan", "tigre", "tigrinya-er",
+ "tigrinya-er-abegede", "tigrinya-et", "tigrinya-et-abegede", "to", "top",
+ "trad-chinese-formal", "trad-chinese-informal",
+ "translate", "translate3d", "translateX", "translateY", "translateZ",
+ "transparent", "ultra-condensed", "ultra-expanded", "underline", "up",
+ "upper-alpha", "upper-armenian", "upper-greek", "upper-hexadecimal",
+ "upper-latin", "upper-norwegian", "upper-roman", "uppercase", "urdu", "url",
+ "var", "vertical", "vertical-text", "visible", "visibleFill", "visiblePainted",
+ "visibleStroke", "visual", "w-resize", "wait", "wave", "wider",
+ "window", "windowframe", "windowtext", "words", "wrap", "wrap-reverse", "x-large", "x-small", "xor",
+ "xx-large", "xx-small"
+ ], valueKeywords = keySet(valueKeywords_);
+
+ var allWords = documentTypes_.concat(mediaTypes_).concat(mediaFeatures_).concat(mediaValueKeywords_)
+ .concat(propertyKeywords_).concat(nonStandardPropertyKeywords_).concat(colorKeywords_)
+ .concat(valueKeywords_);
+ CodeMirror.registerHelper("hintWords", "css", allWords);
+
+ function tokenCComment(stream, state) {
+ var maybeEnd = false, ch;
+ while ((ch = stream.next()) != null) {
+ if (maybeEnd && ch == "/") {
+ state.tokenize = null;
+ break;
+ }
+ maybeEnd = (ch == "*");
+ }
+ return ["comment", "comment"];
+ }
+
+ CodeMirror.defineMIME("text/css", {
+ documentTypes: documentTypes,
+ mediaTypes: mediaTypes,
+ mediaFeatures: mediaFeatures,
+ mediaValueKeywords: mediaValueKeywords,
+ propertyKeywords: propertyKeywords,
+ nonStandardPropertyKeywords: nonStandardPropertyKeywords,
+ fontProperties: fontProperties,
+ counterDescriptors: counterDescriptors,
+ colorKeywords: colorKeywords,
+ valueKeywords: valueKeywords,
+ tokenHooks: {
+ "/": function(stream, state) {
+ if (!stream.eat("*")) return false;
+ state.tokenize = tokenCComment;
+ return tokenCComment(stream, state);
+ }
+ },
+ name: "css"
+ });
+
+ CodeMirror.defineMIME("text/x-scss", {
+ mediaTypes: mediaTypes,
+ mediaFeatures: mediaFeatures,
+ mediaValueKeywords: mediaValueKeywords,
+ propertyKeywords: propertyKeywords,
+ nonStandardPropertyKeywords: nonStandardPropertyKeywords,
+ colorKeywords: colorKeywords,
+ valueKeywords: valueKeywords,
+ fontProperties: fontProperties,
+ allowNested: true,
+ tokenHooks: {
+ "/": function(stream, state) {
+ if (stream.eat("/")) {
+ stream.skipToEnd();
+ return ["comment", "comment"];
+ } else if (stream.eat("*")) {
+ state.tokenize = tokenCComment;
+ return tokenCComment(stream, state);
+ } else {
+ return ["operator", "operator"];
+ }
+ },
+ ":": function(stream) {
+ if (stream.match(/\s*\{/))
+ return [null, "{"];
+ return false;
+ },
+ "$": function(stream) {
+ stream.match(/^[\w-]+/);
+ if (stream.match(/^\s*:/, false))
+ return ["variable-2", "variable-definition"];
+ return ["variable-2", "variable"];
+ },
+ "#": function(stream) {
+ if (!stream.eat("{")) return false;
+ return [null, "interpolation"];
+ }
+ },
+ name: "css",
+ helperType: "scss"
+ });
+
+ CodeMirror.defineMIME("text/x-less", {
+ mediaTypes: mediaTypes,
+ mediaFeatures: mediaFeatures,
+ mediaValueKeywords: mediaValueKeywords,
+ propertyKeywords: propertyKeywords,
+ nonStandardPropertyKeywords: nonStandardPropertyKeywords,
+ colorKeywords: colorKeywords,
+ valueKeywords: valueKeywords,
+ fontProperties: fontProperties,
+ allowNested: true,
+ tokenHooks: {
+ "/": function(stream, state) {
+ if (stream.eat("/")) {
+ stream.skipToEnd();
+ return ["comment", "comment"];
+ } else if (stream.eat("*")) {
+ state.tokenize = tokenCComment;
+ return tokenCComment(stream, state);
+ } else {
+ return ["operator", "operator"];
+ }
+ },
+ "@": function(stream) {
+ if (stream.eat("{")) return [null, "interpolation"];
+ 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"];
+ return ["variable-2", "variable"];
+ },
+ "&": function() {
+ return ["atom", "atom"];
+ }
+ },
+ name: "css",
+ helperType: "less"
+ });
+
+ CodeMirror.defineMIME("text/x-gss", {
+ documentTypes: documentTypes,
+ mediaTypes: mediaTypes,
+ mediaFeatures: mediaFeatures,
+ propertyKeywords: propertyKeywords,
+ nonStandardPropertyKeywords: nonStandardPropertyKeywords,
+ fontProperties: fontProperties,
+ counterDescriptors: counterDescriptors,
+ colorKeywords: colorKeywords,
+ valueKeywords: valueKeywords,
+ supportsAtComponent: true,
+ tokenHooks: {
+ "/": function(stream, state) {
+ if (!stream.eat("*")) return false;
+ state.tokenize = tokenCComment;
+ return tokenCComment(stream, state);
+ }
+ },
+ name: "css",
+ helperType: "gss"
+ });
+
+ });
+
+
+/***/ },
+/* 11 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2), __webpack_require__(9), __webpack_require__(8), __webpack_require__(10));
+ 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";
+
+ var defaultTags = {
+ script: [
+ ["lang", /(javascript|babel)/i, "javascript"],
+ ["type", /^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^$/i, "javascript"],
+ ["type", /./, "text/plain"],
+ [null, null, "javascript"]
+ ],
+ style: [
+ ["lang", /^css$/i, "css"],
+ ["type", /^(text\/)?(x-)?(stylesheet|css)$/i, "css"],
+ ["type", /./, "text/plain"],
+ [null, null, "css"]
+ ]
+ };
+
+ function maybeBackup(stream, pat, style) {
+ var cur = stream.current(), close = cur.search(pat);
+ if (close > -1) {
+ stream.backUp(cur.length - close);
+ } else if (cur.match(/<\/?$/)) {
+ stream.backUp(cur.length);
+ if (!stream.match(pat, false)) stream.match(cur);
+ }
+ return style;
+ }
+
+ var attrRegexpCache = {};
+ function getAttrRegexp(attr) {
+ var regexp = attrRegexpCache[attr];
+ if (regexp) return regexp;
+ return attrRegexpCache[attr] = new RegExp("\\s+" + attr + "\\s*=\\s*('|\")?([^'\"]+)('|\")?\\s*");
+ }
+
+ function getAttrValue(text, attr) {
+ var match = text.match(getAttrRegexp(attr))
+ return match ? match[2] : ""
+ }
+
+ function getTagRegexp(tagName, anchored) {
+ return new RegExp((anchored ? "^" : "") + "<\/\s*" + tagName + "\s*>", "i");
+ }
+
+ function addTags(from, to) {
+ for (var tag in from) {
+ var dest = to[tag] || (to[tag] = []);
+ var source = from[tag];
+ for (var i = source.length - 1; i >= 0; i--)
+ dest.unshift(source[i])
+ }
+ }
+
+ function findMatchingMode(tagInfo, tagText) {
+ for (var i = 0; i < tagInfo.length; i++) {
+ var spec = tagInfo[i];
+ if (!spec[0] || spec[1].test(getAttrValue(tagText, spec[0]))) return spec[2];
+ }
+ }
+
+ CodeMirror.defineMode("htmlmixed", function (config, parserConfig) {
+ var htmlMode = CodeMirror.getMode(config, {
+ name: "xml",
+ htmlMode: true,
+ multilineTagIndentFactor: parserConfig.multilineTagIndentFactor,
+ multilineTagIndentPastTag: parserConfig.multilineTagIndentPastTag
+ });
+
+ var tags = {};
+ var configTags = parserConfig && parserConfig.tags, configScript = parserConfig && parserConfig.scriptTypes;
+ addTags(defaultTags, tags);
+ if (configTags) addTags(configTags, tags);
+ if (configScript) for (var i = configScript.length - 1; i >= 0; i--)
+ tags.script.unshift(["type", configScript[i].matches, configScript[i].mode])
+
+ function html(stream, state) {
+ var style = htmlMode.token(stream, state.htmlState), tag = /\btag\b/.test(style), tagName
+ if (tag && !/[<>\s\/]/.test(stream.current()) &&
+ (tagName = state.htmlState.tagName && state.htmlState.tagName.toLowerCase()) &&
+ tags.hasOwnProperty(tagName)) {
+ state.inTag = tagName + " "
+ } else if (state.inTag && tag && />$/.test(stream.current())) {
+ var inTag = /^([\S]+) (.*)/.exec(state.inTag)
+ state.inTag = null
+ var modeSpec = stream.current() == ">" && findMatchingMode(tags[inTag[1]], inTag[2])
+ var mode = CodeMirror.getMode(config, modeSpec)
+ var endTagA = getTagRegexp(inTag[1], true), endTag = getTagRegexp(inTag[1], false);
+ state.token = function (stream, state) {
+ if (stream.match(endTagA, false)) {
+ state.token = html;
+ state.localState = state.localMode = null;
+ return null;
+ }
+ return maybeBackup(stream, endTag, state.localMode.token(stream, state.localState));
+ };
+ state.localMode = mode;
+ state.localState = CodeMirror.startState(mode, htmlMode.indent(state.htmlState, ""));
+ } else if (state.inTag) {
+ state.inTag += stream.current()
+ if (stream.eol()) state.inTag += " "
+ }
+ return style;
+ };
+
+ return {
+ startState: function () {
+ var state = CodeMirror.startState(htmlMode);
+ return {token: html, inTag: null, localMode: null, localState: null, htmlState: state};
+ },
+
+ copyState: function (state) {
+ var local;
+ if (state.localState) {
+ local = CodeMirror.copyState(state.localMode, state.localState);
+ }
+ return {token: state.token, inTag: state.inTag,
+ localMode: state.localMode, localState: local,
+ htmlState: CodeMirror.copyState(htmlMode, state.htmlState)};
+ },
+
+ token: function (stream, state) {
+ return state.token(stream, state);
+ },
+
+ indent: function (state, textAfter) {
+ if (!state.localMode || /^\s*<\//.test(textAfter))
+ return htmlMode.indent(state.htmlState, textAfter);
+ else if (state.localMode.indent)
+ return state.localMode.indent(state.localState, textAfter);
+ else
+ return CodeMirror.Pass;
+ },
+
+ innerMode: function (state) {
+ return {state: state.localState || state.htmlState, mode: state.localMode || htmlMode};
+ }
+ };
+ }, "xml", "javascript", "css");
+
+ CodeMirror.defineMIME("text/html", "htmlmixed");
+ });
+
+
+/***/ },
+/* 12 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ "use strict";
+
+ function Context(indented, column, type, info, align, prev) {
+ this.indented = indented;
+ this.column = column;
+ this.type = type;
+ this.info = info;
+ this.align = align;
+ this.prev = prev;
+ }
+ function pushContext(state, col, type, info) {
+ var indent = state.indented;
+ if (state.context && state.context.type != "statement" && type != "statement")
+ indent = state.context.indented;
+ return state.context = new Context(indent, col, type, info, null, state.context);
+ }
+ function popContext(state) {
+ var t = state.context.type;
+ if (t == ")" || t == "]" || t == "}")
+ state.indented = state.context.indented;
+ return state.context = state.context.prev;
+ }
+
+ function typeBefore(stream, state, pos) {
+ if (state.prevToken == "variable" || state.prevToken == "variable-3") return true;
+ if (/\S(?:[^- ]>|[*\]])\s*$|\*$/.test(stream.string.slice(0, pos))) return true;
+ if (state.typeAtEndOfLine && stream.column() == stream.indentation()) return true;
+ }
+
+ function isTopScope(context) {
+ for (;;) {
+ if (!context || context.type == "top") return true;
+ if (context.type == "}" && context.prev.info != "namespace") return false;
+ context = context.prev;
+ }
+ }
+
+ CodeMirror.defineMode("clike", function(config, parserConfig) {
+ var indentUnit = config.indentUnit,
+ statementIndentUnit = parserConfig.statementIndentUnit || indentUnit,
+ dontAlignCalls = parserConfig.dontAlignCalls,
+ keywords = parserConfig.keywords || {},
+ types = parserConfig.types || {},
+ builtin = parserConfig.builtin || {},
+ blockKeywords = parserConfig.blockKeywords || {},
+ defKeywords = parserConfig.defKeywords || {},
+ atoms = parserConfig.atoms || {},
+ hooks = parserConfig.hooks || {},
+ multiLineStrings = parserConfig.multiLineStrings,
+ indentStatements = parserConfig.indentStatements !== false,
+ indentSwitch = parserConfig.indentSwitch !== false,
+ namespaceSeparator = parserConfig.namespaceSeparator,
+ isPunctuationChar = parserConfig.isPunctuationChar || /[\[\]{}\(\),;\:\.]/,
+ numberStart = parserConfig.numberStart || /[\d\.]/,
+ number = parserConfig.number || /^(?:0x[a-f\d]+|0b[01]+|(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?)(u|ll?|l|f)?/i,
+ isOperatorChar = parserConfig.isOperatorChar || /[+\-*&%=<>!?|\/]/,
+ endStatement = parserConfig.endStatement || /^[;:,]$/;
+
+ var curPunc, isDefKeyword;
+
+ function tokenBase(stream, state) {
+ var ch = stream.next();
+ if (hooks[ch]) {
+ var result = hooks[ch](stream, state);
+ if (result !== false) return result;
+ }
+ if (ch == '"' || ch == "'") {
+ state.tokenize = tokenString(ch);
+ return state.tokenize(stream, state);
+ }
+ if (isPunctuationChar.test(ch)) {
+ curPunc = ch;
+ return null;
+ }
+ if (numberStart.test(ch)) {
+ stream.backUp(1)
+ if (stream.match(number)) return "number"
+ stream.next()
+ }
+ if (ch == "/") {
+ if (stream.eat("*")) {
+ state.tokenize = tokenComment;
+ return tokenComment(stream, state);
+ }
+ if (stream.eat("/")) {
+ stream.skipToEnd();
+ return "comment";
+ }
+ }
+ if (isOperatorChar.test(ch)) {
+ while (!stream.match(/^\/[\/*]/, false) && stream.eat(isOperatorChar)) {}
+ return "operator";
+ }
+ stream.eatWhile(/[\w\$_\xa1-\uffff]/);
+ if (namespaceSeparator) while (stream.match(namespaceSeparator))
+ stream.eatWhile(/[\w\$_\xa1-\uffff]/);
+
+ var cur = stream.current();
+ if (contains(keywords, cur)) {
+ if (contains(blockKeywords, cur)) curPunc = "newstatement";
+ if (contains(defKeywords, cur)) isDefKeyword = true;
+ return "keyword";
+ }
+ if (contains(types, cur)) return "variable-3";
+ if (contains(builtin, cur)) {
+ if (contains(blockKeywords, cur)) curPunc = "newstatement";
+ return "builtin";
+ }
+ if (contains(atoms, cur)) return "atom";
+ return "variable";
+ }
+
+ function tokenString(quote) {
+ return function(stream, state) {
+ var escaped = false, next, end = false;
+ while ((next = stream.next()) != null) {
+ if (next == quote && !escaped) {end = true; break;}
+ escaped = !escaped && next == "\\";
+ }
+ if (end || !(escaped || multiLineStrings))
+ state.tokenize = null;
+ return "string";
+ };
+ }
+
+ function tokenComment(stream, state) {
+ var maybeEnd = false, ch;
+ while (ch = stream.next()) {
+ if (ch == "/" && maybeEnd) {
+ state.tokenize = null;
+ break;
+ }
+ maybeEnd = (ch == "*");
+ }
+ return "comment";
+ }
+
+ function maybeEOL(stream, state) {
+ if (parserConfig.typeFirstDefinitions && stream.eol() && isTopScope(state.context))
+ state.typeAtEndOfLine = typeBefore(stream, state, stream.pos)
+ }
+
+ // Interface
+
+ return {
+ startState: function(basecolumn) {
+ return {
+ tokenize: null,
+ context: new Context((basecolumn || 0) - indentUnit, 0, "top", null, false),
+ indented: 0,
+ startOfLine: true,
+ prevToken: null
+ };
+ },
+
+ token: function(stream, state) {
+ var ctx = state.context;
+ if (stream.sol()) {
+ if (ctx.align == null) ctx.align = false;
+ state.indented = stream.indentation();
+ state.startOfLine = true;
+ }
+ if (stream.eatSpace()) { maybeEOL(stream, state); return null; }
+ curPunc = isDefKeyword = null;
+ var style = (state.tokenize || tokenBase)(stream, state);
+ if (style == "comment" || style == "meta") return style;
+ if (ctx.align == null) ctx.align = true;
+
+ if (endStatement.test(curPunc)) while (state.context.type == "statement") popContext(state);
+ else if (curPunc == "{") pushContext(state, stream.column(), "}");
+ else if (curPunc == "[") pushContext(state, stream.column(), "]");
+ else if (curPunc == "(") pushContext(state, stream.column(), ")");
+ else if (curPunc == "}") {
+ while (ctx.type == "statement") ctx = popContext(state);
+ if (ctx.type == "}") ctx = popContext(state);
+ while (ctx.type == "statement") ctx = popContext(state);
+ }
+ else if (curPunc == ctx.type) popContext(state);
+ else if (indentStatements &&
+ (((ctx.type == "}" || ctx.type == "top") && curPunc != ";") ||
+ (ctx.type == "statement" && curPunc == "newstatement"))) {
+ pushContext(state, stream.column(), "statement", stream.current());
+ }
+
+ if (style == "variable" &&
+ ((state.prevToken == "def" ||
+ (parserConfig.typeFirstDefinitions && typeBefore(stream, state, stream.start) &&
+ isTopScope(state.context) && stream.match(/^\s*\(/, false)))))
+ style = "def";
+
+ if (hooks.token) {
+ var result = hooks.token(stream, state, style);
+ if (result !== undefined) style = result;
+ }
+
+ if (style == "def" && parserConfig.styleDefs === false) style = "variable";
+
+ state.startOfLine = false;
+ state.prevToken = isDefKeyword ? "def" : style || curPunc;
+ maybeEOL(stream, state);
+ return style;
+ },
+
+ indent: function(state, textAfter) {
+ if (state.tokenize != tokenBase && state.tokenize != null || state.typeAtEndOfLine) return CodeMirror.Pass;
+ var ctx = state.context, firstChar = textAfter && textAfter.charAt(0);
+ if (ctx.type == "statement" && firstChar == "}") ctx = ctx.prev;
+ if (parserConfig.dontIndentStatements)
+ while (ctx.type == "statement" && parserConfig.dontIndentStatements.test(ctx.info))
+ ctx = ctx.prev
+ if (hooks.indent) {
+ var hook = hooks.indent(state, ctx, textAfter);
+ if (typeof hook == "number") return hook
+ }
+ var closing = firstChar == ctx.type;
+ var switchBlock = ctx.prev && ctx.prev.info == "switch";
+ if (parserConfig.allmanIndentation && /[{(]/.test(firstChar)) {
+ while (ctx.type != "top" && ctx.type != "}") ctx = ctx.prev
+ return ctx.indented
+ }
+ if (ctx.type == "statement")
+ return ctx.indented + (firstChar == "{" ? 0 : statementIndentUnit);
+ if (ctx.align && (!dontAlignCalls || ctx.type != ")"))
+ return ctx.column + (closing ? 0 : 1);
+ if (ctx.type == ")" && !closing)
+ return ctx.indented + statementIndentUnit;
+
+ return ctx.indented + (closing ? 0 : indentUnit) +
+ (!closing && switchBlock && !/^(?:case|default)\b/.test(textAfter) ? indentUnit : 0);
+ },
+
+ electricInput: indentSwitch ? /^\s*(?:case .*?:|default:|\{\}?|\})$/ : /^\s*[{}]$/,
+ blockCommentStart: "/*",
+ blockCommentEnd: "*/",
+ lineComment: "//",
+ fold: "brace"
+ };
+ });
+
+ function words(str) {
+ var obj = {}, words = str.split(" ");
+ for (var i = 0; i < words.length; ++i) obj[words[i]] = true;
+ return obj;
+ }
+ function contains(words, word) {
+ if (typeof words === "function") {
+ return words(word);
+ } else {
+ return words.propertyIsEnumerable(word);
+ }
+ }
+ var cKeywords = "auto if break case register continue return default do sizeof " +
+ "static else struct switch extern typedef union for goto while enum const volatile";
+ var cTypes = "int long char short double float unsigned signed void size_t ptrdiff_t";
+
+ function cppHook(stream, state) {
+ if (!state.startOfLine) return false
+ for (var ch, next = null; ch = stream.peek();) {
+ if (ch == "\\" && stream.match(/^.$/)) {
+ next = cppHook
+ break
+ } else if (ch == "/" && stream.match(/^\/[\/\*]/, false)) {
+ break
+ }
+ stream.next()
+ }
+ state.tokenize = next
+ return "meta"
+ }
+
+ function pointerHook(_stream, state) {
+ if (state.prevToken == "variable-3") return "variable-3";
+ return false;
+ }
+
+ function cpp14Literal(stream) {
+ stream.eatWhile(/[\w\.']/);
+ return "number";
+ }
+
+ function cpp11StringHook(stream, state) {
+ stream.backUp(1);
+ // Raw strings.
+ if (stream.match(/(R|u8R|uR|UR|LR)/)) {
+ var match = stream.match(/"([^\s\\()]{0,16})\(/);
+ if (!match) {
+ return false;
+ }
+ state.cpp11RawStringDelim = match[1];
+ state.tokenize = tokenRawString;
+ return tokenRawString(stream, state);
+ }
+ // Unicode strings/chars.
+ if (stream.match(/(u8|u|U|L)/)) {
+ if (stream.match(/["']/, /* eat */ false)) {
+ return "string";
+ }
+ return false;
+ }
+ // Ignore this hook.
+ stream.next();
+ return false;
+ }
+
+ function cppLooksLikeConstructor(word) {
+ var lastTwo = /(\w+)::(\w+)$/.exec(word);
+ return lastTwo && lastTwo[1] == lastTwo[2];
+ }
+
+ // C#-style strings where "" escapes a quote.
+ function tokenAtString(stream, state) {
+ var next;
+ while ((next = stream.next()) != null) {
+ if (next == '"' && !stream.eat('"')) {
+ state.tokenize = null;
+ break;
+ }
+ }
+ return "string";
+ }
+
+ // C++11 raw string literal is <prefix>"<delim>( anything )<delim>", where
+ // <delim> can be a string up to 16 characters long.
+ function tokenRawString(stream, state) {
+ // Escape characters that have special regex meanings.
+ var delim = state.cpp11RawStringDelim.replace(/[^\w\s]/g, '\\$&');
+ var match = stream.match(new RegExp(".*?\\)" + delim + '"'));
+ if (match)
+ state.tokenize = null;
+ else
+ stream.skipToEnd();
+ return "string";
+ }
+
+ function def(mimes, mode) {
+ if (typeof mimes == "string") mimes = [mimes];
+ var words = [];
+ function add(obj) {
+ if (obj) for (var prop in obj) if (obj.hasOwnProperty(prop))
+ words.push(prop);
+ }
+ add(mode.keywords);
+ add(mode.types);
+ add(mode.builtin);
+ add(mode.atoms);
+ if (words.length) {
+ mode.helperType = mimes[0];
+ CodeMirror.registerHelper("hintWords", mimes[0], words);
+ }
+
+ for (var i = 0; i < mimes.length; ++i)
+ CodeMirror.defineMIME(mimes[i], mode);
+ }
+
+ def(["text/x-csrc", "text/x-c", "text/x-chdr"], {
+ name: "clike",
+ keywords: words(cKeywords),
+ types: words(cTypes + " bool _Complex _Bool float_t double_t intptr_t intmax_t " +
+ "int8_t int16_t int32_t int64_t uintptr_t uintmax_t uint8_t uint16_t " +
+ "uint32_t uint64_t"),
+ blockKeywords: words("case do else for if switch while struct"),
+ defKeywords: words("struct"),
+ typeFirstDefinitions: true,
+ atoms: words("null true false"),
+ hooks: {"#": cppHook, "*": pointerHook},
+ modeProps: {fold: ["brace", "include"]}
+ });
+
+ def(["text/x-c++src", "text/x-c++hdr"], {
+ name: "clike",
+ keywords: words(cKeywords + " asm dynamic_cast namespace reinterpret_cast try explicit new " +
+ "static_cast typeid catch operator template typename class friend private " +
+ "this using const_cast inline public throw virtual delete mutable protected " +
+ "alignas alignof constexpr decltype nullptr noexcept thread_local final " +
+ "static_assert override"),
+ types: words(cTypes + " bool wchar_t"),
+ blockKeywords: words("catch class do else finally for if struct switch try while"),
+ defKeywords: words("class namespace struct enum union"),
+ typeFirstDefinitions: true,
+ atoms: words("true false null"),
+ dontIndentStatements: /^template$/,
+ hooks: {
+ "#": cppHook,
+ "*": pointerHook,
+ "u": cpp11StringHook,
+ "U": cpp11StringHook,
+ "L": cpp11StringHook,
+ "R": cpp11StringHook,
+ "0": cpp14Literal,
+ "1": cpp14Literal,
+ "2": cpp14Literal,
+ "3": cpp14Literal,
+ "4": cpp14Literal,
+ "5": cpp14Literal,
+ "6": cpp14Literal,
+ "7": cpp14Literal,
+ "8": cpp14Literal,
+ "9": cpp14Literal,
+ token: function(stream, state, style) {
+ if (style == "variable" && stream.peek() == "(" &&
+ (state.prevToken == ";" || state.prevToken == null ||
+ state.prevToken == "}") &&
+ cppLooksLikeConstructor(stream.current()))
+ return "def";
+ }
+ },
+ namespaceSeparator: "::",
+ modeProps: {fold: ["brace", "include"]}
+ });
+
+ def("text/x-java", {
+ name: "clike",
+ keywords: words("abstract assert break case catch class const continue default " +
+ "do else enum extends final finally float for goto if implements import " +
+ "instanceof interface native new package private protected public " +
+ "return static strictfp super switch synchronized this throw throws transient " +
+ "try volatile while"),
+ types: words("byte short int long float double boolean char void Boolean Byte Character Double Float " +
+ "Integer Long Number Object Short String StringBuffer StringBuilder Void"),
+ blockKeywords: words("catch class do else finally for if switch try while"),
+ defKeywords: words("class interface package enum"),
+ typeFirstDefinitions: true,
+ atoms: words("true false null"),
+ endStatement: /^[;:]$/,
+ number: /^(?:0x[a-f\d_]+|0b[01_]+|(?:[\d_]+\.?\d*|\.\d+)(?:e[-+]?[\d_]+)?)(u|ll?|l|f)?/i,
+ hooks: {
+ "@": function(stream) {
+ stream.eatWhile(/[\w\$_]/);
+ return "meta";
+ }
+ },
+ modeProps: {fold: ["brace", "import"]}
+ });
+
+ def("text/x-csharp", {
+ name: "clike",
+ keywords: words("abstract as async await base break case catch checked class const continue" +
+ " default delegate do else enum event explicit extern finally fixed for" +
+ " foreach goto if implicit in interface internal is lock namespace new" +
+ " operator out override params private protected public readonly ref return sealed" +
+ " sizeof stackalloc static struct switch this throw try typeof unchecked" +
+ " unsafe using virtual void volatile while add alias ascending descending dynamic from get" +
+ " global group into join let orderby partial remove select set value var yield"),
+ types: words("Action Boolean Byte Char DateTime DateTimeOffset Decimal Double Func" +
+ " Guid Int16 Int32 Int64 Object SByte Single String Task TimeSpan UInt16 UInt32" +
+ " UInt64 bool byte char decimal double short int long object" +
+ " sbyte float string ushort uint ulong"),
+ blockKeywords: words("catch class do else finally for foreach if struct switch try while"),
+ defKeywords: words("class interface namespace struct var"),
+ typeFirstDefinitions: true,
+ atoms: words("true false null"),
+ hooks: {
+ "@": function(stream, state) {
+ if (stream.eat('"')) {
+ state.tokenize = tokenAtString;
+ return tokenAtString(stream, state);
+ }
+ stream.eatWhile(/[\w\$_]/);
+ return "meta";
+ }
+ }
+ });
+
+ function tokenTripleString(stream, state) {
+ var escaped = false;
+ while (!stream.eol()) {
+ if (!escaped && stream.match('"""')) {
+ state.tokenize = null;
+ break;
+ }
+ escaped = stream.next() == "\\" && !escaped;
+ }
+ return "string";
+ }
+
+ def("text/x-scala", {
+ name: "clike",
+ keywords: words(
+
+ /* scala */
+ "abstract case catch class def do else extends final finally for forSome if " +
+ "implicit import lazy match new null object override package private protected return " +
+ "sealed super this throw trait try type val var while with yield _ : = => <- <: " +
+ "<% >: # @ " +
+
+ /* package scala */
+ "assert assume require print println printf readLine readBoolean readByte readShort " +
+ "readChar readInt readLong readFloat readDouble " +
+
+ ":: #:: "
+ ),
+ types: words(
+ "AnyVal App Application Array BufferedIterator BigDecimal BigInt Char Console Either " +
+ "Enumeration Equiv Error Exception Fractional Function IndexedSeq Int Integral Iterable " +
+ "Iterator List Map Numeric Nil NotNull Option Ordered Ordering PartialFunction PartialOrdering " +
+ "Product Proxy Range Responder Seq Serializable Set Specializable Stream StringBuilder " +
+ "StringContext Symbol Throwable Traversable TraversableOnce Tuple Unit Vector " +
+
+ /* package java.lang */
+ "Boolean Byte Character CharSequence Class ClassLoader Cloneable Comparable " +
+ "Compiler Double Exception Float Integer Long Math Number Object Package Pair Process " +
+ "Runtime Runnable SecurityManager Short StackTraceElement StrictMath String " +
+ "StringBuffer System Thread ThreadGroup ThreadLocal Throwable Triple Void"
+ ),
+ multiLineStrings: true,
+ blockKeywords: words("catch class do else finally for forSome if match switch try while"),
+ defKeywords: words("class def object package trait type val var"),
+ atoms: words("true false null"),
+ indentStatements: false,
+ indentSwitch: false,
+ hooks: {
+ "@": function(stream) {
+ stream.eatWhile(/[\w\$_]/);
+ return "meta";
+ },
+ '"': function(stream, state) {
+ if (!stream.match('""')) return false;
+ state.tokenize = tokenTripleString;
+ return state.tokenize(stream, state);
+ },
+ "'": function(stream) {
+ stream.eatWhile(/[\w\$_\xa1-\uffff]/);
+ return "atom";
+ },
+ "=": function(stream, state) {
+ var cx = state.context
+ if (cx.type == "}" && cx.align && stream.eat(">")) {
+ state.context = new Context(cx.indented, cx.column, cx.type, cx.info, null, cx.prev)
+ return "operator"
+ } else {
+ return false
+ }
+ }
+ },
+ modeProps: {closeBrackets: {triples: '"'}}
+ });
+
+ function tokenKotlinString(tripleString){
+ return function (stream, state) {
+ var escaped = false, next, end = false;
+ while (!stream.eol()) {
+ if (!tripleString && !escaped && stream.match('"') ) {end = true; break;}
+ if (tripleString && stream.match('"""')) {end = true; break;}
+ next = stream.next();
+ if(!escaped && next == "$" && stream.match('{'))
+ stream.skipTo("}");
+ escaped = !escaped && next == "\\" && !tripleString;
+ }
+ if (end || !tripleString)
+ state.tokenize = null;
+ return "string";
+ }
+ }
+
+ def("text/x-kotlin", {
+ name: "clike",
+ keywords: words(
+ /*keywords*/
+ "package as typealias class interface this super val " +
+ "var fun for is in This throw return " +
+ "break continue object if else while do try when !in !is as? " +
+
+ /*soft keywords*/
+ "file import where by get set abstract enum open inner override private public internal " +
+ "protected catch finally out final vararg reified dynamic companion constructor init " +
+ "sealed field property receiver param sparam lateinit data inline noinline tailrec " +
+ "external annotation crossinline const operator infix"
+ ),
+ types: words(
+ /* package java.lang */
+ "Boolean Byte Character CharSequence Class ClassLoader Cloneable Comparable " +
+ "Compiler Double Exception Float Integer Long Math Number Object Package Pair Process " +
+ "Runtime Runnable SecurityManager Short StackTraceElement StrictMath String " +
+ "StringBuffer System Thread ThreadGroup ThreadLocal Throwable Triple Void"
+ ),
+ intendSwitch: false,
+ indentStatements: false,
+ multiLineStrings: true,
+ blockKeywords: words("catch class do else finally for if where try while enum"),
+ defKeywords: words("class val var object package interface fun"),
+ atoms: words("true false null this"),
+ hooks: {
+ '"': function(stream, state) {
+ state.tokenize = tokenKotlinString(stream.match('""'));
+ return state.tokenize(stream, state);
+ }
+ },
+ modeProps: {closeBrackets: {triples: '"'}}
+ });
+
+ def(["x-shader/x-vertex", "x-shader/x-fragment"], {
+ name: "clike",
+ keywords: words("sampler1D sampler2D sampler3D samplerCube " +
+ "sampler1DShadow sampler2DShadow " +
+ "const attribute uniform varying " +
+ "break continue discard return " +
+ "for while do if else struct " +
+ "in out inout"),
+ types: words("float int bool void " +
+ "vec2 vec3 vec4 ivec2 ivec3 ivec4 bvec2 bvec3 bvec4 " +
+ "mat2 mat3 mat4"),
+ blockKeywords: words("for while do if else struct"),
+ builtin: words("radians degrees sin cos tan asin acos atan " +
+ "pow exp log exp2 sqrt inversesqrt " +
+ "abs sign floor ceil fract mod min max clamp mix step smoothstep " +
+ "length distance dot cross normalize ftransform faceforward " +
+ "reflect refract matrixCompMult " +
+ "lessThan lessThanEqual greaterThan greaterThanEqual " +
+ "equal notEqual any all not " +
+ "texture1D texture1DProj texture1DLod texture1DProjLod " +
+ "texture2D texture2DProj texture2DLod texture2DProjLod " +
+ "texture3D texture3DProj texture3DLod texture3DProjLod " +
+ "textureCube textureCubeLod " +
+ "shadow1D shadow2D shadow1DProj shadow2DProj " +
+ "shadow1DLod shadow2DLod shadow1DProjLod shadow2DProjLod " +
+ "dFdx dFdy fwidth " +
+ "noise1 noise2 noise3 noise4"),
+ atoms: words("true false " +
+ "gl_FragColor gl_SecondaryColor gl_Normal gl_Vertex " +
+ "gl_MultiTexCoord0 gl_MultiTexCoord1 gl_MultiTexCoord2 gl_MultiTexCoord3 " +
+ "gl_MultiTexCoord4 gl_MultiTexCoord5 gl_MultiTexCoord6 gl_MultiTexCoord7 " +
+ "gl_FogCoord gl_PointCoord " +
+ "gl_Position gl_PointSize gl_ClipVertex " +
+ "gl_FrontColor gl_BackColor gl_FrontSecondaryColor gl_BackSecondaryColor " +
+ "gl_TexCoord gl_FogFragCoord " +
+ "gl_FragCoord gl_FrontFacing " +
+ "gl_FragData gl_FragDepth " +
+ "gl_ModelViewMatrix gl_ProjectionMatrix gl_ModelViewProjectionMatrix " +
+ "gl_TextureMatrix gl_NormalMatrix gl_ModelViewMatrixInverse " +
+ "gl_ProjectionMatrixInverse gl_ModelViewProjectionMatrixInverse " +
+ "gl_TexureMatrixTranspose gl_ModelViewMatrixInverseTranspose " +
+ "gl_ProjectionMatrixInverseTranspose " +
+ "gl_ModelViewProjectionMatrixInverseTranspose " +
+ "gl_TextureMatrixInverseTranspose " +
+ "gl_NormalScale gl_DepthRange gl_ClipPlane " +
+ "gl_Point gl_FrontMaterial gl_BackMaterial gl_LightSource gl_LightModel " +
+ "gl_FrontLightModelProduct gl_BackLightModelProduct " +
+ "gl_TextureColor gl_EyePlaneS gl_EyePlaneT gl_EyePlaneR gl_EyePlaneQ " +
+ "gl_FogParameters " +
+ "gl_MaxLights gl_MaxClipPlanes gl_MaxTextureUnits gl_MaxTextureCoords " +
+ "gl_MaxVertexAttribs gl_MaxVertexUniformComponents gl_MaxVaryingFloats " +
+ "gl_MaxVertexTextureImageUnits gl_MaxTextureImageUnits " +
+ "gl_MaxFragmentUniformComponents gl_MaxCombineTextureImageUnits " +
+ "gl_MaxDrawBuffers"),
+ indentSwitch: false,
+ hooks: {"#": cppHook},
+ modeProps: {fold: ["brace", "include"]}
+ });
+
+ def("text/x-nesc", {
+ name: "clike",
+ keywords: words(cKeywords + "as atomic async call command component components configuration event generic " +
+ "implementation includes interface module new norace nx_struct nx_union post provides " +
+ "signal task uses abstract extends"),
+ types: words(cTypes),
+ blockKeywords: words("case do else for if switch while struct"),
+ atoms: words("null true false"),
+ hooks: {"#": cppHook},
+ modeProps: {fold: ["brace", "include"]}
+ });
+
+ def("text/x-objectivec", {
+ name: "clike",
+ keywords: words(cKeywords + "inline restrict _Bool _Complex _Imaginary BOOL Class bycopy byref id IMP in " +
+ "inout nil oneway out Protocol SEL self super atomic nonatomic retain copy readwrite readonly"),
+ types: words(cTypes),
+ atoms: words("YES NO NULL NILL ON OFF true false"),
+ hooks: {
+ "@": function(stream) {
+ stream.eatWhile(/[\w\$]/);
+ return "keyword";
+ },
+ "#": cppHook,
+ indent: function(_state, ctx, textAfter) {
+ if (ctx.type == "statement" && /^@\w/.test(textAfter)) return ctx.indented
+ }
+ },
+ modeProps: {fold: "brace"}
+ });
+
+ def("text/x-squirrel", {
+ name: "clike",
+ keywords: words("base break clone continue const default delete enum extends function in class" +
+ " foreach local resume return this throw typeof yield constructor instanceof static"),
+ types: words(cTypes),
+ blockKeywords: words("case catch class else for foreach if switch try while"),
+ defKeywords: words("function local class"),
+ typeFirstDefinitions: true,
+ atoms: words("true false null"),
+ hooks: {"#": cppHook},
+ modeProps: {fold: ["brace", "include"]}
+ });
+
+ // Ceylon Strings need to deal with interpolation
+ var stringTokenizer = null;
+ function tokenCeylonString(type) {
+ return function(stream, state) {
+ var escaped = false, next, end = false;
+ while (!stream.eol()) {
+ if (!escaped && stream.match('"') &&
+ (type == "single" || stream.match('""'))) {
+ end = true;
+ break;
+ }
+ if (!escaped && stream.match('``')) {
+ stringTokenizer = tokenCeylonString(type);
+ end = true;
+ break;
+ }
+ next = stream.next();
+ escaped = type == "single" && !escaped && next == "\\";
+ }
+ if (end)
+ state.tokenize = null;
+ return "string";
+ }
+ }
+
+ def("text/x-ceylon", {
+ name: "clike",
+ keywords: words("abstracts alias assembly assert assign break case catch class continue dynamic else" +
+ " exists extends finally for function given if import in interface is let module new" +
+ " nonempty object of out outer package return satisfies super switch then this throw" +
+ " try value void while"),
+ types: function(word) {
+ // In Ceylon all identifiers that start with an uppercase are types
+ var first = word.charAt(0);
+ return (first === first.toUpperCase() && first !== first.toLowerCase());
+ },
+ blockKeywords: words("case catch class dynamic else finally for function if interface module new object switch try while"),
+ defKeywords: words("class dynamic function interface module object package value"),
+ builtin: words("abstract actual aliased annotation by default deprecated doc final formal late license" +
+ " native optional sealed see serializable shared suppressWarnings tagged throws variable"),
+ isPunctuationChar: /[\[\]{}\(\),;\:\.`]/,
+ isOperatorChar: /[+\-*&%=<>!?|^~:\/]/,
+ numberStart: /[\d#$]/,
+ number: /^(?:#[\da-fA-F_]+|\$[01_]+|[\d_]+[kMGTPmunpf]?|[\d_]+\.[\d_]+(?:[eE][-+]?\d+|[kMGTPmunpf]|)|)/i,
+ multiLineStrings: true,
+ typeFirstDefinitions: true,
+ atoms: words("true false null larger smaller equal empty finished"),
+ indentSwitch: false,
+ styleDefs: false,
+ hooks: {
+ "@": function(stream) {
+ stream.eatWhile(/[\w\$_]/);
+ return "meta";
+ },
+ '"': function(stream, state) {
+ state.tokenize = tokenCeylonString(stream.match('""') ? "triple" : "single");
+ return state.tokenize(stream, state);
+ },
+ '`': function(stream, state) {
+ if (!stringTokenizer || !stream.match('`')) return false;
+ state.tokenize = stringTokenizer;
+ stringTokenizer = null;
+ return state.tokenize(stream, state);
+ },
+ "'": function(stream) {
+ stream.eatWhile(/[\w\$_\xa1-\uffff]/);
+ return "atom";
+ },
+ token: function(_stream, state, style) {
+ if ((style == "variable" || style == "variable-3") &&
+ state.prevToken == ".") {
+ return "variable-2";
+ }
+ }
+ },
+ modeProps: {
+ fold: ["brace", "import"],
+ closeBrackets: {triples: '"'}
+ }
+ });
+
+ });
+
+
+/***/ },
+/* 13 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+ // WebAssembly experimental syntax highlight add-on for CodeMirror.
+
+ (function (root, factory) {
+ if (true) {
+ !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(2)], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+ } else if (typeof exports !== 'undefined') {
+ factory(require("../../lib/codemirror"));
+ } else {
+ factory(root.CodeMirror);
+ }
+ }(this, function (CodeMirror) {
+ "use strict";
+
+ var isWordChar = /[\w\$_\.\\\/@]/;
+
+ function createLookupTable(list) {
+ var obj = Object.create(null);
+ list.forEach(function (key) {
+ obj[key] = true;
+ });
+ return obj;
+ }
+
+ CodeMirror.defineMode("wasm", function() {
+ var keywords = createLookupTable([
+ "function", "import", "export", "table", "memory", "segment", "as", "type",
+ "of", "from", "typeof", "br", "br_if", "loop", "br_table", "if", "else",
+ "call", "call_import", "call_indirect", "nop", "unreachable", "var",
+ "align", "select", "return"]);
+ var builtins = createLookupTable([
+ "i32:8", "i32:8u", "i32:8s", "i32:16", "i32:16u", "i32:16s",
+ "i64:8", "i64:8u", "i64:8s", "i64:16", "i64:16u", "i64:16s",
+ "i64:32", "i64:32u", "i64:32s",
+ "i32.add", "i32.sub", "i32.mul", "i32.div_s", "i32.div_u",
+ "i32.rem_s", "i32.rem_u", "i32.and", "i32.or", "i32.xor",
+ "i32.shl", "i32.shr_u", "i32.shr_s", "i32.rotr", "i32.rotl",
+ "i32.eq", "i32.ne", "i32.lt_s", "i32.le_s", "i32.lt_u",
+ "i32.le_u", "i32.gt_s", "i32.ge_s", "i32.gt_u", "i32.ge_u",
+ "i32.clz", "i32.ctz", "i32.popcnt", "i32.eqz", "i64.add",
+ "i64.sub", "i64.mul", "i64.div_s", "i64.div_u", "i64.rem_s",
+ "i64.rem_u", "i64.and", "i64.or", "i64.xor", "i64.shl",
+ "i64.shr_u", "i64.shr_s", "i64.rotr", "i64.rotl", "i64.eq",
+ "i64.ne", "i64.lt_s", "i64.le_s", "i64.lt_u", "i64.le_u",
+ "i64.gt_s", "i64.ge_s", "i64.gt_u", "i64.ge_u", "i64.clz",
+ "i64.ctz", "i64.popcnt", "i64.eqz", "f32.add", "f32.sub",
+ "f32.mul", "f32.div", "f32.min", "f32.max", "f32.abs",
+ "f32.neg", "f32.copysign", "f32.ceil", "f32.floor", "f32.trunc",
+ "f32.nearest", "f32.sqrt", "f32.eq", "f32.ne", "f32.lt",
+ "f32.le", "f32.gt", "f32.ge", "f64.add", "f64.sub", "f64.mul",
+ "f64.div", "f64.min", "f64.max", "f64.abs", "f64.neg",
+ "f64.copysign", "f64.ceil", "f64.floor", "f64.trunc", "f64.nearest",
+ "f64.sqrt", "f64.eq", "f64.ne", "f64.lt", "f64.le", "f64.gt",
+ "f64.ge", "i32.trunc_s/f32", "i32.trunc_s/f64", "i32.trunc_u/f32",
+ "i32.trunc_u/f64", "i32.wrap/i64", "i64.trunc_s/f32",
+ "i64.trunc_s/f64", "i64.trunc_u/f32", "i64.trunc_u/f64",
+ "i64.extend_s/i32", "i64.extend_u/i32", "f32.convert_s/i32",
+ "f32.convert_u/i32", "f32.convert_s/i64", "f32.convert_u/i64",
+ "f32.demote/f64", "f32.reinterpret/i32", "f64.convert_s/i32",
+ "f64.convert_u/i32", "f64.convert_s/i64", "f64.convert_u/i64",
+ "f64.promote/f32", "f64.reinterpret/i64", "i32.reinterpret/f32",
+ "i64.reinterpret/f64"]);
+ var dataTypes = createLookupTable(["i32", "i64", "f32", "f64"]);
+ var isUnaryOperator = /[\-!]/;
+ var operators = createLookupTable([
+ "+", "-", "*", "/", "/s", "/u", "%", "%s", "%u",
+ "<<", ">>u", ">>s", ">=", "<=", "==", "!=",
+ "<s", "<u", "<=s", "<=u", ">=s", ">=u", ">s", ">u",
+ "<", ">", "=", "&", "|", "^", "!"]);
+
+ function tokenBase(stream, state) {
+ var ch = stream.next();
+ if (ch === "$") {
+ stream.eatWhile(isWordChar);
+ return "variable";
+ }
+ if (ch === "@") {
+ stream.eatWhile(isWordChar);
+ return "meta";
+ }
+ if (ch === '"') {
+ state.tokenize = tokenString(ch);
+ return state.tokenize(stream, state);
+ }
+ if (ch == "/") {
+ if (stream.eat("*")) {
+ state.tokenize = tokenComment;
+ return tokenComment(stream, state);
+ } else if (stream.eat("/")) {
+ stream.skipToEnd();
+ return "comment";
+ }
+ }
+ if (/\d/.test(ch) ||
+ ((ch === "-" || ch === "+") && /\d/.test(stream.peek()))) {
+ stream.eatWhile(/[\w\._\-+]/);
+ return "number";
+ }
+ if (/[\[\]\(\)\{\},:]/.test(ch)) {
+ return null;
+ }
+ if (isUnaryOperator.test(ch)) {
+ return "operator";
+ }
+ stream.eatWhile(isWordChar);
+ var word = stream.current();
+
+ if (word in operators) {
+ return "operator";
+ }
+ if (word in keywords){
+ return "keyword";
+ }
+ if (word in dataTypes) {
+ if (!stream.eat(":")) {
+ return "builtin";
+ }
+ stream.eatWhile(isWordChar);
+ word = stream.current();
+ // fall thru for "builtin" check
+ }
+ if (word in builtins) {
+ return "builtin";
+ }
+
+ if (word === "Temporary") {
+ // Nightly has header with some text graphics -- skipping it.
+ state.tokenize = tokenTemporary;
+ return state.tokenize(stream, state);
+ }
+ return null;
+ }
+
+ function tokenComment(stream, state) {
+ state.commentDepth = 1;
+ var next;
+ while ((next = stream.next()) != null) {
+ if (next === "*" && stream.eat("/")) {
+ if (--state.commentDepth === 0) {
+ state.tokenize = null;
+ return "comment";
+ }
+ }
+ if (next === "/" && stream.eat("*")) {
+ // Nested comment
+ state.commentDepth++;
+ }
+ }
+ return "comment";
+ }
+
+ function tokenTemporary(stream, state) {
+ var next, endState = state.commentState;
+ // Skipping until "text support (Work In Progress):" is found.
+ while ((next = stream.next()) != null) {
+ if (endState === 0 && next === "t") {
+ endState = 1;
+ } else if (endState === 1 && next === ":") {
+ state.tokenize = null;
+ state.commentState = 0;
+ endState = 2;
+ return "comment";
+ }
+ }
+ state.commentState = endState;
+ return "comment";
+ }
+
+ function tokenString(quote) {
+ return function(stream, state) {
+ var escaped = false, next, end = false;
+ while ((next = stream.next()) != null) {
+ if (next == quote && !escaped) {
+ state.tokenize = null;
+ return "string";
+ }
+ escaped = !escaped && next === "\\";
+ }
+ return "string";
+ };
+ }
+
+ return {
+ startState: function() {
+ return {tokenize: null, commentState: 0, commentDepth: 0};
+ },
+
+ token: function(stream, state) {
+ if (stream.eatSpace()) return null;
+ var style = (state.tokenize || tokenBase)(stream, state);
+ return style;
+ }
+ };
+ });
+
+ CodeMirror.registerHelper("wordChars", "wasm", isWordChar);
+
+ CodeMirror.defineMIME("text/wasm", "wasm");
+
+ }));
+
+
+/***/ },
+/* 14 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ // Because sometimes you need to style the cursor's line.
+ //
+ // Adds an option 'styleActiveLine' which, when enabled, gives the
+ // active line's wrapping <div> the CSS class "CodeMirror-activeline",
+ // and gives its background <div> the class "CodeMirror-activeline-background".
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ "use strict";
+ var WRAP_CLASS = "CodeMirror-activeline";
+ var BACK_CLASS = "CodeMirror-activeline-background";
+ var GUTT_CLASS = "CodeMirror-activeline-gutter";
+
+ CodeMirror.defineOption("styleActiveLine", false, function(cm, val, old) {
+ var prev = old && old != CodeMirror.Init;
+ if (val && !prev) {
+ cm.state.activeLines = [];
+ updateActiveLines(cm, cm.listSelections());
+ cm.on("beforeSelectionChange", selectionChange);
+ } else if (!val && prev) {
+ cm.off("beforeSelectionChange", selectionChange);
+ clearActiveLines(cm);
+ delete cm.state.activeLines;
+ }
+ });
+
+ function clearActiveLines(cm) {
+ for (var i = 0; i < cm.state.activeLines.length; i++) {
+ cm.removeLineClass(cm.state.activeLines[i], "wrap", WRAP_CLASS);
+ cm.removeLineClass(cm.state.activeLines[i], "background", BACK_CLASS);
+ cm.removeLineClass(cm.state.activeLines[i], "gutter", GUTT_CLASS);
+ }
+ }
+
+ function sameArray(a, b) {
+ if (a.length != b.length) return false;
+ for (var i = 0; i < a.length; i++)
+ if (a[i] != b[i]) return false;
+ return true;
+ }
+
+ function updateActiveLines(cm, ranges) {
+ var active = [];
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i];
+ if (!range.empty()) continue;
+ var line = cm.getLineHandleVisualStart(range.head.line);
+ if (active[active.length - 1] != line) active.push(line);
+ }
+ if (sameArray(cm.state.activeLines, active)) return;
+ cm.operation(function() {
+ clearActiveLines(cm);
+ for (var i = 0; i < active.length; i++) {
+ cm.addLineClass(active[i], "wrap", WRAP_CLASS);
+ cm.addLineClass(active[i], "background", BACK_CLASS);
+ cm.addLineClass(active[i], "gutter", GUTT_CLASS);
+ }
+ cm.state.activeLines = active;
+ });
+ }
+
+ function selectionChange(cm, sel) {
+ updateActiveLines(cm, sel.ranges);
+ }
+ });
+
+
+/***/ },
+/* 15 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2));
+ 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"
+ });
+ });
+ });
+
+
+/***/ },
+/* 16 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2));
+ 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 posEq(a, b) { return a.line == b.line && a.ch == b.ch; }
+
+ // Kill 'ring'
+
+ var killRing = [];
+ function addToRing(str) {
+ killRing.push(str);
+ if (killRing.length > 50) killRing.shift();
+ }
+ function growRingTop(str) {
+ if (!killRing.length) return addToRing(str);
+ killRing[killRing.length - 1] += str;
+ }
+ function getFromRing(n) { return killRing[killRing.length - (n ? Math.min(n, 1) : 1)] || ""; }
+ function popFromRing() { if (killRing.length > 1) killRing.pop(); return getFromRing(); }
+
+ var lastKill = null;
+
+ function kill(cm, from, to, mayGrow, text) {
+ if (text == null) text = cm.getRange(from, to);
+
+ if (mayGrow && lastKill && lastKill.cm == cm && posEq(from, lastKill.pos) && cm.isClean(lastKill.gen))
+ growRingTop(text);
+ else
+ addToRing(text);
+ cm.replaceRange("", from, to, "+delete");
+
+ if (mayGrow) lastKill = {cm: cm, pos: from, gen: cm.changeGeneration()};
+ else lastKill = null;
+ }
+
+ // Boundaries of various units
+
+ function byChar(cm, pos, dir) {
+ return cm.findPosH(pos, dir, "char", true);
+ }
+
+ function byWord(cm, pos, dir) {
+ return cm.findPosH(pos, dir, "word", true);
+ }
+
+ function byLine(cm, pos, dir) {
+ return cm.findPosV(pos, dir, "line", cm.doc.sel.goalColumn);
+ }
+
+ function byPage(cm, pos, dir) {
+ return cm.findPosV(pos, dir, "page", cm.doc.sel.goalColumn);
+ }
+
+ function byParagraph(cm, pos, dir) {
+ var no = pos.line, line = cm.getLine(no);
+ var sawText = /\S/.test(dir < 0 ? line.slice(0, pos.ch) : line.slice(pos.ch));
+ var fst = cm.firstLine(), lst = cm.lastLine();
+ for (;;) {
+ no += dir;
+ if (no < fst || no > lst)
+ return cm.clipPos(Pos(no - dir, dir < 0 ? 0 : null));
+ line = cm.getLine(no);
+ var hasText = /\S/.test(line);
+ if (hasText) sawText = true;
+ else if (sawText) return Pos(no, 0);
+ }
+ }
+
+ function bySentence(cm, pos, dir) {
+ var line = pos.line, ch = pos.ch;
+ var text = cm.getLine(pos.line), sawWord = false;
+ for (;;) {
+ var next = text.charAt(ch + (dir < 0 ? -1 : 0));
+ if (!next) { // End/beginning of line reached
+ if (line == (dir < 0 ? cm.firstLine() : cm.lastLine())) return Pos(line, ch);
+ text = cm.getLine(line + dir);
+ if (!/\S/.test(text)) return Pos(line, ch);
+ line += dir;
+ ch = dir < 0 ? text.length : 0;
+ continue;
+ }
+ if (sawWord && /[!?.]/.test(next)) return Pos(line, ch + (dir > 0 ? 1 : 0));
+ if (!sawWord) sawWord = /\w/.test(next);
+ ch += dir;
+ }
+ }
+
+ function byExpr(cm, pos, dir) {
+ var wrap;
+ if (cm.findMatchingBracket && (wrap = cm.findMatchingBracket(pos, true))
+ && wrap.match && (wrap.forward ? 1 : -1) == dir)
+ return dir > 0 ? Pos(wrap.to.line, wrap.to.ch + 1) : wrap.to;
+
+ for (var first = true;; first = false) {
+ var token = cm.getTokenAt(pos);
+ var after = Pos(pos.line, dir < 0 ? token.start : token.end);
+ if (first && dir > 0 && token.end == pos.ch || !/\w/.test(token.string)) {
+ var newPos = cm.findPosH(after, dir, "char");
+ if (posEq(after, newPos)) return pos;
+ else pos = newPos;
+ } else {
+ return after;
+ }
+ }
+ }
+
+ // Prefixes (only crudely supported)
+
+ function getPrefix(cm, precise) {
+ var digits = cm.state.emacsPrefix;
+ if (!digits) return precise ? null : 1;
+ clearPrefix(cm);
+ return digits == "-" ? -1 : Number(digits);
+ }
+
+ function repeated(cmd) {
+ var f = typeof cmd == "string" ? function(cm) { cm.execCommand(cmd); } : cmd;
+ return function(cm) {
+ var prefix = getPrefix(cm);
+ f(cm);
+ for (var i = 1; i < prefix; ++i) f(cm);
+ };
+ }
+
+ function findEnd(cm, pos, by, dir) {
+ var prefix = getPrefix(cm);
+ if (prefix < 0) { dir = -dir; prefix = -prefix; }
+ for (var i = 0; i < prefix; ++i) {
+ var newPos = by(cm, pos, dir);
+ if (posEq(newPos, pos)) break;
+ pos = newPos;
+ }
+ return pos;
+ }
+
+ function move(by, dir) {
+ var f = function(cm) {
+ cm.extendSelection(findEnd(cm, cm.getCursor(), by, dir));
+ };
+ f.motion = true;
+ return f;
+ }
+
+ function killTo(cm, by, dir) {
+ var selections = cm.listSelections(), cursor;
+ var i = selections.length;
+ while (i--) {
+ cursor = selections[i].head;
+ kill(cm, cursor, findEnd(cm, cursor, by, dir), true);
+ }
+ }
+
+ function killRegion(cm) {
+ if (cm.somethingSelected()) {
+ var selections = cm.listSelections(), selection;
+ var i = selections.length;
+ while (i--) {
+ selection = selections[i];
+ kill(cm, selection.anchor, selection.head);
+ }
+ return true;
+ }
+ }
+
+ function addPrefix(cm, digit) {
+ if (cm.state.emacsPrefix) {
+ if (digit != "-") cm.state.emacsPrefix += digit;
+ return;
+ }
+ // Not active yet
+ cm.state.emacsPrefix = digit;
+ cm.on("keyHandled", maybeClearPrefix);
+ cm.on("inputRead", maybeDuplicateInput);
+ }
+
+ var prefixPreservingKeys = {"Alt-G": true, "Ctrl-X": true, "Ctrl-Q": true, "Ctrl-U": true};
+
+ function maybeClearPrefix(cm, arg) {
+ if (!cm.state.emacsPrefixMap && !prefixPreservingKeys.hasOwnProperty(arg))
+ clearPrefix(cm);
+ }
+
+ function clearPrefix(cm) {
+ cm.state.emacsPrefix = null;
+ cm.off("keyHandled", maybeClearPrefix);
+ cm.off("inputRead", maybeDuplicateInput);
+ }
+
+ function maybeDuplicateInput(cm, event) {
+ var dup = getPrefix(cm);
+ if (dup > 1 && event.origin == "+input") {
+ var one = event.text.join("\n"), txt = "";
+ for (var i = 1; i < dup; ++i) txt += one;
+ cm.replaceSelection(txt);
+ }
+ }
+
+ function addPrefixMap(cm) {
+ cm.state.emacsPrefixMap = true;
+ cm.addKeyMap(prefixMap);
+ cm.on("keyHandled", maybeRemovePrefixMap);
+ cm.on("inputRead", maybeRemovePrefixMap);
+ }
+
+ function maybeRemovePrefixMap(cm, arg) {
+ if (typeof arg == "string" && (/^\d$/.test(arg) || arg == "Ctrl-U")) return;
+ cm.removeKeyMap(prefixMap);
+ cm.state.emacsPrefixMap = false;
+ cm.off("keyHandled", maybeRemovePrefixMap);
+ cm.off("inputRead", maybeRemovePrefixMap);
+ }
+
+ // Utilities
+
+ function setMark(cm) {
+ cm.setCursor(cm.getCursor());
+ cm.setExtending(!cm.getExtending());
+ cm.on("change", function() { cm.setExtending(false); });
+ }
+
+ function clearMark(cm) {
+ cm.setExtending(false);
+ cm.setCursor(cm.getCursor());
+ }
+
+ function getInput(cm, msg, f) {
+ if (cm.openDialog)
+ cm.openDialog(msg + ": <input type=\"text\" style=\"width: 10em\"/>", f, {bottom: true});
+ else
+ f(prompt(msg, ""));
+ }
+
+ function operateOnWord(cm, op) {
+ var start = cm.getCursor(), end = cm.findPosH(start, 1, "word");
+ cm.replaceRange(op(cm.getRange(start, end)), start, end);
+ cm.setCursor(end);
+ }
+
+ function toEnclosingExpr(cm) {
+ var pos = cm.getCursor(), line = pos.line, ch = pos.ch;
+ var stack = [];
+ while (line >= cm.firstLine()) {
+ var text = cm.getLine(line);
+ for (var i = ch == null ? text.length : ch; i > 0;) {
+ var ch = text.charAt(--i);
+ if (ch == ")")
+ stack.push("(");
+ else if (ch == "]")
+ stack.push("[");
+ else if (ch == "}")
+ stack.push("{");
+ else if (/[\(\{\[]/.test(ch) && (!stack.length || stack.pop() != ch))
+ return cm.extendSelection(Pos(line, i));
+ }
+ --line; ch = null;
+ }
+ }
+
+ function quit(cm) {
+ cm.execCommand("clearSearch");
+ clearMark(cm);
+ }
+
+ // Actual keymap
+
+ var keyMap = CodeMirror.keyMap.emacs = CodeMirror.normalizeKeyMap({
+ "Ctrl-W": function(cm) {kill(cm, cm.getCursor("start"), cm.getCursor("end"));},
+ "Ctrl-K": repeated(function(cm) {
+ var start = cm.getCursor(), end = cm.clipPos(Pos(start.line));
+ var text = cm.getRange(start, end);
+ if (!/\S/.test(text)) {
+ text += "\n";
+ end = Pos(start.line + 1, 0);
+ }
+ kill(cm, start, end, true, text);
+ }),
+ "Alt-W": function(cm) {
+ addToRing(cm.getSelection());
+ clearMark(cm);
+ },
+ "Ctrl-Y": function(cm) {
+ var start = cm.getCursor();
+ cm.replaceRange(getFromRing(getPrefix(cm)), start, start, "paste");
+ cm.setSelection(start, cm.getCursor());
+ },
+ "Alt-Y": function(cm) {cm.replaceSelection(popFromRing(), "around", "paste");},
+
+ "Ctrl-Space": setMark, "Ctrl-Shift-2": setMark,
+
+ "Ctrl-F": move(byChar, 1), "Ctrl-B": move(byChar, -1),
+ "Right": move(byChar, 1), "Left": move(byChar, -1),
+ "Ctrl-D": function(cm) { killTo(cm, byChar, 1); },
+ "Delete": function(cm) { killRegion(cm) || killTo(cm, byChar, 1); },
+ "Ctrl-H": function(cm) { killTo(cm, byChar, -1); },
+ "Backspace": function(cm) { killRegion(cm) || killTo(cm, byChar, -1); },
+
+ "Alt-F": move(byWord, 1), "Alt-B": move(byWord, -1),
+ "Alt-D": function(cm) { killTo(cm, byWord, 1); },
+ "Alt-Backspace": function(cm) { killTo(cm, byWord, -1); },
+
+ "Ctrl-N": move(byLine, 1), "Ctrl-P": move(byLine, -1),
+ "Down": move(byLine, 1), "Up": move(byLine, -1),
+ "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd",
+ "End": "goLineEnd", "Home": "goLineStart",
+
+ "Alt-V": move(byPage, -1), "Ctrl-V": move(byPage, 1),
+ "PageUp": move(byPage, -1), "PageDown": move(byPage, 1),
+
+ "Ctrl-Up": move(byParagraph, -1), "Ctrl-Down": move(byParagraph, 1),
+
+ "Alt-A": move(bySentence, -1), "Alt-E": move(bySentence, 1),
+ "Alt-K": function(cm) { killTo(cm, bySentence, 1); },
+
+ "Ctrl-Alt-K": function(cm) { killTo(cm, byExpr, 1); },
+ "Ctrl-Alt-Backspace": function(cm) { killTo(cm, byExpr, -1); },
+ "Ctrl-Alt-F": move(byExpr, 1), "Ctrl-Alt-B": move(byExpr, -1),
+
+ "Shift-Ctrl-Alt-2": function(cm) {
+ var cursor = cm.getCursor();
+ cm.setSelection(findEnd(cm, cursor, byExpr, 1), cursor);
+ },
+ "Ctrl-Alt-T": function(cm) {
+ var leftStart = byExpr(cm, cm.getCursor(), -1), leftEnd = byExpr(cm, leftStart, 1);
+ var rightEnd = byExpr(cm, leftEnd, 1), rightStart = byExpr(cm, rightEnd, -1);
+ cm.replaceRange(cm.getRange(rightStart, rightEnd) + cm.getRange(leftEnd, rightStart) +
+ cm.getRange(leftStart, leftEnd), leftStart, rightEnd);
+ },
+ "Ctrl-Alt-U": repeated(toEnclosingExpr),
+
+ "Alt-Space": function(cm) {
+ var pos = cm.getCursor(), from = pos.ch, to = pos.ch, text = cm.getLine(pos.line);
+ while (from && /\s/.test(text.charAt(from - 1))) --from;
+ while (to < text.length && /\s/.test(text.charAt(to))) ++to;
+ cm.replaceRange(" ", Pos(pos.line, from), Pos(pos.line, to));
+ },
+ "Ctrl-O": repeated(function(cm) { cm.replaceSelection("\n", "start"); }),
+ "Ctrl-T": repeated(function(cm) {
+ cm.execCommand("transposeChars");
+ }),
+
+ "Alt-C": repeated(function(cm) {
+ operateOnWord(cm, function(w) {
+ var letter = w.search(/\w/);
+ if (letter == -1) return w;
+ return w.slice(0, letter) + w.charAt(letter).toUpperCase() + w.slice(letter + 1).toLowerCase();
+ });
+ }),
+ "Alt-U": repeated(function(cm) {
+ operateOnWord(cm, function(w) { return w.toUpperCase(); });
+ }),
+ "Alt-L": repeated(function(cm) {
+ operateOnWord(cm, function(w) { return w.toLowerCase(); });
+ }),
+
+ "Alt-;": "toggleComment",
+
+ "Ctrl-/": repeated("undo"), "Shift-Ctrl--": repeated("undo"),
+ "Ctrl-Z": repeated("undo"), "Cmd-Z": repeated("undo"),
+ "Shift-Alt-,": "goDocStart", "Shift-Alt-.": "goDocEnd",
+ "Ctrl-S": "findNext", "Ctrl-R": "findPrev", "Ctrl-G": quit, "Shift-Alt-5": "replace",
+ "Alt-/": "autocomplete",
+ "Ctrl-J": "newlineAndIndent", "Enter": false, "Tab": "indentAuto",
+
+ "Alt-G G": function(cm) {
+ var prefix = getPrefix(cm, true);
+ if (prefix != null && prefix > 0) return cm.setCursor(prefix - 1);
+
+ getInput(cm, "Goto line", function(str) {
+ var num;
+ if (str && !isNaN(num = Number(str)) && num == (num|0) && num > 0)
+ cm.setCursor(num - 1);
+ });
+ },
+
+ "Ctrl-X Tab": function(cm) {
+ cm.indentSelection(getPrefix(cm, true) || cm.getOption("indentUnit"));
+ },
+ "Ctrl-X Ctrl-X": function(cm) {
+ cm.setSelection(cm.getCursor("head"), cm.getCursor("anchor"));
+ },
+ "Ctrl-X Ctrl-S": "save",
+ "Ctrl-X Ctrl-W": "save",
+ "Ctrl-X S": "saveAll",
+ "Ctrl-X F": "open",
+ "Ctrl-X U": repeated("undo"),
+ "Ctrl-X K": "close",
+ "Ctrl-X Delete": function(cm) { kill(cm, cm.getCursor(), bySentence(cm, cm.getCursor(), 1), true); },
+ "Ctrl-X H": "selectAll",
+
+ "Ctrl-Q Tab": repeated("insertTab"),
+ "Ctrl-U": addPrefixMap
+ });
+
+ var prefixMap = {"Ctrl-G": clearPrefix};
+ function regPrefix(d) {
+ prefixMap[d] = function(cm) { addPrefix(cm, d); };
+ keyMap["Ctrl-" + d] = function(cm) { addPrefix(cm, d); };
+ prefixPreservingKeys["Ctrl-" + d] = true;
+ }
+ for (var i = 0; i < 10; ++i) regPrefix(String(i));
+ regPrefix("-");
+ });
+
+
+/***/ },
+/* 17 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ /**
+ * Supported keybindings:
+ * Too many to list. Refer to defaultKeyMap below.
+ *
+ * Supported Ex commands:
+ * Refer to defaultExCommandMap below.
+ *
+ * Registers: unnamed, -, a-z, A-Z, 0-9
+ * (Does not respect the special case for number registers when delete
+ * operator is made with these commands: %, (, ), , /, ?, n, N, {, } )
+ * TODO: Implement the remaining registers.
+ *
+ * Marks: a-z, A-Z, and 0-9
+ * TODO: Implement the remaining special marks. They have more complex
+ * behavior.
+ *
+ * Events:
+ * 'vim-mode-change' - raised on the editor anytime the current mode changes,
+ * Event object: {mode: "visual", subMode: "linewise"}
+ *
+ * Code structure:
+ * 1. Default keymap
+ * 2. Variable declarations and short basic helpers
+ * 3. Instance (External API) implementation
+ * 4. Internal state tracking objects (input state, counter) implementation
+ * and instantiation
+ * 5. Key handler (the main command dispatcher) implementation
+ * 6. Motion, operator, and action implementations
+ * 7. Helper functions for the key handler, motions, operators, and actions
+ * 8. Set up Vim to work as a keymap for CodeMirror.
+ * 9. Ex command implementations.
+ */
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2), __webpack_require__(3), __webpack_require__(1), __webpack_require__(5));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../lib/codemirror", "../addon/search/searchcursor", "../addon/dialog/dialog", "../addon/edit/matchbrackets"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ 'use strict';
+
+ var defaultKeymap = [
+ // Key to key mapping. This goes first to make it possible to override
+ // existing mappings.
+ { keys: '<Left>', type: 'keyToKey', toKeys: 'h' },
+ { keys: '<Right>', type: 'keyToKey', toKeys: 'l' },
+ { keys: '<Up>', type: 'keyToKey', toKeys: 'k' },
+ { keys: '<Down>', type: 'keyToKey', toKeys: 'j' },
+ { keys: '<Space>', type: 'keyToKey', toKeys: 'l' },
+ { keys: '<BS>', type: 'keyToKey', toKeys: 'h', context: 'normal'},
+ { keys: '<C-Space>', type: 'keyToKey', toKeys: 'W' },
+ { keys: '<C-BS>', type: 'keyToKey', toKeys: 'B', context: 'normal' },
+ { keys: '<S-Space>', type: 'keyToKey', toKeys: 'w' },
+ { keys: '<S-BS>', type: 'keyToKey', toKeys: 'b', context: 'normal' },
+ { keys: '<C-n>', type: 'keyToKey', toKeys: 'j' },
+ { keys: '<C-p>', type: 'keyToKey', toKeys: 'k' },
+ { keys: '<C-[>', type: 'keyToKey', toKeys: '<Esc>' },
+ { keys: '<C-c>', type: 'keyToKey', toKeys: '<Esc>' },
+ { keys: '<C-[>', type: 'keyToKey', toKeys: '<Esc>', context: 'insert' },
+ { keys: '<C-c>', type: 'keyToKey', toKeys: '<Esc>', context: 'insert' },
+ { keys: 's', type: 'keyToKey', toKeys: 'cl', context: 'normal' },
+ { keys: 's', type: 'keyToKey', toKeys: 'c', context: 'visual'},
+ { keys: 'S', type: 'keyToKey', toKeys: 'cc', context: 'normal' },
+ { keys: 'S', type: 'keyToKey', toKeys: 'VdO', context: 'visual' },
+ { keys: '<Home>', type: 'keyToKey', toKeys: '0' },
+ { keys: '<End>', type: 'keyToKey', toKeys: '$' },
+ { keys: '<PageUp>', type: 'keyToKey', toKeys: '<C-b>' },
+ { keys: '<PageDown>', type: 'keyToKey', toKeys: '<C-f>' },
+ { keys: '<CR>', type: 'keyToKey', toKeys: 'j^', context: 'normal' },
+ // Motions
+ { keys: 'H', type: 'motion', motion: 'moveToTopLine', motionArgs: { linewise: true, toJumplist: true }},
+ { keys: 'M', type: 'motion', motion: 'moveToMiddleLine', motionArgs: { linewise: true, toJumplist: true }},
+ { keys: 'L', type: 'motion', motion: 'moveToBottomLine', motionArgs: { linewise: true, toJumplist: true }},
+ { keys: 'h', type: 'motion', motion: 'moveByCharacters', motionArgs: { forward: false }},
+ { keys: 'l', type: 'motion', motion: 'moveByCharacters', motionArgs: { forward: true }},
+ { keys: 'j', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, linewise: true }},
+ { keys: 'k', type: 'motion', motion: 'moveByLines', motionArgs: { forward: false, linewise: true }},
+ { keys: 'gj', type: 'motion', motion: 'moveByDisplayLines', motionArgs: { forward: true }},
+ { keys: 'gk', type: 'motion', motion: 'moveByDisplayLines', motionArgs: { forward: false }},
+ { keys: 'w', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: false }},
+ { keys: 'W', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: false, bigWord: true }},
+ { keys: 'e', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: true, inclusive: true }},
+ { keys: 'E', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: true, bigWord: true, inclusive: true }},
+ { keys: 'b', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false }},
+ { keys: 'B', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false, bigWord: true }},
+ { keys: 'ge', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: true, inclusive: true }},
+ { keys: 'gE', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: true, bigWord: true, inclusive: true }},
+ { keys: '{', type: 'motion', motion: 'moveByParagraph', motionArgs: { forward: false, toJumplist: true }},
+ { keys: '}', type: 'motion', motion: 'moveByParagraph', motionArgs: { forward: true, toJumplist: true }},
+ { keys: '<C-f>', type: 'motion', motion: 'moveByPage', motionArgs: { forward: true }},
+ { keys: '<C-b>', type: 'motion', motion: 'moveByPage', motionArgs: { forward: false }},
+ { keys: '<C-d>', type: 'motion', motion: 'moveByScroll', motionArgs: { forward: true, explicitRepeat: true }},
+ { keys: '<C-u>', type: 'motion', motion: 'moveByScroll', motionArgs: { forward: false, explicitRepeat: true }},
+ { keys: 'gg', type: 'motion', motion: 'moveToLineOrEdgeOfDocument', motionArgs: { forward: false, explicitRepeat: true, linewise: true, toJumplist: true }},
+ { keys: 'G', type: 'motion', motion: 'moveToLineOrEdgeOfDocument', motionArgs: { forward: true, explicitRepeat: true, linewise: true, toJumplist: true }},
+ { keys: '0', type: 'motion', motion: 'moveToStartOfLine' },
+ { keys: '^', type: 'motion', motion: 'moveToFirstNonWhiteSpaceCharacter' },
+ { keys: '+', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, toFirstChar:true }},
+ { keys: '-', type: 'motion', motion: 'moveByLines', motionArgs: { forward: false, toFirstChar:true }},
+ { keys: '_', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, toFirstChar:true, repeatOffset:-1 }},
+ { keys: '$', type: 'motion', motion: 'moveToEol', motionArgs: { inclusive: true }},
+ { keys: '%', type: 'motion', motion: 'moveToMatchedSymbol', motionArgs: { inclusive: true, toJumplist: true }},
+ { keys: 'f<character>', type: 'motion', motion: 'moveToCharacter', motionArgs: { forward: true , inclusive: true }},
+ { keys: 'F<character>', type: 'motion', motion: 'moveToCharacter', motionArgs: { forward: false }},
+ { keys: 't<character>', type: 'motion', motion: 'moveTillCharacter', motionArgs: { forward: true, inclusive: true }},
+ { keys: 'T<character>', type: 'motion', motion: 'moveTillCharacter', motionArgs: { forward: false }},
+ { keys: ';', type: 'motion', motion: 'repeatLastCharacterSearch', motionArgs: { forward: true }},
+ { keys: ',', type: 'motion', motion: 'repeatLastCharacterSearch', motionArgs: { forward: false }},
+ { keys: '\'<character>', type: 'motion', motion: 'goToMark', motionArgs: {toJumplist: true, linewise: true}},
+ { keys: '`<character>', type: 'motion', motion: 'goToMark', motionArgs: {toJumplist: true}},
+ { keys: ']`', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true } },
+ { keys: '[`', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false } },
+ { keys: ']\'', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true, linewise: true } },
+ { keys: '[\'', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false, linewise: true } },
+ // the next two aren't motions but must come before more general motion declarations
+ { keys: ']p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: true, isEdit: true, matchIndent: true}},
+ { keys: '[p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: false, isEdit: true, matchIndent: true}},
+ { keys: ']<character>', type: 'motion', motion: 'moveToSymbol', motionArgs: { forward: true, toJumplist: true}},
+ { keys: '[<character>', type: 'motion', motion: 'moveToSymbol', motionArgs: { forward: false, toJumplist: true}},
+ { keys: '|', type: 'motion', motion: 'moveToColumn'},
+ { keys: 'o', type: 'motion', motion: 'moveToOtherHighlightedEnd', context:'visual'},
+ { keys: 'O', type: 'motion', motion: 'moveToOtherHighlightedEnd', motionArgs: {sameLine: true}, context:'visual'},
+ // Operators
+ { keys: 'd', type: 'operator', operator: 'delete' },
+ { keys: 'y', type: 'operator', operator: 'yank' },
+ { keys: 'c', type: 'operator', operator: 'change' },
+ { keys: '>', type: 'operator', operator: 'indent', operatorArgs: { indentRight: true }},
+ { keys: '<', type: 'operator', operator: 'indent', operatorArgs: { indentRight: false }},
+ { keys: 'g~', type: 'operator', operator: 'changeCase' },
+ { keys: 'gu', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: true}, isEdit: true },
+ { keys: 'gU', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: false}, isEdit: true },
+ { keys: 'n', type: 'motion', motion: 'findNext', motionArgs: { forward: true, toJumplist: true }},
+ { keys: 'N', type: 'motion', motion: 'findNext', motionArgs: { forward: false, toJumplist: true }},
+ // Operator-Motion dual commands
+ { keys: 'x', type: 'operatorMotion', operator: 'delete', motion: 'moveByCharacters', motionArgs: { forward: true }, operatorMotionArgs: { visualLine: false }},
+ { keys: 'X', type: 'operatorMotion', operator: 'delete', motion: 'moveByCharacters', motionArgs: { forward: false }, operatorMotionArgs: { visualLine: true }},
+ { keys: 'D', type: 'operatorMotion', operator: 'delete', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'},
+ { keys: 'D', type: 'operator', operator: 'delete', operatorArgs: { linewise: true }, context: 'visual'},
+ { keys: 'Y', type: 'operatorMotion', operator: 'yank', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'},
+ { keys: 'Y', type: 'operator', operator: 'yank', operatorArgs: { linewise: true }, context: 'visual'},
+ { keys: 'C', type: 'operatorMotion', operator: 'change', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'},
+ { keys: 'C', type: 'operator', operator: 'change', operatorArgs: { linewise: true }, context: 'visual'},
+ { keys: '~', type: 'operatorMotion', operator: 'changeCase', motion: 'moveByCharacters', motionArgs: { forward: true }, operatorArgs: { shouldMoveCursor: true }, context: 'normal'},
+ { keys: '~', type: 'operator', operator: 'changeCase', context: 'visual'},
+ { keys: '<C-w>', type: 'operatorMotion', operator: 'delete', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false }, context: 'insert' },
+ // Actions
+ { keys: '<C-i>', type: 'action', action: 'jumpListWalk', actionArgs: { forward: true }},
+ { keys: '<C-o>', type: 'action', action: 'jumpListWalk', actionArgs: { forward: false }},
+ { keys: '<C-e>', type: 'action', action: 'scroll', actionArgs: { forward: true, linewise: true }},
+ { keys: '<C-y>', type: 'action', action: 'scroll', actionArgs: { forward: false, linewise: true }},
+ { keys: 'a', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'charAfter' }, context: 'normal' },
+ { keys: 'A', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'eol' }, context: 'normal' },
+ { keys: 'A', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'endOfSelectedArea' }, context: 'visual' },
+ { keys: 'i', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'inplace' }, context: 'normal' },
+ { keys: 'I', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'firstNonBlank'}, context: 'normal' },
+ { keys: 'I', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'startOfSelectedArea' }, context: 'visual' },
+ { keys: 'o', type: 'action', action: 'newLineAndEnterInsertMode', isEdit: true, interlaceInsertRepeat: true, actionArgs: { after: true }, context: 'normal' },
+ { keys: 'O', type: 'action', action: 'newLineAndEnterInsertMode', isEdit: true, interlaceInsertRepeat: true, actionArgs: { after: false }, context: 'normal' },
+ { keys: 'v', type: 'action', action: 'toggleVisualMode' },
+ { keys: 'V', type: 'action', action: 'toggleVisualMode', actionArgs: { linewise: true }},
+ { keys: '<C-v>', type: 'action', action: 'toggleVisualMode', actionArgs: { blockwise: true }},
+ { keys: '<C-q>', type: 'action', action: 'toggleVisualMode', actionArgs: { blockwise: true }},
+ { keys: 'gv', type: 'action', action: 'reselectLastSelection' },
+ { keys: 'J', type: 'action', action: 'joinLines', isEdit: true },
+ { keys: 'p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: true, isEdit: true }},
+ { keys: 'P', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: false, isEdit: true }},
+ { keys: 'r<character>', type: 'action', action: 'replace', isEdit: true },
+ { keys: '@<character>', type: 'action', action: 'replayMacro' },
+ { keys: 'q<character>', type: 'action', action: 'enterMacroRecordMode' },
+ // Handle Replace-mode as a special case of insert mode.
+ { keys: 'R', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { replace: true }},
+ { keys: 'u', type: 'action', action: 'undo', context: 'normal' },
+ { keys: 'u', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: true}, context: 'visual', isEdit: true },
+ { keys: 'U', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: false}, context: 'visual', isEdit: true },
+ { keys: '<C-r>', type: 'action', action: 'redo' },
+ { keys: 'm<character>', type: 'action', action: 'setMark' },
+ { keys: '"<character>', type: 'action', action: 'setRegister' },
+ { keys: 'zz', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'center' }},
+ { keys: 'z.', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'center' }, motion: 'moveToFirstNonWhiteSpaceCharacter' },
+ { keys: 'zt', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'top' }},
+ { keys: 'z<CR>', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'top' }, motion: 'moveToFirstNonWhiteSpaceCharacter' },
+ { keys: 'z-', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'bottom' }},
+ { keys: 'zb', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'bottom' }, motion: 'moveToFirstNonWhiteSpaceCharacter' },
+ { keys: '.', type: 'action', action: 'repeatLastEdit' },
+ { keys: '<C-a>', type: 'action', action: 'incrementNumberToken', isEdit: true, actionArgs: {increase: true, backtrack: false}},
+ { keys: '<C-x>', type: 'action', action: 'incrementNumberToken', isEdit: true, actionArgs: {increase: false, backtrack: false}},
+ // Text object motions
+ { keys: 'a<character>', type: 'motion', motion: 'textObjectManipulation' },
+ { keys: 'i<character>', type: 'motion', motion: 'textObjectManipulation', motionArgs: { textObjectInner: true }},
+ // Search
+ { keys: '/', type: 'search', searchArgs: { forward: true, querySrc: 'prompt', toJumplist: true }},
+ { keys: '?', type: 'search', searchArgs: { forward: false, querySrc: 'prompt', toJumplist: true }},
+ { keys: '*', type: 'search', searchArgs: { forward: true, querySrc: 'wordUnderCursor', wholeWordOnly: true, toJumplist: true }},
+ { keys: '#', type: 'search', searchArgs: { forward: false, querySrc: 'wordUnderCursor', wholeWordOnly: true, toJumplist: true }},
+ { keys: 'g*', type: 'search', searchArgs: { forward: true, querySrc: 'wordUnderCursor', toJumplist: true }},
+ { keys: 'g#', type: 'search', searchArgs: { forward: false, querySrc: 'wordUnderCursor', toJumplist: true }},
+ // Ex command
+ { keys: ':', type: 'ex' }
+ ];
+
+ /**
+ * Ex commands
+ * Care must be taken when adding to the default Ex command map. For any
+ * pair of commands that have a shared prefix, at least one of their
+ * shortNames must not match the prefix of the other command.
+ */
+ var defaultExCommandMap = [
+ { name: 'colorscheme', shortName: 'colo' },
+ { name: 'map' },
+ { name: 'imap', shortName: 'im' },
+ { name: 'nmap', shortName: 'nm' },
+ { name: 'vmap', shortName: 'vm' },
+ { name: 'unmap' },
+ { name: 'write', shortName: 'w' },
+ { name: 'undo', shortName: 'u' },
+ { name: 'redo', shortName: 'red' },
+ { name: 'set', shortName: 'se' },
+ { name: 'set', shortName: 'se' },
+ { name: 'setlocal', shortName: 'setl' },
+ { name: 'setglobal', shortName: 'setg' },
+ { name: 'sort', shortName: 'sor' },
+ { name: 'substitute', shortName: 's', possiblyAsync: true },
+ { name: 'nohlsearch', shortName: 'noh' },
+ { name: 'yank', shortName: 'y' },
+ { name: 'delmarks', shortName: 'delm' },
+ { name: 'registers', shortName: 'reg', excludeFromCommandHistory: true },
+ { name: 'global', shortName: 'g' }
+ ];
+
+ var Pos = CodeMirror.Pos;
+
+ var Vim = function() {
+ function enterVimMode(cm) {
+ cm.setOption('disableInput', true);
+ cm.setOption('showCursorWhenSelecting', false);
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"});
+ cm.on('cursorActivity', onCursorActivity);
+ maybeInitVimState(cm);
+ CodeMirror.on(cm.getInputField(), 'paste', getOnPasteFn(cm));
+ }
+
+ function leaveVimMode(cm) {
+ cm.setOption('disableInput', false);
+ cm.off('cursorActivity', onCursorActivity);
+ CodeMirror.off(cm.getInputField(), 'paste', getOnPasteFn(cm));
+ cm.state.vim = null;
+ }
+
+ function detachVimMap(cm, next) {
+ if (this == CodeMirror.keyMap.vim)
+ CodeMirror.rmClass(cm.getWrapperElement(), "cm-fat-cursor");
+
+ if (!next || next.attach != attachVimMap)
+ leaveVimMode(cm, false);
+ }
+ function attachVimMap(cm, prev) {
+ if (this == CodeMirror.keyMap.vim)
+ CodeMirror.addClass(cm.getWrapperElement(), "cm-fat-cursor");
+
+ if (!prev || prev.attach != attachVimMap)
+ enterVimMode(cm);
+ }
+
+ // Deprecated, simply setting the keymap works again.
+ CodeMirror.defineOption('vimMode', false, function(cm, val, prev) {
+ if (val && cm.getOption("keyMap") != "vim")
+ cm.setOption("keyMap", "vim");
+ else if (!val && prev != CodeMirror.Init && /^vim/.test(cm.getOption("keyMap")))
+ cm.setOption("keyMap", "default");
+ });
+
+ function cmKey(key, cm) {
+ if (!cm) { return undefined; }
+ var vimKey = cmKeyToVimKey(key);
+ if (!vimKey) {
+ return false;
+ }
+ var cmd = CodeMirror.Vim.findKey(cm, vimKey);
+ if (typeof cmd == 'function') {
+ CodeMirror.signal(cm, 'vim-keypress', vimKey);
+ }
+ return cmd;
+ }
+
+ var modifiers = {'Shift': 'S', 'Ctrl': 'C', 'Alt': 'A', 'Cmd': 'D', 'Mod': 'A'};
+ var specialKeys = {Enter:'CR',Backspace:'BS',Delete:'Del'};
+ function cmKeyToVimKey(key) {
+ if (key.charAt(0) == '\'') {
+ // Keypress character binding of format "'a'"
+ return key.charAt(1);
+ }
+ var pieces = key.split(/-(?!$)/);
+ var lastPiece = pieces[pieces.length - 1];
+ if (pieces.length == 1 && pieces[0].length == 1) {
+ // No-modifier bindings use literal character bindings above. Skip.
+ return false;
+ } else if (pieces.length == 2 && pieces[0] == 'Shift' && lastPiece.length == 1) {
+ // Ignore Shift+char bindings as they should be handled by literal character.
+ return false;
+ }
+ var hasCharacter = false;
+ for (var i = 0; i < pieces.length; i++) {
+ var piece = pieces[i];
+ if (piece in modifiers) { pieces[i] = modifiers[piece]; }
+ else { hasCharacter = true; }
+ if (piece in specialKeys) { pieces[i] = specialKeys[piece]; }
+ }
+ if (!hasCharacter) {
+ // Vim does not support modifier only keys.
+ return false;
+ }
+ // TODO: Current bindings expect the character to be lower case, but
+ // it looks like vim key notation uses upper case.
+ if (isUpperCase(lastPiece)) {
+ pieces[pieces.length - 1] = lastPiece.toLowerCase();
+ }
+ return '<' + pieces.join('-') + '>';
+ }
+
+ function getOnPasteFn(cm) {
+ var vim = cm.state.vim;
+ if (!vim.onPasteFn) {
+ vim.onPasteFn = function() {
+ if (!vim.insertMode) {
+ cm.setCursor(offsetCursor(cm.getCursor(), 0, 1));
+ actions.enterInsertMode(cm, {}, vim);
+ }
+ };
+ }
+ return vim.onPasteFn;
+ }
+
+ var numberRegex = /[\d]/;
+ var wordCharTest = [CodeMirror.isWordChar, function(ch) {
+ return ch && !CodeMirror.isWordChar(ch) && !/\s/.test(ch);
+ }], bigWordCharTest = [function(ch) {
+ return /\S/.test(ch);
+ }];
+ function makeKeyRange(start, size) {
+ var keys = [];
+ for (var i = start; i < start + size; i++) {
+ keys.push(String.fromCharCode(i));
+ }
+ return keys;
+ }
+ var upperCaseAlphabet = makeKeyRange(65, 26);
+ var lowerCaseAlphabet = makeKeyRange(97, 26);
+ var numbers = makeKeyRange(48, 10);
+ var validMarks = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['<', '>']);
+ var validRegisters = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['-', '"', '.', ':', '/']);
+
+ function isLine(cm, line) {
+ return line >= cm.firstLine() && line <= cm.lastLine();
+ }
+ function isLowerCase(k) {
+ return (/^[a-z]$/).test(k);
+ }
+ function isMatchableSymbol(k) {
+ return '()[]{}'.indexOf(k) != -1;
+ }
+ function isNumber(k) {
+ return numberRegex.test(k);
+ }
+ function isUpperCase(k) {
+ return (/^[A-Z]$/).test(k);
+ }
+ function isWhiteSpaceString(k) {
+ return (/^\s*$/).test(k);
+ }
+ function inArray(val, arr) {
+ for (var i = 0; i < arr.length; i++) {
+ if (arr[i] == val) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ var options = {};
+ function defineOption(name, defaultValue, type, aliases, callback) {
+ if (defaultValue === undefined && !callback) {
+ throw Error('defaultValue is required unless callback is provided');
+ }
+ if (!type) { type = 'string'; }
+ options[name] = {
+ type: type,
+ defaultValue: defaultValue,
+ callback: callback
+ };
+ if (aliases) {
+ for (var i = 0; i < aliases.length; i++) {
+ options[aliases[i]] = options[name];
+ }
+ }
+ if (defaultValue) {
+ setOption(name, defaultValue);
+ }
+ }
+
+ function setOption(name, value, cm, cfg) {
+ var option = options[name];
+ cfg = cfg || {};
+ var scope = cfg.scope;
+ if (!option) {
+ 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;
+ }
+ }
+ if (option.callback) {
+ if (scope !== 'local') {
+ option.callback(value, undefined);
+ }
+ if (scope !== 'global' && cm) {
+ option.callback(value, cm);
+ }
+ } else {
+ if (scope !== 'local') {
+ option.value = option.type == 'boolean' ? !!value : value;
+ }
+ if (scope !== 'global' && cm) {
+ cm.state.vim.options[name] = {value: value};
+ }
+ }
+ }
+
+ function getOption(name, cm, cfg) {
+ var option = options[name];
+ cfg = cfg || {};
+ var scope = cfg.scope;
+ if (!option) {
+ throw Error('Unknown option: ' + name);
+ }
+ if (option.callback) {
+ var local = cm && option.callback(undefined, cm);
+ if (scope !== 'global' && local !== undefined) {
+ return local;
+ }
+ if (scope !== 'local') {
+ return option.callback();
+ }
+ return;
+ } else {
+ var local = (scope !== 'global') && (cm && cm.state.vim.options[name]);
+ return (local || (scope !== 'local') && option || {}).value;
+ }
+ }
+
+ defineOption('filetype', undefined, 'string', ['ft'], function(name, cm) {
+ // Option is local. Do nothing for global.
+ if (cm === undefined) {
+ return;
+ }
+ // The 'filetype' option proxies to the CodeMirror 'mode' option.
+ if (name === undefined) {
+ var mode = cm.getOption('mode');
+ return mode == 'null' ? '' : mode;
+ } else {
+ var mode = name == '' ? 'null' : name;
+ cm.setOption('mode', mode);
+ }
+ });
+
+ var createCircularJumpList = function() {
+ var size = 100;
+ var pointer = -1;
+ var head = 0;
+ var tail = 0;
+ var buffer = new Array(size);
+ function add(cm, oldCur, newCur) {
+ var current = pointer % size;
+ var curMark = buffer[current];
+ function useNextSlot(cursor) {
+ var next = ++pointer % size;
+ var trashMark = buffer[next];
+ if (trashMark) {
+ trashMark.clear();
+ }
+ buffer[next] = cm.setBookmark(cursor);
+ }
+ if (curMark) {
+ var markPos = curMark.find();
+ // avoid recording redundant cursor position
+ if (markPos && !cursorEqual(markPos, oldCur)) {
+ useNextSlot(oldCur);
+ }
+ } else {
+ useNextSlot(oldCur);
+ }
+ useNextSlot(newCur);
+ head = pointer;
+ tail = pointer - size + 1;
+ if (tail < 0) {
+ tail = 0;
+ }
+ }
+ function move(cm, offset) {
+ pointer += offset;
+ if (pointer > head) {
+ pointer = head;
+ } else if (pointer < tail) {
+ pointer = tail;
+ }
+ var mark = buffer[(size + pointer) % size];
+ // skip marks that are temporarily removed from text buffer
+ if (mark && !mark.find()) {
+ var inc = offset > 0 ? 1 : -1;
+ var newCur;
+ var oldCur = cm.getCursor();
+ do {
+ pointer += inc;
+ mark = buffer[(size + pointer) % size];
+ // skip marks that are the same as current position
+ if (mark &&
+ (newCur = mark.find()) &&
+ !cursorEqual(oldCur, newCur)) {
+ break;
+ }
+ } while (pointer < head && pointer > tail);
+ }
+ return mark;
+ }
+ return {
+ cachedCursor: undefined, //used for # and * jumps
+ add: add,
+ move: move
+ };
+ };
+
+ // Returns an object to track the changes associated insert mode. It
+ // clones the object that is passed in, or creates an empty object one if
+ // none is provided.
+ var createInsertModeChanges = function(c) {
+ if (c) {
+ // Copy construction
+ return {
+ changes: c.changes,
+ expectCursorActivityForChange: c.expectCursorActivityForChange
+ };
+ }
+ return {
+ // Change list
+ changes: [],
+ // Set to true on change, false on cursorActivity.
+ expectCursorActivityForChange: false
+ };
+ };
+
+ function MacroModeState() {
+ this.latestRegister = undefined;
+ this.isPlaying = false;
+ this.isRecording = false;
+ this.replaySearchQueries = [];
+ this.onRecordingDone = undefined;
+ this.lastInsertModeChanges = createInsertModeChanges();
+ }
+ MacroModeState.prototype = {
+ exitMacroRecordMode: function() {
+ var macroModeState = vimGlobalState.macroModeState;
+ if (macroModeState.onRecordingDone) {
+ macroModeState.onRecordingDone(); // close dialog
+ }
+ macroModeState.onRecordingDone = undefined;
+ macroModeState.isRecording = false;
+ },
+ enterMacroRecordMode: function(cm, registerName) {
+ var register =
+ vimGlobalState.registerController.getRegister(registerName);
+ if (register) {
+ register.clear();
+ this.latestRegister = registerName;
+ if (cm.openDialog) {
+ this.onRecordingDone = cm.openDialog(
+ '(recording)['+registerName+']', null, {bottom:true});
+ }
+ this.isRecording = true;
+ }
+ }
+ };
+
+ function maybeInitVimState(cm) {
+ if (!cm.state.vim) {
+ // Store instance state in the CodeMirror object.
+ cm.state.vim = {
+ inputState: new InputState(),
+ // Vim's input state that triggered the last edit, used to repeat
+ // motions and operators with '.'.
+ lastEditInputState: undefined,
+ // Vim's action command before the last edit, used to repeat actions
+ // with '.' and insert mode repeat.
+ lastEditActionCommand: undefined,
+ // When using jk for navigation, if you move from a longer line to a
+ // shorter line, the cursor may clip to the end of the shorter line.
+ // If j is pressed again and cursor goes to the next line, the
+ // cursor should go back to its horizontal position on the longer
+ // line if it can. This is to keep track of the horizontal position.
+ lastHPos: -1,
+ // Doing the same with screen-position for gj/gk
+ lastHSPos: -1,
+ // The last motion command run. Cleared if a non-motion command gets
+ // executed in between.
+ lastMotion: null,
+ marks: {},
+ // Mark for rendering fake cursor for visual mode.
+ fakeCursor: null,
+ insertMode: false,
+ // Repeat count for changes made in insert mode, triggered by key
+ // sequences like 3,i. Only exists when insertMode is true.
+ insertModeRepeat: undefined,
+ visualMode: false,
+ // If we are in visual line mode. No effect if visualMode is false.
+ visualLine: false,
+ visualBlock: false,
+ lastSelection: null,
+ lastPastedText: null,
+ sel: {},
+ // Buffer-local/window-local values of vim options.
+ options: {}
+ };
+ }
+ return cm.state.vim;
+ }
+ var vimGlobalState;
+ function resetVimGlobalState() {
+ vimGlobalState = {
+ // The current search query.
+ searchQuery: null,
+ // Whether we are searching backwards.
+ searchIsReversed: false,
+ // Replace part of the last substituted pattern
+ lastSubstituteReplacePart: undefined,
+ jumpList: createCircularJumpList(),
+ macroModeState: new MacroModeState,
+ // Recording latest f, t, F or T motion command.
+ lastCharacterSearch: {increment:0, forward:true, selectedCharacter:''},
+ registerController: new RegisterController({}),
+ // search history buffer
+ searchHistoryController: new HistoryController({}),
+ // ex Command history buffer
+ exCommandHistoryController : new HistoryController({})
+ };
+ for (var optionName in options) {
+ var option = options[optionName];
+ option.value = option.defaultValue;
+ }
+ }
+
+ var lastInsertModeKeyTimer;
+ var vimApi= {
+ buildKeyMap: function() {
+ // TODO: Convert keymap into dictionary format for fast lookup.
+ },
+ // Testing hook, though it might be useful to expose the register
+ // controller anyways.
+ getRegisterController: function() {
+ return vimGlobalState.registerController;
+ },
+ // Testing hook.
+ resetVimGlobalState_: resetVimGlobalState,
+
+ // Testing hook.
+ getVimGlobalState_: function() {
+ return vimGlobalState;
+ },
+
+ // Testing hook.
+ maybeInitVimState_: maybeInitVimState,
+
+ suppressErrorLogging: false,
+
+ InsertModeKey: InsertModeKey,
+ map: function(lhs, rhs, ctx) {
+ // Add user defined key bindings.
+ exCommandDispatcher.map(lhs, rhs, ctx);
+ },
+ unmap: function(lhs, ctx) {
+ exCommandDispatcher.unmap(lhs, ctx);
+ },
+ // TODO: Expose setOption and getOption as instance methods. Need to decide how to namespace
+ // them, or somehow make them work with the existing CodeMirror setOption/getOption API.
+ setOption: setOption,
+ getOption: getOption,
+ defineOption: defineOption,
+ defineEx: function(name, prefix, func){
+ if (!prefix) {
+ prefix = name;
+ } else if (name.indexOf(prefix) !== 0) {
+ throw new Error('(Vim.defineEx) "'+prefix+'" is not a prefix of "'+name+'", command not registered');
+ }
+ exCommands[name]=func;
+ exCommandDispatcher.commandMap_[prefix]={name:name, shortName:prefix, type:'api'};
+ },
+ handleKey: function (cm, key, origin) {
+ var command = this.findKey(cm, key, origin);
+ if (typeof command === 'function') {
+ return command();
+ }
+ },
+ /**
+ * This is the outermost function called by CodeMirror, after keys have
+ * been mapped to their Vim equivalents.
+ *
+ * Finds a command based on the key (and cached keys if there is a
+ * multi-key sequence). Returns `undefined` if no key is matched, a noop
+ * function if a partial match is found (multi-key), and a function to
+ * execute the bound command if a a key is matched. The function always
+ * returns true.
+ */
+ findKey: function(cm, key, origin) {
+ var vim = maybeInitVimState(cm);
+ function handleMacroRecording() {
+ var macroModeState = vimGlobalState.macroModeState;
+ if (macroModeState.isRecording) {
+ if (key == 'q') {
+ macroModeState.exitMacroRecordMode();
+ clearInputState(cm);
+ return true;
+ }
+ if (origin != 'mapping') {
+ logKey(macroModeState, key);
+ }
+ }
+ }
+ function handleEsc() {
+ if (key == '<Esc>') {
+ // Clear input state and get back to normal mode.
+ clearInputState(cm);
+ if (vim.visualMode) {
+ exitVisualMode(cm);
+ } else if (vim.insertMode) {
+ exitInsertMode(cm);
+ }
+ return true;
+ }
+ }
+ function doKeyToKey(keys) {
+ // TODO: prevent infinite recursion.
+ var match;
+ while (keys) {
+ // Pull off one command key, which is either a single character
+ // or a special sequence wrapped in '<' and '>', e.g. '<Space>'.
+ match = (/<\w+-.+?>|<\w+>|./).exec(keys);
+ key = match[0];
+ keys = keys.substring(match.index + key.length);
+ CodeMirror.Vim.handleKey(cm, key, 'mapping');
+ }
+ }
+
+ function handleKeyInsertMode() {
+ if (handleEsc()) { return true; }
+ var keys = vim.inputState.keyBuffer = vim.inputState.keyBuffer + key;
+ var keysAreChars = key.length == 1;
+ var match = commandDispatcher.matchCommand(keys, defaultKeymap, vim.inputState, 'insert');
+ // Need to check all key substrings in insert mode.
+ while (keys.length > 1 && match.type != 'full') {
+ var keys = vim.inputState.keyBuffer = keys.slice(1);
+ var thisMatch = commandDispatcher.matchCommand(keys, defaultKeymap, vim.inputState, 'insert');
+ if (thisMatch.type != 'none') { match = thisMatch; }
+ }
+ if (match.type == 'none') { clearInputState(cm); return false; }
+ else if (match.type == 'partial') {
+ if (lastInsertModeKeyTimer) { window.clearTimeout(lastInsertModeKeyTimer); }
+ lastInsertModeKeyTimer = window.setTimeout(
+ function() { if (vim.insertMode && vim.inputState.keyBuffer) { clearInputState(cm); } },
+ getOption('insertModeEscKeysTimeout'));
+ return !keysAreChars;
+ }
+
+ if (lastInsertModeKeyTimer) { window.clearTimeout(lastInsertModeKeyTimer); }
+ if (keysAreChars) {
+ var here = cm.getCursor();
+ cm.replaceRange('', offsetCursor(here, 0, -(keys.length - 1)), here, '+input');
+ }
+ clearInputState(cm);
+ return match.command;
+ }
+
+ function handleKeyNonInsertMode() {
+ if (handleMacroRecording() || handleEsc()) { return true; };
+
+ var keys = vim.inputState.keyBuffer = vim.inputState.keyBuffer + key;
+ if (/^[1-9]\d*$/.test(keys)) { return true; }
+
+ var keysMatcher = /^(\d*)(.*)$/.exec(keys);
+ if (!keysMatcher) { clearInputState(cm); return false; }
+ var context = vim.visualMode ? 'visual' :
+ 'normal';
+ var match = commandDispatcher.matchCommand(keysMatcher[2] || keysMatcher[1], defaultKeymap, vim.inputState, context);
+ if (match.type == 'none') { clearInputState(cm); return false; }
+ else if (match.type == 'partial') { return true; }
+
+ vim.inputState.keyBuffer = '';
+ var keysMatcher = /^(\d*)(.*)$/.exec(keys);
+ if (keysMatcher[1] && keysMatcher[1] != '0') {
+ vim.inputState.pushRepeatDigit(keysMatcher[1]);
+ }
+ return match.command;
+ }
+
+ var command;
+ if (vim.insertMode) { command = handleKeyInsertMode(); }
+ else { command = handleKeyNonInsertMode(); }
+ if (command === false) {
+ return undefined;
+ } else if (command === true) {
+ // TODO: Look into using CodeMirror's multi-key handling.
+ // Return no-op since we are caching the key. Counts as handled, but
+ // don't want act on it just yet.
+ return function() {};
+ } else {
+ return function() {
+ return cm.operation(function() {
+ cm.curOp.isVimOp = true;
+ try {
+ if (command.type == 'keyToKey') {
+ doKeyToKey(command.toKeys);
+ } else {
+ commandDispatcher.processCommand(cm, vim, command);
+ }
+ } catch (e) {
+ // clear VIM state in case it's in a bad state.
+ cm.state.vim = undefined;
+ maybeInitVimState(cm);
+ if (!CodeMirror.Vim.suppressErrorLogging) {
+ console['log'](e);
+ }
+ throw e;
+ }
+ return true;
+ });
+ };
+ }
+ },
+ handleEx: function(cm, input) {
+ exCommandDispatcher.processCommand(cm, input);
+ },
+
+ defineMotion: defineMotion,
+ defineAction: defineAction,
+ defineOperator: defineOperator,
+ mapCommand: mapCommand,
+ _mapCommand: _mapCommand,
+
+ defineRegister: defineRegister,
+
+ exitVisualMode: exitVisualMode,
+ exitInsertMode: exitInsertMode
+ };
+
+ // Represents the current input state.
+ function InputState() {
+ this.prefixRepeat = [];
+ this.motionRepeat = [];
+
+ this.operator = null;
+ this.operatorArgs = null;
+ this.motion = null;
+ this.motionArgs = null;
+ this.keyBuffer = []; // For matching multi-key commands.
+ this.registerName = null; // Defaults to the unnamed register.
+ }
+ InputState.prototype.pushRepeatDigit = function(n) {
+ if (!this.operator) {
+ this.prefixRepeat = this.prefixRepeat.concat(n);
+ } else {
+ this.motionRepeat = this.motionRepeat.concat(n);
+ }
+ };
+ InputState.prototype.getRepeat = function() {
+ var repeat = 0;
+ if (this.prefixRepeat.length > 0 || this.motionRepeat.length > 0) {
+ repeat = 1;
+ if (this.prefixRepeat.length > 0) {
+ repeat *= parseInt(this.prefixRepeat.join(''), 10);
+ }
+ if (this.motionRepeat.length > 0) {
+ repeat *= parseInt(this.motionRepeat.join(''), 10);
+ }
+ }
+ return repeat;
+ };
+
+ function clearInputState(cm, reason) {
+ cm.state.vim.inputState = new InputState();
+ CodeMirror.signal(cm, 'vim-command-done', reason);
+ }
+
+ /*
+ * Register stores information about copy and paste registers. Besides
+ * text, a register must store whether it is linewise (i.e., when it is
+ * pasted, should it insert itself into a new line, or should the text be
+ * inserted at the cursor position.)
+ */
+ function Register(text, linewise, blockwise) {
+ this.clear();
+ this.keyBuffer = [text || ''];
+ this.insertModeChanges = [];
+ this.searchQueries = [];
+ this.linewise = !!linewise;
+ this.blockwise = !!blockwise;
+ }
+ Register.prototype = {
+ setText: function(text, linewise, blockwise) {
+ this.keyBuffer = [text || ''];
+ this.linewise = !!linewise;
+ this.blockwise = !!blockwise;
+ },
+ pushText: function(text, linewise) {
+ // if this register has ever been set to linewise, use linewise.
+ if (linewise) {
+ if (!this.linewise) {
+ this.keyBuffer.push('\n');
+ }
+ this.linewise = true;
+ }
+ this.keyBuffer.push(text);
+ },
+ pushInsertModeChanges: function(changes) {
+ this.insertModeChanges.push(createInsertModeChanges(changes));
+ },
+ pushSearchQuery: function(query) {
+ this.searchQueries.push(query);
+ },
+ clear: function() {
+ this.keyBuffer = [];
+ this.insertModeChanges = [];
+ this.searchQueries = [];
+ this.linewise = false;
+ },
+ toString: function() {
+ return this.keyBuffer.join('');
+ }
+ };
+
+ /**
+ * Defines an external register.
+ *
+ * The name should be a single character that will be used to reference the register.
+ * The register should support setText, pushText, clear, and toString(). See Register
+ * for a reference implementation.
+ */
+ function defineRegister(name, register) {
+ var registers = vimGlobalState.registerController.registers[name];
+ if (!name || name.length != 1) {
+ throw Error('Register name must be 1 character');
+ }
+ if (registers[name]) {
+ throw Error('Register already defined ' + name);
+ }
+ registers[name] = register;
+ validRegisters.push(name);
+ }
+
+ /*
+ * vim registers allow you to keep many independent copy and paste buffers.
+ * See http://usevim.com/2012/04/13/registers/ for an introduction.
+ *
+ * RegisterController keeps the state of all the registers. An initial
+ * state may be passed in. The unnamed register '"' will always be
+ * overridden.
+ */
+ function RegisterController(registers) {
+ this.registers = registers;
+ this.unnamedRegister = registers['"'] = new Register();
+ registers['.'] = new Register();
+ registers[':'] = new Register();
+ registers['/'] = new Register();
+ }
+ RegisterController.prototype = {
+ pushText: function(registerName, operator, text, linewise, blockwise) {
+ if (linewise && text.charAt(0) == '\n') {
+ text = text.slice(1) + '\n';
+ }
+ if (linewise && text.charAt(text.length - 1) !== '\n'){
+ text += '\n';
+ }
+ // Lowercase and uppercase registers refer to the same register.
+ // Uppercase just means append.
+ var register = this.isValidRegister(registerName) ?
+ this.getRegister(registerName) : null;
+ // if no register/an invalid register was specified, things go to the
+ // default registers
+ if (!register) {
+ switch (operator) {
+ case 'yank':
+ // The 0 register contains the text from the most recent yank.
+ this.registers['0'] = new Register(text, linewise, blockwise);
+ break;
+ case 'delete':
+ case 'change':
+ if (text.indexOf('\n') == -1) {
+ // Delete less than 1 line. Update the small delete register.
+ this.registers['-'] = new Register(text, linewise);
+ } else {
+ // Shift down the contents of the numbered registers and put the
+ // deleted text into register 1.
+ this.shiftNumericRegisters_();
+ this.registers['1'] = new Register(text, linewise);
+ }
+ break;
+ }
+ // Make sure the unnamed register is set to what just happened
+ this.unnamedRegister.setText(text, linewise, blockwise);
+ return;
+ }
+
+ // If we've gotten to this point, we've actually specified a register
+ var append = isUpperCase(registerName);
+ if (append) {
+ register.pushText(text, linewise);
+ } else {
+ register.setText(text, linewise, blockwise);
+ }
+ // The unnamed register always has the same value as the last used
+ // register.
+ this.unnamedRegister.setText(register.toString(), linewise);
+ },
+ // Gets the register named @name. If one of @name doesn't already exist,
+ // create it. If @name is invalid, return the unnamedRegister.
+ getRegister: function(name) {
+ if (!this.isValidRegister(name)) {
+ return this.unnamedRegister;
+ }
+ name = name.toLowerCase();
+ if (!this.registers[name]) {
+ this.registers[name] = new Register();
+ }
+ return this.registers[name];
+ },
+ isValidRegister: function(name) {
+ return name && inArray(name, validRegisters);
+ },
+ shiftNumericRegisters_: function() {
+ for (var i = 9; i >= 2; i--) {
+ this.registers[i] = this.getRegister('' + (i - 1));
+ }
+ }
+ };
+ function HistoryController() {
+ this.historyBuffer = [];
+ this.iterator = 0;
+ this.initialPrefix = null;
+ }
+ HistoryController.prototype = {
+ // the input argument here acts a user entered prefix for a small time
+ // until we start autocompletion in which case it is the autocompleted.
+ nextMatch: function (input, up) {
+ var historyBuffer = this.historyBuffer;
+ var dir = up ? -1 : 1;
+ if (this.initialPrefix === null) this.initialPrefix = input;
+ for (var i = this.iterator + dir; up ? i >= 0 : i < historyBuffer.length; i+= dir) {
+ var element = historyBuffer[i];
+ for (var j = 0; j <= element.length; j++) {
+ if (this.initialPrefix == element.substring(0, j)) {
+ this.iterator = i;
+ return element;
+ }
+ }
+ }
+ // should return the user input in case we reach the end of buffer.
+ if (i >= historyBuffer.length) {
+ this.iterator = historyBuffer.length;
+ return this.initialPrefix;
+ }
+ // return the last autocompleted query or exCommand as it is.
+ if (i < 0 ) return input;
+ },
+ pushInput: function(input) {
+ var index = this.historyBuffer.indexOf(input);
+ if (index > -1) this.historyBuffer.splice(index, 1);
+ if (input.length) this.historyBuffer.push(input);
+ },
+ reset: function() {
+ this.initialPrefix = null;
+ this.iterator = this.historyBuffer.length;
+ }
+ };
+ var commandDispatcher = {
+ matchCommand: function(keys, keyMap, inputState, context) {
+ var matches = commandMatches(keys, keyMap, context, inputState);
+ if (!matches.full && !matches.partial) {
+ return {type: 'none'};
+ } else if (!matches.full && matches.partial) {
+ return {type: 'partial'};
+ }
+
+ var bestMatch;
+ for (var i = 0; i < matches.full.length; i++) {
+ var match = matches.full[i];
+ if (!bestMatch) {
+ bestMatch = match;
+ }
+ }
+ if (bestMatch.keys.slice(-11) == '<character>') {
+ inputState.selectedCharacter = lastChar(keys);
+ }
+ return {type: 'full', command: bestMatch};
+ },
+ processCommand: function(cm, vim, command) {
+ vim.inputState.repeatOverride = command.repeatOverride;
+ switch (command.type) {
+ case 'motion':
+ this.processMotion(cm, vim, command);
+ break;
+ case 'operator':
+ this.processOperator(cm, vim, command);
+ break;
+ case 'operatorMotion':
+ this.processOperatorMotion(cm, vim, command);
+ break;
+ case 'action':
+ this.processAction(cm, vim, command);
+ break;
+ case 'search':
+ this.processSearch(cm, vim, command);
+ break;
+ case 'ex':
+ case 'keyToEx':
+ this.processEx(cm, vim, command);
+ break;
+ default:
+ break;
+ }
+ },
+ processMotion: function(cm, vim, command) {
+ vim.inputState.motion = command.motion;
+ vim.inputState.motionArgs = copyArgs(command.motionArgs);
+ this.evalInput(cm, vim);
+ },
+ processOperator: function(cm, vim, command) {
+ var inputState = vim.inputState;
+ if (inputState.operator) {
+ if (inputState.operator == command.operator) {
+ // Typing an operator twice like 'dd' makes the operator operate
+ // linewise
+ inputState.motion = 'expandToLine';
+ inputState.motionArgs = { linewise: true };
+ this.evalInput(cm, vim);
+ return;
+ } else {
+ // 2 different operators in a row doesn't make sense.
+ clearInputState(cm);
+ }
+ }
+ inputState.operator = command.operator;
+ inputState.operatorArgs = copyArgs(command.operatorArgs);
+ if (vim.visualMode) {
+ // Operating on a selection in visual mode. We don't need a motion.
+ this.evalInput(cm, vim);
+ }
+ },
+ processOperatorMotion: function(cm, vim, command) {
+ var visualMode = vim.visualMode;
+ var operatorMotionArgs = copyArgs(command.operatorMotionArgs);
+ if (operatorMotionArgs) {
+ // Operator motions may have special behavior in visual mode.
+ if (visualMode && operatorMotionArgs.visualLine) {
+ vim.visualLine = true;
+ }
+ }
+ this.processOperator(cm, vim, command);
+ if (!visualMode) {
+ this.processMotion(cm, vim, command);
+ }
+ },
+ processAction: function(cm, vim, command) {
+ var inputState = vim.inputState;
+ var repeat = inputState.getRepeat();
+ var repeatIsExplicit = !!repeat;
+ var actionArgs = copyArgs(command.actionArgs) || {};
+ if (inputState.selectedCharacter) {
+ actionArgs.selectedCharacter = inputState.selectedCharacter;
+ }
+ // Actions may or may not have motions and operators. Do these first.
+ if (command.operator) {
+ this.processOperator(cm, vim, command);
+ }
+ if (command.motion) {
+ this.processMotion(cm, vim, command);
+ }
+ if (command.motion || command.operator) {
+ this.evalInput(cm, vim);
+ }
+ actionArgs.repeat = repeat || 1;
+ actionArgs.repeatIsExplicit = repeatIsExplicit;
+ actionArgs.registerName = inputState.registerName;
+ clearInputState(cm);
+ vim.lastMotion = null;
+ if (command.isEdit) {
+ this.recordLastEdit(vim, inputState, command);
+ }
+ actions[command.action](cm, actionArgs, vim);
+ },
+ processSearch: function(cm, vim, command) {
+ if (!cm.getSearchCursor) {
+ // Search depends on SearchCursor.
+ return;
+ }
+ var forward = command.searchArgs.forward;
+ var wholeWordOnly = command.searchArgs.wholeWordOnly;
+ getSearchState(cm).setReversed(!forward);
+ var promptPrefix = (forward) ? '/' : '?';
+ var originalQuery = getSearchState(cm).getQuery();
+ var originalScrollPos = cm.getScrollInfo();
+ function handleQuery(query, ignoreCase, smartCase) {
+ vimGlobalState.searchHistoryController.pushInput(query);
+ vimGlobalState.searchHistoryController.reset();
+ try {
+ updateSearchQuery(cm, query, ignoreCase, smartCase);
+ } catch (e) {
+ showConfirm(cm, 'Invalid regex: ' + query);
+ clearInputState(cm);
+ return;
+ }
+ commandDispatcher.processMotion(cm, vim, {
+ type: 'motion',
+ motion: 'findNext',
+ motionArgs: { forward: true, toJumplist: command.searchArgs.toJumplist }
+ });
+ }
+ function onPromptClose(query) {
+ cm.scrollTo(originalScrollPos.left, originalScrollPos.top);
+ handleQuery(query, true /** ignoreCase */, true /** smartCase */);
+ var macroModeState = vimGlobalState.macroModeState;
+ if (macroModeState.isRecording) {
+ logSearchQuery(macroModeState, query);
+ }
+ }
+ function onPromptKeyUp(e, query, close) {
+ var keyName = CodeMirror.keyName(e), up;
+ if (keyName == 'Up' || keyName == 'Down') {
+ up = keyName == 'Up' ? true : false;
+ query = vimGlobalState.searchHistoryController.nextMatch(query, up) || '';
+ close(query);
+ } else {
+ if ( keyName != 'Left' && keyName != 'Right' && keyName != 'Ctrl' && keyName != 'Alt' && keyName != 'Shift')
+ vimGlobalState.searchHistoryController.reset();
+ }
+ var parsedQuery;
+ try {
+ parsedQuery = updateSearchQuery(cm, query,
+ true /** ignoreCase */, true /** smartCase */);
+ } catch (e) {
+ // Swallow bad regexes for incremental search.
+ }
+ if (parsedQuery) {
+ cm.scrollIntoView(findNext(cm, !forward, parsedQuery), 30);
+ } else {
+ clearSearchHighlight(cm);
+ cm.scrollTo(originalScrollPos.left, originalScrollPos.top);
+ }
+ }
+ function onPromptKeyDown(e, query, close) {
+ var keyName = CodeMirror.keyName(e);
+ if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[' ||
+ (keyName == 'Backspace' && query == '')) {
+ vimGlobalState.searchHistoryController.pushInput(query);
+ vimGlobalState.searchHistoryController.reset();
+ updateSearchQuery(cm, originalQuery);
+ clearSearchHighlight(cm);
+ cm.scrollTo(originalScrollPos.left, originalScrollPos.top);
+ CodeMirror.e_stop(e);
+ clearInputState(cm);
+ close();
+ cm.focus();
+ } else if (keyName == 'Ctrl-U') {
+ // Ctrl-U clears input.
+ CodeMirror.e_stop(e);
+ close('');
+ }
+ }
+ switch (command.searchArgs.querySrc) {
+ case 'prompt':
+ var macroModeState = vimGlobalState.macroModeState;
+ if (macroModeState.isPlaying) {
+ var query = macroModeState.replaySearchQueries.shift();
+ handleQuery(query, true /** ignoreCase */, false /** smartCase */);
+ } else {
+ showPrompt(cm, {
+ onClose: onPromptClose,
+ prefix: promptPrefix,
+ desc: searchPromptDesc,
+ onKeyUp: onPromptKeyUp,
+ onKeyDown: onPromptKeyDown
+ });
+ }
+ break;
+ case 'wordUnderCursor':
+ var word = expandWordUnderCursor(cm, false /** inclusive */,
+ true /** forward */, false /** bigWord */,
+ true /** noSymbol */);
+ var isKeyword = true;
+ if (!word) {
+ word = expandWordUnderCursor(cm, false /** inclusive */,
+ true /** forward */, false /** bigWord */,
+ false /** noSymbol */);
+ isKeyword = false;
+ }
+ if (!word) {
+ return;
+ }
+ var query = cm.getLine(word.start.line).substring(word.start.ch,
+ word.end.ch);
+ if (isKeyword && wholeWordOnly) {
+ query = '\\b' + query + '\\b';
+ } else {
+ query = escapeRegex(query);
+ }
+
+ // cachedCursor is used to save the old position of the cursor
+ // when * or # causes vim to seek for the nearest word and shift
+ // the cursor before entering the motion.
+ vimGlobalState.jumpList.cachedCursor = cm.getCursor();
+ cm.setCursor(word.start);
+
+ handleQuery(query, true /** ignoreCase */, false /** smartCase */);
+ break;
+ }
+ },
+ processEx: function(cm, vim, command) {
+ function onPromptClose(input) {
+ // Give the prompt some time to close so that if processCommand shows
+ // an error, the elements don't overlap.
+ vimGlobalState.exCommandHistoryController.pushInput(input);
+ vimGlobalState.exCommandHistoryController.reset();
+ exCommandDispatcher.processCommand(cm, input);
+ }
+ function onPromptKeyDown(e, input, close) {
+ var keyName = CodeMirror.keyName(e), up;
+ if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[' ||
+ (keyName == 'Backspace' && input == '')) {
+ vimGlobalState.exCommandHistoryController.pushInput(input);
+ vimGlobalState.exCommandHistoryController.reset();
+ CodeMirror.e_stop(e);
+ clearInputState(cm);
+ close();
+ cm.focus();
+ }
+ if (keyName == 'Up' || keyName == 'Down') {
+ up = keyName == 'Up' ? true : false;
+ input = vimGlobalState.exCommandHistoryController.nextMatch(input, up) || '';
+ close(input);
+ } else if (keyName == 'Ctrl-U') {
+ // Ctrl-U clears input.
+ CodeMirror.e_stop(e);
+ close('');
+ } else {
+ if ( keyName != 'Left' && keyName != 'Right' && keyName != 'Ctrl' && keyName != 'Alt' && keyName != 'Shift')
+ vimGlobalState.exCommandHistoryController.reset();
+ }
+ }
+ if (command.type == 'keyToEx') {
+ // Handle user defined Ex to Ex mappings
+ exCommandDispatcher.processCommand(cm, command.exArgs.input);
+ } else {
+ if (vim.visualMode) {
+ showPrompt(cm, { onClose: onPromptClose, prefix: ':', value: '\'<,\'>',
+ onKeyDown: onPromptKeyDown});
+ } else {
+ showPrompt(cm, { onClose: onPromptClose, prefix: ':',
+ onKeyDown: onPromptKeyDown});
+ }
+ }
+ },
+ evalInput: function(cm, vim) {
+ // If the motion command is set, execute both the operator and motion.
+ // Otherwise return.
+ var inputState = vim.inputState;
+ var motion = inputState.motion;
+ var motionArgs = inputState.motionArgs || {};
+ var operator = inputState.operator;
+ var operatorArgs = inputState.operatorArgs || {};
+ var registerName = inputState.registerName;
+ var sel = vim.sel;
+ // TODO: Make sure cm and vim selections are identical outside visual mode.
+ var origHead = copyCursor(vim.visualMode ? clipCursorToContent(cm, sel.head): cm.getCursor('head'));
+ var origAnchor = copyCursor(vim.visualMode ? clipCursorToContent(cm, sel.anchor) : cm.getCursor('anchor'));
+ var oldHead = copyCursor(origHead);
+ var oldAnchor = copyCursor(origAnchor);
+ var newHead, newAnchor;
+ var repeat;
+ if (operator) {
+ this.recordLastEdit(vim, inputState);
+ }
+ if (inputState.repeatOverride !== undefined) {
+ // If repeatOverride is specified, that takes precedence over the
+ // input state's repeat. Used by Ex mode and can be user defined.
+ repeat = inputState.repeatOverride;
+ } else {
+ repeat = inputState.getRepeat();
+ }
+ if (repeat > 0 && motionArgs.explicitRepeat) {
+ motionArgs.repeatIsExplicit = true;
+ } else if (motionArgs.noRepeat ||
+ (!motionArgs.explicitRepeat && repeat === 0)) {
+ repeat = 1;
+ motionArgs.repeatIsExplicit = false;
+ }
+ if (inputState.selectedCharacter) {
+ // If there is a character input, stick it in all of the arg arrays.
+ motionArgs.selectedCharacter = operatorArgs.selectedCharacter =
+ inputState.selectedCharacter;
+ }
+ motionArgs.repeat = repeat;
+ clearInputState(cm);
+ if (motion) {
+ var motionResult = motions[motion](cm, origHead, motionArgs, vim);
+ vim.lastMotion = motions[motion];
+ if (!motionResult) {
+ return;
+ }
+ if (motionArgs.toJumplist) {
+ var jumpList = vimGlobalState.jumpList;
+ // if the current motion is # or *, use cachedCursor
+ var cachedCursor = jumpList.cachedCursor;
+ if (cachedCursor) {
+ recordJumpPosition(cm, cachedCursor, motionResult);
+ delete jumpList.cachedCursor;
+ } else {
+ recordJumpPosition(cm, origHead, motionResult);
+ }
+ }
+ if (motionResult instanceof Array) {
+ newAnchor = motionResult[0];
+ newHead = motionResult[1];
+ } else {
+ newHead = motionResult;
+ }
+ // TODO: Handle null returns from motion commands better.
+ if (!newHead) {
+ newHead = copyCursor(origHead);
+ }
+ if (vim.visualMode) {
+ if (!(vim.visualBlock && newHead.ch === Infinity)) {
+ newHead = clipCursorToContent(cm, newHead, vim.visualBlock);
+ }
+ if (newAnchor) {
+ newAnchor = clipCursorToContent(cm, newAnchor, true);
+ }
+ newAnchor = newAnchor || oldAnchor;
+ sel.anchor = newAnchor;
+ sel.head = newHead;
+ updateCmSelection(cm);
+ updateMark(cm, vim, '<',
+ cursorIsBefore(newAnchor, newHead) ? newAnchor
+ : newHead);
+ updateMark(cm, vim, '>',
+ cursorIsBefore(newAnchor, newHead) ? newHead
+ : newAnchor);
+ } else if (!operator) {
+ newHead = clipCursorToContent(cm, newHead);
+ cm.setCursor(newHead.line, newHead.ch);
+ }
+ }
+ if (operator) {
+ if (operatorArgs.lastSel) {
+ // Replaying a visual mode operation
+ newAnchor = oldAnchor;
+ var lastSel = operatorArgs.lastSel;
+ var lineOffset = Math.abs(lastSel.head.line - lastSel.anchor.line);
+ var chOffset = Math.abs(lastSel.head.ch - lastSel.anchor.ch);
+ if (lastSel.visualLine) {
+ // Linewise Visual mode: The same number of lines.
+ newHead = Pos(oldAnchor.line + lineOffset, oldAnchor.ch);
+ } else if (lastSel.visualBlock) {
+ // Blockwise Visual mode: The same number of lines and columns.
+ newHead = Pos(oldAnchor.line + lineOffset, oldAnchor.ch + chOffset);
+ } else if (lastSel.head.line == lastSel.anchor.line) {
+ // Normal Visual mode within one line: The same number of characters.
+ newHead = Pos(oldAnchor.line, oldAnchor.ch + chOffset);
+ } else {
+ // Normal Visual mode with several lines: The same number of lines, in the
+ // last line the same number of characters as in the last line the last time.
+ newHead = Pos(oldAnchor.line + lineOffset, oldAnchor.ch);
+ }
+ vim.visualMode = true;
+ vim.visualLine = lastSel.visualLine;
+ vim.visualBlock = lastSel.visualBlock;
+ sel = vim.sel = {
+ anchor: newAnchor,
+ head: newHead
+ };
+ updateCmSelection(cm);
+ } else if (vim.visualMode) {
+ operatorArgs.lastSel = {
+ anchor: copyCursor(sel.anchor),
+ head: copyCursor(sel.head),
+ visualBlock: vim.visualBlock,
+ visualLine: vim.visualLine
+ };
+ }
+ var curStart, curEnd, linewise, mode;
+ var cmSel;
+ if (vim.visualMode) {
+ // Init visual op
+ curStart = cursorMin(sel.head, sel.anchor);
+ curEnd = cursorMax(sel.head, sel.anchor);
+ linewise = vim.visualLine || operatorArgs.linewise;
+ mode = vim.visualBlock ? 'block' :
+ linewise ? 'line' :
+ 'char';
+ cmSel = makeCmSelection(cm, {
+ anchor: curStart,
+ head: curEnd
+ }, mode);
+ if (linewise) {
+ var ranges = cmSel.ranges;
+ if (mode == 'block') {
+ // Linewise operators in visual block mode extend to end of line
+ for (var i = 0; i < ranges.length; i++) {
+ ranges[i].head.ch = lineLength(cm, ranges[i].head.line);
+ }
+ } else if (mode == 'line') {
+ ranges[0].head = Pos(ranges[0].head.line + 1, 0);
+ }
+ }
+ } else {
+ // Init motion op
+ curStart = copyCursor(newAnchor || oldAnchor);
+ curEnd = copyCursor(newHead || oldHead);
+ if (cursorIsBefore(curEnd, curStart)) {
+ var tmp = curStart;
+ curStart = curEnd;
+ curEnd = tmp;
+ }
+ linewise = motionArgs.linewise || operatorArgs.linewise;
+ if (linewise) {
+ // Expand selection to entire line.
+ expandSelectionToLine(cm, curStart, curEnd);
+ } else if (motionArgs.forward) {
+ // Clip to trailing newlines only if the motion goes forward.
+ clipToLine(cm, curStart, curEnd);
+ }
+ mode = 'char';
+ var exclusive = !motionArgs.inclusive || linewise;
+ cmSel = makeCmSelection(cm, {
+ anchor: curStart,
+ head: curEnd
+ }, mode, exclusive);
+ }
+ cm.setSelections(cmSel.ranges, cmSel.primary);
+ vim.lastMotion = null;
+ operatorArgs.repeat = repeat; // For indent in visual mode.
+ operatorArgs.registerName = registerName;
+ // Keep track of linewise as it affects how paste and change behave.
+ operatorArgs.linewise = linewise;
+ var operatorMoveTo = operators[operator](
+ cm, operatorArgs, cmSel.ranges, oldAnchor, newHead);
+ if (vim.visualMode) {
+ exitVisualMode(cm, operatorMoveTo != null);
+ }
+ if (operatorMoveTo) {
+ cm.setCursor(operatorMoveTo);
+ }
+ }
+ },
+ recordLastEdit: function(vim, inputState, actionCommand) {
+ var macroModeState = vimGlobalState.macroModeState;
+ if (macroModeState.isPlaying) { return; }
+ vim.lastEditInputState = inputState;
+ vim.lastEditActionCommand = actionCommand;
+ macroModeState.lastInsertModeChanges.changes = [];
+ macroModeState.lastInsertModeChanges.expectCursorActivityForChange = false;
+ }
+ };
+
+ /**
+ * typedef {Object{line:number,ch:number}} Cursor An object containing the
+ * position of the cursor.
+ */
+ // All of the functions below return Cursor objects.
+ var motions = {
+ moveToTopLine: function(cm, _head, motionArgs) {
+ var line = getUserVisibleLines(cm).top + motionArgs.repeat -1;
+ return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line)));
+ },
+ moveToMiddleLine: function(cm) {
+ var range = getUserVisibleLines(cm);
+ var line = Math.floor((range.top + range.bottom) * 0.5);
+ return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line)));
+ },
+ moveToBottomLine: function(cm, _head, motionArgs) {
+ var line = getUserVisibleLines(cm).bottom - motionArgs.repeat +1;
+ return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line)));
+ },
+ expandToLine: function(_cm, head, motionArgs) {
+ // Expands forward to end of line, and then to next line if repeat is
+ // >1. Does not handle backward motion!
+ var cur = head;
+ return Pos(cur.line + motionArgs.repeat - 1, Infinity);
+ },
+ findNext: function(cm, _head, motionArgs) {
+ var state = getSearchState(cm);
+ var query = state.getQuery();
+ if (!query) {
+ return;
+ }
+ var prev = !motionArgs.forward;
+ // If search is initiated with ? instead of /, negate direction.
+ prev = (state.isReversed()) ? !prev : prev;
+ highlightSearchMatches(cm, query);
+ return findNext(cm, prev/** prev */, query, motionArgs.repeat);
+ },
+ goToMark: function(cm, _head, motionArgs, vim) {
+ var mark = vim.marks[motionArgs.selectedCharacter];
+ if (mark) {
+ var pos = mark.find();
+ return motionArgs.linewise ? { line: pos.line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(pos.line)) } : pos;
+ }
+ return null;
+ },
+ moveToOtherHighlightedEnd: function(cm, _head, motionArgs, vim) {
+ if (vim.visualBlock && motionArgs.sameLine) {
+ var sel = vim.sel;
+ return [
+ clipCursorToContent(cm, Pos(sel.anchor.line, sel.head.ch)),
+ clipCursorToContent(cm, Pos(sel.head.line, sel.anchor.ch))
+ ];
+ } else {
+ return ([vim.sel.head, vim.sel.anchor]);
+ }
+ },
+ jumpToMark: function(cm, head, motionArgs, vim) {
+ var best = head;
+ for (var i = 0; i < motionArgs.repeat; i++) {
+ var cursor = best;
+ for (var key in vim.marks) {
+ if (!isLowerCase(key)) {
+ continue;
+ }
+ var mark = vim.marks[key].find();
+ var isWrongDirection = (motionArgs.forward) ?
+ cursorIsBefore(mark, cursor) : cursorIsBefore(cursor, mark);
+
+ if (isWrongDirection) {
+ continue;
+ }
+ if (motionArgs.linewise && (mark.line == cursor.line)) {
+ continue;
+ }
+
+ var equal = cursorEqual(cursor, best);
+ var between = (motionArgs.forward) ?
+ cursorIsBetween(cursor, mark, best) :
+ cursorIsBetween(best, mark, cursor);
+
+ if (equal || between) {
+ best = mark;
+ }
+ }
+ }
+
+ if (motionArgs.linewise) {
+ // Vim places the cursor on the first non-whitespace character of
+ // the line if there is one, else it places the cursor at the end
+ // of the line, regardless of whether a mark was found.
+ best = Pos(best.line, findFirstNonWhiteSpaceCharacter(cm.getLine(best.line)));
+ }
+ return best;
+ },
+ moveByCharacters: function(_cm, head, motionArgs) {
+ var cur = head;
+ var repeat = motionArgs.repeat;
+ var ch = motionArgs.forward ? cur.ch + repeat : cur.ch - repeat;
+ return Pos(cur.line, ch);
+ },
+ moveByLines: function(cm, head, motionArgs, vim) {
+ var cur = head;
+ var endCh = cur.ch;
+ // Depending what our last motion was, we may want to do different
+ // things. If our last motion was moving vertically, we want to
+ // preserve the HPos from our last horizontal move. If our last motion
+ // was going to the end of a line, moving vertically we should go to
+ // the end of the line, etc.
+ switch (vim.lastMotion) {
+ case this.moveByLines:
+ case this.moveByDisplayLines:
+ case this.moveByScroll:
+ case this.moveToColumn:
+ case this.moveToEol:
+ endCh = vim.lastHPos;
+ break;
+ default:
+ vim.lastHPos = endCh;
+ }
+ var repeat = motionArgs.repeat+(motionArgs.repeatOffset||0);
+ var line = motionArgs.forward ? cur.line + repeat : cur.line - repeat;
+ var first = cm.firstLine();
+ var last = cm.lastLine();
+ // Vim go to line begin or line end when cursor at first/last line and
+ // move to previous/next line is triggered.
+ if (line < first && cur.line == first){
+ return this.moveToStartOfLine(cm, head, motionArgs, vim);
+ }else if (line > last && cur.line == last){
+ return this.moveToEol(cm, head, motionArgs, vim);
+ }
+ if (motionArgs.toFirstChar){
+ endCh=findFirstNonWhiteSpaceCharacter(cm.getLine(line));
+ vim.lastHPos = endCh;
+ }
+ vim.lastHSPos = cm.charCoords(Pos(line, endCh),'div').left;
+ return Pos(line, endCh);
+ },
+ moveByDisplayLines: function(cm, head, motionArgs, vim) {
+ var cur = head;
+ switch (vim.lastMotion) {
+ case this.moveByDisplayLines:
+ case this.moveByScroll:
+ case this.moveByLines:
+ case this.moveToColumn:
+ case this.moveToEol:
+ break;
+ default:
+ vim.lastHSPos = cm.charCoords(cur,'div').left;
+ }
+ var repeat = motionArgs.repeat;
+ var res=cm.findPosV(cur,(motionArgs.forward ? repeat : -repeat),'line',vim.lastHSPos);
+ if (res.hitSide) {
+ if (motionArgs.forward) {
+ var lastCharCoords = cm.charCoords(res, 'div');
+ var goalCoords = { top: lastCharCoords.top + 8, left: vim.lastHSPos };
+ var res = cm.coordsChar(goalCoords, 'div');
+ } else {
+ var resCoords = cm.charCoords(Pos(cm.firstLine(), 0), 'div');
+ resCoords.left = vim.lastHSPos;
+ res = cm.coordsChar(resCoords, 'div');
+ }
+ }
+ vim.lastHPos = res.ch;
+ return res;
+ },
+ moveByPage: function(cm, head, motionArgs) {
+ // CodeMirror only exposes functions that move the cursor page down, so
+ // doing this bad hack to move the cursor and move it back. evalInput
+ // will move the cursor to where it should be in the end.
+ var curStart = head;
+ var repeat = motionArgs.repeat;
+ return cm.findPosV(curStart, (motionArgs.forward ? repeat : -repeat), 'page');
+ },
+ moveByParagraph: function(cm, head, motionArgs) {
+ var dir = motionArgs.forward ? 1 : -1;
+ return findParagraph(cm, head, motionArgs.repeat, dir);
+ },
+ moveByScroll: function(cm, head, motionArgs, vim) {
+ var scrollbox = cm.getScrollInfo();
+ var curEnd = null;
+ var repeat = motionArgs.repeat;
+ if (!repeat) {
+ repeat = scrollbox.clientHeight / (2 * cm.defaultTextHeight());
+ }
+ var orig = cm.charCoords(head, 'local');
+ motionArgs.repeat = repeat;
+ var curEnd = motions.moveByDisplayLines(cm, head, motionArgs, vim);
+ if (!curEnd) {
+ return null;
+ }
+ var dest = cm.charCoords(curEnd, 'local');
+ cm.scrollTo(null, scrollbox.top + dest.top - orig.top);
+ return curEnd;
+ },
+ moveByWords: function(cm, head, motionArgs) {
+ return moveToWord(cm, head, motionArgs.repeat, !!motionArgs.forward,
+ !!motionArgs.wordEnd, !!motionArgs.bigWord);
+ },
+ moveTillCharacter: function(cm, _head, motionArgs) {
+ var repeat = motionArgs.repeat;
+ var curEnd = moveToCharacter(cm, repeat, motionArgs.forward,
+ motionArgs.selectedCharacter);
+ var increment = motionArgs.forward ? -1 : 1;
+ recordLastCharacterSearch(increment, motionArgs);
+ if (!curEnd) return null;
+ curEnd.ch += increment;
+ return curEnd;
+ },
+ moveToCharacter: function(cm, head, motionArgs) {
+ var repeat = motionArgs.repeat;
+ recordLastCharacterSearch(0, motionArgs);
+ return moveToCharacter(cm, repeat, motionArgs.forward,
+ motionArgs.selectedCharacter) || head;
+ },
+ moveToSymbol: function(cm, head, motionArgs) {
+ var repeat = motionArgs.repeat;
+ return findSymbol(cm, repeat, motionArgs.forward,
+ motionArgs.selectedCharacter) || head;
+ },
+ moveToColumn: function(cm, head, motionArgs, vim) {
+ var repeat = motionArgs.repeat;
+ // repeat is equivalent to which column we want to move to!
+ vim.lastHPos = repeat - 1;
+ vim.lastHSPos = cm.charCoords(head,'div').left;
+ return moveToColumn(cm, repeat);
+ },
+ moveToEol: function(cm, head, motionArgs, vim) {
+ var cur = head;
+ vim.lastHPos = Infinity;
+ var retval= Pos(cur.line + motionArgs.repeat - 1, Infinity);
+ var end=cm.clipPos(retval);
+ end.ch--;
+ vim.lastHSPos = cm.charCoords(end,'div').left;
+ return retval;
+ },
+ moveToFirstNonWhiteSpaceCharacter: function(cm, head) {
+ // Go to the start of the line where the text begins, or the end for
+ // whitespace-only lines
+ var cursor = head;
+ return Pos(cursor.line,
+ findFirstNonWhiteSpaceCharacter(cm.getLine(cursor.line)));
+ },
+ moveToMatchedSymbol: function(cm, head) {
+ var cursor = head;
+ var line = cursor.line;
+ var ch = cursor.ch;
+ var lineText = cm.getLine(line);
+ var symbol;
+ do {
+ symbol = lineText.charAt(ch++);
+ if (symbol && isMatchableSymbol(symbol)) {
+ var style = cm.getTokenTypeAt(Pos(line, ch));
+ if (style !== "string" && style !== "comment") {
+ break;
+ }
+ }
+ } while (symbol);
+ if (symbol) {
+ var matched = cm.findMatchingBracket(Pos(line, ch));
+ return matched.to;
+ } else {
+ return cursor;
+ }
+ },
+ moveToStartOfLine: function(_cm, head) {
+ return Pos(head.line, 0);
+ },
+ moveToLineOrEdgeOfDocument: function(cm, _head, motionArgs) {
+ var lineNum = motionArgs.forward ? cm.lastLine() : cm.firstLine();
+ if (motionArgs.repeatIsExplicit) {
+ lineNum = motionArgs.repeat - cm.getOption('firstLineNumber');
+ }
+ return Pos(lineNum,
+ findFirstNonWhiteSpaceCharacter(cm.getLine(lineNum)));
+ },
+ textObjectManipulation: function(cm, head, motionArgs, vim) {
+ // TODO: lots of possible exceptions that can be thrown here. Try da(
+ // outside of a () block.
+
+ // TODO: adding <> >< to this map doesn't work, presumably because
+ // they're operators
+ var mirroredPairs = {'(': ')', ')': '(',
+ '{': '}', '}': '{',
+ '[': ']', ']': '['};
+ var selfPaired = {'\'': true, '"': true};
+
+ var character = motionArgs.selectedCharacter;
+ // 'b' refers to '()' block.
+ // 'B' refers to '{}' block.
+ if (character == 'b') {
+ character = '(';
+ } else if (character == 'B') {
+ character = '{';
+ }
+
+ // Inclusive is the difference between a and i
+ // TODO: Instead of using the additional text object map to perform text
+ // object operations, merge the map into the defaultKeyMap and use
+ // motionArgs to define behavior. Define separate entries for 'aw',
+ // 'iw', 'a[', 'i[', etc.
+ var inclusive = !motionArgs.textObjectInner;
+
+ var tmp;
+ if (mirroredPairs[character]) {
+ tmp = selectCompanionObject(cm, head, character, inclusive);
+ } else if (selfPaired[character]) {
+ tmp = findBeginningAndEnd(cm, head, character, inclusive);
+ } else if (character === 'W') {
+ tmp = expandWordUnderCursor(cm, inclusive, true /** forward */,
+ true /** bigWord */);
+ } else if (character === 'w') {
+ tmp = expandWordUnderCursor(cm, inclusive, true /** forward */,
+ false /** bigWord */);
+ } else if (character === 'p') {
+ tmp = findParagraph(cm, head, motionArgs.repeat, 0, inclusive);
+ motionArgs.linewise = true;
+ if (vim.visualMode) {
+ if (!vim.visualLine) { vim.visualLine = true; }
+ } else {
+ var operatorArgs = vim.inputState.operatorArgs;
+ if (operatorArgs) { operatorArgs.linewise = true; }
+ tmp.end.line--;
+ }
+ } else {
+ // No text object defined for this, don't move.
+ return null;
+ }
+
+ if (!cm.state.vim.visualMode) {
+ return [tmp.start, tmp.end];
+ } else {
+ return expandSelection(cm, tmp.start, tmp.end);
+ }
+ },
+
+ repeatLastCharacterSearch: function(cm, head, motionArgs) {
+ var lastSearch = vimGlobalState.lastCharacterSearch;
+ var repeat = motionArgs.repeat;
+ var forward = motionArgs.forward === lastSearch.forward;
+ var increment = (lastSearch.increment ? 1 : 0) * (forward ? -1 : 1);
+ cm.moveH(-increment, 'char');
+ motionArgs.inclusive = forward ? true : false;
+ var curEnd = moveToCharacter(cm, repeat, forward, lastSearch.selectedCharacter);
+ if (!curEnd) {
+ cm.moveH(increment, 'char');
+ return head;
+ }
+ curEnd.ch += increment;
+ return curEnd;
+ }
+ };
+
+ function defineMotion(name, fn) {
+ motions[name] = fn;
+ }
+
+ function fillArray(val, times) {
+ var arr = [];
+ for (var i = 0; i < times; i++) {
+ arr.push(val);
+ }
+ return arr;
+ }
+ /**
+ * An operator acts on a text selection. It receives the list of selections
+ * as input. The corresponding CodeMirror selection is guaranteed to
+ * match the input selection.
+ */
+ var operators = {
+ change: function(cm, args, ranges) {
+ var finalHead, text;
+ var vim = cm.state.vim;
+ vimGlobalState.macroModeState.lastInsertModeChanges.inVisualBlock = vim.visualBlock;
+ if (!vim.visualMode) {
+ var anchor = ranges[0].anchor,
+ head = ranges[0].head;
+ text = cm.getRange(anchor, head);
+ var lastState = vim.lastEditInputState || {};
+ if (lastState.motion == "moveByWords" && !isWhiteSpaceString(text)) {
+ // Exclude trailing whitespace if the range is not all whitespace.
+ var match = (/\s+$/).exec(text);
+ if (match && lastState.motionArgs && lastState.motionArgs.forward) {
+ head = offsetCursor(head, 0, - match[0].length);
+ text = text.slice(0, - match[0].length);
+ }
+ }
+ var prevLineEnd = new Pos(anchor.line - 1, Number.MAX_VALUE);
+ var wasLastLine = cm.firstLine() == cm.lastLine();
+ if (head.line > cm.lastLine() && args.linewise && !wasLastLine) {
+ cm.replaceRange('', prevLineEnd, head);
+ } else {
+ cm.replaceRange('', anchor, head);
+ }
+ if (args.linewise) {
+ // Push the next line back down, if there is a next line.
+ if (!wasLastLine) {
+ cm.setCursor(prevLineEnd);
+ CodeMirror.commands.newlineAndIndent(cm);
+ }
+ // make sure cursor ends up at the end of the line.
+ anchor.ch = Number.MAX_VALUE;
+ }
+ finalHead = anchor;
+ } else {
+ text = cm.getSelection();
+ var replacement = fillArray('', ranges.length);
+ cm.replaceSelections(replacement);
+ finalHead = cursorMin(ranges[0].head, ranges[0].anchor);
+ }
+ vimGlobalState.registerController.pushText(
+ args.registerName, 'change', text,
+ args.linewise, ranges.length > 1);
+ actions.enterInsertMode(cm, {head: finalHead}, cm.state.vim);
+ },
+ // delete is a javascript keyword.
+ 'delete': function(cm, args, ranges) {
+ var finalHead, text;
+ var vim = cm.state.vim;
+ if (!vim.visualBlock) {
+ var anchor = ranges[0].anchor,
+ head = ranges[0].head;
+ if (args.linewise &&
+ head.line != cm.firstLine() &&
+ anchor.line == cm.lastLine() &&
+ anchor.line == head.line - 1) {
+ // Special case for dd on last line (and first line).
+ if (anchor.line == cm.firstLine()) {
+ anchor.ch = 0;
+ } else {
+ anchor = Pos(anchor.line - 1, lineLength(cm, anchor.line - 1));
+ }
+ }
+ text = cm.getRange(anchor, head);
+ cm.replaceRange('', anchor, head);
+ finalHead = anchor;
+ if (args.linewise) {
+ finalHead = motions.moveToFirstNonWhiteSpaceCharacter(cm, anchor);
+ }
+ } else {
+ text = cm.getSelection();
+ var replacement = fillArray('', ranges.length);
+ cm.replaceSelections(replacement);
+ finalHead = ranges[0].anchor;
+ }
+ vimGlobalState.registerController.pushText(
+ args.registerName, 'delete', text,
+ args.linewise, vim.visualBlock);
+ return clipCursorToContent(cm, finalHead);
+ },
+ indent: function(cm, args, ranges) {
+ var vim = cm.state.vim;
+ var startLine = ranges[0].anchor.line;
+ var endLine = vim.visualBlock ?
+ ranges[ranges.length - 1].anchor.line :
+ ranges[0].head.line;
+ // In visual mode, n> shifts the selection right n times, instead of
+ // shifting n lines right once.
+ var repeat = (vim.visualMode) ? args.repeat : 1;
+ if (args.linewise) {
+ // The only way to delete a newline is to delete until the start of
+ // the next line, so in linewise mode evalInput will include the next
+ // line. We don't want this in indent, so we go back a line.
+ endLine--;
+ }
+ for (var i = startLine; i <= endLine; i++) {
+ for (var j = 0; j < repeat; j++) {
+ cm.indentLine(i, args.indentRight);
+ }
+ }
+ return motions.moveToFirstNonWhiteSpaceCharacter(cm, ranges[0].anchor);
+ },
+ changeCase: function(cm, args, ranges, oldAnchor, newHead) {
+ var selections = cm.getSelections();
+ var swapped = [];
+ var toLower = args.toLower;
+ for (var j = 0; j < selections.length; j++) {
+ var toSwap = selections[j];
+ var text = '';
+ if (toLower === true) {
+ text = toSwap.toLowerCase();
+ } else if (toLower === false) {
+ text = toSwap.toUpperCase();
+ } else {
+ for (var i = 0; i < toSwap.length; i++) {
+ var character = toSwap.charAt(i);
+ text += isUpperCase(character) ? character.toLowerCase() :
+ character.toUpperCase();
+ }
+ }
+ swapped.push(text);
+ }
+ cm.replaceSelections(swapped);
+ if (args.shouldMoveCursor){
+ return newHead;
+ } else if (!cm.state.vim.visualMode && args.linewise && ranges[0].anchor.line + 1 == ranges[0].head.line) {
+ return motions.moveToFirstNonWhiteSpaceCharacter(cm, oldAnchor);
+ } else if (args.linewise){
+ return oldAnchor;
+ } else {
+ return cursorMin(ranges[0].anchor, ranges[0].head);
+ }
+ },
+ yank: function(cm, args, ranges, oldAnchor) {
+ var vim = cm.state.vim;
+ var text = cm.getSelection();
+ var endPos = vim.visualMode
+ ? cursorMin(vim.sel.anchor, vim.sel.head, ranges[0].head, ranges[0].anchor)
+ : oldAnchor;
+ vimGlobalState.registerController.pushText(
+ args.registerName, 'yank',
+ text, args.linewise, vim.visualBlock);
+ return endPos;
+ }
+ };
+
+ function defineOperator(name, fn) {
+ operators[name] = fn;
+ }
+
+ var actions = {
+ jumpListWalk: function(cm, actionArgs, vim) {
+ if (vim.visualMode) {
+ return;
+ }
+ var repeat = actionArgs.repeat;
+ var forward = actionArgs.forward;
+ var jumpList = vimGlobalState.jumpList;
+
+ var mark = jumpList.move(cm, forward ? repeat : -repeat);
+ var markPos = mark ? mark.find() : undefined;
+ markPos = markPos ? markPos : cm.getCursor();
+ cm.setCursor(markPos);
+ },
+ scroll: function(cm, actionArgs, vim) {
+ if (vim.visualMode) {
+ return;
+ }
+ var repeat = actionArgs.repeat || 1;
+ var lineHeight = cm.defaultTextHeight();
+ var top = cm.getScrollInfo().top;
+ var delta = lineHeight * repeat;
+ var newPos = actionArgs.forward ? top + delta : top - delta;
+ var cursor = copyCursor(cm.getCursor());
+ var cursorCoords = cm.charCoords(cursor, 'local');
+ if (actionArgs.forward) {
+ if (newPos > cursorCoords.top) {
+ cursor.line += (newPos - cursorCoords.top) / lineHeight;
+ cursor.line = Math.ceil(cursor.line);
+ cm.setCursor(cursor);
+ cursorCoords = cm.charCoords(cursor, 'local');
+ cm.scrollTo(null, cursorCoords.top);
+ } else {
+ // Cursor stays within bounds. Just reposition the scroll window.
+ cm.scrollTo(null, newPos);
+ }
+ } else {
+ var newBottom = newPos + cm.getScrollInfo().clientHeight;
+ if (newBottom < cursorCoords.bottom) {
+ cursor.line -= (cursorCoords.bottom - newBottom) / lineHeight;
+ cursor.line = Math.floor(cursor.line);
+ cm.setCursor(cursor);
+ cursorCoords = cm.charCoords(cursor, 'local');
+ cm.scrollTo(
+ null, cursorCoords.bottom - cm.getScrollInfo().clientHeight);
+ } else {
+ // Cursor stays within bounds. Just reposition the scroll window.
+ cm.scrollTo(null, newPos);
+ }
+ }
+ },
+ scrollToCursor: function(cm, actionArgs) {
+ var lineNum = cm.getCursor().line;
+ var charCoords = cm.charCoords(Pos(lineNum, 0), 'local');
+ var height = cm.getScrollInfo().clientHeight;
+ var y = charCoords.top;
+ var lineHeight = charCoords.bottom - y;
+ switch (actionArgs.position) {
+ case 'center': y = y - (height / 2) + lineHeight;
+ break;
+ case 'bottom': y = y - height + lineHeight;
+ break;
+ }
+ cm.scrollTo(null, y);
+ },
+ replayMacro: function(cm, actionArgs, vim) {
+ var registerName = actionArgs.selectedCharacter;
+ var repeat = actionArgs.repeat;
+ var macroModeState = vimGlobalState.macroModeState;
+ if (registerName == '@') {
+ registerName = macroModeState.latestRegister;
+ }
+ while(repeat--){
+ executeMacroRegister(cm, vim, macroModeState, registerName);
+ }
+ },
+ enterMacroRecordMode: function(cm, actionArgs) {
+ var macroModeState = vimGlobalState.macroModeState;
+ var registerName = actionArgs.selectedCharacter;
+ macroModeState.enterMacroRecordMode(cm, registerName);
+ },
+ enterInsertMode: function(cm, actionArgs, vim) {
+ if (cm.getOption('readOnly')) { return; }
+ vim.insertMode = true;
+ vim.insertModeRepeat = actionArgs && actionArgs.repeat || 1;
+ var insertAt = (actionArgs) ? actionArgs.insertAt : null;
+ var sel = vim.sel;
+ var head = actionArgs.head || cm.getCursor('head');
+ var height = cm.listSelections().length;
+ if (insertAt == 'eol') {
+ head = Pos(head.line, lineLength(cm, head.line));
+ } else if (insertAt == 'charAfter') {
+ head = offsetCursor(head, 0, 1);
+ } else if (insertAt == 'firstNonBlank') {
+ head = motions.moveToFirstNonWhiteSpaceCharacter(cm, head);
+ } else if (insertAt == 'startOfSelectedArea') {
+ if (!vim.visualBlock) {
+ if (sel.head.line < sel.anchor.line) {
+ head = sel.head;
+ } else {
+ head = Pos(sel.anchor.line, 0);
+ }
+ } else {
+ head = Pos(
+ Math.min(sel.head.line, sel.anchor.line),
+ Math.min(sel.head.ch, sel.anchor.ch));
+ height = Math.abs(sel.head.line - sel.anchor.line) + 1;
+ }
+ } else if (insertAt == 'endOfSelectedArea') {
+ if (!vim.visualBlock) {
+ if (sel.head.line >= sel.anchor.line) {
+ head = offsetCursor(sel.head, 0, 1);
+ } else {
+ head = Pos(sel.anchor.line, 0);
+ }
+ } else {
+ head = Pos(
+ Math.min(sel.head.line, sel.anchor.line),
+ Math.max(sel.head.ch + 1, sel.anchor.ch));
+ height = Math.abs(sel.head.line - sel.anchor.line) + 1;
+ }
+ } else if (insertAt == 'inplace') {
+ if (vim.visualMode){
+ return;
+ }
+ }
+ cm.setOption('keyMap', 'vim-insert');
+ cm.setOption('disableInput', false);
+ if (actionArgs && actionArgs.replace) {
+ // Handle Replace-mode as a special case of insert mode.
+ cm.toggleOverwrite(true);
+ cm.setOption('keyMap', 'vim-replace');
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "replace"});
+ } else {
+ cm.setOption('keyMap', 'vim-insert');
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "insert"});
+ }
+ if (!vimGlobalState.macroModeState.isPlaying) {
+ // Only record if not replaying.
+ cm.on('change', onChange);
+ CodeMirror.on(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown);
+ }
+ if (vim.visualMode) {
+ exitVisualMode(cm);
+ }
+ selectForInsert(cm, head, height);
+ },
+ toggleVisualMode: function(cm, actionArgs, vim) {
+ var repeat = actionArgs.repeat;
+ var anchor = cm.getCursor();
+ var head;
+ // TODO: The repeat should actually select number of characters/lines
+ // equal to the repeat times the size of the previous visual
+ // operation.
+ if (!vim.visualMode) {
+ // Entering visual mode
+ vim.visualMode = true;
+ vim.visualLine = !!actionArgs.linewise;
+ vim.visualBlock = !!actionArgs.blockwise;
+ head = clipCursorToContent(
+ cm, Pos(anchor.line, anchor.ch + repeat - 1),
+ true /** includeLineBreak */);
+ vim.sel = {
+ anchor: anchor,
+ head: head
+ };
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : vim.visualBlock ? "blockwise" : ""});
+ updateCmSelection(cm);
+ updateMark(cm, vim, '<', cursorMin(anchor, head));
+ updateMark(cm, vim, '>', cursorMax(anchor, head));
+ } else if (vim.visualLine ^ actionArgs.linewise ||
+ vim.visualBlock ^ actionArgs.blockwise) {
+ // Toggling between modes
+ vim.visualLine = !!actionArgs.linewise;
+ vim.visualBlock = !!actionArgs.blockwise;
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : vim.visualBlock ? "blockwise" : ""});
+ updateCmSelection(cm);
+ } else {
+ exitVisualMode(cm);
+ }
+ },
+ reselectLastSelection: function(cm, _actionArgs, vim) {
+ var lastSelection = vim.lastSelection;
+ if (vim.visualMode) {
+ updateLastSelection(cm, vim);
+ }
+ if (lastSelection) {
+ var anchor = lastSelection.anchorMark.find();
+ var head = lastSelection.headMark.find();
+ if (!anchor || !head) {
+ // If the marks have been destroyed due to edits, do nothing.
+ return;
+ }
+ vim.sel = {
+ anchor: anchor,
+ head: head
+ };
+ vim.visualMode = true;
+ vim.visualLine = lastSelection.visualLine;
+ vim.visualBlock = lastSelection.visualBlock;
+ updateCmSelection(cm);
+ updateMark(cm, vim, '<', cursorMin(anchor, head));
+ updateMark(cm, vim, '>', cursorMax(anchor, head));
+ CodeMirror.signal(cm, 'vim-mode-change', {
+ mode: 'visual',
+ subMode: vim.visualLine ? 'linewise' :
+ vim.visualBlock ? 'blockwise' : ''});
+ }
+ },
+ joinLines: function(cm, actionArgs, vim) {
+ var curStart, curEnd;
+ if (vim.visualMode) {
+ curStart = cm.getCursor('anchor');
+ curEnd = cm.getCursor('head');
+ if (cursorIsBefore(curEnd, curStart)) {
+ var tmp = curEnd;
+ curEnd = curStart;
+ curStart = tmp;
+ }
+ curEnd.ch = lineLength(cm, curEnd.line) - 1;
+ } else {
+ // Repeat is the number of lines to join. Minimum 2 lines.
+ var repeat = Math.max(actionArgs.repeat, 2);
+ curStart = cm.getCursor();
+ curEnd = clipCursorToContent(cm, Pos(curStart.line + repeat - 1,
+ Infinity));
+ }
+ var finalCh = 0;
+ for (var i = curStart.line; i < curEnd.line; i++) {
+ finalCh = lineLength(cm, curStart.line);
+ var tmp = Pos(curStart.line + 1,
+ lineLength(cm, curStart.line + 1));
+ var text = cm.getRange(curStart, tmp);
+ text = text.replace(/\n\s*/g, ' ');
+ cm.replaceRange(text, curStart, tmp);
+ }
+ var curFinalPos = Pos(curStart.line, finalCh);
+ if (vim.visualMode) {
+ exitVisualMode(cm, false);
+ }
+ cm.setCursor(curFinalPos);
+ },
+ newLineAndEnterInsertMode: function(cm, actionArgs, vim) {
+ vim.insertMode = true;
+ var insertAt = copyCursor(cm.getCursor());
+ if (insertAt.line === cm.firstLine() && !actionArgs.after) {
+ // Special case for inserting newline before start of document.
+ cm.replaceRange('\n', Pos(cm.firstLine(), 0));
+ cm.setCursor(cm.firstLine(), 0);
+ } else {
+ insertAt.line = (actionArgs.after) ? insertAt.line :
+ insertAt.line - 1;
+ insertAt.ch = lineLength(cm, insertAt.line);
+ cm.setCursor(insertAt);
+ var newlineFn = CodeMirror.commands.newlineAndIndentContinueComment ||
+ CodeMirror.commands.newlineAndIndent;
+ newlineFn(cm);
+ }
+ this.enterInsertMode(cm, { repeat: actionArgs.repeat }, vim);
+ },
+ paste: function(cm, actionArgs, vim) {
+ var cur = copyCursor(cm.getCursor());
+ var register = vimGlobalState.registerController.getRegister(
+ actionArgs.registerName);
+ var text = register.toString();
+ if (!text) {
+ return;
+ }
+ if (actionArgs.matchIndent) {
+ var tabSize = cm.getOption("tabSize");
+ // length that considers tabs and tabSize
+ var whitespaceLength = function(str) {
+ var tabs = (str.split("\t").length - 1);
+ var spaces = (str.split(" ").length - 1);
+ return tabs * tabSize + spaces * 1;
+ };
+ var currentLine = cm.getLine(cm.getCursor().line);
+ var indent = whitespaceLength(currentLine.match(/^\s*/)[0]);
+ // chomp last newline b/c don't want it to match /^\s*/gm
+ var chompedText = text.replace(/\n$/, '');
+ var wasChomped = text !== chompedText;
+ var firstIndent = whitespaceLength(text.match(/^\s*/)[0]);
+ var text = chompedText.replace(/^\s*/gm, function(wspace) {
+ var newIndent = indent + (whitespaceLength(wspace) - firstIndent);
+ if (newIndent < 0) {
+ return "";
+ }
+ else if (cm.getOption("indentWithTabs")) {
+ var quotient = Math.floor(newIndent / tabSize);
+ return Array(quotient + 1).join('\t');
+ }
+ else {
+ return Array(newIndent + 1).join(' ');
+ }
+ });
+ text += wasChomped ? "\n" : "";
+ }
+ if (actionArgs.repeat > 1) {
+ var text = Array(actionArgs.repeat + 1).join(text);
+ }
+ var linewise = register.linewise;
+ var blockwise = register.blockwise;
+ if (linewise) {
+ if(vim.visualMode) {
+ text = vim.visualLine ? text.slice(0, -1) : '\n' + text.slice(0, text.length - 1) + '\n';
+ } else if (actionArgs.after) {
+ // Move the newline at the end to the start instead, and paste just
+ // before the newline character of the line we are on right now.
+ text = '\n' + text.slice(0, text.length - 1);
+ cur.ch = lineLength(cm, cur.line);
+ } else {
+ cur.ch = 0;
+ }
+ } else {
+ if (blockwise) {
+ text = text.split('\n');
+ for (var i = 0; i < text.length; i++) {
+ text[i] = (text[i] == '') ? ' ' : text[i];
+ }
+ }
+ cur.ch += actionArgs.after ? 1 : 0;
+ }
+ var curPosFinal;
+ var idx;
+ if (vim.visualMode) {
+ // save the pasted text for reselection if the need arises
+ vim.lastPastedText = text;
+ var lastSelectionCurEnd;
+ var selectedArea = getSelectedAreaRange(cm, vim);
+ var selectionStart = selectedArea[0];
+ var selectionEnd = selectedArea[1];
+ var selectedText = cm.getSelection();
+ var selections = cm.listSelections();
+ var emptyStrings = new Array(selections.length).join('1').split('1');
+ // save the curEnd marker before it get cleared due to cm.replaceRange.
+ if (vim.lastSelection) {
+ lastSelectionCurEnd = vim.lastSelection.headMark.find();
+ }
+ // push the previously selected text to unnamed register
+ vimGlobalState.registerController.unnamedRegister.setText(selectedText);
+ if (blockwise) {
+ // first delete the selected text
+ cm.replaceSelections(emptyStrings);
+ // Set new selections as per the block length of the yanked text
+ selectionEnd = Pos(selectionStart.line + text.length-1, selectionStart.ch);
+ cm.setCursor(selectionStart);
+ selectBlock(cm, selectionEnd);
+ cm.replaceSelections(text);
+ curPosFinal = selectionStart;
+ } else if (vim.visualBlock) {
+ cm.replaceSelections(emptyStrings);
+ cm.setCursor(selectionStart);
+ cm.replaceRange(text, selectionStart, selectionStart);
+ curPosFinal = selectionStart;
+ } else {
+ cm.replaceRange(text, selectionStart, selectionEnd);
+ curPosFinal = cm.posFromIndex(cm.indexFromPos(selectionStart) + text.length - 1);
+ }
+ // restore the the curEnd marker
+ if(lastSelectionCurEnd) {
+ vim.lastSelection.headMark = cm.setBookmark(lastSelectionCurEnd);
+ }
+ if (linewise) {
+ curPosFinal.ch=0;
+ }
+ } else {
+ if (blockwise) {
+ cm.setCursor(cur);
+ for (var i = 0; i < text.length; i++) {
+ var line = cur.line+i;
+ if (line > cm.lastLine()) {
+ cm.replaceRange('\n', Pos(line, 0));
+ }
+ var lastCh = lineLength(cm, line);
+ if (lastCh < cur.ch) {
+ extendLineToColumn(cm, line, cur.ch);
+ }
+ }
+ cm.setCursor(cur);
+ selectBlock(cm, Pos(cur.line + text.length-1, cur.ch));
+ cm.replaceSelections(text);
+ curPosFinal = cur;
+ } else {
+ cm.replaceRange(text, cur);
+ // Now fine tune the cursor to where we want it.
+ if (linewise && actionArgs.after) {
+ curPosFinal = Pos(
+ cur.line + 1,
+ findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line + 1)));
+ } else if (linewise && !actionArgs.after) {
+ curPosFinal = Pos(
+ cur.line,
+ findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line)));
+ } else if (!linewise && actionArgs.after) {
+ idx = cm.indexFromPos(cur);
+ curPosFinal = cm.posFromIndex(idx + text.length - 1);
+ } else {
+ idx = cm.indexFromPos(cur);
+ curPosFinal = cm.posFromIndex(idx + text.length);
+ }
+ }
+ }
+ if (vim.visualMode) {
+ exitVisualMode(cm, false);
+ }
+ cm.setCursor(curPosFinal);
+ },
+ undo: function(cm, actionArgs) {
+ cm.operation(function() {
+ repeatFn(cm, CodeMirror.commands.undo, actionArgs.repeat)();
+ cm.setCursor(cm.getCursor('anchor'));
+ });
+ },
+ redo: function(cm, actionArgs) {
+ repeatFn(cm, CodeMirror.commands.redo, actionArgs.repeat)();
+ },
+ setRegister: function(_cm, actionArgs, vim) {
+ vim.inputState.registerName = actionArgs.selectedCharacter;
+ },
+ setMark: function(cm, actionArgs, vim) {
+ var markName = actionArgs.selectedCharacter;
+ updateMark(cm, vim, markName, cm.getCursor());
+ },
+ replace: function(cm, actionArgs, vim) {
+ var replaceWith = actionArgs.selectedCharacter;
+ var curStart = cm.getCursor();
+ var replaceTo;
+ var curEnd;
+ var selections = cm.listSelections();
+ if (vim.visualMode) {
+ curStart = cm.getCursor('start');
+ curEnd = cm.getCursor('end');
+ } else {
+ var line = cm.getLine(curStart.line);
+ replaceTo = curStart.ch + actionArgs.repeat;
+ if (replaceTo > line.length) {
+ replaceTo=line.length;
+ }
+ curEnd = Pos(curStart.line, replaceTo);
+ }
+ if (replaceWith=='\n') {
+ if (!vim.visualMode) cm.replaceRange('', curStart, curEnd);
+ // special case, where vim help says to replace by just one line-break
+ (CodeMirror.commands.newlineAndIndentContinueComment || CodeMirror.commands.newlineAndIndent)(cm);
+ } else {
+ var replaceWithStr = cm.getRange(curStart, curEnd);
+ //replace all characters in range by selected, but keep linebreaks
+ replaceWithStr = replaceWithStr.replace(/[^\n]/g, replaceWith);
+ if (vim.visualBlock) {
+ // Tabs are split in visua block before replacing
+ var spaces = new Array(cm.getOption("tabSize")+1).join(' ');
+ replaceWithStr = cm.getSelection();
+ replaceWithStr = replaceWithStr.replace(/\t/g, spaces).replace(/[^\n]/g, replaceWith).split('\n');
+ cm.replaceSelections(replaceWithStr);
+ } else {
+ cm.replaceRange(replaceWithStr, curStart, curEnd);
+ }
+ if (vim.visualMode) {
+ curStart = cursorIsBefore(selections[0].anchor, selections[0].head) ?
+ selections[0].anchor : selections[0].head;
+ cm.setCursor(curStart);
+ exitVisualMode(cm, false);
+ } else {
+ cm.setCursor(offsetCursor(curEnd, 0, -1));
+ }
+ }
+ },
+ incrementNumberToken: function(cm, actionArgs) {
+ var cur = cm.getCursor();
+ var lineStr = cm.getLine(cur.line);
+ var re = /-?\d+/g;
+ var match;
+ var start;
+ var end;
+ var numberStr;
+ var token;
+ while ((match = re.exec(lineStr)) !== null) {
+ token = match[0];
+ start = match.index;
+ end = start + token.length;
+ if (cur.ch < end)break;
+ }
+ if (!actionArgs.backtrack && (end <= cur.ch))return;
+ if (token) {
+ var increment = actionArgs.increase ? 1 : -1;
+ var number = parseInt(token) + (increment * actionArgs.repeat);
+ var from = Pos(cur.line, start);
+ var to = Pos(cur.line, end);
+ numberStr = number.toString();
+ cm.replaceRange(numberStr, from, to);
+ } else {
+ return;
+ }
+ cm.setCursor(Pos(cur.line, start + numberStr.length - 1));
+ },
+ repeatLastEdit: function(cm, actionArgs, vim) {
+ var lastEditInputState = vim.lastEditInputState;
+ if (!lastEditInputState) { return; }
+ var repeat = actionArgs.repeat;
+ if (repeat && actionArgs.repeatIsExplicit) {
+ vim.lastEditInputState.repeatOverride = repeat;
+ } else {
+ repeat = vim.lastEditInputState.repeatOverride || repeat;
+ }
+ repeatLastEdit(cm, vim, repeat, false /** repeatForInsert */);
+ },
+ exitInsertMode: exitInsertMode
+ };
+
+ function defineAction(name, fn) {
+ actions[name] = fn;
+ }
+
+ /*
+ * Below are miscellaneous utility functions used by vim.js
+ */
+
+ /**
+ * Clips cursor to ensure that line is within the buffer's range
+ * If includeLineBreak is true, then allow cur.ch == lineLength.
+ */
+ function clipCursorToContent(cm, cur, includeLineBreak) {
+ var line = Math.min(Math.max(cm.firstLine(), cur.line), cm.lastLine() );
+ var maxCh = lineLength(cm, line) - 1;
+ maxCh = (includeLineBreak) ? maxCh + 1 : maxCh;
+ var ch = Math.min(Math.max(0, cur.ch), maxCh);
+ return Pos(line, ch);
+ }
+ function copyArgs(args) {
+ var ret = {};
+ for (var prop in args) {
+ if (args.hasOwnProperty(prop)) {
+ ret[prop] = args[prop];
+ }
+ }
+ return ret;
+ }
+ function offsetCursor(cur, offsetLine, offsetCh) {
+ if (typeof offsetLine === 'object') {
+ offsetCh = offsetLine.ch;
+ offsetLine = offsetLine.line;
+ }
+ return Pos(cur.line + offsetLine, cur.ch + offsetCh);
+ }
+ function getOffset(anchor, head) {
+ return {
+ line: head.line - anchor.line,
+ ch: head.line - anchor.line
+ };
+ }
+ function commandMatches(keys, keyMap, context, inputState) {
+ // Partial matches are not applied. They inform the key handler
+ // that the current key sequence is a subsequence of a valid key
+ // sequence, so that the key buffer is not cleared.
+ var match, partial = [], full = [];
+ for (var i = 0; i < keyMap.length; i++) {
+ var command = keyMap[i];
+ if (context == 'insert' && command.context != 'insert' ||
+ command.context && command.context != context ||
+ inputState.operator && command.type == 'action' ||
+ !(match = commandMatch(keys, command.keys))) { continue; }
+ if (match == 'partial') { partial.push(command); }
+ if (match == 'full') { full.push(command); }
+ }
+ return {
+ partial: partial.length && partial,
+ full: full.length && full
+ };
+ }
+ function commandMatch(pressed, mapped) {
+ if (mapped.slice(-11) == '<character>') {
+ // Last character matches anything.
+ var prefixLen = mapped.length - 11;
+ var pressedPrefix = pressed.slice(0, prefixLen);
+ var mappedPrefix = mapped.slice(0, prefixLen);
+ return pressedPrefix == mappedPrefix && pressed.length > prefixLen ? 'full' :
+ mappedPrefix.indexOf(pressedPrefix) == 0 ? 'partial' : false;
+ } else {
+ return pressed == mapped ? 'full' :
+ mapped.indexOf(pressed) == 0 ? 'partial' : false;
+ }
+ }
+ function lastChar(keys) {
+ var match = /^.*(<[\w\-]+>)$/.exec(keys);
+ var selectedCharacter = match ? match[1] : keys.slice(-1);
+ if (selectedCharacter.length > 1){
+ switch(selectedCharacter){
+ case '<CR>':
+ selectedCharacter='\n';
+ break;
+ case '<Space>':
+ selectedCharacter=' ';
+ break;
+ default:
+ break;
+ }
+ }
+ return selectedCharacter;
+ }
+ function repeatFn(cm, fn, repeat) {
+ return function() {
+ for (var i = 0; i < repeat; i++) {
+ fn(cm);
+ }
+ };
+ }
+ function copyCursor(cur) {
+ return Pos(cur.line, cur.ch);
+ }
+ function cursorEqual(cur1, cur2) {
+ return cur1.ch == cur2.ch && cur1.line == cur2.line;
+ }
+ function cursorIsBefore(cur1, cur2) {
+ if (cur1.line < cur2.line) {
+ return true;
+ }
+ if (cur1.line == cur2.line && cur1.ch < cur2.ch) {
+ return true;
+ }
+ return false;
+ }
+ function cursorMin(cur1, cur2) {
+ if (arguments.length > 2) {
+ cur2 = cursorMin.apply(undefined, Array.prototype.slice.call(arguments, 1));
+ }
+ return cursorIsBefore(cur1, cur2) ? cur1 : cur2;
+ }
+ function cursorMax(cur1, cur2) {
+ if (arguments.length > 2) {
+ cur2 = cursorMax.apply(undefined, Array.prototype.slice.call(arguments, 1));
+ }
+ return cursorIsBefore(cur1, cur2) ? cur2 : cur1;
+ }
+ function cursorIsBetween(cur1, cur2, cur3) {
+ // returns true if cur2 is between cur1 and cur3.
+ var cur1before2 = cursorIsBefore(cur1, cur2);
+ var cur2before3 = cursorIsBefore(cur2, cur3);
+ return cur1before2 && cur2before3;
+ }
+ function lineLength(cm, lineNum) {
+ return cm.getLine(lineNum).length;
+ }
+ function trim(s) {
+ if (s.trim) {
+ return s.trim();
+ }
+ return s.replace(/^\s+|\s+$/g, '');
+ }
+ function escapeRegex(s) {
+ return s.replace(/([.?*+$\[\]\/\\(){}|\-])/g, '\\$1');
+ }
+ function extendLineToColumn(cm, lineNum, column) {
+ var endCh = lineLength(cm, lineNum);
+ var spaces = new Array(column-endCh+1).join(' ');
+ cm.setCursor(Pos(lineNum, endCh));
+ cm.replaceRange(spaces, cm.getCursor());
+ }
+ // This functions selects a rectangular block
+ // of text with selectionEnd as any of its corner
+ // Height of block:
+ // Difference in selectionEnd.line and first/last selection.line
+ // Width of the block:
+ // Distance between selectionEnd.ch and any(first considered here) selection.ch
+ function selectBlock(cm, selectionEnd) {
+ var selections = [], ranges = cm.listSelections();
+ var head = copyCursor(cm.clipPos(selectionEnd));
+ var isClipped = !cursorEqual(selectionEnd, head);
+ var curHead = cm.getCursor('head');
+ var primIndex = getIndex(ranges, curHead);
+ var wasClipped = cursorEqual(ranges[primIndex].head, ranges[primIndex].anchor);
+ var max = ranges.length - 1;
+ var index = max - primIndex > primIndex ? max : 0;
+ var base = ranges[index].anchor;
+
+ var firstLine = Math.min(base.line, head.line);
+ var lastLine = Math.max(base.line, head.line);
+ var baseCh = base.ch, headCh = head.ch;
+
+ var dir = ranges[index].head.ch - baseCh;
+ var newDir = headCh - baseCh;
+ if (dir > 0 && newDir <= 0) {
+ baseCh++;
+ if (!isClipped) { headCh--; }
+ } else if (dir < 0 && newDir >= 0) {
+ baseCh--;
+ if (!wasClipped) { headCh++; }
+ } else if (dir < 0 && newDir == -1) {
+ baseCh--;
+ headCh++;
+ }
+ for (var line = firstLine; line <= lastLine; line++) {
+ var range = {anchor: new Pos(line, baseCh), head: new Pos(line, headCh)};
+ selections.push(range);
+ }
+ primIndex = head.line == lastLine ? selections.length - 1 : 0;
+ cm.setSelections(selections);
+ selectionEnd.ch = headCh;
+ base.ch = baseCh;
+ return base;
+ }
+ function selectForInsert(cm, head, height) {
+ var sel = [];
+ for (var i = 0; i < height; i++) {
+ var lineHead = offsetCursor(head, i, 0);
+ sel.push({anchor: lineHead, head: lineHead});
+ }
+ cm.setSelections(sel, 0);
+ }
+ // getIndex returns the index of the cursor in the selections.
+ function getIndex(ranges, cursor, end) {
+ for (var i = 0; i < ranges.length; i++) {
+ var atAnchor = end != 'head' && cursorEqual(ranges[i].anchor, cursor);
+ var atHead = end != 'anchor' && cursorEqual(ranges[i].head, cursor);
+ if (atAnchor || atHead) {
+ return i;
+ }
+ }
+ return -1;
+ }
+ function getSelectedAreaRange(cm, vim) {
+ var lastSelection = vim.lastSelection;
+ var getCurrentSelectedAreaRange = function() {
+ var selections = cm.listSelections();
+ var start = selections[0];
+ var end = selections[selections.length-1];
+ var selectionStart = cursorIsBefore(start.anchor, start.head) ? start.anchor : start.head;
+ var selectionEnd = cursorIsBefore(end.anchor, end.head) ? end.head : end.anchor;
+ return [selectionStart, selectionEnd];
+ };
+ var getLastSelectedAreaRange = function() {
+ var selectionStart = cm.getCursor();
+ var selectionEnd = cm.getCursor();
+ var block = lastSelection.visualBlock;
+ if (block) {
+ var width = block.width;
+ var height = block.height;
+ selectionEnd = Pos(selectionStart.line + height, selectionStart.ch + width);
+ var selections = [];
+ // selectBlock creates a 'proper' rectangular block.
+ // We do not want that in all cases, so we manually set selections.
+ for (var i = selectionStart.line; i < selectionEnd.line; i++) {
+ var anchor = Pos(i, selectionStart.ch);
+ var head = Pos(i, selectionEnd.ch);
+ var range = {anchor: anchor, head: head};
+ selections.push(range);
+ }
+ cm.setSelections(selections);
+ } else {
+ var start = lastSelection.anchorMark.find();
+ var end = lastSelection.headMark.find();
+ var line = end.line - start.line;
+ var ch = end.ch - start.ch;
+ selectionEnd = {line: selectionEnd.line + line, ch: line ? selectionEnd.ch : ch + selectionEnd.ch};
+ if (lastSelection.visualLine) {
+ selectionStart = Pos(selectionStart.line, 0);
+ selectionEnd = Pos(selectionEnd.line, lineLength(cm, selectionEnd.line));
+ }
+ cm.setSelection(selectionStart, selectionEnd);
+ }
+ return [selectionStart, selectionEnd];
+ };
+ if (!vim.visualMode) {
+ // In case of replaying the action.
+ return getLastSelectedAreaRange();
+ } else {
+ return getCurrentSelectedAreaRange();
+ }
+ }
+ // Updates the previous selection with the current selection's values. This
+ // should only be called in visual mode.
+ function updateLastSelection(cm, vim) {
+ var anchor = vim.sel.anchor;
+ var head = vim.sel.head;
+ // To accommodate the effect of lastPastedText in the last selection
+ if (vim.lastPastedText) {
+ head = cm.posFromIndex(cm.indexFromPos(anchor) + vim.lastPastedText.length);
+ vim.lastPastedText = null;
+ }
+ vim.lastSelection = {'anchorMark': cm.setBookmark(anchor),
+ 'headMark': cm.setBookmark(head),
+ 'anchor': copyCursor(anchor),
+ 'head': copyCursor(head),
+ 'visualMode': vim.visualMode,
+ 'visualLine': vim.visualLine,
+ 'visualBlock': vim.visualBlock};
+ }
+ function expandSelection(cm, start, end) {
+ var sel = cm.state.vim.sel;
+ var head = sel.head;
+ var anchor = sel.anchor;
+ var tmp;
+ if (cursorIsBefore(end, start)) {
+ tmp = end;
+ end = start;
+ start = tmp;
+ }
+ if (cursorIsBefore(head, anchor)) {
+ head = cursorMin(start, head);
+ anchor = cursorMax(anchor, end);
+ } else {
+ anchor = cursorMin(start, anchor);
+ head = cursorMax(head, end);
+ head = offsetCursor(head, 0, -1);
+ if (head.ch == -1 && head.line != cm.firstLine()) {
+ head = Pos(head.line - 1, lineLength(cm, head.line - 1));
+ }
+ }
+ return [anchor, head];
+ }
+ /**
+ * Updates the CodeMirror selection to match the provided vim selection.
+ * If no arguments are given, it uses the current vim selection state.
+ */
+ function updateCmSelection(cm, sel, mode) {
+ var vim = cm.state.vim;
+ sel = sel || vim.sel;
+ var mode = mode ||
+ vim.visualLine ? 'line' : vim.visualBlock ? 'block' : 'char';
+ var cmSel = makeCmSelection(cm, sel, mode);
+ cm.setSelections(cmSel.ranges, cmSel.primary);
+ updateFakeCursor(cm);
+ }
+ function makeCmSelection(cm, sel, mode, exclusive) {
+ var head = copyCursor(sel.head);
+ var anchor = copyCursor(sel.anchor);
+ if (mode == 'char') {
+ var headOffset = !exclusive && !cursorIsBefore(sel.head, sel.anchor) ? 1 : 0;
+ var anchorOffset = cursorIsBefore(sel.head, sel.anchor) ? 1 : 0;
+ head = offsetCursor(sel.head, 0, headOffset);
+ anchor = offsetCursor(sel.anchor, 0, anchorOffset);
+ return {
+ ranges: [{anchor: anchor, head: head}],
+ primary: 0
+ };
+ } else if (mode == 'line') {
+ if (!cursorIsBefore(sel.head, sel.anchor)) {
+ anchor.ch = 0;
+
+ var lastLine = cm.lastLine();
+ if (head.line > lastLine) {
+ head.line = lastLine;
+ }
+ head.ch = lineLength(cm, head.line);
+ } else {
+ head.ch = 0;
+ anchor.ch = lineLength(cm, anchor.line);
+ }
+ return {
+ ranges: [{anchor: anchor, head: head}],
+ primary: 0
+ };
+ } else if (mode == 'block') {
+ var top = Math.min(anchor.line, head.line),
+ left = Math.min(anchor.ch, head.ch),
+ bottom = Math.max(anchor.line, head.line),
+ right = Math.max(anchor.ch, head.ch) + 1;
+ var height = bottom - top + 1;
+ var primary = head.line == top ? 0 : height - 1;
+ var ranges = [];
+ for (var i = 0; i < height; i++) {
+ ranges.push({
+ anchor: Pos(top + i, left),
+ head: Pos(top + i, right)
+ });
+ }
+ return {
+ ranges: ranges,
+ primary: primary
+ };
+ }
+ }
+ function getHead(cm) {
+ var cur = cm.getCursor('head');
+ if (cm.getSelection().length == 1) {
+ // Small corner case when only 1 character is selected. The "real"
+ // head is the left of head and anchor.
+ cur = cursorMin(cur, cm.getCursor('anchor'));
+ }
+ return cur;
+ }
+
+ /**
+ * If moveHead is set to false, the CodeMirror selection will not be
+ * touched. The caller assumes the responsibility of putting the cursor
+ * in the right place.
+ */
+ function exitVisualMode(cm, moveHead) {
+ var vim = cm.state.vim;
+ if (moveHead !== false) {
+ cm.setCursor(clipCursorToContent(cm, vim.sel.head));
+ }
+ updateLastSelection(cm, vim);
+ vim.visualMode = false;
+ vim.visualLine = false;
+ vim.visualBlock = false;
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"});
+ if (vim.fakeCursor) {
+ vim.fakeCursor.clear();
+ }
+ }
+
+ // Remove any trailing newlines from the selection. For
+ // example, with the caret at the start of the last word on the line,
+ // 'dw' should word, but not the newline, while 'w' should advance the
+ // caret to the first character of the next line.
+ function clipToLine(cm, curStart, curEnd) {
+ var selection = cm.getRange(curStart, curEnd);
+ // Only clip if the selection ends with trailing newline + whitespace
+ if (/\n\s*$/.test(selection)) {
+ var lines = selection.split('\n');
+ // We know this is all whitespace.
+ lines.pop();
+
+ // Cases:
+ // 1. Last word is an empty line - do not clip the trailing '\n'
+ // 2. Last word is not an empty line - clip the trailing '\n'
+ var line;
+ // Find the line containing the last word, and clip all whitespace up
+ // to it.
+ for (var line = lines.pop(); lines.length > 0 && line && isWhiteSpaceString(line); line = lines.pop()) {
+ curEnd.line--;
+ curEnd.ch = 0;
+ }
+ // If the last word is not an empty line, clip an additional newline
+ if (line) {
+ curEnd.line--;
+ curEnd.ch = lineLength(cm, curEnd.line);
+ } else {
+ curEnd.ch = 0;
+ }
+ }
+ }
+
+ // Expand the selection to line ends.
+ function expandSelectionToLine(_cm, curStart, curEnd) {
+ curStart.ch = 0;
+ curEnd.ch = 0;
+ curEnd.line++;
+ }
+
+ function findFirstNonWhiteSpaceCharacter(text) {
+ if (!text) {
+ return 0;
+ }
+ var firstNonWS = text.search(/\S/);
+ return firstNonWS == -1 ? text.length : firstNonWS;
+ }
+
+ function expandWordUnderCursor(cm, inclusive, _forward, bigWord, noSymbol) {
+ var cur = getHead(cm);
+ var line = cm.getLine(cur.line);
+ var idx = cur.ch;
+
+ // Seek to first word or non-whitespace character, depending on if
+ // noSymbol is true.
+ var test = noSymbol ? wordCharTest[0] : bigWordCharTest [0];
+ while (!test(line.charAt(idx))) {
+ idx++;
+ if (idx >= line.length) { return null; }
+ }
+
+ if (bigWord) {
+ test = bigWordCharTest[0];
+ } else {
+ test = wordCharTest[0];
+ if (!test(line.charAt(idx))) {
+ test = wordCharTest[1];
+ }
+ }
+
+ var end = idx, start = idx;
+ while (test(line.charAt(end)) && end < line.length) { end++; }
+ while (test(line.charAt(start)) && start >= 0) { start--; }
+ start++;
+
+ if (inclusive) {
+ // If present, include all whitespace after word.
+ // Otherwise, include all whitespace before word, except indentation.
+ var wordEnd = end;
+ while (/\s/.test(line.charAt(end)) && end < line.length) { end++; }
+ if (wordEnd == end) {
+ var wordStart = start;
+ while (/\s/.test(line.charAt(start - 1)) && start > 0) { start--; }
+ if (!start) { start = wordStart; }
+ }
+ }
+ return { start: Pos(cur.line, start), end: Pos(cur.line, end) };
+ }
+
+ function recordJumpPosition(cm, oldCur, newCur) {
+ if (!cursorEqual(oldCur, newCur)) {
+ vimGlobalState.jumpList.add(cm, oldCur, newCur);
+ }
+ }
+
+ function recordLastCharacterSearch(increment, args) {
+ vimGlobalState.lastCharacterSearch.increment = increment;
+ vimGlobalState.lastCharacterSearch.forward = args.forward;
+ vimGlobalState.lastCharacterSearch.selectedCharacter = args.selectedCharacter;
+ }
+
+ var symbolToMode = {
+ '(': 'bracket', ')': 'bracket', '{': 'bracket', '}': 'bracket',
+ '[': 'section', ']': 'section',
+ '*': 'comment', '/': 'comment',
+ 'm': 'method', 'M': 'method',
+ '#': 'preprocess'
+ };
+ var findSymbolModes = {
+ bracket: {
+ isComplete: function(state) {
+ if (state.nextCh === state.symb) {
+ state.depth++;
+ if (state.depth >= 1)return true;
+ } else if (state.nextCh === state.reverseSymb) {
+ state.depth--;
+ }
+ return false;
+ }
+ },
+ section: {
+ init: function(state) {
+ state.curMoveThrough = true;
+ state.symb = (state.forward ? ']' : '[') === state.symb ? '{' : '}';
+ },
+ isComplete: function(state) {
+ return state.index === 0 && state.nextCh === state.symb;
+ }
+ },
+ comment: {
+ isComplete: function(state) {
+ var found = state.lastCh === '*' && state.nextCh === '/';
+ state.lastCh = state.nextCh;
+ return found;
+ }
+ },
+ // TODO: The original Vim implementation only operates on level 1 and 2.
+ // The current implementation doesn't check for code block level and
+ // therefore it operates on any levels.
+ method: {
+ init: function(state) {
+ state.symb = (state.symb === 'm' ? '{' : '}');
+ state.reverseSymb = state.symb === '{' ? '}' : '{';
+ },
+ isComplete: function(state) {
+ if (state.nextCh === state.symb)return true;
+ return false;
+ }
+ },
+ preprocess: {
+ init: function(state) {
+ state.index = 0;
+ },
+ isComplete: function(state) {
+ if (state.nextCh === '#') {
+ var token = state.lineText.match(/#(\w+)/)[1];
+ if (token === 'endif') {
+ if (state.forward && state.depth === 0) {
+ return true;
+ }
+ state.depth++;
+ } else if (token === 'if') {
+ if (!state.forward && state.depth === 0) {
+ return true;
+ }
+ state.depth--;
+ }
+ if (token === 'else' && state.depth === 0)return true;
+ }
+ return false;
+ }
+ }
+ };
+ function findSymbol(cm, repeat, forward, symb) {
+ var cur = copyCursor(cm.getCursor());
+ var increment = forward ? 1 : -1;
+ var endLine = forward ? cm.lineCount() : -1;
+ var curCh = cur.ch;
+ var line = cur.line;
+ var lineText = cm.getLine(line);
+ var state = {
+ lineText: lineText,
+ nextCh: lineText.charAt(curCh),
+ lastCh: null,
+ index: curCh,
+ symb: symb,
+ reverseSymb: (forward ? { ')': '(', '}': '{' } : { '(': ')', '{': '}' })[symb],
+ forward: forward,
+ depth: 0,
+ curMoveThrough: false
+ };
+ var mode = symbolToMode[symb];
+ if (!mode)return cur;
+ var init = findSymbolModes[mode].init;
+ var isComplete = findSymbolModes[mode].isComplete;
+ if (init) { init(state); }
+ while (line !== endLine && repeat) {
+ state.index += increment;
+ state.nextCh = state.lineText.charAt(state.index);
+ if (!state.nextCh) {
+ line += increment;
+ state.lineText = cm.getLine(line) || '';
+ if (increment > 0) {
+ state.index = 0;
+ } else {
+ var lineLen = state.lineText.length;
+ state.index = (lineLen > 0) ? (lineLen-1) : 0;
+ }
+ state.nextCh = state.lineText.charAt(state.index);
+ }
+ if (isComplete(state)) {
+ cur.line = line;
+ cur.ch = state.index;
+ repeat--;
+ }
+ }
+ if (state.nextCh || state.curMoveThrough) {
+ return Pos(line, state.index);
+ }
+ return cur;
+ }
+
+ /**
+ * Returns the boundaries of the next word. If the cursor in the middle of
+ * the word, then returns the boundaries of the current word, starting at
+ * the cursor. If the cursor is at the start/end of a word, and we are going
+ * forward/backward, respectively, find the boundaries of the next word.
+ *
+ * @param {CodeMirror} cm CodeMirror object.
+ * @param {Cursor} cur The cursor position.
+ * @param {boolean} forward True to search forward. False to search
+ * backward.
+ * @param {boolean} bigWord True if punctuation count as part of the word.
+ * False if only [a-zA-Z0-9] characters count as part of the word.
+ * @param {boolean} emptyLineIsWord True if empty lines should be treated
+ * as words.
+ * @return {Object{from:number, to:number, line: number}} The boundaries of
+ * the word, or null if there are no more words.
+ */
+ function findWord(cm, cur, forward, bigWord, emptyLineIsWord) {
+ var lineNum = cur.line;
+ var pos = cur.ch;
+ var line = cm.getLine(lineNum);
+ var dir = forward ? 1 : -1;
+ var charTests = bigWord ? bigWordCharTest: wordCharTest;
+
+ if (emptyLineIsWord && line == '') {
+ lineNum += dir;
+ line = cm.getLine(lineNum);
+ if (!isLine(cm, lineNum)) {
+ return null;
+ }
+ pos = (forward) ? 0 : line.length;
+ }
+
+ while (true) {
+ if (emptyLineIsWord && line == '') {
+ return { from: 0, to: 0, line: lineNum };
+ }
+ var stop = (dir > 0) ? line.length : -1;
+ var wordStart = stop, wordEnd = stop;
+ // Find bounds of next word.
+ while (pos != stop) {
+ var foundWord = false;
+ for (var i = 0; i < charTests.length && !foundWord; ++i) {
+ if (charTests[i](line.charAt(pos))) {
+ wordStart = pos;
+ // Advance to end of word.
+ while (pos != stop && charTests[i](line.charAt(pos))) {
+ pos += dir;
+ }
+ wordEnd = pos;
+ foundWord = wordStart != wordEnd;
+ if (wordStart == cur.ch && lineNum == cur.line &&
+ wordEnd == wordStart + dir) {
+ // We started at the end of a word. Find the next one.
+ continue;
+ } else {
+ return {
+ from: Math.min(wordStart, wordEnd + 1),
+ to: Math.max(wordStart, wordEnd),
+ line: lineNum };
+ }
+ }
+ }
+ if (!foundWord) {
+ pos += dir;
+ }
+ }
+ // Advance to next/prev line.
+ lineNum += dir;
+ if (!isLine(cm, lineNum)) {
+ return null;
+ }
+ line = cm.getLine(lineNum);
+ pos = (dir > 0) ? 0 : line.length;
+ }
+ }
+
+ /**
+ * @param {CodeMirror} cm CodeMirror object.
+ * @param {Pos} cur The position to start from.
+ * @param {int} repeat Number of words to move past.
+ * @param {boolean} forward True to search forward. False to search
+ * backward.
+ * @param {boolean} wordEnd True to move to end of word. False to move to
+ * beginning of word.
+ * @param {boolean} bigWord True if punctuation count as part of the word.
+ * False if only alphabet characters count as part of the word.
+ * @return {Cursor} The position the cursor should move to.
+ */
+ function moveToWord(cm, cur, repeat, forward, wordEnd, bigWord) {
+ var curStart = copyCursor(cur);
+ var words = [];
+ if (forward && !wordEnd || !forward && wordEnd) {
+ repeat++;
+ }
+ // For 'e', empty lines are not considered words, go figure.
+ var emptyLineIsWord = !(forward && wordEnd);
+ for (var i = 0; i < repeat; i++) {
+ var word = findWord(cm, cur, forward, bigWord, emptyLineIsWord);
+ if (!word) {
+ var eodCh = lineLength(cm, cm.lastLine());
+ words.push(forward
+ ? {line: cm.lastLine(), from: eodCh, to: eodCh}
+ : {line: 0, from: 0, to: 0});
+ break;
+ }
+ words.push(word);
+ cur = Pos(word.line, forward ? (word.to - 1) : word.from);
+ }
+ var shortCircuit = words.length != repeat;
+ var firstWord = words[0];
+ var lastWord = words.pop();
+ if (forward && !wordEnd) {
+ // w
+ if (!shortCircuit && (firstWord.from != curStart.ch || firstWord.line != curStart.line)) {
+ // We did not start in the middle of a word. Discard the extra word at the end.
+ lastWord = words.pop();
+ }
+ return Pos(lastWord.line, lastWord.from);
+ } else if (forward && wordEnd) {
+ return Pos(lastWord.line, lastWord.to - 1);
+ } else if (!forward && wordEnd) {
+ // ge
+ if (!shortCircuit && (firstWord.to != curStart.ch || firstWord.line != curStart.line)) {
+ // We did not start in the middle of a word. Discard the extra word at the end.
+ lastWord = words.pop();
+ }
+ return Pos(lastWord.line, lastWord.to);
+ } else {
+ // b
+ return Pos(lastWord.line, lastWord.from);
+ }
+ }
+
+ function moveToCharacter(cm, repeat, forward, character) {
+ var cur = cm.getCursor();
+ var start = cur.ch;
+ var idx;
+ for (var i = 0; i < repeat; i ++) {
+ var line = cm.getLine(cur.line);
+ idx = charIdxInLine(start, line, character, forward, true);
+ if (idx == -1) {
+ return null;
+ }
+ start = idx;
+ }
+ return Pos(cm.getCursor().line, idx);
+ }
+
+ function moveToColumn(cm, repeat) {
+ // repeat is always >= 1, so repeat - 1 always corresponds
+ // to the column we want to go to.
+ var line = cm.getCursor().line;
+ return clipCursorToContent(cm, Pos(line, repeat - 1));
+ }
+
+ function updateMark(cm, vim, markName, pos) {
+ if (!inArray(markName, validMarks)) {
+ return;
+ }
+ if (vim.marks[markName]) {
+ vim.marks[markName].clear();
+ }
+ vim.marks[markName] = cm.setBookmark(pos);
+ }
+
+ function charIdxInLine(start, line, character, forward, includeChar) {
+ // Search for char in line.
+ // motion_options: {forward, includeChar}
+ // If includeChar = true, include it too.
+ // If forward = true, search forward, else search backwards.
+ // If char is not found on this line, do nothing
+ var idx;
+ if (forward) {
+ idx = line.indexOf(character, start + 1);
+ if (idx != -1 && !includeChar) {
+ idx -= 1;
+ }
+ } else {
+ idx = line.lastIndexOf(character, start - 1);
+ if (idx != -1 && !includeChar) {
+ idx += 1;
+ }
+ }
+ return idx;
+ }
+
+ function findParagraph(cm, head, repeat, dir, inclusive) {
+ var line = head.line;
+ var min = cm.firstLine();
+ var max = cm.lastLine();
+ var start, end, i = line;
+ function isEmpty(i) { return !cm.getLine(i); }
+ function isBoundary(i, dir, any) {
+ if (any) { return isEmpty(i) != isEmpty(i + dir); }
+ return !isEmpty(i) && isEmpty(i + dir);
+ }
+ if (dir) {
+ while (min <= i && i <= max && repeat > 0) {
+ if (isBoundary(i, dir)) { repeat--; }
+ i += dir;
+ }
+ return new Pos(i, 0);
+ }
+
+ var vim = cm.state.vim;
+ if (vim.visualLine && isBoundary(line, 1, true)) {
+ var anchor = vim.sel.anchor;
+ if (isBoundary(anchor.line, -1, true)) {
+ if (!inclusive || anchor.line != line) {
+ line += 1;
+ }
+ }
+ }
+ var startState = isEmpty(line);
+ for (i = line; i <= max && repeat; i++) {
+ if (isBoundary(i, 1, true)) {
+ if (!inclusive || isEmpty(i) != startState) {
+ repeat--;
+ }
+ }
+ }
+ end = new Pos(i, 0);
+ // select boundary before paragraph for the last one
+ if (i > max && !startState) { startState = true; }
+ else { inclusive = false; }
+ for (i = line; i > min; i--) {
+ if (!inclusive || isEmpty(i) == startState || i == line) {
+ if (isBoundary(i, -1, true)) { break; }
+ }
+ }
+ start = new Pos(i, 0);
+ return { start: start, end: end };
+ }
+
+ // TODO: perhaps this finagling of start and end positions belonds
+ // in codemirror/replaceRange?
+ function selectCompanionObject(cm, head, symb, inclusive) {
+ var cur = head, start, end;
+
+ var bracketRegexp = ({
+ '(': /[()]/, ')': /[()]/,
+ '[': /[[\]]/, ']': /[[\]]/,
+ '{': /[{}]/, '}': /[{}]/})[symb];
+ var openSym = ({
+ '(': '(', ')': '(',
+ '[': '[', ']': '[',
+ '{': '{', '}': '{'})[symb];
+ var curChar = cm.getLine(cur.line).charAt(cur.ch);
+ // Due to the behavior of scanForBracket, we need to add an offset if the
+ // cursor is on a matching open bracket.
+ var offset = curChar === openSym ? 1 : 0;
+
+ start = cm.scanForBracket(Pos(cur.line, cur.ch + offset), -1, null, {'bracketRegex': bracketRegexp});
+ end = cm.scanForBracket(Pos(cur.line, cur.ch + offset), 1, null, {'bracketRegex': bracketRegexp});
+
+ if (!start || !end) {
+ return { start: cur, end: cur };
+ }
+
+ start = start.pos;
+ end = end.pos;
+
+ if ((start.line == end.line && start.ch > end.ch)
+ || (start.line > end.line)) {
+ var tmp = start;
+ start = end;
+ end = tmp;
+ }
+
+ if (inclusive) {
+ end.ch += 1;
+ } else {
+ start.ch += 1;
+ }
+
+ return { start: start, end: end };
+ }
+
+ // Takes in a symbol and a cursor and tries to simulate text objects that
+ // have identical opening and closing symbols
+ // TODO support across multiple lines
+ function findBeginningAndEnd(cm, head, symb, inclusive) {
+ var cur = copyCursor(head);
+ var line = cm.getLine(cur.line);
+ var chars = line.split('');
+ var start, end, i, len;
+ var firstIndex = chars.indexOf(symb);
+
+ // the decision tree is to always look backwards for the beginning first,
+ // but if the cursor is in front of the first instance of the symb,
+ // then move the cursor forward
+ if (cur.ch < firstIndex) {
+ cur.ch = firstIndex;
+ // Why is this line even here???
+ // cm.setCursor(cur.line, firstIndex+1);
+ }
+ // otherwise if the cursor is currently on the closing symbol
+ else if (firstIndex < cur.ch && chars[cur.ch] == symb) {
+ end = cur.ch; // assign end to the current cursor
+ --cur.ch; // make sure to look backwards
+ }
+
+ // if we're currently on the symbol, we've got a start
+ if (chars[cur.ch] == symb && !end) {
+ start = cur.ch + 1; // assign start to ahead of the cursor
+ } else {
+ // go backwards to find the start
+ for (i = cur.ch; i > -1 && !start; i--) {
+ if (chars[i] == symb) {
+ start = i + 1;
+ }
+ }
+ }
+
+ // look forwards for the end symbol
+ if (start && !end) {
+ for (i = start, len = chars.length; i < len && !end; i++) {
+ if (chars[i] == symb) {
+ end = i;
+ }
+ }
+ }
+
+ // nothing found
+ if (!start || !end) {
+ return { start: cur, end: cur };
+ }
+
+ // include the symbols
+ if (inclusive) {
+ --start; ++end;
+ }
+
+ return {
+ start: Pos(cur.line, start),
+ end: Pos(cur.line, end)
+ };
+ }
+
+ // Search functions
+ defineOption('pcre', true, 'boolean');
+ function SearchState() {}
+ SearchState.prototype = {
+ getQuery: function() {
+ return vimGlobalState.query;
+ },
+ setQuery: function(query) {
+ vimGlobalState.query = query;
+ },
+ getOverlay: function() {
+ return this.searchOverlay;
+ },
+ setOverlay: function(overlay) {
+ this.searchOverlay = overlay;
+ },
+ isReversed: function() {
+ return vimGlobalState.isReversed;
+ },
+ setReversed: function(reversed) {
+ vimGlobalState.isReversed = reversed;
+ },
+ getScrollbarAnnotate: function() {
+ return this.annotate;
+ },
+ setScrollbarAnnotate: function(annotate) {
+ this.annotate = annotate;
+ }
+ };
+ function getSearchState(cm) {
+ var vim = cm.state.vim;
+ return vim.searchState_ || (vim.searchState_ = new SearchState());
+ }
+ function dialog(cm, template, shortText, onClose, options) {
+ if (cm.openDialog) {
+ cm.openDialog(template, onClose, { bottom: true, value: options.value,
+ onKeyDown: options.onKeyDown, onKeyUp: options.onKeyUp,
+ selectValueOnOpen: false});
+ }
+ else {
+ onClose(prompt(shortText, ''));
+ }
+ }
+ function splitBySlash(argString) {
+ var slashes = findUnescapedSlashes(argString) || [];
+ if (!slashes.length) return [];
+ var tokens = [];
+ // in case of strings like foo/bar
+ if (slashes[0] !== 0) return;
+ for (var i = 0; i < slashes.length; i++) {
+ if (typeof slashes[i] == 'number')
+ tokens.push(argString.substring(slashes[i] + 1, slashes[i+1]));
+ }
+ return tokens;
+ }
+
+ function findUnescapedSlashes(str) {
+ var escapeNextChar = false;
+ var slashes = [];
+ for (var i = 0; i < str.length; i++) {
+ var c = str.charAt(i);
+ if (!escapeNextChar && c == '/') {
+ slashes.push(i);
+ }
+ escapeNextChar = !escapeNextChar && (c == '\\');
+ }
+ return slashes;
+ }
+
+ // Translates a search string from ex (vim) syntax into javascript form.
+ function translateRegex(str) {
+ // When these match, add a '\' if unescaped or remove one if escaped.
+ var specials = '|(){';
+ // Remove, but never add, a '\' for these.
+ var unescape = '}';
+ var escapeNextChar = false;
+ var out = [];
+ for (var i = -1; i < str.length; i++) {
+ var c = str.charAt(i) || '';
+ var n = str.charAt(i+1) || '';
+ var specialComesNext = (n && specials.indexOf(n) != -1);
+ if (escapeNextChar) {
+ if (c !== '\\' || !specialComesNext) {
+ out.push(c);
+ }
+ escapeNextChar = false;
+ } else {
+ if (c === '\\') {
+ escapeNextChar = true;
+ // Treat the unescape list as special for removing, but not adding '\'.
+ if (n && unescape.indexOf(n) != -1) {
+ specialComesNext = true;
+ }
+ // Not passing this test means removing a '\'.
+ if (!specialComesNext || n === '\\') {
+ out.push(c);
+ }
+ } else {
+ out.push(c);
+ if (specialComesNext && n !== '\\') {
+ out.push('\\');
+ }
+ }
+ }
+ }
+ return out.join('');
+ }
+
+ // Translates the replace part of a search and replace from ex (vim) syntax into
+ // javascript form. Similar to translateRegex, but additionally fixes back references
+ // (translates '\[0..9]' to '$[0..9]') and follows different rules for escaping '$'.
+ var charUnescapes = {'\\n': '\n', '\\r': '\r', '\\t': '\t'};
+ function translateRegexReplace(str) {
+ var escapeNextChar = false;
+ var out = [];
+ for (var i = -1; i < str.length; i++) {
+ var c = str.charAt(i) || '';
+ var n = str.charAt(i+1) || '';
+ if (charUnescapes[c + n]) {
+ out.push(charUnescapes[c+n]);
+ i++;
+ } else if (escapeNextChar) {
+ // At any point in the loop, escapeNextChar is true if the previous
+ // character was a '\' and was not escaped.
+ out.push(c);
+ escapeNextChar = false;
+ } else {
+ if (c === '\\') {
+ escapeNextChar = true;
+ if ((isNumber(n) || n === '$')) {
+ out.push('$');
+ } else if (n !== '/' && n !== '\\') {
+ out.push('\\');
+ }
+ } else {
+ if (c === '$') {
+ out.push('$');
+ }
+ out.push(c);
+ if (n === '/') {
+ out.push('\\');
+ }
+ }
+ }
+ }
+ return out.join('');
+ }
+
+ // Unescape \ and / in the replace part, for PCRE mode.
+ var unescapes = {'\\/': '/', '\\\\': '\\', '\\n': '\n', '\\r': '\r', '\\t': '\t'};
+ function unescapeRegexReplace(str) {
+ var stream = new CodeMirror.StringStream(str);
+ var output = [];
+ while (!stream.eol()) {
+ // Search for \.
+ while (stream.peek() && stream.peek() != '\\') {
+ output.push(stream.next());
+ }
+ var matched = false;
+ for (var matcher in unescapes) {
+ if (stream.match(matcher, true)) {
+ matched = true;
+ output.push(unescapes[matcher]);
+ break;
+ }
+ }
+ if (!matched) {
+ // Don't change anything
+ output.push(stream.next());
+ }
+ }
+ return output.join('');
+ }
+
+ /**
+ * Extract the regular expression from the query and return a Regexp object.
+ * Returns null if the query is blank.
+ * If ignoreCase is passed in, the Regexp object will have the 'i' flag set.
+ * If smartCase is passed in, and the query contains upper case letters,
+ * then ignoreCase is overridden, and the 'i' flag will not be set.
+ * If the query contains the /i in the flag part of the regular expression,
+ * then both ignoreCase and smartCase are ignored, and 'i' will be passed
+ * through to the Regex object.
+ */
+ function parseQuery(query, ignoreCase, smartCase) {
+ // First update the last search register
+ var lastSearchRegister = vimGlobalState.registerController.getRegister('/');
+ lastSearchRegister.setText(query);
+ // Check if the query is already a regex.
+ if (query instanceof RegExp) { return query; }
+ // First try to extract regex + flags from the input. If no flags found,
+ // extract just the regex. IE does not accept flags directly defined in
+ // the regex string in the form /regex/flags
+ var slashes = findUnescapedSlashes(query);
+ var regexPart;
+ var forceIgnoreCase;
+ if (!slashes.length) {
+ // Query looks like 'regexp'
+ regexPart = query;
+ } else {
+ // Query looks like 'regexp/...'
+ regexPart = query.substring(0, slashes[0]);
+ var flagsPart = query.substring(slashes[0]);
+ forceIgnoreCase = (flagsPart.indexOf('i') != -1);
+ }
+ if (!regexPart) {
+ return null;
+ }
+ if (!getOption('pcre')) {
+ regexPart = translateRegex(regexPart);
+ }
+ if (smartCase) {
+ ignoreCase = (/^[^A-Z]*$/).test(regexPart);
+ }
+ var regexp = new RegExp(regexPart,
+ (ignoreCase || forceIgnoreCase) ? 'i' : undefined);
+ return regexp;
+ }
+ function showConfirm(cm, text) {
+ if (cm.openNotification) {
+ cm.openNotification('<span style="color: red">' + text + '</span>',
+ {bottom: true, duration: 5000});
+ } else {
+ alert(text);
+ }
+ }
+ function makePrompt(prefix, desc) {
+ var raw = '<span style="font-family: monospace; white-space: pre">' +
+ (prefix || "") + '<input type="text"></span>';
+ if (desc)
+ raw += ' <span style="color: #888">' + desc + '</span>';
+ return raw;
+ }
+ var searchPromptDesc = '(Javascript regexp)';
+ function showPrompt(cm, options) {
+ var shortText = (options.prefix || '') + ' ' + (options.desc || '');
+ var prompt = makePrompt(options.prefix, options.desc);
+ dialog(cm, prompt, shortText, options.onClose, options);
+ }
+ function regexEqual(r1, r2) {
+ if (r1 instanceof RegExp && r2 instanceof RegExp) {
+ var props = ['global', 'multiline', 'ignoreCase', 'source'];
+ for (var i = 0; i < props.length; i++) {
+ var prop = props[i];
+ if (r1[prop] !== r2[prop]) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+ // Returns true if the query is valid.
+ function updateSearchQuery(cm, rawQuery, ignoreCase, smartCase) {
+ if (!rawQuery) {
+ return;
+ }
+ var state = getSearchState(cm);
+ var query = parseQuery(rawQuery, !!ignoreCase, !!smartCase);
+ if (!query) {
+ return;
+ }
+ highlightSearchMatches(cm, query);
+ if (regexEqual(query, state.getQuery())) {
+ return query;
+ }
+ state.setQuery(query);
+ return query;
+ }
+ function searchOverlay(query) {
+ if (query.source.charAt(0) == '^') {
+ var matchSol = true;
+ }
+ return {
+ token: function(stream) {
+ if (matchSol && !stream.sol()) {
+ stream.skipToEnd();
+ return;
+ }
+ var match = stream.match(query, false);
+ if (match) {
+ if (match[0].length == 0) {
+ // Matched empty string, skip to next.
+ stream.next();
+ return 'searching';
+ }
+ if (!stream.sol()) {
+ // Backtrack 1 to match \b
+ stream.backUp(1);
+ if (!query.exec(stream.next() + match[0])) {
+ stream.next();
+ return null;
+ }
+ }
+ stream.match(query);
+ return 'searching';
+ }
+ while (!stream.eol()) {
+ stream.next();
+ if (stream.match(query, false)) break;
+ }
+ },
+ query: query
+ };
+ }
+ function highlightSearchMatches(cm, query) {
+ var searchState = getSearchState(cm);
+ var overlay = searchState.getOverlay();
+ if (!overlay || query != overlay.query) {
+ if (overlay) {
+ cm.removeOverlay(overlay);
+ }
+ overlay = searchOverlay(query);
+ cm.addOverlay(overlay);
+ if (cm.showMatchesOnScrollbar) {
+ if (searchState.getScrollbarAnnotate()) {
+ searchState.getScrollbarAnnotate().clear();
+ }
+ searchState.setScrollbarAnnotate(cm.showMatchesOnScrollbar(query));
+ }
+ searchState.setOverlay(overlay);
+ }
+ }
+ function findNext(cm, prev, query, repeat) {
+ if (repeat === undefined) { repeat = 1; }
+ return cm.operation(function() {
+ var pos = cm.getCursor();
+ var cursor = cm.getSearchCursor(query, pos);
+ for (var i = 0; i < repeat; i++) {
+ var found = cursor.find(prev);
+ if (i == 0 && found && cursorEqual(cursor.from(), pos)) { found = cursor.find(prev); }
+ if (!found) {
+ // SearchCursor may have returned null because it hit EOF, wrap
+ // around and try again.
+ cursor = cm.getSearchCursor(query,
+ (prev) ? Pos(cm.lastLine()) : Pos(cm.firstLine(), 0) );
+ if (!cursor.find(prev)) {
+ return;
+ }
+ }
+ }
+ return cursor.from();
+ });
+ }
+ function clearSearchHighlight(cm) {
+ var state = getSearchState(cm);
+ cm.removeOverlay(getSearchState(cm).getOverlay());
+ state.setOverlay(null);
+ if (state.getScrollbarAnnotate()) {
+ state.getScrollbarAnnotate().clear();
+ state.setScrollbarAnnotate(null);
+ }
+ }
+ /**
+ * Check if pos is in the specified range, INCLUSIVE.
+ * Range can be specified with 1 or 2 arguments.
+ * If the first range argument is an array, treat it as an array of line
+ * numbers. Match pos against any of the lines.
+ * If the first range argument is a number,
+ * if there is only 1 range argument, check if pos has the same line
+ * number
+ * if there are 2 range arguments, then check if pos is in between the two
+ * range arguments.
+ */
+ function isInRange(pos, start, end) {
+ if (typeof pos != 'number') {
+ // Assume it is a cursor position. Get the line number.
+ pos = pos.line;
+ }
+ if (start instanceof Array) {
+ return inArray(pos, start);
+ } else {
+ if (end) {
+ return (pos >= start && pos <= end);
+ } else {
+ return pos == start;
+ }
+ }
+ }
+ function getUserVisibleLines(cm) {
+ var scrollInfo = cm.getScrollInfo();
+ var occludeToleranceTop = 6;
+ var occludeToleranceBottom = 10;
+ var from = cm.coordsChar({left:0, top: occludeToleranceTop + scrollInfo.top}, 'local');
+ var bottomY = scrollInfo.clientHeight - occludeToleranceBottom + scrollInfo.top;
+ var to = cm.coordsChar({left:0, top: bottomY}, 'local');
+ return {top: from.line, bottom: to.line};
+ }
+
+ var ExCommandDispatcher = function() {
+ this.buildCommandMap_();
+ };
+ ExCommandDispatcher.prototype = {
+ processCommand: function(cm, input, opt_params) {
+ var that = this;
+ cm.operation(function () {
+ cm.curOp.isVimOp = true;
+ that._processCommand(cm, input, opt_params);
+ });
+ },
+ _processCommand: function(cm, input, opt_params) {
+ var vim = cm.state.vim;
+ var commandHistoryRegister = vimGlobalState.registerController.getRegister(':');
+ var previousCommand = commandHistoryRegister.toString();
+ if (vim.visualMode) {
+ exitVisualMode(cm);
+ }
+ var inputStream = new CodeMirror.StringStream(input);
+ // update ": with the latest command whether valid or invalid
+ commandHistoryRegister.setText(input);
+ var params = opt_params || {};
+ params.input = input;
+ try {
+ this.parseInput_(cm, inputStream, params);
+ } catch(e) {
+ showConfirm(cm, e);
+ throw e;
+ }
+ var command;
+ var commandName;
+ if (!params.commandName) {
+ // If only a line range is defined, move to the line.
+ if (params.line !== undefined) {
+ commandName = 'move';
+ }
+ } else {
+ command = this.matchCommand_(params.commandName);
+ if (command) {
+ commandName = command.name;
+ if (command.excludeFromCommandHistory) {
+ commandHistoryRegister.setText(previousCommand);
+ }
+ this.parseCommandArgs_(inputStream, params, command);
+ if (command.type == 'exToKey') {
+ // Handle Ex to Key mapping.
+ for (var i = 0; i < command.toKeys.length; i++) {
+ CodeMirror.Vim.handleKey(cm, command.toKeys[i], 'mapping');
+ }
+ return;
+ } else if (command.type == 'exToEx') {
+ // Handle Ex to Ex mapping.
+ this.processCommand(cm, command.toInput);
+ return;
+ }
+ }
+ }
+ if (!commandName) {
+ showConfirm(cm, 'Not an editor command ":' + input + '"');
+ return;
+ }
+ try {
+ exCommands[commandName](cm, params);
+ // Possibly asynchronous commands (e.g. substitute, which might have a
+ // user confirmation), are responsible for calling the callback when
+ // done. All others have it taken care of for them here.
+ if ((!command || !command.possiblyAsync) && params.callback) {
+ params.callback();
+ }
+ } catch(e) {
+ showConfirm(cm, e);
+ throw e;
+ }
+ },
+ parseInput_: function(cm, inputStream, result) {
+ inputStream.eatWhile(':');
+ // Parse range.
+ if (inputStream.eat('%')) {
+ result.line = cm.firstLine();
+ result.lineEnd = cm.lastLine();
+ } else {
+ result.line = this.parseLineSpec_(cm, inputStream);
+ if (result.line !== undefined && inputStream.eat(',')) {
+ result.lineEnd = this.parseLineSpec_(cm, inputStream);
+ }
+ }
+
+ // Parse command name.
+ var commandMatch = inputStream.match(/^(\w+)/);
+ if (commandMatch) {
+ result.commandName = commandMatch[1];
+ } else {
+ result.commandName = inputStream.match(/.*/)[0];
+ }
+
+ return result;
+ },
+ parseLineSpec_: function(cm, inputStream) {
+ var numberMatch = inputStream.match(/^(\d+)/);
+ if (numberMatch) {
+ return parseInt(numberMatch[1], 10) - 1;
+ }
+ switch (inputStream.next()) {
+ case '.':
+ return cm.getCursor().line;
+ case '$':
+ return cm.lastLine();
+ case '\'':
+ var mark = cm.state.vim.marks[inputStream.next()];
+ if (mark && mark.find()) {
+ return mark.find().line;
+ }
+ throw new Error('Mark not set');
+ default:
+ inputStream.backUp(1);
+ return undefined;
+ }
+ },
+ parseCommandArgs_: function(inputStream, params, command) {
+ if (inputStream.eol()) {
+ return;
+ }
+ params.argString = inputStream.match(/.*/)[0];
+ // Parse command-line arguments
+ var delim = command.argDelimiter || /\s+/;
+ var args = trim(params.argString).split(delim);
+ if (args.length && args[0]) {
+ params.args = args;
+ }
+ },
+ matchCommand_: function(commandName) {
+ // Return the command in the command map that matches the shortest
+ // prefix of the passed in command name. The match is guaranteed to be
+ // unambiguous if the defaultExCommandMap's shortNames are set up
+ // correctly. (see @code{defaultExCommandMap}).
+ for (var i = commandName.length; i > 0; i--) {
+ var prefix = commandName.substring(0, i);
+ if (this.commandMap_[prefix]) {
+ var command = this.commandMap_[prefix];
+ if (command.name.indexOf(commandName) === 0) {
+ return command;
+ }
+ }
+ }
+ return null;
+ },
+ buildCommandMap_: function() {
+ this.commandMap_ = {};
+ for (var i = 0; i < defaultExCommandMap.length; i++) {
+ var command = defaultExCommandMap[i];
+ var key = command.shortName || command.name;
+ this.commandMap_[key] = command;
+ }
+ },
+ map: function(lhs, rhs, ctx) {
+ if (lhs != ':' && lhs.charAt(0) == ':') {
+ if (ctx) { throw Error('Mode not supported for ex mappings'); }
+ var commandName = lhs.substring(1);
+ if (rhs != ':' && rhs.charAt(0) == ':') {
+ // Ex to Ex mapping
+ this.commandMap_[commandName] = {
+ name: commandName,
+ type: 'exToEx',
+ toInput: rhs.substring(1),
+ user: true
+ };
+ } else {
+ // Ex to key mapping
+ this.commandMap_[commandName] = {
+ name: commandName,
+ type: 'exToKey',
+ toKeys: rhs,
+ user: true
+ };
+ }
+ } else {
+ if (rhs != ':' && rhs.charAt(0) == ':') {
+ // Key to Ex mapping.
+ var mapping = {
+ keys: lhs,
+ type: 'keyToEx',
+ exArgs: { input: rhs.substring(1) },
+ user: true};
+ if (ctx) { mapping.context = ctx; }
+ defaultKeymap.unshift(mapping);
+ } else {
+ // Key to key mapping
+ var mapping = {
+ keys: lhs,
+ type: 'keyToKey',
+ toKeys: rhs,
+ user: true
+ };
+ if (ctx) { mapping.context = ctx; }
+ defaultKeymap.unshift(mapping);
+ }
+ }
+ },
+ unmap: function(lhs, ctx) {
+ if (lhs != ':' && lhs.charAt(0) == ':') {
+ // Ex to Ex or Ex to key mapping
+ if (ctx) { throw Error('Mode not supported for ex mappings'); }
+ var commandName = lhs.substring(1);
+ if (this.commandMap_[commandName] && this.commandMap_[commandName].user) {
+ delete this.commandMap_[commandName];
+ return;
+ }
+ } else {
+ // Key to Ex or key to key mapping
+ var keys = lhs;
+ for (var i = 0; i < defaultKeymap.length; i++) {
+ if (keys == defaultKeymap[i].keys
+ && defaultKeymap[i].context === ctx
+ && defaultKeymap[i].user) {
+ defaultKeymap.splice(i, 1);
+ return;
+ }
+ }
+ }
+ throw Error('No such mapping.');
+ }
+ };
+
+ var exCommands = {
+ colorscheme: function(cm, params) {
+ if (!params.args || params.args.length < 1) {
+ showConfirm(cm, cm.getOption('theme'));
+ return;
+ }
+ cm.setOption('theme', params.args[0]);
+ },
+ map: function(cm, params, ctx) {
+ var mapArgs = params.args;
+ if (!mapArgs || mapArgs.length < 2) {
+ if (cm) {
+ showConfirm(cm, 'Invalid mapping: ' + params.input);
+ }
+ return;
+ }
+ exCommandDispatcher.map(mapArgs[0], mapArgs[1], ctx);
+ },
+ imap: function(cm, params) { this.map(cm, params, 'insert'); },
+ nmap: function(cm, params) { this.map(cm, params, 'normal'); },
+ vmap: function(cm, params) { this.map(cm, params, 'visual'); },
+ unmap: function(cm, params, ctx) {
+ var mapArgs = params.args;
+ if (!mapArgs || mapArgs.length < 1) {
+ if (cm) {
+ showConfirm(cm, 'No such mapping: ' + params.input);
+ }
+ return;
+ }
+ exCommandDispatcher.unmap(mapArgs[0], ctx);
+ },
+ move: function(cm, params) {
+ commandDispatcher.processCommand(cm, cm.state.vim, {
+ type: 'motion',
+ motion: 'moveToLineOrEdgeOfDocument',
+ motionArgs: { forward: false, explicitRepeat: true,
+ linewise: true },
+ repeatOverride: params.line+1});
+ },
+ set: function(cm, params) {
+ var setArgs = params.args;
+ // Options passed through to the setOption/getOption calls. May be passed in by the
+ // local/global versions of the set command
+ var setCfg = params.setCfg || {};
+ if (!setArgs || setArgs.length < 1) {
+ if (cm) {
+ showConfirm(cm, 'Invalid mapping: ' + params.input);
+ }
+ return;
+ }
+ var expr = setArgs[0].split('=');
+ var optionName = expr[0];
+ var value = expr[1];
+ var forceGet = false;
+
+ if (optionName.charAt(optionName.length - 1) == '?') {
+ // If post-fixed with ?, then the set is actually a get.
+ if (value) { throw Error('Trailing characters: ' + params.argString); }
+ optionName = optionName.substring(0, optionName.length - 1);
+ forceGet = true;
+ }
+ if (value === undefined && optionName.substring(0, 2) == 'no') {
+ // To set boolean options to false, the option name is prefixed with
+ // 'no'.
+ optionName = optionName.substring(2);
+ value = false;
+ }
+
+ var optionIsBoolean = options[optionName] && options[optionName].type == 'boolean';
+ if (optionIsBoolean && value == undefined) {
+ // Calling set with a boolean option sets it to true.
+ value = true;
+ }
+ // If no value is provided, then we assume this is a get.
+ if (!optionIsBoolean && value === undefined || forceGet) {
+ var oldValue = getOption(optionName, cm, setCfg);
+ if (oldValue === true || oldValue === false) {
+ showConfirm(cm, ' ' + (oldValue ? '' : 'no') + optionName);
+ } else {
+ showConfirm(cm, ' ' + optionName + '=' + oldValue);
+ }
+ } else {
+ setOption(optionName, value, cm, setCfg);
+ }
+ },
+ setlocal: function (cm, params) {
+ // setCfg is passed through to setOption
+ params.setCfg = {scope: 'local'};
+ this.set(cm, params);
+ },
+ setglobal: function (cm, params) {
+ // setCfg is passed through to setOption
+ params.setCfg = {scope: 'global'};
+ this.set(cm, params);
+ },
+ registers: function(cm, params) {
+ var regArgs = params.args;
+ var registers = vimGlobalState.registerController.registers;
+ var regInfo = '----------Registers----------<br><br>';
+ if (!regArgs) {
+ for (var registerName in registers) {
+ var text = registers[registerName].toString();
+ if (text.length) {
+ regInfo += '"' + registerName + ' ' + text + '<br>';
+ }
+ }
+ } else {
+ var registerName;
+ regArgs = regArgs.join('');
+ for (var i = 0; i < regArgs.length; i++) {
+ registerName = regArgs.charAt(i);
+ if (!vimGlobalState.registerController.isValidRegister(registerName)) {
+ continue;
+ }
+ var register = registers[registerName] || new Register();
+ regInfo += '"' + registerName + ' ' + register.toString() + '<br>';
+ }
+ }
+ showConfirm(cm, regInfo);
+ },
+ sort: function(cm, params) {
+ var reverse, ignoreCase, unique, number;
+ function parseArgs() {
+ if (params.argString) {
+ var args = new CodeMirror.StringStream(params.argString);
+ if (args.eat('!')) { reverse = true; }
+ if (args.eol()) { return; }
+ if (!args.eatSpace()) { return 'Invalid arguments'; }
+ var opts = args.match(/[a-z]+/);
+ if (opts) {
+ opts = opts[0];
+ ignoreCase = opts.indexOf('i') != -1;
+ unique = opts.indexOf('u') != -1;
+ var decimal = opts.indexOf('d') != -1 && 1;
+ var hex = opts.indexOf('x') != -1 && 1;
+ var octal = opts.indexOf('o') != -1 && 1;
+ if (decimal + hex + octal > 1) { return 'Invalid arguments'; }
+ number = decimal && 'decimal' || hex && 'hex' || octal && 'octal';
+ }
+ if (args.match(/\/.*\//)) { return 'patterns not supported'; }
+ }
+ }
+ var err = parseArgs();
+ if (err) {
+ showConfirm(cm, err + ': ' + params.argString);
+ return;
+ }
+ var lineStart = params.line || cm.firstLine();
+ var lineEnd = params.lineEnd || params.line || cm.lastLine();
+ if (lineStart == lineEnd) { return; }
+ var curStart = Pos(lineStart, 0);
+ var curEnd = Pos(lineEnd, lineLength(cm, lineEnd));
+ var text = cm.getRange(curStart, curEnd).split('\n');
+ var numberRegex = (number == 'decimal') ? /(-?)([\d]+)/ :
+ (number == 'hex') ? /(-?)(?:0x)?([0-9a-f]+)/i :
+ (number == 'octal') ? /([0-7]+)/ : null;
+ var radix = (number == 'decimal') ? 10 : (number == 'hex') ? 16 : (number == 'octal') ? 8 : null;
+ var numPart = [], textPart = [];
+ if (number) {
+ for (var i = 0; i < text.length; i++) {
+ if (numberRegex.exec(text[i])) {
+ numPart.push(text[i]);
+ } else {
+ textPart.push(text[i]);
+ }
+ }
+ } else {
+ textPart = text;
+ }
+ function compareFn(a, b) {
+ if (reverse) { var tmp; tmp = a; a = b; b = tmp; }
+ if (ignoreCase) { a = a.toLowerCase(); b = b.toLowerCase(); }
+ var anum = number && numberRegex.exec(a);
+ var bnum = number && numberRegex.exec(b);
+ if (!anum) { return a < b ? -1 : 1; }
+ anum = parseInt((anum[1] + anum[2]).toLowerCase(), radix);
+ bnum = parseInt((bnum[1] + bnum[2]).toLowerCase(), radix);
+ return anum - bnum;
+ }
+ numPart.sort(compareFn);
+ textPart.sort(compareFn);
+ text = (!reverse) ? textPart.concat(numPart) : numPart.concat(textPart);
+ if (unique) { // Remove duplicate lines
+ var textOld = text;
+ var lastLine;
+ text = [];
+ for (var i = 0; i < textOld.length; i++) {
+ if (textOld[i] != lastLine) {
+ text.push(textOld[i]);
+ }
+ lastLine = textOld[i];
+ }
+ }
+ cm.replaceRange(text.join('\n'), curStart, curEnd);
+ },
+ global: function(cm, params) {
+ // a global command is of the form
+ // :[range]g/pattern/[cmd]
+ // argString holds the string /pattern/[cmd]
+ var argString = params.argString;
+ if (!argString) {
+ showConfirm(cm, 'Regular Expression missing from global');
+ return;
+ }
+ // range is specified here
+ var lineStart = (params.line !== undefined) ? params.line : cm.firstLine();
+ var lineEnd = params.lineEnd || params.line || cm.lastLine();
+ // get the tokens from argString
+ var tokens = splitBySlash(argString);
+ var regexPart = argString, cmd;
+ if (tokens.length) {
+ regexPart = tokens[0];
+ cmd = tokens.slice(1, tokens.length).join('/');
+ }
+ if (regexPart) {
+ // If regex part is empty, then use the previous query. Otherwise
+ // use the regex part as the new query.
+ try {
+ updateSearchQuery(cm, regexPart, true /** ignoreCase */,
+ true /** smartCase */);
+ } catch (e) {
+ showConfirm(cm, 'Invalid regex: ' + regexPart);
+ return;
+ }
+ }
+ // now that we have the regexPart, search for regex matches in the
+ // specified range of lines
+ var query = getSearchState(cm).getQuery();
+ var matchedLines = [], content = '';
+ for (var i = lineStart; i <= lineEnd; i++) {
+ var matched = query.test(cm.getLine(i));
+ if (matched) {
+ matchedLines.push(i+1);
+ content+= cm.getLine(i) + '<br>';
+ }
+ }
+ // if there is no [cmd], just display the list of matched lines
+ if (!cmd) {
+ showConfirm(cm, content);
+ return;
+ }
+ var index = 0;
+ var nextCommand = function() {
+ if (index < matchedLines.length) {
+ var command = matchedLines[index] + cmd;
+ exCommandDispatcher.processCommand(cm, command, {
+ callback: nextCommand
+ });
+ }
+ index++;
+ };
+ nextCommand();
+ },
+ substitute: function(cm, params) {
+ if (!cm.getSearchCursor) {
+ throw new Error('Search feature not available. Requires searchcursor.js or ' +
+ 'any other getSearchCursor implementation.');
+ }
+ var argString = params.argString;
+ var tokens = argString ? splitBySlash(argString) : [];
+ var regexPart, replacePart = '', trailing, flagsPart, count;
+ var confirm = false; // Whether to confirm each replace.
+ var global = false; // True to replace all instances on a line, false to replace only 1.
+ if (tokens.length) {
+ regexPart = tokens[0];
+ replacePart = tokens[1];
+ if (replacePart !== undefined) {
+ if (getOption('pcre')) {
+ replacePart = unescapeRegexReplace(replacePart);
+ } else {
+ replacePart = translateRegexReplace(replacePart);
+ }
+ vimGlobalState.lastSubstituteReplacePart = replacePart;
+ }
+ trailing = tokens[2] ? tokens[2].split(' ') : [];
+ } else {
+ // either the argString is empty or its of the form ' hello/world'
+ // actually splitBySlash returns a list of tokens
+ // only if the string starts with a '/'
+ if (argString && argString.length) {
+ showConfirm(cm, 'Substitutions should be of the form ' +
+ ':s/pattern/replace/');
+ return;
+ }
+ }
+ // After the 3rd slash, we can have flags followed by a space followed
+ // by count.
+ if (trailing) {
+ flagsPart = trailing[0];
+ count = parseInt(trailing[1]);
+ if (flagsPart) {
+ if (flagsPart.indexOf('c') != -1) {
+ confirm = true;
+ flagsPart.replace('c', '');
+ }
+ if (flagsPart.indexOf('g') != -1) {
+ global = true;
+ flagsPart.replace('g', '');
+ }
+ regexPart = regexPart + '/' + flagsPart;
+ }
+ }
+ if (regexPart) {
+ // If regex part is empty, then use the previous query. Otherwise use
+ // the regex part as the new query.
+ try {
+ updateSearchQuery(cm, regexPart, true /** ignoreCase */,
+ true /** smartCase */);
+ } catch (e) {
+ showConfirm(cm, 'Invalid regex: ' + regexPart);
+ return;
+ }
+ }
+ replacePart = replacePart || vimGlobalState.lastSubstituteReplacePart;
+ if (replacePart === undefined) {
+ showConfirm(cm, 'No previous substitute regular expression');
+ return;
+ }
+ var state = getSearchState(cm);
+ var query = state.getQuery();
+ var lineStart = (params.line !== undefined) ? params.line : cm.getCursor().line;
+ var lineEnd = params.lineEnd || lineStart;
+ if (lineStart == cm.firstLine() && lineEnd == cm.lastLine()) {
+ lineEnd = Infinity;
+ }
+ if (count) {
+ lineStart = lineEnd;
+ lineEnd = lineStart + count - 1;
+ }
+ var startPos = clipCursorToContent(cm, Pos(lineStart, 0));
+ var cursor = cm.getSearchCursor(query, startPos);
+ doReplace(cm, confirm, global, lineStart, lineEnd, cursor, query, replacePart, params.callback);
+ },
+ redo: CodeMirror.commands.redo,
+ undo: CodeMirror.commands.undo,
+ write: function(cm) {
+ if (CodeMirror.commands.save) {
+ // If a save command is defined, call it.
+ CodeMirror.commands.save(cm);
+ } else if (cm.save) {
+ // Saves to text area if no save command is defined and cm.save() is available.
+ cm.save();
+ }
+ },
+ nohlsearch: function(cm) {
+ clearSearchHighlight(cm);
+ },
+ yank: function (cm) {
+ var cur = copyCursor(cm.getCursor());
+ var line = cur.line;
+ var lineText = cm.getLine(line);
+ vimGlobalState.registerController.pushText(
+ '0', 'yank', lineText, true, true);
+ },
+ delmarks: function(cm, params) {
+ if (!params.argString || !trim(params.argString)) {
+ showConfirm(cm, 'Argument required');
+ return;
+ }
+
+ var state = cm.state.vim;
+ var stream = new CodeMirror.StringStream(trim(params.argString));
+ while (!stream.eol()) {
+ stream.eatSpace();
+
+ // Record the streams position at the beginning of the loop for use
+ // in error messages.
+ var count = stream.pos;
+
+ if (!stream.match(/[a-zA-Z]/, false)) {
+ showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count));
+ return;
+ }
+
+ var sym = stream.next();
+ // Check if this symbol is part of a range
+ if (stream.match('-', true)) {
+ // This symbol is part of a range.
+
+ // The range must terminate at an alphabetic character.
+ if (!stream.match(/[a-zA-Z]/, false)) {
+ showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count));
+ return;
+ }
+
+ var startMark = sym;
+ var finishMark = stream.next();
+ // The range must terminate at an alphabetic character which
+ // shares the same case as the start of the range.
+ if (isLowerCase(startMark) && isLowerCase(finishMark) ||
+ isUpperCase(startMark) && isUpperCase(finishMark)) {
+ var start = startMark.charCodeAt(0);
+ var finish = finishMark.charCodeAt(0);
+ if (start >= finish) {
+ showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count));
+ return;
+ }
+
+ // Because marks are always ASCII values, and we have
+ // determined that they are the same case, we can use
+ // their char codes to iterate through the defined range.
+ for (var j = 0; j <= finish - start; j++) {
+ var mark = String.fromCharCode(start + j);
+ delete state.marks[mark];
+ }
+ } else {
+ showConfirm(cm, 'Invalid argument: ' + startMark + '-');
+ return;
+ }
+ } else {
+ // This symbol is a valid mark, and is not part of a range.
+ delete state.marks[sym];
+ }
+ }
+ }
+ };
+
+ var exCommandDispatcher = new ExCommandDispatcher();
+
+ /**
+ * @param {CodeMirror} cm CodeMirror instance we are in.
+ * @param {boolean} confirm Whether to confirm each replace.
+ * @param {Cursor} lineStart Line to start replacing from.
+ * @param {Cursor} lineEnd Line to stop replacing at.
+ * @param {RegExp} query Query for performing matches with.
+ * @param {string} replaceWith Text to replace matches with. May contain $1,
+ * $2, etc for replacing captured groups using Javascript replace.
+ * @param {function()} callback A callback for when the replace is done.
+ */
+ function doReplace(cm, confirm, global, lineStart, lineEnd, searchCursor, query,
+ replaceWith, callback) {
+ // Set up all the functions.
+ cm.state.vim.exMode = true;
+ var done = false;
+ var lastPos = searchCursor.from();
+ function replaceAll() {
+ cm.operation(function() {
+ while (!done) {
+ replace();
+ next();
+ }
+ stop();
+ });
+ }
+ function replace() {
+ var text = cm.getRange(searchCursor.from(), searchCursor.to());
+ var newText = text.replace(query, replaceWith);
+ searchCursor.replace(newText);
+ }
+ function next() {
+ // The below only loops to skip over multiple occurrences on the same
+ // line when 'global' is not true.
+ while(searchCursor.findNext() &&
+ isInRange(searchCursor.from(), lineStart, lineEnd)) {
+ if (!global && lastPos && searchCursor.from().line == lastPos.line) {
+ continue;
+ }
+ cm.scrollIntoView(searchCursor.from(), 30);
+ cm.setSelection(searchCursor.from(), searchCursor.to());
+ lastPos = searchCursor.from();
+ done = false;
+ return;
+ }
+ done = true;
+ }
+ function stop(close) {
+ if (close) { close(); }
+ cm.focus();
+ if (lastPos) {
+ cm.setCursor(lastPos);
+ var vim = cm.state.vim;
+ vim.exMode = false;
+ vim.lastHPos = vim.lastHSPos = lastPos.ch;
+ }
+ if (callback) { callback(); }
+ }
+ function onPromptKeyDown(e, _value, close) {
+ // Swallow all keys.
+ CodeMirror.e_stop(e);
+ var keyName = CodeMirror.keyName(e);
+ switch (keyName) {
+ case 'Y':
+ replace(); next(); break;
+ case 'N':
+ next(); break;
+ case 'A':
+ // replaceAll contains a call to close of its own. We don't want it
+ // to fire too early or multiple times.
+ var savedCallback = callback;
+ callback = undefined;
+ cm.operation(replaceAll);
+ callback = savedCallback;
+ break;
+ case 'L':
+ replace();
+ // fall through and exit.
+ case 'Q':
+ case 'Esc':
+ case 'Ctrl-C':
+ case 'Ctrl-[':
+ stop(close);
+ break;
+ }
+ if (done) { stop(close); }
+ return true;
+ }
+
+ // Actually do replace.
+ next();
+ if (done) {
+ showConfirm(cm, 'No matches for ' + query.source);
+ return;
+ }
+ if (!confirm) {
+ replaceAll();
+ if (callback) { callback(); };
+ return;
+ }
+ showPrompt(cm, {
+ prefix: 'replace with <strong>' + replaceWith + '</strong> (y/n/a/q/l)',
+ onKeyDown: onPromptKeyDown
+ });
+ }
+
+ CodeMirror.keyMap.vim = {
+ attach: attachVimMap,
+ detach: detachVimMap,
+ call: cmKey
+ };
+
+ function exitInsertMode(cm) {
+ var vim = cm.state.vim;
+ var macroModeState = vimGlobalState.macroModeState;
+ var insertModeChangeRegister = vimGlobalState.registerController.getRegister('.');
+ var isPlaying = macroModeState.isPlaying;
+ var lastChange = macroModeState.lastInsertModeChanges;
+ // In case of visual block, the insertModeChanges are not saved as a
+ // single word, so we convert them to a single word
+ // so as to update the ". register as expected in real vim.
+ var text = [];
+ if (!isPlaying) {
+ var selLength = lastChange.inVisualBlock ? vim.lastSelection.visualBlock.height : 1;
+ var changes = lastChange.changes;
+ var text = [];
+ var i = 0;
+ // In case of multiple selections in blockwise visual,
+ // the inserted text, for example: 'f<Backspace>oo', is stored as
+ // 'f', 'f', InsertModeKey 'o', 'o', 'o', 'o'. (if you have a block with 2 lines).
+ // We push the contents of the changes array as per the following:
+ // 1. In case of InsertModeKey, just increment by 1.
+ // 2. In case of a character, jump by selLength (2 in the example).
+ while (i < changes.length) {
+ // This loop will convert 'ff<bs>oooo' to 'f<bs>oo'.
+ text.push(changes[i]);
+ if (changes[i] instanceof InsertModeKey) {
+ i++;
+ } else {
+ i+= selLength;
+ }
+ }
+ lastChange.changes = text;
+ cm.off('change', onChange);
+ CodeMirror.off(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown);
+ }
+ if (!isPlaying && vim.insertModeRepeat > 1) {
+ // Perform insert mode repeat for commands like 3,a and 3,o.
+ repeatLastEdit(cm, vim, vim.insertModeRepeat - 1,
+ true /** repeatForInsert */);
+ vim.lastEditInputState.repeatOverride = vim.insertModeRepeat;
+ }
+ delete vim.insertModeRepeat;
+ vim.insertMode = false;
+ cm.setCursor(cm.getCursor().line, cm.getCursor().ch-1);
+ cm.setOption('keyMap', 'vim');
+ cm.setOption('disableInput', true);
+ cm.toggleOverwrite(false); // exit replace mode if we were in it.
+ // update the ". register before exiting insert mode
+ insertModeChangeRegister.setText(lastChange.changes.join(''));
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"});
+ if (macroModeState.isRecording) {
+ logInsertModeChange(macroModeState);
+ }
+ }
+
+ function _mapCommand(command) {
+ defaultKeymap.unshift(command);
+ }
+
+ function mapCommand(keys, type, name, args, extra) {
+ var command = {keys: keys, type: type};
+ command[type] = name;
+ command[type + "Args"] = args;
+ for (var key in extra)
+ command[key] = extra[key];
+ _mapCommand(command);
+ }
+
+ // The timeout in milliseconds for the two-character ESC keymap should be
+ // adjusted according to your typing speed to prevent false positives.
+ defineOption('insertModeEscKeysTimeout', 200, 'number');
+
+ CodeMirror.keyMap['vim-insert'] = {
+ // TODO: override navigation keys so that Esc will cancel automatic
+ // indentation from o, O, i_<CR>
+ 'Ctrl-N': 'autocomplete',
+ 'Ctrl-P': 'autocomplete',
+ 'Enter': function(cm) {
+ var fn = CodeMirror.commands.newlineAndIndentContinueComment ||
+ CodeMirror.commands.newlineAndIndent;
+ fn(cm);
+ },
+ fallthrough: ['default'],
+ attach: attachVimMap,
+ detach: detachVimMap,
+ call: cmKey
+ };
+
+ CodeMirror.keyMap['vim-replace'] = {
+ 'Backspace': 'goCharLeft',
+ fallthrough: ['vim-insert'],
+ attach: attachVimMap,
+ detach: detachVimMap,
+ call: cmKey
+ };
+
+ function executeMacroRegister(cm, vim, macroModeState, registerName) {
+ var register = vimGlobalState.registerController.getRegister(registerName);
+ if (registerName == ':') {
+ // Read-only register containing last Ex command.
+ if (register.keyBuffer[0]) {
+ exCommandDispatcher.processCommand(cm, register.keyBuffer[0]);
+ }
+ macroModeState.isPlaying = false;
+ return;
+ }
+ var keyBuffer = register.keyBuffer;
+ var imc = 0;
+ macroModeState.isPlaying = true;
+ macroModeState.replaySearchQueries = register.searchQueries.slice(0);
+ for (var i = 0; i < keyBuffer.length; i++) {
+ var text = keyBuffer[i];
+ var match, key;
+ while (text) {
+ // Pull off one command key, which is either a single character
+ // or a special sequence wrapped in '<' and '>', e.g. '<Space>'.
+ match = (/<\w+-.+?>|<\w+>|./).exec(text);
+ key = match[0];
+ text = text.substring(match.index + key.length);
+ CodeMirror.Vim.handleKey(cm, key, 'macro');
+ if (vim.insertMode) {
+ var changes = register.insertModeChanges[imc++].changes;
+ vimGlobalState.macroModeState.lastInsertModeChanges.changes =
+ changes;
+ repeatInsertModeChanges(cm, changes, 1);
+ exitInsertMode(cm);
+ }
+ }
+ };
+ macroModeState.isPlaying = false;
+ }
+
+ function logKey(macroModeState, key) {
+ if (macroModeState.isPlaying) { return; }
+ var registerName = macroModeState.latestRegister;
+ var register = vimGlobalState.registerController.getRegister(registerName);
+ if (register) {
+ register.pushText(key);
+ }
+ }
+
+ function logInsertModeChange(macroModeState) {
+ if (macroModeState.isPlaying) { return; }
+ var registerName = macroModeState.latestRegister;
+ var register = vimGlobalState.registerController.getRegister(registerName);
+ if (register && register.pushInsertModeChanges) {
+ register.pushInsertModeChanges(macroModeState.lastInsertModeChanges);
+ }
+ }
+
+ function logSearchQuery(macroModeState, query) {
+ if (macroModeState.isPlaying) { return; }
+ var registerName = macroModeState.latestRegister;
+ var register = vimGlobalState.registerController.getRegister(registerName);
+ if (register && register.pushSearchQuery) {
+ register.pushSearchQuery(query);
+ }
+ }
+
+ /**
+ * Listens for changes made in insert mode.
+ * Should only be active in insert mode.
+ */
+ function onChange(_cm, changeObj) {
+ var macroModeState = vimGlobalState.macroModeState;
+ var lastChange = macroModeState.lastInsertModeChanges;
+ if (!macroModeState.isPlaying) {
+ while(changeObj) {
+ lastChange.expectCursorActivityForChange = true;
+ if (changeObj.origin == '+input' || changeObj.origin == 'paste'
+ || changeObj.origin === undefined /* only in testing */) {
+ var text = changeObj.text.join('\n');
+ lastChange.changes.push(text);
+ }
+ // Change objects may be chained with next.
+ changeObj = changeObj.next;
+ }
+ }
+ }
+
+ /**
+ * Listens for any kind of cursor activity on CodeMirror.
+ */
+ function onCursorActivity(cm) {
+ var vim = cm.state.vim;
+ if (vim.insertMode) {
+ // Tracking cursor activity in insert mode (for macro support).
+ var macroModeState = vimGlobalState.macroModeState;
+ if (macroModeState.isPlaying) { return; }
+ var lastChange = macroModeState.lastInsertModeChanges;
+ if (lastChange.expectCursorActivityForChange) {
+ lastChange.expectCursorActivityForChange = false;
+ } else {
+ // Cursor moved outside the context of an edit. Reset the change.
+ lastChange.changes = [];
+ }
+ } else if (!cm.curOp.isVimOp) {
+ handleExternalSelection(cm, vim);
+ }
+ if (vim.visualMode) {
+ updateFakeCursor(cm);
+ }
+ }
+ function updateFakeCursor(cm) {
+ var vim = cm.state.vim;
+ var from = clipCursorToContent(cm, copyCursor(vim.sel.head));
+ var to = offsetCursor(from, 0, 1);
+ if (vim.fakeCursor) {
+ vim.fakeCursor.clear();
+ }
+ vim.fakeCursor = cm.markText(from, to, {className: 'cm-animate-fat-cursor'});
+ }
+ function handleExternalSelection(cm, vim) {
+ var anchor = cm.getCursor('anchor');
+ var head = cm.getCursor('head');
+ // Enter or exit visual mode to match mouse selection.
+ if (vim.visualMode && !cm.somethingSelected()) {
+ exitVisualMode(cm, false);
+ } else if (!vim.visualMode && !vim.insertMode && cm.somethingSelected()) {
+ vim.visualMode = true;
+ vim.visualLine = false;
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "visual"});
+ }
+ if (vim.visualMode) {
+ // Bind CodeMirror selection model to vim selection model.
+ // Mouse selections are considered visual characterwise.
+ var headOffset = !cursorIsBefore(head, anchor) ? -1 : 0;
+ var anchorOffset = cursorIsBefore(head, anchor) ? -1 : 0;
+ head = offsetCursor(head, 0, headOffset);
+ anchor = offsetCursor(anchor, 0, anchorOffset);
+ vim.sel = {
+ anchor: anchor,
+ head: head
+ };
+ updateMark(cm, vim, '<', cursorMin(head, anchor));
+ updateMark(cm, vim, '>', cursorMax(head, anchor));
+ } else if (!vim.insertMode) {
+ // Reset lastHPos if selection was modified by something outside of vim mode e.g. by mouse.
+ vim.lastHPos = cm.getCursor().ch;
+ }
+ }
+
+ /** Wrapper for special keys pressed in insert mode */
+ function InsertModeKey(keyName) {
+ this.keyName = keyName;
+ }
+
+ /**
+ * Handles raw key down events from the text area.
+ * - Should only be active in insert mode.
+ * - For recording deletes in insert mode.
+ */
+ function onKeyEventTargetKeyDown(e) {
+ var macroModeState = vimGlobalState.macroModeState;
+ var lastChange = macroModeState.lastInsertModeChanges;
+ var keyName = CodeMirror.keyName(e);
+ if (!keyName) { return; }
+ function onKeyFound() {
+ lastChange.changes.push(new InsertModeKey(keyName));
+ return true;
+ }
+ if (keyName.indexOf('Delete') != -1 || keyName.indexOf('Backspace') != -1) {
+ CodeMirror.lookupKey(keyName, 'vim-insert', onKeyFound);
+ }
+ }
+
+ /**
+ * Repeats the last edit, which includes exactly 1 command and at most 1
+ * insert. Operator and motion commands are read from lastEditInputState,
+ * while action commands are read from lastEditActionCommand.
+ *
+ * If repeatForInsert is true, then the function was called by
+ * exitInsertMode to repeat the insert mode changes the user just made. The
+ * corresponding enterInsertMode call was made with a count.
+ */
+ function repeatLastEdit(cm, vim, repeat, repeatForInsert) {
+ var macroModeState = vimGlobalState.macroModeState;
+ macroModeState.isPlaying = true;
+ var isAction = !!vim.lastEditActionCommand;
+ var cachedInputState = vim.inputState;
+ function repeatCommand() {
+ if (isAction) {
+ commandDispatcher.processAction(cm, vim, vim.lastEditActionCommand);
+ } else {
+ commandDispatcher.evalInput(cm, vim);
+ }
+ }
+ function repeatInsert(repeat) {
+ if (macroModeState.lastInsertModeChanges.changes.length > 0) {
+ // For some reason, repeat cw in desktop VIM does not repeat
+ // insert mode changes. Will conform to that behavior.
+ repeat = !vim.lastEditActionCommand ? 1 : repeat;
+ var changeObject = macroModeState.lastInsertModeChanges;
+ repeatInsertModeChanges(cm, changeObject.changes, repeat);
+ }
+ }
+ vim.inputState = vim.lastEditInputState;
+ if (isAction && vim.lastEditActionCommand.interlaceInsertRepeat) {
+ // o and O repeat have to be interlaced with insert repeats so that the
+ // insertions appear on separate lines instead of the last line.
+ for (var i = 0; i < repeat; i++) {
+ repeatCommand();
+ repeatInsert(1);
+ }
+ } else {
+ if (!repeatForInsert) {
+ // Hack to get the cursor to end up at the right place. If I is
+ // repeated in insert mode repeat, cursor will be 1 insert
+ // change set left of where it should be.
+ repeatCommand();
+ }
+ repeatInsert(repeat);
+ }
+ vim.inputState = cachedInputState;
+ if (vim.insertMode && !repeatForInsert) {
+ // Don't exit insert mode twice. If repeatForInsert is set, then we
+ // were called by an exitInsertMode call lower on the stack.
+ exitInsertMode(cm);
+ }
+ macroModeState.isPlaying = false;
+ };
+
+ function repeatInsertModeChanges(cm, changes, repeat) {
+ function keyHandler(binding) {
+ if (typeof binding == 'string') {
+ CodeMirror.commands[binding](cm);
+ } else {
+ binding(cm);
+ }
+ return true;
+ }
+ var head = cm.getCursor('head');
+ var inVisualBlock = vimGlobalState.macroModeState.lastInsertModeChanges.inVisualBlock;
+ if (inVisualBlock) {
+ // Set up block selection again for repeating the changes.
+ var vim = cm.state.vim;
+ var lastSel = vim.lastSelection;
+ var offset = getOffset(lastSel.anchor, lastSel.head);
+ selectForInsert(cm, head, offset.line + 1);
+ repeat = cm.listSelections().length;
+ cm.setCursor(head);
+ }
+ for (var i = 0; i < repeat; i++) {
+ if (inVisualBlock) {
+ cm.setCursor(offsetCursor(head, i, 0));
+ }
+ for (var j = 0; j < changes.length; j++) {
+ var change = changes[j];
+ if (change instanceof InsertModeKey) {
+ CodeMirror.lookupKey(change.keyName, 'vim-insert', keyHandler);
+ } else {
+ var cur = cm.getCursor();
+ cm.replaceRange(change, cur, cur);
+ }
+ }
+ }
+ if (inVisualBlock) {
+ cm.setCursor(offsetCursor(head, 0, 1));
+ }
+ }
+
+ resetVimGlobalState();
+ return vimApi;
+ };
+ // Initialize Vim and make it available as an API.
+ CodeMirror.Vim = Vim();
+ });
+
+
+/***/ },
+/* 18 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ // A rough approximation of Sublime Text's keybindings
+ // Depends on addon/search/searchcursor.js and optionally addon/dialog/dialogs.js
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2), __webpack_require__(3), __webpack_require__(5));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../lib/codemirror", "../addon/search/searchcursor", "../addon/edit/matchbrackets"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ "use strict";
+
+ var map = CodeMirror.keyMap.sublime = {fallthrough: "default"};
+ var cmds = CodeMirror.commands;
+ var Pos = CodeMirror.Pos;
+ var mac = CodeMirror.keyMap["default"] == CodeMirror.keyMap.macDefault;
+ var ctrl = mac ? "Cmd-" : "Ctrl-";
+
+ // This is not exactly Sublime's algorithm. I couldn't make heads or tails of that.
+ function findPosSubword(doc, start, dir) {
+ if (dir < 0 && start.ch == 0) return doc.clipPos(Pos(start.line - 1));
+ var line = doc.getLine(start.line);
+ if (dir > 0 && start.ch >= line.length) return doc.clipPos(Pos(start.line + 1, 0));
+ var state = "start", type;
+ for (var pos = start.ch, e = dir < 0 ? 0 : line.length, i = 0; pos != e; pos += dir, i++) {
+ var next = line.charAt(dir < 0 ? pos - 1 : pos);
+ var cat = next != "_" && CodeMirror.isWordChar(next) ? "w" : "o";
+ if (cat == "w" && next.toUpperCase() == next) cat = "W";
+ if (state == "start") {
+ if (cat != "o") { state = "in"; type = cat; }
+ } else if (state == "in") {
+ if (type != cat) {
+ if (type == "w" && cat == "W" && dir < 0) pos--;
+ if (type == "W" && cat == "w" && dir > 0) { type = "w"; continue; }
+ break;
+ }
+ }
+ }
+ return Pos(start.line, pos);
+ }
+
+ function moveSubword(cm, dir) {
+ cm.extendSelectionsBy(function(range) {
+ if (cm.display.shift || cm.doc.extend || range.empty())
+ return findPosSubword(cm.doc, range.head, dir);
+ else
+ return dir < 0 ? range.from() : range.to();
+ });
+ }
+
+ cmds[map["Alt-Left"] = "goSubwordLeft"] = function(cm) { moveSubword(cm, -1); };
+ cmds[map["Alt-Right"] = "goSubwordRight"] = function(cm) { moveSubword(cm, 1); };
+
+ if (mac) map["Cmd-Left"] = "goLineStartSmart";
+
+ var scrollLineCombo = mac ? "Ctrl-Alt-" : "Ctrl-";
+
+ cmds[map[scrollLineCombo + "Up"] = "scrollLineUp"] = function(cm) {
+ var info = cm.getScrollInfo();
+ if (!cm.somethingSelected()) {
+ var visibleBottomLine = cm.lineAtHeight(info.top + info.clientHeight, "local");
+ if (cm.getCursor().line >= visibleBottomLine)
+ cm.execCommand("goLineUp");
+ }
+ cm.scrollTo(null, info.top - cm.defaultTextHeight());
+ };
+ cmds[map[scrollLineCombo + "Down"] = "scrollLineDown"] = function(cm) {
+ var info = cm.getScrollInfo();
+ if (!cm.somethingSelected()) {
+ var visibleTopLine = cm.lineAtHeight(info.top, "local")+1;
+ if (cm.getCursor().line <= visibleTopLine)
+ cm.execCommand("goLineDown");
+ }
+ cm.scrollTo(null, info.top + cm.defaultTextHeight());
+ };
+
+ cmds[map["Shift-" + ctrl + "L"] = "splitSelectionByLine"] = function(cm) {
+ var ranges = cm.listSelections(), lineRanges = [];
+ for (var i = 0; i < ranges.length; i++) {
+ var from = ranges[i].from(), to = ranges[i].to();
+ for (var line = from.line; line <= to.line; ++line)
+ if (!(to.line > from.line && line == to.line && to.ch == 0))
+ lineRanges.push({anchor: line == from.line ? from : Pos(line, 0),
+ head: line == to.line ? to : Pos(line)});
+ }
+ cm.setSelections(lineRanges, 0);
+ };
+
+ map["Shift-Tab"] = "indentLess";
+
+ cmds[map["Esc"] = "singleSelectionTop"] = function(cm) {
+ var range = cm.listSelections()[0];
+ cm.setSelection(range.anchor, range.head, {scroll: false});
+ };
+
+ cmds[map[ctrl + "L"] = "selectLine"] = function(cm) {
+ var ranges = cm.listSelections(), extended = [];
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i];
+ extended.push({anchor: Pos(range.from().line, 0),
+ head: Pos(range.to().line + 1, 0)});
+ }
+ cm.setSelections(extended);
+ };
+
+ map["Shift-Ctrl-K"] = "deleteLine";
+
+ function insertLine(cm, above) {
+ if (cm.isReadOnly()) return CodeMirror.Pass
+ cm.operation(function() {
+ var len = cm.listSelections().length, newSelection = [], last = -1;
+ for (var i = 0; i < len; i++) {
+ var head = cm.listSelections()[i].head;
+ if (head.line <= last) continue;
+ var at = Pos(head.line + (above ? 0 : 1), 0);
+ cm.replaceRange("\n", at, null, "+insertLine");
+ cm.indentLine(at.line, null, true);
+ newSelection.push({head: at, anchor: at});
+ last = head.line + 1;
+ }
+ cm.setSelections(newSelection);
+ });
+ cm.execCommand("indentAuto");
+ }
+
+ cmds[map[ctrl + "Enter"] = "insertLineAfter"] = function(cm) { return insertLine(cm, false); };
+
+ cmds[map["Shift-" + ctrl + "Enter"] = "insertLineBefore"] = function(cm) { return insertLine(cm, true); };
+
+ function wordAt(cm, pos) {
+ var start = pos.ch, end = start, line = cm.getLine(pos.line);
+ while (start && CodeMirror.isWordChar(line.charAt(start - 1))) --start;
+ while (end < line.length && CodeMirror.isWordChar(line.charAt(end))) ++end;
+ return {from: Pos(pos.line, start), to: Pos(pos.line, end), word: line.slice(start, end)};
+ }
+
+ cmds[map[ctrl + "D"] = "selectNextOccurrence"] = function(cm) {
+ var from = cm.getCursor("from"), to = cm.getCursor("to");
+ var fullWord = cm.state.sublimeFindFullWord == cm.doc.sel;
+ if (CodeMirror.cmpPos(from, to) == 0) {
+ var word = wordAt(cm, from);
+ if (!word.word) return;
+ cm.setSelection(word.from, word.to);
+ fullWord = true;
+ } else {
+ var text = cm.getRange(from, to);
+ var query = fullWord ? new RegExp("\\b" + text + "\\b") : text;
+ var cur = cm.getSearchCursor(query, to);
+ if (cur.findNext()) {
+ cm.addSelection(cur.from(), cur.to());
+ } else {
+ cur = cm.getSearchCursor(query, Pos(cm.firstLine(), 0));
+ if (cur.findNext())
+ cm.addSelection(cur.from(), cur.to());
+ }
+ }
+ if (fullWord)
+ cm.state.sublimeFindFullWord = cm.doc.sel;
+ };
+
+ var mirror = "(){}[]";
+ function selectBetweenBrackets(cm) {
+ var pos = cm.getCursor(), opening = cm.scanForBracket(pos, -1);
+ if (!opening) return;
+ for (;;) {
+ var closing = cm.scanForBracket(pos, 1);
+ if (!closing) return;
+ if (closing.ch == mirror.charAt(mirror.indexOf(opening.ch) + 1)) {
+ cm.setSelection(Pos(opening.pos.line, opening.pos.ch + 1), closing.pos, false);
+ return true;
+ }
+ pos = Pos(closing.pos.line, closing.pos.ch + 1);
+ }
+ }
+
+ cmds[map["Shift-" + ctrl + "Space"] = "selectScope"] = function(cm) {
+ selectBetweenBrackets(cm) || cm.execCommand("selectAll");
+ };
+ cmds[map["Shift-" + ctrl + "M"] = "selectBetweenBrackets"] = function(cm) {
+ if (!selectBetweenBrackets(cm)) return CodeMirror.Pass;
+ };
+
+ cmds[map[ctrl + "M"] = "goToBracket"] = function(cm) {
+ cm.extendSelectionsBy(function(range) {
+ var next = cm.scanForBracket(range.head, 1);
+ if (next && CodeMirror.cmpPos(next.pos, range.head) != 0) return next.pos;
+ var prev = cm.scanForBracket(range.head, -1);
+ return prev && Pos(prev.pos.line, prev.pos.ch + 1) || range.head;
+ });
+ };
+
+ var swapLineCombo = mac ? "Cmd-Ctrl-" : "Shift-Ctrl-";
+
+ cmds[map[swapLineCombo + "Up"] = "swapLineUp"] = function(cm) {
+ if (cm.isReadOnly()) return CodeMirror.Pass
+ var ranges = cm.listSelections(), linesToMove = [], at = cm.firstLine() - 1, newSels = [];
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i], from = range.from().line - 1, to = range.to().line;
+ newSels.push({anchor: Pos(range.anchor.line - 1, range.anchor.ch),
+ head: Pos(range.head.line - 1, range.head.ch)});
+ if (range.to().ch == 0 && !range.empty()) --to;
+ if (from > at) linesToMove.push(from, to);
+ else if (linesToMove.length) linesToMove[linesToMove.length - 1] = to;
+ at = to;
+ }
+ cm.operation(function() {
+ for (var i = 0; i < linesToMove.length; i += 2) {
+ var from = linesToMove[i], to = linesToMove[i + 1];
+ var line = cm.getLine(from);
+ cm.replaceRange("", Pos(from, 0), Pos(from + 1, 0), "+swapLine");
+ if (to > cm.lastLine())
+ cm.replaceRange("\n" + line, Pos(cm.lastLine()), null, "+swapLine");
+ else
+ cm.replaceRange(line + "\n", Pos(to, 0), null, "+swapLine");
+ }
+ cm.setSelections(newSels);
+ cm.scrollIntoView();
+ });
+ };
+
+ cmds[map[swapLineCombo + "Down"] = "swapLineDown"] = function(cm) {
+ if (cm.isReadOnly()) return CodeMirror.Pass
+ var ranges = cm.listSelections(), linesToMove = [], at = cm.lastLine() + 1;
+ for (var i = ranges.length - 1; i >= 0; i--) {
+ var range = ranges[i], from = range.to().line + 1, to = range.from().line;
+ if (range.to().ch == 0 && !range.empty()) from--;
+ if (from < at) linesToMove.push(from, to);
+ else if (linesToMove.length) linesToMove[linesToMove.length - 1] = to;
+ at = to;
+ }
+ cm.operation(function() {
+ for (var i = linesToMove.length - 2; i >= 0; i -= 2) {
+ var from = linesToMove[i], to = linesToMove[i + 1];
+ var line = cm.getLine(from);
+ if (from == cm.lastLine())
+ cm.replaceRange("", Pos(from - 1), Pos(from), "+swapLine");
+ else
+ cm.replaceRange("", Pos(from, 0), Pos(from + 1, 0), "+swapLine");
+ cm.replaceRange(line + "\n", Pos(to, 0), null, "+swapLine");
+ }
+ cm.scrollIntoView();
+ });
+ };
+
+ cmds[map[ctrl + "/"] = "toggleCommentIndented"] = function(cm) {
+ cm.toggleComment({ indent: true });
+ }
+
+ cmds[map[ctrl + "J"] = "joinLines"] = function(cm) {
+ var ranges = cm.listSelections(), joined = [];
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i], from = range.from();
+ var start = from.line, end = range.to().line;
+ while (i < ranges.length - 1 && ranges[i + 1].from().line == end)
+ end = ranges[++i].to().line;
+ joined.push({start: start, end: end, anchor: !range.empty() && from});
+ }
+ cm.operation(function() {
+ var offset = 0, ranges = [];
+ for (var i = 0; i < joined.length; i++) {
+ var obj = joined[i];
+ var anchor = obj.anchor && Pos(obj.anchor.line - offset, obj.anchor.ch), head;
+ for (var line = obj.start; line <= obj.end; line++) {
+ var actual = line - offset;
+ if (line == obj.end) head = Pos(actual, cm.getLine(actual).length + 1);
+ if (actual < cm.lastLine()) {
+ cm.replaceRange(" ", Pos(actual), Pos(actual + 1, /^\s*/.exec(cm.getLine(actual + 1))[0].length));
+ ++offset;
+ }
+ }
+ ranges.push({anchor: anchor || head, head: head});
+ }
+ cm.setSelections(ranges, 0);
+ });
+ };
+
+ cmds[map["Shift-" + ctrl + "D"] = "duplicateLine"] = function(cm) {
+ cm.operation(function() {
+ var rangeCount = cm.listSelections().length;
+ for (var i = 0; i < rangeCount; i++) {
+ var range = cm.listSelections()[i];
+ if (range.empty())
+ cm.replaceRange(cm.getLine(range.head.line) + "\n", Pos(range.head.line, 0));
+ else
+ cm.replaceRange(cm.getRange(range.from(), range.to()), range.from());
+ }
+ cm.scrollIntoView();
+ });
+ };
+
+ map[ctrl + "T"] = "transposeChars";
+
+ function sortLines(cm, caseSensitive) {
+ if (cm.isReadOnly()) return CodeMirror.Pass
+ var ranges = cm.listSelections(), toSort = [], selected;
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i];
+ if (range.empty()) continue;
+ var from = range.from().line, to = range.to().line;
+ while (i < ranges.length - 1 && ranges[i + 1].from().line == to)
+ to = range[++i].to().line;
+ toSort.push(from, to);
+ }
+ if (toSort.length) selected = true;
+ else toSort.push(cm.firstLine(), cm.lastLine());
+
+ cm.operation(function() {
+ var ranges = [];
+ for (var i = 0; i < toSort.length; i += 2) {
+ var from = toSort[i], to = toSort[i + 1];
+ var start = Pos(from, 0), end = Pos(to);
+ var lines = cm.getRange(start, end, false);
+ if (caseSensitive)
+ lines.sort();
+ else
+ lines.sort(function(a, b) {
+ var au = a.toUpperCase(), bu = b.toUpperCase();
+ if (au != bu) { a = au; b = bu; }
+ return a < b ? -1 : a == b ? 0 : 1;
+ });
+ cm.replaceRange(lines, start, end);
+ if (selected) ranges.push({anchor: start, head: end});
+ }
+ if (selected) cm.setSelections(ranges, 0);
+ });
+ }
+
+ cmds[map["F9"] = "sortLines"] = function(cm) { sortLines(cm, true); };
+ cmds[map[ctrl + "F9"] = "sortLinesInsensitive"] = function(cm) { sortLines(cm, false); };
+
+ cmds[map["F2"] = "nextBookmark"] = function(cm) {
+ var marks = cm.state.sublimeBookmarks;
+ if (marks) while (marks.length) {
+ var current = marks.shift();
+ var found = current.find();
+ if (found) {
+ marks.push(current);
+ return cm.setSelection(found.from, found.to);
+ }
+ }
+ };
+
+ cmds[map["Shift-F2"] = "prevBookmark"] = function(cm) {
+ var marks = cm.state.sublimeBookmarks;
+ if (marks) while (marks.length) {
+ marks.unshift(marks.pop());
+ var found = marks[marks.length - 1].find();
+ if (!found)
+ marks.pop();
+ else
+ return cm.setSelection(found.from, found.to);
+ }
+ };
+
+ cmds[map[ctrl + "F2"] = "toggleBookmark"] = function(cm) {
+ var ranges = cm.listSelections();
+ var marks = cm.state.sublimeBookmarks || (cm.state.sublimeBookmarks = []);
+ for (var i = 0; i < ranges.length; i++) {
+ var from = ranges[i].from(), to = ranges[i].to();
+ var found = cm.findMarks(from, to);
+ for (var j = 0; j < found.length; j++) {
+ if (found[j].sublimeBookmark) {
+ found[j].clear();
+ for (var k = 0; k < marks.length; k++)
+ if (marks[k] == found[j])
+ marks.splice(k--, 1);
+ break;
+ }
+ }
+ if (j == found.length)
+ marks.push(cm.markText(from, to, {sublimeBookmark: true, clearWhenEmpty: false}));
+ }
+ };
+
+ cmds[map["Shift-" + ctrl + "F2"] = "clearBookmarks"] = function(cm) {
+ var marks = cm.state.sublimeBookmarks;
+ if (marks) for (var i = 0; i < marks.length; i++) marks[i].clear();
+ marks.length = 0;
+ };
+
+ cmds[map["Alt-F2"] = "selectBookmarks"] = function(cm) {
+ var marks = cm.state.sublimeBookmarks, ranges = [];
+ if (marks) for (var i = 0; i < marks.length; i++) {
+ var found = marks[i].find();
+ if (!found)
+ marks.splice(i--, 0);
+ else
+ ranges.push({anchor: found.from, head: found.to});
+ }
+ if (ranges.length)
+ cm.setSelections(ranges, 0);
+ };
+
+ map["Alt-Q"] = "wrapLines";
+
+ var cK = ctrl + "K ";
+
+ function modifyWordOrSelection(cm, mod) {
+ cm.operation(function() {
+ var ranges = cm.listSelections(), indices = [], replacements = [];
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i];
+ if (range.empty()) { indices.push(i); replacements.push(""); }
+ else replacements.push(mod(cm.getRange(range.from(), range.to())));
+ }
+ cm.replaceSelections(replacements, "around", "case");
+ for (var i = indices.length - 1, at; i >= 0; i--) {
+ var range = ranges[indices[i]];
+ if (at && CodeMirror.cmpPos(range.head, at) > 0) continue;
+ var word = wordAt(cm, range.head);
+ at = word.from;
+ cm.replaceRange(mod(word.word), word.from, word.to);
+ }
+ });
+ }
+
+ map[cK + ctrl + "Backspace"] = "delLineLeft";
+
+ cmds[map["Backspace"] = "smartBackspace"] = function(cm) {
+ if (cm.somethingSelected()) return CodeMirror.Pass;
+
+ cm.operation(function() {
+ var cursors = cm.listSelections();
+ var indentUnit = cm.getOption("indentUnit");
+
+ for (var i = cursors.length - 1; i >= 0; i--) {
+ var cursor = cursors[i].head;
+ var toStartOfLine = cm.getRange({line: cursor.line, ch: 0}, cursor);
+ var column = CodeMirror.countColumn(toStartOfLine, null, cm.getOption("tabSize"));
+
+ // Delete by one character by default
+ var deletePos = cm.findPosH(cursor, -1, "char", false);
+
+ if (toStartOfLine && !/\S/.test(toStartOfLine) && column % indentUnit == 0) {
+ var prevIndent = new Pos(cursor.line,
+ CodeMirror.findColumn(toStartOfLine, column - indentUnit, indentUnit));
+
+ // Smart delete only if we found a valid prevIndent location
+ if (prevIndent.ch != cursor.ch) deletePos = prevIndent;
+ }
+
+ cm.replaceRange("", deletePos, cursor, "+delete");
+ }
+ });
+ };
+
+ cmds[map[cK + ctrl + "K"] = "delLineRight"] = function(cm) {
+ cm.operation(function() {
+ var ranges = cm.listSelections();
+ for (var i = ranges.length - 1; i >= 0; i--)
+ cm.replaceRange("", ranges[i].anchor, Pos(ranges[i].to().line), "+delete");
+ cm.scrollIntoView();
+ });
+ };
+
+ cmds[map[cK + ctrl + "U"] = "upcaseAtCursor"] = function(cm) {
+ modifyWordOrSelection(cm, function(str) { return str.toUpperCase(); });
+ };
+ cmds[map[cK + ctrl + "L"] = "downcaseAtCursor"] = function(cm) {
+ modifyWordOrSelection(cm, function(str) { return str.toLowerCase(); });
+ };
+
+ cmds[map[cK + ctrl + "Space"] = "setSublimeMark"] = function(cm) {
+ if (cm.state.sublimeMark) cm.state.sublimeMark.clear();
+ cm.state.sublimeMark = cm.setBookmark(cm.getCursor());
+ };
+ cmds[map[cK + ctrl + "A"] = "selectToSublimeMark"] = function(cm) {
+ var found = cm.state.sublimeMark && cm.state.sublimeMark.find();
+ if (found) cm.setSelection(cm.getCursor(), found);
+ };
+ cmds[map[cK + ctrl + "W"] = "deleteToSublimeMark"] = function(cm) {
+ var found = cm.state.sublimeMark && cm.state.sublimeMark.find();
+ if (found) {
+ var from = cm.getCursor(), to = found;
+ if (CodeMirror.cmpPos(from, to) > 0) { var tmp = to; to = from; from = tmp; }
+ cm.state.sublimeKilled = cm.getRange(from, to);
+ cm.replaceRange("", from, to);
+ }
+ };
+ cmds[map[cK + ctrl + "X"] = "swapWithSublimeMark"] = function(cm) {
+ var found = cm.state.sublimeMark && cm.state.sublimeMark.find();
+ if (found) {
+ cm.state.sublimeMark.clear();
+ cm.state.sublimeMark = cm.setBookmark(cm.getCursor());
+ cm.setCursor(found);
+ }
+ };
+ cmds[map[cK + ctrl + "Y"] = "sublimeYank"] = function(cm) {
+ if (cm.state.sublimeKilled != null)
+ cm.replaceSelection(cm.state.sublimeKilled, null, "paste");
+ };
+
+ map[cK + ctrl + "G"] = "clearBookmarks";
+ cmds[map[cK + ctrl + "C"] = "showInCenter"] = function(cm) {
+ var pos = cm.cursorCoords(null, "local");
+ cm.scrollTo(null, (pos.top + pos.bottom) / 2 - cm.getScrollInfo().clientHeight / 2);
+ };
+
+ var selectLinesCombo = mac ? "Ctrl-Shift-" : "Ctrl-Alt-";
+ cmds[map[selectLinesCombo + "Up"] = "selectLinesUpward"] = function(cm) {
+ cm.operation(function() {
+ var ranges = cm.listSelections();
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i];
+ if (range.head.line > cm.firstLine())
+ cm.addSelection(Pos(range.head.line - 1, range.head.ch));
+ }
+ });
+ };
+ cmds[map[selectLinesCombo + "Down"] = "selectLinesDownward"] = function(cm) {
+ cm.operation(function() {
+ var ranges = cm.listSelections();
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i];
+ if (range.head.line < cm.lastLine())
+ cm.addSelection(Pos(range.head.line + 1, range.head.ch));
+ }
+ });
+ };
+
+ function getTarget(cm) {
+ var from = cm.getCursor("from"), to = cm.getCursor("to");
+ if (CodeMirror.cmpPos(from, to) == 0) {
+ var word = wordAt(cm, from);
+ if (!word.word) return;
+ from = word.from;
+ to = word.to;
+ }
+ return {from: from, to: to, query: cm.getRange(from, to), word: word};
+ }
+
+ function findAndGoTo(cm, forward) {
+ var target = getTarget(cm);
+ if (!target) return;
+ var query = target.query;
+ var cur = cm.getSearchCursor(query, forward ? target.to : target.from);
+
+ if (forward ? cur.findNext() : cur.findPrevious()) {
+ cm.setSelection(cur.from(), cur.to());
+ } else {
+ cur = cm.getSearchCursor(query, forward ? Pos(cm.firstLine(), 0)
+ : cm.clipPos(Pos(cm.lastLine())));
+ if (forward ? cur.findNext() : cur.findPrevious())
+ cm.setSelection(cur.from(), cur.to());
+ else if (target.word)
+ cm.setSelection(target.from, target.to);
+ }
+ };
+ cmds[map[ctrl + "F3"] = "findUnder"] = function(cm) { findAndGoTo(cm, true); };
+ cmds[map["Shift-" + ctrl + "F3"] = "findUnderPrevious"] = function(cm) { findAndGoTo(cm,false); };
+ cmds[map["Alt-F3"] = "findAllUnder"] = function(cm) {
+ var target = getTarget(cm);
+ if (!target) return;
+ var cur = cm.getSearchCursor(target.query);
+ var matches = [];
+ var primaryIndex = -1;
+ while (cur.findNext()) {
+ matches.push({anchor: cur.from(), head: cur.to()});
+ if (cur.from().line <= target.from.line && cur.from().ch <= target.from.ch)
+ primaryIndex++;
+ }
+ cm.setSelections(matches, primaryIndex);
+ };
+
+ map["Shift-" + ctrl + "["] = "fold";
+ map["Shift-" + ctrl + "]"] = "unfold";
+ map[cK + ctrl + "0"] = map[cK + ctrl + "j"] = "unfoldAll";
+
+ map[ctrl + "I"] = "findIncremental";
+ map["Shift-" + ctrl + "I"] = "findIncrementalReverse";
+ map[ctrl + "H"] = "replace";
+ map["F3"] = "findNext";
+ map["Shift-F3"] = "findPrev";
+
+ CodeMirror.normalizeKeyMap(map);
+ });
+
+
+/***/ },
+/* 19 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ "use strict";
+
+ function doFold(cm, pos, options, force) {
+ if (options && options.call) {
+ var finder = options;
+ options = null;
+ } else {
+ var finder = getOption(cm, options, "rangeFinder");
+ }
+ if (typeof pos == "number") pos = CodeMirror.Pos(pos, 0);
+ var minSize = getOption(cm, options, "minFoldSize");
+
+ function getRange(allowFolded) {
+ var range = finder(cm, pos);
+ if (!range || range.to.line - range.from.line < minSize) return null;
+ var marks = cm.findMarksAt(range.from);
+ for (var i = 0; i < marks.length; ++i) {
+ if (marks[i].__isFold && force !== "fold") {
+ if (!allowFolded) return null;
+ range.cleared = true;
+ marks[i].clear();
+ }
+ }
+ return range;
+ }
+
+ var range = getRange(true);
+ if (getOption(cm, options, "scanUp")) while (!range && pos.line > cm.firstLine()) {
+ pos = CodeMirror.Pos(pos.line - 1, 0);
+ range = getRange(false);
+ }
+ if (!range || range.cleared || force === "unfold") return;
+
+ var myWidget = makeWidget(cm, options);
+ CodeMirror.on(myWidget, "mousedown", function(e) {
+ myRange.clear();
+ CodeMirror.e_preventDefault(e);
+ });
+ var myRange = cm.markText(range.from, range.to, {
+ replacedWith: myWidget,
+ clearOnEnter: getOption(cm, options, "clearOnEnter"),
+ __isFold: true
+ });
+ myRange.on("clear", function(from, to) {
+ CodeMirror.signal(cm, "unfold", cm, from, to);
+ });
+ CodeMirror.signal(cm, "fold", cm, range.from, range.to);
+ }
+
+ function makeWidget(cm, options) {
+ var widget = getOption(cm, options, "widget");
+ if (typeof widget == "string") {
+ var text = document.createTextNode(widget);
+ widget = document.createElement("span");
+ widget.appendChild(text);
+ widget.className = "CodeMirror-foldmarker";
+ }
+ return widget;
+ }
+
+ // Clumsy backwards-compatible interface
+ CodeMirror.newFoldFunction = function(rangeFinder, widget) {
+ return function(cm, pos) { doFold(cm, pos, {rangeFinder: rangeFinder, widget: widget}); };
+ };
+
+ // New-style interface
+ CodeMirror.defineExtension("foldCode", function(pos, options, force) {
+ doFold(this, pos, options, force);
+ });
+
+ CodeMirror.defineExtension("isFolded", function(pos) {
+ var marks = this.findMarksAt(pos);
+ for (var i = 0; i < marks.length; ++i)
+ if (marks[i].__isFold) return true;
+ });
+
+ CodeMirror.commands.toggleFold = function(cm) {
+ cm.foldCode(cm.getCursor());
+ };
+ CodeMirror.commands.fold = function(cm) {
+ cm.foldCode(cm.getCursor(), null, "fold");
+ };
+ CodeMirror.commands.unfold = function(cm) {
+ cm.foldCode(cm.getCursor(), null, "unfold");
+ };
+ CodeMirror.commands.foldAll = function(cm) {
+ cm.operation(function() {
+ for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++)
+ cm.foldCode(CodeMirror.Pos(i, 0), null, "fold");
+ });
+ };
+ CodeMirror.commands.unfoldAll = function(cm) {
+ cm.operation(function() {
+ for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++)
+ cm.foldCode(CodeMirror.Pos(i, 0), null, "unfold");
+ });
+ };
+
+ CodeMirror.registerHelper("fold", "combine", function() {
+ var funcs = Array.prototype.slice.call(arguments, 0);
+ return function(cm, start) {
+ for (var i = 0; i < funcs.length; ++i) {
+ var found = funcs[i](cm, start);
+ if (found) return found;
+ }
+ };
+ });
+
+ CodeMirror.registerHelper("fold", "auto", function(cm, start) {
+ var helpers = cm.getHelpers(start, "fold");
+ for (var i = 0; i < helpers.length; i++) {
+ var cur = helpers[i](cm, start);
+ if (cur) return cur;
+ }
+ });
+
+ var defaultOptions = {
+ rangeFinder: CodeMirror.fold.auto,
+ widget: "\u2194",
+ minFoldSize: 0,
+ scanUp: false,
+ clearOnEnter: true
+ };
+
+ CodeMirror.defineOption("foldOptions", null);
+
+ function getOption(cm, options, name) {
+ if (options && options[name] !== undefined)
+ return options[name];
+ var editorOptions = cm.options.foldOptions;
+ if (editorOptions && editorOptions[name] !== undefined)
+ return editorOptions[name];
+ return defaultOptions[name];
+ }
+
+ CodeMirror.defineExtension("foldOption", function(options, name) {
+ return getOption(this, options, name);
+ });
+ });
+
+
+/***/ },
+/* 20 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ "use strict";
+
+ CodeMirror.registerHelper("fold", "brace", function(cm, start) {
+ var line = start.line, lineText = cm.getLine(line);
+ var tokenType;
+
+ function findOpening(openCh) {
+ for (var at = start.ch, pass = 0;;) {
+ var found = at <= 0 ? -1 : lineText.lastIndexOf(openCh, at - 1);
+ if (found == -1) {
+ if (pass == 1) break;
+ pass = 1;
+ at = lineText.length;
+ continue;
+ }
+ if (pass == 1 && found < start.ch) break;
+ tokenType = cm.getTokenTypeAt(CodeMirror.Pos(line, found + 1));
+ if (!/^(comment|string)/.test(tokenType)) return found + 1;
+ at = found - 1;
+ }
+ }
+
+ var startToken = "{", endToken = "}", startCh = findOpening("{");
+ if (startCh == null) {
+ startToken = "[", endToken = "]";
+ startCh = findOpening("[");
+ }
+
+ if (startCh == null) return;
+ var count = 1, lastLine = cm.lastLine(), end, endCh;
+ outer: for (var i = line; i <= lastLine; ++i) {
+ var text = cm.getLine(i), pos = i == line ? startCh : 0;
+ for (;;) {
+ var nextOpen = text.indexOf(startToken, pos), nextClose = text.indexOf(endToken, pos);
+ if (nextOpen < 0) nextOpen = text.length;
+ if (nextClose < 0) nextClose = text.length;
+ pos = Math.min(nextOpen, nextClose);
+ if (pos == text.length) break;
+ if (cm.getTokenTypeAt(CodeMirror.Pos(i, pos + 1)) == tokenType) {
+ if (pos == nextOpen) ++count;
+ else if (!--count) { end = i; endCh = pos; break outer; }
+ }
+ ++pos;
+ }
+ }
+ if (end == null || line == end && endCh == startCh) return;
+ return {from: CodeMirror.Pos(line, startCh),
+ to: CodeMirror.Pos(end, endCh)};
+ });
+
+ CodeMirror.registerHelper("fold", "import", function(cm, start) {
+ function hasImport(line) {
+ if (line < cm.firstLine() || line > cm.lastLine()) return null;
+ var start = cm.getTokenAt(CodeMirror.Pos(line, 1));
+ if (!/\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1));
+ if (start.type != "keyword" || start.string != "import") return null;
+ // Now find closing semicolon, return its position
+ for (var i = line, e = Math.min(cm.lastLine(), line + 10); i <= e; ++i) {
+ var text = cm.getLine(i), semi = text.indexOf(";");
+ if (semi != -1) return {startCh: start.end, end: CodeMirror.Pos(i, semi)};
+ }
+ }
+
+ var startLine = start.line, has = hasImport(startLine), prev;
+ if (!has || hasImport(startLine - 1) || ((prev = hasImport(startLine - 2)) && prev.end.line == startLine - 1))
+ return null;
+ for (var end = has.end;;) {
+ var next = hasImport(end.line + 1);
+ if (next == null) break;
+ end = next.end;
+ }
+ return {from: cm.clipPos(CodeMirror.Pos(startLine, has.startCh + 1)), to: end};
+ });
+
+ CodeMirror.registerHelper("fold", "include", function(cm, start) {
+ function hasInclude(line) {
+ if (line < cm.firstLine() || line > cm.lastLine()) return null;
+ var start = cm.getTokenAt(CodeMirror.Pos(line, 1));
+ if (!/\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1));
+ if (start.type == "meta" && start.string.slice(0, 8) == "#include") return start.start + 8;
+ }
+
+ var startLine = start.line, has = hasInclude(startLine);
+ if (has == null || hasInclude(startLine - 1) != null) return null;
+ for (var end = startLine;;) {
+ var next = hasInclude(end + 1);
+ if (next == null) break;
+ ++end;
+ }
+ return {from: CodeMirror.Pos(startLine, has + 1),
+ to: cm.clipPos(CodeMirror.Pos(end))};
+ });
+
+ });
+
+
+/***/ },
+/* 21 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2));
+ 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) {
+ var mode = cm.getModeAt(start), startToken = mode.blockCommentStart, endToken = mode.blockCommentEnd;
+ if (!startToken || !endToken) return;
+ var line = start.line, lineText = cm.getLine(line);
+
+ var startCh;
+ for (var at = start.ch, pass = 0;;) {
+ var found = at <= 0 ? -1 : lineText.lastIndexOf(startToken, at - 1);
+ if (found == -1) {
+ if (pass == 1) return;
+ pass = 1;
+ at = lineText.length;
+ continue;
+ }
+ if (pass == 1 && found < start.ch) return;
+ if (/comment/.test(cm.getTokenTypeAt(CodeMirror.Pos(line, found + 1))) &&
+ (found == 0 || lineText.slice(found - endToken.length, found) == endToken ||
+ !/comment/.test(cm.getTokenTypeAt(CodeMirror.Pos(line, found))))) {
+ startCh = found + startToken.length;
+ break;
+ }
+ at = found - 1;
+ }
+
+ var depth = 1, lastLine = cm.lastLine(), end, endCh;
+ outer: for (var i = line; i <= lastLine; ++i) {
+ var text = cm.getLine(i), pos = i == line ? startCh : 0;
+ for (;;) {
+ var nextOpen = text.indexOf(startToken, pos), nextClose = text.indexOf(endToken, pos);
+ if (nextOpen < 0) nextOpen = text.length;
+ if (nextClose < 0) nextClose = text.length;
+ pos = Math.min(nextOpen, nextClose);
+ if (pos == text.length) break;
+ if (pos == nextOpen) ++depth;
+ else if (!--depth) { end = i; endCh = pos; break outer; }
+ ++pos;
+ }
+ }
+ if (end == null || line == end && endCh == startCh) return;
+ return {from: CodeMirror.Pos(line, startCh),
+ to: CodeMirror.Pos(end, endCh)};
+ });
+
+ });
+
+
+/***/ },
+/* 22 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ "use strict";
+
+ var Pos = CodeMirror.Pos;
+ function cmp(a, b) { return a.line - b.line || a.ch - b.ch; }
+
+ var nameStartChar = "A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD";
+ var nameChar = nameStartChar + "\-\:\.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040";
+ var xmlTagStart = new RegExp("<(/?)([" + nameStartChar + "][" + nameChar + "]*)", "g");
+
+ function Iter(cm, line, ch, range) {
+ this.line = line; this.ch = ch;
+ this.cm = cm; this.text = cm.getLine(line);
+ this.min = range ? range.from : cm.firstLine();
+ this.max = range ? range.to - 1 : cm.lastLine();
+ }
+
+ function tagAt(iter, ch) {
+ var type = iter.cm.getTokenTypeAt(Pos(iter.line, ch));
+ return type && /\btag\b/.test(type);
+ }
+
+ function nextLine(iter) {
+ if (iter.line >= iter.max) return;
+ iter.ch = 0;
+ iter.text = iter.cm.getLine(++iter.line);
+ return true;
+ }
+ function prevLine(iter) {
+ if (iter.line <= iter.min) return;
+ iter.text = iter.cm.getLine(--iter.line);
+ iter.ch = iter.text.length;
+ return true;
+ }
+
+ function toTagEnd(iter) {
+ for (;;) {
+ var gt = iter.text.indexOf(">", iter.ch);
+ if (gt == -1) { if (nextLine(iter)) continue; else return; }
+ if (!tagAt(iter, gt + 1)) { iter.ch = gt + 1; continue; }
+ var lastSlash = iter.text.lastIndexOf("/", gt);
+ var selfClose = lastSlash > -1 && !/\S/.test(iter.text.slice(lastSlash + 1, gt));
+ iter.ch = gt + 1;
+ return selfClose ? "selfClose" : "regular";
+ }
+ }
+ function toTagStart(iter) {
+ for (;;) {
+ var lt = iter.ch ? iter.text.lastIndexOf("<", iter.ch - 1) : -1;
+ if (lt == -1) { if (prevLine(iter)) continue; else return; }
+ if (!tagAt(iter, lt + 1)) { iter.ch = lt; continue; }
+ xmlTagStart.lastIndex = lt;
+ iter.ch = lt;
+ var match = xmlTagStart.exec(iter.text);
+ if (match && match.index == lt) return match;
+ }
+ }
+
+ function toNextTag(iter) {
+ for (;;) {
+ xmlTagStart.lastIndex = iter.ch;
+ var found = xmlTagStart.exec(iter.text);
+ if (!found) { if (nextLine(iter)) continue; else return; }
+ if (!tagAt(iter, found.index + 1)) { iter.ch = found.index + 1; continue; }
+ iter.ch = found.index + found[0].length;
+ return found;
+ }
+ }
+ function toPrevTag(iter) {
+ for (;;) {
+ var gt = iter.ch ? iter.text.lastIndexOf(">", iter.ch - 1) : -1;
+ if (gt == -1) { if (prevLine(iter)) continue; else return; }
+ if (!tagAt(iter, gt + 1)) { iter.ch = gt; continue; }
+ var lastSlash = iter.text.lastIndexOf("/", gt);
+ var selfClose = lastSlash > -1 && !/\S/.test(iter.text.slice(lastSlash + 1, gt));
+ iter.ch = gt + 1;
+ return selfClose ? "selfClose" : "regular";
+ }
+ }
+
+ function findMatchingClose(iter, tag) {
+ var stack = [];
+ for (;;) {
+ var next = toNextTag(iter), end, startLine = iter.line, startCh = iter.ch - (next ? next[0].length : 0);
+ if (!next || !(end = toTagEnd(iter))) return;
+ if (end == "selfClose") continue;
+ if (next[1]) { // closing tag
+ for (var i = stack.length - 1; i >= 0; --i) if (stack[i] == next[2]) {
+ stack.length = i;
+ break;
+ }
+ if (i < 0 && (!tag || tag == next[2])) return {
+ tag: next[2],
+ from: Pos(startLine, startCh),
+ to: Pos(iter.line, iter.ch)
+ };
+ } else { // opening tag
+ stack.push(next[2]);
+ }
+ }
+ }
+ function findMatchingOpen(iter, tag) {
+ var stack = [];
+ for (;;) {
+ var prev = toPrevTag(iter);
+ if (!prev) return;
+ if (prev == "selfClose") { toTagStart(iter); continue; }
+ var endLine = iter.line, endCh = iter.ch;
+ var start = toTagStart(iter);
+ if (!start) return;
+ if (start[1]) { // closing tag
+ stack.push(start[2]);
+ } else { // opening tag
+ for (var i = stack.length - 1; i >= 0; --i) if (stack[i] == start[2]) {
+ stack.length = i;
+ break;
+ }
+ if (i < 0 && (!tag || tag == start[2])) return {
+ tag: start[2],
+ from: Pos(iter.line, iter.ch),
+ to: Pos(endLine, endCh)
+ };
+ }
+ }
+ }
+
+ CodeMirror.registerHelper("fold", "xml", function(cm, start) {
+ var iter = new Iter(cm, start.line, 0);
+ for (;;) {
+ var openTag = toNextTag(iter), end;
+ if (!openTag || iter.line != start.line || !(end = toTagEnd(iter))) return;
+ if (!openTag[1] && end != "selfClose") {
+ var startPos = Pos(iter.line, iter.ch);
+ var endPos = findMatchingClose(iter, openTag[2]);
+ return endPos && {from: startPos, to: endPos.from};
+ }
+ }
+ });
+ CodeMirror.findMatchingTag = function(cm, pos, range) {
+ var iter = new Iter(cm, pos.line, pos.ch, range);
+ if (iter.text.indexOf(">") == -1 && iter.text.indexOf("<") == -1) return;
+ var end = toTagEnd(iter), to = end && Pos(iter.line, iter.ch);
+ var start = end && toTagStart(iter);
+ if (!end || !start || cmp(iter, pos) > 0) return;
+ var here = {from: Pos(iter.line, iter.ch), to: to, tag: start[2]};
+ if (end == "selfClose") return {open: here, close: null, at: "open"};
+
+ if (start[1]) { // closing tag
+ return {open: findMatchingOpen(iter, start[2]), close: here, at: "close"};
+ } else { // opening tag
+ iter = new Iter(cm, to.line, to.ch, range);
+ return {open: here, close: findMatchingClose(iter, start[2]), at: "open"};
+ }
+ };
+
+ CodeMirror.findEnclosingTag = function(cm, pos, range) {
+ var iter = new Iter(cm, pos.line, pos.ch, range);
+ for (;;) {
+ var open = findMatchingOpen(iter);
+ if (!open) break;
+ var forward = new Iter(cm, pos.line, pos.ch, range);
+ var close = findMatchingClose(forward, open.tag);
+ if (close) return {open: open, close: close};
+ }
+ };
+
+ // Used by addon/edit/closetag.js
+ CodeMirror.scanForClosingTag = function(cm, pos, name, end) {
+ var iter = new Iter(cm, pos.line, pos.ch, end ? {from: 0, to: end} : null);
+ return findMatchingClose(iter, name);
+ };
+ });
+
+
+/***/ },
+/* 23 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
+ // Distributed under an MIT license: http://codemirror.net/LICENSE
+
+ (function(mod) {
+ if (true) // CommonJS
+ mod(__webpack_require__(2), __webpack_require__(19));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror", "./foldcode"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+ })(function(CodeMirror) {
+ "use strict";
+
+ CodeMirror.defineOption("foldGutter", false, function(cm, val, old) {
+ if (old && old != CodeMirror.Init) {
+ cm.clearGutter(cm.state.foldGutter.options.gutter);
+ cm.state.foldGutter = null;
+ cm.off("gutterClick", onGutterClick);
+ cm.off("change", onChange);
+ cm.off("viewportChange", onViewportChange);
+ cm.off("fold", onFold);
+ cm.off("unfold", onFold);
+ cm.off("swapDoc", onChange);
+ }
+ if (val) {
+ cm.state.foldGutter = new State(parseOptions(val));
+ updateInViewport(cm);
+ cm.on("gutterClick", onGutterClick);
+ cm.on("change", onChange);
+ cm.on("viewportChange", onViewportChange);
+ cm.on("fold", onFold);
+ cm.on("unfold", onFold);
+ cm.on("swapDoc", onChange);
+ }
+ });
+
+ var Pos = CodeMirror.Pos;
+
+ function State(options) {
+ this.options = options;
+ this.from = this.to = 0;
+ }
+
+ function parseOptions(opts) {
+ if (opts === true) opts = {};
+ if (opts.gutter == null) opts.gutter = "CodeMirror-foldgutter";
+ if (opts.indicatorOpen == null) opts.indicatorOpen = "CodeMirror-foldgutter-open";
+ if (opts.indicatorFolded == null) opts.indicatorFolded = "CodeMirror-foldgutter-folded";
+ return opts;
+ }
+
+ function isFolded(cm, line) {
+ var marks = cm.findMarks(Pos(line, 0), Pos(line + 1, 0));
+ for (var i = 0; i < marks.length; ++i)
+ if (marks[i].__isFold && marks[i].find().from.line == line) return marks[i];
+ }
+
+ function marker(spec) {
+ if (typeof spec == "string") {
+ var elt = document.createElement("div");
+ elt.className = spec + " CodeMirror-guttermarker-subtle";
+ return elt;
+ } else {
+ return spec.cloneNode(true);
+ }
+ }
+
+ function updateFoldInfo(cm, from, to) {
+ var opts = cm.state.foldGutter.options, cur = from;
+ var minSize = cm.foldOption(opts, "minFoldSize");
+ var func = cm.foldOption(opts, "rangeFinder");
+ cm.eachLine(from, to, function(line) {
+ var mark = null;
+ if (isFolded(cm, cur)) {
+ mark = marker(opts.indicatorFolded);
+ } else {
+ var pos = Pos(cur, 0);
+ var range = func && func(cm, pos);
+ if (range && range.to.line - range.from.line >= minSize)
+ mark = marker(opts.indicatorOpen);
+ }
+ cm.setGutterMarker(line, opts.gutter, mark);
+ ++cur;
+ });
+ }
+
+ function updateInViewport(cm) {
+ var vp = cm.getViewport(), state = cm.state.foldGutter;
+ if (!state) return;
+ cm.operation(function() {
+ updateFoldInfo(cm, vp.from, vp.to);
+ });
+ state.from = vp.from; state.to = vp.to;
+ }
+
+ function onGutterClick(cm, line, gutter) {
+ var state = cm.state.foldGutter;
+ if (!state) return;
+ var opts = state.options;
+ if (gutter != opts.gutter) return;
+ var folded = isFolded(cm, line);
+ if (folded) folded.clear();
+ else cm.foldCode(Pos(line, 0), opts.rangeFinder);
+ }
+
+ function onChange(cm) {
+ var state = cm.state.foldGutter;
+ if (!state) return;
+ var opts = state.options;
+ state.from = state.to = 0;
+ clearTimeout(state.changeUpdate);
+ state.changeUpdate = setTimeout(function() { updateInViewport(cm); }, opts.foldOnChangeTimeSpan || 600);
+ }
+
+ function onViewportChange(cm) {
+ var state = cm.state.foldGutter;
+ if (!state) return;
+ var opts = state.options;
+ clearTimeout(state.changeUpdate);
+ state.changeUpdate = setTimeout(function() {
+ var vp = cm.getViewport();
+ if (state.from == state.to || vp.from - state.to > 20 || state.from - vp.to > 20) {
+ updateInViewport(cm);
+ } else {
+ cm.operation(function() {
+ if (vp.from < state.from) {
+ updateFoldInfo(cm, vp.from, state.from);
+ state.from = vp.from;
+ }
+ if (vp.to > state.to) {
+ updateFoldInfo(cm, state.to, vp.to);
+ state.to = vp.to;
+ }
+ });
+ }
+ }, opts.updateViewportTimeSpan || 400);
+ }
+
+ function onFold(cm, from) {
+ var state = cm.state.foldGutter;
+ if (!state) return;
+ var line = from.line;
+ if (line >= state.from && line < state.to)
+ updateFoldInfo(cm, line, line + 1);
+ }
+ });
+
+
+/***/ }
+/******/ ]); \ No newline at end of file
diff --git a/devtools/client/sourceeditor/codemirror/keymap/emacs.js b/devtools/client/sourceeditor/codemirror/keymap/emacs.js
new file mode 100644
index 000000000..3eec1e576
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/keymap/emacs.js
@@ -0,0 +1,412 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ "use strict";
+
+ var Pos = CodeMirror.Pos;
+ function posEq(a, b) { return a.line == b.line && a.ch == b.ch; }
+
+ // Kill 'ring'
+
+ var killRing = [];
+ function addToRing(str) {
+ killRing.push(str);
+ if (killRing.length > 50) killRing.shift();
+ }
+ function growRingTop(str) {
+ if (!killRing.length) return addToRing(str);
+ killRing[killRing.length - 1] += str;
+ }
+ function getFromRing(n) { return killRing[killRing.length - (n ? Math.min(n, 1) : 1)] || ""; }
+ function popFromRing() { if (killRing.length > 1) killRing.pop(); return getFromRing(); }
+
+ var lastKill = null;
+
+ function kill(cm, from, to, mayGrow, text) {
+ if (text == null) text = cm.getRange(from, to);
+
+ if (mayGrow && lastKill && lastKill.cm == cm && posEq(from, lastKill.pos) && cm.isClean(lastKill.gen))
+ growRingTop(text);
+ else
+ addToRing(text);
+ cm.replaceRange("", from, to, "+delete");
+
+ if (mayGrow) lastKill = {cm: cm, pos: from, gen: cm.changeGeneration()};
+ else lastKill = null;
+ }
+
+ // Boundaries of various units
+
+ function byChar(cm, pos, dir) {
+ return cm.findPosH(pos, dir, "char", true);
+ }
+
+ function byWord(cm, pos, dir) {
+ return cm.findPosH(pos, dir, "word", true);
+ }
+
+ function byLine(cm, pos, dir) {
+ return cm.findPosV(pos, dir, "line", cm.doc.sel.goalColumn);
+ }
+
+ function byPage(cm, pos, dir) {
+ return cm.findPosV(pos, dir, "page", cm.doc.sel.goalColumn);
+ }
+
+ function byParagraph(cm, pos, dir) {
+ var no = pos.line, line = cm.getLine(no);
+ var sawText = /\S/.test(dir < 0 ? line.slice(0, pos.ch) : line.slice(pos.ch));
+ var fst = cm.firstLine(), lst = cm.lastLine();
+ for (;;) {
+ no += dir;
+ if (no < fst || no > lst)
+ return cm.clipPos(Pos(no - dir, dir < 0 ? 0 : null));
+ line = cm.getLine(no);
+ var hasText = /\S/.test(line);
+ if (hasText) sawText = true;
+ else if (sawText) return Pos(no, 0);
+ }
+ }
+
+ function bySentence(cm, pos, dir) {
+ var line = pos.line, ch = pos.ch;
+ var text = cm.getLine(pos.line), sawWord = false;
+ for (;;) {
+ var next = text.charAt(ch + (dir < 0 ? -1 : 0));
+ if (!next) { // End/beginning of line reached
+ if (line == (dir < 0 ? cm.firstLine() : cm.lastLine())) return Pos(line, ch);
+ text = cm.getLine(line + dir);
+ if (!/\S/.test(text)) return Pos(line, ch);
+ line += dir;
+ ch = dir < 0 ? text.length : 0;
+ continue;
+ }
+ if (sawWord && /[!?.]/.test(next)) return Pos(line, ch + (dir > 0 ? 1 : 0));
+ if (!sawWord) sawWord = /\w/.test(next);
+ ch += dir;
+ }
+ }
+
+ function byExpr(cm, pos, dir) {
+ var wrap;
+ if (cm.findMatchingBracket && (wrap = cm.findMatchingBracket(pos, true))
+ && wrap.match && (wrap.forward ? 1 : -1) == dir)
+ return dir > 0 ? Pos(wrap.to.line, wrap.to.ch + 1) : wrap.to;
+
+ for (var first = true;; first = false) {
+ var token = cm.getTokenAt(pos);
+ var after = Pos(pos.line, dir < 0 ? token.start : token.end);
+ if (first && dir > 0 && token.end == pos.ch || !/\w/.test(token.string)) {
+ var newPos = cm.findPosH(after, dir, "char");
+ if (posEq(after, newPos)) return pos;
+ else pos = newPos;
+ } else {
+ return after;
+ }
+ }
+ }
+
+ // Prefixes (only crudely supported)
+
+ function getPrefix(cm, precise) {
+ var digits = cm.state.emacsPrefix;
+ if (!digits) return precise ? null : 1;
+ clearPrefix(cm);
+ return digits == "-" ? -1 : Number(digits);
+ }
+
+ function repeated(cmd) {
+ var f = typeof cmd == "string" ? function(cm) { cm.execCommand(cmd); } : cmd;
+ return function(cm) {
+ var prefix = getPrefix(cm);
+ f(cm);
+ for (var i = 1; i < prefix; ++i) f(cm);
+ };
+ }
+
+ function findEnd(cm, pos, by, dir) {
+ var prefix = getPrefix(cm);
+ if (prefix < 0) { dir = -dir; prefix = -prefix; }
+ for (var i = 0; i < prefix; ++i) {
+ var newPos = by(cm, pos, dir);
+ if (posEq(newPos, pos)) break;
+ pos = newPos;
+ }
+ return pos;
+ }
+
+ function move(by, dir) {
+ var f = function(cm) {
+ cm.extendSelection(findEnd(cm, cm.getCursor(), by, dir));
+ };
+ f.motion = true;
+ return f;
+ }
+
+ function killTo(cm, by, dir) {
+ var selections = cm.listSelections(), cursor;
+ var i = selections.length;
+ while (i--) {
+ cursor = selections[i].head;
+ kill(cm, cursor, findEnd(cm, cursor, by, dir), true);
+ }
+ }
+
+ function killRegion(cm) {
+ if (cm.somethingSelected()) {
+ var selections = cm.listSelections(), selection;
+ var i = selections.length;
+ while (i--) {
+ selection = selections[i];
+ kill(cm, selection.anchor, selection.head);
+ }
+ return true;
+ }
+ }
+
+ function addPrefix(cm, digit) {
+ if (cm.state.emacsPrefix) {
+ if (digit != "-") cm.state.emacsPrefix += digit;
+ return;
+ }
+ // Not active yet
+ cm.state.emacsPrefix = digit;
+ cm.on("keyHandled", maybeClearPrefix);
+ cm.on("inputRead", maybeDuplicateInput);
+ }
+
+ var prefixPreservingKeys = {"Alt-G": true, "Ctrl-X": true, "Ctrl-Q": true, "Ctrl-U": true};
+
+ function maybeClearPrefix(cm, arg) {
+ if (!cm.state.emacsPrefixMap && !prefixPreservingKeys.hasOwnProperty(arg))
+ clearPrefix(cm);
+ }
+
+ function clearPrefix(cm) {
+ cm.state.emacsPrefix = null;
+ cm.off("keyHandled", maybeClearPrefix);
+ cm.off("inputRead", maybeDuplicateInput);
+ }
+
+ function maybeDuplicateInput(cm, event) {
+ var dup = getPrefix(cm);
+ if (dup > 1 && event.origin == "+input") {
+ var one = event.text.join("\n"), txt = "";
+ for (var i = 1; i < dup; ++i) txt += one;
+ cm.replaceSelection(txt);
+ }
+ }
+
+ function addPrefixMap(cm) {
+ cm.state.emacsPrefixMap = true;
+ cm.addKeyMap(prefixMap);
+ cm.on("keyHandled", maybeRemovePrefixMap);
+ cm.on("inputRead", maybeRemovePrefixMap);
+ }
+
+ function maybeRemovePrefixMap(cm, arg) {
+ if (typeof arg == "string" && (/^\d$/.test(arg) || arg == "Ctrl-U")) return;
+ cm.removeKeyMap(prefixMap);
+ cm.state.emacsPrefixMap = false;
+ cm.off("keyHandled", maybeRemovePrefixMap);
+ cm.off("inputRead", maybeRemovePrefixMap);
+ }
+
+ // Utilities
+
+ function setMark(cm) {
+ cm.setCursor(cm.getCursor());
+ cm.setExtending(!cm.getExtending());
+ cm.on("change", function() { cm.setExtending(false); });
+ }
+
+ function clearMark(cm) {
+ cm.setExtending(false);
+ cm.setCursor(cm.getCursor());
+ }
+
+ function getInput(cm, msg, f) {
+ if (cm.openDialog)
+ cm.openDialog(msg + ": <input type=\"text\" style=\"width: 10em\"/>", f, {bottom: true});
+ else
+ f(prompt(msg, ""));
+ }
+
+ function operateOnWord(cm, op) {
+ var start = cm.getCursor(), end = cm.findPosH(start, 1, "word");
+ cm.replaceRange(op(cm.getRange(start, end)), start, end);
+ cm.setCursor(end);
+ }
+
+ function toEnclosingExpr(cm) {
+ var pos = cm.getCursor(), line = pos.line, ch = pos.ch;
+ var stack = [];
+ while (line >= cm.firstLine()) {
+ var text = cm.getLine(line);
+ for (var i = ch == null ? text.length : ch; i > 0;) {
+ var ch = text.charAt(--i);
+ if (ch == ")")
+ stack.push("(");
+ else if (ch == "]")
+ stack.push("[");
+ else if (ch == "}")
+ stack.push("{");
+ else if (/[\(\{\[]/.test(ch) && (!stack.length || stack.pop() != ch))
+ return cm.extendSelection(Pos(line, i));
+ }
+ --line; ch = null;
+ }
+ }
+
+ function quit(cm) {
+ cm.execCommand("clearSearch");
+ clearMark(cm);
+ }
+
+ // Actual keymap
+
+ var keyMap = CodeMirror.keyMap.emacs = CodeMirror.normalizeKeyMap({
+ "Ctrl-W": function(cm) {kill(cm, cm.getCursor("start"), cm.getCursor("end"));},
+ "Ctrl-K": repeated(function(cm) {
+ var start = cm.getCursor(), end = cm.clipPos(Pos(start.line));
+ var text = cm.getRange(start, end);
+ if (!/\S/.test(text)) {
+ text += "\n";
+ end = Pos(start.line + 1, 0);
+ }
+ kill(cm, start, end, true, text);
+ }),
+ "Alt-W": function(cm) {
+ addToRing(cm.getSelection());
+ clearMark(cm);
+ },
+ "Ctrl-Y": function(cm) {
+ var start = cm.getCursor();
+ cm.replaceRange(getFromRing(getPrefix(cm)), start, start, "paste");
+ cm.setSelection(start, cm.getCursor());
+ },
+ "Alt-Y": function(cm) {cm.replaceSelection(popFromRing(), "around", "paste");},
+
+ "Ctrl-Space": setMark, "Ctrl-Shift-2": setMark,
+
+ "Ctrl-F": move(byChar, 1), "Ctrl-B": move(byChar, -1),
+ "Right": move(byChar, 1), "Left": move(byChar, -1),
+ "Ctrl-D": function(cm) { killTo(cm, byChar, 1); },
+ "Delete": function(cm) { killRegion(cm) || killTo(cm, byChar, 1); },
+ "Ctrl-H": function(cm) { killTo(cm, byChar, -1); },
+ "Backspace": function(cm) { killRegion(cm) || killTo(cm, byChar, -1); },
+
+ "Alt-F": move(byWord, 1), "Alt-B": move(byWord, -1),
+ "Alt-D": function(cm) { killTo(cm, byWord, 1); },
+ "Alt-Backspace": function(cm) { killTo(cm, byWord, -1); },
+
+ "Ctrl-N": move(byLine, 1), "Ctrl-P": move(byLine, -1),
+ "Down": move(byLine, 1), "Up": move(byLine, -1),
+ "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd",
+ "End": "goLineEnd", "Home": "goLineStart",
+
+ "Alt-V": move(byPage, -1), "Ctrl-V": move(byPage, 1),
+ "PageUp": move(byPage, -1), "PageDown": move(byPage, 1),
+
+ "Ctrl-Up": move(byParagraph, -1), "Ctrl-Down": move(byParagraph, 1),
+
+ "Alt-A": move(bySentence, -1), "Alt-E": move(bySentence, 1),
+ "Alt-K": function(cm) { killTo(cm, bySentence, 1); },
+
+ "Ctrl-Alt-K": function(cm) { killTo(cm, byExpr, 1); },
+ "Ctrl-Alt-Backspace": function(cm) { killTo(cm, byExpr, -1); },
+ "Ctrl-Alt-F": move(byExpr, 1), "Ctrl-Alt-B": move(byExpr, -1),
+
+ "Shift-Ctrl-Alt-2": function(cm) {
+ var cursor = cm.getCursor();
+ cm.setSelection(findEnd(cm, cursor, byExpr, 1), cursor);
+ },
+ "Ctrl-Alt-T": function(cm) {
+ var leftStart = byExpr(cm, cm.getCursor(), -1), leftEnd = byExpr(cm, leftStart, 1);
+ var rightEnd = byExpr(cm, leftEnd, 1), rightStart = byExpr(cm, rightEnd, -1);
+ cm.replaceRange(cm.getRange(rightStart, rightEnd) + cm.getRange(leftEnd, rightStart) +
+ cm.getRange(leftStart, leftEnd), leftStart, rightEnd);
+ },
+ "Ctrl-Alt-U": repeated(toEnclosingExpr),
+
+ "Alt-Space": function(cm) {
+ var pos = cm.getCursor(), from = pos.ch, to = pos.ch, text = cm.getLine(pos.line);
+ while (from && /\s/.test(text.charAt(from - 1))) --from;
+ while (to < text.length && /\s/.test(text.charAt(to))) ++to;
+ cm.replaceRange(" ", Pos(pos.line, from), Pos(pos.line, to));
+ },
+ "Ctrl-O": repeated(function(cm) { cm.replaceSelection("\n", "start"); }),
+ "Ctrl-T": repeated(function(cm) {
+ cm.execCommand("transposeChars");
+ }),
+
+ "Alt-C": repeated(function(cm) {
+ operateOnWord(cm, function(w) {
+ var letter = w.search(/\w/);
+ if (letter == -1) return w;
+ return w.slice(0, letter) + w.charAt(letter).toUpperCase() + w.slice(letter + 1).toLowerCase();
+ });
+ }),
+ "Alt-U": repeated(function(cm) {
+ operateOnWord(cm, function(w) { return w.toUpperCase(); });
+ }),
+ "Alt-L": repeated(function(cm) {
+ operateOnWord(cm, function(w) { return w.toLowerCase(); });
+ }),
+
+ "Alt-;": "toggleComment",
+
+ "Ctrl-/": repeated("undo"), "Shift-Ctrl--": repeated("undo"),
+ "Ctrl-Z": repeated("undo"), "Cmd-Z": repeated("undo"),
+ "Shift-Alt-,": "goDocStart", "Shift-Alt-.": "goDocEnd",
+ "Ctrl-S": "findNext", "Ctrl-R": "findPrev", "Ctrl-G": quit, "Shift-Alt-5": "replace",
+ "Alt-/": "autocomplete",
+ "Ctrl-J": "newlineAndIndent", "Enter": false, "Tab": "indentAuto",
+
+ "Alt-G G": function(cm) {
+ var prefix = getPrefix(cm, true);
+ if (prefix != null && prefix > 0) return cm.setCursor(prefix - 1);
+
+ getInput(cm, "Goto line", function(str) {
+ var num;
+ if (str && !isNaN(num = Number(str)) && num == (num|0) && num > 0)
+ cm.setCursor(num - 1);
+ });
+ },
+
+ "Ctrl-X Tab": function(cm) {
+ cm.indentSelection(getPrefix(cm, true) || cm.getOption("indentUnit"));
+ },
+ "Ctrl-X Ctrl-X": function(cm) {
+ cm.setSelection(cm.getCursor("head"), cm.getCursor("anchor"));
+ },
+ "Ctrl-X Ctrl-S": "save",
+ "Ctrl-X Ctrl-W": "save",
+ "Ctrl-X S": "saveAll",
+ "Ctrl-X F": "open",
+ "Ctrl-X U": repeated("undo"),
+ "Ctrl-X K": "close",
+ "Ctrl-X Delete": function(cm) { kill(cm, cm.getCursor(), bySentence(cm, cm.getCursor(), 1), true); },
+ "Ctrl-X H": "selectAll",
+
+ "Ctrl-Q Tab": repeated("insertTab"),
+ "Ctrl-U": addPrefixMap
+ });
+
+ var prefixMap = {"Ctrl-G": clearPrefix};
+ function regPrefix(d) {
+ prefixMap[d] = function(cm) { addPrefix(cm, d); };
+ keyMap["Ctrl-" + d] = function(cm) { addPrefix(cm, d); };
+ prefixPreservingKeys["Ctrl-" + d] = true;
+ }
+ for (var i = 0; i < 10; ++i) regPrefix(String(i));
+ regPrefix("-");
+});
diff --git a/devtools/client/sourceeditor/codemirror/keymap/sublime.js b/devtools/client/sourceeditor/codemirror/keymap/sublime.js
new file mode 100644
index 000000000..ed6b84742
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/keymap/sublime.js
@@ -0,0 +1,580 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+// A rough approximation of Sublime Text's keybindings
+// Depends on addon/search/searchcursor.js and optionally addon/dialog/dialogs.js
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../lib/codemirror"), require("../addon/search/searchcursor"), require("../addon/edit/matchbrackets"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../lib/codemirror", "../addon/search/searchcursor", "../addon/edit/matchbrackets"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ "use strict";
+
+ var map = CodeMirror.keyMap.sublime = {fallthrough: "default"};
+ var cmds = CodeMirror.commands;
+ var Pos = CodeMirror.Pos;
+ var mac = CodeMirror.keyMap["default"] == CodeMirror.keyMap.macDefault;
+ var ctrl = mac ? "Cmd-" : "Ctrl-";
+
+ // This is not exactly Sublime's algorithm. I couldn't make heads or tails of that.
+ function findPosSubword(doc, start, dir) {
+ if (dir < 0 && start.ch == 0) return doc.clipPos(Pos(start.line - 1));
+ var line = doc.getLine(start.line);
+ if (dir > 0 && start.ch >= line.length) return doc.clipPos(Pos(start.line + 1, 0));
+ var state = "start", type;
+ for (var pos = start.ch, e = dir < 0 ? 0 : line.length, i = 0; pos != e; pos += dir, i++) {
+ var next = line.charAt(dir < 0 ? pos - 1 : pos);
+ var cat = next != "_" && CodeMirror.isWordChar(next) ? "w" : "o";
+ if (cat == "w" && next.toUpperCase() == next) cat = "W";
+ if (state == "start") {
+ if (cat != "o") { state = "in"; type = cat; }
+ } else if (state == "in") {
+ if (type != cat) {
+ if (type == "w" && cat == "W" && dir < 0) pos--;
+ if (type == "W" && cat == "w" && dir > 0) { type = "w"; continue; }
+ break;
+ }
+ }
+ }
+ return Pos(start.line, pos);
+ }
+
+ function moveSubword(cm, dir) {
+ cm.extendSelectionsBy(function(range) {
+ if (cm.display.shift || cm.doc.extend || range.empty())
+ return findPosSubword(cm.doc, range.head, dir);
+ else
+ return dir < 0 ? range.from() : range.to();
+ });
+ }
+
+ cmds[map["Alt-Left"] = "goSubwordLeft"] = function(cm) { moveSubword(cm, -1); };
+ cmds[map["Alt-Right"] = "goSubwordRight"] = function(cm) { moveSubword(cm, 1); };
+
+ if (mac) map["Cmd-Left"] = "goLineStartSmart";
+
+ var scrollLineCombo = mac ? "Ctrl-Alt-" : "Ctrl-";
+
+ cmds[map[scrollLineCombo + "Up"] = "scrollLineUp"] = function(cm) {
+ var info = cm.getScrollInfo();
+ if (!cm.somethingSelected()) {
+ var visibleBottomLine = cm.lineAtHeight(info.top + info.clientHeight, "local");
+ if (cm.getCursor().line >= visibleBottomLine)
+ cm.execCommand("goLineUp");
+ }
+ cm.scrollTo(null, info.top - cm.defaultTextHeight());
+ };
+ cmds[map[scrollLineCombo + "Down"] = "scrollLineDown"] = function(cm) {
+ var info = cm.getScrollInfo();
+ if (!cm.somethingSelected()) {
+ var visibleTopLine = cm.lineAtHeight(info.top, "local")+1;
+ if (cm.getCursor().line <= visibleTopLine)
+ cm.execCommand("goLineDown");
+ }
+ cm.scrollTo(null, info.top + cm.defaultTextHeight());
+ };
+
+ cmds[map["Shift-" + ctrl + "L"] = "splitSelectionByLine"] = function(cm) {
+ var ranges = cm.listSelections(), lineRanges = [];
+ for (var i = 0; i < ranges.length; i++) {
+ var from = ranges[i].from(), to = ranges[i].to();
+ for (var line = from.line; line <= to.line; ++line)
+ if (!(to.line > from.line && line == to.line && to.ch == 0))
+ lineRanges.push({anchor: line == from.line ? from : Pos(line, 0),
+ head: line == to.line ? to : Pos(line)});
+ }
+ cm.setSelections(lineRanges, 0);
+ };
+
+ map["Shift-Tab"] = "indentLess";
+
+ cmds[map["Esc"] = "singleSelectionTop"] = function(cm) {
+ var range = cm.listSelections()[0];
+ cm.setSelection(range.anchor, range.head, {scroll: false});
+ };
+
+ cmds[map[ctrl + "L"] = "selectLine"] = function(cm) {
+ var ranges = cm.listSelections(), extended = [];
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i];
+ extended.push({anchor: Pos(range.from().line, 0),
+ head: Pos(range.to().line + 1, 0)});
+ }
+ cm.setSelections(extended);
+ };
+
+ map["Shift-Ctrl-K"] = "deleteLine";
+
+ function insertLine(cm, above) {
+ if (cm.isReadOnly()) return CodeMirror.Pass
+ cm.operation(function() {
+ var len = cm.listSelections().length, newSelection = [], last = -1;
+ for (var i = 0; i < len; i++) {
+ var head = cm.listSelections()[i].head;
+ if (head.line <= last) continue;
+ var at = Pos(head.line + (above ? 0 : 1), 0);
+ cm.replaceRange("\n", at, null, "+insertLine");
+ cm.indentLine(at.line, null, true);
+ newSelection.push({head: at, anchor: at});
+ last = head.line + 1;
+ }
+ cm.setSelections(newSelection);
+ });
+ cm.execCommand("indentAuto");
+ }
+
+ cmds[map[ctrl + "Enter"] = "insertLineAfter"] = function(cm) { return insertLine(cm, false); };
+
+ cmds[map["Shift-" + ctrl + "Enter"] = "insertLineBefore"] = function(cm) { return insertLine(cm, true); };
+
+ function wordAt(cm, pos) {
+ var start = pos.ch, end = start, line = cm.getLine(pos.line);
+ while (start && CodeMirror.isWordChar(line.charAt(start - 1))) --start;
+ while (end < line.length && CodeMirror.isWordChar(line.charAt(end))) ++end;
+ return {from: Pos(pos.line, start), to: Pos(pos.line, end), word: line.slice(start, end)};
+ }
+
+ cmds[map[ctrl + "D"] = "selectNextOccurrence"] = function(cm) {
+ var from = cm.getCursor("from"), to = cm.getCursor("to");
+ var fullWord = cm.state.sublimeFindFullWord == cm.doc.sel;
+ if (CodeMirror.cmpPos(from, to) == 0) {
+ var word = wordAt(cm, from);
+ if (!word.word) return;
+ cm.setSelection(word.from, word.to);
+ fullWord = true;
+ } else {
+ var text = cm.getRange(from, to);
+ var query = fullWord ? new RegExp("\\b" + text + "\\b") : text;
+ var cur = cm.getSearchCursor(query, to);
+ if (cur.findNext()) {
+ cm.addSelection(cur.from(), cur.to());
+ } else {
+ cur = cm.getSearchCursor(query, Pos(cm.firstLine(), 0));
+ if (cur.findNext())
+ cm.addSelection(cur.from(), cur.to());
+ }
+ }
+ if (fullWord)
+ cm.state.sublimeFindFullWord = cm.doc.sel;
+ };
+
+ var mirror = "(){}[]";
+ function selectBetweenBrackets(cm) {
+ var pos = cm.getCursor(), opening = cm.scanForBracket(pos, -1);
+ if (!opening) return;
+ for (;;) {
+ var closing = cm.scanForBracket(pos, 1);
+ if (!closing) return;
+ if (closing.ch == mirror.charAt(mirror.indexOf(opening.ch) + 1)) {
+ cm.setSelection(Pos(opening.pos.line, opening.pos.ch + 1), closing.pos, false);
+ return true;
+ }
+ pos = Pos(closing.pos.line, closing.pos.ch + 1);
+ }
+ }
+
+ cmds[map["Shift-" + ctrl + "Space"] = "selectScope"] = function(cm) {
+ selectBetweenBrackets(cm) || cm.execCommand("selectAll");
+ };
+ cmds[map["Shift-" + ctrl + "M"] = "selectBetweenBrackets"] = function(cm) {
+ if (!selectBetweenBrackets(cm)) return CodeMirror.Pass;
+ };
+
+ cmds[map[ctrl + "M"] = "goToBracket"] = function(cm) {
+ cm.extendSelectionsBy(function(range) {
+ var next = cm.scanForBracket(range.head, 1);
+ if (next && CodeMirror.cmpPos(next.pos, range.head) != 0) return next.pos;
+ var prev = cm.scanForBracket(range.head, -1);
+ return prev && Pos(prev.pos.line, prev.pos.ch + 1) || range.head;
+ });
+ };
+
+ var swapLineCombo = mac ? "Cmd-Ctrl-" : "Shift-Ctrl-";
+
+ cmds[map[swapLineCombo + "Up"] = "swapLineUp"] = function(cm) {
+ if (cm.isReadOnly()) return CodeMirror.Pass
+ var ranges = cm.listSelections(), linesToMove = [], at = cm.firstLine() - 1, newSels = [];
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i], from = range.from().line - 1, to = range.to().line;
+ newSels.push({anchor: Pos(range.anchor.line - 1, range.anchor.ch),
+ head: Pos(range.head.line - 1, range.head.ch)});
+ if (range.to().ch == 0 && !range.empty()) --to;
+ if (from > at) linesToMove.push(from, to);
+ else if (linesToMove.length) linesToMove[linesToMove.length - 1] = to;
+ at = to;
+ }
+ cm.operation(function() {
+ for (var i = 0; i < linesToMove.length; i += 2) {
+ var from = linesToMove[i], to = linesToMove[i + 1];
+ var line = cm.getLine(from);
+ cm.replaceRange("", Pos(from, 0), Pos(from + 1, 0), "+swapLine");
+ if (to > cm.lastLine())
+ cm.replaceRange("\n" + line, Pos(cm.lastLine()), null, "+swapLine");
+ else
+ cm.replaceRange(line + "\n", Pos(to, 0), null, "+swapLine");
+ }
+ cm.setSelections(newSels);
+ cm.scrollIntoView();
+ });
+ };
+
+ cmds[map[swapLineCombo + "Down"] = "swapLineDown"] = function(cm) {
+ if (cm.isReadOnly()) return CodeMirror.Pass
+ var ranges = cm.listSelections(), linesToMove = [], at = cm.lastLine() + 1;
+ for (var i = ranges.length - 1; i >= 0; i--) {
+ var range = ranges[i], from = range.to().line + 1, to = range.from().line;
+ if (range.to().ch == 0 && !range.empty()) from--;
+ if (from < at) linesToMove.push(from, to);
+ else if (linesToMove.length) linesToMove[linesToMove.length - 1] = to;
+ at = to;
+ }
+ cm.operation(function() {
+ for (var i = linesToMove.length - 2; i >= 0; i -= 2) {
+ var from = linesToMove[i], to = linesToMove[i + 1];
+ var line = cm.getLine(from);
+ if (from == cm.lastLine())
+ cm.replaceRange("", Pos(from - 1), Pos(from), "+swapLine");
+ else
+ cm.replaceRange("", Pos(from, 0), Pos(from + 1, 0), "+swapLine");
+ cm.replaceRange(line + "\n", Pos(to, 0), null, "+swapLine");
+ }
+ cm.scrollIntoView();
+ });
+ };
+
+ cmds[map[ctrl + "/"] = "toggleCommentIndented"] = function(cm) {
+ cm.toggleComment({ indent: true });
+ }
+
+ cmds[map[ctrl + "J"] = "joinLines"] = function(cm) {
+ var ranges = cm.listSelections(), joined = [];
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i], from = range.from();
+ var start = from.line, end = range.to().line;
+ while (i < ranges.length - 1 && ranges[i + 1].from().line == end)
+ end = ranges[++i].to().line;
+ joined.push({start: start, end: end, anchor: !range.empty() && from});
+ }
+ cm.operation(function() {
+ var offset = 0, ranges = [];
+ for (var i = 0; i < joined.length; i++) {
+ var obj = joined[i];
+ var anchor = obj.anchor && Pos(obj.anchor.line - offset, obj.anchor.ch), head;
+ for (var line = obj.start; line <= obj.end; line++) {
+ var actual = line - offset;
+ if (line == obj.end) head = Pos(actual, cm.getLine(actual).length + 1);
+ if (actual < cm.lastLine()) {
+ cm.replaceRange(" ", Pos(actual), Pos(actual + 1, /^\s*/.exec(cm.getLine(actual + 1))[0].length));
+ ++offset;
+ }
+ }
+ ranges.push({anchor: anchor || head, head: head});
+ }
+ cm.setSelections(ranges, 0);
+ });
+ };
+
+ cmds[map["Shift-" + ctrl + "D"] = "duplicateLine"] = function(cm) {
+ cm.operation(function() {
+ var rangeCount = cm.listSelections().length;
+ for (var i = 0; i < rangeCount; i++) {
+ var range = cm.listSelections()[i];
+ if (range.empty())
+ cm.replaceRange(cm.getLine(range.head.line) + "\n", Pos(range.head.line, 0));
+ else
+ cm.replaceRange(cm.getRange(range.from(), range.to()), range.from());
+ }
+ cm.scrollIntoView();
+ });
+ };
+
+ map[ctrl + "T"] = "transposeChars";
+
+ function sortLines(cm, caseSensitive) {
+ if (cm.isReadOnly()) return CodeMirror.Pass
+ var ranges = cm.listSelections(), toSort = [], selected;
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i];
+ if (range.empty()) continue;
+ var from = range.from().line, to = range.to().line;
+ while (i < ranges.length - 1 && ranges[i + 1].from().line == to)
+ to = range[++i].to().line;
+ toSort.push(from, to);
+ }
+ if (toSort.length) selected = true;
+ else toSort.push(cm.firstLine(), cm.lastLine());
+
+ cm.operation(function() {
+ var ranges = [];
+ for (var i = 0; i < toSort.length; i += 2) {
+ var from = toSort[i], to = toSort[i + 1];
+ var start = Pos(from, 0), end = Pos(to);
+ var lines = cm.getRange(start, end, false);
+ if (caseSensitive)
+ lines.sort();
+ else
+ lines.sort(function(a, b) {
+ var au = a.toUpperCase(), bu = b.toUpperCase();
+ if (au != bu) { a = au; b = bu; }
+ return a < b ? -1 : a == b ? 0 : 1;
+ });
+ cm.replaceRange(lines, start, end);
+ if (selected) ranges.push({anchor: start, head: end});
+ }
+ if (selected) cm.setSelections(ranges, 0);
+ });
+ }
+
+ cmds[map["F9"] = "sortLines"] = function(cm) { sortLines(cm, true); };
+ cmds[map[ctrl + "F9"] = "sortLinesInsensitive"] = function(cm) { sortLines(cm, false); };
+
+ cmds[map["F2"] = "nextBookmark"] = function(cm) {
+ var marks = cm.state.sublimeBookmarks;
+ if (marks) while (marks.length) {
+ var current = marks.shift();
+ var found = current.find();
+ if (found) {
+ marks.push(current);
+ return cm.setSelection(found.from, found.to);
+ }
+ }
+ };
+
+ cmds[map["Shift-F2"] = "prevBookmark"] = function(cm) {
+ var marks = cm.state.sublimeBookmarks;
+ if (marks) while (marks.length) {
+ marks.unshift(marks.pop());
+ var found = marks[marks.length - 1].find();
+ if (!found)
+ marks.pop();
+ else
+ return cm.setSelection(found.from, found.to);
+ }
+ };
+
+ cmds[map[ctrl + "F2"] = "toggleBookmark"] = function(cm) {
+ var ranges = cm.listSelections();
+ var marks = cm.state.sublimeBookmarks || (cm.state.sublimeBookmarks = []);
+ for (var i = 0; i < ranges.length; i++) {
+ var from = ranges[i].from(), to = ranges[i].to();
+ var found = cm.findMarks(from, to);
+ for (var j = 0; j < found.length; j++) {
+ if (found[j].sublimeBookmark) {
+ found[j].clear();
+ for (var k = 0; k < marks.length; k++)
+ if (marks[k] == found[j])
+ marks.splice(k--, 1);
+ break;
+ }
+ }
+ if (j == found.length)
+ marks.push(cm.markText(from, to, {sublimeBookmark: true, clearWhenEmpty: false}));
+ }
+ };
+
+ cmds[map["Shift-" + ctrl + "F2"] = "clearBookmarks"] = function(cm) {
+ var marks = cm.state.sublimeBookmarks;
+ if (marks) for (var i = 0; i < marks.length; i++) marks[i].clear();
+ marks.length = 0;
+ };
+
+ cmds[map["Alt-F2"] = "selectBookmarks"] = function(cm) {
+ var marks = cm.state.sublimeBookmarks, ranges = [];
+ if (marks) for (var i = 0; i < marks.length; i++) {
+ var found = marks[i].find();
+ if (!found)
+ marks.splice(i--, 0);
+ else
+ ranges.push({anchor: found.from, head: found.to});
+ }
+ if (ranges.length)
+ cm.setSelections(ranges, 0);
+ };
+
+ map["Alt-Q"] = "wrapLines";
+
+ var cK = ctrl + "K ";
+
+ function modifyWordOrSelection(cm, mod) {
+ cm.operation(function() {
+ var ranges = cm.listSelections(), indices = [], replacements = [];
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i];
+ if (range.empty()) { indices.push(i); replacements.push(""); }
+ else replacements.push(mod(cm.getRange(range.from(), range.to())));
+ }
+ cm.replaceSelections(replacements, "around", "case");
+ for (var i = indices.length - 1, at; i >= 0; i--) {
+ var range = ranges[indices[i]];
+ if (at && CodeMirror.cmpPos(range.head, at) > 0) continue;
+ var word = wordAt(cm, range.head);
+ at = word.from;
+ cm.replaceRange(mod(word.word), word.from, word.to);
+ }
+ });
+ }
+
+ map[cK + ctrl + "Backspace"] = "delLineLeft";
+
+ cmds[map["Backspace"] = "smartBackspace"] = function(cm) {
+ if (cm.somethingSelected()) return CodeMirror.Pass;
+
+ cm.operation(function() {
+ var cursors = cm.listSelections();
+ var indentUnit = cm.getOption("indentUnit");
+
+ for (var i = cursors.length - 1; i >= 0; i--) {
+ var cursor = cursors[i].head;
+ var toStartOfLine = cm.getRange({line: cursor.line, ch: 0}, cursor);
+ var column = CodeMirror.countColumn(toStartOfLine, null, cm.getOption("tabSize"));
+
+ // Delete by one character by default
+ var deletePos = cm.findPosH(cursor, -1, "char", false);
+
+ if (toStartOfLine && !/\S/.test(toStartOfLine) && column % indentUnit == 0) {
+ var prevIndent = new Pos(cursor.line,
+ CodeMirror.findColumn(toStartOfLine, column - indentUnit, indentUnit));
+
+ // Smart delete only if we found a valid prevIndent location
+ if (prevIndent.ch != cursor.ch) deletePos = prevIndent;
+ }
+
+ cm.replaceRange("", deletePos, cursor, "+delete");
+ }
+ });
+ };
+
+ cmds[map[cK + ctrl + "K"] = "delLineRight"] = function(cm) {
+ cm.operation(function() {
+ var ranges = cm.listSelections();
+ for (var i = ranges.length - 1; i >= 0; i--)
+ cm.replaceRange("", ranges[i].anchor, Pos(ranges[i].to().line), "+delete");
+ cm.scrollIntoView();
+ });
+ };
+
+ cmds[map[cK + ctrl + "U"] = "upcaseAtCursor"] = function(cm) {
+ modifyWordOrSelection(cm, function(str) { return str.toUpperCase(); });
+ };
+ cmds[map[cK + ctrl + "L"] = "downcaseAtCursor"] = function(cm) {
+ modifyWordOrSelection(cm, function(str) { return str.toLowerCase(); });
+ };
+
+ cmds[map[cK + ctrl + "Space"] = "setSublimeMark"] = function(cm) {
+ if (cm.state.sublimeMark) cm.state.sublimeMark.clear();
+ cm.state.sublimeMark = cm.setBookmark(cm.getCursor());
+ };
+ cmds[map[cK + ctrl + "A"] = "selectToSublimeMark"] = function(cm) {
+ var found = cm.state.sublimeMark && cm.state.sublimeMark.find();
+ if (found) cm.setSelection(cm.getCursor(), found);
+ };
+ cmds[map[cK + ctrl + "W"] = "deleteToSublimeMark"] = function(cm) {
+ var found = cm.state.sublimeMark && cm.state.sublimeMark.find();
+ if (found) {
+ var from = cm.getCursor(), to = found;
+ if (CodeMirror.cmpPos(from, to) > 0) { var tmp = to; to = from; from = tmp; }
+ cm.state.sublimeKilled = cm.getRange(from, to);
+ cm.replaceRange("", from, to);
+ }
+ };
+ cmds[map[cK + ctrl + "X"] = "swapWithSublimeMark"] = function(cm) {
+ var found = cm.state.sublimeMark && cm.state.sublimeMark.find();
+ if (found) {
+ cm.state.sublimeMark.clear();
+ cm.state.sublimeMark = cm.setBookmark(cm.getCursor());
+ cm.setCursor(found);
+ }
+ };
+ cmds[map[cK + ctrl + "Y"] = "sublimeYank"] = function(cm) {
+ if (cm.state.sublimeKilled != null)
+ cm.replaceSelection(cm.state.sublimeKilled, null, "paste");
+ };
+
+ map[cK + ctrl + "G"] = "clearBookmarks";
+ cmds[map[cK + ctrl + "C"] = "showInCenter"] = function(cm) {
+ var pos = cm.cursorCoords(null, "local");
+ cm.scrollTo(null, (pos.top + pos.bottom) / 2 - cm.getScrollInfo().clientHeight / 2);
+ };
+
+ var selectLinesCombo = mac ? "Ctrl-Shift-" : "Ctrl-Alt-";
+ cmds[map[selectLinesCombo + "Up"] = "selectLinesUpward"] = function(cm) {
+ cm.operation(function() {
+ var ranges = cm.listSelections();
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i];
+ if (range.head.line > cm.firstLine())
+ cm.addSelection(Pos(range.head.line - 1, range.head.ch));
+ }
+ });
+ };
+ cmds[map[selectLinesCombo + "Down"] = "selectLinesDownward"] = function(cm) {
+ cm.operation(function() {
+ var ranges = cm.listSelections();
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i];
+ if (range.head.line < cm.lastLine())
+ cm.addSelection(Pos(range.head.line + 1, range.head.ch));
+ }
+ });
+ };
+
+ function getTarget(cm) {
+ var from = cm.getCursor("from"), to = cm.getCursor("to");
+ if (CodeMirror.cmpPos(from, to) == 0) {
+ var word = wordAt(cm, from);
+ if (!word.word) return;
+ from = word.from;
+ to = word.to;
+ }
+ return {from: from, to: to, query: cm.getRange(from, to), word: word};
+ }
+
+ function findAndGoTo(cm, forward) {
+ var target = getTarget(cm);
+ if (!target) return;
+ var query = target.query;
+ var cur = cm.getSearchCursor(query, forward ? target.to : target.from);
+
+ if (forward ? cur.findNext() : cur.findPrevious()) {
+ cm.setSelection(cur.from(), cur.to());
+ } else {
+ cur = cm.getSearchCursor(query, forward ? Pos(cm.firstLine(), 0)
+ : cm.clipPos(Pos(cm.lastLine())));
+ if (forward ? cur.findNext() : cur.findPrevious())
+ cm.setSelection(cur.from(), cur.to());
+ else if (target.word)
+ cm.setSelection(target.from, target.to);
+ }
+ };
+ cmds[map[ctrl + "F3"] = "findUnder"] = function(cm) { findAndGoTo(cm, true); };
+ cmds[map["Shift-" + ctrl + "F3"] = "findUnderPrevious"] = function(cm) { findAndGoTo(cm,false); };
+ cmds[map["Alt-F3"] = "findAllUnder"] = function(cm) {
+ var target = getTarget(cm);
+ if (!target) return;
+ var cur = cm.getSearchCursor(target.query);
+ var matches = [];
+ var primaryIndex = -1;
+ while (cur.findNext()) {
+ matches.push({anchor: cur.from(), head: cur.to()});
+ if (cur.from().line <= target.from.line && cur.from().ch <= target.from.ch)
+ primaryIndex++;
+ }
+ cm.setSelections(matches, primaryIndex);
+ };
+
+ map["Shift-" + ctrl + "["] = "fold";
+ map["Shift-" + ctrl + "]"] = "unfold";
+ map[cK + ctrl + "0"] = map[cK + ctrl + "j"] = "unfoldAll";
+
+ map[ctrl + "I"] = "findIncremental";
+ map["Shift-" + ctrl + "I"] = "findIncrementalReverse";
+ map[ctrl + "H"] = "replace";
+ map["F3"] = "findNext";
+ map["Shift-F3"] = "findPrev";
+
+ CodeMirror.normalizeKeyMap(map);
+});
diff --git a/devtools/client/sourceeditor/codemirror/keymap/vim.js b/devtools/client/sourceeditor/codemirror/keymap/vim.js
new file mode 100644
index 000000000..4278ee979
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/keymap/vim.js
@@ -0,0 +1,5065 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+/**
+ * Supported keybindings:
+ * Too many to list. Refer to defaultKeyMap below.
+ *
+ * Supported Ex commands:
+ * Refer to defaultExCommandMap below.
+ *
+ * Registers: unnamed, -, a-z, A-Z, 0-9
+ * (Does not respect the special case for number registers when delete
+ * operator is made with these commands: %, (, ), , /, ?, n, N, {, } )
+ * TODO: Implement the remaining registers.
+ *
+ * Marks: a-z, A-Z, and 0-9
+ * TODO: Implement the remaining special marks. They have more complex
+ * behavior.
+ *
+ * Events:
+ * 'vim-mode-change' - raised on the editor anytime the current mode changes,
+ * Event object: {mode: "visual", subMode: "linewise"}
+ *
+ * Code structure:
+ * 1. Default keymap
+ * 2. Variable declarations and short basic helpers
+ * 3. Instance (External API) implementation
+ * 4. Internal state tracking objects (input state, counter) implementation
+ * and instantiation
+ * 5. Key handler (the main command dispatcher) implementation
+ * 6. Motion, operator, and action implementations
+ * 7. Helper functions for the key handler, motions, operators, and actions
+ * 8. Set up Vim to work as a keymap for CodeMirror.
+ * 9. Ex command implementations.
+ */
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../lib/codemirror"), require("../addon/search/searchcursor"), require("../addon/dialog/dialog"), require("../addon/edit/matchbrackets.js"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../lib/codemirror", "../addon/search/searchcursor", "../addon/dialog/dialog", "../addon/edit/matchbrackets"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ 'use strict';
+
+ var defaultKeymap = [
+ // Key to key mapping. This goes first to make it possible to override
+ // existing mappings.
+ { keys: '<Left>', type: 'keyToKey', toKeys: 'h' },
+ { keys: '<Right>', type: 'keyToKey', toKeys: 'l' },
+ { keys: '<Up>', type: 'keyToKey', toKeys: 'k' },
+ { keys: '<Down>', type: 'keyToKey', toKeys: 'j' },
+ { keys: '<Space>', type: 'keyToKey', toKeys: 'l' },
+ { keys: '<BS>', type: 'keyToKey', toKeys: 'h', context: 'normal'},
+ { keys: '<C-Space>', type: 'keyToKey', toKeys: 'W' },
+ { keys: '<C-BS>', type: 'keyToKey', toKeys: 'B', context: 'normal' },
+ { keys: '<S-Space>', type: 'keyToKey', toKeys: 'w' },
+ { keys: '<S-BS>', type: 'keyToKey', toKeys: 'b', context: 'normal' },
+ { keys: '<C-n>', type: 'keyToKey', toKeys: 'j' },
+ { keys: '<C-p>', type: 'keyToKey', toKeys: 'k' },
+ { keys: '<C-[>', type: 'keyToKey', toKeys: '<Esc>' },
+ { keys: '<C-c>', type: 'keyToKey', toKeys: '<Esc>' },
+ { keys: '<C-[>', type: 'keyToKey', toKeys: '<Esc>', context: 'insert' },
+ { keys: '<C-c>', type: 'keyToKey', toKeys: '<Esc>', context: 'insert' },
+ { keys: 's', type: 'keyToKey', toKeys: 'cl', context: 'normal' },
+ { keys: 's', type: 'keyToKey', toKeys: 'c', context: 'visual'},
+ { keys: 'S', type: 'keyToKey', toKeys: 'cc', context: 'normal' },
+ { keys: 'S', type: 'keyToKey', toKeys: 'VdO', context: 'visual' },
+ { keys: '<Home>', type: 'keyToKey', toKeys: '0' },
+ { keys: '<End>', type: 'keyToKey', toKeys: '$' },
+ { keys: '<PageUp>', type: 'keyToKey', toKeys: '<C-b>' },
+ { keys: '<PageDown>', type: 'keyToKey', toKeys: '<C-f>' },
+ { keys: '<CR>', type: 'keyToKey', toKeys: 'j^', context: 'normal' },
+ // Motions
+ { keys: 'H', type: 'motion', motion: 'moveToTopLine', motionArgs: { linewise: true, toJumplist: true }},
+ { keys: 'M', type: 'motion', motion: 'moveToMiddleLine', motionArgs: { linewise: true, toJumplist: true }},
+ { keys: 'L', type: 'motion', motion: 'moveToBottomLine', motionArgs: { linewise: true, toJumplist: true }},
+ { keys: 'h', type: 'motion', motion: 'moveByCharacters', motionArgs: { forward: false }},
+ { keys: 'l', type: 'motion', motion: 'moveByCharacters', motionArgs: { forward: true }},
+ { keys: 'j', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, linewise: true }},
+ { keys: 'k', type: 'motion', motion: 'moveByLines', motionArgs: { forward: false, linewise: true }},
+ { keys: 'gj', type: 'motion', motion: 'moveByDisplayLines', motionArgs: { forward: true }},
+ { keys: 'gk', type: 'motion', motion: 'moveByDisplayLines', motionArgs: { forward: false }},
+ { keys: 'w', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: false }},
+ { keys: 'W', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: false, bigWord: true }},
+ { keys: 'e', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: true, inclusive: true }},
+ { keys: 'E', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: true, bigWord: true, inclusive: true }},
+ { keys: 'b', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false }},
+ { keys: 'B', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false, bigWord: true }},
+ { keys: 'ge', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: true, inclusive: true }},
+ { keys: 'gE', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: true, bigWord: true, inclusive: true }},
+ { keys: '{', type: 'motion', motion: 'moveByParagraph', motionArgs: { forward: false, toJumplist: true }},
+ { keys: '}', type: 'motion', motion: 'moveByParagraph', motionArgs: { forward: true, toJumplist: true }},
+ { keys: '<C-f>', type: 'motion', motion: 'moveByPage', motionArgs: { forward: true }},
+ { keys: '<C-b>', type: 'motion', motion: 'moveByPage', motionArgs: { forward: false }},
+ { keys: '<C-d>', type: 'motion', motion: 'moveByScroll', motionArgs: { forward: true, explicitRepeat: true }},
+ { keys: '<C-u>', type: 'motion', motion: 'moveByScroll', motionArgs: { forward: false, explicitRepeat: true }},
+ { keys: 'gg', type: 'motion', motion: 'moveToLineOrEdgeOfDocument', motionArgs: { forward: false, explicitRepeat: true, linewise: true, toJumplist: true }},
+ { keys: 'G', type: 'motion', motion: 'moveToLineOrEdgeOfDocument', motionArgs: { forward: true, explicitRepeat: true, linewise: true, toJumplist: true }},
+ { keys: '0', type: 'motion', motion: 'moveToStartOfLine' },
+ { keys: '^', type: 'motion', motion: 'moveToFirstNonWhiteSpaceCharacter' },
+ { keys: '+', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, toFirstChar:true }},
+ { keys: '-', type: 'motion', motion: 'moveByLines', motionArgs: { forward: false, toFirstChar:true }},
+ { keys: '_', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, toFirstChar:true, repeatOffset:-1 }},
+ { keys: '$', type: 'motion', motion: 'moveToEol', motionArgs: { inclusive: true }},
+ { keys: '%', type: 'motion', motion: 'moveToMatchedSymbol', motionArgs: { inclusive: true, toJumplist: true }},
+ { keys: 'f<character>', type: 'motion', motion: 'moveToCharacter', motionArgs: { forward: true , inclusive: true }},
+ { keys: 'F<character>', type: 'motion', motion: 'moveToCharacter', motionArgs: { forward: false }},
+ { keys: 't<character>', type: 'motion', motion: 'moveTillCharacter', motionArgs: { forward: true, inclusive: true }},
+ { keys: 'T<character>', type: 'motion', motion: 'moveTillCharacter', motionArgs: { forward: false }},
+ { keys: ';', type: 'motion', motion: 'repeatLastCharacterSearch', motionArgs: { forward: true }},
+ { keys: ',', type: 'motion', motion: 'repeatLastCharacterSearch', motionArgs: { forward: false }},
+ { keys: '\'<character>', type: 'motion', motion: 'goToMark', motionArgs: {toJumplist: true, linewise: true}},
+ { keys: '`<character>', type: 'motion', motion: 'goToMark', motionArgs: {toJumplist: true}},
+ { keys: ']`', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true } },
+ { keys: '[`', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false } },
+ { keys: ']\'', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true, linewise: true } },
+ { keys: '[\'', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false, linewise: true } },
+ // the next two aren't motions but must come before more general motion declarations
+ { keys: ']p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: true, isEdit: true, matchIndent: true}},
+ { keys: '[p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: false, isEdit: true, matchIndent: true}},
+ { keys: ']<character>', type: 'motion', motion: 'moveToSymbol', motionArgs: { forward: true, toJumplist: true}},
+ { keys: '[<character>', type: 'motion', motion: 'moveToSymbol', motionArgs: { forward: false, toJumplist: true}},
+ { keys: '|', type: 'motion', motion: 'moveToColumn'},
+ { keys: 'o', type: 'motion', motion: 'moveToOtherHighlightedEnd', context:'visual'},
+ { keys: 'O', type: 'motion', motion: 'moveToOtherHighlightedEnd', motionArgs: {sameLine: true}, context:'visual'},
+ // Operators
+ { keys: 'd', type: 'operator', operator: 'delete' },
+ { keys: 'y', type: 'operator', operator: 'yank' },
+ { keys: 'c', type: 'operator', operator: 'change' },
+ { keys: '>', type: 'operator', operator: 'indent', operatorArgs: { indentRight: true }},
+ { keys: '<', type: 'operator', operator: 'indent', operatorArgs: { indentRight: false }},
+ { keys: 'g~', type: 'operator', operator: 'changeCase' },
+ { keys: 'gu', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: true}, isEdit: true },
+ { keys: 'gU', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: false}, isEdit: true },
+ { keys: 'n', type: 'motion', motion: 'findNext', motionArgs: { forward: true, toJumplist: true }},
+ { keys: 'N', type: 'motion', motion: 'findNext', motionArgs: { forward: false, toJumplist: true }},
+ // Operator-Motion dual commands
+ { keys: 'x', type: 'operatorMotion', operator: 'delete', motion: 'moveByCharacters', motionArgs: { forward: true }, operatorMotionArgs: { visualLine: false }},
+ { keys: 'X', type: 'operatorMotion', operator: 'delete', motion: 'moveByCharacters', motionArgs: { forward: false }, operatorMotionArgs: { visualLine: true }},
+ { keys: 'D', type: 'operatorMotion', operator: 'delete', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'},
+ { keys: 'D', type: 'operator', operator: 'delete', operatorArgs: { linewise: true }, context: 'visual'},
+ { keys: 'Y', type: 'operatorMotion', operator: 'yank', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'},
+ { keys: 'Y', type: 'operator', operator: 'yank', operatorArgs: { linewise: true }, context: 'visual'},
+ { keys: 'C', type: 'operatorMotion', operator: 'change', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'},
+ { keys: 'C', type: 'operator', operator: 'change', operatorArgs: { linewise: true }, context: 'visual'},
+ { keys: '~', type: 'operatorMotion', operator: 'changeCase', motion: 'moveByCharacters', motionArgs: { forward: true }, operatorArgs: { shouldMoveCursor: true }, context: 'normal'},
+ { keys: '~', type: 'operator', operator: 'changeCase', context: 'visual'},
+ { keys: '<C-w>', type: 'operatorMotion', operator: 'delete', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false }, context: 'insert' },
+ // Actions
+ { keys: '<C-i>', type: 'action', action: 'jumpListWalk', actionArgs: { forward: true }},
+ { keys: '<C-o>', type: 'action', action: 'jumpListWalk', actionArgs: { forward: false }},
+ { keys: '<C-e>', type: 'action', action: 'scroll', actionArgs: { forward: true, linewise: true }},
+ { keys: '<C-y>', type: 'action', action: 'scroll', actionArgs: { forward: false, linewise: true }},
+ { keys: 'a', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'charAfter' }, context: 'normal' },
+ { keys: 'A', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'eol' }, context: 'normal' },
+ { keys: 'A', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'endOfSelectedArea' }, context: 'visual' },
+ { keys: 'i', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'inplace' }, context: 'normal' },
+ { keys: 'I', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'firstNonBlank'}, context: 'normal' },
+ { keys: 'I', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'startOfSelectedArea' }, context: 'visual' },
+ { keys: 'o', type: 'action', action: 'newLineAndEnterInsertMode', isEdit: true, interlaceInsertRepeat: true, actionArgs: { after: true }, context: 'normal' },
+ { keys: 'O', type: 'action', action: 'newLineAndEnterInsertMode', isEdit: true, interlaceInsertRepeat: true, actionArgs: { after: false }, context: 'normal' },
+ { keys: 'v', type: 'action', action: 'toggleVisualMode' },
+ { keys: 'V', type: 'action', action: 'toggleVisualMode', actionArgs: { linewise: true }},
+ { keys: '<C-v>', type: 'action', action: 'toggleVisualMode', actionArgs: { blockwise: true }},
+ { keys: '<C-q>', type: 'action', action: 'toggleVisualMode', actionArgs: { blockwise: true }},
+ { keys: 'gv', type: 'action', action: 'reselectLastSelection' },
+ { keys: 'J', type: 'action', action: 'joinLines', isEdit: true },
+ { keys: 'p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: true, isEdit: true }},
+ { keys: 'P', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: false, isEdit: true }},
+ { keys: 'r<character>', type: 'action', action: 'replace', isEdit: true },
+ { keys: '@<character>', type: 'action', action: 'replayMacro' },
+ { keys: 'q<character>', type: 'action', action: 'enterMacroRecordMode' },
+ // Handle Replace-mode as a special case of insert mode.
+ { keys: 'R', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { replace: true }},
+ { keys: 'u', type: 'action', action: 'undo', context: 'normal' },
+ { keys: 'u', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: true}, context: 'visual', isEdit: true },
+ { keys: 'U', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: false}, context: 'visual', isEdit: true },
+ { keys: '<C-r>', type: 'action', action: 'redo' },
+ { keys: 'm<character>', type: 'action', action: 'setMark' },
+ { keys: '"<character>', type: 'action', action: 'setRegister' },
+ { keys: 'zz', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'center' }},
+ { keys: 'z.', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'center' }, motion: 'moveToFirstNonWhiteSpaceCharacter' },
+ { keys: 'zt', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'top' }},
+ { keys: 'z<CR>', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'top' }, motion: 'moveToFirstNonWhiteSpaceCharacter' },
+ { keys: 'z-', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'bottom' }},
+ { keys: 'zb', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'bottom' }, motion: 'moveToFirstNonWhiteSpaceCharacter' },
+ { keys: '.', type: 'action', action: 'repeatLastEdit' },
+ { keys: '<C-a>', type: 'action', action: 'incrementNumberToken', isEdit: true, actionArgs: {increase: true, backtrack: false}},
+ { keys: '<C-x>', type: 'action', action: 'incrementNumberToken', isEdit: true, actionArgs: {increase: false, backtrack: false}},
+ // Text object motions
+ { keys: 'a<character>', type: 'motion', motion: 'textObjectManipulation' },
+ { keys: 'i<character>', type: 'motion', motion: 'textObjectManipulation', motionArgs: { textObjectInner: true }},
+ // Search
+ { keys: '/', type: 'search', searchArgs: { forward: true, querySrc: 'prompt', toJumplist: true }},
+ { keys: '?', type: 'search', searchArgs: { forward: false, querySrc: 'prompt', toJumplist: true }},
+ { keys: '*', type: 'search', searchArgs: { forward: true, querySrc: 'wordUnderCursor', wholeWordOnly: true, toJumplist: true }},
+ { keys: '#', type: 'search', searchArgs: { forward: false, querySrc: 'wordUnderCursor', wholeWordOnly: true, toJumplist: true }},
+ { keys: 'g*', type: 'search', searchArgs: { forward: true, querySrc: 'wordUnderCursor', toJumplist: true }},
+ { keys: 'g#', type: 'search', searchArgs: { forward: false, querySrc: 'wordUnderCursor', toJumplist: true }},
+ // Ex command
+ { keys: ':', type: 'ex' }
+ ];
+
+ /**
+ * Ex commands
+ * Care must be taken when adding to the default Ex command map. For any
+ * pair of commands that have a shared prefix, at least one of their
+ * shortNames must not match the prefix of the other command.
+ */
+ var defaultExCommandMap = [
+ { name: 'colorscheme', shortName: 'colo' },
+ { name: 'map' },
+ { name: 'imap', shortName: 'im' },
+ { name: 'nmap', shortName: 'nm' },
+ { name: 'vmap', shortName: 'vm' },
+ { name: 'unmap' },
+ { name: 'write', shortName: 'w' },
+ { name: 'undo', shortName: 'u' },
+ { name: 'redo', shortName: 'red' },
+ { name: 'set', shortName: 'se' },
+ { name: 'set', shortName: 'se' },
+ { name: 'setlocal', shortName: 'setl' },
+ { name: 'setglobal', shortName: 'setg' },
+ { name: 'sort', shortName: 'sor' },
+ { name: 'substitute', shortName: 's', possiblyAsync: true },
+ { name: 'nohlsearch', shortName: 'noh' },
+ { name: 'yank', shortName: 'y' },
+ { name: 'delmarks', shortName: 'delm' },
+ { name: 'registers', shortName: 'reg', excludeFromCommandHistory: true },
+ { name: 'global', shortName: 'g' }
+ ];
+
+ var Pos = CodeMirror.Pos;
+
+ var Vim = function() {
+ function enterVimMode(cm) {
+ cm.setOption('disableInput', true);
+ cm.setOption('showCursorWhenSelecting', false);
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"});
+ cm.on('cursorActivity', onCursorActivity);
+ maybeInitVimState(cm);
+ CodeMirror.on(cm.getInputField(), 'paste', getOnPasteFn(cm));
+ }
+
+ function leaveVimMode(cm) {
+ cm.setOption('disableInput', false);
+ cm.off('cursorActivity', onCursorActivity);
+ CodeMirror.off(cm.getInputField(), 'paste', getOnPasteFn(cm));
+ cm.state.vim = null;
+ }
+
+ function detachVimMap(cm, next) {
+ if (this == CodeMirror.keyMap.vim)
+ CodeMirror.rmClass(cm.getWrapperElement(), "cm-fat-cursor");
+
+ if (!next || next.attach != attachVimMap)
+ leaveVimMode(cm, false);
+ }
+ function attachVimMap(cm, prev) {
+ if (this == CodeMirror.keyMap.vim)
+ CodeMirror.addClass(cm.getWrapperElement(), "cm-fat-cursor");
+
+ if (!prev || prev.attach != attachVimMap)
+ enterVimMode(cm);
+ }
+
+ // Deprecated, simply setting the keymap works again.
+ CodeMirror.defineOption('vimMode', false, function(cm, val, prev) {
+ if (val && cm.getOption("keyMap") != "vim")
+ cm.setOption("keyMap", "vim");
+ else if (!val && prev != CodeMirror.Init && /^vim/.test(cm.getOption("keyMap")))
+ cm.setOption("keyMap", "default");
+ });
+
+ function cmKey(key, cm) {
+ if (!cm) { return undefined; }
+ var vimKey = cmKeyToVimKey(key);
+ if (!vimKey) {
+ return false;
+ }
+ var cmd = CodeMirror.Vim.findKey(cm, vimKey);
+ if (typeof cmd == 'function') {
+ CodeMirror.signal(cm, 'vim-keypress', vimKey);
+ }
+ return cmd;
+ }
+
+ var modifiers = {'Shift': 'S', 'Ctrl': 'C', 'Alt': 'A', 'Cmd': 'D', 'Mod': 'A'};
+ var specialKeys = {Enter:'CR',Backspace:'BS',Delete:'Del'};
+ function cmKeyToVimKey(key) {
+ if (key.charAt(0) == '\'') {
+ // Keypress character binding of format "'a'"
+ return key.charAt(1);
+ }
+ var pieces = key.split(/-(?!$)/);
+ var lastPiece = pieces[pieces.length - 1];
+ if (pieces.length == 1 && pieces[0].length == 1) {
+ // No-modifier bindings use literal character bindings above. Skip.
+ return false;
+ } else if (pieces.length == 2 && pieces[0] == 'Shift' && lastPiece.length == 1) {
+ // Ignore Shift+char bindings as they should be handled by literal character.
+ return false;
+ }
+ var hasCharacter = false;
+ for (var i = 0; i < pieces.length; i++) {
+ var piece = pieces[i];
+ if (piece in modifiers) { pieces[i] = modifiers[piece]; }
+ else { hasCharacter = true; }
+ if (piece in specialKeys) { pieces[i] = specialKeys[piece]; }
+ }
+ if (!hasCharacter) {
+ // Vim does not support modifier only keys.
+ return false;
+ }
+ // TODO: Current bindings expect the character to be lower case, but
+ // it looks like vim key notation uses upper case.
+ if (isUpperCase(lastPiece)) {
+ pieces[pieces.length - 1] = lastPiece.toLowerCase();
+ }
+ return '<' + pieces.join('-') + '>';
+ }
+
+ function getOnPasteFn(cm) {
+ var vim = cm.state.vim;
+ if (!vim.onPasteFn) {
+ vim.onPasteFn = function() {
+ if (!vim.insertMode) {
+ cm.setCursor(offsetCursor(cm.getCursor(), 0, 1));
+ actions.enterInsertMode(cm, {}, vim);
+ }
+ };
+ }
+ return vim.onPasteFn;
+ }
+
+ var numberRegex = /[\d]/;
+ var wordCharTest = [CodeMirror.isWordChar, function(ch) {
+ return ch && !CodeMirror.isWordChar(ch) && !/\s/.test(ch);
+ }], bigWordCharTest = [function(ch) {
+ return /\S/.test(ch);
+ }];
+ function makeKeyRange(start, size) {
+ var keys = [];
+ for (var i = start; i < start + size; i++) {
+ keys.push(String.fromCharCode(i));
+ }
+ return keys;
+ }
+ var upperCaseAlphabet = makeKeyRange(65, 26);
+ var lowerCaseAlphabet = makeKeyRange(97, 26);
+ var numbers = makeKeyRange(48, 10);
+ var validMarks = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['<', '>']);
+ var validRegisters = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['-', '"', '.', ':', '/']);
+
+ function isLine(cm, line) {
+ return line >= cm.firstLine() && line <= cm.lastLine();
+ }
+ function isLowerCase(k) {
+ return (/^[a-z]$/).test(k);
+ }
+ function isMatchableSymbol(k) {
+ return '()[]{}'.indexOf(k) != -1;
+ }
+ function isNumber(k) {
+ return numberRegex.test(k);
+ }
+ function isUpperCase(k) {
+ return (/^[A-Z]$/).test(k);
+ }
+ function isWhiteSpaceString(k) {
+ return (/^\s*$/).test(k);
+ }
+ function inArray(val, arr) {
+ for (var i = 0; i < arr.length; i++) {
+ if (arr[i] == val) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ var options = {};
+ function defineOption(name, defaultValue, type, aliases, callback) {
+ if (defaultValue === undefined && !callback) {
+ throw Error('defaultValue is required unless callback is provided');
+ }
+ if (!type) { type = 'string'; }
+ options[name] = {
+ type: type,
+ defaultValue: defaultValue,
+ callback: callback
+ };
+ if (aliases) {
+ for (var i = 0; i < aliases.length; i++) {
+ options[aliases[i]] = options[name];
+ }
+ }
+ if (defaultValue) {
+ setOption(name, defaultValue);
+ }
+ }
+
+ function setOption(name, value, cm, cfg) {
+ var option = options[name];
+ cfg = cfg || {};
+ var scope = cfg.scope;
+ if (!option) {
+ 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;
+ }
+ }
+ if (option.callback) {
+ if (scope !== 'local') {
+ option.callback(value, undefined);
+ }
+ if (scope !== 'global' && cm) {
+ option.callback(value, cm);
+ }
+ } else {
+ if (scope !== 'local') {
+ option.value = option.type == 'boolean' ? !!value : value;
+ }
+ if (scope !== 'global' && cm) {
+ cm.state.vim.options[name] = {value: value};
+ }
+ }
+ }
+
+ function getOption(name, cm, cfg) {
+ var option = options[name];
+ cfg = cfg || {};
+ var scope = cfg.scope;
+ if (!option) {
+ throw Error('Unknown option: ' + name);
+ }
+ if (option.callback) {
+ var local = cm && option.callback(undefined, cm);
+ if (scope !== 'global' && local !== undefined) {
+ return local;
+ }
+ if (scope !== 'local') {
+ return option.callback();
+ }
+ return;
+ } else {
+ var local = (scope !== 'global') && (cm && cm.state.vim.options[name]);
+ return (local || (scope !== 'local') && option || {}).value;
+ }
+ }
+
+ defineOption('filetype', undefined, 'string', ['ft'], function(name, cm) {
+ // Option is local. Do nothing for global.
+ if (cm === undefined) {
+ return;
+ }
+ // The 'filetype' option proxies to the CodeMirror 'mode' option.
+ if (name === undefined) {
+ var mode = cm.getOption('mode');
+ return mode == 'null' ? '' : mode;
+ } else {
+ var mode = name == '' ? 'null' : name;
+ cm.setOption('mode', mode);
+ }
+ });
+
+ var createCircularJumpList = function() {
+ var size = 100;
+ var pointer = -1;
+ var head = 0;
+ var tail = 0;
+ var buffer = new Array(size);
+ function add(cm, oldCur, newCur) {
+ var current = pointer % size;
+ var curMark = buffer[current];
+ function useNextSlot(cursor) {
+ var next = ++pointer % size;
+ var trashMark = buffer[next];
+ if (trashMark) {
+ trashMark.clear();
+ }
+ buffer[next] = cm.setBookmark(cursor);
+ }
+ if (curMark) {
+ var markPos = curMark.find();
+ // avoid recording redundant cursor position
+ if (markPos && !cursorEqual(markPos, oldCur)) {
+ useNextSlot(oldCur);
+ }
+ } else {
+ useNextSlot(oldCur);
+ }
+ useNextSlot(newCur);
+ head = pointer;
+ tail = pointer - size + 1;
+ if (tail < 0) {
+ tail = 0;
+ }
+ }
+ function move(cm, offset) {
+ pointer += offset;
+ if (pointer > head) {
+ pointer = head;
+ } else if (pointer < tail) {
+ pointer = tail;
+ }
+ var mark = buffer[(size + pointer) % size];
+ // skip marks that are temporarily removed from text buffer
+ if (mark && !mark.find()) {
+ var inc = offset > 0 ? 1 : -1;
+ var newCur;
+ var oldCur = cm.getCursor();
+ do {
+ pointer += inc;
+ mark = buffer[(size + pointer) % size];
+ // skip marks that are the same as current position
+ if (mark &&
+ (newCur = mark.find()) &&
+ !cursorEqual(oldCur, newCur)) {
+ break;
+ }
+ } while (pointer < head && pointer > tail);
+ }
+ return mark;
+ }
+ return {
+ cachedCursor: undefined, //used for # and * jumps
+ add: add,
+ move: move
+ };
+ };
+
+ // Returns an object to track the changes associated insert mode. It
+ // clones the object that is passed in, or creates an empty object one if
+ // none is provided.
+ var createInsertModeChanges = function(c) {
+ if (c) {
+ // Copy construction
+ return {
+ changes: c.changes,
+ expectCursorActivityForChange: c.expectCursorActivityForChange
+ };
+ }
+ return {
+ // Change list
+ changes: [],
+ // Set to true on change, false on cursorActivity.
+ expectCursorActivityForChange: false
+ };
+ };
+
+ function MacroModeState() {
+ this.latestRegister = undefined;
+ this.isPlaying = false;
+ this.isRecording = false;
+ this.replaySearchQueries = [];
+ this.onRecordingDone = undefined;
+ this.lastInsertModeChanges = createInsertModeChanges();
+ }
+ MacroModeState.prototype = {
+ exitMacroRecordMode: function() {
+ var macroModeState = vimGlobalState.macroModeState;
+ if (macroModeState.onRecordingDone) {
+ macroModeState.onRecordingDone(); // close dialog
+ }
+ macroModeState.onRecordingDone = undefined;
+ macroModeState.isRecording = false;
+ },
+ enterMacroRecordMode: function(cm, registerName) {
+ var register =
+ vimGlobalState.registerController.getRegister(registerName);
+ if (register) {
+ register.clear();
+ this.latestRegister = registerName;
+ if (cm.openDialog) {
+ this.onRecordingDone = cm.openDialog(
+ '(recording)['+registerName+']', null, {bottom:true});
+ }
+ this.isRecording = true;
+ }
+ }
+ };
+
+ function maybeInitVimState(cm) {
+ if (!cm.state.vim) {
+ // Store instance state in the CodeMirror object.
+ cm.state.vim = {
+ inputState: new InputState(),
+ // Vim's input state that triggered the last edit, used to repeat
+ // motions and operators with '.'.
+ lastEditInputState: undefined,
+ // Vim's action command before the last edit, used to repeat actions
+ // with '.' and insert mode repeat.
+ lastEditActionCommand: undefined,
+ // When using jk for navigation, if you move from a longer line to a
+ // shorter line, the cursor may clip to the end of the shorter line.
+ // If j is pressed again and cursor goes to the next line, the
+ // cursor should go back to its horizontal position on the longer
+ // line if it can. This is to keep track of the horizontal position.
+ lastHPos: -1,
+ // Doing the same with screen-position for gj/gk
+ lastHSPos: -1,
+ // The last motion command run. Cleared if a non-motion command gets
+ // executed in between.
+ lastMotion: null,
+ marks: {},
+ // Mark for rendering fake cursor for visual mode.
+ fakeCursor: null,
+ insertMode: false,
+ // Repeat count for changes made in insert mode, triggered by key
+ // sequences like 3,i. Only exists when insertMode is true.
+ insertModeRepeat: undefined,
+ visualMode: false,
+ // If we are in visual line mode. No effect if visualMode is false.
+ visualLine: false,
+ visualBlock: false,
+ lastSelection: null,
+ lastPastedText: null,
+ sel: {},
+ // Buffer-local/window-local values of vim options.
+ options: {}
+ };
+ }
+ return cm.state.vim;
+ }
+ var vimGlobalState;
+ function resetVimGlobalState() {
+ vimGlobalState = {
+ // The current search query.
+ searchQuery: null,
+ // Whether we are searching backwards.
+ searchIsReversed: false,
+ // Replace part of the last substituted pattern
+ lastSubstituteReplacePart: undefined,
+ jumpList: createCircularJumpList(),
+ macroModeState: new MacroModeState,
+ // Recording latest f, t, F or T motion command.
+ lastCharacterSearch: {increment:0, forward:true, selectedCharacter:''},
+ registerController: new RegisterController({}),
+ // search history buffer
+ searchHistoryController: new HistoryController({}),
+ // ex Command history buffer
+ exCommandHistoryController : new HistoryController({})
+ };
+ for (var optionName in options) {
+ var option = options[optionName];
+ option.value = option.defaultValue;
+ }
+ }
+
+ var lastInsertModeKeyTimer;
+ var vimApi= {
+ buildKeyMap: function() {
+ // TODO: Convert keymap into dictionary format for fast lookup.
+ },
+ // Testing hook, though it might be useful to expose the register
+ // controller anyways.
+ getRegisterController: function() {
+ return vimGlobalState.registerController;
+ },
+ // Testing hook.
+ resetVimGlobalState_: resetVimGlobalState,
+
+ // Testing hook.
+ getVimGlobalState_: function() {
+ return vimGlobalState;
+ },
+
+ // Testing hook.
+ maybeInitVimState_: maybeInitVimState,
+
+ suppressErrorLogging: false,
+
+ InsertModeKey: InsertModeKey,
+ map: function(lhs, rhs, ctx) {
+ // Add user defined key bindings.
+ exCommandDispatcher.map(lhs, rhs, ctx);
+ },
+ unmap: function(lhs, ctx) {
+ exCommandDispatcher.unmap(lhs, ctx);
+ },
+ // TODO: Expose setOption and getOption as instance methods. Need to decide how to namespace
+ // them, or somehow make them work with the existing CodeMirror setOption/getOption API.
+ setOption: setOption,
+ getOption: getOption,
+ defineOption: defineOption,
+ defineEx: function(name, prefix, func){
+ if (!prefix) {
+ prefix = name;
+ } else if (name.indexOf(prefix) !== 0) {
+ throw new Error('(Vim.defineEx) "'+prefix+'" is not a prefix of "'+name+'", command not registered');
+ }
+ exCommands[name]=func;
+ exCommandDispatcher.commandMap_[prefix]={name:name, shortName:prefix, type:'api'};
+ },
+ handleKey: function (cm, key, origin) {
+ var command = this.findKey(cm, key, origin);
+ if (typeof command === 'function') {
+ return command();
+ }
+ },
+ /**
+ * This is the outermost function called by CodeMirror, after keys have
+ * been mapped to their Vim equivalents.
+ *
+ * Finds a command based on the key (and cached keys if there is a
+ * multi-key sequence). Returns `undefined` if no key is matched, a noop
+ * function if a partial match is found (multi-key), and a function to
+ * execute the bound command if a a key is matched. The function always
+ * returns true.
+ */
+ findKey: function(cm, key, origin) {
+ var vim = maybeInitVimState(cm);
+ function handleMacroRecording() {
+ var macroModeState = vimGlobalState.macroModeState;
+ if (macroModeState.isRecording) {
+ if (key == 'q') {
+ macroModeState.exitMacroRecordMode();
+ clearInputState(cm);
+ return true;
+ }
+ if (origin != 'mapping') {
+ logKey(macroModeState, key);
+ }
+ }
+ }
+ function handleEsc() {
+ if (key == '<Esc>') {
+ // Clear input state and get back to normal mode.
+ clearInputState(cm);
+ if (vim.visualMode) {
+ exitVisualMode(cm);
+ } else if (vim.insertMode) {
+ exitInsertMode(cm);
+ }
+ return true;
+ }
+ }
+ function doKeyToKey(keys) {
+ // TODO: prevent infinite recursion.
+ var match;
+ while (keys) {
+ // Pull off one command key, which is either a single character
+ // or a special sequence wrapped in '<' and '>', e.g. '<Space>'.
+ match = (/<\w+-.+?>|<\w+>|./).exec(keys);
+ key = match[0];
+ keys = keys.substring(match.index + key.length);
+ CodeMirror.Vim.handleKey(cm, key, 'mapping');
+ }
+ }
+
+ function handleKeyInsertMode() {
+ if (handleEsc()) { return true; }
+ var keys = vim.inputState.keyBuffer = vim.inputState.keyBuffer + key;
+ var keysAreChars = key.length == 1;
+ var match = commandDispatcher.matchCommand(keys, defaultKeymap, vim.inputState, 'insert');
+ // Need to check all key substrings in insert mode.
+ while (keys.length > 1 && match.type != 'full') {
+ var keys = vim.inputState.keyBuffer = keys.slice(1);
+ var thisMatch = commandDispatcher.matchCommand(keys, defaultKeymap, vim.inputState, 'insert');
+ if (thisMatch.type != 'none') { match = thisMatch; }
+ }
+ if (match.type == 'none') { clearInputState(cm); return false; }
+ else if (match.type == 'partial') {
+ if (lastInsertModeKeyTimer) { window.clearTimeout(lastInsertModeKeyTimer); }
+ lastInsertModeKeyTimer = window.setTimeout(
+ function() { if (vim.insertMode && vim.inputState.keyBuffer) { clearInputState(cm); } },
+ getOption('insertModeEscKeysTimeout'));
+ return !keysAreChars;
+ }
+
+ if (lastInsertModeKeyTimer) { window.clearTimeout(lastInsertModeKeyTimer); }
+ if (keysAreChars) {
+ var here = cm.getCursor();
+ cm.replaceRange('', offsetCursor(here, 0, -(keys.length - 1)), here, '+input');
+ }
+ clearInputState(cm);
+ return match.command;
+ }
+
+ function handleKeyNonInsertMode() {
+ if (handleMacroRecording() || handleEsc()) { return true; };
+
+ var keys = vim.inputState.keyBuffer = vim.inputState.keyBuffer + key;
+ if (/^[1-9]\d*$/.test(keys)) { return true; }
+
+ var keysMatcher = /^(\d*)(.*)$/.exec(keys);
+ if (!keysMatcher) { clearInputState(cm); return false; }
+ var context = vim.visualMode ? 'visual' :
+ 'normal';
+ var match = commandDispatcher.matchCommand(keysMatcher[2] || keysMatcher[1], defaultKeymap, vim.inputState, context);
+ if (match.type == 'none') { clearInputState(cm); return false; }
+ else if (match.type == 'partial') { return true; }
+
+ vim.inputState.keyBuffer = '';
+ var keysMatcher = /^(\d*)(.*)$/.exec(keys);
+ if (keysMatcher[1] && keysMatcher[1] != '0') {
+ vim.inputState.pushRepeatDigit(keysMatcher[1]);
+ }
+ return match.command;
+ }
+
+ var command;
+ if (vim.insertMode) { command = handleKeyInsertMode(); }
+ else { command = handleKeyNonInsertMode(); }
+ if (command === false) {
+ return undefined;
+ } else if (command === true) {
+ // TODO: Look into using CodeMirror's multi-key handling.
+ // Return no-op since we are caching the key. Counts as handled, but
+ // don't want act on it just yet.
+ return function() {};
+ } else {
+ return function() {
+ return cm.operation(function() {
+ cm.curOp.isVimOp = true;
+ try {
+ if (command.type == 'keyToKey') {
+ doKeyToKey(command.toKeys);
+ } else {
+ commandDispatcher.processCommand(cm, vim, command);
+ }
+ } catch (e) {
+ // clear VIM state in case it's in a bad state.
+ cm.state.vim = undefined;
+ maybeInitVimState(cm);
+ if (!CodeMirror.Vim.suppressErrorLogging) {
+ console['log'](e);
+ }
+ throw e;
+ }
+ return true;
+ });
+ };
+ }
+ },
+ handleEx: function(cm, input) {
+ exCommandDispatcher.processCommand(cm, input);
+ },
+
+ defineMotion: defineMotion,
+ defineAction: defineAction,
+ defineOperator: defineOperator,
+ mapCommand: mapCommand,
+ _mapCommand: _mapCommand,
+
+ defineRegister: defineRegister,
+
+ exitVisualMode: exitVisualMode,
+ exitInsertMode: exitInsertMode
+ };
+
+ // Represents the current input state.
+ function InputState() {
+ this.prefixRepeat = [];
+ this.motionRepeat = [];
+
+ this.operator = null;
+ this.operatorArgs = null;
+ this.motion = null;
+ this.motionArgs = null;
+ this.keyBuffer = []; // For matching multi-key commands.
+ this.registerName = null; // Defaults to the unnamed register.
+ }
+ InputState.prototype.pushRepeatDigit = function(n) {
+ if (!this.operator) {
+ this.prefixRepeat = this.prefixRepeat.concat(n);
+ } else {
+ this.motionRepeat = this.motionRepeat.concat(n);
+ }
+ };
+ InputState.prototype.getRepeat = function() {
+ var repeat = 0;
+ if (this.prefixRepeat.length > 0 || this.motionRepeat.length > 0) {
+ repeat = 1;
+ if (this.prefixRepeat.length > 0) {
+ repeat *= parseInt(this.prefixRepeat.join(''), 10);
+ }
+ if (this.motionRepeat.length > 0) {
+ repeat *= parseInt(this.motionRepeat.join(''), 10);
+ }
+ }
+ return repeat;
+ };
+
+ function clearInputState(cm, reason) {
+ cm.state.vim.inputState = new InputState();
+ CodeMirror.signal(cm, 'vim-command-done', reason);
+ }
+
+ /*
+ * Register stores information about copy and paste registers. Besides
+ * text, a register must store whether it is linewise (i.e., when it is
+ * pasted, should it insert itself into a new line, or should the text be
+ * inserted at the cursor position.)
+ */
+ function Register(text, linewise, blockwise) {
+ this.clear();
+ this.keyBuffer = [text || ''];
+ this.insertModeChanges = [];
+ this.searchQueries = [];
+ this.linewise = !!linewise;
+ this.blockwise = !!blockwise;
+ }
+ Register.prototype = {
+ setText: function(text, linewise, blockwise) {
+ this.keyBuffer = [text || ''];
+ this.linewise = !!linewise;
+ this.blockwise = !!blockwise;
+ },
+ pushText: function(text, linewise) {
+ // if this register has ever been set to linewise, use linewise.
+ if (linewise) {
+ if (!this.linewise) {
+ this.keyBuffer.push('\n');
+ }
+ this.linewise = true;
+ }
+ this.keyBuffer.push(text);
+ },
+ pushInsertModeChanges: function(changes) {
+ this.insertModeChanges.push(createInsertModeChanges(changes));
+ },
+ pushSearchQuery: function(query) {
+ this.searchQueries.push(query);
+ },
+ clear: function() {
+ this.keyBuffer = [];
+ this.insertModeChanges = [];
+ this.searchQueries = [];
+ this.linewise = false;
+ },
+ toString: function() {
+ return this.keyBuffer.join('');
+ }
+ };
+
+ /**
+ * Defines an external register.
+ *
+ * The name should be a single character that will be used to reference the register.
+ * The register should support setText, pushText, clear, and toString(). See Register
+ * for a reference implementation.
+ */
+ function defineRegister(name, register) {
+ var registers = vimGlobalState.registerController.registers[name];
+ if (!name || name.length != 1) {
+ throw Error('Register name must be 1 character');
+ }
+ if (registers[name]) {
+ throw Error('Register already defined ' + name);
+ }
+ registers[name] = register;
+ validRegisters.push(name);
+ }
+
+ /*
+ * vim registers allow you to keep many independent copy and paste buffers.
+ * See http://usevim.com/2012/04/13/registers/ for an introduction.
+ *
+ * RegisterController keeps the state of all the registers. An initial
+ * state may be passed in. The unnamed register '"' will always be
+ * overridden.
+ */
+ function RegisterController(registers) {
+ this.registers = registers;
+ this.unnamedRegister = registers['"'] = new Register();
+ registers['.'] = new Register();
+ registers[':'] = new Register();
+ registers['/'] = new Register();
+ }
+ RegisterController.prototype = {
+ pushText: function(registerName, operator, text, linewise, blockwise) {
+ if (linewise && text.charAt(0) == '\n') {
+ text = text.slice(1) + '\n';
+ }
+ if (linewise && text.charAt(text.length - 1) !== '\n'){
+ text += '\n';
+ }
+ // Lowercase and uppercase registers refer to the same register.
+ // Uppercase just means append.
+ var register = this.isValidRegister(registerName) ?
+ this.getRegister(registerName) : null;
+ // if no register/an invalid register was specified, things go to the
+ // default registers
+ if (!register) {
+ switch (operator) {
+ case 'yank':
+ // The 0 register contains the text from the most recent yank.
+ this.registers['0'] = new Register(text, linewise, blockwise);
+ break;
+ case 'delete':
+ case 'change':
+ if (text.indexOf('\n') == -1) {
+ // Delete less than 1 line. Update the small delete register.
+ this.registers['-'] = new Register(text, linewise);
+ } else {
+ // Shift down the contents of the numbered registers and put the
+ // deleted text into register 1.
+ this.shiftNumericRegisters_();
+ this.registers['1'] = new Register(text, linewise);
+ }
+ break;
+ }
+ // Make sure the unnamed register is set to what just happened
+ this.unnamedRegister.setText(text, linewise, blockwise);
+ return;
+ }
+
+ // If we've gotten to this point, we've actually specified a register
+ var append = isUpperCase(registerName);
+ if (append) {
+ register.pushText(text, linewise);
+ } else {
+ register.setText(text, linewise, blockwise);
+ }
+ // The unnamed register always has the same value as the last used
+ // register.
+ this.unnamedRegister.setText(register.toString(), linewise);
+ },
+ // Gets the register named @name. If one of @name doesn't already exist,
+ // create it. If @name is invalid, return the unnamedRegister.
+ getRegister: function(name) {
+ if (!this.isValidRegister(name)) {
+ return this.unnamedRegister;
+ }
+ name = name.toLowerCase();
+ if (!this.registers[name]) {
+ this.registers[name] = new Register();
+ }
+ return this.registers[name];
+ },
+ isValidRegister: function(name) {
+ return name && inArray(name, validRegisters);
+ },
+ shiftNumericRegisters_: function() {
+ for (var i = 9; i >= 2; i--) {
+ this.registers[i] = this.getRegister('' + (i - 1));
+ }
+ }
+ };
+ function HistoryController() {
+ this.historyBuffer = [];
+ this.iterator = 0;
+ this.initialPrefix = null;
+ }
+ HistoryController.prototype = {
+ // the input argument here acts a user entered prefix for a small time
+ // until we start autocompletion in which case it is the autocompleted.
+ nextMatch: function (input, up) {
+ var historyBuffer = this.historyBuffer;
+ var dir = up ? -1 : 1;
+ if (this.initialPrefix === null) this.initialPrefix = input;
+ for (var i = this.iterator + dir; up ? i >= 0 : i < historyBuffer.length; i+= dir) {
+ var element = historyBuffer[i];
+ for (var j = 0; j <= element.length; j++) {
+ if (this.initialPrefix == element.substring(0, j)) {
+ this.iterator = i;
+ return element;
+ }
+ }
+ }
+ // should return the user input in case we reach the end of buffer.
+ if (i >= historyBuffer.length) {
+ this.iterator = historyBuffer.length;
+ return this.initialPrefix;
+ }
+ // return the last autocompleted query or exCommand as it is.
+ if (i < 0 ) return input;
+ },
+ pushInput: function(input) {
+ var index = this.historyBuffer.indexOf(input);
+ if (index > -1) this.historyBuffer.splice(index, 1);
+ if (input.length) this.historyBuffer.push(input);
+ },
+ reset: function() {
+ this.initialPrefix = null;
+ this.iterator = this.historyBuffer.length;
+ }
+ };
+ var commandDispatcher = {
+ matchCommand: function(keys, keyMap, inputState, context) {
+ var matches = commandMatches(keys, keyMap, context, inputState);
+ if (!matches.full && !matches.partial) {
+ return {type: 'none'};
+ } else if (!matches.full && matches.partial) {
+ return {type: 'partial'};
+ }
+
+ var bestMatch;
+ for (var i = 0; i < matches.full.length; i++) {
+ var match = matches.full[i];
+ if (!bestMatch) {
+ bestMatch = match;
+ }
+ }
+ if (bestMatch.keys.slice(-11) == '<character>') {
+ inputState.selectedCharacter = lastChar(keys);
+ }
+ return {type: 'full', command: bestMatch};
+ },
+ processCommand: function(cm, vim, command) {
+ vim.inputState.repeatOverride = command.repeatOverride;
+ switch (command.type) {
+ case 'motion':
+ this.processMotion(cm, vim, command);
+ break;
+ case 'operator':
+ this.processOperator(cm, vim, command);
+ break;
+ case 'operatorMotion':
+ this.processOperatorMotion(cm, vim, command);
+ break;
+ case 'action':
+ this.processAction(cm, vim, command);
+ break;
+ case 'search':
+ this.processSearch(cm, vim, command);
+ break;
+ case 'ex':
+ case 'keyToEx':
+ this.processEx(cm, vim, command);
+ break;
+ default:
+ break;
+ }
+ },
+ processMotion: function(cm, vim, command) {
+ vim.inputState.motion = command.motion;
+ vim.inputState.motionArgs = copyArgs(command.motionArgs);
+ this.evalInput(cm, vim);
+ },
+ processOperator: function(cm, vim, command) {
+ var inputState = vim.inputState;
+ if (inputState.operator) {
+ if (inputState.operator == command.operator) {
+ // Typing an operator twice like 'dd' makes the operator operate
+ // linewise
+ inputState.motion = 'expandToLine';
+ inputState.motionArgs = { linewise: true };
+ this.evalInput(cm, vim);
+ return;
+ } else {
+ // 2 different operators in a row doesn't make sense.
+ clearInputState(cm);
+ }
+ }
+ inputState.operator = command.operator;
+ inputState.operatorArgs = copyArgs(command.operatorArgs);
+ if (vim.visualMode) {
+ // Operating on a selection in visual mode. We don't need a motion.
+ this.evalInput(cm, vim);
+ }
+ },
+ processOperatorMotion: function(cm, vim, command) {
+ var visualMode = vim.visualMode;
+ var operatorMotionArgs = copyArgs(command.operatorMotionArgs);
+ if (operatorMotionArgs) {
+ // Operator motions may have special behavior in visual mode.
+ if (visualMode && operatorMotionArgs.visualLine) {
+ vim.visualLine = true;
+ }
+ }
+ this.processOperator(cm, vim, command);
+ if (!visualMode) {
+ this.processMotion(cm, vim, command);
+ }
+ },
+ processAction: function(cm, vim, command) {
+ var inputState = vim.inputState;
+ var repeat = inputState.getRepeat();
+ var repeatIsExplicit = !!repeat;
+ var actionArgs = copyArgs(command.actionArgs) || {};
+ if (inputState.selectedCharacter) {
+ actionArgs.selectedCharacter = inputState.selectedCharacter;
+ }
+ // Actions may or may not have motions and operators. Do these first.
+ if (command.operator) {
+ this.processOperator(cm, vim, command);
+ }
+ if (command.motion) {
+ this.processMotion(cm, vim, command);
+ }
+ if (command.motion || command.operator) {
+ this.evalInput(cm, vim);
+ }
+ actionArgs.repeat = repeat || 1;
+ actionArgs.repeatIsExplicit = repeatIsExplicit;
+ actionArgs.registerName = inputState.registerName;
+ clearInputState(cm);
+ vim.lastMotion = null;
+ if (command.isEdit) {
+ this.recordLastEdit(vim, inputState, command);
+ }
+ actions[command.action](cm, actionArgs, vim);
+ },
+ processSearch: function(cm, vim, command) {
+ if (!cm.getSearchCursor) {
+ // Search depends on SearchCursor.
+ return;
+ }
+ var forward = command.searchArgs.forward;
+ var wholeWordOnly = command.searchArgs.wholeWordOnly;
+ getSearchState(cm).setReversed(!forward);
+ var promptPrefix = (forward) ? '/' : '?';
+ var originalQuery = getSearchState(cm).getQuery();
+ var originalScrollPos = cm.getScrollInfo();
+ function handleQuery(query, ignoreCase, smartCase) {
+ vimGlobalState.searchHistoryController.pushInput(query);
+ vimGlobalState.searchHistoryController.reset();
+ try {
+ updateSearchQuery(cm, query, ignoreCase, smartCase);
+ } catch (e) {
+ showConfirm(cm, 'Invalid regex: ' + query);
+ clearInputState(cm);
+ return;
+ }
+ commandDispatcher.processMotion(cm, vim, {
+ type: 'motion',
+ motion: 'findNext',
+ motionArgs: { forward: true, toJumplist: command.searchArgs.toJumplist }
+ });
+ }
+ function onPromptClose(query) {
+ cm.scrollTo(originalScrollPos.left, originalScrollPos.top);
+ handleQuery(query, true /** ignoreCase */, true /** smartCase */);
+ var macroModeState = vimGlobalState.macroModeState;
+ if (macroModeState.isRecording) {
+ logSearchQuery(macroModeState, query);
+ }
+ }
+ function onPromptKeyUp(e, query, close) {
+ var keyName = CodeMirror.keyName(e), up;
+ if (keyName == 'Up' || keyName == 'Down') {
+ up = keyName == 'Up' ? true : false;
+ query = vimGlobalState.searchHistoryController.nextMatch(query, up) || '';
+ close(query);
+ } else {
+ if ( keyName != 'Left' && keyName != 'Right' && keyName != 'Ctrl' && keyName != 'Alt' && keyName != 'Shift')
+ vimGlobalState.searchHistoryController.reset();
+ }
+ var parsedQuery;
+ try {
+ parsedQuery = updateSearchQuery(cm, query,
+ true /** ignoreCase */, true /** smartCase */);
+ } catch (e) {
+ // Swallow bad regexes for incremental search.
+ }
+ if (parsedQuery) {
+ cm.scrollIntoView(findNext(cm, !forward, parsedQuery), 30);
+ } else {
+ clearSearchHighlight(cm);
+ cm.scrollTo(originalScrollPos.left, originalScrollPos.top);
+ }
+ }
+ function onPromptKeyDown(e, query, close) {
+ var keyName = CodeMirror.keyName(e);
+ if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[' ||
+ (keyName == 'Backspace' && query == '')) {
+ vimGlobalState.searchHistoryController.pushInput(query);
+ vimGlobalState.searchHistoryController.reset();
+ updateSearchQuery(cm, originalQuery);
+ clearSearchHighlight(cm);
+ cm.scrollTo(originalScrollPos.left, originalScrollPos.top);
+ CodeMirror.e_stop(e);
+ clearInputState(cm);
+ close();
+ cm.focus();
+ } else if (keyName == 'Ctrl-U') {
+ // Ctrl-U clears input.
+ CodeMirror.e_stop(e);
+ close('');
+ }
+ }
+ switch (command.searchArgs.querySrc) {
+ case 'prompt':
+ var macroModeState = vimGlobalState.macroModeState;
+ if (macroModeState.isPlaying) {
+ var query = macroModeState.replaySearchQueries.shift();
+ handleQuery(query, true /** ignoreCase */, false /** smartCase */);
+ } else {
+ showPrompt(cm, {
+ onClose: onPromptClose,
+ prefix: promptPrefix,
+ desc: searchPromptDesc,
+ onKeyUp: onPromptKeyUp,
+ onKeyDown: onPromptKeyDown
+ });
+ }
+ break;
+ case 'wordUnderCursor':
+ var word = expandWordUnderCursor(cm, false /** inclusive */,
+ true /** forward */, false /** bigWord */,
+ true /** noSymbol */);
+ var isKeyword = true;
+ if (!word) {
+ word = expandWordUnderCursor(cm, false /** inclusive */,
+ true /** forward */, false /** bigWord */,
+ false /** noSymbol */);
+ isKeyword = false;
+ }
+ if (!word) {
+ return;
+ }
+ var query = cm.getLine(word.start.line).substring(word.start.ch,
+ word.end.ch);
+ if (isKeyword && wholeWordOnly) {
+ query = '\\b' + query + '\\b';
+ } else {
+ query = escapeRegex(query);
+ }
+
+ // cachedCursor is used to save the old position of the cursor
+ // when * or # causes vim to seek for the nearest word and shift
+ // the cursor before entering the motion.
+ vimGlobalState.jumpList.cachedCursor = cm.getCursor();
+ cm.setCursor(word.start);
+
+ handleQuery(query, true /** ignoreCase */, false /** smartCase */);
+ break;
+ }
+ },
+ processEx: function(cm, vim, command) {
+ function onPromptClose(input) {
+ // Give the prompt some time to close so that if processCommand shows
+ // an error, the elements don't overlap.
+ vimGlobalState.exCommandHistoryController.pushInput(input);
+ vimGlobalState.exCommandHistoryController.reset();
+ exCommandDispatcher.processCommand(cm, input);
+ }
+ function onPromptKeyDown(e, input, close) {
+ var keyName = CodeMirror.keyName(e), up;
+ if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[' ||
+ (keyName == 'Backspace' && input == '')) {
+ vimGlobalState.exCommandHistoryController.pushInput(input);
+ vimGlobalState.exCommandHistoryController.reset();
+ CodeMirror.e_stop(e);
+ clearInputState(cm);
+ close();
+ cm.focus();
+ }
+ if (keyName == 'Up' || keyName == 'Down') {
+ up = keyName == 'Up' ? true : false;
+ input = vimGlobalState.exCommandHistoryController.nextMatch(input, up) || '';
+ close(input);
+ } else if (keyName == 'Ctrl-U') {
+ // Ctrl-U clears input.
+ CodeMirror.e_stop(e);
+ close('');
+ } else {
+ if ( keyName != 'Left' && keyName != 'Right' && keyName != 'Ctrl' && keyName != 'Alt' && keyName != 'Shift')
+ vimGlobalState.exCommandHistoryController.reset();
+ }
+ }
+ if (command.type == 'keyToEx') {
+ // Handle user defined Ex to Ex mappings
+ exCommandDispatcher.processCommand(cm, command.exArgs.input);
+ } else {
+ if (vim.visualMode) {
+ showPrompt(cm, { onClose: onPromptClose, prefix: ':', value: '\'<,\'>',
+ onKeyDown: onPromptKeyDown});
+ } else {
+ showPrompt(cm, { onClose: onPromptClose, prefix: ':',
+ onKeyDown: onPromptKeyDown});
+ }
+ }
+ },
+ evalInput: function(cm, vim) {
+ // If the motion command is set, execute both the operator and motion.
+ // Otherwise return.
+ var inputState = vim.inputState;
+ var motion = inputState.motion;
+ var motionArgs = inputState.motionArgs || {};
+ var operator = inputState.operator;
+ var operatorArgs = inputState.operatorArgs || {};
+ var registerName = inputState.registerName;
+ var sel = vim.sel;
+ // TODO: Make sure cm and vim selections are identical outside visual mode.
+ var origHead = copyCursor(vim.visualMode ? clipCursorToContent(cm, sel.head): cm.getCursor('head'));
+ var origAnchor = copyCursor(vim.visualMode ? clipCursorToContent(cm, sel.anchor) : cm.getCursor('anchor'));
+ var oldHead = copyCursor(origHead);
+ var oldAnchor = copyCursor(origAnchor);
+ var newHead, newAnchor;
+ var repeat;
+ if (operator) {
+ this.recordLastEdit(vim, inputState);
+ }
+ if (inputState.repeatOverride !== undefined) {
+ // If repeatOverride is specified, that takes precedence over the
+ // input state's repeat. Used by Ex mode and can be user defined.
+ repeat = inputState.repeatOverride;
+ } else {
+ repeat = inputState.getRepeat();
+ }
+ if (repeat > 0 && motionArgs.explicitRepeat) {
+ motionArgs.repeatIsExplicit = true;
+ } else if (motionArgs.noRepeat ||
+ (!motionArgs.explicitRepeat && repeat === 0)) {
+ repeat = 1;
+ motionArgs.repeatIsExplicit = false;
+ }
+ if (inputState.selectedCharacter) {
+ // If there is a character input, stick it in all of the arg arrays.
+ motionArgs.selectedCharacter = operatorArgs.selectedCharacter =
+ inputState.selectedCharacter;
+ }
+ motionArgs.repeat = repeat;
+ clearInputState(cm);
+ if (motion) {
+ var motionResult = motions[motion](cm, origHead, motionArgs, vim);
+ vim.lastMotion = motions[motion];
+ if (!motionResult) {
+ return;
+ }
+ if (motionArgs.toJumplist) {
+ var jumpList = vimGlobalState.jumpList;
+ // if the current motion is # or *, use cachedCursor
+ var cachedCursor = jumpList.cachedCursor;
+ if (cachedCursor) {
+ recordJumpPosition(cm, cachedCursor, motionResult);
+ delete jumpList.cachedCursor;
+ } else {
+ recordJumpPosition(cm, origHead, motionResult);
+ }
+ }
+ if (motionResult instanceof Array) {
+ newAnchor = motionResult[0];
+ newHead = motionResult[1];
+ } else {
+ newHead = motionResult;
+ }
+ // TODO: Handle null returns from motion commands better.
+ if (!newHead) {
+ newHead = copyCursor(origHead);
+ }
+ if (vim.visualMode) {
+ if (!(vim.visualBlock && newHead.ch === Infinity)) {
+ newHead = clipCursorToContent(cm, newHead, vim.visualBlock);
+ }
+ if (newAnchor) {
+ newAnchor = clipCursorToContent(cm, newAnchor, true);
+ }
+ newAnchor = newAnchor || oldAnchor;
+ sel.anchor = newAnchor;
+ sel.head = newHead;
+ updateCmSelection(cm);
+ updateMark(cm, vim, '<',
+ cursorIsBefore(newAnchor, newHead) ? newAnchor
+ : newHead);
+ updateMark(cm, vim, '>',
+ cursorIsBefore(newAnchor, newHead) ? newHead
+ : newAnchor);
+ } else if (!operator) {
+ newHead = clipCursorToContent(cm, newHead);
+ cm.setCursor(newHead.line, newHead.ch);
+ }
+ }
+ if (operator) {
+ if (operatorArgs.lastSel) {
+ // Replaying a visual mode operation
+ newAnchor = oldAnchor;
+ var lastSel = operatorArgs.lastSel;
+ var lineOffset = Math.abs(lastSel.head.line - lastSel.anchor.line);
+ var chOffset = Math.abs(lastSel.head.ch - lastSel.anchor.ch);
+ if (lastSel.visualLine) {
+ // Linewise Visual mode: The same number of lines.
+ newHead = Pos(oldAnchor.line + lineOffset, oldAnchor.ch);
+ } else if (lastSel.visualBlock) {
+ // Blockwise Visual mode: The same number of lines and columns.
+ newHead = Pos(oldAnchor.line + lineOffset, oldAnchor.ch + chOffset);
+ } else if (lastSel.head.line == lastSel.anchor.line) {
+ // Normal Visual mode within one line: The same number of characters.
+ newHead = Pos(oldAnchor.line, oldAnchor.ch + chOffset);
+ } else {
+ // Normal Visual mode with several lines: The same number of lines, in the
+ // last line the same number of characters as in the last line the last time.
+ newHead = Pos(oldAnchor.line + lineOffset, oldAnchor.ch);
+ }
+ vim.visualMode = true;
+ vim.visualLine = lastSel.visualLine;
+ vim.visualBlock = lastSel.visualBlock;
+ sel = vim.sel = {
+ anchor: newAnchor,
+ head: newHead
+ };
+ updateCmSelection(cm);
+ } else if (vim.visualMode) {
+ operatorArgs.lastSel = {
+ anchor: copyCursor(sel.anchor),
+ head: copyCursor(sel.head),
+ visualBlock: vim.visualBlock,
+ visualLine: vim.visualLine
+ };
+ }
+ var curStart, curEnd, linewise, mode;
+ var cmSel;
+ if (vim.visualMode) {
+ // Init visual op
+ curStart = cursorMin(sel.head, sel.anchor);
+ curEnd = cursorMax(sel.head, sel.anchor);
+ linewise = vim.visualLine || operatorArgs.linewise;
+ mode = vim.visualBlock ? 'block' :
+ linewise ? 'line' :
+ 'char';
+ cmSel = makeCmSelection(cm, {
+ anchor: curStart,
+ head: curEnd
+ }, mode);
+ if (linewise) {
+ var ranges = cmSel.ranges;
+ if (mode == 'block') {
+ // Linewise operators in visual block mode extend to end of line
+ for (var i = 0; i < ranges.length; i++) {
+ ranges[i].head.ch = lineLength(cm, ranges[i].head.line);
+ }
+ } else if (mode == 'line') {
+ ranges[0].head = Pos(ranges[0].head.line + 1, 0);
+ }
+ }
+ } else {
+ // Init motion op
+ curStart = copyCursor(newAnchor || oldAnchor);
+ curEnd = copyCursor(newHead || oldHead);
+ if (cursorIsBefore(curEnd, curStart)) {
+ var tmp = curStart;
+ curStart = curEnd;
+ curEnd = tmp;
+ }
+ linewise = motionArgs.linewise || operatorArgs.linewise;
+ if (linewise) {
+ // Expand selection to entire line.
+ expandSelectionToLine(cm, curStart, curEnd);
+ } else if (motionArgs.forward) {
+ // Clip to trailing newlines only if the motion goes forward.
+ clipToLine(cm, curStart, curEnd);
+ }
+ mode = 'char';
+ var exclusive = !motionArgs.inclusive || linewise;
+ cmSel = makeCmSelection(cm, {
+ anchor: curStart,
+ head: curEnd
+ }, mode, exclusive);
+ }
+ cm.setSelections(cmSel.ranges, cmSel.primary);
+ vim.lastMotion = null;
+ operatorArgs.repeat = repeat; // For indent in visual mode.
+ operatorArgs.registerName = registerName;
+ // Keep track of linewise as it affects how paste and change behave.
+ operatorArgs.linewise = linewise;
+ var operatorMoveTo = operators[operator](
+ cm, operatorArgs, cmSel.ranges, oldAnchor, newHead);
+ if (vim.visualMode) {
+ exitVisualMode(cm, operatorMoveTo != null);
+ }
+ if (operatorMoveTo) {
+ cm.setCursor(operatorMoveTo);
+ }
+ }
+ },
+ recordLastEdit: function(vim, inputState, actionCommand) {
+ var macroModeState = vimGlobalState.macroModeState;
+ if (macroModeState.isPlaying) { return; }
+ vim.lastEditInputState = inputState;
+ vim.lastEditActionCommand = actionCommand;
+ macroModeState.lastInsertModeChanges.changes = [];
+ macroModeState.lastInsertModeChanges.expectCursorActivityForChange = false;
+ }
+ };
+
+ /**
+ * typedef {Object{line:number,ch:number}} Cursor An object containing the
+ * position of the cursor.
+ */
+ // All of the functions below return Cursor objects.
+ var motions = {
+ moveToTopLine: function(cm, _head, motionArgs) {
+ var line = getUserVisibleLines(cm).top + motionArgs.repeat -1;
+ return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line)));
+ },
+ moveToMiddleLine: function(cm) {
+ var range = getUserVisibleLines(cm);
+ var line = Math.floor((range.top + range.bottom) * 0.5);
+ return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line)));
+ },
+ moveToBottomLine: function(cm, _head, motionArgs) {
+ var line = getUserVisibleLines(cm).bottom - motionArgs.repeat +1;
+ return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line)));
+ },
+ expandToLine: function(_cm, head, motionArgs) {
+ // Expands forward to end of line, and then to next line if repeat is
+ // >1. Does not handle backward motion!
+ var cur = head;
+ return Pos(cur.line + motionArgs.repeat - 1, Infinity);
+ },
+ findNext: function(cm, _head, motionArgs) {
+ var state = getSearchState(cm);
+ var query = state.getQuery();
+ if (!query) {
+ return;
+ }
+ var prev = !motionArgs.forward;
+ // If search is initiated with ? instead of /, negate direction.
+ prev = (state.isReversed()) ? !prev : prev;
+ highlightSearchMatches(cm, query);
+ return findNext(cm, prev/** prev */, query, motionArgs.repeat);
+ },
+ goToMark: function(cm, _head, motionArgs, vim) {
+ var mark = vim.marks[motionArgs.selectedCharacter];
+ if (mark) {
+ var pos = mark.find();
+ return motionArgs.linewise ? { line: pos.line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(pos.line)) } : pos;
+ }
+ return null;
+ },
+ moveToOtherHighlightedEnd: function(cm, _head, motionArgs, vim) {
+ if (vim.visualBlock && motionArgs.sameLine) {
+ var sel = vim.sel;
+ return [
+ clipCursorToContent(cm, Pos(sel.anchor.line, sel.head.ch)),
+ clipCursorToContent(cm, Pos(sel.head.line, sel.anchor.ch))
+ ];
+ } else {
+ return ([vim.sel.head, vim.sel.anchor]);
+ }
+ },
+ jumpToMark: function(cm, head, motionArgs, vim) {
+ var best = head;
+ for (var i = 0; i < motionArgs.repeat; i++) {
+ var cursor = best;
+ for (var key in vim.marks) {
+ if (!isLowerCase(key)) {
+ continue;
+ }
+ var mark = vim.marks[key].find();
+ var isWrongDirection = (motionArgs.forward) ?
+ cursorIsBefore(mark, cursor) : cursorIsBefore(cursor, mark);
+
+ if (isWrongDirection) {
+ continue;
+ }
+ if (motionArgs.linewise && (mark.line == cursor.line)) {
+ continue;
+ }
+
+ var equal = cursorEqual(cursor, best);
+ var between = (motionArgs.forward) ?
+ cursorIsBetween(cursor, mark, best) :
+ cursorIsBetween(best, mark, cursor);
+
+ if (equal || between) {
+ best = mark;
+ }
+ }
+ }
+
+ if (motionArgs.linewise) {
+ // Vim places the cursor on the first non-whitespace character of
+ // the line if there is one, else it places the cursor at the end
+ // of the line, regardless of whether a mark was found.
+ best = Pos(best.line, findFirstNonWhiteSpaceCharacter(cm.getLine(best.line)));
+ }
+ return best;
+ },
+ moveByCharacters: function(_cm, head, motionArgs) {
+ var cur = head;
+ var repeat = motionArgs.repeat;
+ var ch = motionArgs.forward ? cur.ch + repeat : cur.ch - repeat;
+ return Pos(cur.line, ch);
+ },
+ moveByLines: function(cm, head, motionArgs, vim) {
+ var cur = head;
+ var endCh = cur.ch;
+ // Depending what our last motion was, we may want to do different
+ // things. If our last motion was moving vertically, we want to
+ // preserve the HPos from our last horizontal move. If our last motion
+ // was going to the end of a line, moving vertically we should go to
+ // the end of the line, etc.
+ switch (vim.lastMotion) {
+ case this.moveByLines:
+ case this.moveByDisplayLines:
+ case this.moveByScroll:
+ case this.moveToColumn:
+ case this.moveToEol:
+ endCh = vim.lastHPos;
+ break;
+ default:
+ vim.lastHPos = endCh;
+ }
+ var repeat = motionArgs.repeat+(motionArgs.repeatOffset||0);
+ var line = motionArgs.forward ? cur.line + repeat : cur.line - repeat;
+ var first = cm.firstLine();
+ var last = cm.lastLine();
+ // Vim go to line begin or line end when cursor at first/last line and
+ // move to previous/next line is triggered.
+ if (line < first && cur.line == first){
+ return this.moveToStartOfLine(cm, head, motionArgs, vim);
+ }else if (line > last && cur.line == last){
+ return this.moveToEol(cm, head, motionArgs, vim);
+ }
+ if (motionArgs.toFirstChar){
+ endCh=findFirstNonWhiteSpaceCharacter(cm.getLine(line));
+ vim.lastHPos = endCh;
+ }
+ vim.lastHSPos = cm.charCoords(Pos(line, endCh),'div').left;
+ return Pos(line, endCh);
+ },
+ moveByDisplayLines: function(cm, head, motionArgs, vim) {
+ var cur = head;
+ switch (vim.lastMotion) {
+ case this.moveByDisplayLines:
+ case this.moveByScroll:
+ case this.moveByLines:
+ case this.moveToColumn:
+ case this.moveToEol:
+ break;
+ default:
+ vim.lastHSPos = cm.charCoords(cur,'div').left;
+ }
+ var repeat = motionArgs.repeat;
+ var res=cm.findPosV(cur,(motionArgs.forward ? repeat : -repeat),'line',vim.lastHSPos);
+ if (res.hitSide) {
+ if (motionArgs.forward) {
+ var lastCharCoords = cm.charCoords(res, 'div');
+ var goalCoords = { top: lastCharCoords.top + 8, left: vim.lastHSPos };
+ var res = cm.coordsChar(goalCoords, 'div');
+ } else {
+ var resCoords = cm.charCoords(Pos(cm.firstLine(), 0), 'div');
+ resCoords.left = vim.lastHSPos;
+ res = cm.coordsChar(resCoords, 'div');
+ }
+ }
+ vim.lastHPos = res.ch;
+ return res;
+ },
+ moveByPage: function(cm, head, motionArgs) {
+ // CodeMirror only exposes functions that move the cursor page down, so
+ // doing this bad hack to move the cursor and move it back. evalInput
+ // will move the cursor to where it should be in the end.
+ var curStart = head;
+ var repeat = motionArgs.repeat;
+ return cm.findPosV(curStart, (motionArgs.forward ? repeat : -repeat), 'page');
+ },
+ moveByParagraph: function(cm, head, motionArgs) {
+ var dir = motionArgs.forward ? 1 : -1;
+ return findParagraph(cm, head, motionArgs.repeat, dir);
+ },
+ moveByScroll: function(cm, head, motionArgs, vim) {
+ var scrollbox = cm.getScrollInfo();
+ var curEnd = null;
+ var repeat = motionArgs.repeat;
+ if (!repeat) {
+ repeat = scrollbox.clientHeight / (2 * cm.defaultTextHeight());
+ }
+ var orig = cm.charCoords(head, 'local');
+ motionArgs.repeat = repeat;
+ var curEnd = motions.moveByDisplayLines(cm, head, motionArgs, vim);
+ if (!curEnd) {
+ return null;
+ }
+ var dest = cm.charCoords(curEnd, 'local');
+ cm.scrollTo(null, scrollbox.top + dest.top - orig.top);
+ return curEnd;
+ },
+ moveByWords: function(cm, head, motionArgs) {
+ return moveToWord(cm, head, motionArgs.repeat, !!motionArgs.forward,
+ !!motionArgs.wordEnd, !!motionArgs.bigWord);
+ },
+ moveTillCharacter: function(cm, _head, motionArgs) {
+ var repeat = motionArgs.repeat;
+ var curEnd = moveToCharacter(cm, repeat, motionArgs.forward,
+ motionArgs.selectedCharacter);
+ var increment = motionArgs.forward ? -1 : 1;
+ recordLastCharacterSearch(increment, motionArgs);
+ if (!curEnd) return null;
+ curEnd.ch += increment;
+ return curEnd;
+ },
+ moveToCharacter: function(cm, head, motionArgs) {
+ var repeat = motionArgs.repeat;
+ recordLastCharacterSearch(0, motionArgs);
+ return moveToCharacter(cm, repeat, motionArgs.forward,
+ motionArgs.selectedCharacter) || head;
+ },
+ moveToSymbol: function(cm, head, motionArgs) {
+ var repeat = motionArgs.repeat;
+ return findSymbol(cm, repeat, motionArgs.forward,
+ motionArgs.selectedCharacter) || head;
+ },
+ moveToColumn: function(cm, head, motionArgs, vim) {
+ var repeat = motionArgs.repeat;
+ // repeat is equivalent to which column we want to move to!
+ vim.lastHPos = repeat - 1;
+ vim.lastHSPos = cm.charCoords(head,'div').left;
+ return moveToColumn(cm, repeat);
+ },
+ moveToEol: function(cm, head, motionArgs, vim) {
+ var cur = head;
+ vim.lastHPos = Infinity;
+ var retval= Pos(cur.line + motionArgs.repeat - 1, Infinity);
+ var end=cm.clipPos(retval);
+ end.ch--;
+ vim.lastHSPos = cm.charCoords(end,'div').left;
+ return retval;
+ },
+ moveToFirstNonWhiteSpaceCharacter: function(cm, head) {
+ // Go to the start of the line where the text begins, or the end for
+ // whitespace-only lines
+ var cursor = head;
+ return Pos(cursor.line,
+ findFirstNonWhiteSpaceCharacter(cm.getLine(cursor.line)));
+ },
+ moveToMatchedSymbol: function(cm, head) {
+ var cursor = head;
+ var line = cursor.line;
+ var ch = cursor.ch;
+ var lineText = cm.getLine(line);
+ var symbol;
+ do {
+ symbol = lineText.charAt(ch++);
+ if (symbol && isMatchableSymbol(symbol)) {
+ var style = cm.getTokenTypeAt(Pos(line, ch));
+ if (style !== "string" && style !== "comment") {
+ break;
+ }
+ }
+ } while (symbol);
+ if (symbol) {
+ var matched = cm.findMatchingBracket(Pos(line, ch));
+ return matched.to;
+ } else {
+ return cursor;
+ }
+ },
+ moveToStartOfLine: function(_cm, head) {
+ return Pos(head.line, 0);
+ },
+ moveToLineOrEdgeOfDocument: function(cm, _head, motionArgs) {
+ var lineNum = motionArgs.forward ? cm.lastLine() : cm.firstLine();
+ if (motionArgs.repeatIsExplicit) {
+ lineNum = motionArgs.repeat - cm.getOption('firstLineNumber');
+ }
+ return Pos(lineNum,
+ findFirstNonWhiteSpaceCharacter(cm.getLine(lineNum)));
+ },
+ textObjectManipulation: function(cm, head, motionArgs, vim) {
+ // TODO: lots of possible exceptions that can be thrown here. Try da(
+ // outside of a () block.
+
+ // TODO: adding <> >< to this map doesn't work, presumably because
+ // they're operators
+ var mirroredPairs = {'(': ')', ')': '(',
+ '{': '}', '}': '{',
+ '[': ']', ']': '['};
+ var selfPaired = {'\'': true, '"': true};
+
+ var character = motionArgs.selectedCharacter;
+ // 'b' refers to '()' block.
+ // 'B' refers to '{}' block.
+ if (character == 'b') {
+ character = '(';
+ } else if (character == 'B') {
+ character = '{';
+ }
+
+ // Inclusive is the difference between a and i
+ // TODO: Instead of using the additional text object map to perform text
+ // object operations, merge the map into the defaultKeyMap and use
+ // motionArgs to define behavior. Define separate entries for 'aw',
+ // 'iw', 'a[', 'i[', etc.
+ var inclusive = !motionArgs.textObjectInner;
+
+ var tmp;
+ if (mirroredPairs[character]) {
+ tmp = selectCompanionObject(cm, head, character, inclusive);
+ } else if (selfPaired[character]) {
+ tmp = findBeginningAndEnd(cm, head, character, inclusive);
+ } else if (character === 'W') {
+ tmp = expandWordUnderCursor(cm, inclusive, true /** forward */,
+ true /** bigWord */);
+ } else if (character === 'w') {
+ tmp = expandWordUnderCursor(cm, inclusive, true /** forward */,
+ false /** bigWord */);
+ } else if (character === 'p') {
+ tmp = findParagraph(cm, head, motionArgs.repeat, 0, inclusive);
+ motionArgs.linewise = true;
+ if (vim.visualMode) {
+ if (!vim.visualLine) { vim.visualLine = true; }
+ } else {
+ var operatorArgs = vim.inputState.operatorArgs;
+ if (operatorArgs) { operatorArgs.linewise = true; }
+ tmp.end.line--;
+ }
+ } else {
+ // No text object defined for this, don't move.
+ return null;
+ }
+
+ if (!cm.state.vim.visualMode) {
+ return [tmp.start, tmp.end];
+ } else {
+ return expandSelection(cm, tmp.start, tmp.end);
+ }
+ },
+
+ repeatLastCharacterSearch: function(cm, head, motionArgs) {
+ var lastSearch = vimGlobalState.lastCharacterSearch;
+ var repeat = motionArgs.repeat;
+ var forward = motionArgs.forward === lastSearch.forward;
+ var increment = (lastSearch.increment ? 1 : 0) * (forward ? -1 : 1);
+ cm.moveH(-increment, 'char');
+ motionArgs.inclusive = forward ? true : false;
+ var curEnd = moveToCharacter(cm, repeat, forward, lastSearch.selectedCharacter);
+ if (!curEnd) {
+ cm.moveH(increment, 'char');
+ return head;
+ }
+ curEnd.ch += increment;
+ return curEnd;
+ }
+ };
+
+ function defineMotion(name, fn) {
+ motions[name] = fn;
+ }
+
+ function fillArray(val, times) {
+ var arr = [];
+ for (var i = 0; i < times; i++) {
+ arr.push(val);
+ }
+ return arr;
+ }
+ /**
+ * An operator acts on a text selection. It receives the list of selections
+ * as input. The corresponding CodeMirror selection is guaranteed to
+ * match the input selection.
+ */
+ var operators = {
+ change: function(cm, args, ranges) {
+ var finalHead, text;
+ var vim = cm.state.vim;
+ vimGlobalState.macroModeState.lastInsertModeChanges.inVisualBlock = vim.visualBlock;
+ if (!vim.visualMode) {
+ var anchor = ranges[0].anchor,
+ head = ranges[0].head;
+ text = cm.getRange(anchor, head);
+ var lastState = vim.lastEditInputState || {};
+ if (lastState.motion == "moveByWords" && !isWhiteSpaceString(text)) {
+ // Exclude trailing whitespace if the range is not all whitespace.
+ var match = (/\s+$/).exec(text);
+ if (match && lastState.motionArgs && lastState.motionArgs.forward) {
+ head = offsetCursor(head, 0, - match[0].length);
+ text = text.slice(0, - match[0].length);
+ }
+ }
+ var prevLineEnd = new Pos(anchor.line - 1, Number.MAX_VALUE);
+ var wasLastLine = cm.firstLine() == cm.lastLine();
+ if (head.line > cm.lastLine() && args.linewise && !wasLastLine) {
+ cm.replaceRange('', prevLineEnd, head);
+ } else {
+ cm.replaceRange('', anchor, head);
+ }
+ if (args.linewise) {
+ // Push the next line back down, if there is a next line.
+ if (!wasLastLine) {
+ cm.setCursor(prevLineEnd);
+ CodeMirror.commands.newlineAndIndent(cm);
+ }
+ // make sure cursor ends up at the end of the line.
+ anchor.ch = Number.MAX_VALUE;
+ }
+ finalHead = anchor;
+ } else {
+ text = cm.getSelection();
+ var replacement = fillArray('', ranges.length);
+ cm.replaceSelections(replacement);
+ finalHead = cursorMin(ranges[0].head, ranges[0].anchor);
+ }
+ vimGlobalState.registerController.pushText(
+ args.registerName, 'change', text,
+ args.linewise, ranges.length > 1);
+ actions.enterInsertMode(cm, {head: finalHead}, cm.state.vim);
+ },
+ // delete is a javascript keyword.
+ 'delete': function(cm, args, ranges) {
+ var finalHead, text;
+ var vim = cm.state.vim;
+ if (!vim.visualBlock) {
+ var anchor = ranges[0].anchor,
+ head = ranges[0].head;
+ if (args.linewise &&
+ head.line != cm.firstLine() &&
+ anchor.line == cm.lastLine() &&
+ anchor.line == head.line - 1) {
+ // Special case for dd on last line (and first line).
+ if (anchor.line == cm.firstLine()) {
+ anchor.ch = 0;
+ } else {
+ anchor = Pos(anchor.line - 1, lineLength(cm, anchor.line - 1));
+ }
+ }
+ text = cm.getRange(anchor, head);
+ cm.replaceRange('', anchor, head);
+ finalHead = anchor;
+ if (args.linewise) {
+ finalHead = motions.moveToFirstNonWhiteSpaceCharacter(cm, anchor);
+ }
+ } else {
+ text = cm.getSelection();
+ var replacement = fillArray('', ranges.length);
+ cm.replaceSelections(replacement);
+ finalHead = ranges[0].anchor;
+ }
+ vimGlobalState.registerController.pushText(
+ args.registerName, 'delete', text,
+ args.linewise, vim.visualBlock);
+ return clipCursorToContent(cm, finalHead);
+ },
+ indent: function(cm, args, ranges) {
+ var vim = cm.state.vim;
+ var startLine = ranges[0].anchor.line;
+ var endLine = vim.visualBlock ?
+ ranges[ranges.length - 1].anchor.line :
+ ranges[0].head.line;
+ // In visual mode, n> shifts the selection right n times, instead of
+ // shifting n lines right once.
+ var repeat = (vim.visualMode) ? args.repeat : 1;
+ if (args.linewise) {
+ // The only way to delete a newline is to delete until the start of
+ // the next line, so in linewise mode evalInput will include the next
+ // line. We don't want this in indent, so we go back a line.
+ endLine--;
+ }
+ for (var i = startLine; i <= endLine; i++) {
+ for (var j = 0; j < repeat; j++) {
+ cm.indentLine(i, args.indentRight);
+ }
+ }
+ return motions.moveToFirstNonWhiteSpaceCharacter(cm, ranges[0].anchor);
+ },
+ changeCase: function(cm, args, ranges, oldAnchor, newHead) {
+ var selections = cm.getSelections();
+ var swapped = [];
+ var toLower = args.toLower;
+ for (var j = 0; j < selections.length; j++) {
+ var toSwap = selections[j];
+ var text = '';
+ if (toLower === true) {
+ text = toSwap.toLowerCase();
+ } else if (toLower === false) {
+ text = toSwap.toUpperCase();
+ } else {
+ for (var i = 0; i < toSwap.length; i++) {
+ var character = toSwap.charAt(i);
+ text += isUpperCase(character) ? character.toLowerCase() :
+ character.toUpperCase();
+ }
+ }
+ swapped.push(text);
+ }
+ cm.replaceSelections(swapped);
+ if (args.shouldMoveCursor){
+ return newHead;
+ } else if (!cm.state.vim.visualMode && args.linewise && ranges[0].anchor.line + 1 == ranges[0].head.line) {
+ return motions.moveToFirstNonWhiteSpaceCharacter(cm, oldAnchor);
+ } else if (args.linewise){
+ return oldAnchor;
+ } else {
+ return cursorMin(ranges[0].anchor, ranges[0].head);
+ }
+ },
+ yank: function(cm, args, ranges, oldAnchor) {
+ var vim = cm.state.vim;
+ var text = cm.getSelection();
+ var endPos = vim.visualMode
+ ? cursorMin(vim.sel.anchor, vim.sel.head, ranges[0].head, ranges[0].anchor)
+ : oldAnchor;
+ vimGlobalState.registerController.pushText(
+ args.registerName, 'yank',
+ text, args.linewise, vim.visualBlock);
+ return endPos;
+ }
+ };
+
+ function defineOperator(name, fn) {
+ operators[name] = fn;
+ }
+
+ var actions = {
+ jumpListWalk: function(cm, actionArgs, vim) {
+ if (vim.visualMode) {
+ return;
+ }
+ var repeat = actionArgs.repeat;
+ var forward = actionArgs.forward;
+ var jumpList = vimGlobalState.jumpList;
+
+ var mark = jumpList.move(cm, forward ? repeat : -repeat);
+ var markPos = mark ? mark.find() : undefined;
+ markPos = markPos ? markPos : cm.getCursor();
+ cm.setCursor(markPos);
+ },
+ scroll: function(cm, actionArgs, vim) {
+ if (vim.visualMode) {
+ return;
+ }
+ var repeat = actionArgs.repeat || 1;
+ var lineHeight = cm.defaultTextHeight();
+ var top = cm.getScrollInfo().top;
+ var delta = lineHeight * repeat;
+ var newPos = actionArgs.forward ? top + delta : top - delta;
+ var cursor = copyCursor(cm.getCursor());
+ var cursorCoords = cm.charCoords(cursor, 'local');
+ if (actionArgs.forward) {
+ if (newPos > cursorCoords.top) {
+ cursor.line += (newPos - cursorCoords.top) / lineHeight;
+ cursor.line = Math.ceil(cursor.line);
+ cm.setCursor(cursor);
+ cursorCoords = cm.charCoords(cursor, 'local');
+ cm.scrollTo(null, cursorCoords.top);
+ } else {
+ // Cursor stays within bounds. Just reposition the scroll window.
+ cm.scrollTo(null, newPos);
+ }
+ } else {
+ var newBottom = newPos + cm.getScrollInfo().clientHeight;
+ if (newBottom < cursorCoords.bottom) {
+ cursor.line -= (cursorCoords.bottom - newBottom) / lineHeight;
+ cursor.line = Math.floor(cursor.line);
+ cm.setCursor(cursor);
+ cursorCoords = cm.charCoords(cursor, 'local');
+ cm.scrollTo(
+ null, cursorCoords.bottom - cm.getScrollInfo().clientHeight);
+ } else {
+ // Cursor stays within bounds. Just reposition the scroll window.
+ cm.scrollTo(null, newPos);
+ }
+ }
+ },
+ scrollToCursor: function(cm, actionArgs) {
+ var lineNum = cm.getCursor().line;
+ var charCoords = cm.charCoords(Pos(lineNum, 0), 'local');
+ var height = cm.getScrollInfo().clientHeight;
+ var y = charCoords.top;
+ var lineHeight = charCoords.bottom - y;
+ switch (actionArgs.position) {
+ case 'center': y = y - (height / 2) + lineHeight;
+ break;
+ case 'bottom': y = y - height + lineHeight;
+ break;
+ }
+ cm.scrollTo(null, y);
+ },
+ replayMacro: function(cm, actionArgs, vim) {
+ var registerName = actionArgs.selectedCharacter;
+ var repeat = actionArgs.repeat;
+ var macroModeState = vimGlobalState.macroModeState;
+ if (registerName == '@') {
+ registerName = macroModeState.latestRegister;
+ }
+ while(repeat--){
+ executeMacroRegister(cm, vim, macroModeState, registerName);
+ }
+ },
+ enterMacroRecordMode: function(cm, actionArgs) {
+ var macroModeState = vimGlobalState.macroModeState;
+ var registerName = actionArgs.selectedCharacter;
+ macroModeState.enterMacroRecordMode(cm, registerName);
+ },
+ enterInsertMode: function(cm, actionArgs, vim) {
+ if (cm.getOption('readOnly')) { return; }
+ vim.insertMode = true;
+ vim.insertModeRepeat = actionArgs && actionArgs.repeat || 1;
+ var insertAt = (actionArgs) ? actionArgs.insertAt : null;
+ var sel = vim.sel;
+ var head = actionArgs.head || cm.getCursor('head');
+ var height = cm.listSelections().length;
+ if (insertAt == 'eol') {
+ head = Pos(head.line, lineLength(cm, head.line));
+ } else if (insertAt == 'charAfter') {
+ head = offsetCursor(head, 0, 1);
+ } else if (insertAt == 'firstNonBlank') {
+ head = motions.moveToFirstNonWhiteSpaceCharacter(cm, head);
+ } else if (insertAt == 'startOfSelectedArea') {
+ if (!vim.visualBlock) {
+ if (sel.head.line < sel.anchor.line) {
+ head = sel.head;
+ } else {
+ head = Pos(sel.anchor.line, 0);
+ }
+ } else {
+ head = Pos(
+ Math.min(sel.head.line, sel.anchor.line),
+ Math.min(sel.head.ch, sel.anchor.ch));
+ height = Math.abs(sel.head.line - sel.anchor.line) + 1;
+ }
+ } else if (insertAt == 'endOfSelectedArea') {
+ if (!vim.visualBlock) {
+ if (sel.head.line >= sel.anchor.line) {
+ head = offsetCursor(sel.head, 0, 1);
+ } else {
+ head = Pos(sel.anchor.line, 0);
+ }
+ } else {
+ head = Pos(
+ Math.min(sel.head.line, sel.anchor.line),
+ Math.max(sel.head.ch + 1, sel.anchor.ch));
+ height = Math.abs(sel.head.line - sel.anchor.line) + 1;
+ }
+ } else if (insertAt == 'inplace') {
+ if (vim.visualMode){
+ return;
+ }
+ }
+ cm.setOption('keyMap', 'vim-insert');
+ cm.setOption('disableInput', false);
+ if (actionArgs && actionArgs.replace) {
+ // Handle Replace-mode as a special case of insert mode.
+ cm.toggleOverwrite(true);
+ cm.setOption('keyMap', 'vim-replace');
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "replace"});
+ } else {
+ cm.setOption('keyMap', 'vim-insert');
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "insert"});
+ }
+ if (!vimGlobalState.macroModeState.isPlaying) {
+ // Only record if not replaying.
+ cm.on('change', onChange);
+ CodeMirror.on(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown);
+ }
+ if (vim.visualMode) {
+ exitVisualMode(cm);
+ }
+ selectForInsert(cm, head, height);
+ },
+ toggleVisualMode: function(cm, actionArgs, vim) {
+ var repeat = actionArgs.repeat;
+ var anchor = cm.getCursor();
+ var head;
+ // TODO: The repeat should actually select number of characters/lines
+ // equal to the repeat times the size of the previous visual
+ // operation.
+ if (!vim.visualMode) {
+ // Entering visual mode
+ vim.visualMode = true;
+ vim.visualLine = !!actionArgs.linewise;
+ vim.visualBlock = !!actionArgs.blockwise;
+ head = clipCursorToContent(
+ cm, Pos(anchor.line, anchor.ch + repeat - 1),
+ true /** includeLineBreak */);
+ vim.sel = {
+ anchor: anchor,
+ head: head
+ };
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : vim.visualBlock ? "blockwise" : ""});
+ updateCmSelection(cm);
+ updateMark(cm, vim, '<', cursorMin(anchor, head));
+ updateMark(cm, vim, '>', cursorMax(anchor, head));
+ } else if (vim.visualLine ^ actionArgs.linewise ||
+ vim.visualBlock ^ actionArgs.blockwise) {
+ // Toggling between modes
+ vim.visualLine = !!actionArgs.linewise;
+ vim.visualBlock = !!actionArgs.blockwise;
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : vim.visualBlock ? "blockwise" : ""});
+ updateCmSelection(cm);
+ } else {
+ exitVisualMode(cm);
+ }
+ },
+ reselectLastSelection: function(cm, _actionArgs, vim) {
+ var lastSelection = vim.lastSelection;
+ if (vim.visualMode) {
+ updateLastSelection(cm, vim);
+ }
+ if (lastSelection) {
+ var anchor = lastSelection.anchorMark.find();
+ var head = lastSelection.headMark.find();
+ if (!anchor || !head) {
+ // If the marks have been destroyed due to edits, do nothing.
+ return;
+ }
+ vim.sel = {
+ anchor: anchor,
+ head: head
+ };
+ vim.visualMode = true;
+ vim.visualLine = lastSelection.visualLine;
+ vim.visualBlock = lastSelection.visualBlock;
+ updateCmSelection(cm);
+ updateMark(cm, vim, '<', cursorMin(anchor, head));
+ updateMark(cm, vim, '>', cursorMax(anchor, head));
+ CodeMirror.signal(cm, 'vim-mode-change', {
+ mode: 'visual',
+ subMode: vim.visualLine ? 'linewise' :
+ vim.visualBlock ? 'blockwise' : ''});
+ }
+ },
+ joinLines: function(cm, actionArgs, vim) {
+ var curStart, curEnd;
+ if (vim.visualMode) {
+ curStart = cm.getCursor('anchor');
+ curEnd = cm.getCursor('head');
+ if (cursorIsBefore(curEnd, curStart)) {
+ var tmp = curEnd;
+ curEnd = curStart;
+ curStart = tmp;
+ }
+ curEnd.ch = lineLength(cm, curEnd.line) - 1;
+ } else {
+ // Repeat is the number of lines to join. Minimum 2 lines.
+ var repeat = Math.max(actionArgs.repeat, 2);
+ curStart = cm.getCursor();
+ curEnd = clipCursorToContent(cm, Pos(curStart.line + repeat - 1,
+ Infinity));
+ }
+ var finalCh = 0;
+ for (var i = curStart.line; i < curEnd.line; i++) {
+ finalCh = lineLength(cm, curStart.line);
+ var tmp = Pos(curStart.line + 1,
+ lineLength(cm, curStart.line + 1));
+ var text = cm.getRange(curStart, tmp);
+ text = text.replace(/\n\s*/g, ' ');
+ cm.replaceRange(text, curStart, tmp);
+ }
+ var curFinalPos = Pos(curStart.line, finalCh);
+ if (vim.visualMode) {
+ exitVisualMode(cm, false);
+ }
+ cm.setCursor(curFinalPos);
+ },
+ newLineAndEnterInsertMode: function(cm, actionArgs, vim) {
+ vim.insertMode = true;
+ var insertAt = copyCursor(cm.getCursor());
+ if (insertAt.line === cm.firstLine() && !actionArgs.after) {
+ // Special case for inserting newline before start of document.
+ cm.replaceRange('\n', Pos(cm.firstLine(), 0));
+ cm.setCursor(cm.firstLine(), 0);
+ } else {
+ insertAt.line = (actionArgs.after) ? insertAt.line :
+ insertAt.line - 1;
+ insertAt.ch = lineLength(cm, insertAt.line);
+ cm.setCursor(insertAt);
+ var newlineFn = CodeMirror.commands.newlineAndIndentContinueComment ||
+ CodeMirror.commands.newlineAndIndent;
+ newlineFn(cm);
+ }
+ this.enterInsertMode(cm, { repeat: actionArgs.repeat }, vim);
+ },
+ paste: function(cm, actionArgs, vim) {
+ var cur = copyCursor(cm.getCursor());
+ var register = vimGlobalState.registerController.getRegister(
+ actionArgs.registerName);
+ var text = register.toString();
+ if (!text) {
+ return;
+ }
+ if (actionArgs.matchIndent) {
+ var tabSize = cm.getOption("tabSize");
+ // length that considers tabs and tabSize
+ var whitespaceLength = function(str) {
+ var tabs = (str.split("\t").length - 1);
+ var spaces = (str.split(" ").length - 1);
+ return tabs * tabSize + spaces * 1;
+ };
+ var currentLine = cm.getLine(cm.getCursor().line);
+ var indent = whitespaceLength(currentLine.match(/^\s*/)[0]);
+ // chomp last newline b/c don't want it to match /^\s*/gm
+ var chompedText = text.replace(/\n$/, '');
+ var wasChomped = text !== chompedText;
+ var firstIndent = whitespaceLength(text.match(/^\s*/)[0]);
+ var text = chompedText.replace(/^\s*/gm, function(wspace) {
+ var newIndent = indent + (whitespaceLength(wspace) - firstIndent);
+ if (newIndent < 0) {
+ return "";
+ }
+ else if (cm.getOption("indentWithTabs")) {
+ var quotient = Math.floor(newIndent / tabSize);
+ return Array(quotient + 1).join('\t');
+ }
+ else {
+ return Array(newIndent + 1).join(' ');
+ }
+ });
+ text += wasChomped ? "\n" : "";
+ }
+ if (actionArgs.repeat > 1) {
+ var text = Array(actionArgs.repeat + 1).join(text);
+ }
+ var linewise = register.linewise;
+ var blockwise = register.blockwise;
+ if (linewise) {
+ if(vim.visualMode) {
+ text = vim.visualLine ? text.slice(0, -1) : '\n' + text.slice(0, text.length - 1) + '\n';
+ } else if (actionArgs.after) {
+ // Move the newline at the end to the start instead, and paste just
+ // before the newline character of the line we are on right now.
+ text = '\n' + text.slice(0, text.length - 1);
+ cur.ch = lineLength(cm, cur.line);
+ } else {
+ cur.ch = 0;
+ }
+ } else {
+ if (blockwise) {
+ text = text.split('\n');
+ for (var i = 0; i < text.length; i++) {
+ text[i] = (text[i] == '') ? ' ' : text[i];
+ }
+ }
+ cur.ch += actionArgs.after ? 1 : 0;
+ }
+ var curPosFinal;
+ var idx;
+ if (vim.visualMode) {
+ // save the pasted text for reselection if the need arises
+ vim.lastPastedText = text;
+ var lastSelectionCurEnd;
+ var selectedArea = getSelectedAreaRange(cm, vim);
+ var selectionStart = selectedArea[0];
+ var selectionEnd = selectedArea[1];
+ var selectedText = cm.getSelection();
+ var selections = cm.listSelections();
+ var emptyStrings = new Array(selections.length).join('1').split('1');
+ // save the curEnd marker before it get cleared due to cm.replaceRange.
+ if (vim.lastSelection) {
+ lastSelectionCurEnd = vim.lastSelection.headMark.find();
+ }
+ // push the previously selected text to unnamed register
+ vimGlobalState.registerController.unnamedRegister.setText(selectedText);
+ if (blockwise) {
+ // first delete the selected text
+ cm.replaceSelections(emptyStrings);
+ // Set new selections as per the block length of the yanked text
+ selectionEnd = Pos(selectionStart.line + text.length-1, selectionStart.ch);
+ cm.setCursor(selectionStart);
+ selectBlock(cm, selectionEnd);
+ cm.replaceSelections(text);
+ curPosFinal = selectionStart;
+ } else if (vim.visualBlock) {
+ cm.replaceSelections(emptyStrings);
+ cm.setCursor(selectionStart);
+ cm.replaceRange(text, selectionStart, selectionStart);
+ curPosFinal = selectionStart;
+ } else {
+ cm.replaceRange(text, selectionStart, selectionEnd);
+ curPosFinal = cm.posFromIndex(cm.indexFromPos(selectionStart) + text.length - 1);
+ }
+ // restore the the curEnd marker
+ if(lastSelectionCurEnd) {
+ vim.lastSelection.headMark = cm.setBookmark(lastSelectionCurEnd);
+ }
+ if (linewise) {
+ curPosFinal.ch=0;
+ }
+ } else {
+ if (blockwise) {
+ cm.setCursor(cur);
+ for (var i = 0; i < text.length; i++) {
+ var line = cur.line+i;
+ if (line > cm.lastLine()) {
+ cm.replaceRange('\n', Pos(line, 0));
+ }
+ var lastCh = lineLength(cm, line);
+ if (lastCh < cur.ch) {
+ extendLineToColumn(cm, line, cur.ch);
+ }
+ }
+ cm.setCursor(cur);
+ selectBlock(cm, Pos(cur.line + text.length-1, cur.ch));
+ cm.replaceSelections(text);
+ curPosFinal = cur;
+ } else {
+ cm.replaceRange(text, cur);
+ // Now fine tune the cursor to where we want it.
+ if (linewise && actionArgs.after) {
+ curPosFinal = Pos(
+ cur.line + 1,
+ findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line + 1)));
+ } else if (linewise && !actionArgs.after) {
+ curPosFinal = Pos(
+ cur.line,
+ findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line)));
+ } else if (!linewise && actionArgs.after) {
+ idx = cm.indexFromPos(cur);
+ curPosFinal = cm.posFromIndex(idx + text.length - 1);
+ } else {
+ idx = cm.indexFromPos(cur);
+ curPosFinal = cm.posFromIndex(idx + text.length);
+ }
+ }
+ }
+ if (vim.visualMode) {
+ exitVisualMode(cm, false);
+ }
+ cm.setCursor(curPosFinal);
+ },
+ undo: function(cm, actionArgs) {
+ cm.operation(function() {
+ repeatFn(cm, CodeMirror.commands.undo, actionArgs.repeat)();
+ cm.setCursor(cm.getCursor('anchor'));
+ });
+ },
+ redo: function(cm, actionArgs) {
+ repeatFn(cm, CodeMirror.commands.redo, actionArgs.repeat)();
+ },
+ setRegister: function(_cm, actionArgs, vim) {
+ vim.inputState.registerName = actionArgs.selectedCharacter;
+ },
+ setMark: function(cm, actionArgs, vim) {
+ var markName = actionArgs.selectedCharacter;
+ updateMark(cm, vim, markName, cm.getCursor());
+ },
+ replace: function(cm, actionArgs, vim) {
+ var replaceWith = actionArgs.selectedCharacter;
+ var curStart = cm.getCursor();
+ var replaceTo;
+ var curEnd;
+ var selections = cm.listSelections();
+ if (vim.visualMode) {
+ curStart = cm.getCursor('start');
+ curEnd = cm.getCursor('end');
+ } else {
+ var line = cm.getLine(curStart.line);
+ replaceTo = curStart.ch + actionArgs.repeat;
+ if (replaceTo > line.length) {
+ replaceTo=line.length;
+ }
+ curEnd = Pos(curStart.line, replaceTo);
+ }
+ if (replaceWith=='\n') {
+ if (!vim.visualMode) cm.replaceRange('', curStart, curEnd);
+ // special case, where vim help says to replace by just one line-break
+ (CodeMirror.commands.newlineAndIndentContinueComment || CodeMirror.commands.newlineAndIndent)(cm);
+ } else {
+ var replaceWithStr = cm.getRange(curStart, curEnd);
+ //replace all characters in range by selected, but keep linebreaks
+ replaceWithStr = replaceWithStr.replace(/[^\n]/g, replaceWith);
+ if (vim.visualBlock) {
+ // Tabs are split in visua block before replacing
+ var spaces = new Array(cm.getOption("tabSize")+1).join(' ');
+ replaceWithStr = cm.getSelection();
+ replaceWithStr = replaceWithStr.replace(/\t/g, spaces).replace(/[^\n]/g, replaceWith).split('\n');
+ cm.replaceSelections(replaceWithStr);
+ } else {
+ cm.replaceRange(replaceWithStr, curStart, curEnd);
+ }
+ if (vim.visualMode) {
+ curStart = cursorIsBefore(selections[0].anchor, selections[0].head) ?
+ selections[0].anchor : selections[0].head;
+ cm.setCursor(curStart);
+ exitVisualMode(cm, false);
+ } else {
+ cm.setCursor(offsetCursor(curEnd, 0, -1));
+ }
+ }
+ },
+ incrementNumberToken: function(cm, actionArgs) {
+ var cur = cm.getCursor();
+ var lineStr = cm.getLine(cur.line);
+ var re = /-?\d+/g;
+ var match;
+ var start;
+ var end;
+ var numberStr;
+ var token;
+ while ((match = re.exec(lineStr)) !== null) {
+ token = match[0];
+ start = match.index;
+ end = start + token.length;
+ if (cur.ch < end)break;
+ }
+ if (!actionArgs.backtrack && (end <= cur.ch))return;
+ if (token) {
+ var increment = actionArgs.increase ? 1 : -1;
+ var number = parseInt(token) + (increment * actionArgs.repeat);
+ var from = Pos(cur.line, start);
+ var to = Pos(cur.line, end);
+ numberStr = number.toString();
+ cm.replaceRange(numberStr, from, to);
+ } else {
+ return;
+ }
+ cm.setCursor(Pos(cur.line, start + numberStr.length - 1));
+ },
+ repeatLastEdit: function(cm, actionArgs, vim) {
+ var lastEditInputState = vim.lastEditInputState;
+ if (!lastEditInputState) { return; }
+ var repeat = actionArgs.repeat;
+ if (repeat && actionArgs.repeatIsExplicit) {
+ vim.lastEditInputState.repeatOverride = repeat;
+ } else {
+ repeat = vim.lastEditInputState.repeatOverride || repeat;
+ }
+ repeatLastEdit(cm, vim, repeat, false /** repeatForInsert */);
+ },
+ exitInsertMode: exitInsertMode
+ };
+
+ function defineAction(name, fn) {
+ actions[name] = fn;
+ }
+
+ /*
+ * Below are miscellaneous utility functions used by vim.js
+ */
+
+ /**
+ * Clips cursor to ensure that line is within the buffer's range
+ * If includeLineBreak is true, then allow cur.ch == lineLength.
+ */
+ function clipCursorToContent(cm, cur, includeLineBreak) {
+ var line = Math.min(Math.max(cm.firstLine(), cur.line), cm.lastLine() );
+ var maxCh = lineLength(cm, line) - 1;
+ maxCh = (includeLineBreak) ? maxCh + 1 : maxCh;
+ var ch = Math.min(Math.max(0, cur.ch), maxCh);
+ return Pos(line, ch);
+ }
+ function copyArgs(args) {
+ var ret = {};
+ for (var prop in args) {
+ if (args.hasOwnProperty(prop)) {
+ ret[prop] = args[prop];
+ }
+ }
+ return ret;
+ }
+ function offsetCursor(cur, offsetLine, offsetCh) {
+ if (typeof offsetLine === 'object') {
+ offsetCh = offsetLine.ch;
+ offsetLine = offsetLine.line;
+ }
+ return Pos(cur.line + offsetLine, cur.ch + offsetCh);
+ }
+ function getOffset(anchor, head) {
+ return {
+ line: head.line - anchor.line,
+ ch: head.line - anchor.line
+ };
+ }
+ function commandMatches(keys, keyMap, context, inputState) {
+ // Partial matches are not applied. They inform the key handler
+ // that the current key sequence is a subsequence of a valid key
+ // sequence, so that the key buffer is not cleared.
+ var match, partial = [], full = [];
+ for (var i = 0; i < keyMap.length; i++) {
+ var command = keyMap[i];
+ if (context == 'insert' && command.context != 'insert' ||
+ command.context && command.context != context ||
+ inputState.operator && command.type == 'action' ||
+ !(match = commandMatch(keys, command.keys))) { continue; }
+ if (match == 'partial') { partial.push(command); }
+ if (match == 'full') { full.push(command); }
+ }
+ return {
+ partial: partial.length && partial,
+ full: full.length && full
+ };
+ }
+ function commandMatch(pressed, mapped) {
+ if (mapped.slice(-11) == '<character>') {
+ // Last character matches anything.
+ var prefixLen = mapped.length - 11;
+ var pressedPrefix = pressed.slice(0, prefixLen);
+ var mappedPrefix = mapped.slice(0, prefixLen);
+ return pressedPrefix == mappedPrefix && pressed.length > prefixLen ? 'full' :
+ mappedPrefix.indexOf(pressedPrefix) == 0 ? 'partial' : false;
+ } else {
+ return pressed == mapped ? 'full' :
+ mapped.indexOf(pressed) == 0 ? 'partial' : false;
+ }
+ }
+ function lastChar(keys) {
+ var match = /^.*(<[\w\-]+>)$/.exec(keys);
+ var selectedCharacter = match ? match[1] : keys.slice(-1);
+ if (selectedCharacter.length > 1){
+ switch(selectedCharacter){
+ case '<CR>':
+ selectedCharacter='\n';
+ break;
+ case '<Space>':
+ selectedCharacter=' ';
+ break;
+ default:
+ break;
+ }
+ }
+ return selectedCharacter;
+ }
+ function repeatFn(cm, fn, repeat) {
+ return function() {
+ for (var i = 0; i < repeat; i++) {
+ fn(cm);
+ }
+ };
+ }
+ function copyCursor(cur) {
+ return Pos(cur.line, cur.ch);
+ }
+ function cursorEqual(cur1, cur2) {
+ return cur1.ch == cur2.ch && cur1.line == cur2.line;
+ }
+ function cursorIsBefore(cur1, cur2) {
+ if (cur1.line < cur2.line) {
+ return true;
+ }
+ if (cur1.line == cur2.line && cur1.ch < cur2.ch) {
+ return true;
+ }
+ return false;
+ }
+ function cursorMin(cur1, cur2) {
+ if (arguments.length > 2) {
+ cur2 = cursorMin.apply(undefined, Array.prototype.slice.call(arguments, 1));
+ }
+ return cursorIsBefore(cur1, cur2) ? cur1 : cur2;
+ }
+ function cursorMax(cur1, cur2) {
+ if (arguments.length > 2) {
+ cur2 = cursorMax.apply(undefined, Array.prototype.slice.call(arguments, 1));
+ }
+ return cursorIsBefore(cur1, cur2) ? cur2 : cur1;
+ }
+ function cursorIsBetween(cur1, cur2, cur3) {
+ // returns true if cur2 is between cur1 and cur3.
+ var cur1before2 = cursorIsBefore(cur1, cur2);
+ var cur2before3 = cursorIsBefore(cur2, cur3);
+ return cur1before2 && cur2before3;
+ }
+ function lineLength(cm, lineNum) {
+ return cm.getLine(lineNum).length;
+ }
+ function trim(s) {
+ if (s.trim) {
+ return s.trim();
+ }
+ return s.replace(/^\s+|\s+$/g, '');
+ }
+ function escapeRegex(s) {
+ return s.replace(/([.?*+$\[\]\/\\(){}|\-])/g, '\\$1');
+ }
+ function extendLineToColumn(cm, lineNum, column) {
+ var endCh = lineLength(cm, lineNum);
+ var spaces = new Array(column-endCh+1).join(' ');
+ cm.setCursor(Pos(lineNum, endCh));
+ cm.replaceRange(spaces, cm.getCursor());
+ }
+ // This functions selects a rectangular block
+ // of text with selectionEnd as any of its corner
+ // Height of block:
+ // Difference in selectionEnd.line and first/last selection.line
+ // Width of the block:
+ // Distance between selectionEnd.ch and any(first considered here) selection.ch
+ function selectBlock(cm, selectionEnd) {
+ var selections = [], ranges = cm.listSelections();
+ var head = copyCursor(cm.clipPos(selectionEnd));
+ var isClipped = !cursorEqual(selectionEnd, head);
+ var curHead = cm.getCursor('head');
+ var primIndex = getIndex(ranges, curHead);
+ var wasClipped = cursorEqual(ranges[primIndex].head, ranges[primIndex].anchor);
+ var max = ranges.length - 1;
+ var index = max - primIndex > primIndex ? max : 0;
+ var base = ranges[index].anchor;
+
+ var firstLine = Math.min(base.line, head.line);
+ var lastLine = Math.max(base.line, head.line);
+ var baseCh = base.ch, headCh = head.ch;
+
+ var dir = ranges[index].head.ch - baseCh;
+ var newDir = headCh - baseCh;
+ if (dir > 0 && newDir <= 0) {
+ baseCh++;
+ if (!isClipped) { headCh--; }
+ } else if (dir < 0 && newDir >= 0) {
+ baseCh--;
+ if (!wasClipped) { headCh++; }
+ } else if (dir < 0 && newDir == -1) {
+ baseCh--;
+ headCh++;
+ }
+ for (var line = firstLine; line <= lastLine; line++) {
+ var range = {anchor: new Pos(line, baseCh), head: new Pos(line, headCh)};
+ selections.push(range);
+ }
+ primIndex = head.line == lastLine ? selections.length - 1 : 0;
+ cm.setSelections(selections);
+ selectionEnd.ch = headCh;
+ base.ch = baseCh;
+ return base;
+ }
+ function selectForInsert(cm, head, height) {
+ var sel = [];
+ for (var i = 0; i < height; i++) {
+ var lineHead = offsetCursor(head, i, 0);
+ sel.push({anchor: lineHead, head: lineHead});
+ }
+ cm.setSelections(sel, 0);
+ }
+ // getIndex returns the index of the cursor in the selections.
+ function getIndex(ranges, cursor, end) {
+ for (var i = 0; i < ranges.length; i++) {
+ var atAnchor = end != 'head' && cursorEqual(ranges[i].anchor, cursor);
+ var atHead = end != 'anchor' && cursorEqual(ranges[i].head, cursor);
+ if (atAnchor || atHead) {
+ return i;
+ }
+ }
+ return -1;
+ }
+ function getSelectedAreaRange(cm, vim) {
+ var lastSelection = vim.lastSelection;
+ var getCurrentSelectedAreaRange = function() {
+ var selections = cm.listSelections();
+ var start = selections[0];
+ var end = selections[selections.length-1];
+ var selectionStart = cursorIsBefore(start.anchor, start.head) ? start.anchor : start.head;
+ var selectionEnd = cursorIsBefore(end.anchor, end.head) ? end.head : end.anchor;
+ return [selectionStart, selectionEnd];
+ };
+ var getLastSelectedAreaRange = function() {
+ var selectionStart = cm.getCursor();
+ var selectionEnd = cm.getCursor();
+ var block = lastSelection.visualBlock;
+ if (block) {
+ var width = block.width;
+ var height = block.height;
+ selectionEnd = Pos(selectionStart.line + height, selectionStart.ch + width);
+ var selections = [];
+ // selectBlock creates a 'proper' rectangular block.
+ // We do not want that in all cases, so we manually set selections.
+ for (var i = selectionStart.line; i < selectionEnd.line; i++) {
+ var anchor = Pos(i, selectionStart.ch);
+ var head = Pos(i, selectionEnd.ch);
+ var range = {anchor: anchor, head: head};
+ selections.push(range);
+ }
+ cm.setSelections(selections);
+ } else {
+ var start = lastSelection.anchorMark.find();
+ var end = lastSelection.headMark.find();
+ var line = end.line - start.line;
+ var ch = end.ch - start.ch;
+ selectionEnd = {line: selectionEnd.line + line, ch: line ? selectionEnd.ch : ch + selectionEnd.ch};
+ if (lastSelection.visualLine) {
+ selectionStart = Pos(selectionStart.line, 0);
+ selectionEnd = Pos(selectionEnd.line, lineLength(cm, selectionEnd.line));
+ }
+ cm.setSelection(selectionStart, selectionEnd);
+ }
+ return [selectionStart, selectionEnd];
+ };
+ if (!vim.visualMode) {
+ // In case of replaying the action.
+ return getLastSelectedAreaRange();
+ } else {
+ return getCurrentSelectedAreaRange();
+ }
+ }
+ // Updates the previous selection with the current selection's values. This
+ // should only be called in visual mode.
+ function updateLastSelection(cm, vim) {
+ var anchor = vim.sel.anchor;
+ var head = vim.sel.head;
+ // To accommodate the effect of lastPastedText in the last selection
+ if (vim.lastPastedText) {
+ head = cm.posFromIndex(cm.indexFromPos(anchor) + vim.lastPastedText.length);
+ vim.lastPastedText = null;
+ }
+ vim.lastSelection = {'anchorMark': cm.setBookmark(anchor),
+ 'headMark': cm.setBookmark(head),
+ 'anchor': copyCursor(anchor),
+ 'head': copyCursor(head),
+ 'visualMode': vim.visualMode,
+ 'visualLine': vim.visualLine,
+ 'visualBlock': vim.visualBlock};
+ }
+ function expandSelection(cm, start, end) {
+ var sel = cm.state.vim.sel;
+ var head = sel.head;
+ var anchor = sel.anchor;
+ var tmp;
+ if (cursorIsBefore(end, start)) {
+ tmp = end;
+ end = start;
+ start = tmp;
+ }
+ if (cursorIsBefore(head, anchor)) {
+ head = cursorMin(start, head);
+ anchor = cursorMax(anchor, end);
+ } else {
+ anchor = cursorMin(start, anchor);
+ head = cursorMax(head, end);
+ head = offsetCursor(head, 0, -1);
+ if (head.ch == -1 && head.line != cm.firstLine()) {
+ head = Pos(head.line - 1, lineLength(cm, head.line - 1));
+ }
+ }
+ return [anchor, head];
+ }
+ /**
+ * Updates the CodeMirror selection to match the provided vim selection.
+ * If no arguments are given, it uses the current vim selection state.
+ */
+ function updateCmSelection(cm, sel, mode) {
+ var vim = cm.state.vim;
+ sel = sel || vim.sel;
+ var mode = mode ||
+ vim.visualLine ? 'line' : vim.visualBlock ? 'block' : 'char';
+ var cmSel = makeCmSelection(cm, sel, mode);
+ cm.setSelections(cmSel.ranges, cmSel.primary);
+ updateFakeCursor(cm);
+ }
+ function makeCmSelection(cm, sel, mode, exclusive) {
+ var head = copyCursor(sel.head);
+ var anchor = copyCursor(sel.anchor);
+ if (mode == 'char') {
+ var headOffset = !exclusive && !cursorIsBefore(sel.head, sel.anchor) ? 1 : 0;
+ var anchorOffset = cursorIsBefore(sel.head, sel.anchor) ? 1 : 0;
+ head = offsetCursor(sel.head, 0, headOffset);
+ anchor = offsetCursor(sel.anchor, 0, anchorOffset);
+ return {
+ ranges: [{anchor: anchor, head: head}],
+ primary: 0
+ };
+ } else if (mode == 'line') {
+ if (!cursorIsBefore(sel.head, sel.anchor)) {
+ anchor.ch = 0;
+
+ var lastLine = cm.lastLine();
+ if (head.line > lastLine) {
+ head.line = lastLine;
+ }
+ head.ch = lineLength(cm, head.line);
+ } else {
+ head.ch = 0;
+ anchor.ch = lineLength(cm, anchor.line);
+ }
+ return {
+ ranges: [{anchor: anchor, head: head}],
+ primary: 0
+ };
+ } else if (mode == 'block') {
+ var top = Math.min(anchor.line, head.line),
+ left = Math.min(anchor.ch, head.ch),
+ bottom = Math.max(anchor.line, head.line),
+ right = Math.max(anchor.ch, head.ch) + 1;
+ var height = bottom - top + 1;
+ var primary = head.line == top ? 0 : height - 1;
+ var ranges = [];
+ for (var i = 0; i < height; i++) {
+ ranges.push({
+ anchor: Pos(top + i, left),
+ head: Pos(top + i, right)
+ });
+ }
+ return {
+ ranges: ranges,
+ primary: primary
+ };
+ }
+ }
+ function getHead(cm) {
+ var cur = cm.getCursor('head');
+ if (cm.getSelection().length == 1) {
+ // Small corner case when only 1 character is selected. The "real"
+ // head is the left of head and anchor.
+ cur = cursorMin(cur, cm.getCursor('anchor'));
+ }
+ return cur;
+ }
+
+ /**
+ * If moveHead is set to false, the CodeMirror selection will not be
+ * touched. The caller assumes the responsibility of putting the cursor
+ * in the right place.
+ */
+ function exitVisualMode(cm, moveHead) {
+ var vim = cm.state.vim;
+ if (moveHead !== false) {
+ cm.setCursor(clipCursorToContent(cm, vim.sel.head));
+ }
+ updateLastSelection(cm, vim);
+ vim.visualMode = false;
+ vim.visualLine = false;
+ vim.visualBlock = false;
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"});
+ if (vim.fakeCursor) {
+ vim.fakeCursor.clear();
+ }
+ }
+
+ // Remove any trailing newlines from the selection. For
+ // example, with the caret at the start of the last word on the line,
+ // 'dw' should word, but not the newline, while 'w' should advance the
+ // caret to the first character of the next line.
+ function clipToLine(cm, curStart, curEnd) {
+ var selection = cm.getRange(curStart, curEnd);
+ // Only clip if the selection ends with trailing newline + whitespace
+ if (/\n\s*$/.test(selection)) {
+ var lines = selection.split('\n');
+ // We know this is all whitespace.
+ lines.pop();
+
+ // Cases:
+ // 1. Last word is an empty line - do not clip the trailing '\n'
+ // 2. Last word is not an empty line - clip the trailing '\n'
+ var line;
+ // Find the line containing the last word, and clip all whitespace up
+ // to it.
+ for (var line = lines.pop(); lines.length > 0 && line && isWhiteSpaceString(line); line = lines.pop()) {
+ curEnd.line--;
+ curEnd.ch = 0;
+ }
+ // If the last word is not an empty line, clip an additional newline
+ if (line) {
+ curEnd.line--;
+ curEnd.ch = lineLength(cm, curEnd.line);
+ } else {
+ curEnd.ch = 0;
+ }
+ }
+ }
+
+ // Expand the selection to line ends.
+ function expandSelectionToLine(_cm, curStart, curEnd) {
+ curStart.ch = 0;
+ curEnd.ch = 0;
+ curEnd.line++;
+ }
+
+ function findFirstNonWhiteSpaceCharacter(text) {
+ if (!text) {
+ return 0;
+ }
+ var firstNonWS = text.search(/\S/);
+ return firstNonWS == -1 ? text.length : firstNonWS;
+ }
+
+ function expandWordUnderCursor(cm, inclusive, _forward, bigWord, noSymbol) {
+ var cur = getHead(cm);
+ var line = cm.getLine(cur.line);
+ var idx = cur.ch;
+
+ // Seek to first word or non-whitespace character, depending on if
+ // noSymbol is true.
+ var test = noSymbol ? wordCharTest[0] : bigWordCharTest [0];
+ while (!test(line.charAt(idx))) {
+ idx++;
+ if (idx >= line.length) { return null; }
+ }
+
+ if (bigWord) {
+ test = bigWordCharTest[0];
+ } else {
+ test = wordCharTest[0];
+ if (!test(line.charAt(idx))) {
+ test = wordCharTest[1];
+ }
+ }
+
+ var end = idx, start = idx;
+ while (test(line.charAt(end)) && end < line.length) { end++; }
+ while (test(line.charAt(start)) && start >= 0) { start--; }
+ start++;
+
+ if (inclusive) {
+ // If present, include all whitespace after word.
+ // Otherwise, include all whitespace before word, except indentation.
+ var wordEnd = end;
+ while (/\s/.test(line.charAt(end)) && end < line.length) { end++; }
+ if (wordEnd == end) {
+ var wordStart = start;
+ while (/\s/.test(line.charAt(start - 1)) && start > 0) { start--; }
+ if (!start) { start = wordStart; }
+ }
+ }
+ return { start: Pos(cur.line, start), end: Pos(cur.line, end) };
+ }
+
+ function recordJumpPosition(cm, oldCur, newCur) {
+ if (!cursorEqual(oldCur, newCur)) {
+ vimGlobalState.jumpList.add(cm, oldCur, newCur);
+ }
+ }
+
+ function recordLastCharacterSearch(increment, args) {
+ vimGlobalState.lastCharacterSearch.increment = increment;
+ vimGlobalState.lastCharacterSearch.forward = args.forward;
+ vimGlobalState.lastCharacterSearch.selectedCharacter = args.selectedCharacter;
+ }
+
+ var symbolToMode = {
+ '(': 'bracket', ')': 'bracket', '{': 'bracket', '}': 'bracket',
+ '[': 'section', ']': 'section',
+ '*': 'comment', '/': 'comment',
+ 'm': 'method', 'M': 'method',
+ '#': 'preprocess'
+ };
+ var findSymbolModes = {
+ bracket: {
+ isComplete: function(state) {
+ if (state.nextCh === state.symb) {
+ state.depth++;
+ if (state.depth >= 1)return true;
+ } else if (state.nextCh === state.reverseSymb) {
+ state.depth--;
+ }
+ return false;
+ }
+ },
+ section: {
+ init: function(state) {
+ state.curMoveThrough = true;
+ state.symb = (state.forward ? ']' : '[') === state.symb ? '{' : '}';
+ },
+ isComplete: function(state) {
+ return state.index === 0 && state.nextCh === state.symb;
+ }
+ },
+ comment: {
+ isComplete: function(state) {
+ var found = state.lastCh === '*' && state.nextCh === '/';
+ state.lastCh = state.nextCh;
+ return found;
+ }
+ },
+ // TODO: The original Vim implementation only operates on level 1 and 2.
+ // The current implementation doesn't check for code block level and
+ // therefore it operates on any levels.
+ method: {
+ init: function(state) {
+ state.symb = (state.symb === 'm' ? '{' : '}');
+ state.reverseSymb = state.symb === '{' ? '}' : '{';
+ },
+ isComplete: function(state) {
+ if (state.nextCh === state.symb)return true;
+ return false;
+ }
+ },
+ preprocess: {
+ init: function(state) {
+ state.index = 0;
+ },
+ isComplete: function(state) {
+ if (state.nextCh === '#') {
+ var token = state.lineText.match(/#(\w+)/)[1];
+ if (token === 'endif') {
+ if (state.forward && state.depth === 0) {
+ return true;
+ }
+ state.depth++;
+ } else if (token === 'if') {
+ if (!state.forward && state.depth === 0) {
+ return true;
+ }
+ state.depth--;
+ }
+ if (token === 'else' && state.depth === 0)return true;
+ }
+ return false;
+ }
+ }
+ };
+ function findSymbol(cm, repeat, forward, symb) {
+ var cur = copyCursor(cm.getCursor());
+ var increment = forward ? 1 : -1;
+ var endLine = forward ? cm.lineCount() : -1;
+ var curCh = cur.ch;
+ var line = cur.line;
+ var lineText = cm.getLine(line);
+ var state = {
+ lineText: lineText,
+ nextCh: lineText.charAt(curCh),
+ lastCh: null,
+ index: curCh,
+ symb: symb,
+ reverseSymb: (forward ? { ')': '(', '}': '{' } : { '(': ')', '{': '}' })[symb],
+ forward: forward,
+ depth: 0,
+ curMoveThrough: false
+ };
+ var mode = symbolToMode[symb];
+ if (!mode)return cur;
+ var init = findSymbolModes[mode].init;
+ var isComplete = findSymbolModes[mode].isComplete;
+ if (init) { init(state); }
+ while (line !== endLine && repeat) {
+ state.index += increment;
+ state.nextCh = state.lineText.charAt(state.index);
+ if (!state.nextCh) {
+ line += increment;
+ state.lineText = cm.getLine(line) || '';
+ if (increment > 0) {
+ state.index = 0;
+ } else {
+ var lineLen = state.lineText.length;
+ state.index = (lineLen > 0) ? (lineLen-1) : 0;
+ }
+ state.nextCh = state.lineText.charAt(state.index);
+ }
+ if (isComplete(state)) {
+ cur.line = line;
+ cur.ch = state.index;
+ repeat--;
+ }
+ }
+ if (state.nextCh || state.curMoveThrough) {
+ return Pos(line, state.index);
+ }
+ return cur;
+ }
+
+ /**
+ * Returns the boundaries of the next word. If the cursor in the middle of
+ * the word, then returns the boundaries of the current word, starting at
+ * the cursor. If the cursor is at the start/end of a word, and we are going
+ * forward/backward, respectively, find the boundaries of the next word.
+ *
+ * @param {CodeMirror} cm CodeMirror object.
+ * @param {Cursor} cur The cursor position.
+ * @param {boolean} forward True to search forward. False to search
+ * backward.
+ * @param {boolean} bigWord True if punctuation count as part of the word.
+ * False if only [a-zA-Z0-9] characters count as part of the word.
+ * @param {boolean} emptyLineIsWord True if empty lines should be treated
+ * as words.
+ * @return {Object{from:number, to:number, line: number}} The boundaries of
+ * the word, or null if there are no more words.
+ */
+ function findWord(cm, cur, forward, bigWord, emptyLineIsWord) {
+ var lineNum = cur.line;
+ var pos = cur.ch;
+ var line = cm.getLine(lineNum);
+ var dir = forward ? 1 : -1;
+ var charTests = bigWord ? bigWordCharTest: wordCharTest;
+
+ if (emptyLineIsWord && line == '') {
+ lineNum += dir;
+ line = cm.getLine(lineNum);
+ if (!isLine(cm, lineNum)) {
+ return null;
+ }
+ pos = (forward) ? 0 : line.length;
+ }
+
+ while (true) {
+ if (emptyLineIsWord && line == '') {
+ return { from: 0, to: 0, line: lineNum };
+ }
+ var stop = (dir > 0) ? line.length : -1;
+ var wordStart = stop, wordEnd = stop;
+ // Find bounds of next word.
+ while (pos != stop) {
+ var foundWord = false;
+ for (var i = 0; i < charTests.length && !foundWord; ++i) {
+ if (charTests[i](line.charAt(pos))) {
+ wordStart = pos;
+ // Advance to end of word.
+ while (pos != stop && charTests[i](line.charAt(pos))) {
+ pos += dir;
+ }
+ wordEnd = pos;
+ foundWord = wordStart != wordEnd;
+ if (wordStart == cur.ch && lineNum == cur.line &&
+ wordEnd == wordStart + dir) {
+ // We started at the end of a word. Find the next one.
+ continue;
+ } else {
+ return {
+ from: Math.min(wordStart, wordEnd + 1),
+ to: Math.max(wordStart, wordEnd),
+ line: lineNum };
+ }
+ }
+ }
+ if (!foundWord) {
+ pos += dir;
+ }
+ }
+ // Advance to next/prev line.
+ lineNum += dir;
+ if (!isLine(cm, lineNum)) {
+ return null;
+ }
+ line = cm.getLine(lineNum);
+ pos = (dir > 0) ? 0 : line.length;
+ }
+ }
+
+ /**
+ * @param {CodeMirror} cm CodeMirror object.
+ * @param {Pos} cur The position to start from.
+ * @param {int} repeat Number of words to move past.
+ * @param {boolean} forward True to search forward. False to search
+ * backward.
+ * @param {boolean} wordEnd True to move to end of word. False to move to
+ * beginning of word.
+ * @param {boolean} bigWord True if punctuation count as part of the word.
+ * False if only alphabet characters count as part of the word.
+ * @return {Cursor} The position the cursor should move to.
+ */
+ function moveToWord(cm, cur, repeat, forward, wordEnd, bigWord) {
+ var curStart = copyCursor(cur);
+ var words = [];
+ if (forward && !wordEnd || !forward && wordEnd) {
+ repeat++;
+ }
+ // For 'e', empty lines are not considered words, go figure.
+ var emptyLineIsWord = !(forward && wordEnd);
+ for (var i = 0; i < repeat; i++) {
+ var word = findWord(cm, cur, forward, bigWord, emptyLineIsWord);
+ if (!word) {
+ var eodCh = lineLength(cm, cm.lastLine());
+ words.push(forward
+ ? {line: cm.lastLine(), from: eodCh, to: eodCh}
+ : {line: 0, from: 0, to: 0});
+ break;
+ }
+ words.push(word);
+ cur = Pos(word.line, forward ? (word.to - 1) : word.from);
+ }
+ var shortCircuit = words.length != repeat;
+ var firstWord = words[0];
+ var lastWord = words.pop();
+ if (forward && !wordEnd) {
+ // w
+ if (!shortCircuit && (firstWord.from != curStart.ch || firstWord.line != curStart.line)) {
+ // We did not start in the middle of a word. Discard the extra word at the end.
+ lastWord = words.pop();
+ }
+ return Pos(lastWord.line, lastWord.from);
+ } else if (forward && wordEnd) {
+ return Pos(lastWord.line, lastWord.to - 1);
+ } else if (!forward && wordEnd) {
+ // ge
+ if (!shortCircuit && (firstWord.to != curStart.ch || firstWord.line != curStart.line)) {
+ // We did not start in the middle of a word. Discard the extra word at the end.
+ lastWord = words.pop();
+ }
+ return Pos(lastWord.line, lastWord.to);
+ } else {
+ // b
+ return Pos(lastWord.line, lastWord.from);
+ }
+ }
+
+ function moveToCharacter(cm, repeat, forward, character) {
+ var cur = cm.getCursor();
+ var start = cur.ch;
+ var idx;
+ for (var i = 0; i < repeat; i ++) {
+ var line = cm.getLine(cur.line);
+ idx = charIdxInLine(start, line, character, forward, true);
+ if (idx == -1) {
+ return null;
+ }
+ start = idx;
+ }
+ return Pos(cm.getCursor().line, idx);
+ }
+
+ function moveToColumn(cm, repeat) {
+ // repeat is always >= 1, so repeat - 1 always corresponds
+ // to the column we want to go to.
+ var line = cm.getCursor().line;
+ return clipCursorToContent(cm, Pos(line, repeat - 1));
+ }
+
+ function updateMark(cm, vim, markName, pos) {
+ if (!inArray(markName, validMarks)) {
+ return;
+ }
+ if (vim.marks[markName]) {
+ vim.marks[markName].clear();
+ }
+ vim.marks[markName] = cm.setBookmark(pos);
+ }
+
+ function charIdxInLine(start, line, character, forward, includeChar) {
+ // Search for char in line.
+ // motion_options: {forward, includeChar}
+ // If includeChar = true, include it too.
+ // If forward = true, search forward, else search backwards.
+ // If char is not found on this line, do nothing
+ var idx;
+ if (forward) {
+ idx = line.indexOf(character, start + 1);
+ if (idx != -1 && !includeChar) {
+ idx -= 1;
+ }
+ } else {
+ idx = line.lastIndexOf(character, start - 1);
+ if (idx != -1 && !includeChar) {
+ idx += 1;
+ }
+ }
+ return idx;
+ }
+
+ function findParagraph(cm, head, repeat, dir, inclusive) {
+ var line = head.line;
+ var min = cm.firstLine();
+ var max = cm.lastLine();
+ var start, end, i = line;
+ function isEmpty(i) { return !cm.getLine(i); }
+ function isBoundary(i, dir, any) {
+ if (any) { return isEmpty(i) != isEmpty(i + dir); }
+ return !isEmpty(i) && isEmpty(i + dir);
+ }
+ if (dir) {
+ while (min <= i && i <= max && repeat > 0) {
+ if (isBoundary(i, dir)) { repeat--; }
+ i += dir;
+ }
+ return new Pos(i, 0);
+ }
+
+ var vim = cm.state.vim;
+ if (vim.visualLine && isBoundary(line, 1, true)) {
+ var anchor = vim.sel.anchor;
+ if (isBoundary(anchor.line, -1, true)) {
+ if (!inclusive || anchor.line != line) {
+ line += 1;
+ }
+ }
+ }
+ var startState = isEmpty(line);
+ for (i = line; i <= max && repeat; i++) {
+ if (isBoundary(i, 1, true)) {
+ if (!inclusive || isEmpty(i) != startState) {
+ repeat--;
+ }
+ }
+ }
+ end = new Pos(i, 0);
+ // select boundary before paragraph for the last one
+ if (i > max && !startState) { startState = true; }
+ else { inclusive = false; }
+ for (i = line; i > min; i--) {
+ if (!inclusive || isEmpty(i) == startState || i == line) {
+ if (isBoundary(i, -1, true)) { break; }
+ }
+ }
+ start = new Pos(i, 0);
+ return { start: start, end: end };
+ }
+
+ // TODO: perhaps this finagling of start and end positions belonds
+ // in codemirror/replaceRange?
+ function selectCompanionObject(cm, head, symb, inclusive) {
+ var cur = head, start, end;
+
+ var bracketRegexp = ({
+ '(': /[()]/, ')': /[()]/,
+ '[': /[[\]]/, ']': /[[\]]/,
+ '{': /[{}]/, '}': /[{}]/})[symb];
+ var openSym = ({
+ '(': '(', ')': '(',
+ '[': '[', ']': '[',
+ '{': '{', '}': '{'})[symb];
+ var curChar = cm.getLine(cur.line).charAt(cur.ch);
+ // Due to the behavior of scanForBracket, we need to add an offset if the
+ // cursor is on a matching open bracket.
+ var offset = curChar === openSym ? 1 : 0;
+
+ start = cm.scanForBracket(Pos(cur.line, cur.ch + offset), -1, null, {'bracketRegex': bracketRegexp});
+ end = cm.scanForBracket(Pos(cur.line, cur.ch + offset), 1, null, {'bracketRegex': bracketRegexp});
+
+ if (!start || !end) {
+ return { start: cur, end: cur };
+ }
+
+ start = start.pos;
+ end = end.pos;
+
+ if ((start.line == end.line && start.ch > end.ch)
+ || (start.line > end.line)) {
+ var tmp = start;
+ start = end;
+ end = tmp;
+ }
+
+ if (inclusive) {
+ end.ch += 1;
+ } else {
+ start.ch += 1;
+ }
+
+ return { start: start, end: end };
+ }
+
+ // Takes in a symbol and a cursor and tries to simulate text objects that
+ // have identical opening and closing symbols
+ // TODO support across multiple lines
+ function findBeginningAndEnd(cm, head, symb, inclusive) {
+ var cur = copyCursor(head);
+ var line = cm.getLine(cur.line);
+ var chars = line.split('');
+ var start, end, i, len;
+ var firstIndex = chars.indexOf(symb);
+
+ // the decision tree is to always look backwards for the beginning first,
+ // but if the cursor is in front of the first instance of the symb,
+ // then move the cursor forward
+ if (cur.ch < firstIndex) {
+ cur.ch = firstIndex;
+ // Why is this line even here???
+ // cm.setCursor(cur.line, firstIndex+1);
+ }
+ // otherwise if the cursor is currently on the closing symbol
+ else if (firstIndex < cur.ch && chars[cur.ch] == symb) {
+ end = cur.ch; // assign end to the current cursor
+ --cur.ch; // make sure to look backwards
+ }
+
+ // if we're currently on the symbol, we've got a start
+ if (chars[cur.ch] == symb && !end) {
+ start = cur.ch + 1; // assign start to ahead of the cursor
+ } else {
+ // go backwards to find the start
+ for (i = cur.ch; i > -1 && !start; i--) {
+ if (chars[i] == symb) {
+ start = i + 1;
+ }
+ }
+ }
+
+ // look forwards for the end symbol
+ if (start && !end) {
+ for (i = start, len = chars.length; i < len && !end; i++) {
+ if (chars[i] == symb) {
+ end = i;
+ }
+ }
+ }
+
+ // nothing found
+ if (!start || !end) {
+ return { start: cur, end: cur };
+ }
+
+ // include the symbols
+ if (inclusive) {
+ --start; ++end;
+ }
+
+ return {
+ start: Pos(cur.line, start),
+ end: Pos(cur.line, end)
+ };
+ }
+
+ // Search functions
+ defineOption('pcre', true, 'boolean');
+ function SearchState() {}
+ SearchState.prototype = {
+ getQuery: function() {
+ return vimGlobalState.query;
+ },
+ setQuery: function(query) {
+ vimGlobalState.query = query;
+ },
+ getOverlay: function() {
+ return this.searchOverlay;
+ },
+ setOverlay: function(overlay) {
+ this.searchOverlay = overlay;
+ },
+ isReversed: function() {
+ return vimGlobalState.isReversed;
+ },
+ setReversed: function(reversed) {
+ vimGlobalState.isReversed = reversed;
+ },
+ getScrollbarAnnotate: function() {
+ return this.annotate;
+ },
+ setScrollbarAnnotate: function(annotate) {
+ this.annotate = annotate;
+ }
+ };
+ function getSearchState(cm) {
+ var vim = cm.state.vim;
+ return vim.searchState_ || (vim.searchState_ = new SearchState());
+ }
+ function dialog(cm, template, shortText, onClose, options) {
+ if (cm.openDialog) {
+ cm.openDialog(template, onClose, { bottom: true, value: options.value,
+ onKeyDown: options.onKeyDown, onKeyUp: options.onKeyUp,
+ selectValueOnOpen: false});
+ }
+ else {
+ onClose(prompt(shortText, ''));
+ }
+ }
+ function splitBySlash(argString) {
+ var slashes = findUnescapedSlashes(argString) || [];
+ if (!slashes.length) return [];
+ var tokens = [];
+ // in case of strings like foo/bar
+ if (slashes[0] !== 0) return;
+ for (var i = 0; i < slashes.length; i++) {
+ if (typeof slashes[i] == 'number')
+ tokens.push(argString.substring(slashes[i] + 1, slashes[i+1]));
+ }
+ return tokens;
+ }
+
+ function findUnescapedSlashes(str) {
+ var escapeNextChar = false;
+ var slashes = [];
+ for (var i = 0; i < str.length; i++) {
+ var c = str.charAt(i);
+ if (!escapeNextChar && c == '/') {
+ slashes.push(i);
+ }
+ escapeNextChar = !escapeNextChar && (c == '\\');
+ }
+ return slashes;
+ }
+
+ // Translates a search string from ex (vim) syntax into javascript form.
+ function translateRegex(str) {
+ // When these match, add a '\' if unescaped or remove one if escaped.
+ var specials = '|(){';
+ // Remove, but never add, a '\' for these.
+ var unescape = '}';
+ var escapeNextChar = false;
+ var out = [];
+ for (var i = -1; i < str.length; i++) {
+ var c = str.charAt(i) || '';
+ var n = str.charAt(i+1) || '';
+ var specialComesNext = (n && specials.indexOf(n) != -1);
+ if (escapeNextChar) {
+ if (c !== '\\' || !specialComesNext) {
+ out.push(c);
+ }
+ escapeNextChar = false;
+ } else {
+ if (c === '\\') {
+ escapeNextChar = true;
+ // Treat the unescape list as special for removing, but not adding '\'.
+ if (n && unescape.indexOf(n) != -1) {
+ specialComesNext = true;
+ }
+ // Not passing this test means removing a '\'.
+ if (!specialComesNext || n === '\\') {
+ out.push(c);
+ }
+ } else {
+ out.push(c);
+ if (specialComesNext && n !== '\\') {
+ out.push('\\');
+ }
+ }
+ }
+ }
+ return out.join('');
+ }
+
+ // Translates the replace part of a search and replace from ex (vim) syntax into
+ // javascript form. Similar to translateRegex, but additionally fixes back references
+ // (translates '\[0..9]' to '$[0..9]') and follows different rules for escaping '$'.
+ var charUnescapes = {'\\n': '\n', '\\r': '\r', '\\t': '\t'};
+ function translateRegexReplace(str) {
+ var escapeNextChar = false;
+ var out = [];
+ for (var i = -1; i < str.length; i++) {
+ var c = str.charAt(i) || '';
+ var n = str.charAt(i+1) || '';
+ if (charUnescapes[c + n]) {
+ out.push(charUnescapes[c+n]);
+ i++;
+ } else if (escapeNextChar) {
+ // At any point in the loop, escapeNextChar is true if the previous
+ // character was a '\' and was not escaped.
+ out.push(c);
+ escapeNextChar = false;
+ } else {
+ if (c === '\\') {
+ escapeNextChar = true;
+ if ((isNumber(n) || n === '$')) {
+ out.push('$');
+ } else if (n !== '/' && n !== '\\') {
+ out.push('\\');
+ }
+ } else {
+ if (c === '$') {
+ out.push('$');
+ }
+ out.push(c);
+ if (n === '/') {
+ out.push('\\');
+ }
+ }
+ }
+ }
+ return out.join('');
+ }
+
+ // Unescape \ and / in the replace part, for PCRE mode.
+ var unescapes = {'\\/': '/', '\\\\': '\\', '\\n': '\n', '\\r': '\r', '\\t': '\t'};
+ function unescapeRegexReplace(str) {
+ var stream = new CodeMirror.StringStream(str);
+ var output = [];
+ while (!stream.eol()) {
+ // Search for \.
+ while (stream.peek() && stream.peek() != '\\') {
+ output.push(stream.next());
+ }
+ var matched = false;
+ for (var matcher in unescapes) {
+ if (stream.match(matcher, true)) {
+ matched = true;
+ output.push(unescapes[matcher]);
+ break;
+ }
+ }
+ if (!matched) {
+ // Don't change anything
+ output.push(stream.next());
+ }
+ }
+ return output.join('');
+ }
+
+ /**
+ * Extract the regular expression from the query and return a Regexp object.
+ * Returns null if the query is blank.
+ * If ignoreCase is passed in, the Regexp object will have the 'i' flag set.
+ * If smartCase is passed in, and the query contains upper case letters,
+ * then ignoreCase is overridden, and the 'i' flag will not be set.
+ * If the query contains the /i in the flag part of the regular expression,
+ * then both ignoreCase and smartCase are ignored, and 'i' will be passed
+ * through to the Regex object.
+ */
+ function parseQuery(query, ignoreCase, smartCase) {
+ // First update the last search register
+ var lastSearchRegister = vimGlobalState.registerController.getRegister('/');
+ lastSearchRegister.setText(query);
+ // Check if the query is already a regex.
+ if (query instanceof RegExp) { return query; }
+ // First try to extract regex + flags from the input. If no flags found,
+ // extract just the regex. IE does not accept flags directly defined in
+ // the regex string in the form /regex/flags
+ var slashes = findUnescapedSlashes(query);
+ var regexPart;
+ var forceIgnoreCase;
+ if (!slashes.length) {
+ // Query looks like 'regexp'
+ regexPart = query;
+ } else {
+ // Query looks like 'regexp/...'
+ regexPart = query.substring(0, slashes[0]);
+ var flagsPart = query.substring(slashes[0]);
+ forceIgnoreCase = (flagsPart.indexOf('i') != -1);
+ }
+ if (!regexPart) {
+ return null;
+ }
+ if (!getOption('pcre')) {
+ regexPart = translateRegex(regexPart);
+ }
+ if (smartCase) {
+ ignoreCase = (/^[^A-Z]*$/).test(regexPart);
+ }
+ var regexp = new RegExp(regexPart,
+ (ignoreCase || forceIgnoreCase) ? 'i' : undefined);
+ return regexp;
+ }
+ function showConfirm(cm, text) {
+ if (cm.openNotification) {
+ cm.openNotification('<span style="color: red">' + text + '</span>',
+ {bottom: true, duration: 5000});
+ } else {
+ alert(text);
+ }
+ }
+ function makePrompt(prefix, desc) {
+ var raw = '<span style="font-family: monospace; white-space: pre">' +
+ (prefix || "") + '<input type="text"></span>';
+ if (desc)
+ raw += ' <span style="color: #888">' + desc + '</span>';
+ return raw;
+ }
+ var searchPromptDesc = '(Javascript regexp)';
+ function showPrompt(cm, options) {
+ var shortText = (options.prefix || '') + ' ' + (options.desc || '');
+ var prompt = makePrompt(options.prefix, options.desc);
+ dialog(cm, prompt, shortText, options.onClose, options);
+ }
+ function regexEqual(r1, r2) {
+ if (r1 instanceof RegExp && r2 instanceof RegExp) {
+ var props = ['global', 'multiline', 'ignoreCase', 'source'];
+ for (var i = 0; i < props.length; i++) {
+ var prop = props[i];
+ if (r1[prop] !== r2[prop]) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+ // Returns true if the query is valid.
+ function updateSearchQuery(cm, rawQuery, ignoreCase, smartCase) {
+ if (!rawQuery) {
+ return;
+ }
+ var state = getSearchState(cm);
+ var query = parseQuery(rawQuery, !!ignoreCase, !!smartCase);
+ if (!query) {
+ return;
+ }
+ highlightSearchMatches(cm, query);
+ if (regexEqual(query, state.getQuery())) {
+ return query;
+ }
+ state.setQuery(query);
+ return query;
+ }
+ function searchOverlay(query) {
+ if (query.source.charAt(0) == '^') {
+ var matchSol = true;
+ }
+ return {
+ token: function(stream) {
+ if (matchSol && !stream.sol()) {
+ stream.skipToEnd();
+ return;
+ }
+ var match = stream.match(query, false);
+ if (match) {
+ if (match[0].length == 0) {
+ // Matched empty string, skip to next.
+ stream.next();
+ return 'searching';
+ }
+ if (!stream.sol()) {
+ // Backtrack 1 to match \b
+ stream.backUp(1);
+ if (!query.exec(stream.next() + match[0])) {
+ stream.next();
+ return null;
+ }
+ }
+ stream.match(query);
+ return 'searching';
+ }
+ while (!stream.eol()) {
+ stream.next();
+ if (stream.match(query, false)) break;
+ }
+ },
+ query: query
+ };
+ }
+ function highlightSearchMatches(cm, query) {
+ var searchState = getSearchState(cm);
+ var overlay = searchState.getOverlay();
+ if (!overlay || query != overlay.query) {
+ if (overlay) {
+ cm.removeOverlay(overlay);
+ }
+ overlay = searchOverlay(query);
+ cm.addOverlay(overlay);
+ if (cm.showMatchesOnScrollbar) {
+ if (searchState.getScrollbarAnnotate()) {
+ searchState.getScrollbarAnnotate().clear();
+ }
+ searchState.setScrollbarAnnotate(cm.showMatchesOnScrollbar(query));
+ }
+ searchState.setOverlay(overlay);
+ }
+ }
+ function findNext(cm, prev, query, repeat) {
+ if (repeat === undefined) { repeat = 1; }
+ return cm.operation(function() {
+ var pos = cm.getCursor();
+ var cursor = cm.getSearchCursor(query, pos);
+ for (var i = 0; i < repeat; i++) {
+ var found = cursor.find(prev);
+ if (i == 0 && found && cursorEqual(cursor.from(), pos)) { found = cursor.find(prev); }
+ if (!found) {
+ // SearchCursor may have returned null because it hit EOF, wrap
+ // around and try again.
+ cursor = cm.getSearchCursor(query,
+ (prev) ? Pos(cm.lastLine()) : Pos(cm.firstLine(), 0) );
+ if (!cursor.find(prev)) {
+ return;
+ }
+ }
+ }
+ return cursor.from();
+ });
+ }
+ function clearSearchHighlight(cm) {
+ var state = getSearchState(cm);
+ cm.removeOverlay(getSearchState(cm).getOverlay());
+ state.setOverlay(null);
+ if (state.getScrollbarAnnotate()) {
+ state.getScrollbarAnnotate().clear();
+ state.setScrollbarAnnotate(null);
+ }
+ }
+ /**
+ * Check if pos is in the specified range, INCLUSIVE.
+ * Range can be specified with 1 or 2 arguments.
+ * If the first range argument is an array, treat it as an array of line
+ * numbers. Match pos against any of the lines.
+ * If the first range argument is a number,
+ * if there is only 1 range argument, check if pos has the same line
+ * number
+ * if there are 2 range arguments, then check if pos is in between the two
+ * range arguments.
+ */
+ function isInRange(pos, start, end) {
+ if (typeof pos != 'number') {
+ // Assume it is a cursor position. Get the line number.
+ pos = pos.line;
+ }
+ if (start instanceof Array) {
+ return inArray(pos, start);
+ } else {
+ if (end) {
+ return (pos >= start && pos <= end);
+ } else {
+ return pos == start;
+ }
+ }
+ }
+ function getUserVisibleLines(cm) {
+ var scrollInfo = cm.getScrollInfo();
+ var occludeToleranceTop = 6;
+ var occludeToleranceBottom = 10;
+ var from = cm.coordsChar({left:0, top: occludeToleranceTop + scrollInfo.top}, 'local');
+ var bottomY = scrollInfo.clientHeight - occludeToleranceBottom + scrollInfo.top;
+ var to = cm.coordsChar({left:0, top: bottomY}, 'local');
+ return {top: from.line, bottom: to.line};
+ }
+
+ var ExCommandDispatcher = function() {
+ this.buildCommandMap_();
+ };
+ ExCommandDispatcher.prototype = {
+ processCommand: function(cm, input, opt_params) {
+ var that = this;
+ cm.operation(function () {
+ cm.curOp.isVimOp = true;
+ that._processCommand(cm, input, opt_params);
+ });
+ },
+ _processCommand: function(cm, input, opt_params) {
+ var vim = cm.state.vim;
+ var commandHistoryRegister = vimGlobalState.registerController.getRegister(':');
+ var previousCommand = commandHistoryRegister.toString();
+ if (vim.visualMode) {
+ exitVisualMode(cm);
+ }
+ var inputStream = new CodeMirror.StringStream(input);
+ // update ": with the latest command whether valid or invalid
+ commandHistoryRegister.setText(input);
+ var params = opt_params || {};
+ params.input = input;
+ try {
+ this.parseInput_(cm, inputStream, params);
+ } catch(e) {
+ showConfirm(cm, e);
+ throw e;
+ }
+ var command;
+ var commandName;
+ if (!params.commandName) {
+ // If only a line range is defined, move to the line.
+ if (params.line !== undefined) {
+ commandName = 'move';
+ }
+ } else {
+ command = this.matchCommand_(params.commandName);
+ if (command) {
+ commandName = command.name;
+ if (command.excludeFromCommandHistory) {
+ commandHistoryRegister.setText(previousCommand);
+ }
+ this.parseCommandArgs_(inputStream, params, command);
+ if (command.type == 'exToKey') {
+ // Handle Ex to Key mapping.
+ for (var i = 0; i < command.toKeys.length; i++) {
+ CodeMirror.Vim.handleKey(cm, command.toKeys[i], 'mapping');
+ }
+ return;
+ } else if (command.type == 'exToEx') {
+ // Handle Ex to Ex mapping.
+ this.processCommand(cm, command.toInput);
+ return;
+ }
+ }
+ }
+ if (!commandName) {
+ showConfirm(cm, 'Not an editor command ":' + input + '"');
+ return;
+ }
+ try {
+ exCommands[commandName](cm, params);
+ // Possibly asynchronous commands (e.g. substitute, which might have a
+ // user confirmation), are responsible for calling the callback when
+ // done. All others have it taken care of for them here.
+ if ((!command || !command.possiblyAsync) && params.callback) {
+ params.callback();
+ }
+ } catch(e) {
+ showConfirm(cm, e);
+ throw e;
+ }
+ },
+ parseInput_: function(cm, inputStream, result) {
+ inputStream.eatWhile(':');
+ // Parse range.
+ if (inputStream.eat('%')) {
+ result.line = cm.firstLine();
+ result.lineEnd = cm.lastLine();
+ } else {
+ result.line = this.parseLineSpec_(cm, inputStream);
+ if (result.line !== undefined && inputStream.eat(',')) {
+ result.lineEnd = this.parseLineSpec_(cm, inputStream);
+ }
+ }
+
+ // Parse command name.
+ var commandMatch = inputStream.match(/^(\w+)/);
+ if (commandMatch) {
+ result.commandName = commandMatch[1];
+ } else {
+ result.commandName = inputStream.match(/.*/)[0];
+ }
+
+ return result;
+ },
+ parseLineSpec_: function(cm, inputStream) {
+ var numberMatch = inputStream.match(/^(\d+)/);
+ if (numberMatch) {
+ return parseInt(numberMatch[1], 10) - 1;
+ }
+ switch (inputStream.next()) {
+ case '.':
+ return cm.getCursor().line;
+ case '$':
+ return cm.lastLine();
+ case '\'':
+ var mark = cm.state.vim.marks[inputStream.next()];
+ if (mark && mark.find()) {
+ return mark.find().line;
+ }
+ throw new Error('Mark not set');
+ default:
+ inputStream.backUp(1);
+ return undefined;
+ }
+ },
+ parseCommandArgs_: function(inputStream, params, command) {
+ if (inputStream.eol()) {
+ return;
+ }
+ params.argString = inputStream.match(/.*/)[0];
+ // Parse command-line arguments
+ var delim = command.argDelimiter || /\s+/;
+ var args = trim(params.argString).split(delim);
+ if (args.length && args[0]) {
+ params.args = args;
+ }
+ },
+ matchCommand_: function(commandName) {
+ // Return the command in the command map that matches the shortest
+ // prefix of the passed in command name. The match is guaranteed to be
+ // unambiguous if the defaultExCommandMap's shortNames are set up
+ // correctly. (see @code{defaultExCommandMap}).
+ for (var i = commandName.length; i > 0; i--) {
+ var prefix = commandName.substring(0, i);
+ if (this.commandMap_[prefix]) {
+ var command = this.commandMap_[prefix];
+ if (command.name.indexOf(commandName) === 0) {
+ return command;
+ }
+ }
+ }
+ return null;
+ },
+ buildCommandMap_: function() {
+ this.commandMap_ = {};
+ for (var i = 0; i < defaultExCommandMap.length; i++) {
+ var command = defaultExCommandMap[i];
+ var key = command.shortName || command.name;
+ this.commandMap_[key] = command;
+ }
+ },
+ map: function(lhs, rhs, ctx) {
+ if (lhs != ':' && lhs.charAt(0) == ':') {
+ if (ctx) { throw Error('Mode not supported for ex mappings'); }
+ var commandName = lhs.substring(1);
+ if (rhs != ':' && rhs.charAt(0) == ':') {
+ // Ex to Ex mapping
+ this.commandMap_[commandName] = {
+ name: commandName,
+ type: 'exToEx',
+ toInput: rhs.substring(1),
+ user: true
+ };
+ } else {
+ // Ex to key mapping
+ this.commandMap_[commandName] = {
+ name: commandName,
+ type: 'exToKey',
+ toKeys: rhs,
+ user: true
+ };
+ }
+ } else {
+ if (rhs != ':' && rhs.charAt(0) == ':') {
+ // Key to Ex mapping.
+ var mapping = {
+ keys: lhs,
+ type: 'keyToEx',
+ exArgs: { input: rhs.substring(1) },
+ user: true};
+ if (ctx) { mapping.context = ctx; }
+ defaultKeymap.unshift(mapping);
+ } else {
+ // Key to key mapping
+ var mapping = {
+ keys: lhs,
+ type: 'keyToKey',
+ toKeys: rhs,
+ user: true
+ };
+ if (ctx) { mapping.context = ctx; }
+ defaultKeymap.unshift(mapping);
+ }
+ }
+ },
+ unmap: function(lhs, ctx) {
+ if (lhs != ':' && lhs.charAt(0) == ':') {
+ // Ex to Ex or Ex to key mapping
+ if (ctx) { throw Error('Mode not supported for ex mappings'); }
+ var commandName = lhs.substring(1);
+ if (this.commandMap_[commandName] && this.commandMap_[commandName].user) {
+ delete this.commandMap_[commandName];
+ return;
+ }
+ } else {
+ // Key to Ex or key to key mapping
+ var keys = lhs;
+ for (var i = 0; i < defaultKeymap.length; i++) {
+ if (keys == defaultKeymap[i].keys
+ && defaultKeymap[i].context === ctx
+ && defaultKeymap[i].user) {
+ defaultKeymap.splice(i, 1);
+ return;
+ }
+ }
+ }
+ throw Error('No such mapping.');
+ }
+ };
+
+ var exCommands = {
+ colorscheme: function(cm, params) {
+ if (!params.args || params.args.length < 1) {
+ showConfirm(cm, cm.getOption('theme'));
+ return;
+ }
+ cm.setOption('theme', params.args[0]);
+ },
+ map: function(cm, params, ctx) {
+ var mapArgs = params.args;
+ if (!mapArgs || mapArgs.length < 2) {
+ if (cm) {
+ showConfirm(cm, 'Invalid mapping: ' + params.input);
+ }
+ return;
+ }
+ exCommandDispatcher.map(mapArgs[0], mapArgs[1], ctx);
+ },
+ imap: function(cm, params) { this.map(cm, params, 'insert'); },
+ nmap: function(cm, params) { this.map(cm, params, 'normal'); },
+ vmap: function(cm, params) { this.map(cm, params, 'visual'); },
+ unmap: function(cm, params, ctx) {
+ var mapArgs = params.args;
+ if (!mapArgs || mapArgs.length < 1) {
+ if (cm) {
+ showConfirm(cm, 'No such mapping: ' + params.input);
+ }
+ return;
+ }
+ exCommandDispatcher.unmap(mapArgs[0], ctx);
+ },
+ move: function(cm, params) {
+ commandDispatcher.processCommand(cm, cm.state.vim, {
+ type: 'motion',
+ motion: 'moveToLineOrEdgeOfDocument',
+ motionArgs: { forward: false, explicitRepeat: true,
+ linewise: true },
+ repeatOverride: params.line+1});
+ },
+ set: function(cm, params) {
+ var setArgs = params.args;
+ // Options passed through to the setOption/getOption calls. May be passed in by the
+ // local/global versions of the set command
+ var setCfg = params.setCfg || {};
+ if (!setArgs || setArgs.length < 1) {
+ if (cm) {
+ showConfirm(cm, 'Invalid mapping: ' + params.input);
+ }
+ return;
+ }
+ var expr = setArgs[0].split('=');
+ var optionName = expr[0];
+ var value = expr[1];
+ var forceGet = false;
+
+ if (optionName.charAt(optionName.length - 1) == '?') {
+ // If post-fixed with ?, then the set is actually a get.
+ if (value) { throw Error('Trailing characters: ' + params.argString); }
+ optionName = optionName.substring(0, optionName.length - 1);
+ forceGet = true;
+ }
+ if (value === undefined && optionName.substring(0, 2) == 'no') {
+ // To set boolean options to false, the option name is prefixed with
+ // 'no'.
+ optionName = optionName.substring(2);
+ value = false;
+ }
+
+ var optionIsBoolean = options[optionName] && options[optionName].type == 'boolean';
+ if (optionIsBoolean && value == undefined) {
+ // Calling set with a boolean option sets it to true.
+ value = true;
+ }
+ // If no value is provided, then we assume this is a get.
+ if (!optionIsBoolean && value === undefined || forceGet) {
+ var oldValue = getOption(optionName, cm, setCfg);
+ if (oldValue === true || oldValue === false) {
+ showConfirm(cm, ' ' + (oldValue ? '' : 'no') + optionName);
+ } else {
+ showConfirm(cm, ' ' + optionName + '=' + oldValue);
+ }
+ } else {
+ setOption(optionName, value, cm, setCfg);
+ }
+ },
+ setlocal: function (cm, params) {
+ // setCfg is passed through to setOption
+ params.setCfg = {scope: 'local'};
+ this.set(cm, params);
+ },
+ setglobal: function (cm, params) {
+ // setCfg is passed through to setOption
+ params.setCfg = {scope: 'global'};
+ this.set(cm, params);
+ },
+ registers: function(cm, params) {
+ var regArgs = params.args;
+ var registers = vimGlobalState.registerController.registers;
+ var regInfo = '----------Registers----------<br><br>';
+ if (!regArgs) {
+ for (var registerName in registers) {
+ var text = registers[registerName].toString();
+ if (text.length) {
+ regInfo += '"' + registerName + ' ' + text + '<br>';
+ }
+ }
+ } else {
+ var registerName;
+ regArgs = regArgs.join('');
+ for (var i = 0; i < regArgs.length; i++) {
+ registerName = regArgs.charAt(i);
+ if (!vimGlobalState.registerController.isValidRegister(registerName)) {
+ continue;
+ }
+ var register = registers[registerName] || new Register();
+ regInfo += '"' + registerName + ' ' + register.toString() + '<br>';
+ }
+ }
+ showConfirm(cm, regInfo);
+ },
+ sort: function(cm, params) {
+ var reverse, ignoreCase, unique, number;
+ function parseArgs() {
+ if (params.argString) {
+ var args = new CodeMirror.StringStream(params.argString);
+ if (args.eat('!')) { reverse = true; }
+ if (args.eol()) { return; }
+ if (!args.eatSpace()) { return 'Invalid arguments'; }
+ var opts = args.match(/[a-z]+/);
+ if (opts) {
+ opts = opts[0];
+ ignoreCase = opts.indexOf('i') != -1;
+ unique = opts.indexOf('u') != -1;
+ var decimal = opts.indexOf('d') != -1 && 1;
+ var hex = opts.indexOf('x') != -1 && 1;
+ var octal = opts.indexOf('o') != -1 && 1;
+ if (decimal + hex + octal > 1) { return 'Invalid arguments'; }
+ number = decimal && 'decimal' || hex && 'hex' || octal && 'octal';
+ }
+ if (args.match(/\/.*\//)) { return 'patterns not supported'; }
+ }
+ }
+ var err = parseArgs();
+ if (err) {
+ showConfirm(cm, err + ': ' + params.argString);
+ return;
+ }
+ var lineStart = params.line || cm.firstLine();
+ var lineEnd = params.lineEnd || params.line || cm.lastLine();
+ if (lineStart == lineEnd) { return; }
+ var curStart = Pos(lineStart, 0);
+ var curEnd = Pos(lineEnd, lineLength(cm, lineEnd));
+ var text = cm.getRange(curStart, curEnd).split('\n');
+ var numberRegex = (number == 'decimal') ? /(-?)([\d]+)/ :
+ (number == 'hex') ? /(-?)(?:0x)?([0-9a-f]+)/i :
+ (number == 'octal') ? /([0-7]+)/ : null;
+ var radix = (number == 'decimal') ? 10 : (number == 'hex') ? 16 : (number == 'octal') ? 8 : null;
+ var numPart = [], textPart = [];
+ if (number) {
+ for (var i = 0; i < text.length; i++) {
+ if (numberRegex.exec(text[i])) {
+ numPart.push(text[i]);
+ } else {
+ textPart.push(text[i]);
+ }
+ }
+ } else {
+ textPart = text;
+ }
+ function compareFn(a, b) {
+ if (reverse) { var tmp; tmp = a; a = b; b = tmp; }
+ if (ignoreCase) { a = a.toLowerCase(); b = b.toLowerCase(); }
+ var anum = number && numberRegex.exec(a);
+ var bnum = number && numberRegex.exec(b);
+ if (!anum) { return a < b ? -1 : 1; }
+ anum = parseInt((anum[1] + anum[2]).toLowerCase(), radix);
+ bnum = parseInt((bnum[1] + bnum[2]).toLowerCase(), radix);
+ return anum - bnum;
+ }
+ numPart.sort(compareFn);
+ textPart.sort(compareFn);
+ text = (!reverse) ? textPart.concat(numPart) : numPart.concat(textPart);
+ if (unique) { // Remove duplicate lines
+ var textOld = text;
+ var lastLine;
+ text = [];
+ for (var i = 0; i < textOld.length; i++) {
+ if (textOld[i] != lastLine) {
+ text.push(textOld[i]);
+ }
+ lastLine = textOld[i];
+ }
+ }
+ cm.replaceRange(text.join('\n'), curStart, curEnd);
+ },
+ global: function(cm, params) {
+ // a global command is of the form
+ // :[range]g/pattern/[cmd]
+ // argString holds the string /pattern/[cmd]
+ var argString = params.argString;
+ if (!argString) {
+ showConfirm(cm, 'Regular Expression missing from global');
+ return;
+ }
+ // range is specified here
+ var lineStart = (params.line !== undefined) ? params.line : cm.firstLine();
+ var lineEnd = params.lineEnd || params.line || cm.lastLine();
+ // get the tokens from argString
+ var tokens = splitBySlash(argString);
+ var regexPart = argString, cmd;
+ if (tokens.length) {
+ regexPart = tokens[0];
+ cmd = tokens.slice(1, tokens.length).join('/');
+ }
+ if (regexPart) {
+ // If regex part is empty, then use the previous query. Otherwise
+ // use the regex part as the new query.
+ try {
+ updateSearchQuery(cm, regexPart, true /** ignoreCase */,
+ true /** smartCase */);
+ } catch (e) {
+ showConfirm(cm, 'Invalid regex: ' + regexPart);
+ return;
+ }
+ }
+ // now that we have the regexPart, search for regex matches in the
+ // specified range of lines
+ var query = getSearchState(cm).getQuery();
+ var matchedLines = [], content = '';
+ for (var i = lineStart; i <= lineEnd; i++) {
+ var matched = query.test(cm.getLine(i));
+ if (matched) {
+ matchedLines.push(i+1);
+ content+= cm.getLine(i) + '<br>';
+ }
+ }
+ // if there is no [cmd], just display the list of matched lines
+ if (!cmd) {
+ showConfirm(cm, content);
+ return;
+ }
+ var index = 0;
+ var nextCommand = function() {
+ if (index < matchedLines.length) {
+ var command = matchedLines[index] + cmd;
+ exCommandDispatcher.processCommand(cm, command, {
+ callback: nextCommand
+ });
+ }
+ index++;
+ };
+ nextCommand();
+ },
+ substitute: function(cm, params) {
+ if (!cm.getSearchCursor) {
+ throw new Error('Search feature not available. Requires searchcursor.js or ' +
+ 'any other getSearchCursor implementation.');
+ }
+ var argString = params.argString;
+ var tokens = argString ? splitBySlash(argString) : [];
+ var regexPart, replacePart = '', trailing, flagsPart, count;
+ var confirm = false; // Whether to confirm each replace.
+ var global = false; // True to replace all instances on a line, false to replace only 1.
+ if (tokens.length) {
+ regexPart = tokens[0];
+ replacePart = tokens[1];
+ if (replacePart !== undefined) {
+ if (getOption('pcre')) {
+ replacePart = unescapeRegexReplace(replacePart);
+ } else {
+ replacePart = translateRegexReplace(replacePart);
+ }
+ vimGlobalState.lastSubstituteReplacePart = replacePart;
+ }
+ trailing = tokens[2] ? tokens[2].split(' ') : [];
+ } else {
+ // either the argString is empty or its of the form ' hello/world'
+ // actually splitBySlash returns a list of tokens
+ // only if the string starts with a '/'
+ if (argString && argString.length) {
+ showConfirm(cm, 'Substitutions should be of the form ' +
+ ':s/pattern/replace/');
+ return;
+ }
+ }
+ // After the 3rd slash, we can have flags followed by a space followed
+ // by count.
+ if (trailing) {
+ flagsPart = trailing[0];
+ count = parseInt(trailing[1]);
+ if (flagsPart) {
+ if (flagsPart.indexOf('c') != -1) {
+ confirm = true;
+ flagsPart.replace('c', '');
+ }
+ if (flagsPart.indexOf('g') != -1) {
+ global = true;
+ flagsPart.replace('g', '');
+ }
+ regexPart = regexPart + '/' + flagsPart;
+ }
+ }
+ if (regexPart) {
+ // If regex part is empty, then use the previous query. Otherwise use
+ // the regex part as the new query.
+ try {
+ updateSearchQuery(cm, regexPart, true /** ignoreCase */,
+ true /** smartCase */);
+ } catch (e) {
+ showConfirm(cm, 'Invalid regex: ' + regexPart);
+ return;
+ }
+ }
+ replacePart = replacePart || vimGlobalState.lastSubstituteReplacePart;
+ if (replacePart === undefined) {
+ showConfirm(cm, 'No previous substitute regular expression');
+ return;
+ }
+ var state = getSearchState(cm);
+ var query = state.getQuery();
+ var lineStart = (params.line !== undefined) ? params.line : cm.getCursor().line;
+ var lineEnd = params.lineEnd || lineStart;
+ if (lineStart == cm.firstLine() && lineEnd == cm.lastLine()) {
+ lineEnd = Infinity;
+ }
+ if (count) {
+ lineStart = lineEnd;
+ lineEnd = lineStart + count - 1;
+ }
+ var startPos = clipCursorToContent(cm, Pos(lineStart, 0));
+ var cursor = cm.getSearchCursor(query, startPos);
+ doReplace(cm, confirm, global, lineStart, lineEnd, cursor, query, replacePart, params.callback);
+ },
+ redo: CodeMirror.commands.redo,
+ undo: CodeMirror.commands.undo,
+ write: function(cm) {
+ if (CodeMirror.commands.save) {
+ // If a save command is defined, call it.
+ CodeMirror.commands.save(cm);
+ } else if (cm.save) {
+ // Saves to text area if no save command is defined and cm.save() is available.
+ cm.save();
+ }
+ },
+ nohlsearch: function(cm) {
+ clearSearchHighlight(cm);
+ },
+ yank: function (cm) {
+ var cur = copyCursor(cm.getCursor());
+ var line = cur.line;
+ var lineText = cm.getLine(line);
+ vimGlobalState.registerController.pushText(
+ '0', 'yank', lineText, true, true);
+ },
+ delmarks: function(cm, params) {
+ if (!params.argString || !trim(params.argString)) {
+ showConfirm(cm, 'Argument required');
+ return;
+ }
+
+ var state = cm.state.vim;
+ var stream = new CodeMirror.StringStream(trim(params.argString));
+ while (!stream.eol()) {
+ stream.eatSpace();
+
+ // Record the streams position at the beginning of the loop for use
+ // in error messages.
+ var count = stream.pos;
+
+ if (!stream.match(/[a-zA-Z]/, false)) {
+ showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count));
+ return;
+ }
+
+ var sym = stream.next();
+ // Check if this symbol is part of a range
+ if (stream.match('-', true)) {
+ // This symbol is part of a range.
+
+ // The range must terminate at an alphabetic character.
+ if (!stream.match(/[a-zA-Z]/, false)) {
+ showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count));
+ return;
+ }
+
+ var startMark = sym;
+ var finishMark = stream.next();
+ // The range must terminate at an alphabetic character which
+ // shares the same case as the start of the range.
+ if (isLowerCase(startMark) && isLowerCase(finishMark) ||
+ isUpperCase(startMark) && isUpperCase(finishMark)) {
+ var start = startMark.charCodeAt(0);
+ var finish = finishMark.charCodeAt(0);
+ if (start >= finish) {
+ showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count));
+ return;
+ }
+
+ // Because marks are always ASCII values, and we have
+ // determined that they are the same case, we can use
+ // their char codes to iterate through the defined range.
+ for (var j = 0; j <= finish - start; j++) {
+ var mark = String.fromCharCode(start + j);
+ delete state.marks[mark];
+ }
+ } else {
+ showConfirm(cm, 'Invalid argument: ' + startMark + '-');
+ return;
+ }
+ } else {
+ // This symbol is a valid mark, and is not part of a range.
+ delete state.marks[sym];
+ }
+ }
+ }
+ };
+
+ var exCommandDispatcher = new ExCommandDispatcher();
+
+ /**
+ * @param {CodeMirror} cm CodeMirror instance we are in.
+ * @param {boolean} confirm Whether to confirm each replace.
+ * @param {Cursor} lineStart Line to start replacing from.
+ * @param {Cursor} lineEnd Line to stop replacing at.
+ * @param {RegExp} query Query for performing matches with.
+ * @param {string} replaceWith Text to replace matches with. May contain $1,
+ * $2, etc for replacing captured groups using Javascript replace.
+ * @param {function()} callback A callback for when the replace is done.
+ */
+ function doReplace(cm, confirm, global, lineStart, lineEnd, searchCursor, query,
+ replaceWith, callback) {
+ // Set up all the functions.
+ cm.state.vim.exMode = true;
+ var done = false;
+ var lastPos = searchCursor.from();
+ function replaceAll() {
+ cm.operation(function() {
+ while (!done) {
+ replace();
+ next();
+ }
+ stop();
+ });
+ }
+ function replace() {
+ var text = cm.getRange(searchCursor.from(), searchCursor.to());
+ var newText = text.replace(query, replaceWith);
+ searchCursor.replace(newText);
+ }
+ function next() {
+ // The below only loops to skip over multiple occurrences on the same
+ // line when 'global' is not true.
+ while(searchCursor.findNext() &&
+ isInRange(searchCursor.from(), lineStart, lineEnd)) {
+ if (!global && lastPos && searchCursor.from().line == lastPos.line) {
+ continue;
+ }
+ cm.scrollIntoView(searchCursor.from(), 30);
+ cm.setSelection(searchCursor.from(), searchCursor.to());
+ lastPos = searchCursor.from();
+ done = false;
+ return;
+ }
+ done = true;
+ }
+ function stop(close) {
+ if (close) { close(); }
+ cm.focus();
+ if (lastPos) {
+ cm.setCursor(lastPos);
+ var vim = cm.state.vim;
+ vim.exMode = false;
+ vim.lastHPos = vim.lastHSPos = lastPos.ch;
+ }
+ if (callback) { callback(); }
+ }
+ function onPromptKeyDown(e, _value, close) {
+ // Swallow all keys.
+ CodeMirror.e_stop(e);
+ var keyName = CodeMirror.keyName(e);
+ switch (keyName) {
+ case 'Y':
+ replace(); next(); break;
+ case 'N':
+ next(); break;
+ case 'A':
+ // replaceAll contains a call to close of its own. We don't want it
+ // to fire too early or multiple times.
+ var savedCallback = callback;
+ callback = undefined;
+ cm.operation(replaceAll);
+ callback = savedCallback;
+ break;
+ case 'L':
+ replace();
+ // fall through and exit.
+ case 'Q':
+ case 'Esc':
+ case 'Ctrl-C':
+ case 'Ctrl-[':
+ stop(close);
+ break;
+ }
+ if (done) { stop(close); }
+ return true;
+ }
+
+ // Actually do replace.
+ next();
+ if (done) {
+ showConfirm(cm, 'No matches for ' + query.source);
+ return;
+ }
+ if (!confirm) {
+ replaceAll();
+ if (callback) { callback(); };
+ return;
+ }
+ showPrompt(cm, {
+ prefix: 'replace with <strong>' + replaceWith + '</strong> (y/n/a/q/l)',
+ onKeyDown: onPromptKeyDown
+ });
+ }
+
+ CodeMirror.keyMap.vim = {
+ attach: attachVimMap,
+ detach: detachVimMap,
+ call: cmKey
+ };
+
+ function exitInsertMode(cm) {
+ var vim = cm.state.vim;
+ var macroModeState = vimGlobalState.macroModeState;
+ var insertModeChangeRegister = vimGlobalState.registerController.getRegister('.');
+ var isPlaying = macroModeState.isPlaying;
+ var lastChange = macroModeState.lastInsertModeChanges;
+ // In case of visual block, the insertModeChanges are not saved as a
+ // single word, so we convert them to a single word
+ // so as to update the ". register as expected in real vim.
+ var text = [];
+ if (!isPlaying) {
+ var selLength = lastChange.inVisualBlock ? vim.lastSelection.visualBlock.height : 1;
+ var changes = lastChange.changes;
+ var text = [];
+ var i = 0;
+ // In case of multiple selections in blockwise visual,
+ // the inserted text, for example: 'f<Backspace>oo', is stored as
+ // 'f', 'f', InsertModeKey 'o', 'o', 'o', 'o'. (if you have a block with 2 lines).
+ // We push the contents of the changes array as per the following:
+ // 1. In case of InsertModeKey, just increment by 1.
+ // 2. In case of a character, jump by selLength (2 in the example).
+ while (i < changes.length) {
+ // This loop will convert 'ff<bs>oooo' to 'f<bs>oo'.
+ text.push(changes[i]);
+ if (changes[i] instanceof InsertModeKey) {
+ i++;
+ } else {
+ i+= selLength;
+ }
+ }
+ lastChange.changes = text;
+ cm.off('change', onChange);
+ CodeMirror.off(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown);
+ }
+ if (!isPlaying && vim.insertModeRepeat > 1) {
+ // Perform insert mode repeat for commands like 3,a and 3,o.
+ repeatLastEdit(cm, vim, vim.insertModeRepeat - 1,
+ true /** repeatForInsert */);
+ vim.lastEditInputState.repeatOverride = vim.insertModeRepeat;
+ }
+ delete vim.insertModeRepeat;
+ vim.insertMode = false;
+ cm.setCursor(cm.getCursor().line, cm.getCursor().ch-1);
+ cm.setOption('keyMap', 'vim');
+ cm.setOption('disableInput', true);
+ cm.toggleOverwrite(false); // exit replace mode if we were in it.
+ // update the ". register before exiting insert mode
+ insertModeChangeRegister.setText(lastChange.changes.join(''));
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"});
+ if (macroModeState.isRecording) {
+ logInsertModeChange(macroModeState);
+ }
+ }
+
+ function _mapCommand(command) {
+ defaultKeymap.unshift(command);
+ }
+
+ function mapCommand(keys, type, name, args, extra) {
+ var command = {keys: keys, type: type};
+ command[type] = name;
+ command[type + "Args"] = args;
+ for (var key in extra)
+ command[key] = extra[key];
+ _mapCommand(command);
+ }
+
+ // The timeout in milliseconds for the two-character ESC keymap should be
+ // adjusted according to your typing speed to prevent false positives.
+ defineOption('insertModeEscKeysTimeout', 200, 'number');
+
+ CodeMirror.keyMap['vim-insert'] = {
+ // TODO: override navigation keys so that Esc will cancel automatic
+ // indentation from o, O, i_<CR>
+ 'Ctrl-N': 'autocomplete',
+ 'Ctrl-P': 'autocomplete',
+ 'Enter': function(cm) {
+ var fn = CodeMirror.commands.newlineAndIndentContinueComment ||
+ CodeMirror.commands.newlineAndIndent;
+ fn(cm);
+ },
+ fallthrough: ['default'],
+ attach: attachVimMap,
+ detach: detachVimMap,
+ call: cmKey
+ };
+
+ CodeMirror.keyMap['vim-replace'] = {
+ 'Backspace': 'goCharLeft',
+ fallthrough: ['vim-insert'],
+ attach: attachVimMap,
+ detach: detachVimMap,
+ call: cmKey
+ };
+
+ function executeMacroRegister(cm, vim, macroModeState, registerName) {
+ var register = vimGlobalState.registerController.getRegister(registerName);
+ if (registerName == ':') {
+ // Read-only register containing last Ex command.
+ if (register.keyBuffer[0]) {
+ exCommandDispatcher.processCommand(cm, register.keyBuffer[0]);
+ }
+ macroModeState.isPlaying = false;
+ return;
+ }
+ var keyBuffer = register.keyBuffer;
+ var imc = 0;
+ macroModeState.isPlaying = true;
+ macroModeState.replaySearchQueries = register.searchQueries.slice(0);
+ for (var i = 0; i < keyBuffer.length; i++) {
+ var text = keyBuffer[i];
+ var match, key;
+ while (text) {
+ // Pull off one command key, which is either a single character
+ // or a special sequence wrapped in '<' and '>', e.g. '<Space>'.
+ match = (/<\w+-.+?>|<\w+>|./).exec(text);
+ key = match[0];
+ text = text.substring(match.index + key.length);
+ CodeMirror.Vim.handleKey(cm, key, 'macro');
+ if (vim.insertMode) {
+ var changes = register.insertModeChanges[imc++].changes;
+ vimGlobalState.macroModeState.lastInsertModeChanges.changes =
+ changes;
+ repeatInsertModeChanges(cm, changes, 1);
+ exitInsertMode(cm);
+ }
+ }
+ };
+ macroModeState.isPlaying = false;
+ }
+
+ function logKey(macroModeState, key) {
+ if (macroModeState.isPlaying) { return; }
+ var registerName = macroModeState.latestRegister;
+ var register = vimGlobalState.registerController.getRegister(registerName);
+ if (register) {
+ register.pushText(key);
+ }
+ }
+
+ function logInsertModeChange(macroModeState) {
+ if (macroModeState.isPlaying) { return; }
+ var registerName = macroModeState.latestRegister;
+ var register = vimGlobalState.registerController.getRegister(registerName);
+ if (register && register.pushInsertModeChanges) {
+ register.pushInsertModeChanges(macroModeState.lastInsertModeChanges);
+ }
+ }
+
+ function logSearchQuery(macroModeState, query) {
+ if (macroModeState.isPlaying) { return; }
+ var registerName = macroModeState.latestRegister;
+ var register = vimGlobalState.registerController.getRegister(registerName);
+ if (register && register.pushSearchQuery) {
+ register.pushSearchQuery(query);
+ }
+ }
+
+ /**
+ * Listens for changes made in insert mode.
+ * Should only be active in insert mode.
+ */
+ function onChange(_cm, changeObj) {
+ var macroModeState = vimGlobalState.macroModeState;
+ var lastChange = macroModeState.lastInsertModeChanges;
+ if (!macroModeState.isPlaying) {
+ while(changeObj) {
+ lastChange.expectCursorActivityForChange = true;
+ if (changeObj.origin == '+input' || changeObj.origin == 'paste'
+ || changeObj.origin === undefined /* only in testing */) {
+ var text = changeObj.text.join('\n');
+ lastChange.changes.push(text);
+ }
+ // Change objects may be chained with next.
+ changeObj = changeObj.next;
+ }
+ }
+ }
+
+ /**
+ * Listens for any kind of cursor activity on CodeMirror.
+ */
+ function onCursorActivity(cm) {
+ var vim = cm.state.vim;
+ if (vim.insertMode) {
+ // Tracking cursor activity in insert mode (for macro support).
+ var macroModeState = vimGlobalState.macroModeState;
+ if (macroModeState.isPlaying) { return; }
+ var lastChange = macroModeState.lastInsertModeChanges;
+ if (lastChange.expectCursorActivityForChange) {
+ lastChange.expectCursorActivityForChange = false;
+ } else {
+ // Cursor moved outside the context of an edit. Reset the change.
+ lastChange.changes = [];
+ }
+ } else if (!cm.curOp.isVimOp) {
+ handleExternalSelection(cm, vim);
+ }
+ if (vim.visualMode) {
+ updateFakeCursor(cm);
+ }
+ }
+ function updateFakeCursor(cm) {
+ var vim = cm.state.vim;
+ var from = clipCursorToContent(cm, copyCursor(vim.sel.head));
+ var to = offsetCursor(from, 0, 1);
+ if (vim.fakeCursor) {
+ vim.fakeCursor.clear();
+ }
+ vim.fakeCursor = cm.markText(from, to, {className: 'cm-animate-fat-cursor'});
+ }
+ function handleExternalSelection(cm, vim) {
+ var anchor = cm.getCursor('anchor');
+ var head = cm.getCursor('head');
+ // Enter or exit visual mode to match mouse selection.
+ if (vim.visualMode && !cm.somethingSelected()) {
+ exitVisualMode(cm, false);
+ } else if (!vim.visualMode && !vim.insertMode && cm.somethingSelected()) {
+ vim.visualMode = true;
+ vim.visualLine = false;
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "visual"});
+ }
+ if (vim.visualMode) {
+ // Bind CodeMirror selection model to vim selection model.
+ // Mouse selections are considered visual characterwise.
+ var headOffset = !cursorIsBefore(head, anchor) ? -1 : 0;
+ var anchorOffset = cursorIsBefore(head, anchor) ? -1 : 0;
+ head = offsetCursor(head, 0, headOffset);
+ anchor = offsetCursor(anchor, 0, anchorOffset);
+ vim.sel = {
+ anchor: anchor,
+ head: head
+ };
+ updateMark(cm, vim, '<', cursorMin(head, anchor));
+ updateMark(cm, vim, '>', cursorMax(head, anchor));
+ } else if (!vim.insertMode) {
+ // Reset lastHPos if selection was modified by something outside of vim mode e.g. by mouse.
+ vim.lastHPos = cm.getCursor().ch;
+ }
+ }
+
+ /** Wrapper for special keys pressed in insert mode */
+ function InsertModeKey(keyName) {
+ this.keyName = keyName;
+ }
+
+ /**
+ * Handles raw key down events from the text area.
+ * - Should only be active in insert mode.
+ * - For recording deletes in insert mode.
+ */
+ function onKeyEventTargetKeyDown(e) {
+ var macroModeState = vimGlobalState.macroModeState;
+ var lastChange = macroModeState.lastInsertModeChanges;
+ var keyName = CodeMirror.keyName(e);
+ if (!keyName) { return; }
+ function onKeyFound() {
+ lastChange.changes.push(new InsertModeKey(keyName));
+ return true;
+ }
+ if (keyName.indexOf('Delete') != -1 || keyName.indexOf('Backspace') != -1) {
+ CodeMirror.lookupKey(keyName, 'vim-insert', onKeyFound);
+ }
+ }
+
+ /**
+ * Repeats the last edit, which includes exactly 1 command and at most 1
+ * insert. Operator and motion commands are read from lastEditInputState,
+ * while action commands are read from lastEditActionCommand.
+ *
+ * If repeatForInsert is true, then the function was called by
+ * exitInsertMode to repeat the insert mode changes the user just made. The
+ * corresponding enterInsertMode call was made with a count.
+ */
+ function repeatLastEdit(cm, vim, repeat, repeatForInsert) {
+ var macroModeState = vimGlobalState.macroModeState;
+ macroModeState.isPlaying = true;
+ var isAction = !!vim.lastEditActionCommand;
+ var cachedInputState = vim.inputState;
+ function repeatCommand() {
+ if (isAction) {
+ commandDispatcher.processAction(cm, vim, vim.lastEditActionCommand);
+ } else {
+ commandDispatcher.evalInput(cm, vim);
+ }
+ }
+ function repeatInsert(repeat) {
+ if (macroModeState.lastInsertModeChanges.changes.length > 0) {
+ // For some reason, repeat cw in desktop VIM does not repeat
+ // insert mode changes. Will conform to that behavior.
+ repeat = !vim.lastEditActionCommand ? 1 : repeat;
+ var changeObject = macroModeState.lastInsertModeChanges;
+ repeatInsertModeChanges(cm, changeObject.changes, repeat);
+ }
+ }
+ vim.inputState = vim.lastEditInputState;
+ if (isAction && vim.lastEditActionCommand.interlaceInsertRepeat) {
+ // o and O repeat have to be interlaced with insert repeats so that the
+ // insertions appear on separate lines instead of the last line.
+ for (var i = 0; i < repeat; i++) {
+ repeatCommand();
+ repeatInsert(1);
+ }
+ } else {
+ if (!repeatForInsert) {
+ // Hack to get the cursor to end up at the right place. If I is
+ // repeated in insert mode repeat, cursor will be 1 insert
+ // change set left of where it should be.
+ repeatCommand();
+ }
+ repeatInsert(repeat);
+ }
+ vim.inputState = cachedInputState;
+ if (vim.insertMode && !repeatForInsert) {
+ // Don't exit insert mode twice. If repeatForInsert is set, then we
+ // were called by an exitInsertMode call lower on the stack.
+ exitInsertMode(cm);
+ }
+ macroModeState.isPlaying = false;
+ };
+
+ function repeatInsertModeChanges(cm, changes, repeat) {
+ function keyHandler(binding) {
+ if (typeof binding == 'string') {
+ CodeMirror.commands[binding](cm);
+ } else {
+ binding(cm);
+ }
+ return true;
+ }
+ var head = cm.getCursor('head');
+ var inVisualBlock = vimGlobalState.macroModeState.lastInsertModeChanges.inVisualBlock;
+ if (inVisualBlock) {
+ // Set up block selection again for repeating the changes.
+ var vim = cm.state.vim;
+ var lastSel = vim.lastSelection;
+ var offset = getOffset(lastSel.anchor, lastSel.head);
+ selectForInsert(cm, head, offset.line + 1);
+ repeat = cm.listSelections().length;
+ cm.setCursor(head);
+ }
+ for (var i = 0; i < repeat; i++) {
+ if (inVisualBlock) {
+ cm.setCursor(offsetCursor(head, i, 0));
+ }
+ for (var j = 0; j < changes.length; j++) {
+ var change = changes[j];
+ if (change instanceof InsertModeKey) {
+ CodeMirror.lookupKey(change.keyName, 'vim-insert', keyHandler);
+ } else {
+ var cur = cm.getCursor();
+ cm.replaceRange(change, cur, cur);
+ }
+ }
+ }
+ if (inVisualBlock) {
+ cm.setCursor(offsetCursor(head, 0, 1));
+ }
+ }
+
+ resetVimGlobalState();
+ return vimApi;
+ };
+ // Initialize Vim and make it available as an API.
+ CodeMirror.Vim = Vim();
+});
diff --git a/devtools/client/sourceeditor/codemirror/lib/codemirror.css b/devtools/client/sourceeditor/codemirror/lib/codemirror.css
new file mode 100644
index 000000000..18b0bf70d
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/lib/codemirror.css
@@ -0,0 +1,347 @@
+/* BASICS */
+
+.CodeMirror {
+ /* Set height, width, borders, and global font properties here */
+ font-family: monospace;
+ height: 300px;
+ color: black;
+}
+
+/* PADDING */
+
+.CodeMirror-lines {
+ padding: 4px 0; /* Vertical padding around content */
+}
+.CodeMirror pre {
+ padding: 0 4px; /* Horizontal padding of content */
+}
+
+.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
+ background-color: white; /* The little square between H and V scrollbars */
+}
+
+/* GUTTER */
+
+.CodeMirror-gutters {
+ border-right: 1px solid #ddd;
+ background-color: #f7f7f7;
+ white-space: nowrap;
+}
+.CodeMirror-linenumbers {}
+.CodeMirror-linenumber {
+ padding: 0 3px 0 5px;
+ min-width: 20px;
+ text-align: right;
+ color: #999;
+ white-space: nowrap;
+}
+
+.CodeMirror-guttermarker { color: black; }
+.CodeMirror-guttermarker-subtle { color: #999; }
+
+/* CURSOR */
+
+.CodeMirror-cursor {
+ border-left: 1px solid black;
+ border-right: none;
+ width: 0;
+}
+/* Shown when moving in bi-directional text */
+.CodeMirror div.CodeMirror-secondarycursor {
+ border-left: 1px solid silver;
+}
+.cm-fat-cursor .CodeMirror-cursor {
+ width: auto;
+ border: 0 !important;
+ background: #7e7;
+}
+.cm-fat-cursor div.CodeMirror-cursors {
+ z-index: 1;
+}
+
+.cm-animate-fat-cursor {
+ width: auto;
+ border: 0;
+ -webkit-animation: blink 1.06s steps(1) infinite;
+ -moz-animation: blink 1.06s steps(1) infinite;
+ animation: blink 1.06s steps(1) infinite;
+ background-color: #7e7;
+}
+@-moz-keyframes blink {
+ 0% {}
+ 50% { background-color: transparent; }
+ 100% {}
+}
+@-webkit-keyframes blink {
+ 0% {}
+ 50% { background-color: transparent; }
+ 100% {}
+}
+@keyframes blink {
+ 0% {}
+ 50% { background-color: transparent; }
+ 100% {}
+}
+
+/* Can style cursor different in overwrite (non-insert) mode */
+.CodeMirror-overwrite .CodeMirror-cursor {}
+
+.cm-tab { display: inline-block; text-decoration: inherit; }
+
+.CodeMirror-rulers {
+ position: absolute;
+ left: 0; right: 0; top: -50px; bottom: -20px;
+ overflow: hidden;
+}
+.CodeMirror-ruler {
+ border-left: 1px solid #ccc;
+ top: 0; bottom: 0;
+ position: absolute;
+}
+
+/* DEFAULT THEME */
+
+.cm-s-default .cm-header {color: blue;}
+.cm-s-default .cm-quote {color: #090;}
+.cm-negative {color: #d44;}
+.cm-positive {color: #292;}
+.cm-header, .cm-strong {font-weight: bold;}
+.cm-em {font-style: italic;}
+.cm-link {text-decoration: underline;}
+.cm-strikethrough {text-decoration: line-through;}
+
+.cm-s-default .cm-keyword {color: #708;}
+.cm-s-default .cm-atom {color: #219;}
+.cm-s-default .cm-number {color: #164;}
+.cm-s-default .cm-def {color: #00f;}
+.cm-s-default .cm-variable,
+.cm-s-default .cm-punctuation,
+.cm-s-default .cm-property,
+.cm-s-default .cm-operator {}
+.cm-s-default .cm-variable-2 {color: #05a;}
+.cm-s-default .cm-variable-3 {color: #085;}
+.cm-s-default .cm-comment {color: #a50;}
+.cm-s-default .cm-string {color: #a11;}
+.cm-s-default .cm-string-2 {color: #f50;}
+.cm-s-default .cm-meta {color: #555;}
+.cm-s-default .cm-qualifier {color: #555;}
+.cm-s-default .cm-builtin {color: #30a;}
+.cm-s-default .cm-bracket {color: #997;}
+.cm-s-default .cm-tag {color: #170;}
+.cm-s-default .cm-attribute {color: #00c;}
+.cm-s-default .cm-hr {color: #999;}
+.cm-s-default .cm-link {color: #00c;}
+
+.cm-s-default .cm-error {color: #f00;}
+.cm-invalidchar {color: #f00;}
+
+.CodeMirror-composing { border-bottom: 2px solid; }
+
+/* Default styles for common addons */
+
+div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
+div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
+.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
+.CodeMirror-activeline-background {background: #e8f2ff;}
+
+/* STOP */
+
+/* The rest of this file contains styles related to the mechanics of
+ the editor. You probably shouldn't touch them. */
+
+.CodeMirror {
+ position: relative;
+ overflow: hidden;
+ background: white;
+}
+
+.CodeMirror-scroll {
+ overflow: scroll !important; /* Things will break if this is overridden */
+ /* 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;
+ height: 100%;
+ outline: none; /* Prevent dragging from highlighting the element */
+ position: relative;
+}
+.CodeMirror-sizer {
+ position: relative;
+ border-right: 30px solid transparent;
+}
+
+/* The fake, visible scrollbars. Used to force redraw during scrolling
+ before actual scrolling happens, thus preventing shaking and
+ flickering artifacts. */
+.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
+ position: absolute;
+ z-index: 6;
+ display: none;
+}
+.CodeMirror-vscrollbar {
+ right: 0; top: 0;
+ overflow-x: hidden;
+ overflow-y: scroll;
+}
+.CodeMirror-hscrollbar {
+ bottom: 0; left: 0;
+ overflow-y: hidden;
+ overflow-x: scroll;
+}
+.CodeMirror-scrollbar-filler {
+ right: 0; bottom: 0;
+}
+.CodeMirror-gutter-filler {
+ left: 0; bottom: 0;
+}
+
+.CodeMirror-gutters {
+ position: absolute; left: 0; top: 0;
+ min-height: 100%;
+ z-index: 3;
+}
+.CodeMirror-gutter {
+ white-space: normal;
+ height: 100%;
+ display: inline-block;
+ vertical-align: top;
+ margin-bottom: -30px;
+ /* Hack to make IE7 behave */
+ *zoom:1;
+ *display:inline;
+}
+.CodeMirror-gutter-wrapper {
+ position: absolute;
+ z-index: 4;
+ background: none !important;
+ border: none !important;
+}
+.CodeMirror-gutter-background {
+ position: absolute;
+ top: 0; bottom: 0;
+ z-index: 4;
+}
+.CodeMirror-gutter-elt {
+ position: absolute;
+ cursor: default;
+ z-index: 4;
+}
+.CodeMirror-gutter-wrapper {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+}
+
+.CodeMirror-lines {
+ cursor: text;
+ min-height: 1px; /* prevents collapsing before first draw */
+}
+.CodeMirror pre {
+ /* Reset some styles that the rest of the page might have set */
+ -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
+ border-width: 0;
+ background: transparent;
+ font-family: inherit;
+ font-size: inherit;
+ margin: 0;
+ white-space: pre;
+ word-wrap: normal;
+ line-height: inherit;
+ color: inherit;
+ z-index: 2;
+ position: relative;
+ overflow: visible;
+ -webkit-tap-highlight-color: transparent;
+ -webkit-font-variant-ligatures: none;
+ font-variant-ligatures: none;
+}
+.CodeMirror-wrap pre {
+ word-wrap: break-word;
+ white-space: pre-wrap;
+ word-break: normal;
+}
+
+.CodeMirror-linebackground {
+ position: absolute;
+ left: 0; right: 0; top: 0; bottom: 0;
+ z-index: 0;
+}
+
+.CodeMirror-linewidget {
+ position: relative;
+ z-index: 2;
+ overflow: auto;
+}
+
+.CodeMirror-widget {}
+
+.CodeMirror-code {
+ outline: none;
+}
+
+/* Force content-box sizing for the elements where we expect it */
+.CodeMirror-scroll,
+.CodeMirror-sizer,
+.CodeMirror-gutter,
+.CodeMirror-gutters,
+.CodeMirror-linenumber {
+ -moz-box-sizing: content-box;
+ box-sizing: content-box;
+}
+
+.CodeMirror-measure {
+ position: absolute;
+ width: 100%;
+ height: 0;
+ overflow: hidden;
+ visibility: hidden;
+}
+
+.CodeMirror-cursor {
+ position: absolute;
+ pointer-events: none;
+}
+.CodeMirror-measure pre { position: static; }
+
+div.CodeMirror-cursors {
+ visibility: hidden;
+ position: relative;
+ z-index: 3;
+}
+div.CodeMirror-dragcursors {
+ visibility: visible;
+}
+
+.CodeMirror-focused div.CodeMirror-cursors {
+ visibility: visible;
+}
+
+.CodeMirror-selected { background: #d9d9d9; }
+.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
+.CodeMirror-crosshair { cursor: crosshair; }
+.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
+.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
+
+.cm-searching {
+ background: #ffa;
+ background: rgba(255, 255, 0, .4);
+}
+
+/* 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-cursors {
+ visibility: hidden;
+ }
+}
+
+/* See issue #2901 */
+.cm-tab-wrap-hack:after { content: ''; }
+
+/* Help users use markselection to safely style text background */
+span.CodeMirror-selectedtext { background: none; }
diff --git a/devtools/client/sourceeditor/codemirror/lib/codemirror.js b/devtools/client/sourceeditor/codemirror/lib/codemirror.js
new file mode 100644
index 000000000..7dc842d3b
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/lib/codemirror.js
@@ -0,0 +1,8922 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+// This is CodeMirror (http://codemirror.net), a code editor
+// implemented in JavaScript on top of the browser's DOM.
+//
+// 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 || window).CodeMirror = mod();
+})(function() {
+ "use strict";
+
+ // BROWSER SNIFFING
+
+ // Kludges for bugs and behavior differences that can't be feature
+ // detected are enabled based on userAgent etc sniffing.
+ var userAgent = navigator.userAgent;
+ var platform = navigator.platform;
+
+ var gecko = /gecko\/\d/i.test(userAgent);
+ var ie_upto10 = /MSIE \d/.test(userAgent);
+ var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(userAgent);
+ var ie = ie_upto10 || ie_11up;
+ var ie_version = ie && (ie_upto10 ? document.documentMode || 6 : ie_11up[1]);
+ var webkit = /WebKit\//.test(userAgent);
+ var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(userAgent);
+ var chrome = /Chrome\//.test(userAgent);
+ var presto = /Opera\//.test(userAgent);
+ var safari = /Apple Computer/.test(navigator.vendor);
+ var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(userAgent);
+ var phantom = /PhantomJS/.test(userAgent);
+
+ var ios = /AppleWebKit/.test(userAgent) && /Mobile\/\w+/.test(userAgent);
+ // This is woefully incomplete. Suggestions for alternative methods welcome.
+ var mobile = ios || /Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(userAgent);
+ var mac = ios || /Mac/.test(platform);
+ var chromeOS = /\bCrOS\b/.test(userAgent);
+ var windows = /win/i.test(platform);
+
+ var presto_version = presto && 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 || presto && (presto_version == null || presto_version < 12.11));
+ var captureRightClick = gecko || (ie && ie_version >= 9);
+
+ // Optimize some code when these features are not used.
+ var sawReadOnlySpans = false, sawCollapsedSpans = false;
+
+ // 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 ? copyObj(options) : {};
+ // Determine effective options based on given values and defaults.
+ copyObj(defaults, options, false);
+ setGuttersForLineNumbers(options);
+
+ var doc = options.value;
+ if (typeof doc == "string") doc = new Doc(doc, options.mode, null, options.lineSeparator);
+ this.doc = doc;
+
+ var input = new CodeMirror.inputStyles[options.inputStyle](this);
+ var display = this.display = new Display(place, doc, input);
+ display.wrapper.CodeMirror = this;
+ updateGutters(this);
+ themeChanged(this);
+ if (options.lineWrapping)
+ this.display.wrapper.className += " CodeMirror-wrap";
+ if (options.autofocus && !mobile) display.input.focus();
+ initScrollbars(this);
+
+ 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,
+ delayingBlurEvent: 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 input.poll
+ selectingText: false,
+ draggingText: false,
+ highlight: new Delayed(), // stores highlight worker timeout
+ keySeq: null, // Unfinished key sequence
+ specialChars: null
+ };
+
+ var cm = this;
+
+ // Override magic textarea content restore that IE sometimes does
+ // on our hidden textarea on reload
+ if (ie && ie_version < 11) setTimeout(function() { cm.display.input.reset(true); }, 20);
+
+ registerEventHandlers(this);
+ ensureGlobalHandlers();
+
+ startOperation(this);
+ this.curOp.forceUpdate = true;
+ attachDoc(this, doc);
+
+ if ((options.autofocus && !mobile) || cm.hasFocus())
+ setTimeout(bind(onFocus, this), 20);
+ else
+ onBlur(this);
+
+ for (var opt in optionHandlers) if (optionHandlers.hasOwnProperty(opt))
+ optionHandlers[opt](this, options[opt], Init);
+ maybeUpdateLineNumberWidth(this);
+ if (options.finishInit) options.finishInit(this);
+ for (var i = 0; i < initHooks.length; ++i) initHooks[i](this);
+ endOperation(this);
+ // Suppress optimizelegibility in Webkit, since it breaks text
+ // measuring on line wrapping boundaries.
+ if (webkit && options.lineWrapping &&
+ getComputedStyle(display.lineDiv).textRendering == "optimizelegibility")
+ display.lineDiv.style.textRendering = "auto";
+ }
+
+ // DISPLAY CONSTRUCTOR
+
+ // The display handles the DOM integration, both for input reading
+ // and content drawing. It holds references to DOM nodes and
+ // display-related state.
+
+ function Display(place, doc, input) {
+ var d = this;
+ this.input = input;
+
+ // Covers bottom-right square when both scrollbars are present.
+ d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler");
+ d.scrollbarFiller.setAttribute("cm-not-content", "true");
+ // Covers bottom of gutter when coverGutterNextToScrollbar is on
+ // and h scrollbar is present.
+ d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler");
+ d.gutterFiller.setAttribute("cm-not-content", "true");
+ // 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");
+ 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.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 document, allowing scrolling.
+ d.sizer = elt("div", [d.mover], "CodeMirror-sizer");
+ d.sizerWidth = null;
+ // 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: " + scrollerGap + "px; width: 1px;");
+ // Will contain the gutters, if any.
+ d.gutters = elt("div", null, "CodeMirror-gutters");
+ d.lineGutter = null;
+ // 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.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror");
+
+ // Work around IE7 z-index bug (not perfect, hence IE7 not really being supported)
+ if (ie && ie_version < 8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; }
+ if (!webkit && !(gecko && mobile)) d.scroller.draggable = true;
+
+ if (place) {
+ 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;
+ d.reportedViewFrom = d.reportedViewTo = doc.first;
+ // Information about the rendered lines.
+ d.view = [];
+ d.renderedView = null;
+ // 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.lastWrapHeight = d.lastWrapWidth = 0;
+ d.updateLineNumbers = null;
+
+ d.nativeBarWidth = d.barHeight = d.barWidth = 0;
+ d.scrollbarsClipped = false;
+
+ // 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;
+ // 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;
+
+ d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null;
+
+ // Tracks the maximum line length so that the horizontal scrollbar
+ // can be kept static when scrolling.
+ d.maxLine = null;
+ d.maxLineLength = 0;
+ d.maxLineChanged = false;
+
+ // Used for measuring wheel scrolling granularity
+ d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null;
+
+ // True when shift is held down.
+ d.shift = false;
+
+ // Used to track whether anything happened since the context menu
+ // was opened.
+ d.selForContextMenu = null;
+
+ d.activeTouch = null;
+
+ input.init(d);
+ }
+
+ // STATE UPDATES
+
+ // Used to get the editor into a consistent state again when options change.
+
+ function loadMode(cm) {
+ cm.doc.mode = CodeMirror.getMode(cm.options, cm.doc.modeOption);
+ resetModeState(cm);
+ }
+
+ function resetModeState(cm) {
+ cm.doc.iter(function(line) {
+ if (line.stateAfter) line.stateAfter = null;
+ if (line.styles) line.styles = null;
+ });
+ cm.doc.frontier = cm.doc.first;
+ startWorker(cm, 100);
+ cm.state.modeGen++;
+ if (cm.curOp) regChange(cm);
+ }
+
+ function wrappingChanged(cm) {
+ if (cm.options.lineWrapping) {
+ addClass(cm.display.wrapper, "CodeMirror-wrap");
+ cm.display.sizer.style.minWidth = "";
+ cm.display.sizerWidth = null;
+ } else {
+ rmClass(cm.display.wrapper, "CodeMirror-wrap");
+ findMaxLine(cm);
+ }
+ estimateLineHeights(cm);
+ regChange(cm);
+ clearCaches(cm);
+ 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;
+
+ 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 widgetsHeight + th;
+ };
+ }
+
+ function estimateLineHeights(cm) {
+ var doc = cm.doc, est = estimateHeight(cm);
+ doc.iter(function(line) {
+ var estHeight = est(line);
+ if (estHeight != line.height) updateLineHeight(line, estHeight);
+ });
+ }
+
+ function themeChanged(cm) {
+ cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-s-\S+/g, "") +
+ cm.options.theme.replace(/(^|\s)\s*/g, " cm-s-");
+ clearCaches(cm);
+ }
+
+ function guttersChanged(cm) {
+ updateGutters(cm);
+ regChange(cm);
+ 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);
+ for (var i = 0; i < specs.length; ++i) {
+ var gutterClass = specs[i];
+ var gElt = gutters.appendChild(elt("div", null, "CodeMirror-gutter " + gutterClass));
+ if (gutterClass == "CodeMirror-linenumbers") {
+ cm.display.lineGutter = gElt;
+ gElt.style.width = (cm.display.lineNumWidth || 1) + "px";
+ }
+ }
+ gutters.style.display = i ? "" : "none";
+ updateGutterSpace(cm);
+ }
+
+ function updateGutterSpace(cm) {
+ var width = cm.display.gutters.offsetWidth;
+ cm.display.sizer.style.marginLeft = width + "px";
+ }
+
+ // 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(0, true);
+ cur = found.from.line;
+ len += found.from.ch - found.to.ch;
+ }
+ cur = line;
+ while (merged = collapsedSpanAtEnd(cur)) {
+ var found = merged.find(0, true);
+ len -= cur.text.length - found.from.ch;
+ cur = found.to.line;
+ len += cur.text.length - found.to.ch;
+ }
+ return len;
+ }
+
+ // 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(d.maxLine);
+ d.maxLineChanged = true;
+ doc.iter(function(line) {
+ var len = lineLength(line);
+ if (len > d.maxLineLength) {
+ d.maxLineLength = len;
+ d.maxLine = line;
+ }
+ });
+ }
+
+ // Make sure the gutters options contains the element
+ // "CodeMirror-linenumbers" when the lineNumbers option is true.
+ function setGuttersForLineNumbers(options) {
+ var found = indexOf(options.gutters, "CodeMirror-linenumbers");
+ if (found == -1 && options.lineNumbers) {
+ options.gutters = options.gutters.concat(["CodeMirror-linenumbers"]);
+ } else if (found > -1 && !options.lineNumbers) {
+ options.gutters = options.gutters.slice(0);
+ options.gutters.splice(found, 1);
+ }
+ }
+
+ // SCROLLBARS
+
+ // Prepare DOM reads needed to update the scrollbars. Done in one
+ // shot to minimize update/measure roundtrips.
+ function measureForScrollbars(cm) {
+ var d = cm.display, gutterW = d.gutters.offsetWidth;
+ var docH = Math.round(cm.doc.height + paddingVert(cm.display));
+ return {
+ clientHeight: d.scroller.clientHeight,
+ viewHeight: d.wrapper.clientHeight,
+ scrollWidth: d.scroller.scrollWidth, clientWidth: d.scroller.clientWidth,
+ viewWidth: d.wrapper.clientWidth,
+ barLeft: cm.options.fixedGutter ? gutterW : 0,
+ docHeight: docH,
+ scrollHeight: docH + scrollGap(cm) + d.barHeight,
+ nativeBarWidth: d.nativeBarWidth,
+ gutterWidth: gutterW
+ };
+ }
+
+ function NativeScrollbars(place, scroll, cm) {
+ this.cm = cm;
+ var vert = this.vert = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar");
+ var horiz = this.horiz = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar");
+ place(vert); place(horiz);
+
+ on(vert, "scroll", function() {
+ if (vert.clientHeight) scroll(vert.scrollTop, "vertical");
+ });
+ on(horiz, "scroll", function() {
+ if (horiz.clientWidth) scroll(horiz.scrollLeft, "horizontal");
+ });
+
+ this.checkedZeroWidth = false;
+ // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8).
+ if (ie && ie_version < 8) this.horiz.style.minHeight = this.vert.style.minWidth = "18px";
+ }
+
+ NativeScrollbars.prototype = copyObj({
+ update: function(measure) {
+ var needsH = measure.scrollWidth > measure.clientWidth + 1;
+ var needsV = measure.scrollHeight > measure.clientHeight + 1;
+ var sWidth = measure.nativeBarWidth;
+
+ if (needsV) {
+ this.vert.style.display = "block";
+ this.vert.style.bottom = needsH ? sWidth + "px" : "0";
+ var totalHeight = measure.viewHeight - (needsH ? sWidth : 0);
+ // A bug in IE8 can cause this value to be negative, so guard it.
+ this.vert.firstChild.style.height =
+ Math.max(0, measure.scrollHeight - measure.clientHeight + totalHeight) + "px";
+ } else {
+ this.vert.style.display = "";
+ this.vert.firstChild.style.height = "0";
+ }
+
+ if (needsH) {
+ this.horiz.style.display = "block";
+ this.horiz.style.right = needsV ? sWidth + "px" : "0";
+ this.horiz.style.left = measure.barLeft + "px";
+ var totalWidth = measure.viewWidth - measure.barLeft - (needsV ? sWidth : 0);
+ this.horiz.firstChild.style.width =
+ (measure.scrollWidth - measure.clientWidth + totalWidth) + "px";
+ } else {
+ this.horiz.style.display = "";
+ this.horiz.firstChild.style.width = "0";
+ }
+
+ if (!this.checkedZeroWidth && measure.clientHeight > 0) {
+ if (sWidth == 0) this.zeroWidthHack();
+ this.checkedZeroWidth = true;
+ }
+
+ return {right: needsV ? sWidth : 0, bottom: needsH ? sWidth : 0};
+ },
+ setScrollLeft: function(pos) {
+ if (this.horiz.scrollLeft != pos) this.horiz.scrollLeft = pos;
+ if (this.disableHoriz) this.enableZeroWidthBar(this.horiz, this.disableHoriz);
+ },
+ setScrollTop: function(pos) {
+ if (this.vert.scrollTop != pos) this.vert.scrollTop = pos;
+ if (this.disableVert) this.enableZeroWidthBar(this.vert, this.disableVert);
+ },
+ zeroWidthHack: function() {
+ var w = mac && !mac_geMountainLion ? "12px" : "18px";
+ this.horiz.style.height = this.vert.style.width = w;
+ this.horiz.style.pointerEvents = this.vert.style.pointerEvents = "none";
+ this.disableHoriz = new Delayed;
+ this.disableVert = new Delayed;
+ },
+ enableZeroWidthBar: function(bar, delay) {
+ bar.style.pointerEvents = "auto";
+ function maybeDisable() {
+ // To find out whether the scrollbar is still visible, we
+ // check whether the element under the pixel in the bottom
+ // left corner of the scrollbar box is the scrollbar box
+ // itself (when the bar is still visible) or its filler child
+ // (when the bar is hidden). If it is still visible, we keep
+ // it enabled, if it's hidden, we disable pointer events.
+ var box = bar.getBoundingClientRect();
+ var elt = document.elementFromPoint(box.left + 1, box.bottom - 1);
+ if (elt != bar) bar.style.pointerEvents = "none";
+ else delay.set(1000, maybeDisable);
+ }
+ delay.set(1000, maybeDisable);
+ },
+ clear: function() {
+ var parent = this.horiz.parentNode;
+ parent.removeChild(this.horiz);
+ parent.removeChild(this.vert);
+ }
+ }, NativeScrollbars.prototype);
+
+ function NullScrollbars() {}
+
+ NullScrollbars.prototype = copyObj({
+ update: function() { return {bottom: 0, right: 0}; },
+ setScrollLeft: function() {},
+ setScrollTop: function() {},
+ clear: function() {}
+ }, NullScrollbars.prototype);
+
+ CodeMirror.scrollbarModel = {"native": NativeScrollbars, "null": NullScrollbars};
+
+ function initScrollbars(cm) {
+ if (cm.display.scrollbars) {
+ cm.display.scrollbars.clear();
+ if (cm.display.scrollbars.addClass)
+ rmClass(cm.display.wrapper, cm.display.scrollbars.addClass);
+ }
+
+ cm.display.scrollbars = new CodeMirror.scrollbarModel[cm.options.scrollbarStyle](function(node) {
+ cm.display.wrapper.insertBefore(node, cm.display.scrollbarFiller);
+ // Prevent clicks in the scrollbars from killing focus
+ on(node, "mousedown", function() {
+ if (cm.state.focused) setTimeout(function() { cm.display.input.focus(); }, 0);
+ });
+ node.setAttribute("cm-not-content", "true");
+ }, function(pos, axis) {
+ if (axis == "horizontal") setScrollLeft(cm, pos);
+ else setScrollTop(cm, pos);
+ }, cm);
+ if (cm.display.scrollbars.addClass)
+ addClass(cm.display.wrapper, cm.display.scrollbars.addClass);
+ }
+
+ function updateScrollbars(cm, measure) {
+ if (!measure) measure = measureForScrollbars(cm);
+ var startWidth = cm.display.barWidth, startHeight = cm.display.barHeight;
+ updateScrollbarsInner(cm, measure);
+ for (var i = 0; i < 4 && startWidth != cm.display.barWidth || startHeight != cm.display.barHeight; i++) {
+ if (startWidth != cm.display.barWidth && cm.options.lineWrapping)
+ updateHeightsInViewport(cm);
+ updateScrollbarsInner(cm, measureForScrollbars(cm));
+ startWidth = cm.display.barWidth; startHeight = cm.display.barHeight;
+ }
+ }
+
+ // Re-synchronize the fake scrollbars with the actual size of the
+ // content.
+ function updateScrollbarsInner(cm, measure) {
+ var d = cm.display;
+ var sizes = d.scrollbars.update(measure);
+
+ d.sizer.style.paddingRight = (d.barWidth = sizes.right) + "px";
+ d.sizer.style.paddingBottom = (d.barHeight = sizes.bottom) + "px";
+ d.heightForcer.style.borderBottom = sizes.bottom + "px solid transparent"
+
+ if (sizes.right && sizes.bottom) {
+ d.scrollbarFiller.style.display = "block";
+ d.scrollbarFiller.style.height = sizes.bottom + "px";
+ d.scrollbarFiller.style.width = sizes.right + "px";
+ } else d.scrollbarFiller.style.display = "";
+ if (sizes.bottom && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) {
+ d.gutterFiller.style.display = "block";
+ d.gutterFiller.style.height = sizes.bottom + "px";
+ d.gutterFiller.style.width = measure.gutterWidth + "px";
+ } else d.gutterFiller.style.display = "";
+ }
+
+ // 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 = viewport && viewport.top != null ? Math.max(0, viewport.top) : display.scroller.scrollTop;
+ top = Math.floor(top - paddingTop(display));
+ 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) {
+ from = ensureFrom;
+ to = lineAtHeight(doc, heightAtLine(getLine(doc, ensureFrom)) + display.wrapper.clientHeight);
+ } else if (Math.min(ensureTo, doc.lastLine()) >= to) {
+ from = lineAtHeight(doc, heightAtLine(getLine(doc, ensureTo)) - display.wrapper.clientHeight);
+ to = ensureTo;
+ }
+ }
+ return {from: from, to: Math.max(to, from + 1)};
+ }
+
+ // LINE NUMBERS
+
+ // Re-align line numbers and gutter marks to compensate for
+ // horizontal scrolling.
+ function alignHorizontally(cm) {
+ 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, 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;
+ if (last.length != display.lineNumChars) {
+ var test = display.measure.appendChild(elt("div", [elt("div", last)],
+ "CodeMirror-linenumber CodeMirror-gutter-elt"));
+ var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW;
+ display.lineGutter.style.width = "";
+ display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding) + 1;
+ display.lineNumWidth = display.lineNumInnerWidth + padding;
+ display.lineNumChars = display.lineNumInnerWidth ? last.length : -1;
+ display.lineGutter.style.width = display.lineNumWidth + "px";
+ updateGutterSpace(cm);
+ return true;
+ }
+ return false;
+ }
+
+ 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 display.scroller.getBoundingClientRect().left - display.sizer.getBoundingClientRect().left;
+ }
+
+ // DISPLAY DRAWING
+
+ function DisplayUpdate(cm, viewport, force) {
+ var display = cm.display;
+
+ this.viewport = viewport;
+ // Store some values that we'll need later (but don't want to force a relayout for)
+ this.visible = visibleLines(display, cm.doc, viewport);
+ this.editorIsHidden = !display.wrapper.offsetWidth;
+ this.wrapperHeight = display.wrapper.clientHeight;
+ this.wrapperWidth = display.wrapper.clientWidth;
+ this.oldDisplayWidth = displayWidth(cm);
+ this.force = force;
+ this.dims = getDimensions(cm);
+ this.events = [];
+ }
+
+ DisplayUpdate.prototype.signal = function(emitter, type) {
+ if (hasHandler(emitter, type))
+ this.events.push(arguments);
+ };
+ DisplayUpdate.prototype.finish = function() {
+ for (var i = 0; i < this.events.length; i++)
+ signal.apply(null, this.events[i]);
+ };
+
+ function maybeClipScrollbars(cm) {
+ var display = cm.display;
+ if (!display.scrollbarsClipped && display.scroller.offsetWidth) {
+ display.nativeBarWidth = display.scroller.offsetWidth - display.scroller.clientWidth;
+ display.heightForcer.style.height = scrollGap(cm) + "px";
+ display.sizer.style.marginBottom = -display.nativeBarWidth + "px";
+ display.sizer.style.borderRightWidth = scrollGap(cm) + "px";
+ display.scrollbarsClipped = true;
+ }
+ }
+
+ // Does the actual updating of the line display. Bails out
+ // (returning false) when there is nothing to be done and forced is
+ // false.
+ function updateDisplayIfNeeded(cm, update) {
+ var display = cm.display, doc = cm.doc;
+
+ if (update.editorIsHidden) {
+ resetView(cm);
+ return false;
+ }
+
+ // Bail out if the visible area is already rendered and nothing changed.
+ if (!update.force &&
+ update.visible.from >= display.viewFrom && update.visible.to <= display.viewTo &&
+ (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo) &&
+ display.renderedView == display.view && countDirtyView(cm) == 0)
+ return false;
+
+ if (maybeUpdateLineNumberWidth(cm)) {
+ resetView(cm);
+ update.dims = getDimensions(cm);
+ }
+
+ // Compute a suitable new viewport (from & to)
+ var end = doc.first + doc.size;
+ var from = Math.max(update.visible.from - cm.options.viewportMargin, doc.first);
+ var to = Math.min(end, update.visible.to + cm.options.viewportMargin);
+ 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 = visualLineNo(cm.doc, from);
+ to = visualLineEndNo(cm.doc, to);
+ }
+
+ var different = from != display.viewFrom || to != display.viewTo ||
+ display.lastWrapHeight != update.wrapperHeight || display.lastWrapWidth != update.wrapperWidth;
+ 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 && !update.force && display.renderedView == display.view &&
+ (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo))
+ return false;
+
+ // 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, update.dims);
+ if (toUpdate > 4) display.lineDiv.style.display = "";
+ display.renderedView = display.view;
+ // 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 and height.
+ removeChildren(display.cursorDiv);
+ removeChildren(display.selectionDiv);
+ display.gutters.style.height = display.sizer.style.minHeight = 0;
+
+ if (different) {
+ display.lastWrapHeight = update.wrapperHeight;
+ display.lastWrapWidth = update.wrapperWidth;
+ startWorker(cm, 400);
+ }
+
+ display.updateLineNumbers = null;
+
+ return true;
+ }
+
+ function postUpdateDisplay(cm, update) {
+ var viewport = update.viewport;
+
+ for (var first = true;; first = false) {
+ if (!first || !cm.options.lineWrapping || update.oldDisplayWidth == displayWidth(cm)) {
+ // Clip forced viewport to actual scrollable area.
+ if (viewport && viewport.top != null)
+ viewport = {top: Math.min(cm.doc.height + paddingVert(cm.display) - displayHeight(cm), viewport.top)};
+ // Updated line heights might result in the drawn area not
+ // actually covering the viewport. Keep looping until it does.
+ update.visible = visibleLines(cm.display, cm.doc, viewport);
+ if (update.visible.from >= cm.display.viewFrom && update.visible.to <= cm.display.viewTo)
+ break;
+ }
+ if (!updateDisplayIfNeeded(cm, update)) break;
+ updateHeightsInViewport(cm);
+ var barMeasure = measureForScrollbars(cm);
+ updateSelection(cm);
+ updateScrollbars(cm, barMeasure);
+ setDocumentHeight(cm, barMeasure);
+ }
+
+ update.signal(cm, "update", cm);
+ if (cm.display.viewFrom != cm.display.reportedViewFrom || cm.display.viewTo != cm.display.reportedViewTo) {
+ update.signal(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo);
+ cm.display.reportedViewFrom = cm.display.viewFrom; cm.display.reportedViewTo = cm.display.viewTo;
+ }
+ }
+
+ function updateDisplaySimple(cm, viewport) {
+ var update = new DisplayUpdate(cm, viewport);
+ if (updateDisplayIfNeeded(cm, update)) {
+ updateHeightsInViewport(cm);
+ postUpdateDisplay(cm, update);
+ var barMeasure = measureForScrollbars(cm);
+ updateSelection(cm);
+ updateScrollbars(cm, barMeasure);
+ setDocumentHeight(cm, barMeasure);
+ update.finish();
+ }
+ }
+
+ function setDocumentHeight(cm, measure) {
+ cm.display.sizer.style.minHeight = measure.docHeight + "px";
+ cm.display.heightForcer.style.top = measure.docHeight + "px";
+ cm.display.gutters.style.height = (measure.docHeight + cm.display.barHeight + scrollGap(cm)) + "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 i = 0; i < display.view.length; i++) {
+ var cur = display.view[i], height;
+ if (cur.hidden) continue;
+ if (ie && ie_version < 8) {
+ var bot = cur.node.offsetTop + cur.node.offsetHeight;
+ height = bot - prevBottom;
+ prevBottom = bot;
+ } else {
+ var box = cur.node.getBoundingClientRect();
+ height = box.bottom - box.top;
+ }
+ var diff = cur.line.height - height;
+ if (height < 2) height = textHeight(display);
+ if (diff > .001 || diff < -.001) {
+ updateLineHeight(cur.line, height);
+ updateWidgetHeight(cur.line);
+ if (cur.rest) for (var j = 0; j < cur.rest.length; j++)
+ updateWidgetHeight(cur.rest[j]);
+ }
+ }
+ }
+
+ // 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.parentNode.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 = {};
+ var gutterLeft = d.gutters.clientLeft;
+ for (var n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) {
+ left[cm.options.gutters[i]] = n.offsetLeft + n.clientLeft + gutterLeft;
+ width[cm.options.gutters[i]] = n.clientWidth;
+ }
+ return {fixedPos: compensateForHScroll(d),
+ gutterTotalWidth: d.gutters.offsetWidth,
+ gutterLeft: left,
+ gutterWidth: width,
+ wrapperWidth: d.wrapper.clientWidth};
+ }
+
+ // 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;
+ var container = display.lineDiv, cur = container.firstChild;
+
+ function rm(node) {
+ var next = node.nextSibling;
+ // Works around a throw-scroll bug in OS X Webkit
+ if (webkit && mac && cm.display.currentWheelTarget == node)
+ node.style.display = "none";
+ else
+ node.parentNode.removeChild(node);
+ return next;
+ }
+
+ 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 || lineView.node.parentNode != container) { // 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);
+ }
+ if (updateNumber) {
+ removeChildren(lineView.lineNumber);
+ lineView.lineNumber.appendChild(document.createTextNode(lineNumberFor(cm.options, lineN)));
+ }
+ cur = lineView.node.nextSibling;
+ }
+ lineN += lineView.size;
+ }
+ while (cur) cur = rm(cur);
+ }
+
+ // 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(cm, lineView, dims);
+ }
+ 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 && ie_version < 8) lineView.node.style.zIndex = 2;
+ }
+ 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;
+ }
+ if (lineView.gutterBackground) {
+ lineView.node.removeChild(lineView.gutterBackground);
+ lineView.gutterBackground = null;
+ }
+ if (lineView.line.gutterClass) {
+ var wrap = ensureLineWrapped(lineView);
+ lineView.gutterBackground = elt("div", null, "CodeMirror-gutter-background " + lineView.line.gutterClass,
+ "left: " + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) +
+ "px; width: " + dims.gutterTotalWidth + "px");
+ wrap.insertBefore(lineView.gutterBackground, lineView.text);
+ }
+ var markers = lineView.line.gutterMarkers;
+ if (cm.options.lineNumbers || markers) {
+ var wrap = ensureLineWrapped(lineView);
+ var gutterWrap = lineView.gutter = elt("div", null, "CodeMirror-gutter-wrapper", "left: " +
+ (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px");
+ cm.display.input.setUneditable(gutterWrap);
+ wrap.insertBefore(gutterWrap, lineView.text);
+ if (lineView.line.gutterClass)
+ gutterWrap.className += " " + lineView.line.gutterClass;
+ if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"]))
+ lineView.lineNumber = gutterWrap.appendChild(
+ elt("div", lineNumberFor(cm.options, lineN),
+ "CodeMirror-linenumber CodeMirror-gutter-elt",
+ "left: " + dims.gutterLeft["CodeMirror-linenumbers"] + "px; width: "
+ + 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"));
+ }
+ }
+ }
+
+ function updateLineWidgets(cm, 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(cm, 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(cm, 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(cm, lineView, dims) {
+ insertLineWidgetsFor(cm, lineView.line, lineView, dims, true);
+ if (lineView.rest) for (var i = 0; i < lineView.rest.length; i++)
+ insertLineWidgetsFor(cm, lineView.rest[i], lineView, dims, false);
+ }
+
+ function insertLineWidgetsFor(cm, 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.setAttribute("cm-ignore-events", "true");
+ positionLineWidget(widget, node, lineView, dims);
+ cm.display.input.setUneditable(node);
+ if (allowAbove && widget.above)
+ wrap.insertBefore(node, lineView.gutter || lineView.text);
+ else
+ wrap.appendChild(node);
+ signalLater(widget, "redraw");
+ }
+ }
+
+ function positionLineWidget(widget, node, lineView, dims) {
+ if (widget.noHScroll) {
+ (lineView.alignable || (lineView.alignable = [])).push(node);
+ var width = dims.wrapperWidth;
+ node.style.left = dims.fixedPos + "px";
+ if (!widget.coverGutter) {
+ width -= dims.gutterTotalWidth;
+ node.style.paddingLeft = dims.gutterTotalWidth + "px";
+ }
+ node.style.width = width + "px";
+ }
+ if (widget.coverGutter) {
+ node.style.zIndex = 5;
+ node.style.position = "relative";
+ if (!widget.noHScroll) node.style.marginLeft = -dims.gutterTotalWidth + "px";
+ }
+ }
+
+ // 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; }
+
+ // INPUT HANDLING
+
+ function ensureFocus(cm) {
+ if (!cm.state.focused) { cm.display.input.focus(); onFocus(cm); }
+ }
+
+ // This will be set to a {lineWise: bool, text: [string]} object, so
+ // that, when pasting, we know what kind of selections the copied
+ // text was made out of.
+ var lastCopied = null;
+
+ function applyTextInput(cm, inserted, deleted, sel, origin) {
+ var doc = cm.doc;
+ cm.display.shift = false;
+ if (!sel) sel = doc.sel;
+
+ var paste = cm.state.pasteIncoming || origin == "paste";
+ var textLines = doc.splitLines(inserted), multiPaste = null
+ // When pasing N lines into N selections, insert one line per selection
+ if (paste && sel.ranges.length > 1) {
+ if (lastCopied && lastCopied.text.join("\n") == inserted) {
+ if (sel.ranges.length % lastCopied.text.length == 0) {
+ multiPaste = [];
+ for (var i = 0; i < lastCopied.text.length; i++)
+ multiPaste.push(doc.splitLines(lastCopied.text[i]));
+ }
+ } else if (textLines.length == sel.ranges.length) {
+ multiPaste = map(textLines, function(l) { return [l]; });
+ }
+ }
+
+ // Normal behavior is to insert the new text into every selection
+ for (var i = sel.ranges.length - 1; i >= 0; i--) {
+ var range = sel.ranges[i];
+ var from = range.from(), to = range.to();
+ if (range.empty()) {
+ if (deleted && deleted > 0) // Handle deletion
+ from = Pos(from.line, from.ch - deleted);
+ else if (cm.state.overwrite && !paste) // Handle overwrite
+ to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + lst(textLines).length));
+ else if (lastCopied && lastCopied.lineWise && lastCopied.text.join("\n") == inserted)
+ from = to = Pos(from.line, 0)
+ }
+ var updateInput = cm.curOp.updateInput;
+ var changeEvent = {from: from, to: to, text: multiPaste ? multiPaste[i % multiPaste.length] : textLines,
+ origin: origin || (paste ? "paste" : cm.state.cutIncoming ? "cut" : "+input")};
+ makeChange(cm.doc, changeEvent);
+ signalLater(cm, "inputRead", cm, changeEvent);
+ }
+ if (inserted && !paste)
+ triggerElectric(cm, inserted);
+
+ ensureCursorVisible(cm);
+ cm.curOp.updateInput = updateInput;
+ cm.curOp.typing = true;
+ cm.state.pasteIncoming = cm.state.cutIncoming = false;
+ }
+
+ function handlePaste(e, cm) {
+ var pasted = e.clipboardData && e.clipboardData.getData("text/plain");
+ if (pasted) {
+ e.preventDefault();
+ if (!cm.isReadOnly() && !cm.options.disableInput)
+ runInOp(cm, function() { applyTextInput(cm, pasted, 0, null, "paste"); });
+ return true;
+ }
+ }
+
+ function triggerElectric(cm, inserted) {
+ // When an 'electric' character is inserted, immediately trigger a reindent
+ if (!cm.options.electricChars || !cm.options.smartIndent) return;
+ var sel = cm.doc.sel;
+
+ for (var i = sel.ranges.length - 1; i >= 0; i--) {
+ var range = sel.ranges[i];
+ if (range.head.ch > 100 || (i && sel.ranges[i - 1].head.line == range.head.line)) continue;
+ var mode = cm.getModeAt(range.head);
+ var indented = false;
+ if (mode.electricChars) {
+ for (var j = 0; j < mode.electricChars.length; j++)
+ if (inserted.indexOf(mode.electricChars.charAt(j)) > -1) {
+ indented = indentLine(cm, range.head.line, "smart");
+ break;
+ }
+ } else if (mode.electricInput) {
+ if (mode.electricInput.test(getLine(cm.doc, range.head.line).text.slice(0, range.head.ch)))
+ indented = indentLine(cm, range.head.line, "smart");
+ }
+ if (indented) signalLater(cm, "electricInput", cm, range.head.line);
+ }
+ }
+
+ function copyableRanges(cm) {
+ var text = [], ranges = [];
+ for (var i = 0; i < cm.doc.sel.ranges.length; i++) {
+ var line = cm.doc.sel.ranges[i].head.line;
+ var lineRange = {anchor: Pos(line, 0), head: Pos(line + 1, 0)};
+ ranges.push(lineRange);
+ text.push(cm.getRange(lineRange.anchor, lineRange.head));
+ }
+ return {text: text, ranges: ranges};
+ }
+
+ function disableBrowserMagic(field) {
+ field.setAttribute("autocorrect", "off");
+ field.setAttribute("autocapitalize", "off");
+ field.setAttribute("spellcheck", "false");
+ }
+
+ // TEXTAREA INPUT STYLE
+
+ function TextareaInput(cm) {
+ this.cm = cm;
+ // See input.poll and input.reset
+ this.prevInput = "";
+
+ // Flag that indicates whether we expect input to appear real soon
+ // now (after some event like 'keypress' or 'input') and are
+ // polling intensively.
+ this.pollingFast = false;
+ // Self-resetting timeout for the poller
+ this.polling = new Delayed();
+ // Tracks when input.reset has punted to just putting a short
+ // string into the textarea instead of the full selection.
+ this.inaccurateSelection = false;
+ // Used to work around IE issue with selection being forgotten when focus moves away from textarea
+ this.hasSelection = false;
+ this.composing = null;
+ };
+
+ function hiddenTextarea() {
+ var te = elt("textarea", null, null, "position: absolute; padding: 0; width: 1px; height: 1em; outline: none");
+ var div = elt("div", [te], null, "overflow: hidden; position: relative; width: 3px; height: 0px;");
+ // 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) te.style.width = "1000px";
+ else te.setAttribute("wrap", "off");
+ // If border: 0; -- iOS fails to open keyboard (issue #1287)
+ if (ios) te.style.border = "1px solid black";
+ disableBrowserMagic(te);
+ return div;
+ }
+
+ TextareaInput.prototype = copyObj({
+ init: function(display) {
+ var input = this, cm = this.cm;
+
+ // Wraps and hides input textarea
+ var div = this.wrapper = hiddenTextarea();
+ // The semihidden textarea that is focused when the editor is
+ // focused, and receives input.
+ var te = this.textarea = div.firstChild;
+ display.wrapper.insertBefore(div, display.wrapper.firstChild);
+
+ // Needed to hide big blue blinking cursor on Mobile Safari (doesn't seem to work in iOS 8 anymore)
+ if (ios) te.style.width = "0px";
+
+ on(te, "input", function() {
+ if (ie && ie_version >= 9 && input.hasSelection) input.hasSelection = null;
+ input.poll();
+ });
+
+ on(te, "paste", function(e) {
+ if (signalDOMEvent(cm, e) || handlePaste(e, cm)) return
+
+ cm.state.pasteIncoming = true;
+ input.fastPoll();
+ });
+
+ function prepareCopyCut(e) {
+ if (signalDOMEvent(cm, e)) return
+ if (cm.somethingSelected()) {
+ lastCopied = {lineWise: false, text: cm.getSelections()};
+ if (input.inaccurateSelection) {
+ input.prevInput = "";
+ input.inaccurateSelection = false;
+ te.value = lastCopied.text.join("\n");
+ selectInput(te);
+ }
+ } else if (!cm.options.lineWiseCopyCut) {
+ return;
+ } else {
+ var ranges = copyableRanges(cm);
+ lastCopied = {lineWise: true, text: ranges.text};
+ if (e.type == "cut") {
+ cm.setSelections(ranges.ranges, null, sel_dontScroll);
+ } else {
+ input.prevInput = "";
+ te.value = ranges.text.join("\n");
+ selectInput(te);
+ }
+ }
+ if (e.type == "cut") cm.state.cutIncoming = true;
+ }
+ on(te, "cut", prepareCopyCut);
+ on(te, "copy", prepareCopyCut);
+
+ on(display.scroller, "paste", function(e) {
+ if (eventInWidget(display, e) || signalDOMEvent(cm, e)) return;
+ cm.state.pasteIncoming = true;
+ input.focus();
+ });
+
+ // Prevent normal selection in the editor (we handle our own)
+ on(display.lineSpace, "selectstart", function(e) {
+ if (!eventInWidget(display, e)) e_preventDefault(e);
+ });
+
+ on(te, "compositionstart", function() {
+ var start = cm.getCursor("from");
+ if (input.composing) input.composing.range.clear()
+ input.composing = {
+ start: start,
+ range: cm.markText(start, cm.getCursor("to"), {className: "CodeMirror-composing"})
+ };
+ });
+ on(te, "compositionend", function() {
+ if (input.composing) {
+ input.poll();
+ input.composing.range.clear();
+ input.composing = null;
+ }
+ });
+ },
+
+ prepareSelection: function() {
+ // Redraw the selection and/or cursor
+ var cm = this.cm, display = cm.display, doc = cm.doc;
+ var result = prepareSelection(cm);
+
+ // 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();
+ result.teTop = Math.max(0, Math.min(display.wrapper.clientHeight - 10,
+ headPos.top + lineOff.top - wrapOff.top));
+ result.teLeft = Math.max(0, Math.min(display.wrapper.clientWidth - 10,
+ headPos.left + lineOff.left - wrapOff.left));
+ }
+
+ return result;
+ },
+
+ showSelection: function(drawn) {
+ var cm = this.cm, display = cm.display;
+ removeChildrenAndAdd(display.cursorDiv, drawn.cursors);
+ removeChildrenAndAdd(display.selectionDiv, drawn.selection);
+ if (drawn.teTop != null) {
+ this.wrapper.style.top = drawn.teTop + "px";
+ this.wrapper.style.left = drawn.teLeft + "px";
+ }
+ },
+
+ // Reset the input to correspond to the selection (or to be empty,
+ // when not typing and nothing is selected)
+ reset: function(typing) {
+ if (this.contextMenuPending) return;
+ var minimal, selected, cm = this.cm, doc = cm.doc;
+ if (cm.somethingSelected()) {
+ this.prevInput = "";
+ var range = doc.sel.primary();
+ minimal = hasCopyEvent &&
+ (range.to().line - range.from().line > 100 || (selected = cm.getSelection()).length > 1000);
+ var content = minimal ? "-" : selected || cm.getSelection();
+ this.textarea.value = content;
+ if (cm.state.focused) selectInput(this.textarea);
+ if (ie && ie_version >= 9) this.hasSelection = content;
+ } else if (!typing) {
+ this.prevInput = this.textarea.value = "";
+ if (ie && ie_version >= 9) this.hasSelection = null;
+ }
+ this.inaccurateSelection = minimal;
+ },
+
+ getField: function() { return this.textarea; },
+
+ supportsTouch: function() { return false; },
+
+ focus: function() {
+ if (this.cm.options.readOnly != "nocursor" && (!mobile || activeElt() != this.textarea)) {
+ try { this.textarea.focus(); }
+ catch (e) {} // IE8 will throw if the textarea is display: none or not in DOM
+ }
+ },
+
+ blur: function() { this.textarea.blur(); },
+
+ resetPosition: function() {
+ this.wrapper.style.top = this.wrapper.style.left = 0;
+ },
+
+ receivedFocus: function() { this.slowPoll(); },
+
+ // Poll for input changes, using the normal rate of polling. This
+ // runs as long as the editor is focused.
+ slowPoll: function() {
+ var input = this;
+ if (input.pollingFast) return;
+ input.polling.set(this.cm.options.pollInterval, function() {
+ input.poll();
+ if (input.cm.state.focused) input.slowPoll();
+ });
+ },
+
+ // 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.
+ fastPoll: function() {
+ var missed = false, input = this;
+ input.pollingFast = true;
+ function p() {
+ var changed = input.poll();
+ if (!changed && !missed) {missed = true; input.polling.set(60, p);}
+ else {input.pollingFast = false; input.slowPoll();}
+ }
+ input.polling.set(20, p);
+ },
+
+ // 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).
+ poll: function() {
+ var cm = this.cm, input = this.textarea, prevInput = this.prevInput;
+ // 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 (this.contextMenuPending || !cm.state.focused ||
+ (hasSelection(input) && !prevInput && !this.composing) ||
+ cm.isReadOnly() || cm.options.disableInput || cm.state.keySeq)
+ return false;
+
+ var text = input.value;
+ // If nothing changed, bail.
+ if (text == prevInput && !cm.somethingSelected()) return false;
+ // Work around nonsensical selection resetting in IE9/10, and
+ // inexplicable appearance of private area unicode characters on
+ // some key combos in Mac (#2689).
+ if (ie && ie_version >= 9 && this.hasSelection === text ||
+ mac && /[\uf700-\uf7ff]/.test(text)) {
+ cm.display.input.reset();
+ return false;
+ }
+
+ if (cm.doc.sel == cm.display.selForContextMenu) {
+ var first = text.charCodeAt(0);
+ if (first == 0x200b && !prevInput) prevInput = "\u200b";
+ if (first == 0x21da) { this.reset(); return this.cm.execCommand("undo"); }
+ }
+ // 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 self = this;
+ runInOp(cm, function() {
+ applyTextInput(cm, text.slice(same), prevInput.length - same,
+ null, self.composing ? "*compose" : null);
+
+ // Don't leave long text in the textarea, since it makes further polling slow
+ if (text.length > 1000 || text.indexOf("\n") > -1) input.value = self.prevInput = "";
+ else self.prevInput = text;
+
+ if (self.composing) {
+ self.composing.range.clear();
+ self.composing.range = cm.markText(self.composing.start, cm.getCursor("to"),
+ {className: "CodeMirror-composing"});
+ }
+ });
+ return true;
+ },
+
+ ensurePolled: function() {
+ if (this.pollingFast && this.poll()) this.pollingFast = false;
+ },
+
+ onKeyPress: function() {
+ if (ie && ie_version >= 9) this.hasSelection = null;
+ this.fastPoll();
+ },
+
+ onContextMenu: function(e) {
+ var input = this, cm = input.cm, display = cm.display, te = input.textarea;
+ var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop;
+ 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 && cm.doc.sel.contains(pos) == -1)
+ operation(cm, setSelection)(cm.doc, simpleSelection(pos), sel_dontScroll);
+
+ var oldCSS = te.style.cssText, oldWrapperCSS = input.wrapper.style.cssText;
+ input.wrapper.style.cssText = "position: absolute"
+ var wrapperBox = input.wrapper.getBoundingClientRect()
+ te.style.cssText = "position: absolute; width: 30px; height: 30px; top: " + (e.clientY - wrapperBox.top - 5) +
+ "px; left: " + (e.clientX - wrapperBox.left - 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);";
+ if (webkit) var oldScrollY = window.scrollY; // Work around Chrome issue (#2712)
+ display.input.focus();
+ if (webkit) window.scrollTo(null, oldScrollY);
+ display.input.reset();
+ // Adds "Select all" to context menu in FF
+ if (!cm.somethingSelected()) te.value = input.prevInput = " ";
+ input.contextMenuPending = true;
+ display.selForContextMenu = cm.doc.sel;
+ clearTimeout(display.detectingSelectAll);
+
+ // 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 (te.selectionStart != null) {
+ var selected = cm.somethingSelected();
+ var extval = "\u200b" + (selected ? te.value : "");
+ te.value = "\u21da"; // Used to catch context-menu undo
+ te.value = extval;
+ input.prevInput = selected ? "" : "\u200b";
+ te.selectionStart = 1; te.selectionEnd = extval.length;
+ // Re-set this, in case some other handler touched the
+ // selection in the meantime.
+ display.selForContextMenu = cm.doc.sel;
+ }
+ }
+ function rehide() {
+ input.contextMenuPending = false;
+ input.wrapper.style.cssText = oldWrapperCSS
+ te.style.cssText = oldCSS;
+ if (ie && ie_version < 9) display.scrollbars.setScrollTop(display.scroller.scrollTop = scrollPos);
+
+ // Try to detect the user choosing select-all
+ if (te.selectionStart != null) {
+ if (!ie || (ie && ie_version < 9)) prepareSelectAllHack();
+ var i = 0, poll = function() {
+ if (display.selForContextMenu == cm.doc.sel && te.selectionStart == 0 &&
+ te.selectionEnd > 0 && input.prevInput == "\u200b")
+ operation(cm, commands.selectAll)(cm);
+ else if (i++ < 10) display.detectingSelectAll = setTimeout(poll, 500);
+ else display.input.reset();
+ };
+ display.detectingSelectAll = setTimeout(poll, 200);
+ }
+ }
+
+ if (ie && ie_version >= 9) prepareSelectAllHack();
+ if (captureRightClick) {
+ e_stop(e);
+ var mouseup = function() {
+ off(window, "mouseup", mouseup);
+ setTimeout(rehide, 20);
+ };
+ on(window, "mouseup", mouseup);
+ } else {
+ setTimeout(rehide, 50);
+ }
+ },
+
+ readOnlyChanged: function(val) {
+ if (!val) this.reset();
+ },
+
+ setUneditable: nothing,
+
+ needsContentAttribute: false
+ }, TextareaInput.prototype);
+
+ // CONTENTEDITABLE INPUT STYLE
+
+ function ContentEditableInput(cm) {
+ this.cm = cm;
+ this.lastAnchorNode = this.lastAnchorOffset = this.lastFocusNode = this.lastFocusOffset = null;
+ this.polling = new Delayed();
+ this.gracePeriod = false;
+ }
+
+ ContentEditableInput.prototype = copyObj({
+ init: function(display) {
+ var input = this, cm = input.cm;
+ var div = input.div = display.lineDiv;
+ disableBrowserMagic(div);
+
+ on(div, "paste", function(e) {
+ if (!signalDOMEvent(cm, e)) handlePaste(e, cm);
+ })
+
+ on(div, "compositionstart", function(e) {
+ var data = e.data;
+ input.composing = {sel: cm.doc.sel, data: data, startData: data};
+ if (!data) return;
+ var prim = cm.doc.sel.primary();
+ var line = cm.getLine(prim.head.line);
+ var found = line.indexOf(data, Math.max(0, prim.head.ch - data.length));
+ if (found > -1 && found <= prim.head.ch)
+ input.composing.sel = simpleSelection(Pos(prim.head.line, found),
+ Pos(prim.head.line, found + data.length));
+ });
+ on(div, "compositionupdate", function(e) {
+ input.composing.data = e.data;
+ });
+ on(div, "compositionend", function(e) {
+ var ours = input.composing;
+ if (!ours) return;
+ if (e.data != ours.startData && !/\u200b/.test(e.data))
+ ours.data = e.data;
+ // Need a small delay to prevent other code (input event,
+ // selection polling) from doing damage when fired right after
+ // compositionend.
+ setTimeout(function() {
+ if (!ours.handled)
+ input.applyComposition(ours);
+ if (input.composing == ours)
+ input.composing = null;
+ }, 50);
+ });
+
+ on(div, "touchstart", function() {
+ input.forceCompositionEnd();
+ });
+
+ on(div, "input", function() {
+ if (input.composing) return;
+ if (cm.isReadOnly() || !input.pollContent())
+ runInOp(input.cm, function() {regChange(cm);});
+ });
+
+ function onCopyCut(e) {
+ if (signalDOMEvent(cm, e)) return
+ if (cm.somethingSelected()) {
+ lastCopied = {lineWise: false, text: cm.getSelections()};
+ if (e.type == "cut") cm.replaceSelection("", null, "cut");
+ } else if (!cm.options.lineWiseCopyCut) {
+ return;
+ } else {
+ var ranges = copyableRanges(cm);
+ lastCopied = {lineWise: true, text: ranges.text};
+ if (e.type == "cut") {
+ cm.operation(function() {
+ cm.setSelections(ranges.ranges, 0, sel_dontScroll);
+ cm.replaceSelection("", null, "cut");
+ });
+ }
+ }
+ // iOS exposes the clipboard API, but seems to discard content inserted into it
+ if (e.clipboardData && !ios) {
+ e.preventDefault();
+ e.clipboardData.clearData();
+ e.clipboardData.setData("text/plain", lastCopied.text.join("\n"));
+ } else {
+ // Old-fashioned briefly-focus-a-textarea hack
+ var kludge = hiddenTextarea(), te = kludge.firstChild;
+ cm.display.lineSpace.insertBefore(kludge, cm.display.lineSpace.firstChild);
+ te.value = lastCopied.text.join("\n");
+ var hadFocus = document.activeElement;
+ selectInput(te);
+ setTimeout(function() {
+ cm.display.lineSpace.removeChild(kludge);
+ hadFocus.focus();
+ }, 50);
+ }
+ }
+ on(div, "copy", onCopyCut);
+ on(div, "cut", onCopyCut);
+ },
+
+ prepareSelection: function() {
+ var result = prepareSelection(this.cm, false);
+ result.focus = this.cm.state.focused;
+ return result;
+ },
+
+ showSelection: function(info, takeFocus) {
+ if (!info || !this.cm.display.view.length) return;
+ if (info.focus || takeFocus) this.showPrimarySelection();
+ this.showMultipleSelections(info);
+ },
+
+ showPrimarySelection: function() {
+ var sel = window.getSelection(), prim = this.cm.doc.sel.primary();
+ var curAnchor = domToPos(this.cm, sel.anchorNode, sel.anchorOffset);
+ var curFocus = domToPos(this.cm, sel.focusNode, sel.focusOffset);
+ if (curAnchor && !curAnchor.bad && curFocus && !curFocus.bad &&
+ cmp(minPos(curAnchor, curFocus), prim.from()) == 0 &&
+ cmp(maxPos(curAnchor, curFocus), prim.to()) == 0)
+ return;
+
+ var start = posToDOM(this.cm, prim.from());
+ var end = posToDOM(this.cm, prim.to());
+ if (!start && !end) return;
+
+ var view = this.cm.display.view;
+ var old = sel.rangeCount && sel.getRangeAt(0);
+ if (!start) {
+ start = {node: view[0].measure.map[2], offset: 0};
+ } else if (!end) { // FIXME dangerously hacky
+ var measure = view[view.length - 1].measure;
+ var map = measure.maps ? measure.maps[measure.maps.length - 1] : measure.map;
+ end = {node: map[map.length - 1], offset: map[map.length - 2] - map[map.length - 3]};
+ }
+
+ try { var rng = range(start.node, start.offset, end.offset, end.node); }
+ catch(e) {} // Our model of the DOM might be outdated, in which case the range we try to set can be impossible
+ if (rng) {
+ if (!gecko && this.cm.state.focused) {
+ sel.collapse(start.node, start.offset);
+ if (!rng.collapsed) sel.addRange(rng);
+ } else {
+ sel.removeAllRanges();
+ sel.addRange(rng);
+ }
+ if (old && sel.anchorNode == null) sel.addRange(old);
+ else if (gecko) this.startGracePeriod();
+ }
+ this.rememberSelection();
+ },
+
+ startGracePeriod: function() {
+ var input = this;
+ clearTimeout(this.gracePeriod);
+ this.gracePeriod = setTimeout(function() {
+ input.gracePeriod = false;
+ if (input.selectionChanged())
+ input.cm.operation(function() { input.cm.curOp.selectionChanged = true; });
+ }, 20);
+ },
+
+ showMultipleSelections: function(info) {
+ removeChildrenAndAdd(this.cm.display.cursorDiv, info.cursors);
+ removeChildrenAndAdd(this.cm.display.selectionDiv, info.selection);
+ },
+
+ rememberSelection: function() {
+ var sel = window.getSelection();
+ this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset;
+ this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset;
+ },
+
+ selectionInEditor: function() {
+ var sel = window.getSelection();
+ if (!sel.rangeCount) return false;
+ var node = sel.getRangeAt(0).commonAncestorContainer;
+ return contains(this.div, node);
+ },
+
+ focus: function() {
+ if (this.cm.options.readOnly != "nocursor") this.div.focus();
+ },
+ blur: function() { this.div.blur(); },
+ getField: function() { return this.div; },
+
+ supportsTouch: function() { return true; },
+
+ receivedFocus: function() {
+ var input = this;
+ if (this.selectionInEditor())
+ this.pollSelection();
+ else
+ runInOp(this.cm, function() { input.cm.curOp.selectionChanged = true; });
+
+ function poll() {
+ if (input.cm.state.focused) {
+ input.pollSelection();
+ input.polling.set(input.cm.options.pollInterval, poll);
+ }
+ }
+ this.polling.set(this.cm.options.pollInterval, poll);
+ },
+
+ selectionChanged: function() {
+ var sel = window.getSelection();
+ return sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset ||
+ sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset;
+ },
+
+ pollSelection: function() {
+ if (!this.composing && !this.gracePeriod && this.selectionChanged()) {
+ var sel = window.getSelection(), cm = this.cm;
+ this.rememberSelection();
+ var anchor = domToPos(cm, sel.anchorNode, sel.anchorOffset);
+ var head = domToPos(cm, sel.focusNode, sel.focusOffset);
+ if (anchor && head) runInOp(cm, function() {
+ setSelection(cm.doc, simpleSelection(anchor, head), sel_dontScroll);
+ if (anchor.bad || head.bad) cm.curOp.selectionChanged = true;
+ });
+ }
+ },
+
+ pollContent: function() {
+ var cm = this.cm, display = cm.display, sel = cm.doc.sel.primary();
+ var from = sel.from(), to = sel.to();
+ if (from.line < display.viewFrom || to.line > display.viewTo - 1) return false;
+
+ var fromIndex;
+ if (from.line == display.viewFrom || (fromIndex = findViewIndex(cm, from.line)) == 0) {
+ var fromLine = lineNo(display.view[0].line);
+ var fromNode = display.view[0].node;
+ } else {
+ var fromLine = lineNo(display.view[fromIndex].line);
+ var fromNode = display.view[fromIndex - 1].node.nextSibling;
+ }
+ var toIndex = findViewIndex(cm, to.line);
+ if (toIndex == display.view.length - 1) {
+ var toLine = display.viewTo - 1;
+ var toNode = display.lineDiv.lastChild;
+ } else {
+ var toLine = lineNo(display.view[toIndex + 1].line) - 1;
+ var toNode = display.view[toIndex + 1].node.previousSibling;
+ }
+
+ var newText = cm.doc.splitLines(domTextBetween(cm, fromNode, toNode, fromLine, toLine));
+ var oldText = getBetween(cm.doc, Pos(fromLine, 0), Pos(toLine, getLine(cm.doc, toLine).text.length));
+ while (newText.length > 1 && oldText.length > 1) {
+ if (lst(newText) == lst(oldText)) { newText.pop(); oldText.pop(); toLine--; }
+ else if (newText[0] == oldText[0]) { newText.shift(); oldText.shift(); fromLine++; }
+ else break;
+ }
+
+ var cutFront = 0, cutEnd = 0;
+ var newTop = newText[0], oldTop = oldText[0], maxCutFront = Math.min(newTop.length, oldTop.length);
+ while (cutFront < maxCutFront && newTop.charCodeAt(cutFront) == oldTop.charCodeAt(cutFront))
+ ++cutFront;
+ var newBot = lst(newText), oldBot = lst(oldText);
+ var maxCutEnd = Math.min(newBot.length - (newText.length == 1 ? cutFront : 0),
+ oldBot.length - (oldText.length == 1 ? cutFront : 0));
+ while (cutEnd < maxCutEnd &&
+ newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1))
+ ++cutEnd;
+
+ newText[newText.length - 1] = newBot.slice(0, newBot.length - cutEnd);
+ newText[0] = newText[0].slice(cutFront);
+
+ var chFrom = Pos(fromLine, cutFront);
+ var chTo = Pos(toLine, oldText.length ? lst(oldText).length - cutEnd : 0);
+ if (newText.length > 1 || newText[0] || cmp(chFrom, chTo)) {
+ replaceRange(cm.doc, newText, chFrom, chTo, "+input");
+ return true;
+ }
+ },
+
+ ensurePolled: function() {
+ this.forceCompositionEnd();
+ },
+ reset: function() {
+ this.forceCompositionEnd();
+ },
+ forceCompositionEnd: function() {
+ if (!this.composing || this.composing.handled) return;
+ this.applyComposition(this.composing);
+ this.composing.handled = true;
+ this.div.blur();
+ this.div.focus();
+ },
+ applyComposition: function(composing) {
+ if (this.cm.isReadOnly())
+ operation(this.cm, regChange)(this.cm)
+ else if (composing.data && composing.data != composing.startData)
+ operation(this.cm, applyTextInput)(this.cm, composing.data, 0, composing.sel);
+ },
+
+ setUneditable: function(node) {
+ node.contentEditable = "false"
+ },
+
+ onKeyPress: function(e) {
+ e.preventDefault();
+ if (!this.cm.isReadOnly())
+ operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0);
+ },
+
+ readOnlyChanged: function(val) {
+ this.div.contentEditable = String(val != "nocursor")
+ },
+
+ onContextMenu: nothing,
+ resetPosition: nothing,
+
+ needsContentAttribute: true
+ }, ContentEditableInput.prototype);
+
+ function posToDOM(cm, pos) {
+ var view = findViewForLine(cm, pos.line);
+ if (!view || view.hidden) return null;
+ var line = getLine(cm.doc, pos.line);
+ var info = mapFromLineView(view, line, pos.line);
+
+ var order = getOrder(line), side = "left";
+ if (order) {
+ var partPos = getBidiPartAt(order, pos.ch);
+ side = partPos % 2 ? "right" : "left";
+ }
+ var result = nodeAndOffsetInLineMap(info.map, pos.ch, side);
+ result.offset = result.collapse == "right" ? result.end : result.start;
+ return result;
+ }
+
+ function badPos(pos, bad) { if (bad) pos.bad = true; return pos; }
+
+ function domToPos(cm, node, offset) {
+ var lineNode;
+ if (node == cm.display.lineDiv) {
+ lineNode = cm.display.lineDiv.childNodes[offset];
+ if (!lineNode) return badPos(cm.clipPos(Pos(cm.display.viewTo - 1)), true);
+ node = null; offset = 0;
+ } else {
+ for (lineNode = node;; lineNode = lineNode.parentNode) {
+ if (!lineNode || lineNode == cm.display.lineDiv) return null;
+ if (lineNode.parentNode && lineNode.parentNode == cm.display.lineDiv) break;
+ }
+ }
+ for (var i = 0; i < cm.display.view.length; i++) {
+ var lineView = cm.display.view[i];
+ if (lineView.node == lineNode)
+ return locateNodeInLineView(lineView, node, offset);
+ }
+ }
+
+ function locateNodeInLineView(lineView, node, offset) {
+ var wrapper = lineView.text.firstChild, bad = false;
+ if (!node || !contains(wrapper, node)) return badPos(Pos(lineNo(lineView.line), 0), true);
+ if (node == wrapper) {
+ bad = true;
+ node = wrapper.childNodes[offset];
+ offset = 0;
+ if (!node) {
+ var line = lineView.rest ? lst(lineView.rest) : lineView.line;
+ return badPos(Pos(lineNo(line), line.text.length), bad);
+ }
+ }
+
+ var textNode = node.nodeType == 3 ? node : null, topNode = node;
+ if (!textNode && node.childNodes.length == 1 && node.firstChild.nodeType == 3) {
+ textNode = node.firstChild;
+ if (offset) offset = textNode.nodeValue.length;
+ }
+ while (topNode.parentNode != wrapper) topNode = topNode.parentNode;
+ var measure = lineView.measure, maps = measure.maps;
+
+ function find(textNode, topNode, offset) {
+ for (var i = -1; i < (maps ? maps.length : 0); i++) {
+ var map = i < 0 ? measure.map : maps[i];
+ for (var j = 0; j < map.length; j += 3) {
+ var curNode = map[j + 2];
+ if (curNode == textNode || curNode == topNode) {
+ var line = lineNo(i < 0 ? lineView.line : lineView.rest[i]);
+ var ch = map[j] + offset;
+ if (offset < 0 || curNode != textNode) ch = map[j + (offset ? 1 : 0)];
+ return Pos(line, ch);
+ }
+ }
+ }
+ }
+ var found = find(textNode, topNode, offset);
+ if (found) return badPos(found, bad);
+
+ // FIXME this is all really shaky. might handle the few cases it needs to handle, but likely to cause problems
+ for (var after = topNode.nextSibling, dist = textNode ? textNode.nodeValue.length - offset : 0; after; after = after.nextSibling) {
+ found = find(after, after.firstChild, 0);
+ if (found)
+ return badPos(Pos(found.line, found.ch - dist), bad);
+ else
+ dist += after.textContent.length;
+ }
+ for (var before = topNode.previousSibling, dist = offset; before; before = before.previousSibling) {
+ found = find(before, before.firstChild, -1);
+ if (found)
+ return badPos(Pos(found.line, found.ch + dist), bad);
+ else
+ dist += after.textContent.length;
+ }
+ }
+
+ function domTextBetween(cm, from, to, fromLine, toLine) {
+ var text = "", closing = false, lineSep = cm.doc.lineSeparator();
+ function recognizeMarker(id) { return function(marker) { return marker.id == id; }; }
+ function walk(node) {
+ if (node.nodeType == 1) {
+ var cmText = node.getAttribute("cm-text");
+ if (cmText != null) {
+ if (cmText == "") cmText = node.textContent.replace(/\u200b/g, "");
+ text += cmText;
+ return;
+ }
+ var markerID = node.getAttribute("cm-marker"), range;
+ if (markerID) {
+ var found = cm.findMarks(Pos(fromLine, 0), Pos(toLine + 1, 0), recognizeMarker(+markerID));
+ if (found.length && (range = found[0].find()))
+ text += getBetween(cm.doc, range.from, range.to).join(lineSep);
+ return;
+ }
+ if (node.getAttribute("contenteditable") == "false") return;
+ for (var i = 0; i < node.childNodes.length; i++)
+ walk(node.childNodes[i]);
+ if (/^(pre|div|p)$/i.test(node.nodeName))
+ closing = true;
+ } else if (node.nodeType == 3) {
+ var val = node.nodeValue;
+ if (!val) return;
+ if (closing) {
+ text += lineSep;
+ closing = false;
+ }
+ text += val;
+ }
+ }
+ for (;;) {
+ walk(from);
+ if (from == to) break;
+ from = from.nextSibling;
+ }
+ return text;
+ }
+
+ CodeMirror.inputStyles = {"textarea": TextareaInput, "contenteditable": ContentEditableInput};
+
+ // SELECTION / CURSOR
+
+ // 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;
+ }
+
+ 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);
+ }
+ }
+
+ // 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);
+ }
+
+ // 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, options) {
+ 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));
+ },
+ origin: options && options.origin
+ };
+ 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, options);
+
+ var bias = options && options.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 = true;
+ signalCursorActivity(doc.cm);
+ }
+ 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 old = sel.ranges.length == doc.sel.ranges.length && doc.sel.ranges[i];
+ var newAnchor = skipAtomic(doc, range.anchor, old && old.anchor, bias, mayClear);
+ var newHead = skipAtomic(doc, range.head, old && old.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;
+ }
+
+ function skipAtomicInner(doc, pos, oldPos, dir, mayClear) {
+ var line = getLine(doc, pos.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 <= pos.ch : sp.from < pos.ch)) &&
+ (sp.to == null || (m.inclusiveRight ? sp.to >= pos.ch : sp.to > pos.ch))) {
+ if (mayClear) {
+ signal(m, "beforeCursorEnter");
+ if (m.explicitlyCleared) {
+ if (!line.markedSpans) break;
+ else {--i; continue;}
+ }
+ }
+ if (!m.atomic) continue;
+
+ if (oldPos) {
+ var near = m.find(dir < 0 ? 1 : -1), diff;
+ if (dir < 0 ? m.inclusiveRight : m.inclusiveLeft)
+ near = movePos(doc, near, -dir, near && near.line == pos.line ? line : null);
+ if (near && near.line == pos.line && (diff = cmp(near, oldPos)) && (dir < 0 ? diff < 0 : diff > 0))
+ return skipAtomicInner(doc, near, pos, dir, mayClear);
+ }
+
+ var far = m.find(dir < 0 ? -1 : 1);
+ if (dir < 0 ? m.inclusiveLeft : m.inclusiveRight)
+ far = movePos(doc, far, dir, far.line == pos.line ? line : null);
+ return far ? skipAtomicInner(doc, far, pos, dir, mayClear) : null;
+ }
+ }
+ return pos;
+ }
+
+ // Ensure a given position is not inside an atomic range.
+ function skipAtomic(doc, pos, oldPos, bias, mayClear) {
+ var dir = bias || 1;
+ var found = skipAtomicInner(doc, pos, oldPos, dir, mayClear) ||
+ (!mayClear && skipAtomicInner(doc, pos, oldPos, dir, true)) ||
+ skipAtomicInner(doc, pos, oldPos, -dir, mayClear) ||
+ (!mayClear && skipAtomicInner(doc, pos, oldPos, -dir, true));
+ if (!found) {
+ doc.cantEdit = true;
+ return Pos(doc.first, 0);
+ }
+ return found;
+ }
+
+ function movePos(doc, pos, dir, line) {
+ if (dir < 0 && pos.ch == 0) {
+ if (pos.line > doc.first) return clipPos(doc, Pos(pos.line - 1));
+ else return null;
+ } else if (dir > 0 && pos.ch == (line || getLine(doc, pos.line)).text.length) {
+ if (pos.line < doc.first + doc.size - 1) return Pos(pos.line + 1, 0);
+ else return null;
+ } else {
+ return new Pos(pos.line, pos.ch + dir);
+ }
+ }
+
+ // SELECTION DRAWING
+
+ function updateSelection(cm) {
+ cm.display.input.showSelection(cm.display.input.prepareSelection());
+ }
+
+ function prepareSelection(cm, primary) {
+ var doc = cm.doc, result = {};
+ var curFragment = result.cursors = document.createDocumentFragment();
+ var selFragment = result.selection = document.createDocumentFragment();
+
+ for (var i = 0; i < doc.sel.ranges.length; i++) {
+ if (primary === false && i == doc.sel.primIndex) continue;
+ var range = doc.sel.ranges[i];
+ if (range.from().line >= cm.display.viewTo || range.to().line < cm.display.viewFrom) continue;
+ var collapsed = range.empty();
+ if (collapsed || cm.options.showCursorWhenSelecting)
+ drawSelectionCursor(cm, range.head, curFragment);
+ if (!collapsed)
+ drawSelectionRange(cm, range, selFragment);
+ }
+ return result;
+ }
+
+ // Draws a cursor for the given range
+ function drawSelectionCursor(cm, head, output) {
+ var pos = cursorCoords(cm, head, "div", null, null, !cm.options.singleCursorHeightPerLine);
+
+ 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 drawSelectionRange(cm, range, output) {
+ var display = cm.display, doc = cm.doc;
+ var fragment = document.createDocumentFragment();
+ var padding = paddingH(cm.display), leftSide = padding.left;
+ var rightSide = Math.max(display.sizerWidth, displayWidth(cm) - display.sizer.offsetLeft) - padding.right;
+
+ function add(left, top, width, bottom) {
+ if (top < 0) top = 0;
+ top = Math.round(top);
+ bottom = Math.round(bottom);
+ fragment.appendChild(elt("div", null, "CodeMirror-selected", "position: absolute; left: " + left +
+ "px; top: " + top + "px; width: " + (width == null ? rightSide - left : width) +
+ "px; height: " + (bottom - top) + "px"));
+ }
+
+ function drawForLine(line, fromArg, toArg) {
+ var lineObj = getLine(doc, line);
+ var lineLen = lineObj.text.length;
+ var start, end;
+ function coords(ch, bias) {
+ return charCoords(cm, Pos(line, ch), "div", lineObj, bias);
+ }
+
+ iterateBidiSections(getOrder(lineObj), fromArg || 0, toArg == null ? lineLen : toArg, function(from, to, dir) {
+ var leftPos = coords(from, "left"), rightPos, left, right;
+ if (from == to) {
+ rightPos = leftPos;
+ left = right = leftPos.left;
+ } else {
+ rightPos = coords(to - 1, "right");
+ if (dir == "rtl") { var tmp = leftPos; leftPos = rightPos; rightPos = tmp; }
+ left = leftPos.left;
+ right = rightPos.right;
+ }
+ 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 = leftSide;
+ if (leftPos.bottom < rightPos.top) add(left, leftPos.bottom, null, rightPos.top);
+ }
+ 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 < leftSide + 1) left = leftSide;
+ add(left, rightPos.top, right - left, rightPos.bottom);
+ });
+ return {start: start, end: end};
+ }
+
+ var sFrom = range.from(), sTo = range.to();
+ if (sFrom.line == sTo.line) {
+ drawForLine(sFrom.line, sFrom.ch, sTo.ch);
+ } else {
+ 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(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(leftSide, leftEnd.bottom, null, rightStart.top);
+ }
+
+ output.appendChild(fragment);
+ }
+
+ // Cursor-blinking
+ function restartBlink(cm) {
+ if (!cm.state.focused) return;
+ var display = cm.display;
+ clearInterval(display.blinker);
+ var on = true;
+ display.cursorDiv.style.visibility = "";
+ if (cm.options.cursorBlinkRate > 0)
+ display.blinker = setInterval(function() {
+ display.cursorDiv.style.visibility = (on = !on) ? "" : "hidden";
+ }, cm.options.cursorBlinkRate);
+ else if (cm.options.cursorBlinkRate < 0)
+ display.cursorDiv.style.visibility = "hidden";
+ }
+
+ // HIGHLIGHT WORKER
+
+ function startWorker(cm, time) {
+ 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.viewTo) return;
+ var end = +new Date + cm.options.workTime;
+ var state = copyState(doc.mode, getStateBefore(cm, doc.frontier));
+ var changedLines = [];
+
+ 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, tooLong = line.text.length > cm.options.maxHighlightLength;
+ var highlighted = highlightLine(cm, line, tooLong ? copyState(doc.mode, state) : state, true);
+ line.styles = highlighted.styles;
+ var oldCls = line.styleClasses, newCls = highlighted.classes;
+ if (newCls) line.styleClasses = newCls;
+ else if (oldCls) line.styleClasses = null;
+ var ischange = !oldStyles || oldStyles.length != line.styles.length ||
+ oldCls != newCls && (!oldCls || !newCls || oldCls.bgClass != newCls.bgClass || oldCls.textClass != newCls.textClass);
+ for (var i = 0; !ischange && i < oldStyles.length; ++i) ischange = oldStyles[i] != line.styles[i];
+ if (ischange) changedLines.push(doc.frontier);
+ line.stateAfter = tooLong ? state : copyState(doc.mode, state);
+ } else {
+ if (line.text.length <= cm.options.maxHighlightLength)
+ processLine(cm, line.text, state);
+ line.stateAfter = doc.frontier % 5 == 0 ? copyState(doc.mode, state) : null;
+ }
+ ++doc.frontier;
+ if (+new Date > end) {
+ startWorker(cm, cm.options.workDelay);
+ return true;
+ }
+ });
+ if (changedLines.length) runInOp(cm, function() {
+ for (var i = 0; i < changedLines.length; i++)
+ regLineChange(cm, changedLines[i], "text");
+ });
+ }
+
+ // Finds the line to start with when starting a parse. Tries to
+ // find a line with a stateAfter, so that it can start with a
+ // valid state. If that fails, it returns the line with the
+ // smallest indentation, which tends to need the least context to
+ // parse correctly.
+ function findStartLine(cm, n, precise) {
+ var minindent, minline, doc = cm.doc;
+ var lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100);
+ for (var search = n; search > lim; --search) {
+ if (search <= doc.first) return doc.first;
+ var line = getLine(doc, search - 1);
+ if (line.stateAfter && (!precise || search <= doc.frontier)) return search;
+ var indented = countColumn(line.text, null, cm.options.tabSize);
+ if (minline == null || minindent > indented) {
+ minline = search - 1;
+ minindent = indented;
+ }
+ }
+ return minline;
+ }
+
+ function getStateBefore(cm, n, precise) {
+ var doc = cm.doc, display = cm.display;
+ if (!doc.mode.startState) return true;
+ var pos = findStartLine(cm, n, precise), state = pos > doc.first && getLine(doc, pos-1).stateAfter;
+ if (!state) state = startState(doc.mode);
+ 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.viewFrom && pos < display.viewTo;
+ line.stateAfter = save ? copyState(doc.mode, state) : null;
+ ++pos;
+ });
+ if (precise) doc.frontier = pos;
+ return state;
+ }
+
+ // POSITION MEASUREMENT
+
+ function paddingTop(display) {return display.lineSpace.offsetTop;}
+ function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight;}
+ 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;
+ var data = {left: parseInt(style.paddingLeft), right: parseInt(style.paddingRight)};
+ if (!isNaN(data.left) && !isNaN(data.right)) display.cachedPaddingH = data;
+ return data;
+ }
+
+ function scrollGap(cm) { return scrollerGap - cm.display.nativeBarWidth; }
+ function displayWidth(cm) {
+ return cm.display.scroller.clientWidth - scrollGap(cm) - cm.display.barWidth;
+ }
+ function displayHeight(cm) {
+ return cm.display.scroller.clientHeight - scrollGap(cm) - cm.display.barHeight;
+ }
+
+ // 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 && displayWidth(cm);
+ 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);
+ }
+ }
+ heights.push(rect.bottom - rect.top);
+ }
+ }
+
+ // 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));
+ cm.curOp.forceUpdate = true;
+ }
+ 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, varHeight) {
+ 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;
+ }
+ found = measureCharInner(cm, prepared, ch, bias);
+ if (!found.bogus) prepared.cache[key] = found;
+ }
+ return {left: found.left, right: found.right,
+ top: varHeight ? found.rtop : found.top,
+ bottom: varHeight ? found.rbottom : found.bottom};
+ }
+
+ var nullRect = {left: 0, right: 0, top: 0, bottom: 0};
+
+ function nodeAndOffsetInLineMap(map, ch, bias) {
+ 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;
+ }
+ }
+ return {node: node, start: start, end: end, collapse: collapse, coverStart: mStart, coverEnd: mEnd};
+ }
+
+ function measureCharInner(cm, prepared, ch, bias) {
+ var place = nodeAndOffsetInLineMap(prepared.map, ch, bias);
+ var node = place.node, start = place.start, end = place.end, collapse = place.collapse;
+
+ var rect;
+ if (node.nodeType == 3) { // If it is a text node, use a range to retrieve the coordinates.
+ for (var i = 0; i < 4; i++) { // Retry a maximum of 4 times when nonsense rectangles are returned
+ while (start && isExtendingChar(prepared.line.text.charAt(place.coverStart + start))) --start;
+ while (place.coverStart + end < place.coverEnd && isExtendingChar(prepared.line.text.charAt(place.coverStart + end))) ++end;
+ if (ie && ie_version < 9 && start == 0 && end == place.coverEnd - place.coverStart) {
+ 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() || nullRect;
+ }
+ if (rect.left || rect.right || start == 0) break;
+ end = start;
+ start = start - 1;
+ collapse = "right";
+ }
+ if (ie && ie_version < 11) rect = maybeUpdateRectForZooming(cm.display.measure, rect);
+ } 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 && ie_version < 9 && !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 rtop = rect.top - prepared.rect.top, rbot = rect.bottom - prepared.rect.top;
+ var mid = (rtop + rbot) / 2;
+ var heights = prepared.view.measure.heights;
+ for (var i = 0; i < heights.length - 1; i++)
+ if (mid < heights[i]) break;
+ var 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;
+ if (!cm.options.singleCursorHeightPerLine) { result.rtop = rtop; result.rbottom = rbot; }
+
+ return result;
+ }
+
+ // Work around problem with bounding client rects on ranges being
+ // returned incorrectly when zoomed on IE10 and below.
+ function maybeUpdateRectForZooming(measure, rect) {
+ if (!window.screen || screen.logicalXDPI == null ||
+ screen.logicalXDPI == screen.deviceXDPI || !hasBadZoomedRects(measure))
+ return rect;
+ var scaleX = screen.logicalXDPI / screen.deviceXDPI;
+ var scaleY = screen.logicalYDPI / screen.deviceYDPI;
+ return {left: rect.left * scaleX, right: rect.right * scaleX,
+ top: rect.top * scaleY, bottom: rect.bottom * scaleY};
+ }
+
+ 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] = {};
+ }
+ }
+
+ 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) {
+ clearLineMeasurementCache(cm);
+ cm.display.cachedCharWidth = cm.display.cachedTextHeight = cm.display.cachedPaddingH = null;
+ if (!cm.options.lineWrapping) cm.display.maxLineChanged = true;
+ cm.display.lineNumChars = null;
+ }
+
+ function pageScrollX() { return window.pageXOffset || (document.documentElement || document.body).scrollLeft; }
+ function pageScrollY() { return window.pageYOffset || (document.documentElement || document.body).scrollTop; }
+
+ // 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), "window",
+ // 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]);
+ rect.top += size; rect.bottom += size;
+ }
+ if (context == "line") return rect;
+ if (!context) context = "local";
+ var yOff = heightAtLine(lineObj);
+ if (context == "local") yOff += paddingTop(cm.display);
+ else yOff -= cm.display.viewOffset;
+ if (context == "page" || context == "window") {
+ 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;
+ }
+ rect.top += yOff; rect.bottom += yOff;
+ return rect;
+ }
+
+ // 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;
+ // First move into "page" coordinate system
+ if (context == "page") {
+ left -= pageScrollX();
+ top -= pageScrollY();
+ } else if (context == "local" || !context) {
+ var localBox = cm.display.sizer.getBoundingClientRect();
+ left += localBox.left;
+ top += localBox.top;
+ }
+
+ 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, bias), context);
+ }
+
+ // 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, varHeight) {
+ lineObj = lineObj || getLine(cm.doc, pos.line);
+ if (!preparedMeasure) preparedMeasure = prepareMeasureForLine(cm, lineObj);
+ function get(ch, right) {
+ var m = measureCharPrepared(cm, preparedMeasure, ch, right ? "right" : "left", varHeight);
+ if (right) m.left = m.right; else m.right = m.left;
+ return intoCoordSystem(cm, lineObj, m, context);
+ }
+ function getBidi(ch, partPos) {
+ var part = order[partPos], right = part.level % 2;
+ if (ch == bidiLeft(part) && partPos && part.level < order[partPos - 1].level) {
+ part = order[--partPos];
+ ch = bidiRight(part) - (part.level % 2 ? 0 : 1);
+ right = true;
+ } else if (ch == bidiRight(part) && partPos < order.length - 1 && part.level < order[partPos + 1].level) {
+ part = order[++partPos];
+ ch = bidiLeft(part) - part.level % 2;
+ right = false;
+ }
+ if (right && ch == part.to && ch > part.from) return get(ch - 1);
+ return get(ch, right);
+ }
+ var order = getOrder(lineObj), ch = pos.ch;
+ if (!order) return get(ch);
+ var partPos = getBidiPartAt(order, ch);
+ var val = getBidi(ch, partPos);
+ if (bidiOther != null) val.other = getBidi(ch, bidiOther);
+ 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 = Pos(line, ch);
+ pos.xRel = xRel;
+ if (outside) pos.outside = true;
+ return pos;
+ }
+
+ // 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 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 found = coordsCharInner(cm, lineObj, lineN, x, y);
+ var merged = collapsedSpanAtEnd(lineObj);
+ var mergedPos = merged && merged.find(0, true);
+ if (merged && (found.ch > mergedPos.from.ch || found.ch == mergedPos.from.ch && found.xRel > 0))
+ lineN = lineNo(lineObj = mergedPos.to.line);
+ else
+ return found;
+ }
+ }
+
+ function coordsCharInner(cm, lineObj, lineNo, x, y) {
+ var innerOff = y - heightAtLine(lineObj);
+ var wrongLine = false, adjust = 2 * cm.display.wrapper.clientWidth;
+ var preparedMeasure = prepareMeasureForLine(cm, lineObj);
+
+ function getX(ch) {
+ 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;
+ else wrongLine = false;
+ return sp.left;
+ }
+
+ var bidi = getOrder(lineObj), dist = lineObj.text.length;
+ var from = lineLeft(lineObj), to = lineRight(lineObj);
+ var fromX = getX(from), fromOutside = wrongLine, toX = getX(to), toOutside = wrongLine;
+
+ if (x > toX) return PosWithInfo(lineNo, to, toOutside, 1);
+ // Do a binary search between these bounds.
+ for (;;) {
+ if (bidi ? to == from || to == moveVisually(lineObj, from, 1) : to - from <= 1) {
+ var ch = x < fromX || x - fromX <= toX - x ? from : to;
+ var outside = ch == from ? fromOutside : toOutside
+ var xDiff = x - (ch == from ? fromX : toX);
+ // This is a kludge to handle the case where the coordinates
+ // are after a line-wrapped line. We should replace it with a
+ // more general handling of cursor positions around line
+ // breaks. (Issue #4078)
+ if (toOutside && !bidi && !/\s/.test(lineObj.text.charAt(ch)) && xDiff > 0 &&
+ ch < lineObj.text.length && preparedMeasure.view.measure.heights.length > 1) {
+ var charSize = measureCharPrepared(cm, preparedMeasure, ch, "right");
+ if (innerOff <= charSize.bottom && innerOff >= charSize.top && Math.abs(x - charSize.right) < xDiff) {
+ outside = false
+ ch++
+ xDiff = x - charSize.right
+ }
+ }
+ while (isExtendingChar(lineObj.text.charAt(ch))) ++ch;
+ var pos = PosWithInfo(lineNo, ch, outside, xDiff < -1 ? -1 : xDiff > 1 ? 1 : 0);
+ return pos;
+ }
+ var step = Math.ceil(dist / 2), middle = from + step;
+ if (bidi) {
+ middle = from;
+ for (var i = 0; i < step; ++i) middle = moveVisually(lineObj, middle, 1);
+ }
+ var middleX = getX(middle);
+ if (middleX > x) {to = middle; toX = middleX; if (toOutside = wrongLine) toX += 1000; dist = step;}
+ else {from = middle; fromX = middleX; fromOutside = wrongLine; dist -= step;}
+ }
+ }
+
+ var measureText;
+ // Compute the default text height.
+ function textHeight(display) {
+ if (display.cachedTextHeight != null) return display.cachedTextHeight;
+ if (measureText == null) {
+ measureText = elt("pre");
+ // Measure a bunch of lines, for browsers that compute
+ // fractional heights.
+ for (var i = 0; i < 49; ++i) {
+ measureText.appendChild(document.createTextNode("x"));
+ measureText.appendChild(elt("br"));
+ }
+ measureText.appendChild(document.createTextNode("x"));
+ }
+ removeChildrenAndAdd(display.measure, measureText);
+ var height = measureText.offsetHeight / 50;
+ if (height > 3) display.cachedTextHeight = height;
+ removeChildren(display.measure);
+ return height || 1;
+ }
+
+ // Compute the default character width.
+ function charWidth(display) {
+ if (display.cachedCharWidth != null) return display.cachedCharWidth;
+ var anchor = elt("span", "xxxxxxxxxx");
+ var pre = elt("pre", [anchor]);
+ removeChildrenAndAdd(display.measure, pre);
+ 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 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 operationGroup = null;
+
+ var nextOpId = 0;
+ // Start a new operation.
+ function startOperation(cm) {
+ cm.curOp = {
+ cm: cm,
+ 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
+ cursorActivityHandlers: null, // Set of handlers to fire cursorActivity on
+ cursorActivityCalled: 0, // Tracks which cursorActivity handlers have been called already
+ 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
+ focus: false,
+ id: ++nextOpId // Unique ID
+ };
+ if (operationGroup) {
+ operationGroup.ops.push(cm.curOp);
+ } else {
+ cm.curOp.ownsGroup = operationGroup = {
+ ops: [cm.curOp],
+ delayedCallbacks: []
+ };
+ }
+ }
+
+ function fireCallbacksForOps(group) {
+ // Calls delayed callbacks and cursorActivity handlers until no
+ // new ones appear
+ var callbacks = group.delayedCallbacks, i = 0;
+ do {
+ for (; i < callbacks.length; i++)
+ callbacks[i].call(null);
+ for (var j = 0; j < group.ops.length; j++) {
+ var op = group.ops[j];
+ if (op.cursorActivityHandlers)
+ while (op.cursorActivityCalled < op.cursorActivityHandlers.length)
+ op.cursorActivityHandlers[op.cursorActivityCalled++].call(null, op.cm);
+ }
+ } while (i < callbacks.length);
+ }
+
+ // Finish an operation, updating the display and signalling delayed events
+ function endOperation(cm) {
+ var op = cm.curOp, group = op.ownsGroup;
+ if (!group) return;
+
+ try { fireCallbacksForOps(group); }
+ finally {
+ operationGroup = null;
+ for (var i = 0; i < group.ops.length; i++)
+ group.ops[i].cm.curOp = null;
+ endOperations(group);
+ }
+ }
+
+ // The DOM updates done when an operation finishes are batched so
+ // that the minimum number of relayouts are required.
+ function endOperations(group) {
+ var ops = group.ops;
+ for (var i = 0; i < ops.length; i++) // Read DOM
+ endOperation_R1(ops[i]);
+ for (var i = 0; i < ops.length; i++) // Write DOM (maybe)
+ endOperation_W1(ops[i]);
+ for (var i = 0; i < ops.length; i++) // Read DOM
+ endOperation_R2(ops[i]);
+ for (var i = 0; i < ops.length; i++) // Write DOM (maybe)
+ endOperation_W2(ops[i]);
+ for (var i = 0; i < ops.length; i++) // Read DOM
+ endOperation_finish(ops[i]);
+ }
+
+ function endOperation_R1(op) {
+ var cm = op.cm, display = cm.display;
+ maybeClipScrollbars(cm);
+ if (op.updateMaxLine) findMaxLine(cm);
+
+ op.mustUpdate = 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;
+ op.update = op.mustUpdate &&
+ new DisplayUpdate(cm, op.mustUpdate && {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate);
+ }
+
+ function endOperation_W1(op) {
+ op.updatedDisplay = op.mustUpdate && updateDisplayIfNeeded(op.cm, op.update);
+ }
+
+ function endOperation_R2(op) {
+ var cm = op.cm, display = cm.display;
+ if (op.updatedDisplay) updateHeightsInViewport(cm);
+
+ op.barMeasure = measureForScrollbars(cm);
+
+ // If the max line changed since it was last measured, measure it,
+ // and ensure the document's width matches it.
+ // updateDisplay_W2 will use these properties to do the actual resizing
+ if (display.maxLineChanged && !cm.options.lineWrapping) {
+ op.adjustWidthTo = measureChar(cm, display.maxLine, display.maxLine.text.length).left + 3;
+ cm.display.sizerWidth = op.adjustWidthTo;
+ op.barMeasure.scrollWidth =
+ Math.max(display.scroller.clientWidth, display.sizer.offsetLeft + op.adjustWidthTo + scrollGap(cm) + cm.display.barWidth);
+ op.maxScrollLeft = Math.max(0, display.sizer.offsetLeft + op.adjustWidthTo - displayWidth(cm));
+ }
+
+ if (op.updatedDisplay || op.selectionChanged)
+ op.preparedSelection = display.input.prepareSelection(op.focus);
+ }
+
+ function endOperation_W2(op) {
+ var cm = op.cm;
+
+ if (op.adjustWidthTo != null) {
+ cm.display.sizer.style.minWidth = op.adjustWidthTo + "px";
+ if (op.maxScrollLeft < cm.doc.scrollLeft)
+ setScrollLeft(cm, Math.min(cm.display.scroller.scrollLeft, op.maxScrollLeft), true);
+ cm.display.maxLineChanged = false;
+ }
+
+ var takeFocus = op.focus && op.focus == activeElt() && (!document.hasFocus || document.hasFocus())
+ if (op.preparedSelection)
+ cm.display.input.showSelection(op.preparedSelection, takeFocus);
+ if (op.updatedDisplay || op.startHeight != cm.doc.height)
+ updateScrollbars(cm, op.barMeasure);
+ if (op.updatedDisplay)
+ setDocumentHeight(cm, op.barMeasure);
+
+ if (op.selectionChanged) restartBlink(cm);
+
+ if (cm.state.focused && op.updateInput)
+ cm.display.input.reset(op.typing);
+ if (takeFocus) ensureFocus(op.cm);
+ }
+
+ function endOperation_finish(op) {
+ var cm = op.cm, display = cm.display, doc = cm.doc;
+
+ if (op.updatedDisplay) postUpdateDisplay(cm, op.update);
+
+ // Abort mouse wheel delta measurement, when scrolling explicitly
+ if (display.wheelStartX != null && (op.scrollTop != null || op.scrollLeft != null || op.scrollToPos))
+ display.wheelStartX = display.wheelStartY = null;
+
+ // Propagate the scroll position to the actual DOM scroller
+ if (op.scrollTop != null && (display.scroller.scrollTop != op.scrollTop || op.forceScroll)) {
+ doc.scrollTop = Math.max(0, Math.min(display.scroller.scrollHeight - display.scroller.clientHeight, op.scrollTop));
+ display.scrollbars.setScrollTop(doc.scrollTop);
+ display.scroller.scrollTop = doc.scrollTop;
+ }
+ if (op.scrollLeft != null && (display.scroller.scrollLeft != op.scrollLeft || op.forceScroll)) {
+ doc.scrollLeft = Math.max(0, Math.min(display.scroller.scrollWidth - display.scroller.clientWidth, op.scrollLeft));
+ display.scrollbars.setScrollLeft(doc.scrollLeft);
+ display.scroller.scrollLeft = doc.scrollLeft;
+ alignHorizontally(cm);
+ }
+ // If we need to scroll a specific position into view, do so.
+ if (op.scrollToPos) {
+ var coords = scrollPosIntoView(cm, clipPos(doc, op.scrollToPos.from),
+ clipPos(doc, op.scrollToPos.to), op.scrollToPos.margin);
+ if (op.scrollToPos.isCursor && cm.state.focused) maybeScrollWindow(cm, coords);
+ }
+
+ // 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");
+ if (unhidden) for (var i = 0; i < unhidden.length; ++i)
+ if (unhidden[i].lines.length) signal(unhidden[i], "unhide");
+
+ if (display.wrapper.offsetHeight)
+ doc.scrollTop = cm.display.scroller.scrollTop;
+
+ // Fire change events, and delayed event handlers
+ if (op.changeObjs)
+ signal(cm, "changes", cm, op.changeObjs);
+ if (op.update)
+ op.update.finish();
+ }
+
+ // Run the given function in an operation
+ function runInOp(cm, f) {
+ 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;
+ 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 || newN == cm.doc.first + cm.doc.size)
+ 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;
+ }
+
+ // 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));
+ // Older IE's will not fire a second mousedown for a double click
+ if (ie && ie_version < 11)
+ 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 = cm.findWordAt(pos);
+ extendSelection(cm.doc, word.anchor, word.head);
+ }));
+ else
+ on(d.scroller, "dblclick", function(e) { signalDOMEvent(cm, e) || e_preventDefault(e); });
+ // 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 these browsers.
+ if (!captureRightClick) on(d.scroller, "contextmenu", function(e) {onContextMenu(cm, e);});
+
+ // Used to suppress mouse event handling when a touch happens
+ var touchFinished, prevTouch = {end: 0};
+ function finishTouch() {
+ if (d.activeTouch) {
+ touchFinished = setTimeout(function() {d.activeTouch = null;}, 1000);
+ prevTouch = d.activeTouch;
+ prevTouch.end = +new Date;
+ }
+ };
+ function isMouseLikeTouchEvent(e) {
+ if (e.touches.length != 1) return false;
+ var touch = e.touches[0];
+ return touch.radiusX <= 1 && touch.radiusY <= 1;
+ }
+ function farAway(touch, other) {
+ if (other.left == null) return true;
+ var dx = other.left - touch.left, dy = other.top - touch.top;
+ return dx * dx + dy * dy > 20 * 20;
+ }
+ on(d.scroller, "touchstart", function(e) {
+ if (!signalDOMEvent(cm, e) && !isMouseLikeTouchEvent(e)) {
+ clearTimeout(touchFinished);
+ var now = +new Date;
+ d.activeTouch = {start: now, moved: false,
+ prev: now - prevTouch.end <= 300 ? prevTouch : null};
+ if (e.touches.length == 1) {
+ d.activeTouch.left = e.touches[0].pageX;
+ d.activeTouch.top = e.touches[0].pageY;
+ }
+ }
+ });
+ on(d.scroller, "touchmove", function() {
+ if (d.activeTouch) d.activeTouch.moved = true;
+ });
+ on(d.scroller, "touchend", function(e) {
+ var touch = d.activeTouch;
+ if (touch && !eventInWidget(d, e) && touch.left != null &&
+ !touch.moved && new Date - touch.start < 300) {
+ var pos = cm.coordsChar(d.activeTouch, "page"), range;
+ if (!touch.prev || farAway(touch, touch.prev)) // Single tap
+ range = new Range(pos, pos);
+ else if (!touch.prev.prev || farAway(touch, touch.prev.prev)) // Double tap
+ range = cm.findWordAt(pos);
+ else // Triple tap
+ range = new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0)));
+ cm.setSelection(range.anchor, range.head);
+ cm.focus();
+ e_preventDefault(e);
+ }
+ finishTouch();
+ });
+ on(d.scroller, "touchcancel", finishTouch);
+
+ // 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);
+ setScrollLeft(cm, d.scroller.scrollLeft, true);
+ signal(cm, "scroll", cm);
+ }
+ });
+
+ // 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 wrapper from ever scrolling
+ on(d.wrapper, "scroll", function() { d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; });
+
+ d.dragFunctions = {
+ enter: function(e) {if (!signalDOMEvent(cm, e)) e_stop(e);},
+ over: function(e) {if (!signalDOMEvent(cm, e)) { onDragOver(cm, e); e_stop(e); }},
+ start: function(e){onDragStart(cm, e);},
+ drop: operation(cm, onDrop),
+ leave: function(e) {if (!signalDOMEvent(cm, e)) { clearDragCursor(cm); }}
+ };
+
+ var inp = d.input.getField();
+ on(inp, "keyup", function(e) { onKeyUp.call(cm, e); });
+ on(inp, "keydown", operation(cm, onKeyDown));
+ on(inp, "keypress", operation(cm, onKeyPress));
+ on(inp, "focus", bind(onFocus, cm));
+ on(inp, "blur", bind(onBlur, cm));
+ }
+
+ function dragDropChanged(cm, value, old) {
+ var wasOn = old && old != CodeMirror.Init;
+ if (!value != !wasOn) {
+ var funcs = cm.display.dragFunctions;
+ var toggle = value ? on : off;
+ toggle(cm.display.scroller, "dragstart", funcs.start);
+ toggle(cm.display.scroller, "dragenter", funcs.enter);
+ toggle(cm.display.scroller, "dragover", funcs.over);
+ toggle(cm.display.scroller, "dragleave", funcs.leave);
+ toggle(cm.display.scroller, "drop", funcs.drop);
+ }
+ }
+
+ // Called when the window resizes
+ function onResize(cm) {
+ var d = cm.display;
+ if (d.lastWrapHeight == d.wrapper.clientHeight && d.lastWrapWidth == d.wrapper.clientWidth)
+ return;
+ // Might be a text scaling operation, clear size caches.
+ d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null;
+ d.scrollbarsClipped = false;
+ cm.setSize();
+ }
+
+ // 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.nodeType == 1 && n.getAttribute("cm-ignore-events") == "true") ||
+ (n.parentNode == display.sizer && n != display.mover))
+ return true;
+ }
+ }
+
+ // 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 && e_target(e).getAttribute("cm-not-content") == "true") return null;
+
+ var x, y, space = display.lineSpace.getBoundingClientRect();
+ // Fails unpredictably on IE[67] when mouse is dragged around quickly.
+ 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.max(0, Math.round((x - paddingH(cm.display).left) / charWidth(cm.display)) - colDiff));
+ }
+ return coords;
+ }
+
+ // 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) {
+ var cm = this, display = cm.display;
+ if (signalDOMEvent(cm, e) || display.activeTouch && display.input.supportsTouch()) return;
+ 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);
+ }
+ return;
+ }
+ if (clickInGutter(cm, e)) return;
+ var start = posFromMouse(cm, e);
+ window.focus();
+
+ switch (e_button(e)) {
+ case 1:
+ // #3261: make sure, that we're not starting a second selection
+ if (cm.state.selectingText)
+ cm.state.selectingText(e);
+ else 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(function() {display.input.focus();}, 20);
+ e_preventDefault(e);
+ break;
+ case 3:
+ if (captureRightClick) onContextMenu(cm, e);
+ else delayBlurEvent(cm);
+ break;
+ }
+ }
+
+ var lastClick, lastDoubleClick;
+ function leftButtonDown(cm, e, start) {
+ if (ie) setTimeout(bind(ensureFocus, cm), 0);
+ else cm.curOp.focus = activeElt();
+
+ var now = +new Date, type;
+ if (lastDoubleClick && lastDoubleClick.time > now - 400 && cmp(lastDoubleClick.pos, start) == 0) {
+ type = "triple";
+ } else if (lastClick && lastClick.time > now - 400 && cmp(lastClick.pos, start) == 0) {
+ type = "double";
+ lastDoubleClick = {time: now, pos: start};
+ } else {
+ type = "single";
+ lastClick = {time: now, pos: start};
+ }
+
+ var sel = cm.doc.sel, modifier = mac ? e.metaKey : e.ctrlKey, contained;
+ if (cm.options.dragDrop && dragAndDrop && !cm.isReadOnly() &&
+ type == "single" && (contained = sel.contains(start)) > -1 &&
+ (cmp((contained = sel.ranges[contained]).from(), start) < 0 || start.xRel > 0) &&
+ (cmp(contained.to(), start) > 0 || start.xRel < 0))
+ leftButtonStartDrag(cm, e, start, modifier);
+ else
+ leftButtonSelect(cm, e, start, type, modifier);
+ }
+
+ // 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, modifier) {
+ var display = cm.display, startTime = +new Date;
+ 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);
+ if (!modifier && +new Date - 200 < startTime)
+ extendSelection(cm.doc, start);
+ // Work around unexplainable focus problem in IE9 (#2127) and Chrome (#3081)
+ if (webkit || ie && ie_version == 9)
+ setTimeout(function() {document.body.focus(); display.input.focus();}, 20);
+ else
+ display.input.focus();
+ }
+ });
+ // Let the drag handler handle this.
+ if (webkit) display.scroller.draggable = true;
+ cm.state.draggingText = dragEnd;
+ dragEnd.copy = mac ? e.altKey : e.ctrlKey
+ // 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);
+
+ var ourRange, ourIndex, startSel = doc.sel, ranges = startSel.ranges;
+ if (addNew && !e.shiftKey) {
+ ourIndex = doc.sel.contains(start);
+ if (ourIndex > -1)
+ ourRange = ranges[ourIndex];
+ else
+ ourRange = new Range(start, start);
+ } else {
+ ourRange = doc.sel.primary();
+ ourIndex = doc.sel.primIndex;
+ }
+
+ if (chromeOS ? e.shiftKey && e.metaKey : 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 = cm.findWordAt(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 (!addNew) {
+ ourIndex = 0;
+ setSelection(doc, new Selection([ourRange], 0), sel_mouse);
+ startSel = doc.sel;
+ } else if (ourIndex == -1) {
+ ourIndex = ranges.length;
+ setSelection(doc, normalizeSelection(ranges.concat([ourRange]), ourIndex),
+ {scroll: false, origin: "*mouse"});
+ } else if (ranges.length > 1 && ranges[ourIndex].empty() && type == "single" && !e.shiftKey) {
+ setSelection(doc, normalizeSelection(ranges.slice(0, ourIndex).concat(ranges.slice(ourIndex + 1)), 0),
+ {scroll: false, origin: "*mouse"});
+ startSel = doc.sel;
+ } else {
+ replaceOneSelection(doc, ourIndex, ourRange, sel_mouse);
+ }
+
+ 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),
+ {origin: "*mouse", scroll: false});
+ cm.scrollIntoView(pos);
+ } else {
+ var oldRange = ourRange;
+ var anchor = oldRange.anchor, head = pos;
+ if (type != "single") {
+ if (type == "double")
+ var range = cm.findWordAt(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 = 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,
+ // if the clear happens after their scheduled firing time).
+ var counter = 0;
+
+ function extend(e) {
+ var curCount = ++counter;
+ var cur = posFromMouse(cm, e, true, type == "rect");
+ if (!cur) return;
+ if (cmp(cur, lastPos) != 0) {
+ cm.curOp.focus = activeElt();
+ 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);
+ } else {
+ var outside = e.clientY < editorSize.top ? -20 : e.clientY > editorSize.bottom ? 20 : 0;
+ if (outside) setTimeout(operation(cm, function() {
+ if (counter != curCount) return;
+ display.scroller.scrollTop += outside;
+ extend(e);
+ }), 50);
+ }
+ }
+
+ function done(e) {
+ cm.state.selectingText = false;
+ counter = Infinity;
+ e_preventDefault(e);
+ display.input.focus();
+ off(document, "mousemove", move);
+ off(document, "mouseup", up);
+ doc.history.lastSelOrigin = null;
+ }
+
+ var move = operation(cm, function(e) {
+ if (!e_button(e)) done(e);
+ else extend(e);
+ });
+ var up = operation(cm, done);
+ cm.state.selectingText = up;
+ on(document, "mousemove", move);
+ 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) {
+ try { var mX = e.clientX, mY = e.clientY; }
+ catch(e) { return false; }
+ if (mX >= Math.floor(cm.display.gutters.getBoundingClientRect().right)) return false;
+ if (prevent) e_preventDefault(e);
+
+ var display = cm.display;
+ 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 && g.getBoundingClientRect().right >= mX) {
+ var line = lineAtHeight(cm.doc, mY);
+ var gutter = cm.options.gutters[i];
+ signal(cm, type, cm, line, gutter, e);
+ return e_defaultPrevented(e);
+ }
+ }
+ }
+
+ function clickInGutter(cm, e) {
+ return gutterEvent(cm, e, "gutterClick", true);
+ }
+
+ // Kludge to work around strange IE behavior where it'll sometimes
+ // re-fire a series of drag-related events right after the drop (#1551)
+ var lastDrop = 0;
+
+ function onDrop(e) {
+ var cm = this;
+ clearDragCursor(cm);
+ if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e))
+ return;
+ e_preventDefault(e);
+ if (ie) lastDrop = +new Date;
+ var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files;
+ if (!pos || cm.isReadOnly()) 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) {
+ if (cm.options.allowDropFileTypes &&
+ indexOf(cm.options.allowDropFileTypes, file.type) == -1)
+ return;
+
+ var reader = new FileReader;
+ reader.onload = operation(cm, function() {
+ var content = reader.result;
+ if (/[\x00-\x08\x0e-\x1f]{2}/.test(content)) content = "";
+ text[i] = content;
+ if (++read == n) {
+ pos = clipPos(cm.doc, pos);
+ var change = {from: pos, to: pos,
+ text: cm.doc.splitLines(text.join(cm.doc.lineSeparator())),
+ 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 { // Normal drop
+ // Don't do a replace if the drop happened inside of the selected text.
+ if (cm.state.draggingText && cm.doc.sel.contains(pos) > -1) {
+ cm.state.draggingText(e);
+ // Ensure the editor is re-focused
+ setTimeout(function() {cm.display.input.focus();}, 20);
+ return;
+ }
+ try {
+ var text = e.dataTransfer.getData("Text");
+ if (text) {
+ if (cm.state.draggingText && !cm.state.draggingText.copy)
+ var selected = 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");
+ cm.display.input.focus();
+ }
+ }
+ catch(e){}
+ }
+ }
+
+ function onDragStart(cm, e) {
+ if (ie && (!cm.state.draggingText || +new Date - lastDrop < 100)) { e_stop(e); return; }
+ if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) return;
+
+ e.dataTransfer.setData("Text", cm.getSelection());
+ e.dataTransfer.effectAllowed = "copyMove"
+
+ // 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 (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 (presto) img.parentNode.removeChild(img);
+ }
+ }
+
+ function onDragOver(cm, e) {
+ var pos = posFromMouse(cm, e);
+ if (!pos) return;
+ var frag = document.createDocumentFragment();
+ drawSelectionCursor(cm, pos, frag);
+ if (!cm.display.dragCursor) {
+ cm.display.dragCursor = elt("div", null, "CodeMirror-cursors CodeMirror-dragcursors");
+ cm.display.lineSpace.insertBefore(cm.display.dragCursor, cm.display.cursorDiv);
+ }
+ removeChildrenAndAdd(cm.display.dragCursor, frag);
+ }
+
+ function clearDragCursor(cm) {
+ if (cm.display.dragCursor) {
+ cm.display.lineSpace.removeChild(cm.display.dragCursor);
+ cm.display.dragCursor = null;
+ }
+ }
+
+ // 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) updateDisplaySimple(cm, {top: val});
+ if (cm.display.scroller.scrollTop != val) cm.display.scroller.scrollTop = val;
+ cm.display.scrollbars.setScrollTop(val);
+ if (gecko) updateDisplaySimple(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);
+ cm.doc.scrollLeft = val;
+ alignHorizontally(cm);
+ if (cm.display.scroller.scrollLeft != val) cm.display.scroller.scrollLeft = val;
+ cm.display.scrollbars.setScrollLeft(val);
+ }
+
+ // Since the delta values reported on mouse wheel events are
+ // unstandardized between browsers and even browser versions, and
+ // generally horribly unpredictable, this code starts by measuring
+ // the scroll effect that the first few mouse wheel events have,
+ // and, from that, detects the way it can convert deltas to pixel
+ // offsets afterwards.
+ //
+ // The reason we want to know the amount a wheel event will scroll
+ // is that it gives us a chance to update the display before the
+ // actual scrolling happens, reducing flickering.
+
+ var wheelSamples = 0, wheelPixelsPerUnit = null;
+ // Fill in a browser-detected starting value on browsers where we
+ // 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 (ie) wheelPixelsPerUnit = -.53;
+ else if (gecko) wheelPixelsPerUnit = 15;
+ else if (chrome) wheelPixelsPerUnit = -.7;
+ else if (safari) wheelPixelsPerUnit = -1/3;
+
+ var wheelEventDelta = function(e) {
+ var dx = e.wheelDeltaX, dy = e.wheelDeltaY;
+ if (dx == null && e.detail && e.axis == e.HORIZONTAL_AXIS) dx = e.detail;
+ if (dy == null && e.detail && e.axis == e.VERTICAL_AXIS) dy = e.detail;
+ else if (dy == null) dy = e.wheelDelta;
+ return {x: dx, y: dy};
+ };
+ CodeMirror.wheelEventPixels = function(e) {
+ var delta = wheelEventDelta(e);
+ delta.x *= wheelPixelsPerUnit;
+ delta.y *= wheelPixelsPerUnit;
+ return delta;
+ };
+
+ function onScrollWheel(cm, e) {
+ var delta = wheelEventDelta(e), dx = delta.x, dy = delta.y;
+
+ var display = cm.display, scroll = display.scroller;
+ // Quit if there's nothing to scroll here
+ var canScrollX = scroll.scrollWidth > scroll.clientWidth;
+ var canScrollY = scroll.scrollHeight > scroll.clientHeight;
+ if (!(dx && canScrollX || dy && canScrollY)) return;
+
+ // Webkit browsers on OS X abort momentum scrolls when the target
+ // of the scroll event is removed from the scrollable element.
+ // This hack (see related code in patchDisplay) makes sure the
+ // element is kept around.
+ if (dy && mac && webkit) {
+ 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;
+ }
+ }
+ }
+ }
+
+ // On some browsers, horizontal scrolling will cause redraws to
+ // happen before the gutter has been realigned, causing it to
+ // wriggle around in a most unseemly way. When we have an
+ // 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 && !presto && wheelPixelsPerUnit != null) {
+ if (dy && canScrollY)
+ 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)));
+ // Only prevent default scrolling if vertical scrolling is
+ // actually possible. Otherwise, it causes vertical scroll
+ // jitter on OSX trackpads when deltaX is small and deltaY
+ // is large (issue #3579)
+ if (!dy || (dy && canScrollY))
+ e_preventDefault(e);
+ display.wheelStartX = null; // Abort measurement, if in progress
+ 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);
+ updateDisplaySimple(cm, {top: top, bottom: bot});
+ }
+
+ if (wheelSamples < 20) {
+ if (display.wheelStartX == null) {
+ display.wheelStartX = scroll.scrollLeft; display.wheelStartY = scroll.scrollTop;
+ display.wheelDX = dx; display.wheelDY = dy;
+ setTimeout(function() {
+ if (display.wheelStartX == null) return;
+ var movedX = scroll.scrollLeft - display.wheelStartX;
+ var movedY = scroll.scrollTop - display.wheelStartY;
+ var sample = (movedY && display.wheelDY && movedY / display.wheelDY) ||
+ (movedX && display.wheelDX && movedX / display.wheelDX);
+ display.wheelStartX = display.wheelStartY = null;
+ if (!sample) return;
+ wheelPixelsPerUnit = (wheelPixelsPerUnit * wheelSamples + sample) / (wheelSamples + 1);
+ ++wheelSamples;
+ }, 200);
+ } else {
+ display.wheelDX += dx; display.wheelDY += dy;
+ }
+ }
+ }
+
+ // KEY EVENTS
+
+ // Run a handler that was bound to a key.
+ function doHandleBinding(cm, bound, dropShift) {
+ if (typeof bound == "string") {
+ bound = commands[bound];
+ if (!bound) return false;
+ }
+ // Ensure previous input has been read, so that the handler sees a
+ // consistent view of the document
+ cm.display.input.ensurePolled();
+ var prevShift = cm.display.shift, done = false;
+ try {
+ if (cm.isReadOnly()) cm.state.suppressEdits = true;
+ if (dropShift) cm.display.shift = false;
+ done = bound(cm) != Pass;
+ } finally {
+ cm.display.shift = prevShift;
+ cm.state.suppressEdits = false;
+ }
+ return done;
+ }
+
+ function lookupKeyForEditor(cm, name, handle) {
+ for (var i = 0; i < cm.state.keyMaps.length; i++) {
+ var result = lookupKey(name, cm.state.keyMaps[i], handle, cm);
+ if (result) return result;
+ }
+ return (cm.options.extraKeys && lookupKey(name, cm.options.extraKeys, handle, cm))
+ || lookupKey(name, cm.options.keyMap, handle, cm);
+ }
+
+ var stopSeq = new Delayed;
+ function dispatchKey(cm, name, e, handle) {
+ var seq = cm.state.keySeq;
+ if (seq) {
+ if (isModifierKey(name)) return "handled";
+ stopSeq.set(50, function() {
+ if (cm.state.keySeq == seq) {
+ cm.state.keySeq = null;
+ cm.display.input.reset();
+ }
+ });
+ name = seq + " " + name;
+ }
+ var result = lookupKeyForEditor(cm, name, handle);
+
+ if (result == "multi")
+ cm.state.keySeq = name;
+ if (result == "handled")
+ signalLater(cm, "keyHandled", cm, name, e);
+
+ if (result == "handled" || result == "multi") {
+ e_preventDefault(e);
+ restartBlink(cm);
+ }
+
+ if (seq && !result && /\'$/.test(name)) {
+ e_preventDefault(e);
+ return true;
+ }
+ return !!result;
+ }
+
+ // Handle a key from the keydown event.
+ function handleKeyBinding(cm, e) {
+ var name = keyName(e, true);
+ if (!name) return false;
+
+ if (e.shiftKey && !cm.state.keySeq) {
+ // First try to resolve full name (including 'Shift-'). Failing
+ // that, see if there is a cursor-motion command (starting with
+ // 'go') bound to the keyname without 'Shift-'.
+ return dispatchKey(cm, "Shift-" + name, e, function(b) {return doHandleBinding(cm, b, true);})
+ || dispatchKey(cm, name, e, function(b) {
+ if (typeof b == "string" ? /^go[A-Z]/.test(b) : b.motion)
+ return doHandleBinding(cm, b);
+ });
+ } else {
+ return dispatchKey(cm, name, e, function(b) { return doHandleBinding(cm, b); });
+ }
+ }
+
+ // Handle a key from the keypress event
+ function handleCharBinding(cm, e, ch) {
+ return dispatchKey(cm, "'" + ch + "'", e,
+ function(b) { return doHandleBinding(cm, b, true); });
+ }
+
+ var lastStoppedKey = null;
+ function onKeyDown(e) {
+ var cm = this;
+ cm.curOp.focus = activeElt();
+ if (signalDOMEvent(cm, e)) return;
+ // IE does strange things with escape.
+ if (ie && ie_version < 11 && e.keyCode == 27) e.returnValue = false;
+ var code = e.keyCode;
+ cm.display.shift = code == 16 || e.shiftKey;
+ var handled = handleKeyBinding(cm, e);
+ 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("", null, "cut");
+ }
+
+ // Turn mouse into crosshair when Alt is held on Mac.
+ if (code == 18 && !/\bCodeMirror-crosshair\b/.test(cm.display.lineDiv.className))
+ showCrossHair(cm);
+ }
+
+ function showCrossHair(cm) {
+ var lineDiv = cm.display.lineDiv;
+ addClass(lineDiv, "CodeMirror-crosshair");
+
+ function up(e) {
+ if (e.keyCode == 18 || !e.altKey) {
+ rmClass(lineDiv, "CodeMirror-crosshair");
+ off(document, "keyup", up);
+ off(document, "mouseover", up);
+ }
+ }
+ on(document, "keyup", up);
+ on(document, "mouseover", up);
+ }
+
+ function onKeyUp(e) {
+ if (e.keyCode == 16) this.doc.sel.shift = false;
+ signalDOMEvent(this, e);
+ }
+
+ function onKeyPress(e) {
+ var cm = this;
+ if (eventInWidget(cm.display, e) || signalDOMEvent(cm, e) || e.ctrlKey && !e.altKey || mac && e.metaKey) return;
+ var keyCode = e.keyCode, charCode = e.charCode;
+ if (presto && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;}
+ if ((presto && (!e.which || e.which < 10)) && handleKeyBinding(cm, e)) return;
+ var ch = String.fromCharCode(charCode == null ? keyCode : charCode);
+ if (handleCharBinding(cm, e, ch)) return;
+ cm.display.input.onKeyPress(e);
+ }
+
+ // FOCUS/BLUR EVENTS
+
+ function delayBlurEvent(cm) {
+ cm.state.delayingBlurEvent = true;
+ setTimeout(function() {
+ if (cm.state.delayingBlurEvent) {
+ cm.state.delayingBlurEvent = false;
+ onBlur(cm);
+ }
+ }, 100);
+ }
+
+ function onFocus(cm) {
+ if (cm.state.delayingBlurEvent) cm.state.delayingBlurEvent = false;
+
+ if (cm.options.readOnly == "nocursor") return;
+ if (!cm.state.focused) {
+ signal(cm, "focus", cm);
+ cm.state.focused = true;
+ addClass(cm.display.wrapper, "CodeMirror-focused");
+ // This test prevents this from firing when a context
+ // menu is closed (since the input reset would kill the
+ // select-all detection hack)
+ if (!cm.curOp && cm.display.selForContextMenu != cm.doc.sel) {
+ cm.display.input.reset();
+ if (webkit) setTimeout(function() { cm.display.input.reset(true); }, 20); // Issue #1730
+ }
+ cm.display.input.receivedFocus();
+ }
+ restartBlink(cm);
+ }
+ function onBlur(cm) {
+ if (cm.state.delayingBlurEvent) return;
+
+ if (cm.state.focused) {
+ signal(cm, "blur", cm);
+ cm.state.focused = false;
+ rmClass(cm.display.wrapper, "CodeMirror-focused");
+ }
+ clearInterval(cm.display.blinker);
+ setTimeout(function() {if (!cm.state.focused) cm.display.shift = false;}, 150);
+ }
+
+ // CONTEXT MENU HANDLING
+
+ // 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 (eventInWidget(cm.display, e) || contextMenuInGutter(cm, e)) return;
+ if (signalDOMEvent(cm, e, "contextmenu")) return;
+ cm.display.input.onContextMenu(e);
+ }
+
+ function contextMenuInGutter(cm, e) {
+ if (!hasHandler(cm, "gutterContextMenu")) return false;
+ return gutterEvent(cm, e, "gutterContextMenu", false);
+ }
+
+ // 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));
+ };
+
+ // 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)));
+ }
+ return normalizeSelection(out, doc.sel.primIndex);
+ }
+
+ 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,
+ from: change.from,
+ to: change.to,
+ text: change.text,
+ origin: change.origin,
+ cancel: function() { this.canceled = true; }
+ };
+ if (update) obj.update = function(from, to, text, origin) {
+ if (from) this.from = clipPos(doc, from);
+ if (to) this.to = clipPos(doc, to);
+ if (text) this.text = text;
+ if (origin !== undefined) this.origin = origin;
+ };
+ signal(doc, "beforeChange", doc, obj);
+ if (doc.cm) signal(doc.cm, "beforeChange", doc.cm, obj);
+
+ if (obj.canceled) return null;
+ return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin};
+ }
+
+ // 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, ignoreReadOnly);
+ if (doc.cm.state.suppressEdits) return;
+ }
+
+ if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) {
+ change = filterChange(doc, change, true);
+ if (!change) return;
+ }
+
+ // Possibly split or suppress the update based on the presence
+ // 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 >= 0; --i)
+ makeChangeInner(doc, {from: split[i].from, to: split[i].to, text: i ? [""] : change.text});
+ } else {
+ makeChangeInner(doc, change);
+ }
+ }
+
+ 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 = [];
+
+ linkedDocs(doc, function(doc, sharedHist) {
+ if (!sharedHist && indexOf(rebased, doc.history) == -1) {
+ rebaseHist(doc.history, change);
+ rebased.push(doc.history);
+ }
+ makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change));
+ });
+ }
+
+ // 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, event, selAfter = doc.sel;
+ var source = type == "undo" ? hist.done : hist.undone, dest = type == "undo" ? hist.undone : hist.done;
+
+ // 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");
+
+ for (var i = event.changes.length - 1; i >= 0; --i) {
+ var change = event.changes[i];
+ change.origin = type;
+ if (filter && !filterChange(doc, change, false)) {
+ source.length = 0;
+ return;
+ }
+
+ antiChanges.push(historyChangeFromChange(doc, change));
+
+ var after = i ? computeSelAfterChange(doc, change) : lst(source);
+ makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change));
+ if (!i && doc.cm) doc.cm.scrollIntoView({from: change.from, to: changeEnd(change)});
+ var rebased = [];
+
+ // Propagate to the linked documents
+ linkedDocs(doc, function(doc, sharedHist) {
+ if (!sharedHist && indexOf(rebased, doc.history) == -1) {
+ rebaseHist(doc.history, change);
+ rebased.push(doc.history);
+ }
+ makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change));
+ });
+ }
+ }
+
+ // Sub-views need their line numbers shifted when text is added
+ // above or below them in the parent document.
+ function shiftDoc(doc, distance) {
+ if (distance == 0) return;
+ doc.first += distance;
+ 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);
+ for (var d = doc.cm.display, l = d.viewFrom; l < d.viewTo; l++)
+ regLineChange(doc.cm, l, "gutter");
+ }
+ }
+
+ // 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);
+
+ if (change.to.line < doc.first) {
+ shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line));
+ return;
+ }
+ if (change.from.line > doc.lastLine()) return;
+
+ // Clip the change to the size of this doc
+ if (change.from.line < doc.first) {
+ var shift = change.text.length - 1 - (doc.first - change.from.line);
+ shiftDoc(doc, shift);
+ change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch),
+ text: [lst(change.text)], origin: change.origin};
+ }
+ var last = doc.lastLine();
+ if (change.to.line > last) {
+ change = {from: change.from, to: Pos(last, getLine(doc, last).text.length),
+ text: [change.text[0]], origin: change.origin};
+ }
+
+ change.removed = getBetween(doc, change.from, change.to);
+
+ if (!selAfter) selAfter = computeSelAfterChange(doc, change);
+ if (doc.cm) makeChangeSingleDocInEditor(doc.cm, change, spans);
+ else updateDoc(doc, change, spans);
+ setSelectionNoUndo(doc, selAfter, sel_dontScroll);
+ }
+
+ // 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(getLine(doc, from.line)));
+ doc.iter(checkWidthStart, to.line + 1, function(line) {
+ if (line == display.maxLine) {
+ recomputeMaxLength = true;
+ return true;
+ }
+ });
+ }
+
+ if (doc.sel.contains(change.from, change.to) > -1)
+ signalCursorActivity(cm);
+
+ updateDoc(doc, change, spans, estimateHeight(cm));
+
+ if (!cm.options.lineWrapping) {
+ doc.iter(checkWidthStart, from.line + change.text.length, function(line) {
+ var len = lineLength(line);
+ if (len > display.maxLineLength) {
+ display.maxLine = line;
+ display.maxLineLength = len;
+ display.maxLineChanged = true;
+ recomputeMaxLength = false;
+ }
+ });
+ if (recomputeMaxLength) cm.curOp.updateMaxLine = true;
+ }
+
+ // Adjust frontier, schedule worker
+ doc.frontier = Math.min(doc.frontier, from.line);
+ startWorker(cm, 400);
+
+ var lendiff = change.text.length - (to.line - from.line) - 1;
+ // Remember that these lines changed, for updating the display
+ if (change.full)
+ regChange(cm);
+ else 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);
+
+ var changesHandler = hasHandler(cm, "changes"), changeHandler = hasHandler(cm, "change");
+ if (changeHandler || changesHandler) {
+ var obj = {
+ from: from, to: to,
+ text: change.text,
+ removed: change.removed,
+ origin: change.origin
+ };
+ if (changeHandler) signalLater(cm, "change", cm, obj);
+ if (changesHandler) (cm.curOp.changeObjs || (cm.curOp.changeObjs = [])).push(obj);
+ }
+ cm.display.selForContextMenu = null;
+ }
+
+ function replaceRange(doc, code, from, to, origin) {
+ if (!to) to = from;
+ if (cmp(to, from) < 0) { var tmp = to; to = from; from = tmp; }
+ if (typeof code == "string") code = doc.splitLines(code);
+ makeChange(doc, {from: from, to: to, text: code, origin: origin});
+ }
+
+ // SCROLLING THINGS INTO VIEW
+
+ // 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) {
+ if (signalDOMEvent(cm, "scrollCursorIntoView")) return;
+
+ 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 - paddingTop(cm.display)) + "px; height: " +
+ (coords.bottom - coords.top + scrollGap(cm) + display.barHeight) + "px; left: " +
+ coords.left + "px; width: 2px;");
+ cm.display.lineSpace.appendChild(scrollNode);
+ scrollNode.scrollIntoView(doScroll);
+ cm.display.lineSpace.removeChild(scrollNode);
+ }
+ }
+
+ // 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 (var limit = 0; limit < 5; limit++) {
+ var changed = false, coords = cursorCoords(cm, pos);
+ var endCoords = !end || end == pos ? coords : cursorCoords(cm, end);
+ var scrollPos = calculateScrollPos(cm, Math.min(coords.left, endCoords.left),
+ Math.min(coords.top, endCoords.top) - margin,
+ Math.max(coords.left, endCoords.left),
+ Math.max(coords.bottom, endCoords.bottom) + margin);
+ var startTop = cm.doc.scrollTop, startLeft = cm.doc.scrollLeft;
+ if (scrollPos.scrollTop != null) {
+ setScrollTop(cm, scrollPos.scrollTop);
+ if (Math.abs(cm.doc.scrollTop - startTop) > 1) changed = true;
+ }
+ if (scrollPos.scrollLeft != null) {
+ setScrollLeft(cm, scrollPos.scrollLeft);
+ if (Math.abs(cm.doc.scrollLeft - startLeft) > 1) changed = true;
+ }
+ if (!changed) break;
+ }
+ return coords;
+ }
+
+ // 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 screentop = cm.curOp && cm.curOp.scrollTop != null ? cm.curOp.scrollTop : display.scroller.scrollTop;
+ var screen = displayHeight(cm), result = {};
+ if (y2 - y1 > screen) y2 = y1 + screen;
+ var docBottom = cm.doc.height + paddingVert(display);
+ var atTop = y1 < snapMargin, atBottom = y2 > docBottom - snapMargin;
+ if (y1 < screentop) {
+ result.scrollTop = atTop ? 0 : y1;
+ } else if (y2 > screentop + screen) {
+ var newTop = Math.min(y1, (atBottom ? docBottom : y2) - screen);
+ if (newTop != screentop) result.scrollTop = newTop;
+ }
+
+ var screenleft = cm.curOp && cm.curOp.scrollLeft != null ? cm.curOp.scrollLeft : display.scroller.scrollLeft;
+ var screenw = displayWidth(cm) - (cm.options.fixedGutter ? display.gutters.offsetWidth : 0);
+ var tooWide = x2 - x1 > screenw;
+ if (tooWide) x2 = x1 + screenw;
+ if (x1 < 10)
+ result.scrollLeft = 0;
+ else if (x1 < screenleft)
+ result.scrollLeft = Math.max(0, x1 - (tooWide ? 0 : 10));
+ else if (x2 > screenw + screenleft - 3)
+ result.scrollLeft = x2 + (tooWide ? 0 : 10) - screenw;
+ return result;
+ }
+
+ // 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;
+ }
+
+ // 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, state;
+ if (how == null) how = "add";
+ if (how == "smart") {
+ // Fall back to "prev" when the mode doesn't have an indentation
+ // method.
+ if (!doc.mode.indent) how = "prev";
+ 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;
+ how = "not";
+ } else if (how == "smart") {
+ indentation = doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text);
+ if (indentation == Pass || indentation > 150) {
+ if (!aggressive) return;
+ how = "prev";
+ }
+ }
+ if (how == "prev") {
+ if (n > doc.first) indentation = countColumn(getLine(doc, n-1).text, null, tabSize);
+ else indentation = 0;
+ } else if (how == "add") {
+ indentation = curSpace + cm.options.indentUnit;
+ } else if (how == "subtract") {
+ indentation = curSpace - cm.options.indentUnit;
+ } else if (typeof how == "number") {
+ indentation = curSpace + how;
+ }
+ indentation = Math.max(0, indentation);
+
+ var indentString = "", pos = 0;
+ if (cm.options.indentWithTabs)
+ for (var i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += "\t";}
+ if (pos < indentation) indentString += spaceStr(indentation - pos);
+
+ if (indentString != curSpaceString) {
+ replaceRange(doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input");
+ line.stateAfter = null;
+ return true;
+ } 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;
+ }
+ }
+ }
+ }
+
+ // Utility for applying a change to a line by handle or number,
+ // returning the number and optionally registering the line as
+ // changed.
+ function changeLine(doc, handle, changeType, op) {
+ var no = handle, line = handle;
+ if (typeof handle == "number") line = getLine(doc, clipLine(doc, handle));
+ else no = lineNo(handle);
+ if (no == null) return null;
+ if (op(line, no) && doc.cm) regLineChange(doc.cm, no, changeType);
+ 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);
+ function findNextLine() {
+ var l = line + dir;
+ if (l < doc.first || l >= doc.first + doc.size) return false
+ line = l;
+ return lineObj = getLine(doc, l);
+ }
+ function moveOnce(boundToLine) {
+ var next = (visually ? moveVisually : moveLogically)(lineObj, ch, dir, true);
+ if (next == null) {
+ if (!boundToLine && findNextLine()) {
+ if (visually) ch = (dir < 0 ? lineRight : lineLeft)(lineObj);
+ else ch = dir < 0 ? lineObj.text.length : 0;
+ } else return false
+ } else ch = next;
+ return true;
+ }
+
+ if (unit == "char") {
+ moveOnce()
+ } else if (unit == "column") {
+ moveOnce(true)
+ } else if (unit == "word" || unit == "group") {
+ var sawType = null, group = unit == "group";
+ var helper = doc.cm && doc.cm.getHelper(pos, "wordChars");
+ for (var first = true;; first = false) {
+ if (dir < 0 && !moveOnce(!first)) break;
+ var cur = lineObj.text.charAt(ch) || "\n";
+ var type = isWordChar(cur, helper) ? "w"
+ : 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;
+ }
+ }
+ var result = skipAtomic(doc, Pos(line, ch), pos, origDir, true);
+ if (!cmp(pos, result)) result.hitSide = true;
+ 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") {
+ var pageSize = Math.min(cm.display.wrapper.clientHeight, window.innerHeight || document.documentElement.clientHeight);
+ y = pos.top + dir * (pageSize - (dir < 0 ? 1.5 : .5) * textHeight(cm.display));
+ } else if (unit == "line") {
+ y = dir > 0 ? pos.bottom + 3 : pos.top - 3;
+ }
+ for (;;) {
+ var target = coordsChar(cm, x, y);
+ if (!target.outside) break;
+ if (dir < 0 ? y <= 0 : y >= doc.height) { target.hitSide = true; break; }
+ y += dir * 5;
+ }
+ return target;
+ }
+
+ // EDITOR METHODS
+
+ // The publicly visible API. Note that methodOp(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,
+ focus: function(){window.focus(); this.display.input.focus();},
+
+ setOption: function(option, value) {
+ var options = this.options, old = options[option];
+ if (options[option] == value && option != "mode") return;
+ options[option] = value;
+ if (optionHandlers.hasOwnProperty(option))
+ operation(this, optionHandlers[option])(this, value, old);
+ },
+
+ getOption: function(option) {return this.options[option];},
+ getDoc: function() {return this.doc;},
+
+ addKeyMap: function(map, bottom) {
+ this.state.keyMaps[bottom ? "push" : "unshift"](getKeyMap(map));
+ },
+ removeKeyMap: function(map) {
+ var maps = this.state.keyMaps;
+ for (var i = 0; i < maps.length; ++i)
+ if (maps[i] == map || maps[i].name == map) {
+ maps.splice(i, 1);
+ return true;
+ }
+ },
+
+ 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: methodOp(function(spec) {
+ var overlays = this.state.overlays;
+ for (var i = 0; i < overlays.length; ++i) {
+ var cur = overlays[i].modeSpec;
+ if (cur == spec || typeof spec == "string" && cur.name == spec) {
+ overlays.splice(i, 1);
+ this.state.modeGen++;
+ regChange(this);
+ return;
+ }
+ }
+ }),
+
+ 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: 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 from = range.from(), to = range.to();
+ var start = Math.max(end, from.line);
+ end = Math.min(this.lastLine(), to.line - (to.ch ? 0 : 1)) + 1;
+ for (var j = start; j < end; ++j)
+ indentLine(this, j, how);
+ var newRanges = this.doc.sel.ranges;
+ if (from.ch == 0 && ranges.length == newRanges.length && newRanges[i].from().ch > 0)
+ replaceOneSelection(this.doc, i, new Range(from, newRanges[i].to()), sel_dontScroll);
+ } 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
+ // that want to inspect the mode state (say, for completion).
+ getTokenAt: function(pos, precise) {
+ return takeToken(this, pos, precise);
+ },
+
+ getLineTokens: function(line, precise) {
+ return takeToken(this, Pos(line), precise, true);
+ },
+
+ getTokenTypeAt: function(pos) {
+ pos = clipPos(this.doc, pos);
+ var styles = getLineStyles(this, getLine(this.doc, pos.line));
+ var before = 0, after = (styles.length - 1) / 2, ch = pos.ch;
+ var type;
+ if (ch == 0) type = styles[2];
+ else for (;;) {
+ var mid = (before + after) >> 1;
+ if ((mid ? styles[mid * 2 - 1] : 0) >= ch) after = mid;
+ else if (styles[mid * 2 + 1] < ch) before = mid + 1;
+ else { type = styles[mid * 2 + 2]; break; }
+ }
+ var cut = type ? type.indexOf("cm-overlay ") : -1;
+ return cut < 0 ? type : cut == 0 ? null : type.slice(0, cut - 1);
+ },
+
+ getModeAt: function(pos) {
+ var mode = this.doc.mode;
+ if (!mode.innerMode) return mode;
+ return CodeMirror.innerMode(mode, this.getTokenAt(pos).state).mode;
+ },
+
+ getHelper: function(pos, type) {
+ return this.getHelpers(pos, type)[0];
+ },
+
+ getHelpers: function(pos, type) {
+ var found = [];
+ if (!helpers.hasOwnProperty(type)) return found;
+ var help = helpers[type], mode = this.getModeAt(pos);
+ if (typeof mode[type] == "string") {
+ if (help[mode[type]]) found.push(help[mode[type]]);
+ } else if (mode[type]) {
+ for (var i = 0; i < mode[type].length; i++) {
+ var val = help[mode[type][i]];
+ if (val) found.push(val);
+ }
+ } else if (mode.helperType && help[mode.helperType]) {
+ found.push(help[mode.helperType]);
+ } else if (help[mode.name]) {
+ found.push(help[mode.name]);
+ }
+ for (var i = 0; i < help._global.length; i++) {
+ var cur = help._global[i];
+ if (cur.pred(mode, this) && indexOf(found, cur.val) == -1)
+ found.push(cur.val);
+ }
+ return found;
+ },
+
+ getStateAfter: function(line, precise) {
+ var doc = this.doc;
+ line = clipLine(doc, line == null ? doc.first + doc.size - 1: line);
+ return getStateBefore(this, line + 1, precise);
+ },
+
+ cursorCoords: function(start, mode) {
+ 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 ? range.from() : range.to();
+ return cursorCoords(this, pos, mode || "page");
+ },
+
+ charCoords: function(pos, mode) {
+ return charCoords(this, clipPos(this.doc, pos), mode || "page");
+ },
+
+ coordsChar: function(coords, mode) {
+ coords = fromCoordSystem(this, coords, mode || "page");
+ return coordsChar(this, coords.left, coords.top);
+ },
+
+ lineAtHeight: function(height, mode) {
+ height = fromCoordSystem(this, {top: height, left: 0}, mode || "page").top;
+ return lineAtHeight(this.doc, height + this.display.viewOffset);
+ },
+ heightAtLine: function(line, mode) {
+ var end = false, lineObj;
+ if (typeof line == "number") {
+ var last = this.doc.first + this.doc.size - 1;
+ if (line < this.doc.first) line = this.doc.first;
+ else if (line > last) { line = last; end = true; }
+ lineObj = getLine(this.doc, line);
+ } else {
+ lineObj = line;
+ }
+ 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: methodOp(function(line, gutterID, value) {
+ return changeLine(this.doc, line, "gutter", function(line) {
+ var markers = line.gutterMarkers || (line.gutterMarkers = {});
+ markers[gutterID] = value;
+ if (!value && isEmpty(markers)) line.gutterMarkers = null;
+ return true;
+ });
+ }),
+
+ 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;
+ regLineChange(cm, i, "gutter");
+ if (isEmpty(line.gutterMarkers)) line.gutterMarkers = null;
+ }
+ ++i;
+ });
+ }),
+
+ lineInfo: function(line) {
+ if (typeof line == "number") {
+ if (!isLine(this.doc, line)) return null;
+ var n = line;
+ line = getLine(this.doc, line);
+ if (!line) return null;
+ } else {
+ var n = lineNo(line);
+ if (n == null) return null;
+ }
+ return {line: n, handle: line, text: line.text, gutterMarkers: line.gutterMarkers,
+ textClass: line.textClass, bgClass: line.bgClass, wrapClass: line.wrapClass,
+ widgets: line.widgets};
+ },
+
+ getViewport: function() { return {from: this.display.viewFrom, to: this.display.viewTo};},
+
+ addWidget: function(pos, node, scroll, vert, horiz) {
+ var display = this.display;
+ pos = cursorCoords(this, clipPos(this.doc, pos));
+ var top = pos.bottom, left = pos.left;
+ node.style.position = "absolute";
+ node.setAttribute("cm-ignore-events", "true");
+ this.display.input.setUneditable(node);
+ display.sizer.appendChild(node);
+ if (vert == "over") {
+ top = pos.top;
+ } else if (vert == "above" || vert == "near") {
+ var vspace = Math.max(display.wrapper.clientHeight, this.doc.height),
+ hspace = Math.max(display.sizer.clientWidth, display.lineSpace.clientWidth);
+ // Default to positioning above (if specified and possible); otherwise default to positioning below
+ if ((vert == 'above' || pos.bottom + node.offsetHeight > vspace) && pos.top > node.offsetHeight)
+ top = pos.top - node.offsetHeight;
+ else if (pos.bottom + node.offsetHeight <= vspace)
+ top = pos.bottom;
+ if (left + node.offsetWidth > hspace)
+ left = hspace - node.offsetWidth;
+ }
+ node.style.top = top + "px";
+ node.style.left = node.style.right = "";
+ if (horiz == "right") {
+ left = display.sizer.clientWidth - node.offsetWidth;
+ node.style.right = "0px";
+ } else {
+ if (horiz == "left") left = 0;
+ else if (horiz == "middle") left = (display.sizer.clientWidth - node.offsetWidth) / 2;
+ node.style.left = left + "px";
+ }
+ if (scroll)
+ scrollIntoView(this, left, top, left + node.offsetWidth, top + node.offsetHeight);
+ },
+
+ triggerOnKeyDown: methodOp(onKeyDown),
+ triggerOnKeyPress: methodOp(onKeyPress),
+ triggerOnKeyUp: onKeyUp,
+
+ execCommand: function(cmd) {
+ if (commands.hasOwnProperty(cmd))
+ return commands[cmd].call(null, this);
+ },
+
+ triggerElectric: methodOp(function(text) { triggerElectric(this, text); }),
+
+ findPosH: function(from, amount, unit, visually) {
+ var dir = 1;
+ if (amount < 0) { dir = -1; amount = -amount; }
+ for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) {
+ cur = findPosH(this.doc, cur, dir, unit, visually);
+ if (cur.hitSide) break;
+ }
+ return cur;
+ },
+
+ 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: 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) {
+ var dir = 1, x = goalColumn;
+ if (amount < 0) { dir = -1; amount = -amount; }
+ for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) {
+ var coords = cursorCoords(this, cur, "div");
+ if (x == null) x = coords.left;
+ else coords.left = x;
+ cur = findPosV(this, coords, dir, unit);
+ if (cur.hitSide) break;
+ }
+ return cur;
+ },
+
+ 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];
+ }),
+
+ // Find the word at the given position (as returned by coordsChar).
+ findWordAt: function(pos) {
+ var doc = this.doc, line = getLine(doc, pos.line).text;
+ var start = pos.ch, end = pos.ch;
+ if (line) {
+ var helper = this.getHelper(pos, "wordChars");
+ if ((pos.xRel < 0 || end == line.length) && start) --start; else ++end;
+ var startChar = line.charAt(start);
+ var check = isWordChar(startChar, helper)
+ ? function(ch) { return isWordChar(ch, helper); }
+ : /\s/.test(startChar) ? function(ch) {return /\s/.test(ch);}
+ : function(ch) {return !/\s/.test(ch) && !isWordChar(ch);};
+ while (start > 0 && check(line.charAt(start - 1))) --start;
+ while (end < line.length && check(line.charAt(end))) ++end;
+ }
+ return new Range(Pos(pos.line, start), Pos(pos.line, end));
+ },
+
+ toggleOverwrite: function(value) {
+ if (value != null && value == this.state.overwrite) return;
+ if (this.state.overwrite = !this.state.overwrite)
+ addClass(this.display.cursorDiv, "CodeMirror-overwrite");
+ else
+ rmClass(this.display.cursorDiv, "CodeMirror-overwrite");
+
+ signal(this, "overwriteToggle", this, this.state.overwrite);
+ },
+ hasFocus: function() { return this.display.input.getField() == activeElt(); },
+ isReadOnly: function() { return !!(this.options.readOnly || this.doc.cantEdit); },
+
+ 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;
+ return {left: scroller.scrollLeft, top: scroller.scrollTop,
+ height: scroller.scrollHeight - scrollGap(this) - this.display.barHeight,
+ width: scroller.scrollWidth - scrollGap(this) - this.display.barWidth,
+ clientHeight: displayHeight(this), clientWidth: displayWidth(this)};
+ },
+
+ 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);
+ }
+ }),
+
+ setSize: methodOp(function(width, height) {
+ var cm = this;
+ function interpret(val) {
+ return typeof val == "number" || /^\d+$/.test(String(val)) ? val + "px" : val;
+ }
+ if (width != null) cm.display.wrapper.style.width = interpret(width);
+ if (height != null) cm.display.wrapper.style.height = interpret(height);
+ if (cm.options.lineWrapping) clearLineMeasurementCache(this);
+ var lineNo = cm.display.viewFrom;
+ cm.doc.iter(lineNo, cm.display.viewTo, function(line) {
+ if (line.widgets) for (var i = 0; i < line.widgets.length; i++)
+ if (line.widgets[i].noHScroll) { regLineChange(cm, lineNo, "widget"); break; }
+ ++lineNo;
+ });
+ cm.curOp.forceUpdate = true;
+ signal(cm, "refresh", this);
+ }),
+
+ operation: function(f){return runInOp(this, f);},
+
+ refresh: methodOp(function() {
+ var oldHeight = this.display.cachedTextHeight;
+ regChange(this);
+ this.curOp.forceUpdate = true;
+ clearCaches(this);
+ this.scrollTo(this.doc.scrollLeft, this.doc.scrollTop);
+ updateGutterSpace(this);
+ if (oldHeight == null || Math.abs(oldHeight - textHeight(this.display)) > .5)
+ estimateLineHeights(this);
+ signal(this, "refresh", this);
+ }),
+
+ swapDoc: methodOp(function(doc) {
+ var old = this.doc;
+ old.cm = null;
+ attachDoc(this, doc);
+ clearCaches(this);
+ this.display.input.reset();
+ this.scrollTo(doc.scrollLeft, doc.scrollTop);
+ this.curOp.forceScroll = true;
+ signalLater(this, "swapDoc", this, old);
+ return old;
+ }),
+
+ getInputField: function(){return this.display.input.getField();},
+ getWrapperElement: function(){return this.display.wrapper;},
+ getScrollerElement: function(){return this.display.scroller;},
+ getGutterElement: function(){return this.display.gutters;}
+ };
+ eventMixin(CodeMirror);
+
+ // OPTION DEFAULTS
+
+ // 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;
+ if (handle) optionHandlers[name] =
+ 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
+ // have to be initialized before the editor can start at all.
+ option("value", "", function(cm, val) {
+ cm.setValue(val);
+ }, true);
+ option("mode", null, function(cm, val) {
+ cm.doc.modeOption = val;
+ loadMode(cm);
+ }, true);
+
+ option("indentUnit", 2, loadMode, true);
+ option("indentWithTabs", false);
+ option("smartIndent", true);
+ option("tabSize", 4, function(cm) {
+ resetModeState(cm);
+ clearCaches(cm);
+ regChange(cm);
+ }, true);
+ option("lineSeparator", null, function(cm, val) {
+ cm.doc.lineSep = val;
+ if (!val) return;
+ var newBreaks = [], lineNo = cm.doc.first;
+ cm.doc.iter(function(line) {
+ for (var pos = 0;;) {
+ var found = line.text.indexOf(val, pos);
+ if (found == -1) break;
+ pos = found + val.length;
+ newBreaks.push(Pos(lineNo, found));
+ }
+ lineNo++;
+ });
+ for (var i = newBreaks.length - 1; i >= 0; i--)
+ replaceRange(cm.doc, val, newBreaks[i], Pos(newBreaks[i].line, newBreaks[i].ch + val.length))
+ });
+ option("specialChars", /[\u0000-\u001f\u007f\u00ad\u200b-\u200f\u2028\u2029\ufeff]/g, function(cm, val, old) {
+ cm.state.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g");
+ if (old != CodeMirror.Init) cm.refresh();
+ });
+ option("specialCharPlaceholder", defaultSpecialCharPlaceholder, function(cm) {cm.refresh();}, true);
+ option("electricChars", true);
+ option("inputStyle", mobile ? "contenteditable" : "textarea", function() {
+ throw new Error("inputStyle can not (yet) be changed in a running editor"); // FIXME
+ }, true);
+ option("rtlMoveVisually", !windows);
+ option("wholeLineUpdateBefore", true);
+
+ option("theme", "default", function(cm) {
+ themeChanged(cm);
+ guttersChanged(cm);
+ }, true);
+ option("keyMap", "default", function(cm, val, old) {
+ var next = getKeyMap(val);
+ var prev = old != CodeMirror.Init && getKeyMap(old);
+ if (prev && prev.detach) prev.detach(cm, next);
+ if (next.attach) next.attach(cm, prev || null);
+ });
+ option("extraKeys", null);
+
+ option("lineWrapping", false, wrappingChanged, true);
+ option("gutters", [], function(cm) {
+ setGuttersForLineNumbers(cm.options);
+ guttersChanged(cm);
+ }, true);
+ option("fixedGutter", true, function(cm, val) {
+ cm.display.gutters.style.left = val ? compensateForHScroll(cm.display) + "px" : "0";
+ cm.refresh();
+ }, true);
+ option("coverGutterNextToScrollbar", false, function(cm) {updateScrollbars(cm);}, true);
+ option("scrollbarStyle", "native", function(cm) {
+ initScrollbars(cm);
+ updateScrollbars(cm);
+ cm.display.scrollbars.setScrollTop(cm.doc.scrollTop);
+ cm.display.scrollbars.setScrollLeft(cm.doc.scrollLeft);
+ }, true);
+ option("lineNumbers", false, function(cm) {
+ setGuttersForLineNumbers(cm.options);
+ guttersChanged(cm);
+ }, true);
+ option("firstLineNumber", 1, guttersChanged, true);
+ option("lineNumberFormatter", function(integer) {return integer;}, guttersChanged, true);
+ option("showCursorWhenSelecting", false, updateSelection, true);
+
+ option("resetSelectionOnContextMenu", true);
+ option("lineWiseCopyCut", true);
+
+ option("readOnly", false, function(cm, val) {
+ if (val == "nocursor") {
+ onBlur(cm);
+ cm.display.input.blur();
+ cm.display.disabled = true;
+ } else {
+ cm.display.disabled = false;
+ }
+ cm.display.input.readOnlyChanged(val)
+ });
+ option("disableInput", false, function(cm, val) {if (!val) cm.display.input.reset();}, true);
+ option("dragDrop", true, dragDropChanged);
+ option("allowDropFileTypes", null);
+
+ option("cursorBlinkRate", 530);
+ option("cursorScrollMargin", 0);
+ option("cursorHeight", 1, updateSelection, true);
+ option("singleCursorHeightPerLine", true, updateSelection, true);
+ option("workTime", 100);
+ option("workDelay", 100);
+ option("flattenSpans", true, resetModeState, true);
+ option("addModeClass", false, resetModeState, true);
+ option("pollInterval", 100);
+ 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("moveInputWithCursor", true, function(cm, val) {
+ if (!val) cm.display.input.resetPosition();
+ });
+
+ option("tabindex", null, function(cm, val) {
+ cm.display.input.getField().tabIndex = val || "";
+ });
+ option("autofocus", null);
+
+ // MODE DEFINITION AND QUERYING
+
+ // 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)
+ mode.dependencies = Array.prototype.slice.call(arguments, 2);
+ modes[name] = mode;
+ };
+
+ CodeMirror.defineMIME = function(mime, spec) {
+ 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)) {
+ return CodeMirror.resolveMode("application/xml");
+ }
+ if (typeof spec == "string") return {name: spec};
+ 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];
+ if (!mfactory) return CodeMirror.getMode(options, "text/plain");
+ var modeObj = mfactory(options, spec);
+ if (modeExtensions.hasOwnProperty(spec.name)) {
+ var exts = modeExtensions[spec.name];
+ for (var prop in exts) {
+ if (!exts.hasOwnProperty(prop)) continue;
+ if (modeObj.hasOwnProperty(prop)) modeObj["_" + prop] = modeObj[prop];
+ modeObj[prop] = exts[prop];
+ }
+ }
+ modeObj.name = spec.name;
+ if (spec.helperType) modeObj.helperType = spec.helperType;
+ if (spec.modeProps) for (var prop in spec.modeProps)
+ modeObj[prop] = spec.modeProps[prop];
+
+ 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] = {});
+ copyObj(properties, exts);
+ };
+
+ // EXTENSIONS
+
+ CodeMirror.defineExtension = function(name, func) {
+ CodeMirror.prototype[name] = func;
+ };
+ CodeMirror.defineDocExtension = function(name, func) {
+ Doc.prototype[name] = func;
+ };
+ CodeMirror.defineOption = option;
+
+ var initHooks = [];
+ CodeMirror.defineInitHook = function(f) {initHooks.push(f);};
+
+ var helpers = CodeMirror.helpers = {};
+ CodeMirror.registerHelper = function(type, name, value) {
+ if (!helpers.hasOwnProperty(type)) helpers[type] = CodeMirror[type] = {_global: []};
+ helpers[type][name] = value;
+ };
+ CodeMirror.registerGlobalHelper = function(type, name, predicate, value) {
+ CodeMirror.registerHelper(type, name, value);
+ helpers[type]._global.push({pred: predicate, val: value});
+ };
+
+ // MODE STATE HANDLING
+
+ // 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 = {};
+ for (var n in state) {
+ var val = state[n];
+ if (val instanceof Array) val = val.concat([]);
+ nstate[n] = val;
+ }
+ return nstate;
+ };
+
+ var startState = CodeMirror.startState = function(mode, a1, a2) {
+ return mode.startState ? mode.startState(a1, a2) : true;
+ };
+
+ // 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);
+ if (!info || info.mode == mode) break;
+ state = info.state;
+ mode = info.mode;
+ }
+ return info || {mode: mode, state: state};
+ };
+
+ // 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()), sel_dontScroll);},
+ singleSelection: function(cm) {
+ cm.setSelection(cm.getCursor("anchor"), cm.getCursor("head"), sel_dontScroll);
+ },
+ killLine: function(cm) {
+ 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) {
+ deleteNearSelection(cm, function(range) {
+ return {from: Pos(range.from().line, 0),
+ to: clipPos(cm.doc, Pos(range.to().line + 1, 0))};
+ });
+ },
+ delLineLeft: function(cm) {
+ deleteNearSelection(cm, function(range) {
+ return {from: Pos(range.from().line, 0), to: range.from()};
+ });
+ },
+ delWrappedLineLeft: function(cm) {
+ deleteNearSelection(cm, function(range) {
+ var top = cm.charCoords(range.head, "div").top + 5;
+ var leftPos = cm.coordsChar({left: 0, top: top}, "div");
+ return {from: leftPos, to: range.from()};
+ });
+ },
+ delWrappedLineRight: function(cm) {
+ deleteNearSelection(cm, function(range) {
+ var top = cm.charCoords(range.head, "div").top + 5;
+ var rightPos = cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div");
+ return {from: range.from(), to: rightPos };
+ });
+ },
+ 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.extendSelectionsBy(function(range) { return lineStart(cm, range.head.line); },
+ {origin: "+move", bias: 1});
+ },
+ goLineStartSmart: function(cm) {
+ cm.extendSelectionsBy(function(range) {
+ return lineStartSmart(cm, range.head);
+ }, {origin: "+move", bias: 1});
+ },
+ goLineEnd: function(cm) {
+ cm.extendSelectionsBy(function(range) { return lineEnd(cm, range.head.line); },
+ {origin: "+move", bias: -1});
+ },
+ goLineRight: function(cm) {
+ 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) {
+ cm.extendSelectionsBy(function(range) {
+ var top = cm.charCoords(range.head, "div").top + 5;
+ return cm.coordsChar({left: 0, top: top}, "div");
+ }, sel_move);
+ },
+ goLineLeftSmart: function(cm) {
+ cm.extendSelectionsBy(function(range) {
+ var top = cm.charCoords(range.head, "div").top + 5;
+ var pos = cm.coordsChar({left: 0, top: top}, "div");
+ if (pos.ch < cm.getLine(pos.line).search(/\S/)) return lineStartSmart(cm, range.head);
+ return pos;
+ }, sel_move);
+ },
+ goLineUp: function(cm) {cm.moveV(-1, "line");},
+ goLineDown: function(cm) {cm.moveV(1, "line");},
+ goPageUp: function(cm) {cm.moveV(-1, "page");},
+ goPageDown: function(cm) {cm.moveV(1, "page");},
+ goCharLeft: function(cm) {cm.moveH(-1, "char");},
+ goCharRight: function(cm) {cm.moveH(1, "char");},
+ goColumnLeft: function(cm) {cm.moveH(-1, "column");},
+ goColumnRight: function(cm) {cm.moveH(1, "column");},
+ goWordLeft: function(cm) {cm.moveH(-1, "word");},
+ goGroupRight: function(cm) {cm.moveH(1, "group");},
+ goGroupLeft: function(cm) {cm.moveH(-1, "group");},
+ goWordRight: function(cm) {cm.moveH(1, "word");},
+ delCharBefore: function(cm) {cm.deleteH(-1, "char");},
+ delCharAfter: function(cm) {cm.deleteH(1, "char");},
+ delWordBefore: function(cm) {cm.deleteH(-1, "word");},
+ delWordAfter: function(cm) {cm.deleteH(1, "word");},
+ delGroupBefore: function(cm) {cm.deleteH(-1, "group");},
+ delGroupAfter: function(cm) {cm.deleteH(1, "group");},
+ indentAuto: function(cm) {cm.indentSelection("smart");},
+ indentMore: function(cm) {cm.indentSelection("add");},
+ indentLess: function(cm) {cm.indentSelection("subtract");},
+ insertTab: function(cm) {cm.replaceSelection("\t");},
+ insertSoftTab: function(cm) {
+ var spaces = [], ranges = cm.listSelections(), tabSize = cm.options.tabSize;
+ for (var i = 0; i < ranges.length; i++) {
+ var pos = ranges[i].from();
+ var col = countColumn(cm.getLine(pos.line), pos.ch, tabSize);
+ spaces.push(spaceStr(tabSize - col % tabSize));
+ }
+ cm.replaceSelections(spaces);
+ },
+ defaultTab: function(cm) {
+ if (cm.somethingSelected()) cm.indentSelection("add");
+ else cm.execCommand("insertTab");
+ },
+ transposeChars: function(cm) {
+ runInOp(cm, function() {
+ var ranges = cm.listSelections(), newSel = [];
+ for (var i = 0; i < ranges.length; i++) {
+ var cur = ranges[i].head, line = getLine(cm.doc, cur.line).text;
+ if (line) {
+ if (cur.ch == line.length) cur = new Pos(cur.line, cur.ch - 1);
+ if (cur.ch > 0) {
+ cur = new Pos(cur.line, cur.ch + 1);
+ cm.replaceRange(line.charAt(cur.ch - 1) + line.charAt(cur.ch - 2),
+ Pos(cur.line, cur.ch - 2), cur, "+transpose");
+ } else if (cur.line > cm.doc.first) {
+ var prev = getLine(cm.doc, cur.line - 1).text;
+ if (prev)
+ cm.replaceRange(line.charAt(0) + cm.doc.lineSeparator() +
+ prev.charAt(prev.length - 1),
+ Pos(cur.line - 1, prev.length - 1), Pos(cur.line, 1), "+transpose");
+ }
+ }
+ newSel.push(new Range(cur, cur));
+ }
+ cm.setSelections(newSel);
+ });
+ },
+ newlineAndIndent: function(cm) {
+ runInOp(cm, function() {
+ var len = cm.listSelections().length;
+ for (var i = 0; i < len; i++) {
+ var range = cm.listSelections()[i];
+ cm.replaceRange(cm.doc.lineSeparator(), range.anchor, range.head, "+input");
+ cm.indentLine(range.from().line + 1, null, true);
+ }
+ ensureCursorVisible(cm);
+ });
+ },
+ openLine: function(cm) {cm.replaceSelection("\n", "start")},
+ toggleOverwrite: function(cm) {cm.toggleOverwrite();}
+ };
+
+
+ // STANDARD KEYMAPS
+
+ var keyMap = CodeMirror.keyMap = {};
+
+ keyMap.basic = {
+ "Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown",
+ "End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown",
+ "Delete": "delCharAfter", "Backspace": "delCharBefore", "Shift-Backspace": "delCharBefore",
+ "Tab": "defaultTab", "Shift-Tab": "indentAuto",
+ "Enter": "newlineAndIndent", "Insert": "toggleOverwrite",
+ "Esc": "singleSelection"
+ };
+ // Note that the save and find-related commands aren't defined by
+ // 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", "Ctrl-End": "goDocEnd", "Ctrl-Up": "goLineUp", "Ctrl-Down": "goLineDown",
+ "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"
+ };
+ // 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",
+ "Ctrl-O": "openLine"
+ };
+ keyMap.macDefault = {
+ "Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo",
+ "Cmd-Home": "goDocStart", "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goGroupLeft",
+ "Alt-Right": "goGroupRight", "Cmd-Left": "goLineLeft", "Cmd-Right": "goLineRight", "Alt-Backspace": "delGroupBefore",
+ "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": "delWrappedLineLeft", "Cmd-Delete": "delWrappedLineRight",
+ "Cmd-U": "undoSelection", "Shift-Cmd-U": "redoSelection", "Ctrl-Up": "goDocStart", "Ctrl-Down": "goDocEnd",
+ fallthrough: ["basic", "emacsy"]
+ };
+ keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault;
+
+ // KEYMAP DISPATCH
+
+ function normalizeKeyName(name) {
+ var parts = name.split(/-(?!$)/), name = parts[parts.length - 1];
+ var alt, ctrl, shift, cmd;
+ for (var i = 0; i < parts.length - 1; i++) {
+ var mod = parts[i];
+ if (/^(cmd|meta|m)$/i.test(mod)) cmd = true;
+ else if (/^a(lt)?$/i.test(mod)) alt = true;
+ else if (/^(c|ctrl|control)$/i.test(mod)) ctrl = true;
+ else if (/^s(hift)$/i.test(mod)) shift = true;
+ else throw new Error("Unrecognized modifier name: " + mod);
+ }
+ if (alt) name = "Alt-" + name;
+ if (ctrl) name = "Ctrl-" + name;
+ if (cmd) name = "Cmd-" + name;
+ if (shift) name = "Shift-" + name;
+ return name;
+ }
+
+ // This is a kludge to keep keymaps mostly working as raw objects
+ // (backwards compatibility) while at the same time support features
+ // like normalization and multi-stroke key bindings. It compiles a
+ // new normalized keymap, and then updates the old object to reflect
+ // this.
+ CodeMirror.normalizeKeyMap = function(keymap) {
+ var copy = {};
+ for (var keyname in keymap) if (keymap.hasOwnProperty(keyname)) {
+ var value = keymap[keyname];
+ if (/^(name|fallthrough|(de|at)tach)$/.test(keyname)) continue;
+ if (value == "...") { delete keymap[keyname]; continue; }
+
+ var keys = map(keyname.split(" "), normalizeKeyName);
+ for (var i = 0; i < keys.length; i++) {
+ var val, name;
+ if (i == keys.length - 1) {
+ name = keys.join(" ");
+ val = value;
+ } else {
+ name = keys.slice(0, i + 1).join(" ");
+ val = "...";
+ }
+ var prev = copy[name];
+ if (!prev) copy[name] = val;
+ else if (prev != val) throw new Error("Inconsistent bindings for " + name);
+ }
+ delete keymap[keyname];
+ }
+ for (var prop in copy) keymap[prop] = copy[prop];
+ return keymap;
+ };
+
+ var lookupKey = CodeMirror.lookupKey = function(key, map, handle, context) {
+ map = getKeyMap(map);
+ var found = map.call ? map.call(key, context) : map[key];
+ if (found === false) return "nothing";
+ if (found === "...") return "multi";
+ if (found != null && handle(found)) return "handled";
+
+ if (map.fallthrough) {
+ if (Object.prototype.toString.call(map.fallthrough) != "[object Array]")
+ return lookupKey(key, map.fallthrough, handle, context);
+ for (var i = 0; i < map.fallthrough.length; i++) {
+ var result = lookupKey(key, map.fallthrough[i], handle, context);
+ if (result) return result;
+ }
+ }
+ };
+
+ // Modifier key presses don't count as 'real' key presses for the
+ // purpose of keymap fallthrough.
+ var isModifierKey = CodeMirror.isModifierKey = function(value) {
+ var name = typeof value == "string" ? value : keyNames[value.keyCode];
+ return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod";
+ };
+
+ // 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 base = keyNames[event.keyCode], name = base;
+ if (name == null || event.altGraphKey) return false;
+ if (event.altKey && base != "Alt") name = "Alt-" + name;
+ if ((flipCtrlCmd ? event.metaKey : event.ctrlKey) && base != "Ctrl") name = "Ctrl-" + name;
+ if ((flipCtrlCmd ? event.ctrlKey : event.metaKey) && base != "Cmd") name = "Cmd-" + name;
+ if (!noShift && event.shiftKey && base != "Shift") name = "Shift-" + name;
+ return name;
+ };
+
+ function getKeyMap(val) {
+ return typeof val == "string" ? keyMap[val] : val;
+ }
+
+ // FROMTEXTAREA
+
+ CodeMirror.fromTextArea = function(textarea, options) {
+ options = options ? copyObj(options) : {};
+ options.value = textarea.value;
+ if (!options.tabindex && textarea.tabIndex)
+ options.tabindex = textarea.tabIndex;
+ if (!options.placeholder && textarea.placeholder)
+ options.placeholder = textarea.placeholder;
+ // 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 = activeElt();
+ options.autofocus = hasFocus == textarea ||
+ textarea.getAttribute("autofocus") != null && hasFocus == document.body;
+ }
+
+ function save() {textarea.value = cm.getValue();}
+ if (textarea.form) {
+ on(textarea.form, "submit", save);
+ // Deplorable hack to make the submit method do the right thing.
+ if (!options.leaveSubmitMethodAlone) {
+ var form = textarea.form, realSubmit = form.submit;
+ try {
+ var wrappedSubmit = form.submit = function() {
+ save();
+ form.submit = realSubmit;
+ form.submit();
+ form.submit = wrappedSubmit;
+ };
+ } catch(e) {}
+ }
+ }
+
+ options.finishInit = function(cm) {
+ cm.save = save;
+ cm.getTextArea = function() { return textarea; };
+ cm.toTextArea = function() {
+ cm.toTextArea = isNaN; // Prevent this from being ran twice
+ save();
+ textarea.parentNode.removeChild(cm.getWrapperElement());
+ textarea.style.display = "";
+ if (textarea.form) {
+ off(textarea.form, "submit", save);
+ if (typeof textarea.form.submit == "function")
+ textarea.form.submit = realSubmit;
+ }
+ };
+ };
+
+ textarea.style.display = "none";
+ var cm = CodeMirror(function(node) {
+ textarea.parentNode.insertBefore(node, textarea.nextSibling);
+ }, options);
+ return cm;
+ };
+
+ // STRING STREAM
+
+ // Fed to the mode parsers, provides helper functions to make
+ // parsers more succinct.
+
+ 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;},
+ sol: function() {return this.pos == this.lineStart;},
+ peek: function() {return this.string.charAt(this.pos) || undefined;},
+ next: function() {
+ if (this.pos < this.string.length)
+ return this.string.charAt(this.pos++);
+ },
+ eat: function(match) {
+ var ch = this.string.charAt(this.pos);
+ if (typeof match == "string") var ok = ch == match;
+ else var ok = ch && (match.test ? match.test(ch) : match(ch));
+ if (ok) {++this.pos; return ch;}
+ },
+ eatWhile: function(match) {
+ var start = this.pos;
+ while (this.eat(match)){}
+ return this.pos > start;
+ },
+ eatSpace: function() {
+ var start = this.pos;
+ while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) ++this.pos;
+ return this.pos > start;
+ },
+ skipToEnd: function() {this.pos = this.string.length;},
+ skipTo: function(ch) {
+ var found = this.string.indexOf(ch, this.pos);
+ if (found > -1) {this.pos = found; return true;}
+ },
+ backUp: function(n) {this.pos -= n;},
+ column: function() {
+ if (this.lastColumnPos < this.start) {
+ this.lastColumnValue = countColumn(this.string, this.start, this.tabSize, this.lastColumnPos, this.lastColumnValue);
+ this.lastColumnPos = this.start;
+ }
+ return this.lastColumnValue - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0);
+ },
+ indentation: function() {
+ return countColumn(this.string, null, this.tabSize) -
+ (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0);
+ },
+ match: function(pattern, consume, caseInsensitive) {
+ if (typeof pattern == "string") {
+ var cased = function(str) {return caseInsensitive ? str.toLowerCase() : str;};
+ var substr = this.string.substr(this.pos, pattern.length);
+ if (cased(substr) == cased(pattern)) {
+ if (consume !== false) this.pos += pattern.length;
+ return true;
+ }
+ } else {
+ var match = this.string.slice(this.pos).match(pattern);
+ if (match && match.index > 0) return null;
+ if (match && consume !== false) this.pos += match[0].length;
+ return match;
+ }
+ },
+ current: function(){return this.string.slice(this.start, this.pos);},
+ hideFirstChars: function(n, inner) {
+ this.lineStart += n;
+ try { return inner(); }
+ finally { this.lineStart -= n; }
+ }
+ };
+
+ // TEXTMARKERS
+
+ // 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 nextMarkerId = 0;
+
+ var TextMarker = CodeMirror.TextMarker = function(doc, type) {
+ this.lines = [];
+ this.type = type;
+ this.doc = doc;
+ this.id = ++nextMarkerId;
+ };
+ eventMixin(TextMarker);
+
+ // Clear the marker.
+ TextMarker.prototype.clear = function() {
+ if (this.explicitlyCleared) return;
+ var cm = this.doc.cm, withOp = cm && !cm.curOp;
+ if (withOp) startOperation(cm);
+ if (hasHandler(this, "clear")) {
+ var found = this.find();
+ if (found) signalLater(this, "clear", found.from, found.to);
+ }
+ var min = null, max = null;
+ for (var i = 0; i < this.lines.length; ++i) {
+ var line = this.lines[i];
+ var span = getMarkedSpanFor(line.markedSpans, this);
+ 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 && 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(this.lines[i]), len = lineLength(visual);
+ if (len > cm.display.maxLineLength) {
+ cm.display.maxLine = visual;
+ cm.display.maxLineLength = len;
+ cm.display.maxLineChanged = true;
+ }
+ }
+
+ 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.doc);
+ }
+ if (cm) signalLater(cm, "markerCleared", cm, this);
+ if (withOp) endOperation(cm);
+ if (this.parent) this.parent.clear();
+ };
+
+ // 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) {
+ 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;
+ }
+ }
+ 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(-1, true), widget = this, cm = this.doc.cm;
+ if (!pos || !cm) return;
+ 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;
+ }
+ 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) {
+ if (!this.lines.length && this.doc.cm) {
+ var op = this.doc.cm.curOp;
+ if (!op.maybeHiddenMarkers || indexOf(op.maybeHiddenMarkers, this) == -1)
+ (op.maybeUnhiddenMarkers || (op.maybeUnhiddenMarkers = [])).push(this);
+ }
+ this.lines.push(line);
+ };
+ TextMarker.prototype.detachLine = function(line) {
+ this.lines.splice(indexOf(this.lines, line), 1);
+ if (!this.lines.length && this.doc.cm) {
+ var op = this.doc.cm.curOp;
+ (op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this);
+ }
+ };
+
+ // 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), diff = cmp(from, to);
+ if (options) copyObj(options, marker, 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.widgetNode = elt("span", [marker.replacedWith], "CodeMirror-widget");
+ if (!options.handleMouseEvents) marker.widgetNode.setAttribute("cm-ignore-events", "true");
+ if (options.insertLeft) marker.widgetNode.insertLeft = true;
+ }
+ if (marker.collapsed) {
+ if (conflictingCollapsedRange(doc, from.line, from, to, marker) ||
+ from.line != to.line && conflictingCollapsedRange(doc, to.line, from, to, marker))
+ throw new Error("Inserting collapsed marker partially overlapping an existing one");
+ sawCollapsedSpans = true;
+ }
+
+ if (marker.addToHistory)
+ 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(line) == cm.display.maxLine)
+ updateMaxLine = true;
+ if (marker.collapsed && curLine != from.line) updateLineHeight(line, 0);
+ 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);
+ });
+
+ if (marker.clearOnEnter) on(marker, "beforeCursorEnter", function() { marker.clear(); });
+
+ if (marker.readOnly) {
+ sawReadOnlySpans = true;
+ if (doc.history.done.length || doc.history.undone.length)
+ doc.clearHistory();
+ }
+ if (marker.collapsed) {
+ marker.id = ++nextMarkerId;
+ marker.atomic = true;
+ }
+ if (cm) {
+ // Sync editor state
+ if (updateMaxLine) cm.curOp.updateMaxLine = true;
+ if (marker.collapsed)
+ regChange(cm, from.line, to.line + 1);
+ else if (marker.className || marker.title || marker.startStyle || marker.endStyle || marker.css)
+ 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
+
+ // 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; i < markers.length; ++i)
+ markers[i].parent = this;
+ };
+ eventMixin(SharedTextMarker);
+
+ SharedTextMarker.prototype.clear = function() {
+ if (this.explicitlyCleared) return;
+ this.explicitlyCleared = true;
+ for (var i = 0; i < this.markers.length; ++i)
+ this.markers[i].clear();
+ signalLater(this, "clear");
+ };
+ 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.widgetNode;
+ linkedDocs(doc, function(doc) {
+ 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;
+ primary = lst(markers);
+ });
+ return new SharedTextMarker(markers, primary);
+ }
+
+ function findSharedMarkers(doc) {
+ return doc.findMarks(Pos(doc.first, 0), doc.clipPos(Pos(doc.lastLine())),
+ function(m) { return m.parent; });
+ }
+
+ function copySharedMarkers(doc, markers) {
+ for (var i = 0; i < markers.length; i++) {
+ var marker = markers[i], pos = marker.find();
+ var mFrom = doc.clipPos(pos.from), mTo = doc.clipPos(pos.to);
+ if (cmp(mFrom, mTo)) {
+ var subMark = markText(doc, mFrom, mTo, marker.primary, marker.primary.type);
+ marker.markers.push(subMark);
+ subMark.parent = marker;
+ }
+ }
+ }
+
+ function detachSharedMarkers(markers) {
+ for (var i = 0; i < markers.length; i++) {
+ var marker = markers[i], linked = [marker.primary.doc];;
+ linkedDocs(marker.primary.doc, function(d) { linked.push(d); });
+ for (var j = 0; j < marker.markers.length; j++) {
+ var subMarker = marker.markers[j];
+ if (indexOf(linked, subMarker.doc) == -1) {
+ subMarker.parent = null;
+ marker.markers.splice(j--, 1);
+ }
+ }
+ }
+ }
+
+ // 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(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(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) {
+ if (change.full) return null;
+ 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 = 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);
+
+ // Next, merge those two ends
+ var sameLine = change.text.length == 1, offset = lst(change.text).length + (sameLine ? startCh : 0);
+ if (first) {
+ // Fix up .to properties of first
+ for (var i = 0; i < first.length; ++i) {
+ var span = first[i];
+ if (span.to == null) {
+ var found = getMarkedSpanFor(last, span.marker);
+ if (!found) span.to = startCh;
+ else if (sameLine) span.to = found.to == null ? null : found.to + offset;
+ }
+ }
+ }
+ if (last) {
+ // Fix up .from in last (or move them into first in case of sameLine)
+ for (var i = 0; i < last.length; ++i) {
+ var span = last[i];
+ if (span.to != null) span.to += offset;
+ if (span.from == null) {
+ var found = getMarkedSpanFor(first, span.marker);
+ if (!found) {
+ span.from = offset;
+ if (sameLine) (first || (first = [])).push(span);
+ }
+ } else {
+ span.from += offset;
+ if (sameLine) (first || (first = [])).push(span);
+ }
+ }
+ }
+ // Make sure we didn't create any zero-length spans
+ if (first) first = clearEmptySpans(first);
+ if (last && last != first) last = clearEmptySpans(last);
+
+ var newMarkers = [first];
+ if (!sameLine) {
+ // Fill gap with whole-line-spans
+ var gap = change.text.length - 2, gapMarkers;
+ if (gap > 0 && first)
+ for (var i = 0; i < first.length; ++i)
+ if (first[i].to == null)
+ (gapMarkers || (gapMarkers = [])).push(new MarkedSpan(first[i].marker, null, null));
+ for (var i = 0; i < gap; ++i)
+ newMarkers.push(gapMarkers);
+ newMarkers.push(last);
+ }
+ 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];
+ if (span.from != null && span.from == span.to && span.marker.clearWhenEmpty !== false)
+ spans.splice(i--, 1);
+ }
+ if (!spans.length) return null;
+ 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);
+ if (!old) return stretched;
+ if (!stretched) return old;
+
+ for (var i = 0; i < old.length; ++i) {
+ var oldCur = old[i], stretchCur = stretched[i];
+ if (oldCur && stretchCur) {
+ spans: for (var j = 0; j < stretchCur.length; ++j) {
+ var span = stretchCur[j];
+ for (var k = 0; k < oldCur.length; ++k)
+ if (oldCur[k].marker == span.marker) continue spans;
+ oldCur.push(span);
+ }
+ } else if (stretchCur) {
+ old[i] = stretchCur;
+ }
+ }
+ 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) {
+ if (line.markedSpans) for (var i = 0; i < line.markedSpans.length; ++i) {
+ var mark = line.markedSpans[i].marker;
+ if (mark.readOnly && (!markers || indexOf(markers, mark) == -1))
+ (markers || (markers = [])).push(mark);
+ }
+ });
+ 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(0);
+ for (var j = 0; j < parts.length; ++j) {
+ var p = parts[j];
+ 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 (dto > 0 || !mk.inclusiveRight && !dto)
+ newParts.push({from: m.to, to: p.to});
+ parts.splice.apply(parts, newParts);
+ j += newParts.length - 1;
+ }
+ }
+ 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;
+ var aPos = a.find(), bPos = b.find();
+ var fromCmp = cmp(aPos.from, bPos.from) || extraLeft(a) - extraLeft(b);
+ if (fromCmp) return -fromCmp;
+ var toCmp = cmp(aPos.to, bPos.to) || extraRight(a) - extraRight(b);
+ if (toCmp) return toCmp;
+ 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) {
+ sp = sps[i];
+ if (sp.marker.collapsed && (start ? sp.from : sp.to) == null &&
+ (!found || compareCollapsedMarkers(found, sp.marker) < 0))
+ found = sp.marker;
+ }
+ return found;
+ }
+ 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(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;
+ if (fromCmp <= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.to, from) >= 0 : cmp(found.to, from) > 0) ||
+ fromCmp >= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.from, to) <= 0 : cmp(found.from, to) < 0))
+ return true;
+ }
+ }
+
+ // 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 = 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.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(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.widgetNode && sp.from == span.to &&
+ (sp.to == null || sp.to != span.from) &&
+ (sp.marker.inclusiveLeft || span.marker.inclusiveRight) &&
+ lineIsHiddenInner(doc, line, sp)) return true;
+ }
+ }
+
+ // LINE WIDGETS
+
+ // Line widgets are block elements displayed above or below a line.
+
+ var LineWidget = CodeMirror.LineWidget = function(doc, node, options) {
+ if (options) for (var opt in options) if (options.hasOwnProperty(opt))
+ this[opt] = options[opt];
+ this.doc = doc;
+ this.node = node;
+ };
+ eventMixin(LineWidget);
+
+ function adjustScrollWhenAboveVisible(cm, line, diff) {
+ if (heightAtLine(line) < ((cm.curOp && cm.curOp.scrollTop) || cm.doc.scrollTop))
+ addToScrollPos(cm, null, diff);
+ }
+
+ LineWidget.prototype.clear = function() {
+ var cm = this.doc.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) line.widgets = null;
+ var height = widgetHeight(this);
+ updateLineHeight(line, Math.max(0, line.height - height));
+ if (cm) runInOp(cm, function() {
+ adjustScrollWhenAboveVisible(cm, line, -height);
+ regLineChange(cm, no, "widget");
+ });
+ };
+ LineWidget.prototype.changed = function() {
+ var oldH = this.height, cm = this.doc.cm, line = this.line;
+ this.height = null;
+ var diff = widgetHeight(this) - oldH;
+ if (!diff) return;
+ updateLineHeight(line, line.height + diff);
+ if (cm) runInOp(cm, function() {
+ cm.curOp.forceUpdate = true;
+ adjustScrollWhenAboveVisible(cm, line, diff);
+ });
+ };
+
+ function widgetHeight(widget) {
+ if (widget.height != null) return widget.height;
+ var cm = widget.doc.cm;
+ if (!cm) return 0;
+ if (!contains(document.body, widget.node)) {
+ var parentStyle = "position: relative;";
+ if (widget.coverGutter)
+ parentStyle += "margin-left: -" + cm.display.gutters.offsetWidth + "px;";
+ if (widget.noHScroll)
+ parentStyle += "width: " + cm.display.wrapper.clientWidth + "px;";
+ removeChildrenAndAdd(cm.display.measure, elt("div", [widget.node], null, parentStyle));
+ }
+ return widget.height = widget.node.parentNode.offsetHeight;
+ }
+
+ function addLineWidget(doc, handle, node, options) {
+ var widget = new LineWidget(doc, node, options);
+ var cm = doc.cm;
+ if (cm && widget.noHScroll) cm.display.alignWidgets = true;
+ changeLine(doc, 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 (cm && !lineIsHidden(doc, line)) {
+ var aboveVisible = heightAtLine(line) < doc.scrollTop;
+ updateLineHeight(line, line.height + widgetHeight(widget));
+ if (aboveVisible) addToScrollPos(cm, null, widget.height);
+ cm.curOp.forceUpdate = true;
+ }
+ return true;
+ });
+ return widget;
+ }
+
+ // LINE DATA STRUCTURE
+
+ // Line objects. These hold state related to a line, including
+ // highlighting info (the styles array).
+ var Line = CodeMirror.Line = function(text, markedSpans, estimateHeight) {
+ this.text = text;
+ attachMarkedSpans(this, markedSpans);
+ this.height = estimateHeight ? estimateHeight(this) : 1;
+ };
+ 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;
+ if (line.styles) line.styles = null;
+ if (line.order != null) line.order = null;
+ detachMarkedSpans(line);
+ attachMarkedSpans(line, markedSpans);
+ var estHeight = estimateHeight ? estimateHeight(line) : 1;
+ 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);
+ }
+
+ function extractLineClasses(type, output) {
+ if (type) for (;;) {
+ var lineClass = type.match(/(?:^|\s+)line-(background-)?(\S+)/);
+ if (!lineClass) break;
+ type = type.slice(0, lineClass.index) + type.slice(lineClass.index + lineClass[0].length);
+ var prop = lineClass[1] ? "bgClass" : "textClass";
+ if (output[prop] == null)
+ output[prop] = lineClass[2];
+ else if (!(new RegExp("(?:^|\s)" + lineClass[2] + "(?:$|\s)")).test(output[prop]))
+ output[prop] += " " + lineClass[2];
+ }
+ return type;
+ }
+
+ function callBlankLine(mode, state) {
+ if (mode.blankLine) return mode.blankLine(state);
+ if (!mode.innerMode) return;
+ var inner = CodeMirror.innerMode(mode, state);
+ if (inner.mode.blankLine) return inner.mode.blankLine(inner.state);
+ }
+
+ function readToken(mode, stream, state, inner) {
+ for (var i = 0; i < 10; i++) {
+ if (inner) inner[0] = CodeMirror.innerMode(mode, state).mode;
+ var style = mode.token(stream, state);
+ if (stream.pos > stream.start) return style;
+ }
+ throw new Error("Mode " + mode.name + " failed to advance stream.");
+ }
+
+ // Utility for getTokenAt and getLineTokens
+ function takeToken(cm, pos, precise, asArray) {
+ function getObj(copy) {
+ return {start: stream.start, end: stream.pos,
+ string: stream.current(),
+ type: style || null,
+ state: copy ? copyState(doc.mode, state) : state};
+ }
+
+ var doc = cm.doc, mode = doc.mode, style;
+ pos = clipPos(doc, pos);
+ var line = getLine(doc, pos.line), state = getStateBefore(cm, pos.line, precise);
+ var stream = new StringStream(line.text, cm.options.tabSize), tokens;
+ if (asArray) tokens = [];
+ while ((asArray || stream.pos < pos.ch) && !stream.eol()) {
+ stream.start = stream.pos;
+ style = readToken(mode, stream, state);
+ if (asArray) tokens.push(getObj(true));
+ }
+ return asArray ? tokens : getObj();
+ }
+
+ // Run the given mode's parser over a line, calling f for each token.
+ function runMode(cm, text, mode, state, f, lineClasses, forceToEnd) {
+ var flattenSpans = mode.flattenSpans;
+ if (flattenSpans == null) flattenSpans = cm.options.flattenSpans;
+ var curStart = 0, curStyle = null;
+ var stream = new StringStream(text, cm.options.tabSize), style;
+ var inner = cm.options.addModeClass && [null];
+ if (text == "") extractLineClasses(callBlankLine(mode, state), lineClasses);
+ while (!stream.eol()) {
+ if (stream.pos > cm.options.maxHighlightLength) {
+ flattenSpans = false;
+ if (forceToEnd) processLine(cm, text, state, stream.pos);
+ stream.pos = text.length;
+ style = null;
+ } else {
+ style = extractLineClasses(readToken(mode, stream, state, inner), lineClasses);
+ }
+ if (inner) {
+ var mName = inner[0].name;
+ if (mName) style = "m-" + (style ? mName + " " + style : mName);
+ }
+ if (!flattenSpans || curStyle != style) {
+ while (curStart < stream.start) {
+ curStart = Math.min(stream.start, curStart + 50000);
+ f(curStart, curStyle);
+ }
+ curStyle = style;
+ }
+ stream.start = stream.pos;
+ }
+ while (curStart < stream.pos) {
+ // Webkit seems to refuse to render text nodes longer than 57444 characters
+ var pos = Math.min(stream.pos, curStart + 50000);
+ f(pos, curStyle);
+ curStart = pos;
+ }
+ }
+
+ // 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).
+ var st = [cm.state.modeGen], lineClasses = {};
+ // Compute the base array of styles
+ runMode(cm, line.text, cm.doc.mode, state, function(end, style) {
+ st.push(end, style);
+ }, lineClasses, forceToEnd);
+
+ // Run overlays, adjust style array.
+ for (var o = 0; o < cm.state.overlays.length; ++o) {
+ var overlay = cm.state.overlays[o], i = 1, at = 0;
+ runMode(cm, line.text, overlay.mode, true, function(end, style) {
+ var start = i;
+ // Ensure there's a token end at the current position, and that i points at it
+ while (at < end) {
+ var i_end = st[i];
+ if (i_end > end)
+ st.splice(i, 1, end, st[i+1], i_end);
+ i += 2;
+ at = Math.min(end, i_end);
+ }
+ if (!style) return;
+ if (overlay.opaque) {
+ st.splice(start, i - start, end, "cm-overlay " + style);
+ i = start + 2;
+ } else {
+ for (; start < i; start += 2) {
+ var cur = st[start+1];
+ st[start+1] = (cur ? cur + " " : "") + "cm-overlay " + style;
+ }
+ }
+ }, lineClasses);
+ }
+
+ return {styles: st, classes: lineClasses.bgClass || lineClasses.textClass ? lineClasses : null};
+ }
+
+ function getLineStyles(cm, line, updateFrontier) {
+ if (!line.styles || line.styles[0] != cm.state.modeGen) {
+ var state = getStateBefore(cm, lineNo(line));
+ var result = highlightLine(cm, line, line.text.length > cm.options.maxHighlightLength ? copyState(cm.doc.mode, state) : state);
+ line.stateAfter = state;
+ line.styles = result.styles;
+ if (result.classes) line.styleClasses = result.classes;
+ else if (line.styleClasses) line.styleClasses = null;
+ if (updateFrontier === cm.doc.frontier) cm.doc.frontier++;
+ }
+ return line.styles;
+ }
+
+ // Lightweight form of highlight -- proceed over this line and
+ // 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);
+ stream.start = stream.pos = startAt || 0;
+ if (text == "") callBlankLine(mode, state);
+ while (!stream.eol()) {
+ readToken(mode, stream, state);
+ stream.start = stream.pos;
+ }
+ }
+
+ // 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, options) {
+ if (!style || /^\s*$/.test(style)) return null;
+ var cache = options.addModeClass ? styleToClassCacheWithMode : styleToClassCache;
+ return cache[style] ||
+ (cache[style] = style.replace(/\S+/g, "cm-$&"));
+ }
+
+ // 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], "CodeMirror-line"), content: content,
+ col: 0, pos: 0, cm: cm,
+ splitSpaces: (ie || webkit) && cm.getOption("lineWrapping")};
+ lineView.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 = buildToken;
+ // Optionally wire in some hacks into the token-rendering
+ // algorithm, to deal with browser quirks.
+ if (hasBadBidiRects(cm.display.measure) && (order = getOrder(line)))
+ builder.addToken = buildTokenBadBidi(builder.addToken, order);
+ builder.map = [];
+ var allowFrontierUpdate = lineView != cm.display.externalMeasured && lineNo(line);
+ insertLineContent(line, builder, getLineStyles(cm, line, allowFrontierUpdate));
+ if (line.styleClasses) {
+ if (line.styleClasses.bgClass)
+ builder.bgClass = joinClasses(line.styleClasses.bgClass, builder.bgClass || "");
+ if (line.styleClasses.textClass)
+ builder.textClass = joinClasses(line.styleClasses.textClass, builder.textClass || "");
+ }
+
+ // 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)));
+
+ // 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({});
+ }
+ }
+
+ // See issue #2901
+ if (webkit) {
+ var last = builder.content.lastChild
+ if (/\bcm-tab\b/.test(last.className) || (last.querySelector && last.querySelector(".cm-tab")))
+ builder.content.className = "cm-tab-wrap-hack";
+ }
+
+ signal(cm, "renderLine", cm, lineView.line, builder.pre);
+ if (builder.pre.className)
+ builder.textClass = joinClasses(builder.pre.className, builder.textClass || "");
+
+ return builder;
+ }
+
+ function defaultSpecialCharPlaceholder(ch) {
+ var token = elt("span", "\u2022", "cm-invalidchar");
+ token.title = "\\u" + ch.charCodeAt(0).toString(16);
+ token.setAttribute("aria-label", token.title);
+ 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, css) {
+ if (!text) return;
+ var displayText = builder.splitSpaces ? text.replace(/ {3,}/g, splitSpaces) : text;
+ var special = builder.cm.state.specialChars, mustWrap = false;
+ if (!special.test(text)) {
+ builder.col += text.length;
+ var content = document.createTextNode(displayText);
+ builder.map.push(builder.pos, builder.pos + text.length, content);
+ if (ie && ie_version < 9) mustWrap = true;
+ builder.pos += text.length;
+ } else {
+ var content = document.createDocumentFragment(), pos = 0;
+ while (true) {
+ special.lastIndex = pos;
+ var m = special.exec(text);
+ var skipped = m ? m.index - pos : text.length - pos;
+ if (skipped) {
+ var txt = document.createTextNode(displayText.slice(pos, pos + skipped));
+ if (ie && ie_version < 9) 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;
+ var txt = content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab"));
+ txt.setAttribute("role", "presentation");
+ txt.setAttribute("cm-text", "\t");
+ builder.col += tabWidth;
+ } else if (m[0] == "\r" || m[0] == "\n") {
+ var txt = content.appendChild(elt("span", m[0] == "\r" ? "\u240d" : "\u2424", "cm-invalidchar"));
+ txt.setAttribute("cm-text", m[0]);
+ builder.col += 1;
+ } else {
+ var txt = builder.cm.options.specialCharPlaceholder(m[0]);
+ txt.setAttribute("cm-text", m[0]);
+ if (ie && ie_version < 9) 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 || mustWrap || css) {
+ var fullStyle = style || "";
+ if (startStyle) fullStyle += startStyle;
+ if (endStyle) fullStyle += endStyle;
+ var token = elt("span", [content], fullStyle, css);
+ if (title) token.title = title;
+ return builder.content.appendChild(token);
+ }
+ builder.content.appendChild(content);
+ }
+
+ function splitSpaces(old) {
+ var out = " ";
+ for (var i = 0; i < old.length - 2; ++i) out += i % 2 ? " " : "\u00a0";
+ out += " ";
+ return out;
+ }
+
+ // 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, css) {
+ 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, css);
+ inner(builder, text.slice(0, part.to - start), style, startStyle, null, title, css);
+ startStyle = null;
+ text = text.slice(part.to - start);
+ start = part.to;
+ }
+ };
+ }
+
+ function buildCollapsedSpan(builder, size, marker, ignoreWidget) {
+ var widget = !ignoreWidget && marker.widgetNode;
+ if (widget) builder.map.push(builder.pos, builder.pos + size, widget);
+ if (!ignoreWidget && builder.cm.display.input.needsContentAttribute) {
+ if (!widget)
+ widget = builder.content.appendChild(document.createElement("span"));
+ widget.setAttribute("cm-marker", marker.id);
+ }
+ if (widget) {
+ builder.cm.display.input.setUneditable(widget);
+ builder.content.appendChild(widget);
+ }
+ builder.pos += size;
+ }
+
+ // Outputs a number of spans to make up a line, taking highlighting
+ // and marked text into account.
+ function insertLineContent(line, builder, styles) {
+ var spans = line.markedSpans, allText = line.text, at = 0;
+ if (!spans) {
+ for (var i = 1; i < styles.length; i+=2)
+ builder.addToken(builder, allText.slice(at, at = styles[i]), interpretTokenStyle(styles[i+1], builder.cm.options));
+ return;
+ }
+
+ var len = allText.length, pos = 0, i = 1, text = "", style, css;
+ var nextChange = 0, spanStyle, spanEndStyle, spanStartStyle, title, collapsed;
+ for (;;) {
+ if (nextChange == pos) { // Update current marker set
+ spanStyle = spanEndStyle = spanStartStyle = title = css = "";
+ collapsed = null; nextChange = Infinity;
+ var foundBookmarks = [], endStyles
+ for (var j = 0; j < spans.length; ++j) {
+ var sp = spans[j], m = sp.marker;
+ if (m.type == "bookmark" && sp.from == pos && m.widgetNode) {
+ foundBookmarks.push(m);
+ } else if (sp.from <= pos && (sp.to == null || sp.to > pos || m.collapsed && sp.to == pos && sp.from == pos)) {
+ if (sp.to != null && sp.to != pos && nextChange > sp.to) {
+ nextChange = sp.to;
+ spanEndStyle = "";
+ }
+ if (m.className) spanStyle += " " + m.className;
+ if (m.css) css = (css ? css + ";" : "") + m.css;
+ if (m.startStyle && sp.from == pos) spanStartStyle += " " + m.startStyle;
+ if (m.endStyle && sp.to == nextChange) (endStyles || (endStyles = [])).push(m.endStyle, sp.to)
+ if (m.title && !title) title = m.title;
+ if (m.collapsed && (!collapsed || compareCollapsedMarkers(collapsed.marker, m) < 0))
+ collapsed = sp;
+ } else if (sp.from > pos && nextChange > sp.from) {
+ nextChange = sp.from;
+ }
+ }
+ if (endStyles) for (var j = 0; j < endStyles.length; j += 2)
+ if (endStyles[j + 1] == nextChange) spanEndStyle += " " + endStyles[j]
+
+ if (!collapsed || collapsed.from == pos) for (var j = 0; j < foundBookmarks.length; ++j)
+ buildCollapsedSpan(builder, 0, foundBookmarks[j]);
+ if (collapsed && (collapsed.from || 0) == pos) {
+ buildCollapsedSpan(builder, (collapsed.to == null ? len + 1 : collapsed.to) - pos,
+ collapsed.marker, collapsed.from == null);
+ if (collapsed.to == null) return;
+ if (collapsed.to == pos) collapsed = false;
+ }
+ }
+ if (pos >= len) break;
+
+ var upto = Math.min(len, nextChange);
+ while (true) {
+ if (text) {
+ var end = pos + text.length;
+ if (!collapsed) {
+ var tokenText = end > upto ? text.slice(0, upto - pos) : text;
+ builder.addToken(builder, tokenText, style ? style + spanStyle : spanStyle,
+ spanStartStyle, pos + tokenText.length == nextChange ? spanEndStyle : "", title, css);
+ }
+ if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;}
+ pos = end;
+ spanStartStyle = "";
+ }
+ text = allText.slice(at, at = styles[i++]);
+ style = interpretTokenStyle(styles[i++], builder.cm.options);
+ }
+ }
+ }
+
+ // DOCUMENT DATA STRUCTURE
+
+ // 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);
+ signalLater(line, "change", line, change);
+ }
+ function linesFor(start, end) {
+ for (var i = start, result = []; i < end; ++i)
+ result.push(new Line(text[i], spansFor(i), estimateHeight));
+ return result;
+ }
+
+ var from = change.from, to = change.to, text = change.text;
+ 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;
+
+ // Adjust the line structure
+ if (change.full) {
+ doc.insert(0, linesFor(0, text.length));
+ doc.remove(text.length, doc.size - text.length);
+ } else if (isWholeLineUpdate(doc, change)) {
+ // This is a whole-line replace. Treated specially to make
+ // sure line objects move the way they are supposed to.
+ var added = linesFor(0, text.length - 1);
+ update(lastLine, lastLine.text, lastSpans);
+ if (nlines) doc.remove(from.line, nlines);
+ if (added.length) doc.insert(from.line, added);
+ } else if (firstLine == lastLine) {
+ if (text.length == 1) {
+ update(firstLine, firstLine.text.slice(0, from.ch) + lastText + firstLine.text.slice(to.ch), lastSpans);
+ } else {
+ var added = linesFor(1, text.length - 1);
+ added.push(new Line(lastText + firstLine.text.slice(to.ch), lastSpans, estimateHeight));
+ update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0));
+ doc.insert(from.line + 1, added);
+ }
+ } else if (text.length == 1) {
+ update(firstLine, firstLine.text.slice(0, from.ch) + text[0] + lastLine.text.slice(to.ch), spansFor(0));
+ doc.remove(from.line + 1, nlines);
+ } else {
+ update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0));
+ update(lastLine, lastText + lastLine.text.slice(to.ch), lastSpans);
+ var added = linesFor(1, text.length - 1);
+ if (nlines > 1) doc.remove(from.line + 1, nlines - 1);
+ doc.insert(from.line + 1, added);
+ }
+
+ signalLater(doc, "change", doc, change);
+ }
+
+ // 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, height = 0; i < lines.length; ++i) {
+ lines[i].parent = this;
+ height += lines[i].height;
+ }
+ this.height = height;
+ }
+
+ 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];
+ this.height -= line.height;
+ cleanUpLine(line);
+ signalLater(line, "delete");
+ }
+ this.lines.splice(at, n);
+ },
+ // Helper used to collapse a small branch into a single leaf.
+ collapse: function(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; 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;
+ }
+ };
+
+ function BranchChunk(children) {
+ this.children = children;
+ var size = 0, height = 0;
+ for (var i = 0; i < children.length; ++i) {
+ var ch = children[i];
+ size += ch.chunkSize(); height += ch.height;
+ ch.parent = this;
+ }
+ this.size = size;
+ this.height = height;
+ this.parent = null;
+ }
+
+ BranchChunk.prototype = {
+ chunkSize: function() { return this.size; },
+ removeInner: function(at, n) {
+ this.size -= n;
+ for (var i = 0; i < this.children.length; ++i) {
+ var child = this.children[i], sz = child.chunkSize();
+ if (at < sz) {
+ var rm = Math.min(n, sz - at), oldHeight = child.height;
+ child.removeInner(at, rm);
+ this.height -= oldHeight - child.height;
+ if (sz == rm) { this.children.splice(i--, 1); child.parent = null; }
+ if ((n -= rm) == 0) break;
+ at = 0;
+ } else at -= sz;
+ }
+ // 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)];
+ this.children[0].parent = this;
+ }
+ },
+ collapse: function(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; i < this.children.length; ++i) {
+ var child = this.children[i], sz = child.chunkSize();
+ if (at <= sz) {
+ child.insertInner(at, lines, height);
+ if (child.lines && child.lines.length > 50) {
+ // To avoid memory thrashing when child.lines is huge (e.g. first view of a large file), it's never spliced.
+ // Instead, small slices are taken. They're taken in order because sequential memory accesses are fastest.
+ var remaining = child.lines.length % 25 + 25
+ for (var pos = remaining; pos < child.lines.length;) {
+ var leaf = new LeafChunk(child.lines.slice(pos, pos += 25));
+ child.height -= leaf.height;
+ this.children.splice(++i, 0, leaf);
+ leaf.parent = this;
+ }
+ child.lines = child.lines.slice(0, remaining);
+ this.maybeSpill();
+ }
+ break;
+ }
+ at -= sz;
+ }
+ },
+ // When a node has grown, check whether it should be split.
+ maybeSpill: function() {
+ if (this.children.length <= 10) return;
+ var me = this;
+ do {
+ var spilled = me.children.splice(me.children.length - 5, 5);
+ var sibling = new BranchChunk(spilled);
+ if (!me.parent) { // Become the parent node
+ var copy = new BranchChunk(me.children);
+ copy.parent = me;
+ me.children = [copy, sibling];
+ me = copy;
+ } else {
+ me.size -= sibling.size;
+ me.height -= sibling.height;
+ var myIndex = indexOf(me.parent.children, me);
+ me.parent.children.splice(myIndex + 1, 0, sibling);
+ }
+ sibling.parent = me.parent;
+ } while (me.children.length > 10);
+ me.parent.maybeSpill();
+ },
+ iterN: function(at, n, op) {
+ 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);
+ if (child.iterN(at, used, op)) return true;
+ if ((n -= used) == 0) break;
+ at = 0;
+ } else at -= sz;
+ }
+ }
+ };
+
+ var nextDocId = 0;
+ var Doc = CodeMirror.Doc = function(text, mode, firstLine, lineSep) {
+ if (!(this instanceof Doc)) return new Doc(text, mode, firstLine, lineSep);
+ if (firstLine == null) firstLine = 0;
+
+ BranchChunk.call(this, [new LeafChunk([new Line("", null)])]);
+ this.first = firstLine;
+ this.scrollTop = this.scrollLeft = 0;
+ this.cantEdit = false;
+ this.cleanGeneration = 1;
+ this.frontier = firstLine;
+ var start = Pos(firstLine, 0);
+ this.sel = simpleSelection(start);
+ this.history = new History(null);
+ this.id = ++nextDocId;
+ this.modeOption = mode;
+ this.lineSep = lineSep;
+ this.extend = false;
+
+ if (typeof text == "string") text = this.splitLines(text);
+ 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; 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 || this.lineSeparator());
+ },
+ 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: this.splitLines(code), origin: "setValue", full: true}, true);
+ setSelection(this, simpleSelection(top));
+ }),
+ replaceRange: function(code, from, to, origin) {
+ from = clipPos(this, from);
+ to = to ? clipPos(this, to) : from;
+ replaceRange(this, code, from, to, origin);
+ },
+ getRange: function(from, to, lineSep) {
+ var lines = getBetween(this, clipPos(this, from), clipPos(this, to));
+ if (lineSep === false) return lines;
+ return lines.join(lineSep || this.lineSeparator());
+ },
+
+ getLine: function(line) {var l = this.getLineHandle(line); return l && l.text;},
+
+ 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(line);
+ },
+
+ lineCount: function() {return this.size;},
+ firstLine: function() {return this.first;},
+ lastLine: function() {return this.first + this.size - 1;},
+
+ clipPos: function(pos) {return clipPos(this, pos);},
+
+ getCursor: function(start) {
+ 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;
+ },
+ listSelections: function() { return this.sel.ranges; },
+ somethingSelected: function() {return this.sel.somethingSelected();},
+
+ setCursor: docMethodOp(function(line, ch, options) {
+ setSimpleSelection(this, clipPos(this, typeof line == "number" ? Pos(line, ch || 0) : line), null, options);
+ }),
+ setSelection: docMethodOp(function(anchor, head, options) {
+ setSimpleSelection(this, clipPos(this, anchor), clipPos(this, head || anchor), options);
+ }),
+ 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) {
+ var heads = map(this.sel.ranges, f);
+ extendSelections(this, clipPosArray(this, heads), 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) {
+ 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 || this.lineSeparator());
+ },
+ 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 || this.lineSeparator());
+ parts[i] = sel;
+ }
+ return parts;
+ },
+ replaceSelection: 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: docMethodOp(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: this.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.extend = val;},
+ getExtending: function() {return this.extend;},
+
+ historySize: function() {
+ 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 = new History(this.history.maxGeneration);},
+
+ markClean: function() {
+ this.cleanGeneration = this.changeGeneration(true);
+ },
+ changeGeneration: function(forceSplit) {
+ if (forceSplit)
+ this.history.lastOp = this.history.lastSelOp = this.history.lastOrigin = null;
+ return this.history.generation;
+ },
+ isClean: function (gen) {
+ return this.history.generation == (gen || this.cleanGeneration);
+ },
+
+ getHistory: function() {
+ return {done: copyHistoryArray(this.history.done),
+ undone: copyHistoryArray(this.history.undone)};
+ },
+ setHistory: function(histData) {
+ 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);
+ },
+
+ addLineClass: docMethodOp(function(handle, where, cls) {
+ return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function(line) {
+ var prop = where == "text" ? "textClass"
+ : where == "background" ? "bgClass"
+ : where == "gutter" ? "gutterClass" : "wrapClass";
+ if (!line[prop]) line[prop] = cls;
+ else if (classTest(cls).test(line[prop])) return false;
+ else line[prop] += " " + cls;
+ return true;
+ });
+ }),
+ removeLineClass: docMethodOp(function(handle, where, cls) {
+ return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function(line) {
+ var prop = where == "text" ? "textClass"
+ : where == "background" ? "bgClass"
+ : where == "gutter" ? "gutterClass" : "wrapClass";
+ var cur = line[prop];
+ if (!cur) return false;
+ else if (cls == null) line[prop] = null;
+ else {
+ var found = cur.match(classTest(cls));
+ if (!found) return false;
+ var end = found.index + found[0].length;
+ line[prop] = cur.slice(0, found.index) + (!found.index || end == cur.length ? "" : " ") + cur.slice(end) || null;
+ }
+ return true;
+ });
+ }),
+
+ addLineWidget: docMethodOp(function(handle, node, options) {
+ return addLineWidget(this, handle, node, options);
+ }),
+ removeLineWidget: function(widget) { widget.clear(); },
+
+ markText: function(from, to, options) {
+ return markText(this, clipPos(this, from), clipPos(this, to), options, options && options.type || "range");
+ },
+ setBookmark: function(pos, options) {
+ var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options),
+ insertLeft: options && options.insertLeft,
+ clearWhenEmpty: false, shared: options && options.shared,
+ handleMouseEvents: options && options.handleMouseEvents};
+ pos = clipPos(this, pos);
+ return markText(this, pos, pos, realOpts, "bookmark");
+ },
+ findMarksAt: function(pos) {
+ pos = clipPos(this, pos);
+ var markers = [], spans = getLine(this, pos.line).markedSpans;
+ if (spans) for (var i = 0; i < spans.length; ++i) {
+ var span = spans[i];
+ if ((span.from == null || span.from <= pos.ch) &&
+ (span.to == null || span.to >= pos.ch))
+ markers.push(span.marker.parent || span.marker);
+ }
+ return markers;
+ },
+ findMarks: function(from, to, filter) {
+ 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 (!(span.to != null && lineNo == from.line && from.ch >= span.to ||
+ span.from == null && lineNo != from.line ||
+ span.from != null && lineNo == to.line && span.from >= to.ch) &&
+ (!filter || filter(span.marker)))
+ found.push(span.marker.parent || span.marker);
+ }
+ ++lineNo;
+ });
+ return found;
+ },
+ getAllMarks: function() {
+ var markers = [];
+ this.iter(function(line) {
+ var sps = line.markedSpans;
+ if (sps) for (var i = 0; i < sps.length; ++i)
+ if (sps[i].from != null) markers.push(sps[i].marker);
+ });
+ return markers;
+ },
+
+ posFromIndex: function(off) {
+ var ch, lineNo = this.first, sepSize = this.lineSeparator().length;
+ this.iter(function(line) {
+ var sz = line.text.length + sepSize;
+ if (sz > off) { ch = off; return true; }
+ off -= sz;
+ ++lineNo;
+ });
+ return clipPos(this, Pos(lineNo, ch));
+ },
+ indexFromPos: function (coords) {
+ coords = clipPos(this, coords);
+ var index = coords.ch;
+ if (coords.line < this.first || coords.ch < 0) return 0;
+ var sepSize = this.lineSeparator().length;
+ this.iter(this.first, coords.line, function (line) {
+ index += line.text.length + sepSize;
+ });
+ return index;
+ },
+
+ copy: function(copyHistory) {
+ var doc = new Doc(getLines(this, this.first, this.first + this.size),
+ this.modeOption, this.first, this.lineSep);
+ doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft;
+ doc.sel = this.sel;
+ doc.extend = false;
+ if (copyHistory) {
+ doc.history.undoDepth = this.history.undoDepth;
+ doc.setHistory(this.getHistory());
+ }
+ return doc;
+ },
+
+ linkedDoc: function(options) {
+ if (!options) options = {};
+ var from = this.first, to = this.first + this.size;
+ if (options.from != null && options.from > from) from = options.from;
+ if (options.to != null && options.to < to) to = options.to;
+ var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from, this.lineSep);
+ if (options.sharedHist) copy.history = this.history;
+ (this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist});
+ copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}];
+ copySharedMarkers(copy, findSharedMarkers(this));
+ return copy;
+ },
+ unlinkDoc: function(other) {
+ if (other instanceof CodeMirror) other = other.doc;
+ if (this.linked) for (var i = 0; i < this.linked.length; ++i) {
+ var link = this.linked[i];
+ if (link.doc != other) continue;
+ this.linked.splice(i, 1);
+ other.unlinkDoc(this);
+ detachSharedMarkers(findSharedMarkers(this));
+ break;
+ }
+ // If the histories were shared, split them again
+ if (other.history == this.history) {
+ var splitIds = [other.id];
+ linkedDocs(other, function(doc) {splitIds.push(doc.id);}, true);
+ other.history = new History(null);
+ other.history.done = copyHistoryArray(this.history.done, splitIds);
+ other.history.undone = copyHistoryArray(this.history.undone, splitIds);
+ }
+ },
+ iterLinkedDocs: function(f) {linkedDocs(this, f);},
+
+ getMode: function() {return this.mode;},
+ getEditor: function() {return this.cm;},
+
+ splitLines: function(str) {
+ if (this.lineSep) return str.split(this.lineSep);
+ return splitLinesAuto(str);
+ },
+ lineSeparator: function() { return this.lineSep || "\n"; }
+ });
+
+ // Public alias.
+ Doc.prototype.eachLine = Doc.prototype.iter;
+
+ // Set up methods on CodeMirror's prototype to redirect to the editor's document.
+ var dontDelegate = "iter insert remove copy getEditor constructor".split(" ");
+ for (var prop in Doc.prototype) if (Doc.prototype.hasOwnProperty(prop) && indexOf(dontDelegate, prop) < 0)
+ CodeMirror.prototype[prop] = (function(method) {
+ return function() {return method.apply(this.doc, arguments);};
+ })(Doc.prototype[prop]);
+
+ 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) {
+ var rel = doc.linked[i];
+ if (rel.doc == skip) continue;
+ var shared = sharedHist && rel.sharedHist;
+ if (sharedHistOnly && !shared) continue;
+ f(rel.doc, shared);
+ propagate(rel.doc, doc, shared);
+ }
+ }
+ 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) findMaxLine(cm);
+ cm.options.mode = doc.modeOption;
+ regChange(cm);
+ }
+
+ // LINE UTILITIES
+
+ // 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; }
+ n -= sz;
+ }
+ }
+ 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) {
+ var text = line.text;
+ if (n == end.line) text = text.slice(0, end.ch);
+ if (n == start.line) text = text.slice(start.ch);
+ out.push(text);
+ ++n;
+ });
+ 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;
+ 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);
+ for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) {
+ for (var i = 0;; ++i) {
+ if (chunk.children[i] == cur) break;
+ no += chunk.children[i].chunkSize();
+ }
+ }
+ 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; i < chunk.children.length; ++i) {
+ var child = chunk.children[i], ch = child.height;
+ if (h < ch) { chunk = child; continue outer; }
+ h -= ch;
+ n += child.chunkSize();
+ }
+ return n;
+ } while (!chunk.lines);
+ for (var i = 0; i < chunk.lines.length; ++i) {
+ var line = chunk.lines[i], lh = line.height;
+ if (h < lh) break;
+ h -= lh;
+ }
+ return n + i;
+ }
+
+
+ // 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) {
+ var line = chunk.lines[i];
+ if (line == lineObj) break;
+ else h += line.height;
+ }
+ for (var p = chunk.parent; p; chunk = p, p = chunk.parent) {
+ for (var i = 0; i < p.children.length; ++i) {
+ var cur = p.children[i];
+ if (cur == chunk) break;
+ else h += cur.height;
+ }
+ }
+ 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);
+ return order;
+ }
+
+ // HISTORY
+
+ 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 = this.lastSelOp = 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 = hist.lastSelOp = 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.lastSelOp ||
+ (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.lastSelOp = 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) {
+ if (line.markedSpans)
+ (existing || (existing = change["spans_" + doc.id] = {}))[n] = line.markedSpans;
+ ++n;
+ });
+ }
+
+ // 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) {
+ if (spans[i].marker.explicitlyCleared) { if (!out) out = spans.slice(0, i); }
+ else if (out) out.push(spans[i]);
+ }
+ 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;
+ for (var i = 0, nw = []; i < change.text.length; ++i)
+ nw.push(removeClearedSpans(found[i]));
+ return nw;
+ }
+
+ // 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, instantiateSel) {
+ for (var i = 0, copy = []; i < events.length; ++i) {
+ 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});
+ if (newGroup) for (var prop in change) if (m = prop.match(/^spans_(\d+)$/)) {
+ if (indexOf(newGroup, Number(m[1])) > -1) {
+ lst(newChanges)[prop] = change[prop];
+ delete change[prop];
+ }
+ }
+ }
+ }
+ return copy;
+ }
+
+ // Rebasing/resetting history to deal with externally-sourced changes
+
+ function rebaseHistSelSingle(pos, from, to, diff) {
+ if (to < pos.line) {
+ pos.line += diff;
+ } else if (from < pos.line) {
+ pos.line = from;
+ pos.ch = 0;
+ }
+ }
+
+ // Tries to rebase an array of history events given a change in the
+ // document. If the change touches the same lines as the event, the
+ // event, and everything 'behind' it, is discarded. If the change is
+ // before the event, the event's positions are updated. Uses a
+ // copy-on-write scheme for the positions, to avoid having to
+ // reallocate them all on every rebase, but also avoid problems with
+ // shared position objects being unsafely updated.
+ 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 (to < cur.from.line) {
+ 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 (!ok) {
+ array.splice(0, i + 1);
+ i = 0;
+ }
+ }
+ }
+
+ function rebaseHist(hist, change) {
+ var from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1;
+ rebaseHistArray(hist.done, from, to, diff);
+ rebaseHistArray(hist.undone, from, to, diff);
+ }
+
+ // EVENT UTILITIES
+
+ // Due to the fact that we still support jurassic IE versions, some
+ // compatibility wrappers are needed.
+
+ var e_preventDefault = CodeMirror.e_preventDefault = function(e) {
+ if (e.preventDefault) e.preventDefault();
+ else e.returnValue = false;
+ };
+ 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;
+ }
+ 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) {
+ var b = e.which;
+ if (b == null) {
+ if (e.button & 1) b = 1;
+ else if (e.button & 2) b = 3;
+ else if (e.button & 4) b = 2;
+ }
+ if (mac && e.ctrlKey && b == 1) b = 3;
+ return b;
+ }
+
+ // EVENT HANDLING
+
+ // 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)
+ emitter.attachEvent("on" + type, f);
+ else {
+ var map = emitter._handlers || (emitter._handlers = {});
+ var arr = map[type] || (map[type] = []);
+ arr.push(f);
+ }
+ };
+
+ var noHandlers = []
+ function getHandlers(emitter, type, copy) {
+ var arr = emitter._handlers && emitter._handlers[type]
+ if (copy) return arr && arr.length > 0 ? arr.slice() : noHandlers
+ else return arr || noHandlers
+ }
+
+ var off = CodeMirror.off = function(emitter, type, f) {
+ if (emitter.removeEventListener)
+ emitter.removeEventListener(type, f, false);
+ else if (emitter.detachEvent)
+ emitter.detachEvent("on" + type, f);
+ else {
+ var handlers = getHandlers(emitter, type, false)
+ for (var i = 0; i < handlers.length; ++i)
+ if (handlers[i] == f) { handlers.splice(i, 1); break; }
+ }
+ };
+
+ var signal = CodeMirror.signal = function(emitter, type /*, values...*/) {
+ var handlers = getHandlers(emitter, type, true)
+ if (!handlers.length) return;
+ var args = Array.prototype.slice.call(arguments, 2);
+ for (var i = 0; i < handlers.length; ++i) handlers[i].apply(null, args);
+ };
+
+ var orphanDelayedCallbacks = null;
+
+ // 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.
+ function signalLater(emitter, type /*, values...*/) {
+ var arr = getHandlers(emitter, type, false)
+ if (!arr.length) return;
+ var args = Array.prototype.slice.call(arguments, 2), list;
+ if (operationGroup) {
+ list = operationGroup.delayedCallbacks;
+ } else if (orphanDelayedCallbacks) {
+ list = orphanDelayedCallbacks;
+ } else {
+ list = orphanDelayedCallbacks = [];
+ setTimeout(fireOrphanDelayed, 0);
+ }
+ function bnd(f) {return function(){f.apply(null, args);};};
+ for (var i = 0; i < arr.length; ++i)
+ list.push(bnd(arr[i]));
+ }
+
+ function fireOrphanDelayed() {
+ var delayed = orphanDelayedCallbacks;
+ orphanDelayedCallbacks = null;
+ 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) {
+ if (typeof e == "string")
+ e = {type: e, preventDefault: function() { this.defaultPrevented = true; }};
+ signal(cm, override || e.type, cm, e);
+ return e_defaultPrevented(e) || e.codemirrorIgnore;
+ }
+
+ function signalCursorActivity(cm) {
+ var arr = cm._handlers && cm._handlers.cursorActivity;
+ if (!arr) return;
+ var set = cm.curOp.cursorActivityHandlers || (cm.curOp.cursorActivityHandlers = []);
+ for (var i = 0; i < arr.length; ++i) if (indexOf(set, arr[i]) == -1)
+ set.push(arr[i]);
+ }
+
+ function hasHandler(emitter, type) {
+ return getHandlers(emitter, type).length > 0
+ }
+
+ // 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);};
+ }
+
+ // MISC UTILITIES
+
+ // Number of pixels added to scroller and sizer to hide scrollbar
+ var scrollerGap = 30;
+
+ // Returned or thrown by various protocols to signal 'I'm not
+ // 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);
+ };
+
+ // Counts the column offset in a string, taking tabs into account.
+ // Used mostly to find indentation.
+ 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;;) {
+ 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.
+ var findColumn = CodeMirror.findColumn = function(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;
+ }
+ }
+
+ var spaceStrs = [""];
+ function spaceStr(n) {
+ while (spaceStrs.length <= n)
+ spaceStrs.push(lst(spaceStrs) + " ");
+ return spaceStrs[n];
+ }
+
+ function lst(arr) { return arr[arr.length-1]; }
+
+ 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(array, elt) {
+ for (var i = 0; i < array.length; ++i)
+ if (array[i] == elt) return i;
+ return -1;
+ }
+ function map(array, f) {
+ var out = [];
+ for (var i = 0; i < array.length; i++) out[i] = f(array[i], i);
+ return out;
+ }
+
+ function nothing() {}
+
+ function createObj(base, props) {
+ var inst;
+ if (Object.create) {
+ inst = Object.create(base);
+ } else {
+ nothing.prototype = base;
+ inst = new nothing();
+ }
+ if (props) copyObj(props, inst);
+ return inst;
+ };
+
+ function copyObj(obj, target, overwrite) {
+ if (!target) target = {};
+ for (var prop in obj)
+ if (obj.hasOwnProperty(prop) && (overwrite !== false || !target.hasOwnProperty(prop)))
+ target[prop] = obj[prop];
+ return target;
+ }
+
+ function bind(f) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ return function(){return f.apply(null, args);};
+ }
+
+ var nonASCIISingleCaseWordChar = /[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/;
+ var isWordCharBasic = CodeMirror.isWordChar = function(ch) {
+ return /\w/.test(ch) || ch > "\x80" &&
+ (ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch));
+ };
+ function isWordChar(ch, helper) {
+ if (!helper) return isWordCharBasic(ch);
+ if (helper.source.indexOf("\\w") > -1 && isWordCharBasic(ch)) return true;
+ return helper.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); }
+
+ // DOM UTILITIES
+
+ function elt(tag, content, className, style) {
+ var e = document.createElement(tag);
+ if (className) e.className = className;
+ if (style) e.style.cssText = style;
+ 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, endNode) {
+ var r = document.createRange();
+ r.setEnd(endNode || node, end);
+ r.setStart(node, start);
+ return r;
+ };
+ else range = function(node, start, end) {
+ var r = document.body.createTextRange();
+ try { r.moveToElementText(node.parentNode); }
+ catch(e) { return r; }
+ 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);
+ return e;
+ }
+
+ function removeChildrenAndAdd(parent, e) {
+ return removeChildren(parent).appendChild(e);
+ }
+
+ var contains = CodeMirror.contains = function(parent, child) {
+ if (child.nodeType == 3) // Android browser always returns false when child is a textnode
+ child = child.parentNode;
+ if (parent.contains)
+ return parent.contains(child);
+ do {
+ if (child.nodeType == 11) child = child.host;
+ if (child == parent) return true;
+ } while (child = child.parentNode);
+ };
+
+ function activeElt() {
+ var activeElement = document.activeElement;
+ while (activeElement && activeElement.root && activeElement.root.activeElement)
+ activeElement = activeElement.root.activeElement;
+ return activeElement;
+ }
+ // Older versions of IE throws unspecified error when touching
+ // document.activeElement in some cases (during loading, in iframe)
+ if (ie && ie_version < 11) activeElt = function() {
+ try { return document.activeElement; }
+ catch(e) { return document.body; }
+ };
+
+ function classTest(cls) { return new RegExp("(^|\\s)" + cls + "(?:$|\\s)\\s*"); }
+ var rmClass = CodeMirror.rmClass = function(node, cls) {
+ var current = node.className;
+ var match = classTest(cls).exec(current);
+ if (match) {
+ var after = current.slice(match.index + match[0].length);
+ node.className = current.slice(0, match.index) + (after ? match[1] + after : "");
+ }
+ };
+ var addClass = CodeMirror.addClass = function(node, cls) {
+ var current = node.className;
+ if (!classTest(cls).test(current)) node.className += (current ? " " : "") + cls;
+ };
+ function joinClasses(a, b) {
+ var as = a.split(" ");
+ for (var i = 0; i < as.length; i++)
+ if (as[i] && !classTest(as[i]).test(b)) b += " " + as[i];
+ return b;
+ }
+
+ // WINDOW-WIDE EVENTS
+
+ // These must be handled carefully, because naively registering a
+ // handler for each editor will cause the editors to never be
+ // garbage collected.
+
+ function forEachCodeMirror(f) {
+ if (!document.body.getElementsByClassName) return;
+ var byClass = document.body.getElementsByClassName("CodeMirror");
+ for (var i = 0; i < byClass.length; i++) {
+ var cm = byClass[i].CodeMirror;
+ if (cm) f(cm);
+ }
+ }
+
+ var globalsRegistered = false;
+ function ensureGlobalHandlers() {
+ if (globalsRegistered) return;
+ registerGlobalHandlers();
+ globalsRegistered = true;
+ }
+ function registerGlobalHandlers() {
+ // When the window resizes, we need to refresh active editors.
+ var resizeTimer;
+ on(window, "resize", function() {
+ if (resizeTimer == null) resizeTimer = setTimeout(function() {
+ resizeTimer = null;
+ forEachCodeMirror(onResize);
+ }, 100);
+ });
+ // When the window loses focus, we want to show the editor as blurred
+ on(window, "blur", function() {
+ forEachCodeMirror(onBlur);
+ });
+ }
+
+ // FEATURE DETECTION
+
+ // Detect drag-and-drop
+ 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 && ie_version < 9) return false;
+ var div = elt('div');
+ return "draggable" in div || "dragDrop" in div;
+ }();
+
+ var zwspSupported;
+ function zeroWidthElement(measure) {
+ if (zwspSupported == null) {
+ 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 && ie_version < 8);
+ }
+ var node = zwspSupported ? elt("span", "\u200b") :
+ elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px");
+ node.setAttribute("cm-text", "");
+ return node;
+ }
+
+ // 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 || r0.left == r0.right) return false; // Safari returns null in some cases (#2780)
+ 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 splitLinesAuto = 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);
+ if (nl == -1) nl = string.length;
+ var line = string.slice(pos, string.charAt(nl - 1) == "\r" ? nl - 1 : nl);
+ var rt = line.indexOf("\r");
+ if (rt != -1) {
+ result.push(line.slice(0, rt));
+ pos += rt + 1;
+ } else {
+ result.push(line);
+ pos = nl + 1;
+ }
+ }
+ return result;
+ } : function(string){return string.split(/\r\n?|\n/);};
+
+ var hasSelection = window.getSelection ? function(te) {
+ try { return te.selectionStart != te.selectionEnd; }
+ catch(e) { return false; }
+ } : function(te) {
+ try {var range = te.ownerDocument.selection.createRange();}
+ catch(e) {}
+ if (!range || range.parentElement() != te) return false;
+ return range.compareEndPoints("StartToEnd", range) != 0;
+ };
+
+ var hasCopyEvent = (function() {
+ var e = elt("div");
+ if ("oncopy" in e) return true;
+ e.setAttribute("oncopy", "return;");
+ return typeof e.oncopy == "function";
+ })();
+
+ var badZoomedRects = null;
+ function hasBadZoomedRects(measure) {
+ if (badZoomedRects != null) return badZoomedRects;
+ var node = removeChildrenAndAdd(measure, elt("span", "x"));
+ var normal = node.getBoundingClientRect();
+ var fromRange = range(node, 0, 1).getBoundingClientRect();
+ return badZoomedRects = Math.abs(normal.left - fromRange.left) > 1;
+ }
+
+ // KEY NAMES
+
+ var keyNames = CodeMirror.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",
+ 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert",
+ 46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod",
+ 106: "*", 107: "=", 109: "-", 110: ".", 111: "/", 127: "Delete",
+ 173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\",
+ 221: "]", 222: "'", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete",
+ 63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert"
+ };
+ (function() {
+ // Number keys
+ for (var i = 0; i < 10; i++) keyNames[i + 48] = keyNames[i + 96] = String(i);
+ // Alphabetic keys
+ for (var i = 65; i <= 90; i++) keyNames[i] = String.fromCharCode(i);
+ // Function keys
+ for (var i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i;
+ })();
+
+ // BIDI HELPERS
+
+ function iterateBidiSections(order, from, to, f) {
+ if (!order) return f(from, to, "ltr");
+ var found = false;
+ for (var i = 0; i < order.length; ++i) {
+ var part = order[i];
+ if (part.from < to && part.to > from || from == to && part.to == from) {
+ f(Math.max(part.from, from), Math.min(part.to, to), part.level == 1 ? "rtl" : "ltr");
+ found = true;
+ }
+ }
+ if (!found) f(from, to, "ltr");
+ }
+
+ function bidiLeft(part) { return part.level % 2 ? part.to : part.from; }
+ function bidiRight(part) { return part.level % 2 ? part.from : part.to; }
+
+ function lineLeft(line) { var order = getOrder(line); return order ? bidiLeft(order[0]) : 0; }
+ function lineRight(line) {
+ var order = getOrder(line);
+ if (!order) return line.text.length;
+ return bidiRight(lst(order));
+ }
+
+ function lineStart(cm, lineN) {
+ var line = getLine(cm.doc, lineN);
+ 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 = 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 == null ? lineNo(line) : lineN, ch);
+ }
+ function lineStartSmart(cm, pos) {
+ var start = lineStart(cm, pos.line);
+ var line = getLine(cm.doc, start.line);
+ var order = getOrder(line);
+ if (!order || order[0].level == 0) {
+ var firstNonWS = Math.max(0, line.text.search(/\S/));
+ var inWS = pos.line == start.line && pos.ch <= firstNonWS && pos.ch;
+ return Pos(start.line, inWS ? 0 : firstNonWS);
+ }
+ return start;
+ }
+
+ function compareBidiLevel(order, a, b) {
+ var linedir = order[0].level;
+ if (a == linedir) return true;
+ if (b == linedir) return false;
+ return a < b;
+ }
+ var bidiOther;
+ function getBidiPartAt(order, pos) {
+ bidiOther = null;
+ for (var i = 0, found; i < order.length; ++i) {
+ var cur = order[i];
+ if (cur.from < pos && cur.to > pos) return i;
+ if ((cur.from == pos || cur.to == pos)) {
+ if (found == null) {
+ found = i;
+ } else if (compareBidiLevel(order, cur.level, order[found].level)) {
+ if (cur.from != cur.to) bidiOther = found;
+ return i;
+ } else {
+ if (cur.from != cur.to) bidiOther = i;
+ return found;
+ }
+ }
+ }
+ return found;
+ }
+
+ function moveInLine(line, pos, dir, byUnit) {
+ if (!byUnit) return pos + dir;
+ do pos += dir;
+ while (pos > 0 && isExtendingChar(line.text.charAt(pos)));
+ return pos;
+ }
+
+ // 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);
+ var pos = getBidiPartAt(bidi, start), part = bidi[pos];
+ var target = moveInLine(line, start, part.level % 2 ? -dir : dir, byUnit);
+
+ for (;;) {
+ if (target > part.from && target < part.to) return target;
+ if (target == part.from || target == part.to) {
+ if (getBidiPartAt(bidi, target) == pos) return target;
+ part = bidi[pos += dir];
+ return (dir > 0) == part.level % 2 ? part.to : part.from;
+ } else {
+ part = bidi[pos += dir];
+ if (!part) return null;
+ if ((dir > 0) == part.level % 2)
+ target = moveInLine(line, part.to, -1, byUnit);
+ else
+ target = moveInLine(line, part.from, 1, byUnit);
+ }
+ }
+ }
+
+ function moveLogically(line, start, dir, byUnit) {
+ var target = start + dir;
+ if (byUnit) while (target > 0 && isExtendingChar(line.text.charAt(target))) target += dir;
+ return target < 0 || target > line.text.length ? null : target;
+ }
+
+ // Bidirectional ordering algorithm
+ // See http://unicode.org/reports/tr9/tr9-13.html for the algorithm
+ // that this (partially) implements.
+
+ // One-char codes used for character types:
+ // L (L): Left-to-Right
+ // R (R): Right-to-Left
+ // r (AL): Right-to-Left Arabic
+ // 1 (EN): European Number
+ // + (ES): European Number Separator
+ // % (ET): European Number Terminator
+ // n (AN): Arabic Number
+ // , (CS): Common Number Separator
+ // m (NSM): Non-Spacing Mark
+ // b (BN): Boundary Neutral
+ // s (B): Paragraph Separator
+ // t (S): Segment Separator
+ // w (WS): Whitespace
+ // N (ON): Other Neutrals
+
+ // Returns null if characters are ordered as they appear
+ // (left-to-right), or an array of sections ({from, to, level}
+ // 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%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN";
+ // Character types for codepoints 0x600 to 0x6ff
+ var arabicTypes = "rrrrrrrrrrrr,rNNmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmrrrrrrrnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmNmmmm";
+ function charType(code) {
+ if (code <= 0xf7) return lowTypes.charAt(code);
+ else if (0x590 <= code && code <= 0x5f4) 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";
+ }
+
+ var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/;
+ var isNeutral = /[stwN]/, isStrong = /[LRr]/, countsAsLeft = /[Lb1n]/, countsAsNum = /[1n]/;
+ // 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 = [];
+ for (var i = 0, type; i < len; ++i)
+ types.push(type = charType(str.charCodeAt(i)));
+
+ // W1. Examine each non-spacing mark (NSM) in the level run, and
+ // change the type of the NSM to the type of the previous
+ // character. If the NSM is at the start of the level run, it will
+ // get the type of sor.
+ for (var i = 0, prev = outerType; i < len; ++i) {
+ var type = types[i];
+ if (type == "m") types[i] = prev;
+ else prev = type;
+ }
+
+ // W2. Search backwards from each instance of a European number
+ // until the first strong type (R, L, AL, or sor) is found. If an
+ // AL is found, change the type of the European number to Arabic
+ // number.
+ // W3. Change all ALs to R.
+ for (var i = 0, cur = outerType; i < len; ++i) {
+ var type = types[i];
+ if (type == "1" && cur == "r") types[i] = "n";
+ else if (isStrong.test(type)) { cur = type; if (type == "r") types[i] = "R"; }
+ }
+
+ // W4. A single European separator between two European numbers
+ // changes to a European number. A single common separator between
+ // two numbers of the same type changes to that type.
+ for (var i = 1, prev = types[0]; i < len - 1; ++i) {
+ var type = types[i];
+ if (type == "+" && prev == "1" && types[i+1] == "1") types[i] = "1";
+ else if (type == "," && prev == types[i+1] &&
+ (prev == "1" || prev == "n")) types[i] = prev;
+ prev = type;
+ }
+
+ // W5. A sequence of European terminators adjacent to European
+ // numbers changes to all European numbers.
+ // W6. Otherwise, separators and terminators change to Other
+ // Neutral.
+ for (var i = 0; i < len; ++i) {
+ var type = types[i];
+ if (type == ",") types[i] = "N";
+ else if (type == "%") {
+ for (var end = i + 1; end < len && types[end] == "%"; ++end) {}
+ var replace = (i && types[i-1] == "!") || (end < len && types[end] == "1") ? "1" : "N";
+ for (var j = i; j < end; ++j) types[j] = replace;
+ i = end - 1;
+ }
+ }
+
+ // W7. Search backwards from each instance of a European number
+ // until the first strong type (R, L, or sor) is found. If an L is
+ // found, then change the type of the European number to L.
+ for (var i = 0, cur = outerType; i < len; ++i) {
+ var type = types[i];
+ if (cur == "L" && type == "1") types[i] = "L";
+ else if (isStrong.test(type)) cur = type;
+ }
+
+ // N1. A sequence of neutrals takes the direction of the
+ // surrounding strong text if the text on both sides has the same
+ // direction. European and Arabic numbers act as if they were R in
+ // terms of their influence on neutrals. Start-of-level-run (sor)
+ // and end-of-level-run (eor) are used at level run boundaries.
+ // N2. Any remaining neutrals take the embedding direction.
+ for (var i = 0; i < len; ++i) {
+ if (isNeutral.test(types[i])) {
+ for (var end = i + 1; end < len && isNeutral.test(types[end]); ++end) {}
+ var before = (i ? types[i-1] : outerType) == "L";
+ var after = (end < len ? types[end] : outerType) == "L";
+ var replace = before || after ? "L" : "R";
+ for (var j = i; j < end; ++j) types[j] = replace;
+ i = end - 1;
+ }
+ }
+
+ // Here we depart from the documented algorithm, in order to avoid
+ // building up an actual levels array. Since there are only three
+ // levels (0, 1, 2) in an implementation that doesn't take
+ // explicit embedding into account, we can build up the order on
+ // the fly, without following the level-based algorithm.
+ var order = [], m;
+ for (var i = 0; i < len;) {
+ if (countsAsLeft.test(types[i])) {
+ var start = i;
+ for (++i; i < len && countsAsLeft.test(types[i]); ++i) {}
+ 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, new BidiSpan(1, pos, j));
+ var nstart = j;
+ for (++j; j < i && countsAsNum.test(types[j]); ++j) {}
+ order.splice(at, 0, new BidiSpan(2, nstart, j));
+ pos = j;
+ } else ++j;
+ }
+ 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(new BidiSpan(0, 0, m[0].length));
+ }
+ if (lst(order).level == 1 && (m = str.match(/\s+$/))) {
+ lst(order).to -= m[0].length;
+ order.push(new BidiSpan(0, len - m[0].length, len));
+ }
+ if (order[0].level == 2)
+ order.unshift(new BidiSpan(1, order[0].to, order[0].to));
+ if (order[0].level != lst(order).level)
+ order.push(new BidiSpan(order[0].level, len, len));
+
+ return order;
+ };
+ })();
+
+ // THE END
+
+ CodeMirror.version = "5.16.0";
+
+ return CodeMirror;
+});
diff --git a/devtools/client/sourceeditor/codemirror/mode/clike/clike.js b/devtools/client/sourceeditor/codemirror/mode/clike/clike.js
new file mode 100644
index 000000000..a37921fda
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/mode/clike/clike.js
@@ -0,0 +1,786 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+"use strict";
+
+function Context(indented, column, type, info, align, prev) {
+ this.indented = indented;
+ this.column = column;
+ this.type = type;
+ this.info = info;
+ this.align = align;
+ this.prev = prev;
+}
+function pushContext(state, col, type, info) {
+ var indent = state.indented;
+ if (state.context && state.context.type != "statement" && type != "statement")
+ indent = state.context.indented;
+ return state.context = new Context(indent, col, type, info, null, state.context);
+}
+function popContext(state) {
+ var t = state.context.type;
+ if (t == ")" || t == "]" || t == "}")
+ state.indented = state.context.indented;
+ return state.context = state.context.prev;
+}
+
+function typeBefore(stream, state, pos) {
+ if (state.prevToken == "variable" || state.prevToken == "variable-3") return true;
+ if (/\S(?:[^- ]>|[*\]])\s*$|\*$/.test(stream.string.slice(0, pos))) return true;
+ if (state.typeAtEndOfLine && stream.column() == stream.indentation()) return true;
+}
+
+function isTopScope(context) {
+ for (;;) {
+ if (!context || context.type == "top") return true;
+ if (context.type == "}" && context.prev.info != "namespace") return false;
+ context = context.prev;
+ }
+}
+
+CodeMirror.defineMode("clike", function(config, parserConfig) {
+ var indentUnit = config.indentUnit,
+ statementIndentUnit = parserConfig.statementIndentUnit || indentUnit,
+ dontAlignCalls = parserConfig.dontAlignCalls,
+ keywords = parserConfig.keywords || {},
+ types = parserConfig.types || {},
+ builtin = parserConfig.builtin || {},
+ blockKeywords = parserConfig.blockKeywords || {},
+ defKeywords = parserConfig.defKeywords || {},
+ atoms = parserConfig.atoms || {},
+ hooks = parserConfig.hooks || {},
+ multiLineStrings = parserConfig.multiLineStrings,
+ indentStatements = parserConfig.indentStatements !== false,
+ indentSwitch = parserConfig.indentSwitch !== false,
+ namespaceSeparator = parserConfig.namespaceSeparator,
+ isPunctuationChar = parserConfig.isPunctuationChar || /[\[\]{}\(\),;\:\.]/,
+ numberStart = parserConfig.numberStart || /[\d\.]/,
+ number = parserConfig.number || /^(?:0x[a-f\d]+|0b[01]+|(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?)(u|ll?|l|f)?/i,
+ isOperatorChar = parserConfig.isOperatorChar || /[+\-*&%=<>!?|\/]/,
+ endStatement = parserConfig.endStatement || /^[;:,]$/;
+
+ var curPunc, isDefKeyword;
+
+ function tokenBase(stream, state) {
+ var ch = stream.next();
+ if (hooks[ch]) {
+ var result = hooks[ch](stream, state);
+ if (result !== false) return result;
+ }
+ if (ch == '"' || ch == "'") {
+ state.tokenize = tokenString(ch);
+ return state.tokenize(stream, state);
+ }
+ if (isPunctuationChar.test(ch)) {
+ curPunc = ch;
+ return null;
+ }
+ if (numberStart.test(ch)) {
+ stream.backUp(1)
+ if (stream.match(number)) return "number"
+ stream.next()
+ }
+ if (ch == "/") {
+ if (stream.eat("*")) {
+ state.tokenize = tokenComment;
+ return tokenComment(stream, state);
+ }
+ if (stream.eat("/")) {
+ stream.skipToEnd();
+ return "comment";
+ }
+ }
+ if (isOperatorChar.test(ch)) {
+ while (!stream.match(/^\/[\/*]/, false) && stream.eat(isOperatorChar)) {}
+ return "operator";
+ }
+ stream.eatWhile(/[\w\$_\xa1-\uffff]/);
+ if (namespaceSeparator) while (stream.match(namespaceSeparator))
+ stream.eatWhile(/[\w\$_\xa1-\uffff]/);
+
+ var cur = stream.current();
+ if (contains(keywords, cur)) {
+ if (contains(blockKeywords, cur)) curPunc = "newstatement";
+ if (contains(defKeywords, cur)) isDefKeyword = true;
+ return "keyword";
+ }
+ if (contains(types, cur)) return "variable-3";
+ if (contains(builtin, cur)) {
+ if (contains(blockKeywords, cur)) curPunc = "newstatement";
+ return "builtin";
+ }
+ if (contains(atoms, cur)) return "atom";
+ return "variable";
+ }
+
+ function tokenString(quote) {
+ return function(stream, state) {
+ var escaped = false, next, end = false;
+ while ((next = stream.next()) != null) {
+ if (next == quote && !escaped) {end = true; break;}
+ escaped = !escaped && next == "\\";
+ }
+ if (end || !(escaped || multiLineStrings))
+ state.tokenize = null;
+ return "string";
+ };
+ }
+
+ function tokenComment(stream, state) {
+ var maybeEnd = false, ch;
+ while (ch = stream.next()) {
+ if (ch == "/" && maybeEnd) {
+ state.tokenize = null;
+ break;
+ }
+ maybeEnd = (ch == "*");
+ }
+ return "comment";
+ }
+
+ function maybeEOL(stream, state) {
+ if (parserConfig.typeFirstDefinitions && stream.eol() && isTopScope(state.context))
+ state.typeAtEndOfLine = typeBefore(stream, state, stream.pos)
+ }
+
+ // Interface
+
+ return {
+ startState: function(basecolumn) {
+ return {
+ tokenize: null,
+ context: new Context((basecolumn || 0) - indentUnit, 0, "top", null, false),
+ indented: 0,
+ startOfLine: true,
+ prevToken: null
+ };
+ },
+
+ token: function(stream, state) {
+ var ctx = state.context;
+ if (stream.sol()) {
+ if (ctx.align == null) ctx.align = false;
+ state.indented = stream.indentation();
+ state.startOfLine = true;
+ }
+ if (stream.eatSpace()) { maybeEOL(stream, state); return null; }
+ curPunc = isDefKeyword = null;
+ var style = (state.tokenize || tokenBase)(stream, state);
+ if (style == "comment" || style == "meta") return style;
+ if (ctx.align == null) ctx.align = true;
+
+ if (endStatement.test(curPunc)) while (state.context.type == "statement") popContext(state);
+ else if (curPunc == "{") pushContext(state, stream.column(), "}");
+ else if (curPunc == "[") pushContext(state, stream.column(), "]");
+ else if (curPunc == "(") pushContext(state, stream.column(), ")");
+ else if (curPunc == "}") {
+ while (ctx.type == "statement") ctx = popContext(state);
+ if (ctx.type == "}") ctx = popContext(state);
+ while (ctx.type == "statement") ctx = popContext(state);
+ }
+ else if (curPunc == ctx.type) popContext(state);
+ else if (indentStatements &&
+ (((ctx.type == "}" || ctx.type == "top") && curPunc != ";") ||
+ (ctx.type == "statement" && curPunc == "newstatement"))) {
+ pushContext(state, stream.column(), "statement", stream.current());
+ }
+
+ if (style == "variable" &&
+ ((state.prevToken == "def" ||
+ (parserConfig.typeFirstDefinitions && typeBefore(stream, state, stream.start) &&
+ isTopScope(state.context) && stream.match(/^\s*\(/, false)))))
+ style = "def";
+
+ if (hooks.token) {
+ var result = hooks.token(stream, state, style);
+ if (result !== undefined) style = result;
+ }
+
+ if (style == "def" && parserConfig.styleDefs === false) style = "variable";
+
+ state.startOfLine = false;
+ state.prevToken = isDefKeyword ? "def" : style || curPunc;
+ maybeEOL(stream, state);
+ return style;
+ },
+
+ indent: function(state, textAfter) {
+ if (state.tokenize != tokenBase && state.tokenize != null || state.typeAtEndOfLine) return CodeMirror.Pass;
+ var ctx = state.context, firstChar = textAfter && textAfter.charAt(0);
+ if (ctx.type == "statement" && firstChar == "}") ctx = ctx.prev;
+ if (parserConfig.dontIndentStatements)
+ while (ctx.type == "statement" && parserConfig.dontIndentStatements.test(ctx.info))
+ ctx = ctx.prev
+ if (hooks.indent) {
+ var hook = hooks.indent(state, ctx, textAfter);
+ if (typeof hook == "number") return hook
+ }
+ var closing = firstChar == ctx.type;
+ var switchBlock = ctx.prev && ctx.prev.info == "switch";
+ if (parserConfig.allmanIndentation && /[{(]/.test(firstChar)) {
+ while (ctx.type != "top" && ctx.type != "}") ctx = ctx.prev
+ return ctx.indented
+ }
+ if (ctx.type == "statement")
+ return ctx.indented + (firstChar == "{" ? 0 : statementIndentUnit);
+ if (ctx.align && (!dontAlignCalls || ctx.type != ")"))
+ return ctx.column + (closing ? 0 : 1);
+ if (ctx.type == ")" && !closing)
+ return ctx.indented + statementIndentUnit;
+
+ return ctx.indented + (closing ? 0 : indentUnit) +
+ (!closing && switchBlock && !/^(?:case|default)\b/.test(textAfter) ? indentUnit : 0);
+ },
+
+ electricInput: indentSwitch ? /^\s*(?:case .*?:|default:|\{\}?|\})$/ : /^\s*[{}]$/,
+ blockCommentStart: "/*",
+ blockCommentEnd: "*/",
+ lineComment: "//",
+ fold: "brace"
+ };
+});
+
+ function words(str) {
+ var obj = {}, words = str.split(" ");
+ for (var i = 0; i < words.length; ++i) obj[words[i]] = true;
+ return obj;
+ }
+ function contains(words, word) {
+ if (typeof words === "function") {
+ return words(word);
+ } else {
+ return words.propertyIsEnumerable(word);
+ }
+ }
+ var cKeywords = "auto if break case register continue return default do sizeof " +
+ "static else struct switch extern typedef union for goto while enum const volatile";
+ var cTypes = "int long char short double float unsigned signed void size_t ptrdiff_t";
+
+ function cppHook(stream, state) {
+ if (!state.startOfLine) return false
+ for (var ch, next = null; ch = stream.peek();) {
+ if (ch == "\\" && stream.match(/^.$/)) {
+ next = cppHook
+ break
+ } else if (ch == "/" && stream.match(/^\/[\/\*]/, false)) {
+ break
+ }
+ stream.next()
+ }
+ state.tokenize = next
+ return "meta"
+ }
+
+ function pointerHook(_stream, state) {
+ if (state.prevToken == "variable-3") return "variable-3";
+ return false;
+ }
+
+ function cpp14Literal(stream) {
+ stream.eatWhile(/[\w\.']/);
+ return "number";
+ }
+
+ function cpp11StringHook(stream, state) {
+ stream.backUp(1);
+ // Raw strings.
+ if (stream.match(/(R|u8R|uR|UR|LR)/)) {
+ var match = stream.match(/"([^\s\\()]{0,16})\(/);
+ if (!match) {
+ return false;
+ }
+ state.cpp11RawStringDelim = match[1];
+ state.tokenize = tokenRawString;
+ return tokenRawString(stream, state);
+ }
+ // Unicode strings/chars.
+ if (stream.match(/(u8|u|U|L)/)) {
+ if (stream.match(/["']/, /* eat */ false)) {
+ return "string";
+ }
+ return false;
+ }
+ // Ignore this hook.
+ stream.next();
+ return false;
+ }
+
+ function cppLooksLikeConstructor(word) {
+ var lastTwo = /(\w+)::(\w+)$/.exec(word);
+ return lastTwo && lastTwo[1] == lastTwo[2];
+ }
+
+ // C#-style strings where "" escapes a quote.
+ function tokenAtString(stream, state) {
+ var next;
+ while ((next = stream.next()) != null) {
+ if (next == '"' && !stream.eat('"')) {
+ state.tokenize = null;
+ break;
+ }
+ }
+ return "string";
+ }
+
+ // C++11 raw string literal is <prefix>"<delim>( anything )<delim>", where
+ // <delim> can be a string up to 16 characters long.
+ function tokenRawString(stream, state) {
+ // Escape characters that have special regex meanings.
+ var delim = state.cpp11RawStringDelim.replace(/[^\w\s]/g, '\\$&');
+ var match = stream.match(new RegExp(".*?\\)" + delim + '"'));
+ if (match)
+ state.tokenize = null;
+ else
+ stream.skipToEnd();
+ return "string";
+ }
+
+ function def(mimes, mode) {
+ if (typeof mimes == "string") mimes = [mimes];
+ var words = [];
+ function add(obj) {
+ if (obj) for (var prop in obj) if (obj.hasOwnProperty(prop))
+ words.push(prop);
+ }
+ add(mode.keywords);
+ add(mode.types);
+ add(mode.builtin);
+ add(mode.atoms);
+ if (words.length) {
+ mode.helperType = mimes[0];
+ CodeMirror.registerHelper("hintWords", mimes[0], words);
+ }
+
+ for (var i = 0; i < mimes.length; ++i)
+ CodeMirror.defineMIME(mimes[i], mode);
+ }
+
+ def(["text/x-csrc", "text/x-c", "text/x-chdr"], {
+ name: "clike",
+ keywords: words(cKeywords),
+ types: words(cTypes + " bool _Complex _Bool float_t double_t intptr_t intmax_t " +
+ "int8_t int16_t int32_t int64_t uintptr_t uintmax_t uint8_t uint16_t " +
+ "uint32_t uint64_t"),
+ blockKeywords: words("case do else for if switch while struct"),
+ defKeywords: words("struct"),
+ typeFirstDefinitions: true,
+ atoms: words("null true false"),
+ hooks: {"#": cppHook, "*": pointerHook},
+ modeProps: {fold: ["brace", "include"]}
+ });
+
+ def(["text/x-c++src", "text/x-c++hdr"], {
+ name: "clike",
+ keywords: words(cKeywords + " asm dynamic_cast namespace reinterpret_cast try explicit new " +
+ "static_cast typeid catch operator template typename class friend private " +
+ "this using const_cast inline public throw virtual delete mutable protected " +
+ "alignas alignof constexpr decltype nullptr noexcept thread_local final " +
+ "static_assert override"),
+ types: words(cTypes + " bool wchar_t"),
+ blockKeywords: words("catch class do else finally for if struct switch try while"),
+ defKeywords: words("class namespace struct enum union"),
+ typeFirstDefinitions: true,
+ atoms: words("true false null"),
+ dontIndentStatements: /^template$/,
+ hooks: {
+ "#": cppHook,
+ "*": pointerHook,
+ "u": cpp11StringHook,
+ "U": cpp11StringHook,
+ "L": cpp11StringHook,
+ "R": cpp11StringHook,
+ "0": cpp14Literal,
+ "1": cpp14Literal,
+ "2": cpp14Literal,
+ "3": cpp14Literal,
+ "4": cpp14Literal,
+ "5": cpp14Literal,
+ "6": cpp14Literal,
+ "7": cpp14Literal,
+ "8": cpp14Literal,
+ "9": cpp14Literal,
+ token: function(stream, state, style) {
+ if (style == "variable" && stream.peek() == "(" &&
+ (state.prevToken == ";" || state.prevToken == null ||
+ state.prevToken == "}") &&
+ cppLooksLikeConstructor(stream.current()))
+ return "def";
+ }
+ },
+ namespaceSeparator: "::",
+ modeProps: {fold: ["brace", "include"]}
+ });
+
+ def("text/x-java", {
+ name: "clike",
+ keywords: words("abstract assert break case catch class const continue default " +
+ "do else enum extends final finally float for goto if implements import " +
+ "instanceof interface native new package private protected public " +
+ "return static strictfp super switch synchronized this throw throws transient " +
+ "try volatile while"),
+ types: words("byte short int long float double boolean char void Boolean Byte Character Double Float " +
+ "Integer Long Number Object Short String StringBuffer StringBuilder Void"),
+ blockKeywords: words("catch class do else finally for if switch try while"),
+ defKeywords: words("class interface package enum"),
+ typeFirstDefinitions: true,
+ atoms: words("true false null"),
+ endStatement: /^[;:]$/,
+ number: /^(?:0x[a-f\d_]+|0b[01_]+|(?:[\d_]+\.?\d*|\.\d+)(?:e[-+]?[\d_]+)?)(u|ll?|l|f)?/i,
+ hooks: {
+ "@": function(stream) {
+ stream.eatWhile(/[\w\$_]/);
+ return "meta";
+ }
+ },
+ modeProps: {fold: ["brace", "import"]}
+ });
+
+ def("text/x-csharp", {
+ name: "clike",
+ keywords: words("abstract as async await base break case catch checked class const continue" +
+ " default delegate do else enum event explicit extern finally fixed for" +
+ " foreach goto if implicit in interface internal is lock namespace new" +
+ " operator out override params private protected public readonly ref return sealed" +
+ " sizeof stackalloc static struct switch this throw try typeof unchecked" +
+ " unsafe using virtual void volatile while add alias ascending descending dynamic from get" +
+ " global group into join let orderby partial remove select set value var yield"),
+ types: words("Action Boolean Byte Char DateTime DateTimeOffset Decimal Double Func" +
+ " Guid Int16 Int32 Int64 Object SByte Single String Task TimeSpan UInt16 UInt32" +
+ " UInt64 bool byte char decimal double short int long object" +
+ " sbyte float string ushort uint ulong"),
+ blockKeywords: words("catch class do else finally for foreach if struct switch try while"),
+ defKeywords: words("class interface namespace struct var"),
+ typeFirstDefinitions: true,
+ atoms: words("true false null"),
+ hooks: {
+ "@": function(stream, state) {
+ if (stream.eat('"')) {
+ state.tokenize = tokenAtString;
+ return tokenAtString(stream, state);
+ }
+ stream.eatWhile(/[\w\$_]/);
+ return "meta";
+ }
+ }
+ });
+
+ function tokenTripleString(stream, state) {
+ var escaped = false;
+ while (!stream.eol()) {
+ if (!escaped && stream.match('"""')) {
+ state.tokenize = null;
+ break;
+ }
+ escaped = stream.next() == "\\" && !escaped;
+ }
+ return "string";
+ }
+
+ def("text/x-scala", {
+ name: "clike",
+ keywords: words(
+
+ /* scala */
+ "abstract case catch class def do else extends final finally for forSome if " +
+ "implicit import lazy match new null object override package private protected return " +
+ "sealed super this throw trait try type val var while with yield _ : = => <- <: " +
+ "<% >: # @ " +
+
+ /* package scala */
+ "assert assume require print println printf readLine readBoolean readByte readShort " +
+ "readChar readInt readLong readFloat readDouble " +
+
+ ":: #:: "
+ ),
+ types: words(
+ "AnyVal App Application Array BufferedIterator BigDecimal BigInt Char Console Either " +
+ "Enumeration Equiv Error Exception Fractional Function IndexedSeq Int Integral Iterable " +
+ "Iterator List Map Numeric Nil NotNull Option Ordered Ordering PartialFunction PartialOrdering " +
+ "Product Proxy Range Responder Seq Serializable Set Specializable Stream StringBuilder " +
+ "StringContext Symbol Throwable Traversable TraversableOnce Tuple Unit Vector " +
+
+ /* package java.lang */
+ "Boolean Byte Character CharSequence Class ClassLoader Cloneable Comparable " +
+ "Compiler Double Exception Float Integer Long Math Number Object Package Pair Process " +
+ "Runtime Runnable SecurityManager Short StackTraceElement StrictMath String " +
+ "StringBuffer System Thread ThreadGroup ThreadLocal Throwable Triple Void"
+ ),
+ multiLineStrings: true,
+ blockKeywords: words("catch class do else finally for forSome if match switch try while"),
+ defKeywords: words("class def object package trait type val var"),
+ atoms: words("true false null"),
+ indentStatements: false,
+ indentSwitch: false,
+ hooks: {
+ "@": function(stream) {
+ stream.eatWhile(/[\w\$_]/);
+ return "meta";
+ },
+ '"': function(stream, state) {
+ if (!stream.match('""')) return false;
+ state.tokenize = tokenTripleString;
+ return state.tokenize(stream, state);
+ },
+ "'": function(stream) {
+ stream.eatWhile(/[\w\$_\xa1-\uffff]/);
+ return "atom";
+ },
+ "=": function(stream, state) {
+ var cx = state.context
+ if (cx.type == "}" && cx.align && stream.eat(">")) {
+ state.context = new Context(cx.indented, cx.column, cx.type, cx.info, null, cx.prev)
+ return "operator"
+ } else {
+ return false
+ }
+ }
+ },
+ modeProps: {closeBrackets: {triples: '"'}}
+ });
+
+ function tokenKotlinString(tripleString){
+ return function (stream, state) {
+ var escaped = false, next, end = false;
+ while (!stream.eol()) {
+ if (!tripleString && !escaped && stream.match('"') ) {end = true; break;}
+ if (tripleString && stream.match('"""')) {end = true; break;}
+ next = stream.next();
+ if(!escaped && next == "$" && stream.match('{'))
+ stream.skipTo("}");
+ escaped = !escaped && next == "\\" && !tripleString;
+ }
+ if (end || !tripleString)
+ state.tokenize = null;
+ return "string";
+ }
+ }
+
+ def("text/x-kotlin", {
+ name: "clike",
+ keywords: words(
+ /*keywords*/
+ "package as typealias class interface this super val " +
+ "var fun for is in This throw return " +
+ "break continue object if else while do try when !in !is as? " +
+
+ /*soft keywords*/
+ "file import where by get set abstract enum open inner override private public internal " +
+ "protected catch finally out final vararg reified dynamic companion constructor init " +
+ "sealed field property receiver param sparam lateinit data inline noinline tailrec " +
+ "external annotation crossinline const operator infix"
+ ),
+ types: words(
+ /* package java.lang */
+ "Boolean Byte Character CharSequence Class ClassLoader Cloneable Comparable " +
+ "Compiler Double Exception Float Integer Long Math Number Object Package Pair Process " +
+ "Runtime Runnable SecurityManager Short StackTraceElement StrictMath String " +
+ "StringBuffer System Thread ThreadGroup ThreadLocal Throwable Triple Void"
+ ),
+ intendSwitch: false,
+ indentStatements: false,
+ multiLineStrings: true,
+ blockKeywords: words("catch class do else finally for if where try while enum"),
+ defKeywords: words("class val var object package interface fun"),
+ atoms: words("true false null this"),
+ hooks: {
+ '"': function(stream, state) {
+ state.tokenize = tokenKotlinString(stream.match('""'));
+ return state.tokenize(stream, state);
+ }
+ },
+ modeProps: {closeBrackets: {triples: '"'}}
+ });
+
+ def(["x-shader/x-vertex", "x-shader/x-fragment"], {
+ name: "clike",
+ keywords: words("sampler1D sampler2D sampler3D samplerCube " +
+ "sampler1DShadow sampler2DShadow " +
+ "const attribute uniform varying " +
+ "break continue discard return " +
+ "for while do if else struct " +
+ "in out inout"),
+ types: words("float int bool void " +
+ "vec2 vec3 vec4 ivec2 ivec3 ivec4 bvec2 bvec3 bvec4 " +
+ "mat2 mat3 mat4"),
+ blockKeywords: words("for while do if else struct"),
+ builtin: words("radians degrees sin cos tan asin acos atan " +
+ "pow exp log exp2 sqrt inversesqrt " +
+ "abs sign floor ceil fract mod min max clamp mix step smoothstep " +
+ "length distance dot cross normalize ftransform faceforward " +
+ "reflect refract matrixCompMult " +
+ "lessThan lessThanEqual greaterThan greaterThanEqual " +
+ "equal notEqual any all not " +
+ "texture1D texture1DProj texture1DLod texture1DProjLod " +
+ "texture2D texture2DProj texture2DLod texture2DProjLod " +
+ "texture3D texture3DProj texture3DLod texture3DProjLod " +
+ "textureCube textureCubeLod " +
+ "shadow1D shadow2D shadow1DProj shadow2DProj " +
+ "shadow1DLod shadow2DLod shadow1DProjLod shadow2DProjLod " +
+ "dFdx dFdy fwidth " +
+ "noise1 noise2 noise3 noise4"),
+ atoms: words("true false " +
+ "gl_FragColor gl_SecondaryColor gl_Normal gl_Vertex " +
+ "gl_MultiTexCoord0 gl_MultiTexCoord1 gl_MultiTexCoord2 gl_MultiTexCoord3 " +
+ "gl_MultiTexCoord4 gl_MultiTexCoord5 gl_MultiTexCoord6 gl_MultiTexCoord7 " +
+ "gl_FogCoord gl_PointCoord " +
+ "gl_Position gl_PointSize gl_ClipVertex " +
+ "gl_FrontColor gl_BackColor gl_FrontSecondaryColor gl_BackSecondaryColor " +
+ "gl_TexCoord gl_FogFragCoord " +
+ "gl_FragCoord gl_FrontFacing " +
+ "gl_FragData gl_FragDepth " +
+ "gl_ModelViewMatrix gl_ProjectionMatrix gl_ModelViewProjectionMatrix " +
+ "gl_TextureMatrix gl_NormalMatrix gl_ModelViewMatrixInverse " +
+ "gl_ProjectionMatrixInverse gl_ModelViewProjectionMatrixInverse " +
+ "gl_TexureMatrixTranspose gl_ModelViewMatrixInverseTranspose " +
+ "gl_ProjectionMatrixInverseTranspose " +
+ "gl_ModelViewProjectionMatrixInverseTranspose " +
+ "gl_TextureMatrixInverseTranspose " +
+ "gl_NormalScale gl_DepthRange gl_ClipPlane " +
+ "gl_Point gl_FrontMaterial gl_BackMaterial gl_LightSource gl_LightModel " +
+ "gl_FrontLightModelProduct gl_BackLightModelProduct " +
+ "gl_TextureColor gl_EyePlaneS gl_EyePlaneT gl_EyePlaneR gl_EyePlaneQ " +
+ "gl_FogParameters " +
+ "gl_MaxLights gl_MaxClipPlanes gl_MaxTextureUnits gl_MaxTextureCoords " +
+ "gl_MaxVertexAttribs gl_MaxVertexUniformComponents gl_MaxVaryingFloats " +
+ "gl_MaxVertexTextureImageUnits gl_MaxTextureImageUnits " +
+ "gl_MaxFragmentUniformComponents gl_MaxCombineTextureImageUnits " +
+ "gl_MaxDrawBuffers"),
+ indentSwitch: false,
+ hooks: {"#": cppHook},
+ modeProps: {fold: ["brace", "include"]}
+ });
+
+ def("text/x-nesc", {
+ name: "clike",
+ keywords: words(cKeywords + "as atomic async call command component components configuration event generic " +
+ "implementation includes interface module new norace nx_struct nx_union post provides " +
+ "signal task uses abstract extends"),
+ types: words(cTypes),
+ blockKeywords: words("case do else for if switch while struct"),
+ atoms: words("null true false"),
+ hooks: {"#": cppHook},
+ modeProps: {fold: ["brace", "include"]}
+ });
+
+ def("text/x-objectivec", {
+ name: "clike",
+ keywords: words(cKeywords + "inline restrict _Bool _Complex _Imaginary BOOL Class bycopy byref id IMP in " +
+ "inout nil oneway out Protocol SEL self super atomic nonatomic retain copy readwrite readonly"),
+ types: words(cTypes),
+ atoms: words("YES NO NULL NILL ON OFF true false"),
+ hooks: {
+ "@": function(stream) {
+ stream.eatWhile(/[\w\$]/);
+ return "keyword";
+ },
+ "#": cppHook,
+ indent: function(_state, ctx, textAfter) {
+ if (ctx.type == "statement" && /^@\w/.test(textAfter)) return ctx.indented
+ }
+ },
+ modeProps: {fold: "brace"}
+ });
+
+ def("text/x-squirrel", {
+ name: "clike",
+ keywords: words("base break clone continue const default delete enum extends function in class" +
+ " foreach local resume return this throw typeof yield constructor instanceof static"),
+ types: words(cTypes),
+ blockKeywords: words("case catch class else for foreach if switch try while"),
+ defKeywords: words("function local class"),
+ typeFirstDefinitions: true,
+ atoms: words("true false null"),
+ hooks: {"#": cppHook},
+ modeProps: {fold: ["brace", "include"]}
+ });
+
+ // Ceylon Strings need to deal with interpolation
+ var stringTokenizer = null;
+ function tokenCeylonString(type) {
+ return function(stream, state) {
+ var escaped = false, next, end = false;
+ while (!stream.eol()) {
+ if (!escaped && stream.match('"') &&
+ (type == "single" || stream.match('""'))) {
+ end = true;
+ break;
+ }
+ if (!escaped && stream.match('``')) {
+ stringTokenizer = tokenCeylonString(type);
+ end = true;
+ break;
+ }
+ next = stream.next();
+ escaped = type == "single" && !escaped && next == "\\";
+ }
+ if (end)
+ state.tokenize = null;
+ return "string";
+ }
+ }
+
+ def("text/x-ceylon", {
+ name: "clike",
+ keywords: words("abstracts alias assembly assert assign break case catch class continue dynamic else" +
+ " exists extends finally for function given if import in interface is let module new" +
+ " nonempty object of out outer package return satisfies super switch then this throw" +
+ " try value void while"),
+ types: function(word) {
+ // In Ceylon all identifiers that start with an uppercase are types
+ var first = word.charAt(0);
+ return (first === first.toUpperCase() && first !== first.toLowerCase());
+ },
+ blockKeywords: words("case catch class dynamic else finally for function if interface module new object switch try while"),
+ defKeywords: words("class dynamic function interface module object package value"),
+ builtin: words("abstract actual aliased annotation by default deprecated doc final formal late license" +
+ " native optional sealed see serializable shared suppressWarnings tagged throws variable"),
+ isPunctuationChar: /[\[\]{}\(\),;\:\.`]/,
+ isOperatorChar: /[+\-*&%=<>!?|^~:\/]/,
+ numberStart: /[\d#$]/,
+ number: /^(?:#[\da-fA-F_]+|\$[01_]+|[\d_]+[kMGTPmunpf]?|[\d_]+\.[\d_]+(?:[eE][-+]?\d+|[kMGTPmunpf]|)|)/i,
+ multiLineStrings: true,
+ typeFirstDefinitions: true,
+ atoms: words("true false null larger smaller equal empty finished"),
+ indentSwitch: false,
+ styleDefs: false,
+ hooks: {
+ "@": function(stream) {
+ stream.eatWhile(/[\w\$_]/);
+ return "meta";
+ },
+ '"': function(stream, state) {
+ state.tokenize = tokenCeylonString(stream.match('""') ? "triple" : "single");
+ return state.tokenize(stream, state);
+ },
+ '`': function(stream, state) {
+ if (!stringTokenizer || !stream.match('`')) return false;
+ state.tokenize = stringTokenizer;
+ stringTokenizer = null;
+ return state.tokenize(stream, state);
+ },
+ "'": function(stream) {
+ stream.eatWhile(/[\w\$_\xa1-\uffff]/);
+ return "atom";
+ },
+ token: function(_stream, state, style) {
+ if ((style == "variable" || style == "variable-3") &&
+ state.prevToken == ".") {
+ return "variable-2";
+ }
+ }
+ },
+ modeProps: {
+ fold: ["brace", "import"],
+ closeBrackets: {triples: '"'}
+ }
+ });
+
+});
diff --git a/devtools/client/sourceeditor/codemirror/mode/css/css.js b/devtools/client/sourceeditor/codemirror/mode/css/css.js
new file mode 100644
index 000000000..cf6a2f2eb
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/mode/css/css.js
@@ -0,0 +1,825 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+"use strict";
+
+CodeMirror.defineMode("css", function(config, parserConfig) {
+ var inline = parserConfig.inline
+ if (!parserConfig.propertyKeywords) parserConfig = CodeMirror.resolveMode("text/css");
+
+ var indentUnit = config.indentUnit,
+ tokenHooks = parserConfig.tokenHooks,
+ documentTypes = parserConfig.documentTypes || {},
+ mediaTypes = parserConfig.mediaTypes || {},
+ mediaFeatures = parserConfig.mediaFeatures || {},
+ mediaValueKeywords = parserConfig.mediaValueKeywords || {},
+ propertyKeywords = parserConfig.propertyKeywords || {},
+ nonStandardPropertyKeywords = parserConfig.nonStandardPropertyKeywords || {},
+ fontProperties = parserConfig.fontProperties || {},
+ counterDescriptors = parserConfig.counterDescriptors || {},
+ colorKeywords = parserConfig.colorKeywords || {},
+ valueKeywords = parserConfig.valueKeywords || {},
+ allowNested = parserConfig.allowNested,
+ supportsAtComponent = parserConfig.supportsAtComponent === true;
+
+ var type, override;
+ function ret(style, tp) { type = tp; return style; }
+
+ // Tokenizers
+
+ function tokenBase(stream, state) {
+ var ch = stream.next();
+ if (tokenHooks[ch]) {
+ var result = tokenHooks[ch](stream, state);
+ if (result !== false) return result;
+ }
+ if (ch == "@") {
+ stream.eatWhile(/[\w\\\-]/);
+ return ret("def", stream.current());
+ } else if (ch == "=" || (ch == "~" || ch == "|") && stream.eat("=")) {
+ return ret(null, "compare");
+ } else if (ch == "\"" || ch == "'") {
+ state.tokenize = tokenString(ch);
+ return state.tokenize(stream, state);
+ } else if (ch == "#") {
+ stream.eatWhile(/[\w\\\-]/);
+ return ret("atom", "hash");
+ } else if (ch == "!") {
+ stream.match(/^\s*\w*/);
+ return ret("keyword", "important");
+ } else if (/\d/.test(ch) || ch == "." && stream.eat(/\d/)) {
+ stream.eatWhile(/[\w.%]/);
+ return ret("number", "unit");
+ } else if (ch === "-") {
+ if (/[\d.]/.test(stream.peek())) {
+ stream.eatWhile(/[\w.%]/);
+ return ret("number", "unit");
+ } else if (stream.match(/^-[\w\\\-]+/)) {
+ stream.eatWhile(/[\w\\\-]/);
+ if (stream.match(/^\s*:/, false))
+ return ret("variable-2", "variable-definition");
+ return ret("variable-2", "variable");
+ } else if (stream.match(/^\w+-/)) {
+ return ret("meta", "meta");
+ }
+ } else if (/[,+>*\/]/.test(ch)) {
+ return ret(null, "select-op");
+ } else if (ch == "." && stream.match(/^-?[_a-z][_a-z0-9-]*/i)) {
+ return ret("qualifier", "qualifier");
+ } else if (/[:;{}\[\]\(\)]/.test(ch)) {
+ return ret(null, ch);
+ } else if ((ch == "u" && stream.match(/rl(-prefix)?\(/)) ||
+ (ch == "d" && stream.match("omain(")) ||
+ (ch == "r" && stream.match("egexp("))) {
+ stream.backUp(1);
+ state.tokenize = tokenParenthesized;
+ return ret("property", "word");
+ } else if (/[\w\\\-]/.test(ch)) {
+ stream.eatWhile(/[\w\\\-]/);
+ return ret("property", "word");
+ } else {
+ return ret(null, null);
+ }
+ }
+
+ function tokenString(quote) {
+ return function(stream, state) {
+ var escaped = false, ch;
+ while ((ch = stream.next()) != null) {
+ if (ch == quote && !escaped) {
+ if (quote == ")") stream.backUp(1);
+ break;
+ }
+ escaped = !escaped && ch == "\\";
+ }
+ if (ch == quote || !escaped && quote != ")") state.tokenize = null;
+ return ret("string", "string");
+ };
+ }
+
+ function tokenParenthesized(stream, state) {
+ stream.next(); // Must be '('
+ if (!stream.match(/\s*[\"\')]/, false))
+ state.tokenize = tokenString(")");
+ else
+ state.tokenize = null;
+ return ret(null, "(");
+ }
+
+ // Context management
+
+ function Context(type, indent, prev) {
+ this.type = type;
+ this.indent = indent;
+ this.prev = prev;
+ }
+
+ function pushContext(state, stream, type, indent) {
+ state.context = new Context(type, stream.indentation() + (indent === false ? 0 : indentUnit), state.context);
+ return type;
+ }
+
+ function popContext(state) {
+ if (state.context.prev)
+ state.context = state.context.prev;
+ return state.context.type;
+ }
+
+ function pass(type, stream, state) {
+ return states[state.context.type](type, stream, state);
+ }
+ function popAndPass(type, stream, state, n) {
+ for (var i = n || 1; i > 0; i--)
+ state.context = state.context.prev;
+ return pass(type, stream, state);
+ }
+
+ // Parser
+
+ function wordAsValue(stream) {
+ var word = stream.current().toLowerCase();
+ if (valueKeywords.hasOwnProperty(word))
+ override = "atom";
+ else if (colorKeywords.hasOwnProperty(word))
+ override = "keyword";
+ else
+ override = "variable";
+ }
+
+ var states = {};
+
+ states.top = function(type, stream, state) {
+ if (type == "{") {
+ return pushContext(state, stream, "block");
+ } else if (type == "}" && state.context.prev) {
+ return popContext(state);
+ } else if (supportsAtComponent && /@component/.test(type)) {
+ return pushContext(state, stream, "atComponentBlock");
+ } else if (/^@(-moz-)?document$/.test(type)) {
+ return pushContext(state, stream, "documentTypes");
+ } else if (/^@(media|supports|(-moz-)?document|import)$/.test(type)) {
+ return pushContext(state, stream, "atBlock");
+ } else if (/^@(font-face|counter-style)/.test(type)) {
+ state.stateArg = type;
+ return "restricted_atBlock_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") {
+ override = "builtin";
+ } else if (type == "word") {
+ override = "tag";
+ } else if (type == "variable-definition") {
+ return "maybeprop";
+ } else if (type == "interpolation") {
+ return pushContext(state, stream, "interpolation");
+ } else if (type == ":") {
+ return "pseudo";
+ } else if (allowNested && type == "(") {
+ return pushContext(state, stream, "parens");
+ }
+ return state.context.type;
+ };
+
+ states.block = function(type, stream, state) {
+ if (type == "word") {
+ var word = stream.current().toLowerCase();
+ if (propertyKeywords.hasOwnProperty(word)) {
+ override = "property";
+ return "maybeprop";
+ } else if (nonStandardPropertyKeywords.hasOwnProperty(word)) {
+ override = "string-2";
+ return "maybeprop";
+ } else if (allowNested) {
+ override = stream.match(/^\s*:(?:\s|$)/, false) ? "property" : "tag";
+ return "block";
+ } else {
+ override += " error";
+ return "maybeprop";
+ }
+ } else if (type == "meta") {
+ return "block";
+ } else if (!allowNested && (type == "hash" || type == "qualifier")) {
+ override = "error";
+ return "block";
+ } else {
+ return states.top(type, stream, state);
+ }
+ };
+
+ states.maybeprop = function(type, stream, state) {
+ if (type == ":") return pushContext(state, stream, "prop");
+ return pass(type, stream, state);
+ };
+
+ states.prop = function(type, stream, state) {
+ if (type == ";") return popContext(state);
+ if (type == "{" && allowNested) return pushContext(state, stream, "propBlock");
+ if (type == "}" || type == "{") return popAndPass(type, stream, state);
+ if (type == "(") return pushContext(state, stream, "parens");
+
+ if (type == "hash" && !/^#([0-9a-fA-f]{3,4}|[0-9a-fA-f]{6}|[0-9a-fA-f]{8})$/.test(stream.current())) {
+ override += " error";
+ } else if (type == "word") {
+ wordAsValue(stream);
+ } else if (type == "interpolation") {
+ return pushContext(state, stream, "interpolation");
+ }
+ return "prop";
+ };
+
+ states.propBlock = function(type, _stream, state) {
+ if (type == "}") return popContext(state);
+ if (type == "word") { override = "property"; return "maybeprop"; }
+ return state.context.type;
+ };
+
+ states.parens = function(type, stream, state) {
+ if (type == "{" || type == "}") return popAndPass(type, stream, state);
+ if (type == ")") return popContext(state);
+ if (type == "(") return pushContext(state, stream, "parens");
+ if (type == "interpolation") return pushContext(state, stream, "interpolation");
+ if (type == "word") wordAsValue(stream);
+ return "parens";
+ };
+
+ states.pseudo = function(type, stream, state) {
+ if (type == "word") {
+ override = "variable-3";
+ return state.context.type;
+ }
+ return pass(type, stream, state);
+ };
+
+ states.documentTypes = function(type, stream, state) {
+ if (type == "word" && documentTypes.hasOwnProperty(stream.current())) {
+ override = "tag";
+ return state.context.type;
+ } else {
+ return states.atBlock(type, stream, state);
+ }
+ };
+
+ states.atBlock = function(type, stream, state) {
+ if (type == "(") return pushContext(state, stream, "atBlock_parens");
+ if (type == "}" || type == ";") return popAndPass(type, stream, state);
+ if (type == "{") return popContext(state) && pushContext(state, stream, allowNested ? "block" : "top");
+
+ if (type == "interpolation") return pushContext(state, stream, "interpolation");
+
+ if (type == "word") {
+ var word = stream.current().toLowerCase();
+ if (word == "only" || word == "not" || word == "and" || word == "or")
+ override = "keyword";
+ else if (mediaTypes.hasOwnProperty(word))
+ override = "attribute";
+ else if (mediaFeatures.hasOwnProperty(word))
+ override = "property";
+ else if (mediaValueKeywords.hasOwnProperty(word))
+ override = "keyword";
+ else if (propertyKeywords.hasOwnProperty(word))
+ override = "property";
+ else if (nonStandardPropertyKeywords.hasOwnProperty(word))
+ override = "string-2";
+ else if (valueKeywords.hasOwnProperty(word))
+ override = "atom";
+ else if (colorKeywords.hasOwnProperty(word))
+ override = "keyword";
+ else
+ override = "error";
+ }
+ return state.context.type;
+ };
+
+ states.atComponentBlock = function(type, stream, state) {
+ if (type == "}")
+ return popAndPass(type, stream, state);
+ if (type == "{")
+ return popContext(state) && pushContext(state, stream, allowNested ? "block" : "top", false);
+ if (type == "word")
+ override = "error";
+ return state.context.type;
+ };
+
+ states.atBlock_parens = function(type, stream, state) {
+ if (type == ")") return popContext(state);
+ if (type == "{" || type == "}") return popAndPass(type, stream, state, 2);
+ return states.atBlock(type, stream, state);
+ };
+
+ states.restricted_atBlock_before = function(type, stream, state) {
+ if (type == "{")
+ return pushContext(state, stream, "restricted_atBlock");
+ if (type == "word" && state.stateArg == "@counter-style") {
+ override = "variable";
+ return "restricted_atBlock_before";
+ }
+ return pass(type, stream, state);
+ };
+
+ states.restricted_atBlock = function(type, stream, state) {
+ if (type == "}") {
+ state.stateArg = null;
+ return popContext(state);
+ }
+ if (type == "word") {
+ if ((state.stateArg == "@font-face" && !fontProperties.hasOwnProperty(stream.current().toLowerCase())) ||
+ (state.stateArg == "@counter-style" && !counterDescriptors.hasOwnProperty(stream.current().toLowerCase())))
+ override = "error";
+ else
+ override = "property";
+ return "maybeprop";
+ }
+ return "restricted_atBlock";
+ };
+
+ 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);
+ if (type == "word") override = "tag";
+ else if (type == "hash") override = "builtin";
+ return "at";
+ };
+
+ states.interpolation = function(type, stream, state) {
+ if (type == "}") return popContext(state);
+ if (type == "{" || type == ";") return popAndPass(type, stream, state);
+ if (type == "word") override = "variable";
+ else if (type != "variable" && type != "(" && type != ")") override = "error";
+ return "interpolation";
+ };
+
+ return {
+ startState: function(base) {
+ return {tokenize: null,
+ state: inline ? "block" : "top",
+ stateArg: null,
+ context: new Context(inline ? "block" : "top", base || 0, null)};
+ },
+
+ token: function(stream, state) {
+ if (!state.tokenize && stream.eatSpace()) return null;
+ var style = (state.tokenize || tokenBase)(stream, state);
+ if (style && typeof style == "object") {
+ type = style[1];
+ style = style[0];
+ }
+ override = style;
+ state.state = states[state.state](type, stream, state);
+ return override;
+ },
+
+ indent: function(state, textAfter) {
+ var cx = state.context, ch = textAfter && textAfter.charAt(0);
+ var indent = cx.indent;
+ if (cx.type == "prop" && (ch == "}" || ch == ")")) cx = cx.prev;
+ if (cx.prev) {
+ if (ch == "}" && (cx.type == "block" || cx.type == "top" ||
+ cx.type == "interpolation" || cx.type == "restricted_atBlock")) {
+ // Resume indentation from parent context.
+ cx = cx.prev;
+ indent = cx.indent;
+ } else if (ch == ")" && (cx.type == "parens" || cx.type == "atBlock_parens") ||
+ ch == "{" && (cx.type == "at" || cx.type == "atBlock")) {
+ // Dedent relative to current context.
+ indent = Math.max(0, cx.indent - indentUnit);
+ cx = cx.prev;
+ }
+ }
+ return indent;
+ },
+
+ electricChars: "}",
+ blockCommentStart: "/*",
+ blockCommentEnd: "*/",
+ fold: "brace"
+ };
+});
+
+ function keySet(array) {
+ var keys = {};
+ for (var i = 0; i < array.length; ++i) {
+ keys[array[i]] = true;
+ }
+ return keys;
+ }
+
+ var documentTypes_ = [
+ "domain", "regexp", "url", "url-prefix"
+ ], documentTypes = keySet(documentTypes_);
+
+ var mediaTypes_ = [
+ "all", "aural", "braille", "handheld", "print", "projection", "screen",
+ "tty", "tv", "embossed"
+ ], mediaTypes = keySet(mediaTypes_);
+
+ var mediaFeatures_ = [
+ "width", "min-width", "max-width", "height", "min-height", "max-height",
+ "device-width", "min-device-width", "max-device-width", "device-height",
+ "min-device-height", "max-device-height", "aspect-ratio",
+ "min-aspect-ratio", "max-aspect-ratio", "device-aspect-ratio",
+ "min-device-aspect-ratio", "max-device-aspect-ratio", "color", "min-color",
+ "max-color", "color-index", "min-color-index", "max-color-index",
+ "monochrome", "min-monochrome", "max-monochrome", "resolution",
+ "min-resolution", "max-resolution", "scan", "grid", "orientation",
+ "device-pixel-ratio", "min-device-pixel-ratio", "max-device-pixel-ratio",
+ "pointer", "any-pointer", "hover", "any-hover"
+ ], mediaFeatures = keySet(mediaFeatures_);
+
+ var mediaValueKeywords_ = [
+ "landscape", "portrait", "none", "coarse", "fine", "on-demand", "hover",
+ "interlace", "progressive"
+ ], mediaValueKeywords = keySet(mediaValueKeywords_);
+
+ var propertyKeywords_ = [
+ "align-content", "align-items", "align-self", "alignment-adjust",
+ "alignment-baseline", "anchor-point", "animation", "animation-delay",
+ "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-blend-mode", "background-clip",
+ "background-color", "background-image", "background-origin", "background-position",
+ "background-repeat", "background-size", "baseline-shift", "binding",
+ "bleed", "bookmark-label", "bookmark-level", "bookmark-state",
+ "bookmark-target", "border", "border-bottom", "border-bottom-color",
+ "border-bottom-left-radius", "border-bottom-right-radius",
+ "border-bottom-style", "border-bottom-width", "border-collapse",
+ "border-color", "border-image", "border-image-outset",
+ "border-image-repeat", "border-image-slice", "border-image-source",
+ "border-image-width", "border-left", "border-left-color",
+ "border-left-style", "border-left-width", "border-radius", "border-right",
+ "border-right-color", "border-right-style", "border-right-width",
+ "border-spacing", "border-style", "border-top", "border-top-color",
+ "border-top-left-radius", "border-top-right-radius", "border-top-style",
+ "border-top-width", "border-width", "bottom", "box-decoration-break",
+ "box-shadow", "box-sizing", "break-after", "break-before", "break-inside",
+ "caption-side", "clear", "clip", "color", "color-profile", "column-count",
+ "column-fill", "column-gap", "column-rule", "column-rule-color",
+ "column-rule-style", "column-rule-width", "column-span", "column-width",
+ "columns", "content", "counter-increment", "counter-reset", "crop", "cue",
+ "cue-after", "cue-before", "cursor", "direction", "display",
+ "dominant-baseline", "drop-initial-after-adjust",
+ "drop-initial-after-align", "drop-initial-before-adjust",
+ "drop-initial-before-align", "drop-initial-size", "drop-initial-value",
+ "elevation", "empty-cells", "fit", "fit-position", "flex", "flex-basis",
+ "flex-direction", "flex-flow", "flex-grow", "flex-shrink", "flex-wrap",
+ "float", "float-offset", "flow-from", "flow-into", "font", "font-feature-settings",
+ "font-family", "font-kerning", "font-language-override", "font-size", "font-size-adjust",
+ "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", "grid-area", "grid-auto-columns", "grid-auto-flow",
+ "grid-auto-rows", "grid-column", "grid-column-end", "grid-column-gap",
+ "grid-column-start", "grid-gap", "grid-row", "grid-row-end", "grid-row-gap",
+ "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",
+ "line-stacking-shift", "line-stacking-strategy", "list-style",
+ "list-style-image", "list-style-position", "list-style-type", "margin",
+ "margin-bottom", "margin-left", "margin-right", "margin-top",
+ "marks", "marquee-direction", "marquee-loop",
+ "marquee-play-count", "marquee-speed", "marquee-style", "max-height",
+ "max-width", "min-height", "min-width", "move-to", "nav-down", "nav-index",
+ "nav-left", "nav-right", "nav-up", "object-fit", "object-position",
+ "opacity", "order", "orphans", "outline",
+ "outline-color", "outline-offset", "outline-style", "outline-width",
+ "overflow", "overflow-style", "overflow-wrap", "overflow-x", "overflow-y",
+ "padding", "padding-bottom", "padding-left", "padding-right", "padding-top",
+ "page", "page-break-after", "page-break-before", "page-break-inside",
+ "page-policy", "pause", "pause-after", "pause-before", "perspective",
+ "perspective-origin", "pitch", "pitch-range", "play-during", "position",
+ "presentation-level", "punctuation-trim", "quotes", "region-break-after",
+ "region-break-before", "region-break-inside", "region-fragment",
+ "rendering-intent", "resize", "rest", "rest-after", "rest-before", "richness",
+ "right", "rotation", "rotation-point", "ruby-align", "ruby-overhang",
+ "ruby-position", "ruby-span", "shape-image-threshold", "shape-inside", "shape-margin",
+ "shape-outside", "size", "speak", "speak-as", "speak-header",
+ "speak-numeral", "speak-punctuation", "speech-rate", "stress", "string-set",
+ "tab-size", "table-layout", "target", "target-name", "target-new",
+ "target-position", "text-align", "text-align-last", "text-decoration",
+ "text-decoration-color", "text-decoration-line", "text-decoration-skip",
+ "text-decoration-style", "text-emphasis", "text-emphasis-color",
+ "text-emphasis-position", "text-emphasis-style", "text-height",
+ "text-indent", "text-justify", "text-outline", "text-overflow", "text-shadow",
+ "text-size-adjust", "text-space-collapse", "text-transform", "text-underline-position",
+ "text-wrap", "top", "transform", "transform-origin", "transform-style",
+ "transition", "transition-delay", "transition-duration",
+ "transition-property", "transition-timing-function", "unicode-bidi",
+ "vertical-align", "visibility", "voice-balance", "voice-duration",
+ "voice-family", "voice-pitch", "voice-range", "voice-rate", "voice-stress",
+ "voice-volume", "volume", "white-space", "widows", "width", "word-break",
+ "word-spacing", "word-wrap", "z-index",
+ // SVG-specific
+ "clip-path", "clip-rule", "mask", "enable-background", "filter", "flood-color",
+ "flood-opacity", "lighting-color", "stop-color", "stop-opacity", "pointer-events",
+ "color-interpolation", "color-interpolation-filters",
+ "color-rendering", "fill", "fill-opacity", "fill-rule", "image-rendering",
+ "marker", "marker-end", "marker-mid", "marker-start", "shape-rendering", "stroke",
+ "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin",
+ "stroke-miterlimit", "stroke-opacity", "stroke-width", "text-rendering",
+ "baseline-shift", "dominant-baseline", "glyph-orientation-horizontal",
+ "glyph-orientation-vertical", "text-anchor", "writing-mode"
+ ], propertyKeywords = keySet(propertyKeywords_);
+
+ var nonStandardPropertyKeywords_ = [
+ "scrollbar-arrow-color", "scrollbar-base-color", "scrollbar-dark-shadow-color",
+ "scrollbar-face-color", "scrollbar-highlight-color", "scrollbar-shadow-color",
+ "scrollbar-3d-light-color", "scrollbar-track-color", "shape-inside",
+ "searchfield-cancel-button", "searchfield-decoration", "searchfield-results-button",
+ "searchfield-results-decoration", "zoom"
+ ], nonStandardPropertyKeywords = keySet(nonStandardPropertyKeywords_);
+
+ var fontProperties_ = [
+ "font-family", "src", "unicode-range", "font-variant", "font-feature-settings",
+ "font-stretch", "font-weight", "font-style"
+ ], fontProperties = keySet(fontProperties_);
+
+ var counterDescriptors_ = [
+ "additive-symbols", "fallback", "negative", "pad", "prefix", "range",
+ "speak-as", "suffix", "symbols", "system"
+ ], counterDescriptors = keySet(counterDescriptors_);
+
+ var colorKeywords_ = [
+ "aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", "beige",
+ "bisque", "black", "blanchedalmond", "blue", "blueviolet", "brown",
+ "burlywood", "cadetblue", "chartreuse", "chocolate", "coral", "cornflowerblue",
+ "cornsilk", "crimson", "cyan", "darkblue", "darkcyan", "darkgoldenrod",
+ "darkgray", "darkgreen", "darkkhaki", "darkmagenta", "darkolivegreen",
+ "darkorange", "darkorchid", "darkred", "darksalmon", "darkseagreen",
+ "darkslateblue", "darkslategray", "darkturquoise", "darkviolet",
+ "deeppink", "deepskyblue", "dimgray", "dodgerblue", "firebrick",
+ "floralwhite", "forestgreen", "fuchsia", "gainsboro", "ghostwhite",
+ "gold", "goldenrod", "gray", "grey", "green", "greenyellow", "honeydew",
+ "hotpink", "indianred", "indigo", "ivory", "khaki", "lavender",
+ "lavenderblush", "lawngreen", "lemonchiffon", "lightblue", "lightcoral",
+ "lightcyan", "lightgoldenrodyellow", "lightgray", "lightgreen", "lightpink",
+ "lightsalmon", "lightseagreen", "lightskyblue", "lightslategray",
+ "lightsteelblue", "lightyellow", "lime", "limegreen", "linen", "magenta",
+ "maroon", "mediumaquamarine", "mediumblue", "mediumorchid", "mediumpurple",
+ "mediumseagreen", "mediumslateblue", "mediumspringgreen", "mediumturquoise",
+ "mediumvioletred", "midnightblue", "mintcream", "mistyrose", "moccasin",
+ "navajowhite", "navy", "oldlace", "olive", "olivedrab", "orange", "orangered",
+ "orchid", "palegoldenrod", "palegreen", "paleturquoise", "palevioletred",
+ "papayawhip", "peachpuff", "peru", "pink", "plum", "powderblue",
+ "purple", "rebeccapurple", "red", "rosybrown", "royalblue", "saddlebrown",
+ "salmon", "sandybrown", "seagreen", "seashell", "sienna", "silver", "skyblue",
+ "slateblue", "slategray", "snow", "springgreen", "steelblue", "tan",
+ "teal", "thistle", "tomato", "turquoise", "violet", "wheat", "white",
+ "whitesmoke", "yellow", "yellowgreen"
+ ], colorKeywords = keySet(colorKeywords_);
+
+ var valueKeywords_ = [
+ "above", "absolute", "activeborder", "additive", "activecaption", "afar",
+ "after-white-space", "ahead", "alias", "all", "all-scroll", "alphabetic", "alternate",
+ "always", "amharic", "amharic-abegede", "antialiased", "appworkspace",
+ "arabic-indic", "armenian", "asterisks", "attr", "auto", "avoid", "avoid-column", "avoid-page",
+ "avoid-region", "background", "backwards", "baseline", "below", "bidi-override", "binary",
+ "bengali", "blink", "block", "block-axis", "bold", "bolder", "border", "border-box",
+ "both", "bottom", "break", "break-all", "break-word", "bullets", "button", "button-bevel",
+ "buttonface", "buttonhighlight", "buttonshadow", "buttontext", "calc", "cambodian",
+ "capitalize", "caps-lock-indicator", "caption", "captiontext", "caret",
+ "cell", "center", "checkbox", "circle", "cjk-decimal", "cjk-earthly-branch",
+ "cjk-heavenly-stem", "cjk-ideographic", "clear", "clip", "close-quote",
+ "col-resize", "collapse", "color", "color-burn", "color-dodge", "column", "column-reverse",
+ "compact", "condensed", "contain", "content",
+ "content-box", "context-menu", "continuous", "copy", "counter", "counters", "cover", "crop",
+ "cross", "crosshair", "currentcolor", "cursive", "cyclic", "darken", "dashed", "decimal",
+ "decimal-leading-zero", "default", "default-button", "dense", "destination-atop",
+ "destination-in", "destination-out", "destination-over", "devanagari", "difference",
+ "disc", "discard", "disclosure-closed", "disclosure-open", "document",
+ "dot-dash", "dot-dot-dash",
+ "dotted", "double", "down", "e-resize", "ease", "ease-in", "ease-in-out", "ease-out",
+ "element", "ellipse", "ellipsis", "embed", "end", "ethiopic", "ethiopic-abegede",
+ "ethiopic-abegede-am-et", "ethiopic-abegede-gez", "ethiopic-abegede-ti-er",
+ "ethiopic-abegede-ti-et", "ethiopic-halehame-aa-er",
+ "ethiopic-halehame-aa-et", "ethiopic-halehame-am-et",
+ "ethiopic-halehame-gez", "ethiopic-halehame-om-et",
+ "ethiopic-halehame-sid-et", "ethiopic-halehame-so-et",
+ "ethiopic-halehame-ti-er", "ethiopic-halehame-ti-et", "ethiopic-halehame-tig",
+ "ethiopic-numeric", "ew-resize", "exclusion", "expanded", "extends", "extra-condensed",
+ "extra-expanded", "fantasy", "fast", "fill", "fixed", "flat", "flex", "flex-end", "flex-start", "footnotes",
+ "forwards", "from", "geometricPrecision", "georgian", "graytext", "grid", "groove",
+ "gujarati", "gurmukhi", "hand", "hangul", "hangul-consonant", "hard-light", "hebrew",
+ "help", "hidden", "hide", "higher", "highlight", "highlighttext",
+ "hiragana", "hiragana-iroha", "horizontal", "hsl", "hsla", "hue", "icon", "ignore",
+ "inactiveborder", "inactivecaption", "inactivecaptiontext", "infinite",
+ "infobackground", "infotext", "inherit", "initial", "inline", "inline-axis",
+ "inline-block", "inline-flex", "inline-grid", "inline-table", "inset", "inside", "intrinsic", "invert",
+ "italic", "japanese-formal", "japanese-informal", "justify", "kannada",
+ "katakana", "katakana-iroha", "keep-all", "khmer",
+ "korean-hangul-formal", "korean-hanja-formal", "korean-hanja-informal",
+ "landscape", "lao", "large", "larger", "left", "level", "lighter", "lighten",
+ "line-through", "linear", "linear-gradient", "lines", "list-item", "listbox", "listitem",
+ "local", "logical", "loud", "lower", "lower-alpha", "lower-armenian",
+ "lower-greek", "lower-hexadecimal", "lower-latin", "lower-norwegian",
+ "lower-roman", "lowercase", "ltr", "luminosity", "malayalam", "match", "matrix", "matrix3d",
+ "media-controls-background", "media-current-time-display",
+ "media-fullscreen-button", "media-mute-button", "media-play-button",
+ "media-return-to-realtime-button", "media-rewind-button",
+ "media-seek-back-button", "media-seek-forward-button", "media-slider",
+ "media-sliderthumb", "media-time-remaining-display", "media-volume-slider",
+ "media-volume-slider-container", "media-volume-sliderthumb", "medium",
+ "menu", "menulist", "menulist-button", "menulist-text",
+ "menulist-textfield", "menutext", "message-box", "middle", "min-intrinsic",
+ "mix", "mongolian", "monospace", "move", "multiple", "multiply", "myanmar", "n-resize",
+ "narrower", "ne-resize", "nesw-resize", "no-close-quote", "no-drop",
+ "no-open-quote", "no-repeat", "none", "normal", "not-allowed", "nowrap",
+ "ns-resize", "numbers", "numeric", "nw-resize", "nwse-resize", "oblique", "octal", "open-quote",
+ "optimizeLegibility", "optimizeSpeed", "oriya", "oromo", "outset",
+ "outside", "outside-shape", "overlay", "overline", "padding", "padding-box",
+ "painted", "page", "paused", "persian", "perspective", "plus-darker", "plus-lighter",
+ "pointer", "polygon", "portrait", "pre", "pre-line", "pre-wrap", "preserve-3d",
+ "progress", "push-button", "radial-gradient", "radio", "read-only",
+ "read-write", "read-write-plaintext-only", "rectangle", "region",
+ "relative", "repeat", "repeating-linear-gradient",
+ "repeating-radial-gradient", "repeat-x", "repeat-y", "reset", "reverse",
+ "rgb", "rgba", "ridge", "right", "rotate", "rotate3d", "rotateX", "rotateY",
+ "rotateZ", "round", "row", "row-resize", "row-reverse", "rtl", "run-in", "running",
+ "s-resize", "sans-serif", "saturation", "scale", "scale3d", "scaleX", "scaleY", "scaleZ", "screen",
+ "scroll", "scrollbar", "se-resize", "searchfield",
+ "searchfield-cancel-button", "searchfield-decoration",
+ "searchfield-results-button", "searchfield-results-decoration",
+ "semi-condensed", "semi-expanded", "separate", "serif", "show", "sidama",
+ "simp-chinese-formal", "simp-chinese-informal", "single",
+ "skew", "skewX", "skewY", "skip-white-space", "slide", "slider-horizontal",
+ "slider-vertical", "sliderthumb-horizontal", "sliderthumb-vertical", "slow",
+ "small", "small-caps", "small-caption", "smaller", "soft-light", "solid", "somali",
+ "source-atop", "source-in", "source-out", "source-over", "space", "space-around", "space-between", "spell-out", "square",
+ "square-button", "start", "static", "status-bar", "stretch", "stroke", "sub",
+ "subpixel-antialiased", "super", "sw-resize", "symbolic", "symbols", "table",
+ "table-caption", "table-cell", "table-column", "table-column-group",
+ "table-footer-group", "table-header-group", "table-row", "table-row-group",
+ "tamil",
+ "telugu", "text", "text-bottom", "text-top", "textarea", "textfield", "thai",
+ "thick", "thin", "threeddarkshadow", "threedface", "threedhighlight",
+ "threedlightshadow", "threedshadow", "tibetan", "tigre", "tigrinya-er",
+ "tigrinya-er-abegede", "tigrinya-et", "tigrinya-et-abegede", "to", "top",
+ "trad-chinese-formal", "trad-chinese-informal",
+ "translate", "translate3d", "translateX", "translateY", "translateZ",
+ "transparent", "ultra-condensed", "ultra-expanded", "underline", "up",
+ "upper-alpha", "upper-armenian", "upper-greek", "upper-hexadecimal",
+ "upper-latin", "upper-norwegian", "upper-roman", "uppercase", "urdu", "url",
+ "var", "vertical", "vertical-text", "visible", "visibleFill", "visiblePainted",
+ "visibleStroke", "visual", "w-resize", "wait", "wave", "wider",
+ "window", "windowframe", "windowtext", "words", "wrap", "wrap-reverse", "x-large", "x-small", "xor",
+ "xx-large", "xx-small"
+ ], valueKeywords = keySet(valueKeywords_);
+
+ var allWords = documentTypes_.concat(mediaTypes_).concat(mediaFeatures_).concat(mediaValueKeywords_)
+ .concat(propertyKeywords_).concat(nonStandardPropertyKeywords_).concat(colorKeywords_)
+ .concat(valueKeywords_);
+ CodeMirror.registerHelper("hintWords", "css", allWords);
+
+ function tokenCComment(stream, state) {
+ var maybeEnd = false, ch;
+ while ((ch = stream.next()) != null) {
+ if (maybeEnd && ch == "/") {
+ state.tokenize = null;
+ break;
+ }
+ maybeEnd = (ch == "*");
+ }
+ return ["comment", "comment"];
+ }
+
+ CodeMirror.defineMIME("text/css", {
+ documentTypes: documentTypes,
+ mediaTypes: mediaTypes,
+ mediaFeatures: mediaFeatures,
+ mediaValueKeywords: mediaValueKeywords,
+ propertyKeywords: propertyKeywords,
+ nonStandardPropertyKeywords: nonStandardPropertyKeywords,
+ fontProperties: fontProperties,
+ counterDescriptors: counterDescriptors,
+ colorKeywords: colorKeywords,
+ valueKeywords: valueKeywords,
+ tokenHooks: {
+ "/": function(stream, state) {
+ if (!stream.eat("*")) return false;
+ state.tokenize = tokenCComment;
+ return tokenCComment(stream, state);
+ }
+ },
+ name: "css"
+ });
+
+ CodeMirror.defineMIME("text/x-scss", {
+ mediaTypes: mediaTypes,
+ mediaFeatures: mediaFeatures,
+ mediaValueKeywords: mediaValueKeywords,
+ propertyKeywords: propertyKeywords,
+ nonStandardPropertyKeywords: nonStandardPropertyKeywords,
+ colorKeywords: colorKeywords,
+ valueKeywords: valueKeywords,
+ fontProperties: fontProperties,
+ allowNested: true,
+ tokenHooks: {
+ "/": function(stream, state) {
+ if (stream.eat("/")) {
+ stream.skipToEnd();
+ return ["comment", "comment"];
+ } else if (stream.eat("*")) {
+ state.tokenize = tokenCComment;
+ return tokenCComment(stream, state);
+ } else {
+ return ["operator", "operator"];
+ }
+ },
+ ":": function(stream) {
+ if (stream.match(/\s*\{/))
+ return [null, "{"];
+ return false;
+ },
+ "$": function(stream) {
+ stream.match(/^[\w-]+/);
+ if (stream.match(/^\s*:/, false))
+ return ["variable-2", "variable-definition"];
+ return ["variable-2", "variable"];
+ },
+ "#": function(stream) {
+ if (!stream.eat("{")) return false;
+ return [null, "interpolation"];
+ }
+ },
+ name: "css",
+ helperType: "scss"
+ });
+
+ CodeMirror.defineMIME("text/x-less", {
+ mediaTypes: mediaTypes,
+ mediaFeatures: mediaFeatures,
+ mediaValueKeywords: mediaValueKeywords,
+ propertyKeywords: propertyKeywords,
+ nonStandardPropertyKeywords: nonStandardPropertyKeywords,
+ colorKeywords: colorKeywords,
+ valueKeywords: valueKeywords,
+ fontProperties: fontProperties,
+ allowNested: true,
+ tokenHooks: {
+ "/": function(stream, state) {
+ if (stream.eat("/")) {
+ stream.skipToEnd();
+ return ["comment", "comment"];
+ } else if (stream.eat("*")) {
+ state.tokenize = tokenCComment;
+ return tokenCComment(stream, state);
+ } else {
+ return ["operator", "operator"];
+ }
+ },
+ "@": function(stream) {
+ if (stream.eat("{")) return [null, "interpolation"];
+ 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"];
+ return ["variable-2", "variable"];
+ },
+ "&": function() {
+ return ["atom", "atom"];
+ }
+ },
+ name: "css",
+ helperType: "less"
+ });
+
+ CodeMirror.defineMIME("text/x-gss", {
+ documentTypes: documentTypes,
+ mediaTypes: mediaTypes,
+ mediaFeatures: mediaFeatures,
+ propertyKeywords: propertyKeywords,
+ nonStandardPropertyKeywords: nonStandardPropertyKeywords,
+ fontProperties: fontProperties,
+ counterDescriptors: counterDescriptors,
+ colorKeywords: colorKeywords,
+ valueKeywords: valueKeywords,
+ supportsAtComponent: true,
+ tokenHooks: {
+ "/": function(stream, state) {
+ if (!stream.eat("*")) return false;
+ state.tokenize = tokenCComment;
+ return tokenCComment(stream, state);
+ }
+ },
+ name: "css",
+ helperType: "gss"
+ });
+
+});
diff --git a/devtools/client/sourceeditor/codemirror/mode/htmlmixed/htmlmixed.js b/devtools/client/sourceeditor/codemirror/mode/htmlmixed/htmlmixed.js
new file mode 100644
index 000000000..d74083ee1
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/mode/htmlmixed/htmlmixed.js
@@ -0,0 +1,152 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"), require("../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";
+
+ var defaultTags = {
+ script: [
+ ["lang", /(javascript|babel)/i, "javascript"],
+ ["type", /^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^$/i, "javascript"],
+ ["type", /./, "text/plain"],
+ [null, null, "javascript"]
+ ],
+ style: [
+ ["lang", /^css$/i, "css"],
+ ["type", /^(text\/)?(x-)?(stylesheet|css)$/i, "css"],
+ ["type", /./, "text/plain"],
+ [null, null, "css"]
+ ]
+ };
+
+ function maybeBackup(stream, pat, style) {
+ var cur = stream.current(), close = cur.search(pat);
+ if (close > -1) {
+ stream.backUp(cur.length - close);
+ } else if (cur.match(/<\/?$/)) {
+ stream.backUp(cur.length);
+ if (!stream.match(pat, false)) stream.match(cur);
+ }
+ return style;
+ }
+
+ var attrRegexpCache = {};
+ function getAttrRegexp(attr) {
+ var regexp = attrRegexpCache[attr];
+ if (regexp) return regexp;
+ return attrRegexpCache[attr] = new RegExp("\\s+" + attr + "\\s*=\\s*('|\")?([^'\"]+)('|\")?\\s*");
+ }
+
+ function getAttrValue(text, attr) {
+ var match = text.match(getAttrRegexp(attr))
+ return match ? match[2] : ""
+ }
+
+ function getTagRegexp(tagName, anchored) {
+ return new RegExp((anchored ? "^" : "") + "<\/\s*" + tagName + "\s*>", "i");
+ }
+
+ function addTags(from, to) {
+ for (var tag in from) {
+ var dest = to[tag] || (to[tag] = []);
+ var source = from[tag];
+ for (var i = source.length - 1; i >= 0; i--)
+ dest.unshift(source[i])
+ }
+ }
+
+ function findMatchingMode(tagInfo, tagText) {
+ for (var i = 0; i < tagInfo.length; i++) {
+ var spec = tagInfo[i];
+ if (!spec[0] || spec[1].test(getAttrValue(tagText, spec[0]))) return spec[2];
+ }
+ }
+
+ CodeMirror.defineMode("htmlmixed", function (config, parserConfig) {
+ var htmlMode = CodeMirror.getMode(config, {
+ name: "xml",
+ htmlMode: true,
+ multilineTagIndentFactor: parserConfig.multilineTagIndentFactor,
+ multilineTagIndentPastTag: parserConfig.multilineTagIndentPastTag
+ });
+
+ var tags = {};
+ var configTags = parserConfig && parserConfig.tags, configScript = parserConfig && parserConfig.scriptTypes;
+ addTags(defaultTags, tags);
+ if (configTags) addTags(configTags, tags);
+ if (configScript) for (var i = configScript.length - 1; i >= 0; i--)
+ tags.script.unshift(["type", configScript[i].matches, configScript[i].mode])
+
+ function html(stream, state) {
+ var style = htmlMode.token(stream, state.htmlState), tag = /\btag\b/.test(style), tagName
+ if (tag && !/[<>\s\/]/.test(stream.current()) &&
+ (tagName = state.htmlState.tagName && state.htmlState.tagName.toLowerCase()) &&
+ tags.hasOwnProperty(tagName)) {
+ state.inTag = tagName + " "
+ } else if (state.inTag && tag && />$/.test(stream.current())) {
+ var inTag = /^([\S]+) (.*)/.exec(state.inTag)
+ state.inTag = null
+ var modeSpec = stream.current() == ">" && findMatchingMode(tags[inTag[1]], inTag[2])
+ var mode = CodeMirror.getMode(config, modeSpec)
+ var endTagA = getTagRegexp(inTag[1], true), endTag = getTagRegexp(inTag[1], false);
+ state.token = function (stream, state) {
+ if (stream.match(endTagA, false)) {
+ state.token = html;
+ state.localState = state.localMode = null;
+ return null;
+ }
+ return maybeBackup(stream, endTag, state.localMode.token(stream, state.localState));
+ };
+ state.localMode = mode;
+ state.localState = CodeMirror.startState(mode, htmlMode.indent(state.htmlState, ""));
+ } else if (state.inTag) {
+ state.inTag += stream.current()
+ if (stream.eol()) state.inTag += " "
+ }
+ return style;
+ };
+
+ return {
+ startState: function () {
+ var state = CodeMirror.startState(htmlMode);
+ return {token: html, inTag: null, localMode: null, localState: null, htmlState: state};
+ },
+
+ copyState: function (state) {
+ var local;
+ if (state.localState) {
+ local = CodeMirror.copyState(state.localMode, state.localState);
+ }
+ return {token: state.token, inTag: state.inTag,
+ localMode: state.localMode, localState: local,
+ htmlState: CodeMirror.copyState(htmlMode, state.htmlState)};
+ },
+
+ token: function (stream, state) {
+ return state.token(stream, state);
+ },
+
+ indent: function (state, textAfter) {
+ if (!state.localMode || /^\s*<\//.test(textAfter))
+ return htmlMode.indent(state.htmlState, textAfter);
+ else if (state.localMode.indent)
+ return state.localMode.indent(state.localState, textAfter);
+ else
+ return CodeMirror.Pass;
+ },
+
+ innerMode: function (state) {
+ return {state: state.localState || state.htmlState, mode: state.localMode || htmlMode};
+ }
+ };
+ }, "xml", "javascript", "css");
+
+ CodeMirror.defineMIME("text/html", "htmlmixed");
+});
diff --git a/devtools/client/sourceeditor/codemirror/mode/javascript/javascript.js b/devtools/client/sourceeditor/codemirror/mode/javascript/javascript.js
new file mode 100644
index 000000000..ca875411a
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/mode/javascript/javascript.js
@@ -0,0 +1,748 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+// 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";
+
+function expressionAllowed(stream, state, backUp) {
+ return /^(?:operator|sof|keyword c|case|new|[\[{}\(,;:]|=>)$/.test(state.lastType) ||
+ (state.lastType == "quasi" && /\{\s*$/.test(stream.string.slice(0, stream.pos - (backUp || 0))))
+}
+
+CodeMirror.defineMode("javascript", function(config, parserConfig) {
+ var indentUnit = config.indentUnit;
+ var statementIndent = parserConfig.statementIndent;
+ var jsonldMode = parserConfig.jsonld;
+ var jsonMode = parserConfig.json || jsonldMode;
+ var isTS = parserConfig.typescript;
+ var wordRE = parserConfig.wordCharacters || /[\w$\xa1-\uffff]/;
+
+ // Tokenizer
+
+ var keywords = function(){
+ function kw(type) {return {type: type, style: "keyword"};}
+ var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c");
+ var operator = kw("operator"), atom = {type: "atom", style: "atom"};
+
+ var jsKeywords = {
+ "if": kw("if"), "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B,
+ "return": C, "break": C, "continue": C, "new": kw("new"), "delete": C, "throw": C, "debugger": C,
+ "var": kw("var"), "const": kw("var"), "let": kw("var"),
+ "function": kw("function"), "catch": kw("catch"),
+ "for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"),
+ "in": operator, "typeof": operator, "instanceof": operator,
+ "true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom,
+ "this": kw("this"), "class": kw("class"), "super": kw("atom"),
+ "yield": C, "export": kw("export"), "import": kw("import"), "extends": C,
+ "await": C, "async": kw("async")
+ };
+
+ // Extend the 'normal' keywords with the TypeScript language extensions
+ if (isTS) {
+ var type = {type: "variable", style: "variable-3"};
+ var tsKeywords = {
+ // object-like things
+ "interface": kw("class"),
+ "implements": C,
+ "namespace": C,
+ "module": kw("module"),
+ "enum": kw("module"),
+
+ // scope modifiers
+ "public": kw("modifier"),
+ "private": kw("modifier"),
+ "protected": kw("modifier"),
+ "abstract": kw("modifier"),
+
+ // operators
+ "as": operator,
+
+ // types
+ "string": type, "number": type, "boolean": type, "any": type
+ };
+
+ for (var attr in tsKeywords) {
+ jsKeywords[attr] = tsKeywords[attr];
+ }
+ }
+
+ return jsKeywords;
+ }();
+
+ 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;
+ while ((next = stream.next()) != null) {
+ if (!escaped) {
+ if (next == "/" && !inSet) return;
+ if (next == "[") inSet = true;
+ else if (inSet && next == "]") inSet = false;
+ }
+ escaped = !escaped && next == "\\";
+ }
+ }
+
+ // Used as scratch variables to communicate multiple values without
+ // consing up tons of objects.
+ var type, content;
+ function ret(tp, style, cont) {
+ type = tp; content = cont;
+ return style;
+ }
+ function tokenBase(stream, state) {
+ var ch = stream.next();
+ if (ch == '"' || ch == "'") {
+ state.tokenize = tokenString(ch);
+ return state.tokenize(stream, state);
+ } else if (ch == "." && stream.match(/^\d+(?:[eE][+\-]?\d+)?/)) {
+ return ret("number", "number");
+ } else if (ch == "." && stream.match("..")) {
+ return ret("spread", "meta");
+ } else if (/[\[\]{}\(\),;\:\.]/.test(ch)) {
+ return ret(ch);
+ } else if (ch == "=" && stream.eat(">")) {
+ return ret("=>", "operator");
+ } else if (ch == "0" && stream.eat(/x/i)) {
+ stream.eatWhile(/[\da-f]/i);
+ return ret("number", "number");
+ } else if (ch == "0" && stream.eat(/o/i)) {
+ stream.eatWhile(/[0-7]/i);
+ return ret("number", "number");
+ } else if (ch == "0" && stream.eat(/b/i)) {
+ stream.eatWhile(/[01]/i);
+ return ret("number", "number");
+ } else if (/\d/.test(ch)) {
+ stream.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/);
+ return ret("number", "number");
+ } else if (ch == "/") {
+ if (stream.eat("*")) {
+ state.tokenize = tokenComment;
+ return tokenComment(stream, state);
+ } else if (stream.eat("/")) {
+ stream.skipToEnd();
+ return ret("comment", "comment");
+ } else if (expressionAllowed(stream, state, 1)) {
+ readRegexp(stream);
+ stream.match(/^\b(([gimyu])(?![gimyu]*\2))+\b/);
+ return ret("regexp", "string-2");
+ } else {
+ stream.eatWhile(isOperatorChar);
+ return ret("operator", "operator", stream.current());
+ }
+ } else if (ch == "`") {
+ state.tokenize = tokenQuasi;
+ return tokenQuasi(stream, state);
+ } else if (ch == "#") {
+ stream.skipToEnd();
+ return ret("error", "error");
+ } else if (isOperatorChar.test(ch)) {
+ stream.eatWhile(isOperatorChar);
+ return ret("operator", "operator", stream.current());
+ } else if (wordRE.test(ch)) {
+ stream.eatWhile(wordRE);
+ var word = stream.current(), known = keywords.propertyIsEnumerable(word) && keywords[word];
+ return (known && state.lastType != ".") ? ret(known.type, known.style, word) :
+ ret("variable", "variable", word);
+ }
+ }
+
+ 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 == "\\";
+ }
+ if (!escaped) state.tokenize = tokenBase;
+ return ret("string", "string");
+ };
+ }
+
+ function tokenComment(stream, state) {
+ var maybeEnd = false, ch;
+ while (ch = stream.next()) {
+ if (ch == "/" && maybeEnd) {
+ state.tokenize = tokenBase;
+ break;
+ }
+ maybeEnd = (ch == "*");
+ }
+ return ret("comment", "comment");
+ }
+
+ function tokenQuasi(stream, state) {
+ var escaped = false, next;
+ while ((next = stream.next()) != null) {
+ if (!escaped && (next == "`" || next == "$" && stream.eat("{"))) {
+ state.tokenize = tokenBase;
+ break;
+ }
+ escaped = !escaped && next == "\\";
+ }
+ return ret("quasi", "string-2", stream.current());
+ }
+
+ var brackets = "([{}])";
+ // This is a crude lookahead trick to try and notice that we're
+ // parsing the argument patterns for a fat-arrow function before we
+ // actually hit the arrow token. It only works if the arrow is on
+ // the same line as the arguments and there's no strange noise
+ // (comments) in between. Fallback is to only notice when we hit the
+ // arrow, and not declare the arguments as locals for the arrow
+ // body.
+ function findFatArrow(stream, state) {
+ if (state.fatArrowAt) state.fatArrowAt = null;
+ var arrow = stream.string.indexOf("=>", stream.start);
+ if (arrow < 0) return;
+
+ var depth = 0, sawSomething = false;
+ for (var pos = arrow - 1; pos >= 0; --pos) {
+ var ch = stream.string.charAt(pos);
+ var bracket = brackets.indexOf(ch);
+ if (bracket >= 0 && bracket < 3) {
+ if (!depth) { ++pos; break; }
+ if (--depth == 0) break;
+ } else if (bracket >= 3 && bracket < 6) {
+ ++depth;
+ } else if (wordRE.test(ch)) {
+ sawSomething = true;
+ } else if (/["'\/]/.test(ch)) {
+ return;
+ } else if (sawSomething && !depth) {
+ ++pos;
+ break;
+ }
+ }
+ if (sawSomething && !depth) state.fatArrowAt = pos;
+ }
+
+ // Parser
+
+ 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;
+ this.column = column;
+ this.type = type;
+ this.prev = prev;
+ this.info = info;
+ if (align != null) this.align = align;
+ }
+
+ function inScope(state, varname) {
+ for (var v = state.localVars; v; v = v.next)
+ if (v.name == varname) return true;
+ for (var cx = state.context; cx; cx = cx.prev) {
+ for (var v = cx.vars; v; v = v.next)
+ if (v.name == varname) return true;
+ }
+ }
+
+ function parseJS(state, style, type, content, stream) {
+ var cc = state.cc;
+ // Communicate our context to the combinators.
+ // (Less wasteful than consing up a hundred closures on every call.)
+ cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; cx.style = style;
+
+ if (!state.lexical.hasOwnProperty("align"))
+ state.lexical.align = true;
+
+ while(true) {
+ var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement;
+ if (combinator(type, content)) {
+ while(cc.length && cc[cc.length - 1].lex)
+ cc.pop()();
+ if (cx.marked) return cx.marked;
+ if (type == "variable" && inScope(state, content)) return "variable-2";
+ return style;
+ }
+ }
+ }
+
+ // Combinator utils
+
+ var cx = {state: null, column: null, marked: null, cc: null};
+ function pass() {
+ for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]);
+ }
+ function cont() {
+ pass.apply(null, arguments);
+ return true;
+ }
+ function register(varname) {
+ function inList(list) {
+ for (var v = list; v; v = v.next)
+ if (v.name == varname) return true;
+ return false;
+ }
+ var state = cx.state;
+ cx.marked = "def";
+ if (state.context) {
+ if (inList(state.localVars)) return;
+ state.localVars = {name: varname, next: state.localVars};
+ } else {
+ if (inList(state.globalVars)) return;
+ if (parserConfig.globalVars)
+ state.globalVars = {name: varname, next: state.globalVars};
+ }
+ }
+
+ // Combinators
+
+ var defaultVars = {name: "this", next: {name: "arguments"}};
+ function pushcontext() {
+ cx.state.context = {prev: cx.state.context, vars: cx.state.localVars};
+ cx.state.localVars = defaultVars;
+ }
+ function popcontext() {
+ cx.state.localVars = cx.state.context.vars;
+ cx.state.context = cx.state.context.prev;
+ }
+ function pushlex(type, info) {
+ var result = function() {
+ var state = cx.state, indent = state.indented;
+ if (state.lexical.type == "stat") indent = state.lexical.indented;
+ else for (var outer = state.lexical; outer && outer.type == ")" && outer.align; outer = outer.prev)
+ indent = outer.indented;
+ state.lexical = new JSLexical(indent, cx.stream.column(), type, null, state.lexical, info);
+ };
+ result.lex = true;
+ return result;
+ }
+ function poplex() {
+ var state = cx.state;
+ if (state.lexical.prev) {
+ if (state.lexical.type == ")")
+ state.indented = state.lexical.indented;
+ state.lexical = state.lexical.prev;
+ }
+ }
+ poplex.lex = true;
+
+ function expect(wanted) {
+ function exp(type) {
+ if (type == wanted) return cont();
+ else if (wanted == ";") return pass();
+ else return cont(exp);
+ };
+ return exp;
+ }
+
+ function statement(type, value) {
+ if (type == "var") return cont(pushlex("vardef", value.length), vardef, expect(";"), poplex);
+ if (type == "keyword a") return cont(pushlex("form"), expression, statement, poplex);
+ if (type == "keyword b") return cont(pushlex("form"), statement, poplex);
+ if (type == "{") return cont(pushlex("}"), block, poplex);
+ if (type == ";") return cont();
+ if (type == "if") {
+ if (cx.state.lexical.info == "else" && cx.state.cc[cx.state.cc.length - 1] == poplex)
+ cx.state.cc.pop()();
+ return cont(pushlex("form"), expression, statement, poplex, maybeelse);
+ }
+ if (type == "function") return cont(functiondef);
+ if (type == "for") return cont(pushlex("form"), forspec, statement, poplex);
+ if (type == "variable") return cont(pushlex("stat"), maybelabel);
+ if (type == "switch") return cont(pushlex("form"), expression, pushlex("}", "switch"), expect("{"),
+ block, poplex, poplex);
+ if (type == "case") return cont(expression, expect(":"));
+ if (type == "default") return cont(expect(":"));
+ if (type == "catch") return cont(pushlex("form"), pushcontext, expect("("), funarg, expect(")"),
+ statement, poplex, popcontext);
+ if (type == "class") return cont(pushlex("form"), className, poplex);
+ if (type == "export") return cont(pushlex("stat"), afterExport, poplex);
+ if (type == "import") return cont(pushlex("stat"), afterImport, poplex);
+ if (type == "module") return cont(pushlex("form"), pattern, pushlex("}"), expect("{"), block, poplex, poplex)
+ if (type == "async") return cont(statement)
+ return pass(pushlex("stat"), expression, expect(";"), poplex);
+ }
+ function expression(type) {
+ return expressionInner(type, false);
+ }
+ function expressionNoComma(type) {
+ return expressionInner(type, true);
+ }
+ function expressionInner(type, noComma) {
+ if (cx.state.fatArrowAt == cx.stream.start) {
+ var body = noComma ? arrowBodyNoComma : arrowBody;
+ if (type == "(") return cont(pushcontext, pushlex(")"), commasep(pattern, ")"), poplex, expect("=>"), body, popcontext);
+ else if (type == "variable") return pass(pushcontext, pattern, expect("=>"), body, popcontext);
+ }
+
+ var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma;
+ if (atomicTypes.hasOwnProperty(type)) return cont(maybeop);
+ if (type == "function") return cont(functiondef, maybeop);
+ if (type == "keyword c") return cont(noComma ? maybeexpressionNoComma : maybeexpression);
+ if (type == "(") return cont(pushlex(")"), maybeexpression, comprehension, expect(")"), poplex, maybeop);
+ if (type == "operator" || type == "spread") return cont(noComma ? expressionNoComma : expression);
+ if (type == "[") return cont(pushlex("]"), arrayLiteral, poplex, maybeop);
+ if (type == "{") return contCommasep(objprop, "}", null, maybeop);
+ if (type == "quasi") return pass(quasi, maybeop);
+ if (type == "new") return cont(maybeTarget(noComma));
+ return cont();
+ }
+ function maybeexpression(type) {
+ if (type.match(/[;\}\)\],]/)) return pass();
+ return pass(expression);
+ }
+ function maybeexpressionNoComma(type) {
+ if (type.match(/[;\}\)\],]/)) return pass();
+ return pass(expressionNoComma);
+ }
+
+ function maybeoperatorComma(type, value) {
+ if (type == ",") return cont(expression);
+ return maybeoperatorNoComma(type, value, false);
+ }
+ function maybeoperatorNoComma(type, value, noComma) {
+ var me = noComma == false ? maybeoperatorComma : maybeoperatorNoComma;
+ var expr = noComma == false ? expression : expressionNoComma;
+ if (type == "=>") return cont(pushcontext, noComma ? arrowBodyNoComma : arrowBody, popcontext);
+ if (type == "operator") {
+ if (/\+\+|--/.test(value)) return cont(me);
+ if (value == "?") return cont(expression, expect(":"), expr);
+ return cont(expr);
+ }
+ if (type == "quasi") { return pass(quasi, me); }
+ if (type == ";") return;
+ if (type == "(") return contCommasep(expressionNoComma, ")", "call", me);
+ if (type == ".") return cont(property, me);
+ if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me);
+ }
+ function quasi(type, value) {
+ if (type != "quasi") return pass();
+ if (value.slice(value.length - 2) != "${") return cont(quasi);
+ return cont(expression, continueQuasi);
+ }
+ function continueQuasi(type) {
+ if (type == "}") {
+ cx.marked = "string-2";
+ cx.state.tokenize = tokenQuasi;
+ return cont(quasi);
+ }
+ }
+ function arrowBody(type) {
+ findFatArrow(cx.stream, cx.state);
+ return pass(type == "{" ? statement : expression);
+ }
+ function arrowBodyNoComma(type) {
+ findFatArrow(cx.stream, cx.state);
+ return pass(type == "{" ? statement : expressionNoComma);
+ }
+ function maybeTarget(noComma) {
+ return function(type) {
+ if (type == ".") return cont(noComma ? targetNoComma : target);
+ else return pass(noComma ? expressionNoComma : expression);
+ };
+ }
+ function target(_, value) {
+ if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorComma); }
+ }
+ function targetNoComma(_, value) {
+ if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorNoComma); }
+ }
+ function maybelabel(type) {
+ if (type == ":") return cont(poplex, statement);
+ return pass(maybeoperatorComma, expect(";"), poplex);
+ }
+ function property(type) {
+ if (type == "variable") {cx.marked = "property"; return cont();}
+ }
+ function objprop(type, value) {
+ if (type == "variable" || cx.style == "keyword") {
+ cx.marked = "property";
+ if (value == "get" || value == "set") return cont(getterSetter);
+ return cont(afterprop);
+ } else if (type == "number" || type == "string") {
+ cx.marked = jsonldMode ? "property" : (cx.style + " property");
+ return cont(afterprop);
+ } else if (type == "jsonld-keyword") {
+ return cont(afterprop);
+ } else if (type == "modifier") {
+ return cont(objprop)
+ } else if (type == "[") {
+ return cont(expression, expect("]"), afterprop);
+ } else if (type == "spread") {
+ return cont(expression);
+ }
+ }
+ function getterSetter(type) {
+ if (type != "variable") return pass(afterprop);
+ cx.marked = "property";
+ return cont(functiondef);
+ }
+ function afterprop(type) {
+ if (type == ":") return cont(expressionNoComma);
+ if (type == "(") return pass(functiondef);
+ }
+ function commasep(what, end) {
+ function proceed(type, value) {
+ if (type == ",") {
+ var lex = cx.state.lexical;
+ if (lex.info == "call") lex.pos = (lex.pos || 0) + 1;
+ return cont(what, proceed);
+ }
+ if (type == end || value == end) return cont();
+ return cont(expect(end));
+ }
+ return function(type, value) {
+ if (type == end || value == end) return cont();
+ return pass(what, proceed);
+ };
+ }
+ function contCommasep(what, end, info) {
+ for (var i = 3; i < arguments.length; i++)
+ cx.cc.push(arguments[i]);
+ return cont(pushlex(end, info), commasep(what, end), poplex);
+ }
+ function block(type) {
+ if (type == "}") return cont();
+ return pass(statement, block);
+ }
+ function maybetype(type) {
+ if (isTS && type == ":") return cont(typeexpr);
+ }
+ function maybedefault(_, value) {
+ if (value == "=") return cont(expressionNoComma);
+ }
+ function typeexpr(type) {
+ if (type == "variable") {cx.marked = "variable-3"; return cont(afterType);}
+ }
+ function afterType(type, value) {
+ if (value == "<") return cont(commasep(typeexpr, ">"), afterType)
+ if (type == "[") return cont(expect("]"), afterType)
+ }
+ function vardef() {
+ return pass(pattern, maybetype, maybeAssign, vardefCont);
+ }
+ function pattern(type, value) {
+ if (type == "modifier") return cont(pattern)
+ if (type == "variable") { register(value); return cont(); }
+ if (type == "spread") return cont(pattern);
+ if (type == "[") return contCommasep(pattern, "]");
+ if (type == "{") return contCommasep(proppattern, "}");
+ }
+ function proppattern(type, value) {
+ if (type == "variable" && !cx.stream.match(/^\s*:/, false)) {
+ register(value);
+ return cont(maybeAssign);
+ }
+ if (type == "variable") cx.marked = "property";
+ if (type == "spread") return cont(pattern);
+ if (type == "}") return pass();
+ return cont(expect(":"), pattern, maybeAssign);
+ }
+ function maybeAssign(_type, value) {
+ if (value == "=") return cont(expressionNoComma);
+ }
+ function vardefCont(type) {
+ if (type == ",") return cont(vardef);
+ }
+ function maybeelse(type, value) {
+ if (type == "keyword b" && value == "else") return cont(pushlex("form", "else"), statement, poplex);
+ }
+ function forspec(type) {
+ if (type == "(") return cont(pushlex(")"), forspec1, expect(")"), poplex);
+ }
+ function forspec1(type) {
+ if (type == "var") return cont(vardef, expect(";"), forspec2);
+ if (type == ";") return cont(forspec2);
+ if (type == "variable") return cont(formaybeinof);
+ return pass(expression, expect(";"), forspec2);
+ }
+ function formaybeinof(_type, value) {
+ if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression); }
+ return cont(maybeoperatorComma, forspec2);
+ }
+ function forspec2(type, value) {
+ if (type == ";") return cont(forspec3);
+ if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression); }
+ return pass(expression, expect(";"), forspec3);
+ }
+ function forspec3(type) {
+ if (type != ")") cont(expression);
+ }
+ function functiondef(type, value) {
+ if (value == "*") {cx.marked = "keyword"; return cont(functiondef);}
+ if (type == "variable") {register(value); return cont(functiondef);}
+ if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, maybetype, statement, popcontext);
+ }
+ function funarg(type) {
+ if (type == "spread") return cont(funarg);
+ return pass(pattern, maybetype, maybedefault);
+ }
+ function className(type, value) {
+ if (type == "variable") {register(value); return cont(classNameAfter);}
+ }
+ function classNameAfter(type, value) {
+ if (value == "extends") return cont(expression, classNameAfter);
+ if (type == "{") return cont(pushlex("}"), classBody, poplex);
+ }
+ function classBody(type, value) {
+ if (type == "variable" || cx.style == "keyword") {
+ if (value == "static") {
+ cx.marked = "keyword";
+ return cont(classBody);
+ }
+ cx.marked = "property";
+ if (value == "get" || value == "set") return cont(classGetterSetter, functiondef, classBody);
+ return cont(functiondef, classBody);
+ }
+ if (value == "*") {
+ cx.marked = "keyword";
+ return cont(classBody);
+ }
+ if (type == ";") return cont(classBody);
+ if (type == "}") return cont();
+ }
+ function classGetterSetter(type) {
+ if (type != "variable") return pass();
+ cx.marked = "property";
+ return cont();
+ }
+ function afterExport(_type, value) {
+ if (value == "*") { cx.marked = "keyword"; return cont(maybeFrom, expect(";")); }
+ if (value == "default") { cx.marked = "keyword"; return cont(expression, expect(";")); }
+ return pass(statement);
+ }
+ function afterImport(type) {
+ if (type == "string") return cont();
+ return pass(importSpec, maybeFrom);
+ }
+ function importSpec(type, value) {
+ if (type == "{") return contCommasep(importSpec, "}");
+ if (type == "variable") register(value);
+ if (value == "*") cx.marked = "keyword";
+ return cont(maybeAs);
+ }
+ function maybeAs(_type, value) {
+ if (value == "as") { cx.marked = "keyword"; return cont(importSpec); }
+ }
+ function maybeFrom(_type, value) {
+ if (value == "from") { cx.marked = "keyword"; return cont(expression); }
+ }
+ function arrayLiteral(type) {
+ if (type == "]") return cont();
+ return pass(expressionNoComma, maybeArrayComprehension);
+ }
+ function maybeArrayComprehension(type) {
+ if (type == "for") return pass(comprehension, expect("]"));
+ if (type == ",") return cont(commasep(maybeexpressionNoComma, "]"));
+ return pass(commasep(expressionNoComma, "]"));
+ }
+ function comprehension(type) {
+ if (type == "for") return cont(forspec, comprehension);
+ if (type == "if") return cont(expression, comprehension);
+ }
+
+ function isContinuedStatement(state, textAfter) {
+ return state.lastType == "operator" || state.lastType == "," ||
+ isOperatorChar.test(textAfter.charAt(0)) ||
+ /[,.]/.test(textAfter.charAt(0));
+ }
+
+ // Interface
+
+ return {
+ startState: function(basecolumn) {
+ var state = {
+ tokenize: tokenBase,
+ lastType: "sof",
+ cc: [],
+ lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false),
+ localVars: parserConfig.localVars,
+ context: parserConfig.localVars && {vars: parserConfig.localVars},
+ indented: basecolumn || 0
+ };
+ if (parserConfig.globalVars && typeof parserConfig.globalVars == "object")
+ state.globalVars = parserConfig.globalVars;
+ return state;
+ },
+
+ token: function(stream, state) {
+ if (stream.sol()) {
+ if (!state.lexical.hasOwnProperty("align"))
+ state.lexical.align = false;
+ state.indented = stream.indentation();
+ findFatArrow(stream, state);
+ }
+ if (state.tokenize != tokenComment && stream.eatSpace()) return null;
+ var style = state.tokenize(stream, state);
+ if (type == "comment") return style;
+ state.lastType = type == "operator" && (content == "++" || content == "--") ? "incdec" : type;
+ return parseJS(state, style, type, content, stream);
+ },
+
+ indent: function(state, textAfter) {
+ if (state.tokenize == tokenComment) return CodeMirror.Pass;
+ if (state.tokenize != tokenBase) return 0;
+ var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical;
+ // Kludge to prevent 'maybelse' from blocking lexical scope pops
+ if (!/^\s*else\b/.test(textAfter)) for (var i = state.cc.length - 1; i >= 0; --i) {
+ var c = state.cc[i];
+ if (c == poplex) lexical = lexical.prev;
+ else if (c != maybeelse) break;
+ }
+ if (lexical.type == "stat" && firstChar == "}") lexical = lexical.prev;
+ if (statementIndent && lexical.type == ")" && lexical.prev.type == "stat")
+ lexical = lexical.prev;
+ var type = lexical.type, closing = firstChar == type;
+
+ if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info + 1 : 0);
+ else if (type == "form" && firstChar == "{") return lexical.indented;
+ else if (type == "form") return lexical.indented + indentUnit;
+ else if (type == "stat")
+ return lexical.indented + (isContinuedStatement(state, textAfter) ? statementIndent || indentUnit : 0);
+ else if (lexical.info == "switch" && !closing && parserConfig.doubleIndentSwitch != false)
+ return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit);
+ else if (lexical.align) return lexical.column + (closing ? 0 : 1);
+ else return lexical.indented + (closing ? 0 : indentUnit);
+ },
+
+ electricInput: /^\s*(?:case .*?:|default:|\{|\})$/,
+ blockCommentStart: jsonMode ? null : "/*",
+ blockCommentEnd: jsonMode ? null : "*/",
+ lineComment: jsonMode ? null : "//",
+ fold: "brace",
+ closeBrackets: "()[]{}''\"\"``",
+
+ helperType: jsonMode ? "json" : "javascript",
+ jsonldMode: jsonldMode,
+ jsonMode: jsonMode,
+
+ expressionAllowed: expressionAllowed,
+ skipExpression: function(state) {
+ var top = state.cc[state.cc.length - 1]
+ if (top == expression || top == expressionNoComma) state.cc.pop()
+ }
+ };
+});
+
+CodeMirror.registerHelper("wordChars", "javascript", /[\w$]/);
+
+CodeMirror.defineMIME("text/javascript", "javascript");
+CodeMirror.defineMIME("text/ecmascript", "javascript");
+CodeMirror.defineMIME("application/javascript", "javascript");
+CodeMirror.defineMIME("application/x-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/devtools/client/sourceeditor/codemirror/mode/wasm/wasm.js b/devtools/client/sourceeditor/codemirror/mode/wasm/wasm.js
new file mode 100644
index 000000000..2c42af391
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/mode/wasm/wasm.js
@@ -0,0 +1,203 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// WebAssembly experimental syntax highlight add-on for CodeMirror.
+
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ define(["../../lib/codemirror"], factory);
+ } else if (typeof exports !== 'undefined') {
+ factory(require("../../lib/codemirror"));
+ } else {
+ factory(root.CodeMirror);
+ }
+}(this, function (CodeMirror) {
+"use strict";
+
+var isWordChar = /[\w\$_\.\\\/@]/;
+
+function createLookupTable(list) {
+ var obj = Object.create(null);
+ list.forEach(function (key) {
+ obj[key] = true;
+ });
+ return obj;
+}
+
+CodeMirror.defineMode("wasm", function() {
+ var keywords = createLookupTable([
+ "function", "import", "export", "table", "memory", "segment", "as", "type",
+ "of", "from", "typeof", "br", "br_if", "loop", "br_table", "if", "else",
+ "call", "call_import", "call_indirect", "nop", "unreachable", "var",
+ "align", "select", "return"]);
+ var builtins = createLookupTable([
+ "i32:8", "i32:8u", "i32:8s", "i32:16", "i32:16u", "i32:16s",
+ "i64:8", "i64:8u", "i64:8s", "i64:16", "i64:16u", "i64:16s",
+ "i64:32", "i64:32u", "i64:32s",
+ "i32.add", "i32.sub", "i32.mul", "i32.div_s", "i32.div_u",
+ "i32.rem_s", "i32.rem_u", "i32.and", "i32.or", "i32.xor",
+ "i32.shl", "i32.shr_u", "i32.shr_s", "i32.rotr", "i32.rotl",
+ "i32.eq", "i32.ne", "i32.lt_s", "i32.le_s", "i32.lt_u",
+ "i32.le_u", "i32.gt_s", "i32.ge_s", "i32.gt_u", "i32.ge_u",
+ "i32.clz", "i32.ctz", "i32.popcnt", "i32.eqz", "i64.add",
+ "i64.sub", "i64.mul", "i64.div_s", "i64.div_u", "i64.rem_s",
+ "i64.rem_u", "i64.and", "i64.or", "i64.xor", "i64.shl",
+ "i64.shr_u", "i64.shr_s", "i64.rotr", "i64.rotl", "i64.eq",
+ "i64.ne", "i64.lt_s", "i64.le_s", "i64.lt_u", "i64.le_u",
+ "i64.gt_s", "i64.ge_s", "i64.gt_u", "i64.ge_u", "i64.clz",
+ "i64.ctz", "i64.popcnt", "i64.eqz", "f32.add", "f32.sub",
+ "f32.mul", "f32.div", "f32.min", "f32.max", "f32.abs",
+ "f32.neg", "f32.copysign", "f32.ceil", "f32.floor", "f32.trunc",
+ "f32.nearest", "f32.sqrt", "f32.eq", "f32.ne", "f32.lt",
+ "f32.le", "f32.gt", "f32.ge", "f64.add", "f64.sub", "f64.mul",
+ "f64.div", "f64.min", "f64.max", "f64.abs", "f64.neg",
+ "f64.copysign", "f64.ceil", "f64.floor", "f64.trunc", "f64.nearest",
+ "f64.sqrt", "f64.eq", "f64.ne", "f64.lt", "f64.le", "f64.gt",
+ "f64.ge", "i32.trunc_s/f32", "i32.trunc_s/f64", "i32.trunc_u/f32",
+ "i32.trunc_u/f64", "i32.wrap/i64", "i64.trunc_s/f32",
+ "i64.trunc_s/f64", "i64.trunc_u/f32", "i64.trunc_u/f64",
+ "i64.extend_s/i32", "i64.extend_u/i32", "f32.convert_s/i32",
+ "f32.convert_u/i32", "f32.convert_s/i64", "f32.convert_u/i64",
+ "f32.demote/f64", "f32.reinterpret/i32", "f64.convert_s/i32",
+ "f64.convert_u/i32", "f64.convert_s/i64", "f64.convert_u/i64",
+ "f64.promote/f32", "f64.reinterpret/i64", "i32.reinterpret/f32",
+ "i64.reinterpret/f64"]);
+ var dataTypes = createLookupTable(["i32", "i64", "f32", "f64"]);
+ var isUnaryOperator = /[\-!]/;
+ var operators = createLookupTable([
+ "+", "-", "*", "/", "/s", "/u", "%", "%s", "%u",
+ "<<", ">>u", ">>s", ">=", "<=", "==", "!=",
+ "<s", "<u", "<=s", "<=u", ">=s", ">=u", ">s", ">u",
+ "<", ">", "=", "&", "|", "^", "!"]);
+
+ function tokenBase(stream, state) {
+ var ch = stream.next();
+ if (ch === "$") {
+ stream.eatWhile(isWordChar);
+ return "variable";
+ }
+ if (ch === "@") {
+ stream.eatWhile(isWordChar);
+ return "meta";
+ }
+ if (ch === '"') {
+ state.tokenize = tokenString(ch);
+ return state.tokenize(stream, state);
+ }
+ if (ch == "/") {
+ if (stream.eat("*")) {
+ state.tokenize = tokenComment;
+ return tokenComment(stream, state);
+ } else if (stream.eat("/")) {
+ stream.skipToEnd();
+ return "comment";
+ }
+ }
+ if (/\d/.test(ch) ||
+ ((ch === "-" || ch === "+") && /\d/.test(stream.peek()))) {
+ stream.eatWhile(/[\w\._\-+]/);
+ return "number";
+ }
+ if (/[\[\]\(\)\{\},:]/.test(ch)) {
+ return null;
+ }
+ if (isUnaryOperator.test(ch)) {
+ return "operator";
+ }
+ stream.eatWhile(isWordChar);
+ var word = stream.current();
+
+ if (word in operators) {
+ return "operator";
+ }
+ if (word in keywords){
+ return "keyword";
+ }
+ if (word in dataTypes) {
+ if (!stream.eat(":")) {
+ return "builtin";
+ }
+ stream.eatWhile(isWordChar);
+ word = stream.current();
+ // fall thru for "builtin" check
+ }
+ if (word in builtins) {
+ return "builtin";
+ }
+
+ if (word === "Temporary") {
+ // Nightly has header with some text graphics -- skipping it.
+ state.tokenize = tokenTemporary;
+ return state.tokenize(stream, state);
+ }
+ return null;
+ }
+
+ function tokenComment(stream, state) {
+ state.commentDepth = 1;
+ var next;
+ while ((next = stream.next()) != null) {
+ if (next === "*" && stream.eat("/")) {
+ if (--state.commentDepth === 0) {
+ state.tokenize = null;
+ return "comment";
+ }
+ }
+ if (next === "/" && stream.eat("*")) {
+ // Nested comment
+ state.commentDepth++;
+ }
+ }
+ return "comment";
+ }
+
+ function tokenTemporary(stream, state) {
+ var next, endState = state.commentState;
+ // Skipping until "text support (Work In Progress):" is found.
+ while ((next = stream.next()) != null) {
+ if (endState === 0 && next === "t") {
+ endState = 1;
+ } else if (endState === 1 && next === ":") {
+ state.tokenize = null;
+ state.commentState = 0;
+ endState = 2;
+ return "comment";
+ }
+ }
+ state.commentState = endState;
+ return "comment";
+ }
+
+ function tokenString(quote) {
+ return function(stream, state) {
+ var escaped = false, next, end = false;
+ while ((next = stream.next()) != null) {
+ if (next == quote && !escaped) {
+ state.tokenize = null;
+ return "string";
+ }
+ escaped = !escaped && next === "\\";
+ }
+ return "string";
+ };
+ }
+
+ return {
+ startState: function() {
+ return {tokenize: null, commentState: 0, commentDepth: 0};
+ },
+
+ token: function(stream, state) {
+ if (stream.eatSpace()) return null;
+ var style = (state.tokenize || tokenBase)(stream, state);
+ return style;
+ }
+ };
+});
+
+CodeMirror.registerHelper("wordChars", "wasm", isWordChar);
+
+CodeMirror.defineMIME("text/wasm", "wasm");
+
+}));
diff --git a/devtools/client/sourceeditor/codemirror/mode/xml/xml.js b/devtools/client/sourceeditor/codemirror/mode/xml/xml.js
new file mode 100644
index 000000000..f987a3a3c
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/mode/xml/xml.js
@@ -0,0 +1,394 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+"use strict";
+
+var htmlConfig = {
+ autoSelfClosers: {'area': true, 'base': true, 'br': true, 'col': true, 'command': true,
+ 'embed': true, 'frame': true, 'hr': true, 'img': true, 'input': true,
+ 'keygen': true, 'link': true, 'meta': true, 'param': true, 'source': true,
+ 'track': true, 'wbr': true, 'menuitem': true},
+ implicitlyClosed: {'dd': true, 'li': true, 'optgroup': true, 'option': true, 'p': true,
+ 'rp': true, 'rt': true, 'tbody': true, 'td': true, 'tfoot': true,
+ 'th': true, 'tr': true},
+ contextGrabbers: {
+ 'dd': {'dd': true, 'dt': true},
+ 'dt': {'dd': true, 'dt': true},
+ 'li': {'li': true},
+ 'option': {'option': true, 'optgroup': true},
+ 'optgroup': {'optgroup': true},
+ 'p': {'address': true, 'article': true, 'aside': true, 'blockquote': true, 'dir': true,
+ 'div': true, 'dl': true, 'fieldset': true, 'footer': true, 'form': true,
+ 'h1': true, 'h2': true, 'h3': true, 'h4': true, 'h5': true, 'h6': true,
+ 'header': true, 'hgroup': true, 'hr': true, 'menu': true, 'nav': true, 'ol': true,
+ 'p': true, 'pre': true, 'section': true, 'table': true, 'ul': true},
+ 'rp': {'rp': true, 'rt': true},
+ 'rt': {'rp': true, 'rt': true},
+ 'tbody': {'tbody': true, 'tfoot': true},
+ 'td': {'td': true, 'th': true},
+ 'tfoot': {'tbody': true},
+ 'th': {'td': true, 'th': true},
+ 'thead': {'tbody': true, 'tfoot': true},
+ 'tr': {'tr': true}
+ },
+ doNotIndent: {"pre": true},
+ allowUnquoted: true,
+ allowMissing: true,
+ caseFold: true
+}
+
+var xmlConfig = {
+ autoSelfClosers: {},
+ implicitlyClosed: {},
+ contextGrabbers: {},
+ doNotIndent: {},
+ allowUnquoted: false,
+ allowMissing: false,
+ caseFold: false
+}
+
+CodeMirror.defineMode("xml", function(editorConf, config_) {
+ var indentUnit = editorConf.indentUnit
+ var config = {}
+ var defaults = config_.htmlMode ? htmlConfig : xmlConfig
+ for (var prop in defaults) config[prop] = defaults[prop]
+ for (var prop in config_) config[prop] = config_[prop]
+
+ // Return variables for tokenizers
+ var type, setStyle;
+
+ function inText(stream, state) {
+ function chain(parser) {
+ state.tokenize = parser;
+ return parser(stream, state);
+ }
+
+ var ch = stream.next();
+ if (ch == "<") {
+ if (stream.eat("!")) {
+ if (stream.eat("[")) {
+ if (stream.match("CDATA[")) return chain(inBlock("atom", "]]>"));
+ else return null;
+ } else if (stream.match("--")) {
+ return chain(inBlock("comment", "-->"));
+ } else if (stream.match("DOCTYPE", true, true)) {
+ stream.eatWhile(/[\w\._\-]/);
+ return chain(doctype(1));
+ } else {
+ return null;
+ }
+ } else if (stream.eat("?")) {
+ stream.eatWhile(/[\w\._\-]/);
+ state.tokenize = inBlock("meta", "?>");
+ return "meta";
+ } else {
+ type = stream.eat("/") ? "closeTag" : "openTag";
+ state.tokenize = inTag;
+ return "tag bracket";
+ }
+ } else if (ch == "&") {
+ var ok;
+ if (stream.eat("#")) {
+ if (stream.eat("x")) {
+ ok = stream.eatWhile(/[a-fA-F\d]/) && stream.eat(";");
+ } else {
+ ok = stream.eatWhile(/[\d]/) && stream.eat(";");
+ }
+ } else {
+ ok = stream.eatWhile(/[\w\.\-:]/) && stream.eat(";");
+ }
+ return ok ? "atom" : "error";
+ } else {
+ stream.eatWhile(/[^&<]/);
+ return null;
+ }
+ }
+ inText.isInText = true;
+
+ function inTag(stream, state) {
+ var ch = stream.next();
+ if (ch == ">" || (ch == "/" && stream.eat(">"))) {
+ state.tokenize = inText;
+ type = ch == ">" ? "endTag" : "selfcloseTag";
+ return "tag bracket";
+ } else if (ch == "=") {
+ type = "equals";
+ return null;
+ } else if (ch == "<") {
+ state.tokenize = inText;
+ state.state = baseState;
+ state.tagName = state.tagStart = null;
+ var next = state.tokenize(stream, state);
+ return next ? next + " tag error" : "tag error";
+ } else if (/[\'\"]/.test(ch)) {
+ state.tokenize = inAttribute(ch);
+ state.stringStartCol = stream.column();
+ return state.tokenize(stream, state);
+ } else {
+ stream.match(/^[^\s\u00a0=<>\"\']*[^\s\u00a0=<>\"\'\/]/);
+ return "word";
+ }
+ }
+
+ function inAttribute(quote) {
+ var closure = function(stream, state) {
+ while (!stream.eol()) {
+ if (stream.next() == quote) {
+ state.tokenize = inTag;
+ break;
+ }
+ }
+ return "string";
+ };
+ closure.isInAttribute = true;
+ return closure;
+ }
+
+ function inBlock(style, terminator) {
+ return function(stream, state) {
+ while (!stream.eol()) {
+ if (stream.match(terminator)) {
+ state.tokenize = inText;
+ break;
+ }
+ stream.next();
+ }
+ return style;
+ };
+ }
+ function doctype(depth) {
+ return function(stream, state) {
+ var ch;
+ while ((ch = stream.next()) != null) {
+ if (ch == "<") {
+ state.tokenize = doctype(depth + 1);
+ return state.tokenize(stream, state);
+ } else if (ch == ">") {
+ if (depth == 1) {
+ state.tokenize = inText;
+ break;
+ } else {
+ state.tokenize = doctype(depth - 1);
+ return state.tokenize(stream, state);
+ }
+ }
+ }
+ return "meta";
+ };
+ }
+
+ function Context(state, tagName, startOfLine) {
+ this.prev = state.context;
+ this.tagName = tagName;
+ this.indent = state.indented;
+ this.startOfLine = startOfLine;
+ if (config.doNotIndent.hasOwnProperty(tagName) || (state.context && state.context.noIndent))
+ this.noIndent = true;
+ }
+ function popContext(state) {
+ if (state.context) state.context = state.context.prev;
+ }
+ function maybePopContext(state, nextTagName) {
+ var parentTagName;
+ while (true) {
+ if (!state.context) {
+ return;
+ }
+ parentTagName = state.context.tagName;
+ if (!config.contextGrabbers.hasOwnProperty(parentTagName) ||
+ !config.contextGrabbers[parentTagName].hasOwnProperty(nextTagName)) {
+ return;
+ }
+ popContext(state);
+ }
+ }
+
+ function baseState(type, stream, state) {
+ if (type == "openTag") {
+ state.tagStart = stream.column();
+ return tagNameState;
+ } else if (type == "closeTag") {
+ return closeTagNameState;
+ } else {
+ return baseState;
+ }
+ }
+ function tagNameState(type, stream, state) {
+ if (type == "word") {
+ state.tagName = stream.current();
+ setStyle = "tag";
+ return attrState;
+ } else {
+ setStyle = "error";
+ return tagNameState;
+ }
+ }
+ function closeTagNameState(type, stream, state) {
+ if (type == "word") {
+ var tagName = stream.current();
+ if (state.context && state.context.tagName != tagName &&
+ config.implicitlyClosed.hasOwnProperty(state.context.tagName))
+ popContext(state);
+ if ((state.context && state.context.tagName == tagName) || config.matchClosing === false) {
+ setStyle = "tag";
+ return closeState;
+ } else {
+ setStyle = "tag error";
+ return closeStateErr;
+ }
+ } else {
+ setStyle = "error";
+ return closeStateErr;
+ }
+ }
+
+ function closeState(type, _stream, state) {
+ if (type != "endTag") {
+ setStyle = "error";
+ return closeState;
+ }
+ popContext(state);
+ return baseState;
+ }
+ function closeStateErr(type, stream, state) {
+ setStyle = "error";
+ return closeState(type, stream, state);
+ }
+
+ function attrState(type, _stream, state) {
+ if (type == "word") {
+ setStyle = "attribute";
+ return attrEqState;
+ } else if (type == "endTag" || type == "selfcloseTag") {
+ var tagName = state.tagName, tagStart = state.tagStart;
+ state.tagName = state.tagStart = null;
+ if (type == "selfcloseTag" ||
+ config.autoSelfClosers.hasOwnProperty(tagName)) {
+ maybePopContext(state, tagName);
+ } else {
+ maybePopContext(state, tagName);
+ state.context = new Context(state, tagName, tagStart == state.indented);
+ }
+ return baseState;
+ }
+ setStyle = "error";
+ return attrState;
+ }
+ function attrEqState(type, stream, state) {
+ if (type == "equals") return attrValueState;
+ if (!config.allowMissing) setStyle = "error";
+ return attrState(type, stream, state);
+ }
+ function attrValueState(type, stream, state) {
+ if (type == "string") return attrContinuedState;
+ if (type == "word" && config.allowUnquoted) {setStyle = "string"; return attrState;}
+ setStyle = "error";
+ return attrState(type, stream, state);
+ }
+ function attrContinuedState(type, stream, state) {
+ if (type == "string") return attrContinuedState;
+ return attrState(type, stream, state);
+ }
+
+ return {
+ startState: function(baseIndent) {
+ var state = {tokenize: inText,
+ state: baseState,
+ indented: baseIndent || 0,
+ tagName: null, tagStart: null,
+ context: null}
+ if (baseIndent != null) state.baseIndent = baseIndent
+ return state
+ },
+
+ token: function(stream, state) {
+ if (!state.tagName && stream.sol())
+ state.indented = stream.indentation();
+
+ if (stream.eatSpace()) return null;
+ type = null;
+ var style = state.tokenize(stream, state);
+ if ((style || type) && style != "comment") {
+ setStyle = null;
+ state.state = state.state(type || style, stream, state);
+ if (setStyle)
+ style = setStyle == "error" ? style + " error" : setStyle;
+ }
+ return style;
+ },
+
+ indent: function(state, textAfter, fullLine) {
+ var context = state.context;
+ // Indent multi-line strings (e.g. css).
+ if (state.tokenize.isInAttribute) {
+ if (state.tagStart == state.indented)
+ return state.stringStartCol + 1;
+ else
+ return state.indented + indentUnit;
+ }
+ if (context && context.noIndent) return CodeMirror.Pass;
+ if (state.tokenize != inTag && state.tokenize != inText)
+ return fullLine ? fullLine.match(/^(\s*)/)[0].length : 0;
+ // Indent the starts of attribute names.
+ if (state.tagName) {
+ if (config.multilineTagIndentPastTag !== false)
+ return state.tagStart + state.tagName.length + 2;
+ else
+ return state.tagStart + indentUnit * (config.multilineTagIndentFactor || 1);
+ }
+ if (config.alignCDATA && /<!\[CDATA\[/.test(textAfter)) return 0;
+ var tagAfter = textAfter && /^<(\/)?([\w_:\.-]*)/.exec(textAfter);
+ if (tagAfter && tagAfter[1]) { // Closing tag spotted
+ while (context) {
+ if (context.tagName == tagAfter[2]) {
+ context = context.prev;
+ break;
+ } else if (config.implicitlyClosed.hasOwnProperty(context.tagName)) {
+ context = context.prev;
+ } else {
+ break;
+ }
+ }
+ } else if (tagAfter) { // Opening tag spotted
+ while (context) {
+ var grabbers = config.contextGrabbers[context.tagName];
+ if (grabbers && grabbers.hasOwnProperty(tagAfter[2]))
+ context = context.prev;
+ else
+ break;
+ }
+ }
+ while (context && context.prev && !context.startOfLine)
+ context = context.prev;
+ if (context) return context.indent + indentUnit;
+ else return state.baseIndent || 0;
+ },
+
+ electricInput: /<\/[\s\w:]+>$/,
+ blockCommentStart: "<!--",
+ blockCommentEnd: "-->",
+
+ configuration: config.htmlMode ? "html" : "xml",
+ helperType: config.htmlMode ? "html" : "xml",
+
+ skipAttribute: function(state) {
+ if (state.state == attrValueState)
+ state.state = attrState
+ }
+ };
+});
+
+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/devtools/client/sourceeditor/codemirror/mozilla.css b/devtools/client/sourceeditor/codemirror/mozilla.css
new file mode 100644
index 000000000..96aeef2af
--- /dev/null
+++ b/devtools/client/sourceeditor/codemirror/mozilla.css
@@ -0,0 +1,263 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+:root {
+ --breakpoint-background: url("chrome://devtools/skin/images/breakpoint.svg#light");
+ --breakpoint-hover-background: url("chrome://devtools/skin/images/breakpoint.svg#light-hover");
+ --breakpoint-active-color: rgba(44,187,15,.2);
+ --breakpoint-conditional-background: url("chrome://devtools/skin/images/breakpoint.svg#light-conditional");
+}
+
+.theme-dark:root {
+ --breakpoint-background: url("chrome://devtools/skin/images/breakpoint.svg#dark");
+ --breakpoint-hover-background: url("chrome://devtools/skin/images/breakpoint.svg#dark-hover");
+ --breakpoint-active-color: rgba(112,191,83,.4);
+ --breakpoint-conditional-background: url("chrome://devtools/skin/images/breakpoint.svg#dark-conditional");
+}
+
+.CodeMirror {
+ height: 100%;
+ cursor: text;
+}
+
+.CodeMirror .errors {
+ width: 16px;
+}
+
+.CodeMirror .error {
+ display: inline-block;
+ margin-left: 5px;
+ width: 12px;
+ height: 12px;
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: contain;
+ background-image: url("chrome://devtools/skin/images/editor-error.png");
+ opacity: 0.75;
+}
+
+.CodeMirror .hit-counts {
+ width: 6px;
+}
+
+.CodeMirror .hit-count {
+ display: inline-block;
+ height: 12px;
+ border: solid rgba(0,0,0,0.2);
+ border-width: 1px 1px 1px 0;
+ border-radius: 0 3px 3px 0;
+ padding: 0 3px;
+ font-size: 10px;
+ pointer-events: none;
+}
+
+.CodeMirror-linenumber:before {
+ content: " ";
+ display: block;
+ width: calc(100% - 3px);
+ position: absolute;
+ top: 1px;
+ left: 0;
+ height: 12px;
+ z-index: -1;
+ background-size: calc(100% - 2px) 12px;
+ background-repeat: no-repeat;
+ background-position: right center;
+ padding-inline-end: 9px;
+}
+
+.breakpoint .CodeMirror-linenumber {
+ color: var(--theme-body-background);
+}
+
+.breakpoint .CodeMirror-linenumber:before {
+ background-image: var(--breakpoint-background) !important;
+}
+
+.conditional .CodeMirror-linenumber:before {
+ background-image: var(--breakpoint-conditional-background) !important;
+}
+
+.debug-line .CodeMirror-linenumber {
+ background-color: var(--breakpoint-active-color);
+}
+
+.theme-dark .debug-line .CodeMirror-linenumber {
+ color: #c0c0c0;
+}
+
+.debug-line .CodeMirror-line {
+ background-color: var(--breakpoint-active-color) !important;
+}
+
+/* Don't display the highlight color since the debug line
+ is already highlighted */
+.debug-line .CodeMirror-activeline-background {
+ display: none;
+}
+
+.CodeMirror-gutters {
+ cursor: default;
+}
+
+/* This is to avoid the fake horizontal scrollbar div of codemirror to go 0
+height when floating scrollbars are active. Make sure that this value is equal
+to the maximum of `min-height` specific to the `scrollbar[orient="horizontal"]`
+selector in floating-scrollbar-light.css across all platforms. */
+.CodeMirror-hscrollbar {
+ min-height: 10px;
+}
+
+/* This is to avoid the fake vertical scrollbar div of codemirror to go 0
+width when floating scrollbars are active. Make sure that this value is equal
+to the maximum of `min-width` specific to the `scrollbar[orient="vertical"]`
+selector in floating-scrollbar-light.css across all platforms. */
+.CodeMirror-vscrollbar {
+ min-width: 10px;
+}
+
+.cm-trailingspace {
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAACCAYAAAB/qH1jAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QUXCToH00Y1UgAAACFJREFUCNdjPMDBUc/AwNDAAAFMTAwMDA0OP34wQgX/AQBYgwYEx4f9lQAAAABJRU5ErkJggg==");
+ opacity: 0.75;
+ background-position: left bottom;
+ background-repeat: repeat-x;
+}
+
+/* CodeMirror dialogs styling */
+
+.CodeMirror-dialog {
+ padding: 4px 3px;
+}
+
+.CodeMirror-dialog,
+.CodeMirror-dialog input {
+ font: message-box;
+}
+
+/* Fold addon */
+
+.CodeMirror-foldmarker {
+ color: blue;
+ text-shadow: #b9f 1px 1px 2px, #b9f -1px -1px 2px, #b9f 1px -1px 2px, #b9f -1px 1px 2px;
+ font-family: sans-serif;
+ line-height: .3;
+ cursor: pointer;
+}
+
+.CodeMirror-foldgutter {
+ width: 16px; /* Same as breakpoints gutter above */
+}
+
+.CodeMirror-foldgutter-open,
+.CodeMirror-foldgutter-folded {
+ color: #555;
+ cursor: pointer;
+}
+
+.CodeMirror-foldgutter-open:after {
+ font-size: 120%;
+ content: "\25BE";
+}
+
+.CodeMirror-foldgutter-folded:after {
+ font-size: 120%;
+ content: "\25B8";
+}
+
+.CodeMirror-hints {
+ position: absolute;
+ z-index: 10;
+ overflow: hidden;
+ list-style: none;
+ margin: 0;
+ padding: 2px;
+ border-radius: 3px;
+ font-size: 90%;
+ max-height: 20em;
+ overflow-y: auto;
+}
+
+.CodeMirror-hint {
+ margin: 0;
+ padding: 0 4px;
+ border-radius: 2px;
+ max-width: 19em;
+ overflow: hidden;
+ white-space: pre;
+ cursor: pointer;
+}
+
+.CodeMirror-Tern-completion {
+ padding-inline-start: 22px;
+ position: relative;
+ line-height: 18px;
+}
+
+.CodeMirror-Tern-completion:before {
+ position: absolute;
+ left: 2px;
+ bottom: 2px;
+ border-radius: 50%;
+ font-size: 12px;
+ font-weight: bold;
+ height: 15px;
+ width: 15px;
+ line-height: 16px;
+ text-align: center;
+ color: #ffffff;
+ box-sizing: border-box;
+}
+
+.CodeMirror-Tern-completion-unknown:before {
+ content: "?";
+}
+
+.CodeMirror-Tern-completion-object:before {
+ content: "O";
+}
+
+.CodeMirror-Tern-completion-fn:before {
+ content: "F";
+}
+
+.CodeMirror-Tern-completion-array:before {
+ content: "A";
+}
+
+.CodeMirror-Tern-completion-number:before {
+ content: "N";
+}
+
+.CodeMirror-Tern-completion-string:before {
+ content: "S";
+}
+
+.CodeMirror-Tern-completion-bool:before {
+ content: "B";
+}
+
+.CodeMirror-Tern-completion-guess {
+ color: #999;
+}
+
+.CodeMirror-Tern-tooltip {
+ border-radius: 3px;
+ padding: 2px 5px;
+ white-space: pre-wrap;
+ max-width: 40em;
+ position: absolute;
+ z-index: 10;
+}
+
+.CodeMirror-Tern-hint-doc {
+ max-width: 25em;
+}
+
+.CodeMirror-Tern-farg-current {
+ text-decoration: underline;
+}
+
+.CodeMirror-Tern-fhint-guess {
+ opacity: .7;
+}
diff --git a/devtools/client/sourceeditor/css-autocompleter.js b/devtools/client/sourceeditor/css-autocompleter.js
new file mode 100644
index 000000000..46c00cc5d
--- /dev/null
+++ b/devtools/client/sourceeditor/css-autocompleter.js
@@ -0,0 +1,1214 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* eslint-disable complexity */
+const {cssTokenizer, cssTokenizerWithLineColumn} = require("devtools/shared/css/parsing-utils");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+/**
+ * Here is what this file (+ css-parsing-utils.js) do.
+ *
+ * The main objective here is to provide as much suggestions to the user editing
+ * a stylesheet in Style Editor. The possible things that can be suggested are:
+ * - CSS property names
+ * - CSS property values
+ * - CSS Selectors
+ * - Some other known CSS keywords
+ *
+ * Gecko provides a list of both property names and their corresponding values.
+ * We take out a list of matching selectors using the Inspector actor's
+ * `getSuggestionsForQuery` method. Now the only thing is to parse the CSS being
+ * edited by the user, figure out what token or word is being written and last
+ * but the most difficult, what is being edited.
+ *
+ * The file 'css-parsing-utils' helps to convert the CSS into meaningful tokens,
+ * each having a certain type associated with it. These tokens help us to figure
+ * out the currently edited word and to write a CSS state machine to figure out
+ * what the user is currently editing. By that, I mean, whether he is editing a
+ * selector or a property or a value, or even fine grained information like an
+ * id in the selector.
+ *
+ * The `resolveState` method iterated over the tokens spitted out by the
+ * tokenizer, using switch cases, follows a state machine logic and finally
+ * figures out these informations:
+ * - The state of the CSS at the cursor (one out of CSS_STATES)
+ * - The current token that is being edited `cmpleting`
+ * - If the state is "selector", the selector state (one of SELECTOR_STATES)
+ * - If the state is "selector", the current selector till the cursor
+ * - If the state is "value", the corresponding property name
+ *
+ * In case of "value" and "property" states, we simply use the information
+ * provided by Gecko to filter out the possible suggestions.
+ * For "selector" state, we request the Inspector actor to query the page DOM
+ * and filter out the possible suggestions.
+ * For "media" and "keyframes" state, the only possible suggestions for now are
+ * "media" and "keyframes" respectively, although "media" can have suggestions
+ * like "max-width", "orientation" etc. Similarly "value" state can also have
+ * much better logical suggestions if we fine grain identify a sub state just
+ * like we do for the "selector" state.
+ */
+
+// Autocompletion types.
+
+/* eslint-disable no-inline-comments */
+const CSS_STATES = {
+ "null": "null",
+ property: "property", // foo { bar|: … }
+ value: "value", // foo {bar: baz|}
+ selector: "selector", // f| {bar: baz}
+ media: "media", // @med| , or , @media scr| { }
+ keyframes: "keyframes", // @keyf|
+ frame: "frame", // @keyframs foobar { t|
+};
+
+const SELECTOR_STATES = {
+ "null": "null",
+ id: "id", // #f|
+ class: "class", // #foo.b|
+ tag: "tag", // fo|
+ pseudo: "pseudo", // foo:|
+ attribute: "attribute", // foo[b|
+ value: "value", // foo[bar=b|
+};
+/* eslint-enable no-inline-comments */
+
+/**
+ * Constructor for the autocompletion object.
+ *
+ * @param options {Object} An options object containing the following options:
+ * - walker {Object} The object used for query selecting from the current
+ * target's DOM.
+ * - maxEntries {Number} Maximum selectors suggestions to display.
+ * - cssProperties {Object} The database of CSS properties.
+ */
+function CSSCompleter(options = {}) {
+ this.walker = options.walker;
+ this.maxEntries = options.maxEntries || 15;
+ // If no css properties database is passed in, default to the client list.
+ this.cssProperties = options.cssProperties || getClientCssProperties();
+
+ this.propertyNames = this.cssProperties.getNames().sort();
+
+ // Array containing the [line, ch, scopeStack] for the locations where the
+ // CSS state is "null"
+ this.nullStates = [];
+}
+
+CSSCompleter.prototype = {
+
+ /**
+ * Returns a list of suggestions based on the caret position.
+ *
+ * @param source {String} String of the source code.
+ * @param caret {Object} Cursor location with line and ch properties.
+ *
+ * @returns [{object}] A sorted list of objects containing the following
+ * peroperties:
+ * - label {String} Full keyword for the suggestion
+ * - preLabel {String} Already entered part of the label
+ */
+ complete: function (source, caret) {
+ // Getting the context from the caret position.
+ if (!this.resolveState(source, caret)) {
+ // We couldn't resolve the context, we won't be able to complete.
+ return Promise.resolve([]);
+ }
+
+ // Properly suggest based on the state.
+ switch (this.state) {
+ case CSS_STATES.property:
+ return this.completeProperties(this.completing);
+
+ case CSS_STATES.value:
+ return this.completeValues(this.propertyName, this.completing);
+
+ case CSS_STATES.selector:
+ return this.suggestSelectors();
+
+ case CSS_STATES.media:
+ case CSS_STATES.keyframes:
+ if ("media".startsWith(this.completing)) {
+ return Promise.resolve([{
+ label: "media",
+ preLabel: this.completing,
+ text: "media"
+ }]);
+ } else if ("keyframes".startsWith(this.completing)) {
+ return Promise.resolve([{
+ label: "keyframes",
+ preLabel: this.completing,
+ text: "keyframes"
+ }]);
+ }
+ }
+ return Promise.resolve([]);
+ },
+
+ /**
+ * Resolves the state of CSS at the cursor location. This method implements a
+ * custom written CSS state machine. The various switch statements provide the
+ * transition rules for the state. It also finds out various informatino about
+ * the nearby CSS like the property name being completed, the complete
+ * selector, etc.
+ *
+ * @param source {String} String of the source code.
+ * @param caret {Object} Cursor location with line and ch properties.
+ *
+ * @returns CSS_STATE
+ * One of CSS_STATE enum or null if the state cannot be resolved.
+ */
+ resolveState: function (source, {line, ch}) {
+ // Function to return the last element of an array
+ let peek = arr => arr[arr.length - 1];
+ // _state can be one of CSS_STATES;
+ let _state = CSS_STATES.null;
+ let selector = "";
+ let selectorState = SELECTOR_STATES.null;
+ let propertyName = null;
+ let scopeStack = [];
+ let selectors = [];
+
+ // Fetch the closest null state line, ch from cached null state locations
+ let matchedStateIndex = this.findNearestNullState(line);
+ if (matchedStateIndex > -1) {
+ let state = this.nullStates[matchedStateIndex];
+ line -= state[0];
+ if (line == 0) {
+ ch -= state[1];
+ }
+ source = source.split("\n").slice(state[0]);
+ source[0] = source[0].slice(state[1]);
+ source = source.join("\n");
+ scopeStack = [...state[2]];
+ this.nullStates.length = matchedStateIndex + 1;
+ } else {
+ this.nullStates = [];
+ }
+ let tokens = cssTokenizerWithLineColumn(source);
+ let tokIndex = tokens.length - 1;
+ if (tokIndex >= 0 &&
+ (tokens[tokIndex].loc.end.line < line ||
+ (tokens[tokIndex].loc.end.line === line &&
+ tokens[tokIndex].loc.end.column < ch))) {
+ // If the last token ends before the cursor location, we didn't
+ // tokenize it correctly. This special case can happen if the
+ // final token is a comment.
+ return null;
+ }
+
+ let cursor = 0;
+ // This will maintain a stack of paired elements like { & }, @m & }, : & ;
+ // etc
+ let token = null;
+ let selectorBeforeNot = null;
+ while (cursor <= tokIndex && (token = tokens[cursor++])) {
+ switch (_state) {
+ case CSS_STATES.property:
+ // From CSS_STATES.property, we can either go to CSS_STATES.value
+ // state when we hit the first ':' or CSS_STATES.selector if "}" is
+ // reached.
+ if (token.tokenType === "symbol") {
+ switch (token.text) {
+ case ":":
+ scopeStack.push(":");
+ if (tokens[cursor - 2].tokenType != "whitespace") {
+ propertyName = tokens[cursor - 2].text;
+ } else {
+ propertyName = tokens[cursor - 3].text;
+ }
+ _state = CSS_STATES.value;
+ break;
+
+ case "}":
+ if (/[{f]/.test(peek(scopeStack))) {
+ let popped = scopeStack.pop();
+ if (popped == "f") {
+ _state = CSS_STATES.frame;
+ } else {
+ selector = "";
+ selectors = [];
+ _state = CSS_STATES.null;
+ }
+ }
+ break;
+ }
+ }
+ break;
+
+ case CSS_STATES.value:
+ // From CSS_STATES.value, we can go to one of CSS_STATES.property,
+ // CSS_STATES.frame, CSS_STATES.selector and CSS_STATES.null
+ if (token.tokenType === "symbol") {
+ switch (token.text) {
+ case ";":
+ if (/[:]/.test(peek(scopeStack))) {
+ scopeStack.pop();
+ _state = CSS_STATES.property;
+ }
+ break;
+
+ case "}":
+ if (peek(scopeStack) == ":") {
+ scopeStack.pop();
+ }
+
+ if (/[{f]/.test(peek(scopeStack))) {
+ let popped = scopeStack.pop();
+ if (popped == "f") {
+ _state = CSS_STATES.frame;
+ } else {
+ selector = "";
+ selectors = [];
+ _state = CSS_STATES.null;
+ }
+ }
+ break;
+ }
+ }
+ break;
+
+ case CSS_STATES.selector:
+ // From CSS_STATES.selector, we can only go to CSS_STATES.property
+ // when we hit "{"
+ if (token.tokenType === "symbol" && token.text == "{") {
+ scopeStack.push("{");
+ _state = CSS_STATES.property;
+ selectors.push(selector);
+ selector = "";
+ break;
+ }
+
+ switch (selectorState) {
+ case SELECTOR_STATES.id:
+ case SELECTOR_STATES.class:
+ case SELECTOR_STATES.tag:
+ switch (token.tokenType) {
+ case "hash":
+ case "id":
+ selectorState = SELECTOR_STATES.id;
+ selector += "#" + token.text;
+ break;
+
+ case "symbol":
+ if (token.text == ".") {
+ selectorState = SELECTOR_STATES.class;
+ selector += ".";
+ if (cursor <= tokIndex &&
+ tokens[cursor].tokenType == "ident") {
+ token = tokens[cursor++];
+ selector += token.text;
+ }
+ } else if (token.text == "#") {
+ selectorState = SELECTOR_STATES.id;
+ selector += "#";
+ } else if (/[>~+]/.test(token.text)) {
+ selectorState = SELECTOR_STATES.null;
+ selector += token.text;
+ } else if (token.text == ",") {
+ selectorState = SELECTOR_STATES.null;
+ selectors.push(selector);
+ selector = "";
+ } else if (token.text == ":") {
+ selectorState = SELECTOR_STATES.pseudo;
+ selector += ":";
+ if (cursor > tokIndex) {
+ break;
+ }
+
+ token = tokens[cursor++];
+ switch (token.tokenType) {
+ case "function":
+ if (token.text == "not") {
+ selectorBeforeNot = selector;
+ selector = "";
+ scopeStack.push("(");
+ } else {
+ selector += token.text + "(";
+ }
+ selectorState = SELECTOR_STATES.null;
+ break;
+
+ case "ident":
+ selector += token.text;
+ break;
+ }
+ } else if (token.text == "[") {
+ selectorState = SELECTOR_STATES.attribute;
+ scopeStack.push("[");
+ selector += "[";
+ } else if (token.text == ")") {
+ if (peek(scopeStack) == "(") {
+ scopeStack.pop();
+ selector = selectorBeforeNot + "not(" + selector + ")";
+ selectorBeforeNot = null;
+ } else {
+ selector += ")";
+ }
+ selectorState = SELECTOR_STATES.null;
+ }
+ break;
+
+ case "whitespace":
+ selectorState = SELECTOR_STATES.null;
+ selector && (selector += " ");
+ break;
+ }
+ break;
+
+ case SELECTOR_STATES.null:
+ // From SELECTOR_STATES.null state, we can go to one of
+ // SELECTOR_STATES.id, SELECTOR_STATES.class or
+ // SELECTOR_STATES.tag
+ switch (token.tokenType) {
+ case "hash":
+ case "id":
+ selectorState = SELECTOR_STATES.id;
+ selector += "#" + token.text;
+ break;
+
+ case "ident":
+ selectorState = SELECTOR_STATES.tag;
+ selector += token.text;
+ break;
+
+ case "symbol":
+ if (token.text == ".") {
+ selectorState = SELECTOR_STATES.class;
+ selector += ".";
+ if (cursor <= tokIndex &&
+ tokens[cursor].tokenType == "ident") {
+ token = tokens[cursor++];
+ selector += token.text;
+ }
+ } else if (token.text == "#") {
+ selectorState = SELECTOR_STATES.id;
+ selector += "#";
+ } else if (token.text == "*") {
+ selectorState = SELECTOR_STATES.tag;
+ selector += "*";
+ } else if (/[>~+]/.test(token.text)) {
+ selector += token.text;
+ } else if (token.text == ",") {
+ selectorState = SELECTOR_STATES.null;
+ selectors.push(selector);
+ selector = "";
+ } else if (token.text == ":") {
+ selectorState = SELECTOR_STATES.pseudo;
+ selector += ":";
+ if (cursor > tokIndex) {
+ break;
+ }
+
+ token = tokens[cursor++];
+ switch (token.tokenType) {
+ case "function":
+ if (token.text == "not") {
+ selectorBeforeNot = selector;
+ selector = "";
+ scopeStack.push("(");
+ } else {
+ selector += token.text + "(";
+ }
+ selectorState = SELECTOR_STATES.null;
+ break;
+
+ case "ident":
+ selector += token.text;
+ break;
+ }
+ } else if (token.text == "[") {
+ selectorState = SELECTOR_STATES.attribute;
+ scopeStack.push("[");
+ selector += "[";
+ } else if (token.text == ")") {
+ if (peek(scopeStack) == "(") {
+ scopeStack.pop();
+ selector = selectorBeforeNot + "not(" + selector + ")";
+ selectorBeforeNot = null;
+ } else {
+ selector += ")";
+ }
+ selectorState = SELECTOR_STATES.null;
+ }
+ break;
+
+ case "whitespace":
+ selector && (selector += " ");
+ break;
+ }
+ break;
+
+ case SELECTOR_STATES.pseudo:
+ switch (token.tokenType) {
+ case "symbol":
+ if (/[>~+]/.test(token.text)) {
+ selectorState = SELECTOR_STATES.null;
+ selector += token.text;
+ } else if (token.text == ",") {
+ selectorState = SELECTOR_STATES.null;
+ selectors.push(selector);
+ selector = "";
+ } else if (token.text == ":") {
+ selectorState = SELECTOR_STATES.pseudo;
+ selector += ":";
+ if (cursor > tokIndex) {
+ break;
+ }
+
+ token = tokens[cursor++];
+ switch (token.tokenType) {
+ case "function":
+ if (token.text == "not") {
+ selectorBeforeNot = selector;
+ selector = "";
+ scopeStack.push("(");
+ } else {
+ selector += token.text + "(";
+ }
+ selectorState = SELECTOR_STATES.null;
+ break;
+
+ case "ident":
+ selector += token.text;
+ break;
+ }
+ } else if (token.text == "[") {
+ selectorState = SELECTOR_STATES.attribute;
+ scopeStack.push("[");
+ selector += "[";
+ }
+ break;
+
+ case "whitespace":
+ selectorState = SELECTOR_STATES.null;
+ selector && (selector += " ");
+ break;
+ }
+ break;
+
+ case SELECTOR_STATES.attribute:
+ switch (token.tokenType) {
+ case "symbol":
+ if (/[~|^$*]/.test(token.text)) {
+ selector += token.text;
+ token = tokens[cursor++];
+ } else if (token.text == "=") {
+ selectorState = SELECTOR_STATES.value;
+ selector += token.text;
+ } else if (token.text == "]") {
+ if (peek(scopeStack) == "[") {
+ scopeStack.pop();
+ }
+
+ selectorState = SELECTOR_STATES.null;
+ selector += "]";
+ }
+ break;
+
+ case "ident":
+ case "string":
+ selector += token.text;
+ break;
+
+ case "whitespace":
+ selector && (selector += " ");
+ break;
+ }
+ break;
+
+ case SELECTOR_STATES.value:
+ switch (token.tokenType) {
+ case "string":
+ case "ident":
+ selector += token.text;
+ break;
+
+ case "symbol":
+ if (token.text == "]") {
+ if (peek(scopeStack) == "[") {
+ scopeStack.pop();
+ }
+
+ selectorState = SELECTOR_STATES.null;
+ selector += "]";
+ }
+ break;
+
+ case "whitespace":
+ selector && (selector += " ");
+ break;
+ }
+ break;
+ }
+ break;
+
+ case CSS_STATES.null:
+ // From CSS_STATES.null state, we can go to either CSS_STATES.media or
+ // CSS_STATES.selector.
+ switch (token.tokenType) {
+ case "hash":
+ case "id":
+ selectorState = SELECTOR_STATES.id;
+ selector = "#" + token.text;
+ _state = CSS_STATES.selector;
+ break;
+
+ case "ident":
+ selectorState = SELECTOR_STATES.tag;
+ selector = token.text;
+ _state = CSS_STATES.selector;
+ break;
+
+ case "symbol":
+ if (token.text == ".") {
+ selectorState = SELECTOR_STATES.class;
+ selector = ".";
+ _state = CSS_STATES.selector;
+ if (cursor <= tokIndex &&
+ tokens[cursor].tokenType == "ident") {
+ token = tokens[cursor++];
+ selector += token.text;
+ }
+ } else if (token.text == "#") {
+ selectorState = SELECTOR_STATES.id;
+ selector = "#";
+ _state = CSS_STATES.selector;
+ } else if (token.text == "*") {
+ selectorState = SELECTOR_STATES.tag;
+ selector = "*";
+ _state = CSS_STATES.selector;
+ } else if (token.text == ":") {
+ _state = CSS_STATES.selector;
+ selectorState = SELECTOR_STATES.pseudo;
+ selector += ":";
+ if (cursor > tokIndex) {
+ break;
+ }
+
+ token = tokens[cursor++];
+ switch (token.tokenType) {
+ case "function":
+ if (token.text == "not") {
+ selectorBeforeNot = selector;
+ selector = "";
+ scopeStack.push("(");
+ } else {
+ selector += token.text + "(";
+ }
+ selectorState = SELECTOR_STATES.null;
+ break;
+
+ case "ident":
+ selector += token.text;
+ break;
+ }
+ } else if (token.text == "[") {
+ _state = CSS_STATES.selector;
+ selectorState = SELECTOR_STATES.attribute;
+ scopeStack.push("[");
+ selector += "[";
+ } else if (token.text == "}") {
+ if (peek(scopeStack) == "@m") {
+ scopeStack.pop();
+ }
+ }
+ break;
+
+ case "at":
+ _state = token.text.startsWith("m") ? CSS_STATES.media
+ : CSS_STATES.keyframes;
+ break;
+ }
+ break;
+
+ case CSS_STATES.media:
+ // From CSS_STATES.media, we can only go to CSS_STATES.null state when
+ // we hit the first '{'
+ if (token.tokenType == "symbol" && token.text == "{") {
+ scopeStack.push("@m");
+ _state = CSS_STATES.null;
+ }
+ break;
+
+ case CSS_STATES.keyframes:
+ // From CSS_STATES.keyframes, we can only go to CSS_STATES.frame state
+ // when we hit the first '{'
+ if (token.tokenType == "symbol" && token.text == "{") {
+ scopeStack.push("@k");
+ _state = CSS_STATES.frame;
+ }
+ break;
+
+ case CSS_STATES.frame:
+ // From CSS_STATES.frame, we can either go to CSS_STATES.property
+ // state when we hit the first '{' or to CSS_STATES.selector when we
+ // hit '}'
+ if (token.tokenType == "symbol") {
+ if (token.text == "{") {
+ scopeStack.push("f");
+ _state = CSS_STATES.property;
+ } else if (token.text == "}") {
+ if (peek(scopeStack) == "@k") {
+ scopeStack.pop();
+ }
+
+ _state = CSS_STATES.null;
+ }
+ }
+ break;
+ }
+ if (_state == CSS_STATES.null) {
+ if (this.nullStates.length == 0) {
+ this.nullStates.push([token.loc.end.line, token.loc.end.column,
+ [...scopeStack]]);
+ continue;
+ }
+ let tokenLine = token.loc.end.line;
+ let tokenCh = token.loc.end.column;
+ if (tokenLine == 0) {
+ continue;
+ }
+ if (matchedStateIndex > -1) {
+ tokenLine += this.nullStates[matchedStateIndex][0];
+ }
+ this.nullStates.push([tokenLine, tokenCh, [...scopeStack]]);
+ }
+ }
+ this.state = _state;
+ this.propertyName = _state == CSS_STATES.value ? propertyName : null;
+ this.selectorState = _state == CSS_STATES.selector ? selectorState : null;
+ this.selectorBeforeNot = selectorBeforeNot == null ?
+ null : selectorBeforeNot;
+ if (token) {
+ selector = selector.slice(0, selector.length + token.loc.end.column - ch);
+ this.selector = selector;
+ } else {
+ this.selector = "";
+ }
+ this.selectors = selectors;
+
+ if (token && token.tokenType != "whitespace") {
+ let text;
+ if (token.tokenType == "dimension" || !token.text) {
+ text = source.substring(token.startOffset, token.endOffset);
+ } else {
+ text = token.text;
+ }
+ this.completing = (text.slice(0, ch - token.loc.start.column)
+ .replace(/^[.#]$/, ""));
+ } else {
+ this.completing = "";
+ }
+ // Special case the situation when the user just entered ":" after typing a
+ // property name.
+ if (this.completing == ":" && _state == CSS_STATES.value) {
+ this.completing = "";
+ }
+
+ // Special check for !important; case.
+ if (token && tokens[cursor - 2] && tokens[cursor - 2].text == "!" &&
+ this.completing == "important".slice(0, this.completing.length)) {
+ this.completing = "!" + this.completing;
+ }
+ return _state;
+ },
+
+ /**
+ * Queries the DOM Walker actor for suggestions regarding the selector being
+ * completed
+ */
+ suggestSelectors: function () {
+ let walker = this.walker;
+ if (!walker) {
+ return Promise.resolve([]);
+ }
+
+ let query = this.selector;
+ // Even though the selector matched atleast one node, there is still
+ // possibility of suggestions.
+ switch (this.selectorState) {
+ case SELECTOR_STATES.null:
+ if (this.completing === ",") {
+ return Promise.resolve([]);
+ }
+
+ query += "*";
+ break;
+
+ case SELECTOR_STATES.tag:
+ query = query.slice(0, query.length - this.completing.length);
+ break;
+
+ case SELECTOR_STATES.id:
+ case SELECTOR_STATES.class:
+ case SELECTOR_STATES.pseudo:
+ if (/^[.:#]$/.test(this.completing)) {
+ query = query.slice(0, query.length - this.completing.length);
+ this.completing = "";
+ } else {
+ query = query.slice(0, query.length - this.completing.length - 1);
+ }
+ break;
+ }
+
+ if (/[\s+>~]$/.test(query) &&
+ this.selectorState != SELECTOR_STATES.attribute &&
+ this.selectorState != SELECTOR_STATES.value) {
+ query += "*";
+ }
+
+ // Set the values that this request was supposed to suggest to.
+ this._currentQuery = query;
+ return walker.getSuggestionsForQuery(query, this.completing,
+ this.selectorState)
+ .then(result => this.prepareSelectorResults(result));
+ },
+
+ /**
+ * Prepares the selector suggestions returned by the walker actor.
+ */
+ prepareSelectorResults: function (result) {
+ if (this._currentQuery != result.query) {
+ return [];
+ }
+
+ result = result.suggestions;
+ let query = this.selector;
+ let completion = [];
+ for (let [value, count, state] of result) {
+ switch (this.selectorState) {
+ case SELECTOR_STATES.id:
+ case SELECTOR_STATES.class:
+ case SELECTOR_STATES.pseudo:
+ if (/^[.:#]$/.test(this.completing)) {
+ value = query.slice(0, query.length - this.completing.length) +
+ value;
+ } else {
+ value = query.slice(0, query.length - this.completing.length - 1) +
+ value;
+ }
+ break;
+
+ case SELECTOR_STATES.tag:
+ value = query.slice(0, query.length - this.completing.length) +
+ value;
+ break;
+
+ case SELECTOR_STATES.null:
+ value = query + value;
+ break;
+
+ default:
+ value = query.slice(0, query.length - this.completing.length) +
+ value;
+ }
+
+ let item = {
+ label: value,
+ preLabel: query,
+ text: value,
+ score: count
+ };
+
+ // In case the query's state is tag and the item's state is id or class
+ // adjust the preLabel
+ if (this.selectorState === SELECTOR_STATES.tag &&
+ state === SELECTOR_STATES.class) {
+ item.preLabel = "." + item.preLabel;
+ }
+ if (this.selectorState === SELECTOR_STATES.tag &&
+ state === SELECTOR_STATES.id) {
+ item.preLabel = "#" + item.preLabel;
+ }
+
+ completion.push(item);
+
+ if (completion.length > this.maxEntries - 1) {
+ break;
+ }
+ }
+ return completion;
+ },
+
+ /**
+ * Returns CSS property name suggestions based on the input.
+ *
+ * @param startProp {String} Initial part of the property being completed.
+ */
+ completeProperties: function (startProp) {
+ let finalList = [];
+ if (!startProp) {
+ return Promise.resolve(finalList);
+ }
+
+ let length = this.propertyNames.length;
+ let i = 0, count = 0;
+ for (; i < length && count < this.maxEntries; i++) {
+ if (this.propertyNames[i].startsWith(startProp)) {
+ count++;
+ let propName = this.propertyNames[i];
+ finalList.push({
+ preLabel: startProp,
+ label: propName,
+ text: propName + ": "
+ });
+ } else if (this.propertyNames[i] > startProp) {
+ // We have crossed all possible matches alphabetically.
+ break;
+ }
+ }
+ return Promise.resolve(finalList);
+ },
+
+ /**
+ * Returns CSS value suggestions based on the corresponding property.
+ *
+ * @param propName {String} The property to which the value being completed
+ * belongs.
+ * @param startValue {String} Initial part of the value being completed.
+ */
+ completeValues: function (propName, startValue) {
+ let finalList = [];
+ let list = ["!important;", ...this.cssProperties.getValues(propName)];
+ // If there is no character being completed, we are showing an initial list
+ // of possible values. Skipping '!important' in this case.
+ if (!startValue) {
+ list.splice(0, 1);
+ }
+
+ let length = list.length;
+ let i = 0, count = 0;
+ for (; i < length && count < this.maxEntries; i++) {
+ if (list[i].startsWith(startValue)) {
+ count++;
+ let value = list[i];
+ finalList.push({
+ preLabel: startValue,
+ label: value,
+ text: value
+ });
+ } else if (list[i] > startValue) {
+ // We have crossed all possible matches alphabetically.
+ break;
+ }
+ }
+ return Promise.resolve(finalList);
+ },
+
+ /**
+ * A biased binary search in a sorted array where the middle element is
+ * calculated based on the values at the lower and the upper index in each
+ * iteration.
+ *
+ * This method returns the index of the closest null state from the passed
+ * `line` argument. Once we have the closest null state, we can start applying
+ * the state machine logic from that location instead of the absolute starting
+ * of the CSS source. This speeds up the tokenizing and the state machine a
+ * lot while using autocompletion at high line numbers in a CSS source.
+ */
+ findNearestNullState: function (line) {
+ let arr = this.nullStates;
+ let high = arr.length - 1;
+ let low = 0;
+ let target = 0;
+
+ if (high < 0) {
+ return -1;
+ }
+ if (arr[high][0] <= line) {
+ return high;
+ }
+ if (arr[low][0] > line) {
+ return -1;
+ }
+
+ while (high > low) {
+ if (arr[low][0] <= line && arr[low [0] + 1] > line) {
+ return low;
+ }
+ if (arr[high][0] > line && arr[high - 1][0] <= line) {
+ return high - 1;
+ }
+
+ target = (((line - arr[low][0]) / (arr[high][0] - arr[low][0])) *
+ (high - low)) | 0;
+
+ if (arr[target][0] <= line && arr[target + 1][0] > line) {
+ return target;
+ } else if (line > arr[target][0]) {
+ low = target + 1;
+ high--;
+ } else {
+ high = target - 1;
+ low++;
+ }
+ }
+
+ return -1;
+ },
+
+ /**
+ * Invalidates the state cache for and above the line.
+ */
+ invalidateCache: function (line) {
+ this.nullStates.length = this.findNearestNullState(line) + 1;
+ },
+
+ /**
+ * Get the state information about a token surrounding the {line, ch} position
+ *
+ * @param {string} source
+ * The complete source of the CSS file. Unlike resolve state method,
+ * this method requires the full source.
+ * @param {object} caret
+ * The line, ch position of the caret.
+ *
+ * @returns {object}
+ * An object containing the state of token covered by the caret.
+ * The object has following properties when the the state is
+ * "selector", "value" or "property", null otherwise:
+ * - state {string} one of CSS_STATES - "selector", "value" etc.
+ * - selector {string} The selector at the caret when `state` is
+ * selector. OR
+ * - selectors {[string]} Array of selector strings in case when
+ * `state` is "value" or "property"
+ * - propertyName {string} The property name at the current caret or
+ * the property name corresponding to the value at
+ * the caret.
+ * - value {string} The css value at the current caret.
+ * - loc {object} An object containing the starting and the ending
+ * caret position of the whole selector, value or property.
+ * - { start: {line, ch}, end: {line, ch}}
+ */
+ getInfoAt: function (source, caret) {
+ // Limits the input source till the {line, ch} caret position
+ function limit(sourceArg, {line, ch}) {
+ line++;
+ let list = sourceArg.split("\n");
+ if (list.length < line) {
+ return sourceArg;
+ }
+ if (line == 1) {
+ return list[0].slice(0, ch);
+ }
+ return [...list.slice(0, line - 1),
+ list[line - 1].slice(0, ch)].join("\n");
+ }
+
+ // Get the state at the given line, ch
+ let state = this.resolveState(limit(source, caret), caret);
+ let propertyName = this.propertyName;
+ let {line, ch} = caret;
+ let sourceArray = source.split("\n");
+ let limitedSource = limit(source, caret);
+
+ /**
+ * Method to traverse forwards from the caret location to figure out the
+ * ending point of a selector or css value.
+ *
+ * @param {function} check
+ * A method which takes the current state as an input and determines
+ * whether the state changed or not.
+ */
+ let traverseForward = check => {
+ let location;
+ // Backward loop to determine the beginning location of the selector.
+ do {
+ let lineText = sourceArray[line];
+ if (line == caret.line) {
+ lineText = lineText.substring(caret.ch);
+ }
+
+ let prevToken = undefined;
+ let tokens = cssTokenizer(lineText);
+ let found = false;
+ let ech = line == caret.line ? caret.ch : 0;
+ for (let token of tokens) {
+ // If the line is completely spaces, handle it differently
+ if (lineText.trim() == "") {
+ limitedSource += lineText;
+ } else {
+ limitedSource += sourceArray[line]
+ .substring(ech + token.startOffset,
+ ech + token.endOffset);
+ }
+
+ // Whitespace cannot change state.
+ if (token.tokenType == "whitespace") {
+ prevToken = token;
+ continue;
+ }
+
+ let forwState = this.resolveState(limitedSource, {
+ line: line,
+ ch: token.endOffset + ech
+ });
+ if (check(forwState)) {
+ if (prevToken && prevToken.tokenType == "whitespace") {
+ token = prevToken;
+ }
+ location = {
+ line: line,
+ ch: token.startOffset + ech
+ };
+ found = true;
+ break;
+ }
+ prevToken = token;
+ }
+ limitedSource += "\n";
+ if (found) {
+ break;
+ }
+ } while (line++ < sourceArray.length);
+ return location;
+ };
+
+ /**
+ * Method to traverse backwards from the caret location to figure out the
+ * starting point of a selector or css value.
+ *
+ * @param {function} check
+ * A method which takes the current state as an input and determines
+ * whether the state changed or not.
+ * @param {boolean} isValue
+ * true if the traversal is being done for a css value state.
+ */
+ let traverseBackwards = (check, isValue) => {
+ let location;
+ // Backward loop to determine the beginning location of the selector.
+ do {
+ let lineText = sourceArray[line];
+ if (line == caret.line) {
+ lineText = lineText.substring(0, caret.ch);
+ }
+
+ let tokens = Array.from(cssTokenizer(lineText));
+ let found = false;
+ for (let i = tokens.length - 1; i >= 0; i--) {
+ let token = tokens[i];
+ // If the line is completely spaces, handle it differently
+ if (lineText.trim() == "") {
+ limitedSource = limitedSource.slice(0, -1 * lineText.length);
+ } else {
+ let length = token.endOffset - token.startOffset;
+ limitedSource = limitedSource.slice(0, -1 * length);
+ }
+
+ // Whitespace cannot change state.
+ if (token.tokenType == "whitespace") {
+ continue;
+ }
+
+ let backState = this.resolveState(limitedSource, {
+ line: line,
+ ch: token.startOffset
+ });
+ if (check(backState)) {
+ if (tokens[i + 1] && tokens[i + 1].tokenType == "whitespace") {
+ token = tokens[i + 1];
+ }
+ location = {
+ line: line,
+ ch: isValue ? token.endOffset : token.startOffset
+ };
+ found = true;
+ break;
+ }
+ }
+ limitedSource = limitedSource.slice(0, -1);
+ if (found) {
+ break;
+ }
+ } while (line-- >= 0);
+ return location;
+ };
+
+ if (state == CSS_STATES.selector) {
+ // For selector state, the ending and starting point of the selector is
+ // either when the state changes or the selector becomes empty and a
+ // single selector can span multiple lines.
+ // Backward loop to determine the beginning location of the selector.
+ let start = traverseBackwards(backState => {
+ return (backState != CSS_STATES.selector ||
+ (this.selector == "" && this.selectorBeforeNot == null));
+ });
+
+ line = caret.line;
+ limitedSource = limit(source, caret);
+ // Forward loop to determine the ending location of the selector.
+ let end = traverseForward(forwState => {
+ return (forwState != CSS_STATES.selector ||
+ (this.selector == "" && this.selectorBeforeNot == null));
+ });
+
+ // Since we have start and end positions, figure out the whole selector.
+ let selector = source.split("\n").slice(start.line, end.line + 1);
+ selector[selector.length - 1] =
+ selector[selector.length - 1].substring(0, end.ch);
+ selector[0] = selector[0].substring(start.ch);
+ selector = selector.join("\n");
+ return {
+ state: state,
+ selector: selector,
+ loc: {
+ start: start,
+ end: end
+ }
+ };
+ } else if (state == CSS_STATES.property) {
+ // A property can only be a single word and thus very easy to calculate.
+ let tokens = cssTokenizer(sourceArray[line]);
+ for (let token of tokens) {
+ // Note that, because we're tokenizing a single line, the
+ // token's offset is also the column number.
+ if (token.startOffset <= ch && token.endOffset >= ch) {
+ return {
+ state: state,
+ propertyName: token.text,
+ selectors: this.selectors,
+ loc: {
+ start: {
+ line: line,
+ ch: token.startOffset
+ },
+ end: {
+ line: line,
+ ch: token.endOffset
+ }
+ }
+ };
+ }
+ }
+ } else if (state == CSS_STATES.value) {
+ // CSS value can be multiline too, so we go forward and backwards to
+ // determine the bounds of the value at caret
+ let start = traverseBackwards(backState => backState != CSS_STATES.value, true);
+
+ line = caret.line;
+ limitedSource = limit(source, caret);
+ let end = traverseForward(forwState => forwState != CSS_STATES.value);
+
+ let value = source.split("\n").slice(start.line, end.line + 1);
+ value[value.length - 1] = value[value.length - 1].substring(0, end.ch);
+ value[0] = value[0].substring(start.ch);
+ value = value.join("\n");
+ return {
+ state: state,
+ propertyName: propertyName,
+ selectors: this.selectors,
+ value: value,
+ loc: {
+ start: start,
+ end: end
+ }
+ };
+ }
+ return null;
+ }
+};
+
+module.exports = CSSCompleter;
diff --git a/devtools/client/sourceeditor/debugger.js b/devtools/client/sourceeditor/debugger.js
new file mode 100644
index 000000000..de63962a6
--- /dev/null
+++ b/devtools/client/sourceeditor/debugger.js
@@ -0,0 +1,336 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const promise = require("promise");
+const dbginfo = new WeakMap();
+
+// These functions implement search within the debugger. Since
+// search in the debugger is different from other components,
+// we can't use search.js CodeMirror addon. This is a slightly
+// modified version of that addon. Depends on searchcursor.js.
+
+function SearchState() {
+ this.posFrom = this.posTo = this.query = null;
+}
+
+function getSearchState(cm) {
+ return cm.state.search || (cm.state.search = new SearchState());
+}
+
+function getSearchCursor(cm, query, pos) {
+ // If the query string is all lowercase, do a case insensitive search.
+ return cm.getSearchCursor(query, pos,
+ typeof query == "string" && query == query.toLowerCase());
+}
+
+/**
+ * If there's a saved search, selects the next results.
+ * Otherwise, creates a new search and selects the first
+ * result.
+ */
+function doSearch(ctx, rev, query) {
+ let { cm } = ctx;
+ let state = getSearchState(cm);
+
+ if (state.query) {
+ searchNext(ctx, rev);
+ return;
+ }
+
+ cm.operation(function () {
+ if (state.query) {
+ return;
+ }
+
+ state.query = query;
+ state.posFrom = state.posTo = { line: 0, ch: 0 };
+ searchNext(ctx, rev);
+ });
+}
+
+/**
+ * Selects the next result of a saved search.
+ */
+function searchNext(ctx, rev) {
+ let { cm, ed } = ctx;
+ cm.operation(function () {
+ let state = getSearchState(cm);
+ let cursor = getSearchCursor(cm, state.query,
+ rev ? state.posFrom : state.posTo);
+
+ if (!cursor.find(rev)) {
+ cursor = getSearchCursor(cm, state.query, rev ?
+ { line: cm.lastLine(), ch: null } : { line: cm.firstLine(), ch: 0 });
+ if (!cursor.find(rev)) {
+ return;
+ }
+ }
+
+ ed.alignLine(cursor.from().line, "center");
+ cm.setSelection(cursor.from(), cursor.to());
+ state.posFrom = cursor.from();
+ state.posTo = cursor.to();
+ });
+}
+
+/**
+ * Clears the currently saved search.
+ */
+function clearSearch(cm) {
+ let state = getSearchState(cm);
+
+ if (!state.query) {
+ return;
+ }
+
+ state.query = null;
+}
+
+// Exported functions
+
+/**
+ * This function is called whenever Editor is extended with functions
+ * from this module. See Editor.extend for more info.
+ */
+function initialize(ctx) {
+ let { ed } = ctx;
+
+ dbginfo.set(ed, {
+ breakpoints: {},
+ debugLocation: null
+ });
+}
+
+/**
+ * True if editor has a visual breakpoint at that line, false
+ * otherwise.
+ */
+function hasBreakpoint(ctx, line) {
+ let { cm } = ctx;
+ // In some rare occasions CodeMirror might not be properly initialized yet, so
+ // return an exceptional value in that case.
+ if (cm.lineInfo(line) === null) {
+ return null;
+ }
+ let markers = cm.lineInfo(line).wrapClass;
+
+ return markers != null &&
+ markers.includes("breakpoint");
+}
+
+/**
+ * Adds a visual breakpoint for a specified line. Third
+ * parameter 'cond' can hold any object.
+ *
+ * After adding a breakpoint, this function makes Editor to
+ * emit a breakpointAdded event.
+ */
+function addBreakpoint(ctx, line, cond) {
+ function _addBreakpoint() {
+ let { ed, cm } = ctx;
+ let meta = dbginfo.get(ed);
+ let info = cm.lineInfo(line);
+
+ // The line does not exist in the editor. This is harmless, the
+ // architecture calling this assumes the editor will handle this
+ // gracefully, and make sure breakpoints exist when they need to.
+ if (!info) {
+ return;
+ }
+
+ ed.addLineClass(line, "breakpoint");
+ meta.breakpoints[line] = { condition: cond };
+
+ // TODO(jwl): why is `info` null when breaking on page reload?
+ info.handle.on("delete", function onDelete() {
+ info.handle.off("delete", onDelete);
+ meta.breakpoints[info.line] = null;
+ });
+
+ if (cond) {
+ setBreakpointCondition(ctx, line);
+ }
+ ed.emit("breakpointAdded", line);
+ deferred.resolve();
+ }
+
+ if (hasBreakpoint(ctx, line)) {
+ return null;
+ }
+
+ let deferred = promise.defer();
+ // If lineInfo() returns null, wait a tick to give the editor a chance to
+ // initialize properly.
+ if (ctx.cm.lineInfo(line) === null) {
+ DevToolsUtils.executeSoon(() => _addBreakpoint());
+ } else {
+ _addBreakpoint();
+ }
+ return deferred.promise;
+}
+
+/**
+ * Helps reset the debugger's breakpoint state
+ * - removes the breakpoints in the editor
+ * - cleares the debugger's breakpoint state
+ *
+ * Note, does not *actually* remove a source's breakpoints.
+ * The canonical state is kept in the app state.
+ *
+ */
+function removeBreakpoints(ctx) {
+ let { ed, cm } = ctx;
+
+ let meta = dbginfo.get(ed);
+ if (meta.breakpoints != null) {
+ meta.breakpoints = {};
+ }
+
+ cm.doc.iter((line) => {
+ // The hasBreakpoint is a slow operation: checks the line type, whether cm
+ // is initialized and creates several new objects. Inlining the line's
+ // wrapClass property check directly.
+ if (line.wrapClass == null || !line.wrapClass.includes("breakpoint")) {
+ return;
+ }
+ removeBreakpoint(ctx, line);
+ });
+}
+
+/**
+ * Removes a visual breakpoint from a specified line and
+ * makes Editor emit a breakpointRemoved event.
+ */
+function removeBreakpoint(ctx, line) {
+ if (!hasBreakpoint(ctx, line)) {
+ return;
+ }
+
+ let { ed, cm } = ctx;
+ let meta = dbginfo.get(ed);
+ let info = cm.lineInfo(line);
+
+ meta.breakpoints[info.line] = null;
+ ed.removeLineClass(info.line, "breakpoint");
+ ed.removeLineClass(info.line, "conditional");
+ ed.emit("breakpointRemoved", line);
+}
+
+function moveBreakpoint(ctx, fromLine, toLine) {
+ let { ed } = ctx;
+
+ ed.removeBreakpoint(fromLine);
+ ed.addBreakpoint(toLine);
+}
+
+function setBreakpointCondition(ctx, line) {
+ let { ed, cm } = ctx;
+ let info = cm.lineInfo(line);
+
+ // The line does not exist in the editor. This is harmless, the
+ // architecture calling this assumes the editor will handle this
+ // gracefully, and make sure breakpoints exist when they need to.
+ if (!info) {
+ return;
+ }
+
+ ed.addLineClass(line, "conditional");
+}
+
+function removeBreakpointCondition(ctx, line) {
+ let { ed } = ctx;
+
+ ed.removeLineClass(line, "conditional");
+}
+
+/**
+ * Returns a list of all breakpoints in the current Editor.
+ */
+function getBreakpoints(ctx) {
+ let { ed } = ctx;
+ let meta = dbginfo.get(ed);
+
+ return Object.keys(meta.breakpoints).reduce((acc, line) => {
+ if (meta.breakpoints[line] != null) {
+ acc.push({ line: line, condition: meta.breakpoints[line].condition });
+ }
+ return acc;
+ }, []);
+}
+
+/**
+ * Saves a debug location information and adds a visual anchor to
+ * the breakpoints gutter. This is used by the debugger UI to
+ * display the line on which the Debugger is currently paused.
+ */
+function setDebugLocation(ctx, line) {
+ let { ed } = ctx;
+ let meta = dbginfo.get(ed);
+
+ clearDebugLocation(ctx);
+
+ meta.debugLocation = line;
+ ed.addLineClass(line, "debug-line");
+}
+
+/**
+ * Returns a line number that corresponds to the current debug
+ * location.
+ */
+function getDebugLocation(ctx) {
+ let { ed } = ctx;
+ let meta = dbginfo.get(ed);
+
+ return meta.debugLocation;
+}
+
+/**
+ * Clears the debug location. Clearing the debug location
+ * also removes a visual anchor from the breakpoints gutter.
+ */
+function clearDebugLocation(ctx) {
+ let { ed } = ctx;
+ let meta = dbginfo.get(ed);
+
+ if (meta.debugLocation != null) {
+ ed.removeLineClass(meta.debugLocation, "debug-line");
+ meta.debugLocation = null;
+ }
+}
+
+/**
+ * Starts a new search.
+ */
+function find(ctx, query) {
+ clearSearch(ctx.cm);
+ doSearch(ctx, false, query);
+}
+
+/**
+ * Finds the next item based on the currently saved search.
+ */
+function findNext(ctx, query) {
+ doSearch(ctx, false, query);
+}
+
+/**
+ * Finds the previous item based on the currently saved search.
+ */
+function findPrev(ctx, query) {
+ doSearch(ctx, true, query);
+}
+
+// Export functions
+
+[
+ initialize, hasBreakpoint, addBreakpoint, removeBreakpoint, moveBreakpoint,
+ setBreakpointCondition, removeBreakpointCondition, getBreakpoints, removeBreakpoints,
+ setDebugLocation, getDebugLocation, clearDebugLocation, find, findNext,
+ findPrev
+].forEach(func => {
+ module.exports[func.name] = func;
+});
diff --git a/devtools/client/sourceeditor/editor.js b/devtools/client/sourceeditor/editor.js
new file mode 100644
index 000000000..ce2136afc
--- /dev/null
+++ b/devtools/client/sourceeditor/editor.js
@@ -0,0 +1,1410 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2; fill-column: 80 -*- */
+/* vim:set ts=2 sw=2 sts=2 et tw=80:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ EXPAND_TAB,
+ TAB_SIZE,
+ DETECT_INDENT,
+ getIndentationFromIteration
+} = require("devtools/shared/indentation");
+
+const ENABLE_CODE_FOLDING = "devtools.editor.enableCodeFolding";
+const KEYMAP = "devtools.editor.keymap";
+const AUTO_CLOSE = "devtools.editor.autoclosebrackets";
+const AUTOCOMPLETE = "devtools.editor.autocomplete";
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const VALID_KEYMAPS = new Set(["emacs", "vim", "sublime"]);
+
+// Maximum allowed margin (in number of lines) from top or bottom of the editor
+// while shifting to a line which was initially out of view.
+const MAX_VERTICAL_OFFSET = 3;
+
+// Match @Scratchpad/N:LINE[:COLUMN] or (LINE[:COLUMN]) anywhere at an end of
+// line in text selection.
+const RE_SCRATCHPAD_ERROR = /(?:@Scratchpad\/\d+:|\()(\d+):?(\d+)?(?:\)|\n)/;
+const RE_JUMP_TO_LINE = /^(\d+):?(\d+)?/;
+
+const Services = require("Services");
+const promise = require("promise");
+const events = require("devtools/shared/event-emitter");
+const { PrefObserver } = require("devtools/client/styleeditor/utils");
+const { getClientCssProperties } = require("devtools/shared/fronts/css-properties");
+const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/sourceeditor.properties");
+
+const { OS } = Services.appinfo;
+
+// CM_STYLES, CM_SCRIPTS and CM_IFRAME represent the HTML,
+// JavaScript and CSS that is injected into an iframe in
+// order to initialize a CodeMirror instance.
+
+const CM_STYLES = [
+ "chrome://devtools/content/sourceeditor/codemirror/lib/codemirror.css",
+ "chrome://devtools/content/sourceeditor/codemirror/addon/dialog/dialog.css",
+ "chrome://devtools/content/sourceeditor/codemirror/mozilla.css"
+];
+
+const CM_SCRIPTS = [
+ "chrome://devtools/content/sourceeditor/codemirror/codemirror.bundle.js",
+];
+
+const CM_IFRAME =
+ "data:text/html;charset=utf8,<!DOCTYPE html>" +
+ "<html dir='ltr'>" +
+ " <head>" +
+ " <style>" +
+ " html, body { height: 100%; }" +
+ " body { margin: 0; overflow: hidden; }" +
+ " .CodeMirror { width: 100% !important; line-height: 1.25 !important; }" +
+ " </style>" +
+ CM_STYLES.map(style => "<link rel='stylesheet' href='" + style + "'>").join("\n") +
+ " </head>" +
+ " <body class='theme-body devtools-monospace'></body>" +
+ "</html>";
+
+const CM_MAPPING = [
+ "focus",
+ "hasFocus",
+ "lineCount",
+ "somethingSelected",
+ "getCursor",
+ "setSelection",
+ "getSelection",
+ "replaceSelection",
+ "extendSelection",
+ "undo",
+ "redo",
+ "clearHistory",
+ "openDialog",
+ "refresh",
+ "getScrollInfo",
+ "getViewport"
+];
+
+const editors = new WeakMap();
+
+Editor.modes = {
+ text: { name: "text" },
+ html: { name: "htmlmixed" },
+ css: { name: "css" },
+ wasm: { name: "wasm" },
+ js: { name: "javascript" },
+ vs: { name: "x-shader/x-vertex" },
+ fs: { name: "x-shader/x-fragment" }
+};
+
+/**
+ * A very thin wrapper around CodeMirror. Provides a number
+ * of helper methods to make our use of CodeMirror easier and
+ * another method, appendTo, to actually create and append
+ * the CodeMirror instance.
+ *
+ * Note that Editor doesn't expose CodeMirror instance to the
+ * outside world.
+ *
+ * Constructor accepts one argument, config. It is very
+ * similar to the CodeMirror configuration object so for most
+ * properties go to CodeMirror's documentation (see below).
+ *
+ * Other than that, it accepts one additional and optional
+ * property contextMenu. This property should be an element, or
+ * an ID of an element that we can use as a context menu.
+ *
+ * This object is also an event emitter.
+ *
+ * CodeMirror docs: http://codemirror.net/doc/manual.html
+ */
+function Editor(config) {
+ const tabSize = Services.prefs.getIntPref(TAB_SIZE);
+ const useTabs = !Services.prefs.getBoolPref(EXPAND_TAB);
+ const useAutoClose = Services.prefs.getBoolPref(AUTO_CLOSE);
+
+ this.version = null;
+ this.config = {
+ value: "",
+ mode: Editor.modes.text,
+ indentUnit: tabSize,
+ tabSize: tabSize,
+ contextMenu: null,
+ matchBrackets: true,
+ extraKeys: {},
+ indentWithTabs: useTabs,
+ inputStyle: "textarea",
+ styleActiveLine: true,
+ autoCloseBrackets: "()[]{}''\"\"``",
+ autoCloseEnabled: useAutoClose,
+ theme: "mozilla",
+ themeSwitching: true,
+ autocomplete: false,
+ autocompleteOpts: {}
+ };
+
+ // Additional shortcuts.
+ this.config.extraKeys[Editor.keyFor("jumpToLine")] = () => this.jumpToLine();
+ this.config.extraKeys[Editor.keyFor("moveLineUp", { noaccel: true })] =
+ () => this.moveLineUp();
+ this.config.extraKeys[Editor.keyFor("moveLineDown", { noaccel: true })] =
+ () => this.moveLineDown();
+ this.config.extraKeys[Editor.keyFor("toggleComment")] = "toggleComment";
+
+ // Disable ctrl-[ and ctrl-] because toolbox uses those shortcuts.
+ this.config.extraKeys[Editor.keyFor("indentLess")] = false;
+ this.config.extraKeys[Editor.keyFor("indentMore")] = false;
+
+ // Overwrite default config with user-provided, if needed.
+ Object.keys(config).forEach(k => {
+ if (k != "extraKeys") {
+ this.config[k] = config[k];
+ return;
+ }
+
+ if (!config.extraKeys) {
+ return;
+ }
+
+ Object.keys(config.extraKeys).forEach(key => {
+ this.config.extraKeys[key] = config.extraKeys[key];
+ });
+ });
+
+ if (!this.config.gutters) {
+ this.config.gutters = [];
+ }
+ if (this.config.lineNumbers
+ && this.config.gutters.indexOf("CodeMirror-linenumbers") === -1) {
+ this.config.gutters.push("CodeMirror-linenumbers");
+ }
+
+ // Remember the initial value of autoCloseBrackets.
+ this.config.autoCloseBracketsSaved = this.config.autoCloseBrackets;
+
+ // Overwrite default tab behavior. If something is selected,
+ // indent those lines. If nothing is selected and we're
+ // indenting with tabs, insert one tab. Otherwise insert N
+ // whitespaces where N == indentUnit option.
+ this.config.extraKeys.Tab = cm => {
+ if (cm.somethingSelected()) {
+ cm.indentSelection("add");
+ return;
+ }
+
+ if (this.config.indentWithTabs) {
+ cm.replaceSelection("\t", "end", "+input");
+ return;
+ }
+
+ let num = cm.getOption("indentUnit");
+ if (cm.getCursor().ch !== 0) {
+ num -= 1;
+ }
+ cm.replaceSelection(" ".repeat(num), "end", "+input");
+ };
+
+ // Allow add-ons to inject scripts for their editor instances
+ if (!this.config.externalScripts) {
+ this.config.externalScripts = [];
+ }
+
+ if (this.config.cssProperties) {
+ // Ensure that autocompletion has cssProperties if it's passed in via the options.
+ this.config.autocompleteOpts.cssProperties = this.config.cssProperties;
+ } else {
+ // Use a static client-side database of CSS values if none is provided.
+ this.config.cssProperties = getClientCssProperties();
+ }
+
+ events.decorate(this);
+}
+
+Editor.prototype = {
+ container: null,
+ version: null,
+ config: null,
+ Doc: null,
+
+ /**
+ * Exposes the CodeMirror instance. We want to get away from trying to
+ * abstract away the API entirely, and this makes it easier to integrate in
+ * various environments and do complex things.
+ */
+ get codeMirror() {
+ if (!editors.has(this)) {
+ throw new Error(
+ "CodeMirror instance does not exist. You must wait " +
+ "for it to be appended to the DOM."
+ );
+ }
+ return editors.get(this);
+ },
+
+ /**
+ * Appends the current Editor instance to the element specified by
+ * 'el'. You can also provide your won iframe to host the editor as
+ * an optional second parameter. This method actually creates and
+ * loads CodeMirror and all its dependencies.
+ *
+ * This method is asynchronous and returns a promise.
+ */
+ appendTo: function (el, env) {
+ let def = promise.defer();
+ let cm = editors.get(this);
+
+ if (!env) {
+ env = el.ownerDocument.createElementNS(XUL_NS, "iframe");
+ }
+
+ env.flex = 1;
+
+ if (cm) {
+ throw new Error("You can append an editor only once.");
+ }
+
+ let onLoad = () => {
+ let win = env.contentWindow.wrappedJSObject;
+
+ if (!this.config.themeSwitching) {
+ win.document.documentElement.setAttribute("force-theme", "light");
+ }
+
+ Services.scriptloader.loadSubScript(
+ "chrome://devtools/content/shared/theme-switching.js",
+ win, "utf8"
+ );
+ this.container = env;
+ this._setup(win.document.body, el.ownerDocument);
+ env.removeEventListener("load", onLoad, true);
+
+ def.resolve();
+ };
+
+ env.addEventListener("load", onLoad, true);
+ env.setAttribute("src", CM_IFRAME);
+ el.appendChild(env);
+
+ this.once("destroy", () => el.removeChild(env));
+ return def.promise;
+ },
+
+ appendToLocalElement: function (el) {
+ this._setup(el);
+ },
+
+ /**
+ * Do the actual appending and configuring of the CodeMirror instance. This is
+ * used by both append functions above, and does all the hard work to
+ * configure CodeMirror with all the right options/modes/etc.
+ */
+ _setup: function (el, doc) {
+ doc = doc || el.ownerDocument;
+ let win = el.ownerDocument.defaultView;
+
+ let scriptsToInject = CM_SCRIPTS.concat(this.config.externalScripts);
+ scriptsToInject.forEach(url => {
+ if (url.startsWith("chrome://")) {
+ Services.scriptloader.loadSubScript(url, win, "utf8");
+ }
+ });
+
+ // Replace the propertyKeywords, colorKeywords and valueKeywords
+ // properties of the CSS MIME type with the values provided by the CSS properties
+ // database.
+ const {
+ propertyKeywords,
+ colorKeywords,
+ valueKeywords
+ } = getCSSKeywords(this.config.cssProperties);
+
+ let cssSpec = win.CodeMirror.resolveMode("text/css");
+ cssSpec.propertyKeywords = propertyKeywords;
+ cssSpec.colorKeywords = colorKeywords;
+ cssSpec.valueKeywords = valueKeywords;
+ win.CodeMirror.defineMIME("text/css", cssSpec);
+
+ let scssSpec = win.CodeMirror.resolveMode("text/x-scss");
+ scssSpec.propertyKeywords = propertyKeywords;
+ scssSpec.colorKeywords = colorKeywords;
+ scssSpec.valueKeywords = valueKeywords;
+ win.CodeMirror.defineMIME("text/x-scss", scssSpec);
+
+ win.CodeMirror.commands.save = () => this.emit("saveRequested");
+
+ // Create a CodeMirror instance add support for context menus,
+ // overwrite the default controller (otherwise items in the top and
+ // context menus won't work).
+
+ let cm = win.CodeMirror(el, this.config);
+ this.Doc = win.CodeMirror.Doc;
+
+ // Disable APZ for source editors. It currently causes the line numbers to
+ // "tear off" and swim around on top of the content. Bug 1160601 tracks
+ // finding a solution that allows APZ to work with CodeMirror.
+ cm.getScrollerElement().addEventListener("wheel", ev => {
+ // By handling the wheel events ourselves, we force the platform to
+ // scroll synchronously, like it did before APZ. However, we lose smooth
+ // scrolling for users with mouse wheels. This seems acceptible vs.
+ // doing nothing and letting the gutter slide around.
+ ev.preventDefault();
+
+ let { deltaX, deltaY } = ev;
+
+ if (ev.deltaMode == ev.DOM_DELTA_LINE) {
+ deltaX *= cm.defaultCharWidth();
+ deltaY *= cm.defaultTextHeight();
+ } else if (ev.deltaMode == ev.DOM_DELTA_PAGE) {
+ deltaX *= cm.getWrapperElement().clientWidth;
+ deltaY *= cm.getWrapperElement().clientHeight;
+ }
+
+ cm.getScrollerElement().scrollBy(deltaX, deltaY);
+ });
+
+ cm.getWrapperElement().addEventListener("contextmenu", ev => {
+ ev.preventDefault();
+
+ if (!this.config.contextMenu) {
+ return;
+ }
+
+ let popup = this.config.contextMenu;
+ if (typeof popup == "string") {
+ popup = doc.getElementById(this.config.contextMenu);
+ }
+
+ this.emit("popupOpen", ev, popup);
+ popup.openPopupAtScreen(ev.screenX, ev.screenY, true);
+ }, false);
+
+ cm.on("focus", () => this.emit("focus"));
+ cm.on("scroll", () => this.emit("scroll"));
+ cm.on("change", () => {
+ this.emit("change");
+ if (!this._lastDirty) {
+ this._lastDirty = true;
+ this.emit("dirty-change");
+ }
+ });
+ cm.on("cursorActivity", () => this.emit("cursorActivity"));
+
+ cm.on("gutterClick", (cmArg, line, gutter, ev) => {
+ let head = { line: line, ch: 0 };
+ let tail = { line: line, ch: this.getText(line).length };
+
+ // Shift-click on a gutter selects the whole line.
+ if (ev.shiftKey) {
+ cmArg.setSelection(head, tail);
+ return;
+ }
+
+ this.emit("gutterClick", line, ev.button);
+ });
+
+ win.CodeMirror.defineExtension("l10n", (name) => {
+ return L10N.getStr(name);
+ });
+
+ this._initShortcuts(win);
+
+ editors.set(this, cm);
+
+ this.reloadPreferences = this.reloadPreferences.bind(this);
+ this._prefObserver = new PrefObserver("devtools.editor.");
+ this._prefObserver.on(TAB_SIZE, this.reloadPreferences);
+ this._prefObserver.on(EXPAND_TAB, this.reloadPreferences);
+ this._prefObserver.on(KEYMAP, this.reloadPreferences);
+ this._prefObserver.on(AUTO_CLOSE, this.reloadPreferences);
+ this._prefObserver.on(AUTOCOMPLETE, this.reloadPreferences);
+ this._prefObserver.on(DETECT_INDENT, this.reloadPreferences);
+ this._prefObserver.on(ENABLE_CODE_FOLDING, this.reloadPreferences);
+
+ this.reloadPreferences();
+
+ win.editor = this;
+ let editorReadyEvent = new win.CustomEvent("editorReady");
+ win.dispatchEvent(editorReadyEvent);
+ },
+
+ /**
+ * Returns a boolean indicating whether the editor is ready to
+ * use. Use appendTo(el).then(() => {}) for most cases
+ */
+ isAppended: function () {
+ return editors.has(this);
+ },
+
+ /**
+ * Returns the currently active highlighting mode.
+ * See Editor.modes for the list of all suppoert modes.
+ */
+ getMode: function () {
+ return this.getOption("mode");
+ },
+
+ /**
+ * Loads a script into editor's containing window.
+ */
+ loadScript: function (url) {
+ if (!this.container) {
+ throw new Error("Can't load a script until the editor is loaded.");
+ }
+ let win = this.container.contentWindow.wrappedJSObject;
+ Services.scriptloader.loadSubScript(url, win, "utf8");
+ },
+
+ /**
+ * Creates a CodeMirror Document
+ * @returns CodeMirror.Doc
+ */
+ createDocument: function () {
+ return new this.Doc("");
+ },
+
+ /**
+ * Replaces the current document with a new source document
+ */
+ replaceDocument: function (doc) {
+ let cm = editors.get(this);
+ cm.swapDoc(doc);
+ },
+
+ /**
+ * Changes the value of a currently used highlighting mode.
+ * See Editor.modes for the list of all supported modes.
+ */
+ setMode: function (value) {
+ this.setOption("mode", value);
+
+ // If autocomplete was set up and the mode is changing, then
+ // turn it off and back on again so the proper mode can be used.
+ if (this.config.autocomplete) {
+ this.setOption("autocomplete", false);
+ this.setOption("autocomplete", true);
+ }
+ },
+
+ /**
+ * Returns text from the text area. If line argument is provided
+ * the method returns only that line.
+ */
+ getText: function (line) {
+ let cm = editors.get(this);
+
+ if (line == null) {
+ return cm.getValue();
+ }
+
+ let info = cm.lineInfo(line);
+ return info ? cm.lineInfo(line).text : "";
+ },
+
+ /**
+ * Replaces whatever is in the text area with the contents of
+ * the 'value' argument.
+ */
+ setText: function (value) {
+ let cm = editors.get(this);
+ cm.setValue(value);
+
+ this.resetIndentUnit();
+ },
+
+ /**
+ * Reloads the state of the editor based on all current preferences.
+ * This is called automatically when any of the relevant preferences
+ * change.
+ */
+ reloadPreferences: function () {
+ // Restore the saved autoCloseBrackets value if it is preffed on.
+ let useAutoClose = Services.prefs.getBoolPref(AUTO_CLOSE);
+ this.setOption("autoCloseBrackets",
+ useAutoClose ? this.config.autoCloseBracketsSaved : false);
+
+ // If alternative keymap is provided, use it.
+ const keyMap = Services.prefs.getCharPref(KEYMAP);
+ if (VALID_KEYMAPS.has(keyMap)) {
+ this.setOption("keyMap", keyMap);
+ } else {
+ this.setOption("keyMap", "default");
+ }
+ this.updateCodeFoldingGutter();
+
+ this.resetIndentUnit();
+ this.setupAutoCompletion();
+ },
+
+ /**
+ * Sets the editor's indentation based on the current prefs and
+ * re-detect indentation if we should.
+ */
+ resetIndentUnit: function () {
+ let cm = editors.get(this);
+
+ let iterFn = function (start, end, callback) {
+ cm.eachLine(start, end, (line) => {
+ return callback(line.text);
+ });
+ };
+
+ let {indentUnit, indentWithTabs} = getIndentationFromIteration(iterFn);
+
+ cm.setOption("tabSize", indentUnit);
+ cm.setOption("indentUnit", indentUnit);
+ cm.setOption("indentWithTabs", indentWithTabs);
+ },
+
+ /**
+ * Replaces contents of a text area within the from/to {line, ch}
+ * range. If neither `from` nor `to` arguments are provided works
+ * exactly like setText. If only `from` object is provided, inserts
+ * text at that point, *overwriting* as many characters as needed.
+ */
+ replaceText: function (value, from, to) {
+ let cm = editors.get(this);
+
+ if (!from) {
+ this.setText(value);
+ return;
+ }
+
+ if (!to) {
+ let text = cm.getRange({ line: 0, ch: 0 }, from);
+ this.setText(text + value);
+ return;
+ }
+
+ cm.replaceRange(value, from, to);
+ },
+
+ /**
+ * Inserts text at the specified {line, ch} position, shifting existing
+ * contents as necessary.
+ */
+ insertText: function (value, at) {
+ let cm = editors.get(this);
+ cm.replaceRange(value, at, at);
+ },
+
+ /**
+ * Deselects contents of the text area.
+ */
+ dropSelection: function () {
+ if (!this.somethingSelected()) {
+ return;
+ }
+
+ this.setCursor(this.getCursor());
+ },
+
+ /**
+ * Returns true if there is more than one selection in the editor.
+ */
+ hasMultipleSelections: function () {
+ let cm = editors.get(this);
+ return cm.listSelections().length > 1;
+ },
+
+ /**
+ * Gets the first visible line number in the editor.
+ */
+ getFirstVisibleLine: function () {
+ let cm = editors.get(this);
+ return cm.lineAtHeight(0, "local");
+ },
+
+ /**
+ * Scrolls the view such that the given line number is the first visible line.
+ */
+ setFirstVisibleLine: function (line) {
+ let cm = editors.get(this);
+ let { top } = cm.charCoords({line: line, ch: 0}, "local");
+ cm.scrollTo(0, top);
+ },
+
+ /**
+ * Sets the cursor to the specified {line, ch} position with an additional
+ * option to align the line at the "top", "center" or "bottom" of the editor
+ * with "top" being default value.
+ */
+ setCursor: function ({line, ch}, align) {
+ let cm = editors.get(this);
+ this.alignLine(line, align);
+ cm.setCursor({line: line, ch: ch});
+ this.emit("cursorActivity");
+ },
+
+ /**
+ * Aligns the provided line to either "top", "center" or "bottom" of the
+ * editor view with a maximum margin of MAX_VERTICAL_OFFSET lines from top or
+ * bottom.
+ */
+ alignLine: function (line, align) {
+ let cm = editors.get(this);
+ let from = cm.lineAtHeight(0, "page");
+ let to = cm.lineAtHeight(cm.getWrapperElement().clientHeight, "page");
+ let linesVisible = to - from;
+ let halfVisible = Math.round(linesVisible / 2);
+
+ // If the target line is in view, skip the vertical alignment part.
+ if (line <= to && line >= from) {
+ return;
+ }
+
+ // Setting the offset so that the line always falls in the upper half
+ // of visible lines (lower half for bottom aligned).
+ // MAX_VERTICAL_OFFSET is the maximum allowed value.
+ let offset = Math.min(halfVisible, MAX_VERTICAL_OFFSET);
+
+ let topLine = {
+ "center": Math.max(line - halfVisible, 0),
+ "bottom": Math.max(line - linesVisible + offset, 0),
+ "top": Math.max(line - offset, 0)
+ }[align || "top"] || offset;
+
+ // Bringing down the topLine to total lines in the editor if exceeding.
+ topLine = Math.min(topLine, this.lineCount());
+ this.setFirstVisibleLine(topLine);
+ },
+
+ /**
+ * Returns whether a marker of a specified class exists in a line's gutter.
+ */
+ hasMarker: function (line, gutterName, markerClass) {
+ let marker = this.getMarker(line, gutterName);
+ if (!marker) {
+ return false;
+ }
+
+ return marker.classList.contains(markerClass);
+ },
+
+ /**
+ * Adds a marker with a specified class to a line's gutter. If another marker
+ * exists on that line, the new marker class is added to its class list.
+ */
+ addMarker: function (line, gutterName, markerClass) {
+ let cm = editors.get(this);
+ let info = cm.lineInfo(line);
+ if (!info) {
+ return;
+ }
+
+ let gutterMarkers = info.gutterMarkers;
+ let marker;
+ if (gutterMarkers) {
+ marker = gutterMarkers[gutterName];
+ if (marker) {
+ marker.classList.add(markerClass);
+ return;
+ }
+ }
+
+ marker = cm.getWrapperElement().ownerDocument.createElement("div");
+ marker.className = markerClass;
+ cm.setGutterMarker(info.line, gutterName, marker);
+ },
+
+ /**
+ * The reverse of addMarker. Removes a marker of a specified class from a
+ * line's gutter.
+ */
+ removeMarker: function (line, gutterName, markerClass) {
+ if (!this.hasMarker(line, gutterName, markerClass)) {
+ return;
+ }
+
+ let cm = editors.get(this);
+ cm.lineInfo(line).gutterMarkers[gutterName].classList.remove(markerClass);
+ },
+
+ /**
+ * Adds a marker with a specified class and an HTML content to a line's
+ * gutter. If another marker exists on that line, it is overwritten by a new
+ * marker.
+ */
+ addContentMarker: function (line, gutterName, markerClass, content) {
+ let cm = editors.get(this);
+ let info = cm.lineInfo(line);
+ if (!info) {
+ return;
+ }
+
+ let marker = cm.getWrapperElement().ownerDocument.createElement("div");
+ marker.className = markerClass;
+ marker.innerHTML = content;
+ cm.setGutterMarker(info.line, gutterName, marker);
+ },
+
+ /**
+ * The reverse of addContentMarker. Removes any line's markers in the
+ * specified gutter.
+ */
+ removeContentMarker: function (line, gutterName) {
+ let cm = editors.get(this);
+ let info = cm.lineInfo(line);
+ if (!info) {
+ return;
+ }
+
+ cm.setGutterMarker(info.line, gutterName, null);
+ },
+
+ getMarker: function (line, gutterName) {
+ let cm = editors.get(this);
+ let info = cm.lineInfo(line);
+ if (!info) {
+ return null;
+ }
+
+ let gutterMarkers = info.gutterMarkers;
+ if (!gutterMarkers) {
+ return null;
+ }
+
+ return gutterMarkers[gutterName];
+ },
+
+ /**
+ * Removes all gutter markers in the gutter with the given name.
+ */
+ removeAllMarkers: function (gutterName) {
+ let cm = editors.get(this);
+ cm.clearGutter(gutterName);
+ },
+
+ /**
+ * Handles attaching a set of events listeners on a marker. They should
+ * be passed as an object literal with keys as event names and values as
+ * function listeners. The line number, marker node and optional data
+ * will be passed as arguments to the function listener.
+ *
+ * You don't need to worry about removing these event listeners.
+ * They're automatically orphaned when clearing markers.
+ */
+ setMarkerListeners: function (line, gutterName, markerClass, eventsArg, data) {
+ if (!this.hasMarker(line, gutterName, markerClass)) {
+ return;
+ }
+
+ let cm = editors.get(this);
+ let marker = cm.lineInfo(line).gutterMarkers[gutterName];
+
+ for (let name in eventsArg) {
+ let listener = eventsArg[name].bind(this, line, marker, data);
+ marker.addEventListener(name, listener);
+ }
+ },
+
+ /**
+ * Returns whether a line is decorated using the specified class name.
+ */
+ hasLineClass: function (line, className) {
+ let cm = editors.get(this);
+ let info = cm.lineInfo(line);
+
+ if (!info || !info.wrapClass) {
+ return false;
+ }
+
+ return info.wrapClass.split(" ").indexOf(className) != -1;
+ },
+
+ /**
+ * Sets a CSS class name for the given line, including the text and gutter.
+ */
+ addLineClass: function (line, className) {
+ let cm = editors.get(this);
+ cm.addLineClass(line, "wrap", className);
+ },
+
+ /**
+ * The reverse of addLineClass.
+ */
+ removeLineClass: function (line, className) {
+ let cm = editors.get(this);
+ cm.removeLineClass(line, "wrap", className);
+ },
+
+ /**
+ * Mark a range of text inside the two {line, ch} bounds. Since the range may
+ * be modified, for example, when typing text, this method returns a function
+ * that can be used to remove the mark.
+ */
+ markText: function (from, to, className = "marked-text") {
+ let cm = editors.get(this);
+ let text = cm.getRange(from, to);
+ let span = cm.getWrapperElement().ownerDocument.createElement("span");
+ span.className = className;
+ span.textContent = text;
+
+ let mark = cm.markText(from, to, { replacedWith: span });
+ return {
+ anchor: span,
+ clear: () => mark.clear()
+ };
+ },
+
+ /**
+ * Calculates and returns one or more {line, ch} objects for
+ * a zero-based index who's value is relative to the start of
+ * the editor's text.
+ *
+ * If only one argument is given, this method returns a single
+ * {line,ch} object. Otherwise it returns an array.
+ */
+ getPosition: function (...args) {
+ let cm = editors.get(this);
+ let res = args.map((ind) => cm.posFromIndex(ind));
+ return args.length === 1 ? res[0] : res;
+ },
+
+ /**
+ * The reverse of getPosition. Similarly to getPosition this
+ * method returns a single value if only one argument was given
+ * and an array otherwise.
+ */
+ getOffset: function (...args) {
+ let cm = editors.get(this);
+ let res = args.map((pos) => cm.indexFromPos(pos));
+ return args.length > 1 ? res : res[0];
+ },
+
+ /**
+ * Returns a {line, ch} object that corresponds to the
+ * left, top coordinates.
+ */
+ getPositionFromCoords: function ({left, top}) {
+ let cm = editors.get(this);
+ return cm.coordsChar({ left: left, top: top });
+ },
+
+ /**
+ * The reverse of getPositionFromCoords. Similarly, returns a {left, top}
+ * object that corresponds to the specified line and character number.
+ */
+ getCoordsFromPosition: function ({line, ch}) {
+ let cm = editors.get(this);
+ return cm.charCoords({ line: ~~line, ch: ~~ch });
+ },
+
+ /**
+ * Returns true if there's something to undo and false otherwise.
+ */
+ canUndo: function () {
+ let cm = editors.get(this);
+ return cm.historySize().undo > 0;
+ },
+
+ /**
+ * Returns true if there's something to redo and false otherwise.
+ */
+ canRedo: function () {
+ let cm = editors.get(this);
+ return cm.historySize().redo > 0;
+ },
+
+ /**
+ * Marks the contents as clean and returns the current
+ * version number.
+ */
+ setClean: function () {
+ let cm = editors.get(this);
+ this.version = cm.changeGeneration();
+ this._lastDirty = false;
+ this.emit("dirty-change");
+ return this.version;
+ },
+
+ /**
+ * Returns true if contents of the text area are
+ * clean i.e. no changes were made since the last version.
+ */
+ isClean: function () {
+ let cm = editors.get(this);
+ return cm.isClean(this.version);
+ },
+
+ /**
+ * This method opens an in-editor dialog asking for a line to
+ * jump to. Once given, it changes cursor to that line.
+ */
+ jumpToLine: function () {
+ let doc = editors.get(this).getWrapperElement().ownerDocument;
+ let div = doc.createElement("div");
+ let inp = doc.createElement("input");
+ let txt = doc.createTextNode(L10N.getStr("gotoLineCmd.promptTitle"));
+
+ inp.type = "text";
+ inp.style.width = "10em";
+ inp.style.marginInlineStart = "1em";
+
+ div.appendChild(txt);
+ div.appendChild(inp);
+
+ if (!this.hasMultipleSelections()) {
+ let cm = editors.get(this);
+ let sel = cm.getSelection();
+ // Scratchpad inserts and selects a comment after an error happens:
+ // "@Scratchpad/1:10:2". Parse this to get the line and column.
+ // In the string above this is line 10, column 2.
+ let match = sel.match(RE_SCRATCHPAD_ERROR);
+ if (match) {
+ let [, line, column ] = match;
+ inp.value = column ? line + ":" + column : line;
+ inp.selectionStart = inp.selectionEnd = inp.value.length;
+ }
+ }
+
+ this.openDialog(div, (line) => {
+ // Handle LINE:COLUMN as well as LINE
+ let match = line.toString().match(RE_JUMP_TO_LINE);
+ if (match) {
+ let [, matchLine, column ] = match;
+ this.setCursor({line: matchLine - 1, ch: column ? column - 1 : 0 });
+ }
+ });
+ },
+
+ /**
+ * Moves the content of the current line or the lines selected up a line.
+ */
+ moveLineUp: function () {
+ let cm = editors.get(this);
+ let start = cm.getCursor("start");
+ let end = cm.getCursor("end");
+
+ if (start.line === 0) {
+ return;
+ }
+
+ // Get the text in the lines selected or the current line of the cursor
+ // and append the text of the previous line.
+ let value;
+ if (start.line !== end.line) {
+ value = cm.getRange({ line: start.line, ch: 0 },
+ { line: end.line, ch: cm.getLine(end.line).length }) + "\n";
+ } else {
+ value = cm.getLine(start.line) + "\n";
+ }
+ value += cm.getLine(start.line - 1);
+
+ // Replace the previous line and the currently selected lines with the new
+ // value and maintain the selection of the text.
+ cm.replaceRange(value, { line: start.line - 1, ch: 0 },
+ { line: end.line, ch: cm.getLine(end.line).length });
+ cm.setSelection({ line: start.line - 1, ch: start.ch },
+ { line: end.line - 1, ch: end.ch });
+ },
+
+ /**
+ * Moves the content of the current line or the lines selected down a line.
+ */
+ moveLineDown: function () {
+ let cm = editors.get(this);
+ let start = cm.getCursor("start");
+ let end = cm.getCursor("end");
+
+ if (end.line + 1 === cm.lineCount()) {
+ return;
+ }
+
+ // Get the text of next line and append the text in the lines selected
+ // or the current line of the cursor.
+ let value = cm.getLine(end.line + 1) + "\n";
+ if (start.line !== end.line) {
+ value += cm.getRange({ line: start.line, ch: 0 },
+ { line: end.line, ch: cm.getLine(end.line).length });
+ } else {
+ value += cm.getLine(start.line);
+ }
+
+ // Replace the currently selected lines and the next line with the new
+ // value and maintain the selection of the text.
+ cm.replaceRange(value, { line: start.line, ch: 0 },
+ { line: end.line + 1, ch: cm.getLine(end.line + 1).length});
+ cm.setSelection({ line: start.line + 1, ch: start.ch },
+ { line: end.line + 1, ch: end.ch });
+ },
+
+ /**
+ * Intercept CodeMirror's Find and replace key shortcut to select the search input
+ */
+ findOrReplace: function (node, isReplaceAll) {
+ let cm = editors.get(this);
+ let isInput = node.tagName === "INPUT";
+ let isSearchInput = isInput && node.type === "search";
+ // replace box is a different input instance than search, and it is
+ // located in a code mirror dialog
+ let isDialogInput = isInput &&
+ node.parentNode &&
+ node.parentNode.classList.contains("CodeMirror-dialog");
+ if (!(isSearchInput || isDialogInput)) {
+ return;
+ }
+
+ if (isSearchInput || isReplaceAll) {
+ // select the search input
+ // it's the precise reason why we reimplement these key shortcuts
+ node.select();
+ }
+
+ // need to call it since we prevent the propagation of the event and
+ // cancel codemirror's key handling
+ cm.execCommand("find");
+ },
+
+ /**
+ * Intercept CodeMirror's findNext and findPrev key shortcut to allow
+ * immediately search for next occurance after typing a word to search.
+ */
+ findNextOrPrev: function (node, isFindPrev) {
+ let cm = editors.get(this);
+ let isInput = node.tagName === "INPUT";
+ let isSearchInput = isInput && node.type === "search";
+ if (!isSearchInput) {
+ return;
+ }
+ let query = node.value;
+ // cm.state.search allows to automatically start searching for the next occurance
+ // it's the precise reason why we reimplement these key shortcuts
+ if (!cm.state.search || cm.state.search.query !== query) {
+ cm.state.search = {
+ posFrom: null,
+ posTo: null,
+ overlay: null,
+ query
+ };
+ }
+
+ // need to call it since we prevent the propagation of the event and
+ // cancel codemirror's key handling
+ if (isFindPrev) {
+ cm.execCommand("findPrev");
+ } else {
+ cm.execCommand("findNext");
+ }
+ },
+
+ /**
+ * Returns current font size for the editor area, in pixels.
+ */
+ getFontSize: function () {
+ let cm = editors.get(this);
+ let el = cm.getWrapperElement();
+ let win = el.ownerDocument.defaultView;
+
+ return parseInt(win.getComputedStyle(el).getPropertyValue("font-size"), 10);
+ },
+
+ /**
+ * Sets font size for the editor area.
+ */
+ setFontSize: function (size) {
+ let cm = editors.get(this);
+ cm.getWrapperElement().style.fontSize = parseInt(size, 10) + "px";
+ cm.refresh();
+ },
+
+ /**
+ * Sets an option for the editor. For most options it just defers to
+ * CodeMirror.setOption, but certain ones are maintained within the editor
+ * instance.
+ */
+ setOption: function (o, v) {
+ let cm = editors.get(this);
+
+ // Save the state of a valid autoCloseBrackets string, so we can reset
+ // it if it gets preffed off and back on.
+ if (o === "autoCloseBrackets" && v) {
+ this.config.autoCloseBracketsSaved = v;
+ }
+
+ if (o === "autocomplete") {
+ this.config.autocomplete = v;
+ this.setupAutoCompletion();
+ } else {
+ cm.setOption(o, v);
+ this.config[o] = v;
+ }
+
+ if (o === "enableCodeFolding") {
+ // The new value maybe explicitly force foldGUtter on or off, ignoring
+ // the prefs service.
+ this.updateCodeFoldingGutter();
+ }
+ },
+
+ /**
+ * Gets an option for the editor. For most options it just defers to
+ * CodeMirror.getOption, but certain ones are maintained within the editor
+ * instance.
+ */
+ getOption: function (o) {
+ let cm = editors.get(this);
+ if (o === "autocomplete") {
+ return this.config.autocomplete;
+ }
+
+ return cm.getOption(o);
+ },
+
+ /**
+ * Sets up autocompletion for the editor. Lazily imports the required
+ * dependencies because they vary by editor mode.
+ *
+ * Autocompletion is special, because we don't want to automatically use
+ * it just because it is preffed on (it still needs to be requested by the
+ * editor), but we do want to always disable it if it is preffed off.
+ */
+ setupAutoCompletion: function () {
+ // The autocomplete module will overwrite this.initializeAutoCompletion
+ // with a mode specific autocompletion handler.
+ if (!this.initializeAutoCompletion) {
+ this.extend(require("./autocomplete"));
+ }
+
+ if (this.config.autocomplete && Services.prefs.getBoolPref(AUTOCOMPLETE)) {
+ this.initializeAutoCompletion(this.config.autocompleteOpts);
+ } else {
+ this.destroyAutoCompletion();
+ }
+ },
+
+ /**
+ * Extends an instance of the Editor object with additional
+ * functions. Each function will be called with context as
+ * the first argument. Context is a {ed, cm} object where
+ * 'ed' is an instance of the Editor object and 'cm' is an
+ * instance of the CodeMirror object. Example:
+ *
+ * function hello(ctx, name) {
+ * let { cm, ed } = ctx;
+ * cm; // CodeMirror instance
+ * ed; // Editor instance
+ * name; // 'Mozilla'
+ * }
+ *
+ * editor.extend({ hello: hello });
+ * editor.hello('Mozilla');
+ */
+ extend: function (funcs) {
+ Object.keys(funcs).forEach(name => {
+ let cm = editors.get(this);
+ let ctx = { ed: this, cm: cm, Editor: Editor};
+
+ if (name === "initialize") {
+ funcs[name](ctx);
+ return;
+ }
+
+ this[name] = funcs[name].bind(null, ctx);
+ });
+ },
+
+ destroy: function () {
+ this.container = null;
+ this.config = null;
+ this.version = null;
+
+ if (this._prefObserver) {
+ this._prefObserver.off(TAB_SIZE, this.reloadPreferences);
+ this._prefObserver.off(EXPAND_TAB, this.reloadPreferences);
+ this._prefObserver.off(KEYMAP, this.reloadPreferences);
+ this._prefObserver.off(AUTO_CLOSE, this.reloadPreferences);
+ this._prefObserver.off(AUTOCOMPLETE, this.reloadPreferences);
+ this._prefObserver.off(DETECT_INDENT, this.reloadPreferences);
+ this._prefObserver.off(ENABLE_CODE_FOLDING, this.reloadPreferences);
+ this._prefObserver.destroy();
+ }
+
+ this.emit("destroy");
+ },
+
+ updateCodeFoldingGutter: function () {
+ let shouldFoldGutter = this.config.enableCodeFolding;
+ let foldGutterIndex = this.config.gutters.indexOf("CodeMirror-foldgutter");
+ let cm = editors.get(this);
+
+ if (shouldFoldGutter === undefined) {
+ shouldFoldGutter = Services.prefs.getBoolPref(ENABLE_CODE_FOLDING);
+ }
+
+ if (shouldFoldGutter) {
+ // Add the gutter before enabling foldGutter
+ if (foldGutterIndex === -1) {
+ let gutters = this.config.gutters.slice();
+ gutters.push("CodeMirror-foldgutter");
+ this.setOption("gutters", gutters);
+ }
+
+ this.setOption("foldGutter", true);
+ } else {
+ // No code should remain folded when folding is off.
+ if (cm) {
+ cm.execCommand("unfoldAll");
+ }
+
+ // Remove the gutter so it doesn't take up space
+ if (foldGutterIndex !== -1) {
+ let gutters = this.config.gutters.slice();
+ gutters.splice(foldGutterIndex, 1);
+ this.setOption("gutters", gutters);
+ }
+
+ this.setOption("foldGutter", false);
+ }
+ },
+
+ /**
+ * Register all key shortcuts.
+ */
+ _initShortcuts: function (win) {
+ let shortcuts = new KeyShortcuts({
+ window: win
+ });
+ this._onShortcut = this._onShortcut.bind(this);
+ let keys = [
+ "find.key",
+ "findNext.key",
+ "findPrev.key"
+ ];
+
+ if (OS === "Darwin") {
+ keys.push("replaceAllMac.key");
+ } else {
+ keys.push("replaceAll.key");
+ }
+ // Process generic keys:
+ keys.forEach(name => {
+ let key = L10N.getStr(name);
+ shortcuts.on(key, (_, event) => this._onShortcut(name, event));
+ });
+ },
+ /**
+ * Key shortcut listener.
+ */
+ _onShortcut: function (name, event) {
+ if (!this._isInputOrTextarea(event.target)) {
+ return;
+ }
+ let node = event.originalTarget;
+
+ switch (name) {
+ // replaceAll.key is Alt + find.key
+ case "replaceAllMac.key":
+ this.findOrReplace(node, true);
+ break;
+ // replaceAll.key is Shift + find.key
+ case "replaceAll.key":
+ this.findOrReplace(node, true);
+ break;
+ case "find.key":
+ this.findOrReplace(node, false);
+ break;
+ // findPrev.key is Shift + findNext.key
+ case "findPrev.key":
+ this.findNextOrPrev(node, true);
+ break;
+ case "findNext.key":
+ this.findNextOrPrev(node, false);
+ break;
+ default:
+ console.error("Unexpected editor key shortcut", name);
+ return;
+ }
+ // Prevent default for this action
+ event.stopPropagation();
+ event.preventDefault();
+ },
+
+ /**
+ * Check if a node is an input or textarea
+ */
+ _isInputOrTextarea: function (element) {
+ let name = element.tagName.toLowerCase();
+ return name === "input" || name === "textarea";
+ }
+};
+
+// Since Editor is a thin layer over CodeMirror some methods
+// are mapped directly—without any changes.
+
+CM_MAPPING.forEach(name => {
+ Editor.prototype[name] = function (...args) {
+ let cm = editors.get(this);
+ return cm[name].apply(cm, args);
+ };
+});
+
+// Static methods on the Editor object itself.
+
+/**
+ * Returns a string representation of a shortcut 'key' with
+ * a OS specific modifier. Cmd- for Macs, Ctrl- for other
+ * platforms. Useful with extraKeys configuration option.
+ *
+ * CodeMirror defines all keys with modifiers in the following
+ * order: Shift - Ctrl/Cmd - Alt - Key
+ */
+Editor.accel = function (key, modifiers = {}) {
+ return (modifiers.shift ? "Shift-" : "") +
+ (Services.appinfo.OS == "Darwin" ? "Cmd-" : "Ctrl-") +
+ (modifiers.alt ? "Alt-" : "") + key;
+};
+
+/**
+ * Returns a string representation of a shortcut for a
+ * specified command 'cmd'. Append Cmd- for macs, Ctrl- for other
+ * platforms unless noaccel is specified in the options. Useful when overwriting
+ * or disabling default shortcuts.
+ */
+Editor.keyFor = function (cmd, opts = { noaccel: false }) {
+ let key = L10N.getStr(cmd + ".commandkey");
+ return opts.noaccel ? key : Editor.accel(key);
+};
+
+/**
+ * We compute the CSS property names, values, and color names to be used with
+ * CodeMirror to more closely reflect what is supported by the target platform.
+ * The database is used to replace the values used in CodeMirror while initiating
+ * an editor object. This is done here instead of the file codemirror/css.js so
+ * as to leave that file untouched and easily upgradable.
+ */
+function getCSSKeywords(cssProperties) {
+ function keySet(array) {
+ let keys = {};
+ for (let i = 0; i < array.length; ++i) {
+ keys[array[i]] = true;
+ }
+ return keys;
+ }
+
+ let propertyKeywords = cssProperties.getNames();
+ let colorKeywords = {};
+ let valueKeywords = {};
+
+ propertyKeywords.forEach(property => {
+ if (property.includes("color")) {
+ cssProperties.getValues(property).forEach(value => {
+ colorKeywords[value] = true;
+ });
+ } else {
+ cssProperties.getValues(property).forEach(value => {
+ valueKeywords[value] = true;
+ });
+ }
+ });
+
+ return {
+ propertyKeywords: keySet(propertyKeywords),
+ colorKeywords: colorKeywords,
+ valueKeywords: valueKeywords
+ };
+}
+
+module.exports = Editor;
diff --git a/devtools/client/sourceeditor/moz.build b/devtools/client/sourceeditor/moz.build
new file mode 100644
index 000000000..5081325c5
--- /dev/null
+++ b/devtools/client/sourceeditor/moz.build
@@ -0,0 +1,18 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += [
+ 'tern',
+]
+
+DevToolsModules(
+ 'autocomplete.js',
+ 'css-autocompleter.js',
+ 'debugger.js',
+ 'editor.js'
+)
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
diff --git a/devtools/client/sourceeditor/tern/README b/devtools/client/sourceeditor/tern/README
new file mode 100644
index 000000000..d41e7b456
--- /dev/null
+++ b/devtools/client/sourceeditor/tern/README
@@ -0,0 +1,13 @@
+This is the Tern code-analysis engine packaged for the Mozilla Project.
+
+Tern is a stand-alone code-analysis engine for JavaScript. It is intended to be used with a code editor plugin to enhance the editor's support for intelligent JavaScript editing
+
+
+# Upgrade
+
+Currently used version is 0.6.2. To upgrade, download the latest release from http://ternjs.net/, and copy the files from lib/ into this directory.
+
+You may also need to update the CodeMirror plugin found in devtools/client/sourceeditor/codemirror/addon/tern, but it will most likely work without updating.
+
+Replace instances of `require("acorn")` with `require("acorn/acorn")`
+Replace instances of `acorn/dist/` with `acorn/` \ No newline at end of file
diff --git a/devtools/client/sourceeditor/tern/browser.js b/devtools/client/sourceeditor/tern/browser.js
new file mode 100644
index 000000000..a37d6569a
--- /dev/null
+++ b/devtools/client/sourceeditor/tern/browser.js
@@ -0,0 +1,2921 @@
+module.exports = {
+ "!name": "browser",
+ "location": {
+ "assign": {
+ "!type": "fn(url: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.location",
+ "!doc": "Load the document at the provided URL."
+ },
+ "replace": {
+ "!type": "fn(url: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.location",
+ "!doc": "Replace the current document with the one at the provided URL. The difference from the assign() method is that after using replace() the current page will not be saved in session history, meaning the user won't be able to use the Back button to navigate to it."
+ },
+ "reload": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.location",
+ "!doc": "Reload the document from the current URL. forceget is a boolean, which, when it is true, causes the page to always be reloaded from the server. If it is false or not specified, the browser may reload the page from its cache."
+ },
+ "origin": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.location",
+ "!doc": "The origin of the URL."
+ },
+ "hash": {
+ "!type": "string",
+ "!url": "https://developer.mthat follows the # symbol, including the # symbol."
+ },
+ "search": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.location",
+ "!doc": "The part of the URL that follows the ? symbol, including the ? symbol."
+ },
+ "pathname": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.location",
+ "!doc": "The path (relative to the host)."
+ },
+ "port": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.location",
+ "!doc": "The port number of the URL."
+ },
+ "hostname": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.location",
+ "!doc": "The host name (without the port number or square brackets)."
+ },
+ "host": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.location",
+ "!doc": "The host name and port number."
+ },
+ "protocol": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.location",
+ "!doc": "The protocol of the URL."
+ },
+ "href": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.location",
+ "!doc": "The entire URL."
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.location",
+ "!doc": "Returns a location object with information about the current location of the document. Assigning to the location property changes the current page to the new address."
+ },
+ "Node": {
+ "!type": "fn()",
+ "prototype": {
+ "parentElement": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.parentElement",
+ "!doc": "Returns the DOM node's parent Element, or null if the node either has no parent, or its parent isn't a DOM Element."
+ },
+ "textContent": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.textContent",
+ "!doc": "Gets or sets the text content of a node and its descendants."
+ },
+ "baseURI": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.baseURI",
+ "!doc": "The absolute base URI of a node or null if unable to obtain an absolute URI."
+ },
+ "localName": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.localName",
+ "!doc": "Returns the local part of the qualified name of this node."
+ },
+ "prefix": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.prefix",
+ "!doc": "Returns the namespace prefix of the specified node, or null if no prefix is specified. This property is read only."
+ },
+ "namespaceURI": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.namespaceURI",
+ "!doc": "The namespace URI of the node, or null if the node is not in a namespace (read-only). When the node is a document, it returns the XML namespace for the current document."
+ },
+ "ownerDocument": {
+ "!type": "+Document",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.ownerDocument",
+ "!doc": "The ownerDocument property returns the top-level document object for this node."
+ },
+ "attributes": {
+ "!type": "+NamedNodeMap",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.attributes",
+ "!doc": "A collection of all attribute nodes registered to the specified node. It is a NamedNodeMap,not an Array, so it has no Array methods and the Attr nodes' indexes may differ among browsers."
+ },
+ "nextSibling": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.nextSibling",
+ "!doc": "Returns the node immediately following the specified one in its parent's childNodes list, or null if the specified node is the last node in that list."
+ },
+ "previousSibling": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.previousSibling",
+ "!doc": "Returns the node immediately preceding the specified one in its parent's childNodes list, null if the specified node is the first in that list."
+ },
+ "lastChild": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.lastChild",
+ "!doc": "Returns the last child of a node."
+ },
+ "firstChild": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.firstChild",
+ "!doc": "Returns the node's first child in the tree, or null if the node is childless. If the node is a Document, it returns the first node in the list of its direct children."
+ },
+ "childNodes": {
+ "!type": "+NodeList",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.childNodes",
+ "!doc": "Returns a collection of child nodes of the given element."
+ },
+ "parentNode": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.parentNode",
+ "!doc": "Returns the parent of the specified node in the DOM tree."
+ },
+ "nodeType": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.nodeType",
+ "!doc": "Returns an integer code representing the type of the node."
+ },
+ "nodeValue": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.nodeValue",
+ "!doc": "Returns or sets the value of the current node."
+ },
+ "nodeName": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.nodeName",
+ "!doc": "Returns the name of the current node as a string."
+ },
+ "tagName": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.nodeName",
+ "!doc": "Returns the name of the current node as a string."
+ },
+ "insertBefore": {
+ "!type": "fn(newElt: +Element, before: +Element) -> +Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.insertBefore",
+ "!doc": "Inserts the specified node before a reference element as a child of the current node."
+ },
+ "replaceChild": {
+ "!type": "fn(newElt: +Element, oldElt: +Element) -> +Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.replaceChild",
+ "!doc": "Replaces one child node of the specified element with another."
+ },
+ "removeChild": {
+ "!type": "fn(oldElt: +Element) -> +Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.removeChild",
+ "!doc": "Removes a child node from the DOM. Returns removed node."
+ },
+ "appendChild": {
+ "!type": "fn(newElt: +Element) -> +Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.appendChild",
+ "!doc": "Adds a node to the end of the list of children of a specified parent node. If the node already exists it is removed from current parent node, then added to new parent node."
+ },
+ "hasChildNodes": {
+ "!type": "fn() -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.hasChildNodes",
+ "!doc": "Returns a Boolean value indicating whether the current Node has child nodes or not."
+ },
+ "cloneNode": {
+ "!type": "fn(deep: bool) -> +Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.cloneNode",
+ "!doc": "Returns a duplicate of the node on which this method was called."
+ },
+ "normalize": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.normalize",
+ "!doc": "Puts the specified node and all of its subtree into a \"normalized\" form. In a normalized subtree, no text nodes in the subtree are empty and there are no adjacent text nodes."
+ },
+ "isSupported": {
+ "!type": "fn(features: string, version: number) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.isSupported",
+ "!doc": "Tests whether the DOM implementation implements a specific feature and that feature is supported by this node."
+ },
+ "hasAttributes": {
+ "!type": "fn() -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.hasAttributes",
+ "!doc": "Returns a boolean value of true or false, indicating if the current element has any attributes or not."
+ },
+ "lookupPrefix": {
+ "!type": "fn(uri: string) -> string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.lookupPrefix",
+ "!doc": "Returns the prefix for a given namespaceURI if present, and null if not. When multiple prefixes are possible, the result is implementation-dependent."
+ },
+ "isDefaultNamespace": {
+ "!type": "fn(uri: string) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.isDefaultNamespace",
+ "!doc": "Accepts a namespace URI as an argument and returns true if the namespace is the default namespace on the given node or false if not."
+ },
+ "lookupNamespaceURI": {
+ "!type": "fn(uri: string) -> string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.lookupNamespaceURI",
+ "!doc": "Takes a prefix and returns the namespaceURI associated with it on the given node if found (and null if not). Supplying null for the prefix will return the default namespace."
+ },
+ "addEventListener": {
+ "!type": "fn(type: string, listener: fn(e: +Event), capture: bool)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/EventTarget.addEventListener",
+ "!doc": "Registers a single event listener on a single target. The event target may be a single element in a document, the document itself, a window, or an XMLHttpRequest."
+ },
+ "removeEventListener": {
+ "!type": "fn(type: string, listener: fn(), capture: bool)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/EventTarget.removeEventListener",
+ "!doc": "Allows the removal of event listeners from the event target."
+ },
+ "isSameNode": {
+ "!type": "fn(other: +Node) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.isSameNode",
+ "!doc": "Tests whether two nodes are the same, that is they reference the same object."
+ },
+ "isEqualNode": {
+ "!type": "fn(other: +Node) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.isEqualNode",
+ "!doc": "Tests whether two nodes are equal."
+ },
+ "compareDocumentPosition": {
+ "!type": "fn(other: +Node) -> number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.compareDocumentPosition",
+ "!doc": "Compares the position of the current node against another node in any other document."
+ },
+ "contains": {
+ "!type": "fn(other: +Node) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node.contains",
+ "!doc": "Indicates whether a node is a descendent of a given node."
+ },
+ "dispatchEvent": {
+ "!type": "fn(event: +Event) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/EventTarget.dispatchEvent",
+ "!doc": "Dispatches an event into the event system. The event is subject to the same capturing and bubbling behavior as directly dispatched events."
+ },
+ "ELEMENT_NODE": "number",
+ "ATTRIBUTE_NODE": "number",
+ "TEXT_NODE": "number",
+ "CDATA_SECTION_NODE": "number",
+ "ENTITY_REFERENCE_NODE": "number",
+ "ENTITY_NODE": "number",
+ "PROCESSING_INSTRUCTION_NODE": "number",
+ "COMMENT_NODE": "number",
+ "DOCUMENT_NODE": "number",
+ "DOCUMENT_TYPE_NODE": "number",
+ "DOCUMENT_FRAGMENT_NODE": "number",
+ "NOTATION_NODE": "number",
+ "DOCUMENT_POSITION_DISCONNECTED": "number",
+ "DOCUMENT_POSITION_PRECEDING": "number",
+ "DOCUMENT_POSITION_FOLLOWING": "number",
+ "DOCUMENT_POSITION_CONTAINS": "number",
+ "DOCUMENT_POSITION_CONTAINED_BY": "number",
+ "DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC": "number"
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Node",
+ "!doc": "A Node is an interface from which a number of DOM types inherit, and allows these various types to be treated (or tested) similarly."
+ },
+ "Element": {
+ "!type": "fn()",
+ "prototype": {
+ "!proto": "Node.prototype",
+ "getAttribute": {
+ "!type": "fn(name: string) -> string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.getAttribute",
+ "!doc": "Returns the value of the named attribute on the specified element. If the named attribute does not exist, the value returned will either be null or \"\" (the empty string)."
+ },
+ "setAttribute": {
+ "!type": "fn(name: string, value: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.setAttribute",
+ "!doc": "Adds a new attribute or changes the value of an existing attribute on the specified element."
+ },
+ "removeAttribute": {
+ "!type": "fn(name: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.removeAttribute",
+ "!doc": "Removes an attribute from the specified element."
+ },
+ "getAttributeNode": {
+ "!type": "fn(name: string) -> +Attr",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.getAttributeNode",
+ "!doc": "Returns the specified attribute of the specified element, as an Attr node."
+ },
+ "getElementsByTagName": {
+ "!type": "fn(tagName: string) -> +NodeList",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.getElementsByTagName",
+ "!doc": "Returns a list of elements with the given tag name. The subtree underneath the specified element is searched, excluding the element itself. The returned list is live, meaning that it updates itself with the DOM tree automatically. Consequently, there is no need to call several times element.getElementsByTagName with the same element and arguments."
+ },
+ "getElementsByTagNameNS": {
+ "!type": "fn(ns: string, tagName: string) -> +NodeList",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.getElementsByTagNameNS",
+ "!doc": "Returns a list of elements with the given tag name belonging to the given namespace."
+ },
+ "getAttributeNS": {
+ "!type": "fn(ns: string, name: string) -> string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.getAttributeNS",
+ "!doc": "Returns the string value of the attribute with the specified namespace and name. If the named attribute does not exist, the value returned will either be null or \"\" (the empty string)."
+ },
+ "setAttributeNS": {
+ "!type": "fn(ns: string, name: string, value: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.setAttributeNS",
+ "!doc": "Adds a new attribute or changes the value of an attribute with the given namespace and name."
+ },
+ "removeAttributeNS": {
+ "!type": "fn(ns: string, name: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.removeAttributeNS",
+ "!doc": "removeAttributeNS removes the specified attribute from an element."
+ },
+ "getAttributeNodeNS": {
+ "!type": "fn(ns: string, name: string) -> +Attr",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.getAttributeNodeNS",
+ "!doc": "Returns the Attr node for the attribute with the given namespace and name."
+ },
+ "hasAttribute": {
+ "!type": "fn(name: string) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.hasAttribute",
+ "!doc": "hasAttribute returns a boolean value indicating whether the specified element has the specified attribute or not."
+ },
+ "hasAttributeNS": {
+ "!type": "fn(ns: string, name: string) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.hasAttributeNS",
+ "!doc": "hasAttributeNS returns a boolean value indicating whether the current element has the specified attribute."
+ },
+ "focus": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.focus",
+ "!doc": "Sets focus on the specified element, if it can be focused."
+ },
+ "blur": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.blur",
+ "!doc": "The blur method removes keyboard focus from the current element."
+ },
+ "scrollIntoView": {
+ "!type": "fn(top: bool)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.scrollIntoView",
+ "!doc": "The scrollIntoView() method scrolls the element into view."
+ },
+ "scrollByLines": {
+ "!type": "fn(lines: number)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.scrollByLines",
+ "!doc": "Scrolls the document by the given number of lines."
+ },
+ "scrollByPages": {
+ "!type": "fn(pages: number)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.scrollByPages",
+ "!doc": "Scrolls the current document by the specified number of pages."
+ },
+ "getElementsByClassName": {
+ "!type": "fn(name: string) -> +NodeList",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.getElementsByClassName",
+ "!doc": "Returns a set of elements which have all the given class names. When called on the document object, the complete document is searched, including the root node. You may also call getElementsByClassName on any element; it will return only elements which are descendants of the specified root element with the given class names."
+ },
+ "querySelector": {
+ "!type": "fn(selectors: string) -> +Node",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Element.querySelector",
+ "!doc": "Returns the first element that is a descendent of the element on which it is invoked that matches the specified group of selectors."
+ },
+ "querySelectorAll": {
+ "!type": "fn(selectors: string) -> +NodeList",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Element.querySelectorAll",
+ "!doc": "Returns a non-live NodeList of all elements descended from the element on which it is invoked that match the specified group of CSS selectors."
+ },
+ "getClientRects": {
+ "!type": "fn() -> [+ClientRect]",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.getClientRects",
+ "!doc": "Returns a collection of rectangles that indicate the bounding rectangles for each box in a client."
+ },
+ "getBoundingClientRect": {
+ "!type": "fn() -> +ClientRect",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.getBoundingClientRect",
+ "!doc": "Returns a text rectangle object that encloses a group of text rectangles."
+ },
+ "setAttributeNode": {
+ "!type": "fn(attr: +Attr) -> +Attr",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.setAttributeNode",
+ "!doc": "Adds a new Attr node to the specified element."
+ },
+ "removeAttributeNode": {
+ "!type": "fn(attr: +Attr) -> +Attr",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.removeAttributeNode",
+ "!doc": "Removes the specified attribute from the current element."
+ },
+ "setAttributeNodeNS": {
+ "!type": "fn(attr: +Attr) -> +Attr",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.setAttributeNodeNS",
+ "!doc": "Adds a new namespaced attribute node to an element."
+ },
+ "insertAdjacentHTML": {
+ "!type": "fn(position: string, text: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.insertAdjacentHTML",
+ "!doc": "Parses the specified text as HTML or XML and inserts the resulting nodes into the DOM tree at a specified position. It does not reparse the element it is being used on and thus it does not corrupt the existing elements inside the element. This, and avoiding the extra step of serialization make it much faster than direct innerHTML manipulation."
+ },
+ "children": {
+ "!type": "+HTMLCollection",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Element.children",
+ "!doc": "Returns a collection of child elements of the given element."
+ },
+ "childElementCount": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Element.childElementCount",
+ "!doc": "Returns the number of child elements of the given element."
+ },
+ "className": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.className",
+ "!doc": "Gets and sets the value of the class attribute of the specified element."
+ },
+ "style": {
+ "cssText": "string",
+ "alignmentBaseline": "string",
+ "background": "string",
+ "backgroundAttachment": "string",
+ "backgroundClip": "string",
+ "backgroundColor": "string",
+ "backgroundImage": "string",
+ "backgroundOrigin": "string",
+ "backgroundPosition": "string",
+ "backgroundPositionX": "string",
+ "backgroundPositionY": "string",
+ "backgroundRepeat": "string",
+ "backgroundRepeatX": "string",
+ "backgroundRepeatY": "string",
+ "backgroundSize": "string",
+ "baselineShift": "string",
+ "border": "string",
+ "borderBottom": "string",
+ "borderBottomColor": "string",
+ "borderBottomLeftRadius": "string",
+ "borderBottomRightRadius": "string",
+ "borderBottomStyle": "string",
+ "borderBottomWidth": "string",
+ "borderCollapse": "string",
+ "borderColor": "string",
+ "borderImage": "string",
+ "borderImageOutset": "string",
+ "borderImageRepeat": "string",
+ "borderImageSlice": "string",
+ "borderImageSource": "string",
+ "borderImageWidth": "string",
+ "borderLeft": "string",
+ "borderLeftColor": "string",
+ "borderLeftStyle": "string",
+ "borderLeftWidth": "string",
+ "borderRadius": "string",
+ "borderRight": "string",
+ "borderRightColor": "string",
+ "borderRightStyle": "string",
+ "borderRightWidth": "string",
+ "borderSpacing": "string",
+ "borderStyle": "string",
+ "borderTop": "string",
+ "borderTopColor": "string",
+ "borderTopLeftRadius": "string",
+ "borderTopRightRadius": "string",
+ "borderTopStyle": "string",
+ "borderTopWidth": "string",
+ "borderWidth": "string",
+ "bottom": "string",
+ "boxShadow": "string",
+ "boxSizing": "string",
+ "captionSide": "string",
+ "clear": "string",
+ "clip": "string",
+ "clipPath": "string",
+ "clipRule": "string",
+ "color": "string",
+ "colorInterpolation": "string",
+ "colorInterpolationFilters": "string",
+ "colorProfile": "string",
+ "colorRendering": "string",
+ "content": "string",
+ "counterIncrement": "string",
+ "counterReset": "string",
+ "cursor": "string",
+ "direction": "string",
+ "display": "string",
+ "dominantBaseline": "string",
+ "emptyCells": "string",
+ "enableBackground": "string",
+ "fill": "string",
+ "fillOpacity": "string",
+ "fillRule": "string",
+ "filter": "string",
+ "float": "string",
+ "floodColor": "string",
+ "floodOpacity": "string",
+ "font": "string",
+ "fontFamily": "string",
+ "fontSize": "string",
+ "fontStretch": "string",
+ "fontStyle": "string",
+ "fontVariant": "string",
+ "fontWeight": "string",
+ "glyphOrientationHorizontal": "string",
+ "glyphOrientationVertical": "string",
+ "height": "string",
+ "imageRendering": "string",
+ "kerning": "string",
+ "left": "string",
+ "letterSpacing": "string",
+ "lightingColor": "string",
+ "lineHeight": "string",
+ "listStyle": "string",
+ "listStyleImage": "string",
+ "listStylePosition": "string",
+ "listStyleType": "string",
+ "margin": "string",
+ "marginBottom": "string",
+ "marginLeft": "string",
+ "marginRight": "string",
+ "marginTop": "string",
+ "marker": "string",
+ "markerEnd": "string",
+ "markerMid": "string",
+ "markerStart": "string",
+ "mask": "string",
+ "maxHeight": "string",
+ "maxWidth": "string",
+ "minHeight": "string",
+ "minWidth": "string",
+ "opacity": "string",
+ "orphans": "string",
+ "outline": "string",
+ "outlineColor": "string",
+ "outlineOffset": "string",
+ "outlineStyle": "string",
+ "outlineWidth": "string",
+ "overflow": "string",
+ "overflowWrap": "string",
+ "overflowX": "string",
+ "overflowY": "string",
+ "padding": "string",
+ "paddingBottom": "string",
+ "paddingLeft": "string",
+ "paddingRight": "string",
+ "paddingTop": "string",
+ "page": "string",
+ "pageBreakAfter": "string",
+ "pageBreakBefore": "string",
+ "pageBreakInside": "string",
+ "pointerEvents": "string",
+ "position": "string",
+ "quotes": "string",
+ "resize": "string",
+ "right": "string",
+ "shapeRendering": "string",
+ "size": "string",
+ "speak": "string",
+ "src": "string",
+ "stopColor": "string",
+ "stopOpacity": "string",
+ "stroke": "string",
+ "strokeDasharray": "string",
+ "strokeDashoffset": "string",
+ "strokeLinecap": "string",
+ "strokeLinejoin": "string",
+ "strokeMiterlimit": "string",
+ "strokeOpacity": "string",
+ "strokeWidth": "string",
+ "tabSize": "string",
+ "tableLayout": "string",
+ "textAlign": "string",
+ "textAnchor": "string",
+ "textDecoration": "string",
+ "textIndent": "string",
+ "textLineThrough": "string",
+ "textLineThroughColor": "string",
+ "textLineThroughMode": "string",
+ "textLineThroughStyle": "string",
+ "textLineThroughWidth": "string",
+ "textOverflow": "string",
+ "textOverline": "string",
+ "textOverlineColor": "string",
+ "textOverlineMode": "string",
+ "textOverlineStyle": "string",
+ "textOverlineWidth": "string",
+ "textRendering": "string",
+ "textShadow": "string",
+ "textTransform": "string",
+ "textUnderline": "string",
+ "textUnderlineColor": "string",
+ "textUnderlineMode": "string",
+ "textUnderlineStyle": "string",
+ "textUnderlineWidth": "string",
+ "top": "string",
+ "unicodeBidi": "string",
+ "unicodeRange": "string",
+ "vectorEffect": "string",
+ "verticalAlign": "string",
+ "visibility": "string",
+ "whiteSpace": "string",
+ "width": "string",
+ "wordBreak": "string",
+ "wordSpacing": "string",
+ "wordWrap": "string",
+ "writingMode": "string",
+ "zIndex": "string",
+ "zoom": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.style",
+ "!doc": "Returns an object that represents the element's style attribute."
+ },
+ "classList": {
+ "!type": "+DOMTokenList",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.classList",
+ "!doc": "Returns a token list of the class attribute of the element."
+ },
+ "contentEditable": {
+ "!type": "bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Element.contentEditable",
+ "!doc": "Indicates whether or not the element is editable."
+ },
+ "firstElementChild": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Element.firstElementChild",
+ "!doc": "Returns the element's first child element or null if there are no child elements."
+ },
+ "lastElementChild": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Element.lastElementChild",
+ "!doc": "Returns the element's last child element or null if there are no child elements."
+ },
+ "nextElementSibling": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Element.nextElementSibling",
+ "!doc": "Returns the element immediately following the specified one in its parent's children list, or null if the specified element is the last one in the list."
+ },
+ "previousElementSibling": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Element.previousElementSibling",
+ "!doc": "Returns the element immediately prior to the specified one in its parent's children list, or null if the specified element is the first one in the list."
+ },
+ "tabIndex": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.tabIndex",
+ "!doc": "Gets/sets the tab order of the current element."
+ },
+ "title": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.title",
+ "!doc": "Establishes the text to be displayed in a 'tool tip' popup when the mouse is over the displayed node."
+ },
+ "width": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.offsetWidth",
+ "!doc": "Returns the layout width of an element."
+ },
+ "height": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.offsetHeight",
+ "!doc": "Height of an element relative to the element's offsetParent."
+ },
+ "getContext": {
+ "!type": "fn(id: string) -> CanvasRenderingContext2D",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/HTMLCanvasElement",
+ "!doc": "DOM canvas elements expose the HTMLCanvasElement interface, which provides properties and methods for manipulating the layout and presentation of canvas elements. The HTMLCanvasElement interface inherits the properties and methods of the element object interface."
+ },
+ "supportsContext": "fn(id: string) -> bool",
+ "oncopy": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.oncopy",
+ "!doc": "The oncopy property returns the onCopy event handler code on the current element."
+ },
+ "oncut": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.oncut",
+ "!doc": "The oncut property returns the onCut event handler code on the current element."
+ },
+ "onpaste": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onpaste",
+ "!doc": "The onpaste property returns the onPaste event handler code on the current element."
+ },
+ "onbeforeunload": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/HTML/Element/body",
+ "!doc": "The HTML <body> element represents the main content of an HTML document. There is only one <body> element in a document."
+ },
+ "onfocus": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onfocus",
+ "!doc": "The onfocus property returns the onFocus event handler code on the current element."
+ },
+ "onblur": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onblur",
+ "!doc": "The onblur property returns the onBlur event handler code, if any, that exists on the current element."
+ },
+ "onchange": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onchange",
+ "!doc": "The onchange property sets and returns the onChange event handler code for the current element."
+ },
+ "onclick": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onclick",
+ "!doc": "The onclick property returns the onClick event handler code on the current element."
+ },
+ "ondblclick": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.ondblclick",
+ "!doc": "The ondblclick property returns the onDblClick event handler code on the current element."
+ },
+ "onmousedown": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onmousedown",
+ "!doc": "The onmousedown property returns the onMouseDown event handler code on the current element."
+ },
+ "onmouseup": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onmouseup",
+ "!doc": "The onmouseup property returns the onMouseUp event handler code on the current element."
+ },
+ "onmousewheel": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Mozilla_event_reference/wheel",
+ "!doc": "The wheel event is fired when a wheel button of a pointing device (usually a mouse) is rotated. This event deprecates the legacy mousewheel event."
+ },
+ "onmouseover": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onmouseover",
+ "!doc": "The onmouseover property returns the onMouseOver event handler code on the current element."
+ },
+ "onmouseout": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onmouseout",
+ "!doc": "The onmouseout property returns the onMouseOut event handler code on the current element."
+ },
+ "onmousemove": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onmousemove",
+ "!doc": "The onmousemove property returns the mousemove event handler code on the current element."
+ },
+ "oncontextmenu": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.oncontextmenu",
+ "!doc": "An event handler property for right-click events on the window. Unless the default behavior is prevented, the browser context menu will activate. Note that this event will occur with any non-disabled right-click event and does not depend on an element possessing the \"contextmenu\" attribute."
+ },
+ "onkeydown": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onkeydown",
+ "!doc": "The onkeydown property returns the onKeyDown event handler code on the current element."
+ },
+ "onkeyup": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onkeyup",
+ "!doc": "The onkeyup property returns the onKeyUp event handler code for the current element."
+ },
+ "onkeypress": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onkeypress",
+ "!doc": "The onkeypress property sets and returns the onKeyPress event handler code for the current element."
+ },
+ "onresize": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onresize",
+ "!doc": "onresize returns the element's onresize event handler code. It can also be used to set the code to be executed when the resize event occurs."
+ },
+ "onscroll": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onscroll",
+ "!doc": "The onscroll property returns the onScroll event handler code on the current element."
+ },
+ "ondragstart": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DragDrop/Drag_Operations",
+ "!doc": "The following describes the steps that occur during a drag and drop operation."
+ },
+ "ondragover": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Mozilla_event_reference/dragover",
+ "!doc": "The dragover event is fired when an element or text selection is being dragged over a valid drop target (every few hundred milliseconds)."
+ },
+ "ondragleave": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Mozilla_event_reference/dragleave",
+ "!doc": "The dragleave event is fired when a dragged element or text selection leaves a valid drop target."
+ },
+ "ondragenter": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Mozilla_event_reference/dragenter",
+ "!doc": "The dragenter event is fired when a dragged element or text selection enters a valid drop target."
+ },
+ "ondragend": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Mozilla_event_reference/dragend",
+ "!doc": "The dragend event is fired when a drag operation is being ended (by releasing a mouse button or hitting the escape key)."
+ },
+ "ondrag": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Mozilla_event_reference/drag",
+ "!doc": "The drag event is fired when an element or text selection is being dragged (every few hundred milliseconds)."
+ },
+ "offsetTop": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.offsetTop",
+ "!doc": "Returns the distance of the current element relative to the top of the offsetParent node."
+ },
+ "offsetLeft": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.offsetLeft",
+ "!doc": "Returns the number of pixels that the upper left corner of the current element is offset to the left within the offsetParent node."
+ },
+ "offsetHeight": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.offsetHeight",
+ "!doc": "Height of an element relative to the element's offsetParent."
+ },
+ "offsetWidth": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.offsetWidth",
+ "!doc": "Returns the layout width of an element."
+ },
+ "scrollTop": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.scrollTop",
+ "!doc": "Gets or sets the number of pixels that the content of an element is scrolled upward."
+ },
+ "scrollLeft": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.scrollLeft",
+ "!doc": "Gets or sets the number of pixels that an element's content is scrolled to the left."
+ },
+ "scrollHeight": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.scrollHeight",
+ "!doc": "Height of the scroll view of an element; it includes the element padding but not its margin."
+ },
+ "scrollWidth": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.scrollWidth",
+ "!doc": "Read-only property that returns either the width in pixels of the content of an element or the width of the element itself, whichever is greater."
+ },
+ "clientTop": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.clientTop",
+ "!doc": "The width of the top border of an element in pixels. It does not include the top margin or padding. clientTop is read-only."
+ },
+ "clientLeft": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.clientLeft",
+ "!doc": "The width of the left border of an element in pixels. It includes the width of the vertical scrollbar if the text direction of the element is right-to-left and if there is an overflow causing a left vertical scrollbar to be rendered. clientLeft does not include the left margin or the left padding. clientLeft is read-only."
+ },
+ "clientHeight": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.clientHeight",
+ "!doc": "Returns the inner height of an element in pixels, including padding but not the horizontal scrollbar height, border, or margin."
+ },
+ "clientWidth": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.clientWidth",
+ "!doc": "The inner width of an element in pixels. It includes padding but not the vertical scrollbar (if present, if rendered), border or margin."
+ },
+ "innerHTML": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.innerHTML",
+ "!doc": "Sets or gets the HTML syntax describing the element's descendants."
+ }
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Element",
+ "!doc": "Represents an element in an HTML or XML document."
+ },
+ "Text": {
+ "!type": "fn()",
+ "prototype": {
+ "!proto": "Node.prototype",
+ "wholeText": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Text.wholeText",
+ "!doc": "Returns all text of all Text nodes logically adjacent to the node. The text is concatenated in document order. This allows you to specify any text node and obtain all adjacent text as a single string."
+ },
+ "splitText": {
+ "!type": "fn(offset: number) -> +Text",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Text.splitText",
+ "!doc": "Breaks the Text node into two nodes at the specified offset, keeping both nodes in the tree as siblings."
+ }
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Text",
+ "!doc": "In the DOM, the Text interface represents the textual content of an Element or Attr. If an element has no markup within its content, it has a single child implementing Text that contains the element's text. However, if the element contains markup, it is parsed into information items and Text nodes that form its children."
+ },
+ "Document": {
+ "!type": "fn()",
+ "prototype": {
+ "!proto": "Node.prototype",
+ "activeElement": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.activeElement",
+ "!doc": "Returns the currently focused element, that is, the element that will get keystroke events if the user types any. This attribute is read only."
+ },
+ "compatMode": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.compatMode",
+ "!doc": "Indicates whether the document is rendered in Quirks mode or Strict mode."
+ },
+ "designMode": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.designMode",
+ "!doc": "Can be used to make any document editable, for example in a <iframe />:"
+ },
+ "dir": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Document.dir",
+ "!doc": "This property should indicate and allow the setting of the directionality of the text of the document, whether left to right (default) or right to left."
+ },
+ "height": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.height",
+ "!doc": "Returns the height of the <body> element of the current document."
+ },
+ "width": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.width",
+ "!doc": "Returns the width of the <body> element of the current document in pixels."
+ },
+ "characterSet": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.characterSet",
+ "!doc": "Returns the character encoding of the current document."
+ },
+ "readyState": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.readyState",
+ "!doc": "Returns \"loading\" while the document is loading, \"interactive\" once it is finished parsing but still loading sub-resources, and \"complete\" once it has loaded."
+ },
+ "location": {
+ "!type": "location",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.location",
+ "!doc": "Returns a Location object, which contains information about the URL of the document and provides methods for changing that URL."
+ },
+ "lastModified": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.lastModified",
+ "!doc": "Returns a string containing the date and time on which the current document was last modified."
+ },
+ "head": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.head",
+ "!doc": "Returns the <head> element of the current document. If there are more than one <head> elements, the first one is returned."
+ },
+ "body": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.body",
+ "!doc": "Returns the <body> or <frameset> node of the current document."
+ },
+ "cookie": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.cookie",
+ "!doc": "Get and set the cookies associated with the current document."
+ },
+ "URL": "string",
+ "domain": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.domain",
+ "!doc": "Gets/sets the domain portion of the origin of the current document, as used by the same origin policy."
+ },
+ "referrer": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.referrer",
+ "!doc": "Returns the URI of the page that linked to this page."
+ },
+ "title": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.title",
+ "!doc": "Gets or sets the title of the document."
+ },
+ "defaultView": {
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.defaultView",
+ "!doc": "In browsers returns the window object associated with the document or null if none available."
+ },
+ "documentURI": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.documentURI",
+ "!doc": "Returns the document location as string. It is read-only per DOM4 specification."
+ },
+ "xmlStandalone": "bool",
+ "xmlVersion": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.xmlVersion",
+ "!doc": "Returns the version number as specified in the XML declaration (e.g., <?xml version=\"1.0\"?>) or \"1.0\" if the declaration is absent."
+ },
+ "xmlEncoding": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Document.xmlEncoding",
+ "!doc": "Returns the encoding as determined by the XML declaration. Should be null if unspecified or unknown."
+ },
+ "inputEncoding": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.inputEncoding",
+ "!doc": "Returns a string representing the encoding under which the document was parsed (e.g. ISO-8859-1)."
+ },
+ "documentElement": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.documentElement",
+ "!doc": "Read-only"
+ },
+ "implementation": {
+ "hasFeature": "fn(feature: string, version: number) -> bool",
+ "createDocumentType": {
+ "!type": "fn(qualifiedName: string, publicId: string, systemId: string) -> +Node",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/DOMImplementation.createDocumentType",
+ "!doc": "Returns a DocumentType object which can either be used with DOMImplementation.createDocument upon document creation or they can be put into the document via Node.insertBefore() or Node.replaceChild(): http://www.w3.org/TR/DOM-Level-3-Cor...l#ID-B63ED1A31 (less ideal due to features not likely being as accessible: http://www.w3.org/TR/DOM-Level-3-Cor...createDocument ). In any case, entity declarations and notations will not be available: http://www.w3.org/TR/DOM-Level-3-Cor...-createDocType "
+ },
+ "createHTMLDocument": {
+ "!type": "fn(title: string) -> +Document",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/DOMImplementation.createHTMLDocument",
+ "!doc": "This method (available from document.implementation) creates a new HTML document."
+ },
+ "createDocument": {
+ "!type": "fn(namespaceURI: string, qualifiedName: string, type: +Node) -> +Document",
+ "!url": "https://developer.mozilla.org/en-US/docs/DOM/DOMImplementation.createHTMLDocument",
+ "!doc": "This method creates a new HTML document."
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.implementation",
+ "!doc": "Returns a DOMImplementation object associated with the current document."
+ },
+ "doctype": {
+ "!type": "+Node",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.doctype",
+ "!doc": "Returns the Document Type Declaration (DTD) associated with current document. The returned object implements the DocumentType interface. Use DOMImplementation.createDocumentType() to create a DocumentType."
+ },
+ "open": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.open",
+ "!doc": "The document.open() method opens a document for writing."
+ },
+ "close": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.close",
+ "!doc": "The document.close() method finishes writing to a document, opened with document.open()."
+ },
+ "write": {
+ "!type": "fn(html: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.write",
+ "!doc": "Writes a string of text to a document stream opened by document.open()."
+ },
+ "writeln": {
+ "!type": "fn(html: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.writeln",
+ "!doc": "Writes a string of text followed by a newline character to a document."
+ },
+ "clear": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.clear",
+ "!doc": "In recent versions of Mozilla-based applications as well as in Internet Explorer and Netscape 4 this method does nothing."
+ },
+ "hasFocus": {
+ "!type": "fn() -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.hasFocus",
+ "!doc": "Returns a Boolean value indicating whether the document or any element inside the document has focus. This method can be used to determine whether the active element in a document has focus."
+ },
+ "createElement": {
+ "!type": "fn(tagName: string) -> +Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.createElement",
+ "!doc": "Creates the specified element."
+ },
+ "createElementNS": {
+ "!type": "fn(ns: string, tagName: string) -> +Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.createElementNS",
+ "!doc": "Creates an element with the specified namespace URI and qualified name."
+ },
+ "createDocumentFragment": {
+ "!type": "fn() -> +DocumentFragment",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.createDocumentFragment",
+ "!doc": "Creates a new empty DocumentFragment."
+ },
+ "createTextNode": {
+ "!type": "fn(content: string) -> +Text",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.createTextNode",
+ "!doc": "Creates a new Text node."
+ },
+ "createComment": {
+ "!type": "fn(content: string) -> +Node",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.createComment",
+ "!doc": "Creates a new comment node, and returns it."
+ },
+ "createCDATASection": {
+ "!type": "fn(content: string) -> +Node",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.createCDATASection",
+ "!doc": "Creates a new CDATA section node, and returns it. "
+ },
+ "createProcessingInstruction": {
+ "!type": "fn(content: string) -> +Node",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.createProcessingInstruction",
+ "!doc": "Creates a new processing instruction node, and returns it."
+ },
+ "createAttribute": {
+ "!type": "fn(name: string) -> +Attr",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.createAttribute",
+ "!doc": "Creates a new attribute node, and returns it."
+ },
+ "createAttributeNS": {
+ "!type": "fn(ns: string, name: string) -> +Attr",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Attr",
+ "!doc": "This type represents a DOM element's attribute as an object. In most DOM methods, you will probably directly retrieve the attribute as a string (e.g., Element.getAttribute(), but certain functions (e.g., Element.getAttributeNode()) or means of iterating give Attr types."
+ },
+ "importNode": {
+ "!type": "fn(node: +Node, deep: bool) -> +Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.importNode",
+ "!doc": "Creates a copy of a node from an external document that can be inserted into the current document."
+ },
+ "getElementById": {
+ "!type": "fn(id: string) -> +Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.getElementById",
+ "!doc": "Returns a reference to the element by its ID."
+ },
+ "getElementsByTagName": {
+ "!type": "fn(tagName: string) -> +NodeList",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.getElementsByTagName",
+ "!doc": "Returns a NodeList of elements with the given tag name. The complete document is searched, including the root node. The returned NodeList is live, meaning that it updates itself automatically to stay in sync with the DOM tree without having to call document.getElementsByTagName again."
+ },
+ "getElementsByTagNameNS": {
+ "!type": "fn(ns: string, tagName: string) -> +NodeList",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.getElementsByTagNameNS",
+ "!doc": "Returns a list of elements with the given tag name belonging to the given namespace. The complete document is searched, including the root node."
+ },
+ "createEvent": {
+ "!type": "fn(type: string) -> +Event",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.createEvent",
+ "!doc": "Creates an event of the type specified. The returned object should be first initialized and can then be passed to element.dispatchEvent."
+ },
+ "createRange": {
+ "!type": "fn() -> +Range",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.createRange",
+ "!doc": "Returns a new Range object."
+ },
+ "evaluate": {
+ "!type": "fn(expr: ?) -> +XPathResult",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.evaluate",
+ "!doc": "Returns an XPathResult based on an XPath expression and other given parameters."
+ },
+ "execCommand": {
+ "!type": "fn(cmd: string)",
+ "!url": "https://developer.mozilla.org/en-US/docs/Rich-Text_Editing_in_Mozilla#Executing_Commands",
+ "!doc": "Run command to manipulate the contents of an editable region."
+ },
+ "queryCommandEnabled": {
+ "!type": "fn(cmd: string) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document",
+ "!doc": "Returns true if the Midas command can be executed on the current range."
+ },
+ "queryCommandIndeterm": {
+ "!type": "fn(cmd: string) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document",
+ "!doc": "Returns true if the Midas command is in a indeterminate state on the current range."
+ },
+ "queryCommandState": {
+ "!type": "fn(cmd: string) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document",
+ "!doc": "Returns true if the Midas command has been executed on the current range."
+ },
+ "queryCommandSupported": {
+ "!type": "fn(cmd: string) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.queryCommandSupported",
+ "!doc": "Reports whether or not the specified editor query command is supported by the browser."
+ },
+ "queryCommandValue": {
+ "!type": "fn(cmd: string) -> string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document",
+ "!doc": "Returns the current value of the current range for Midas command."
+ },
+ "getElementsByName": {
+ "!type": "fn(name: string) -> +HTMLCollection",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.getElementsByName",
+ "!doc": "Returns a list of elements with a given name in the HTML document."
+ },
+ "elementFromPoint": {
+ "!type": "fn(x: number, y: number) -> +Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.elementFromPoint",
+ "!doc": "Returns the element from the document whose elementFromPoint method is being called which is the topmost element which lies under the given point. To get an element, specify the point via coordinates, in CSS pixels, relative to the upper-left-most point in the window or frame containing the document."
+ },
+ "getSelection": {
+ "!type": "fn() -> +Selection",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.getSelection",
+ "!doc": "The DOM getSelection() method is available on the Window and Document interfaces."
+ },
+ "adoptNode": {
+ "!type": "fn(node: +Node) -> +Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.adoptNode",
+ "!doc": "Adopts a node from an external document. The node and its subtree is removed from the document it's in (if any), and its ownerDocument is changed to the current document. The node can then be inserted into the current document."
+ },
+ "createTreeWalker": {
+ "!type": "fn(root: +Node, mask: number) -> ?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.createTreeWalker",
+ "!doc": "Returns a new TreeWalker object."
+ },
+ "createExpression": {
+ "!type": "fn(text: string) -> ?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.createExpression",
+ "!doc": "This method compiles an XPathExpression which can then be used for (repeated) evaluations."
+ },
+ "createNSResolver": {
+ "!type": "fn(node: +Node)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.createNSResolver",
+ "!doc": "Creates an XPathNSResolver which resolves namespaces with respect to the definitions in scope for a specified node."
+ },
+ "scripts": {
+ "!type": "+HTMLCollection",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Document.scripts",
+ "!doc": "Returns a list of the <script> elements in the document. The returned object is an HTMLCollection."
+ },
+ "plugins": {
+ "!type": "+HTMLCollection",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.plugins",
+ "!doc": "Returns an HTMLCollection object containing one or more HTMLEmbedElements or null which represent the <embed> elements in the current document."
+ },
+ "embeds": {
+ "!type": "+HTMLCollection",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.embeds",
+ "!doc": "Returns a list of the embedded OBJECTS within the current document."
+ },
+ "anchors": {
+ "!type": "+HTMLCollection",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.anchors",
+ "!doc": "Returns a list of all of the anchors in the document."
+ },
+ "links": {
+ "!type": "+HTMLCollection",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.links",
+ "!doc": "The links property returns a collection of all AREA elements and anchor elements in a document with a value for the href attribute. "
+ },
+ "forms": {
+ "!type": "+HTMLCollection",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.forms",
+ "!doc": "Returns a collection (an HTMLCollection) of the form elements within the current document."
+ },
+ "styleSheets": {
+ "!type": "+HTMLCollection",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.styleSheets",
+ "!doc": "Returns a list of stylesheet objects for stylesheets explicitly linked into or embedded in a document."
+ },
+ "querySelector": "Element.prototype.querySelector",
+ "querySelectorAll": "Element.prototype.querySelectorAll"
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document",
+ "!doc": "Each web page loaded in the browser has its own document object. This object serves as an entry point to the web page's content (the DOM tree, including elements such as <body> and <table>) and provides functionality global to the document (such as obtaining the page's URL and creating new elements in the document)."
+ },
+ "document": {
+ "!type": "+Document",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document",
+ "!doc": "Each web page loaded in the browser has its own document object. This object serves as an entry point to the web page's content (the DOM tree, including elements such as <body> and <table>) and provides functionality global to the document (such as obtaining the page's URL and creating new elements in the document)."
+ },
+ "XMLDocument": {
+ "!type": "fn()",
+ "prototype": "Document.prototype",
+ "!url": "https://developer.mozilla.org/en/docs/Parsing_and_serializing_XML",
+ "!doc": "The Web platform provides the following objects for parsing and serializing XML:"
+ },
+ "Attr": {
+ "!type": "fn()",
+ "prototype": {
+ "isId": {
+ "!type": "bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Attr",
+ "!doc": "This type represents a DOM element's attribute as an object. In most DOM methods, you will probably directly retrieve the attribute as a string (e.g., Element.getAttribute(), but certain functions (e.g., Element.getAttributeNode()) or means of iterating give Attr types."
+ },
+ "name": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Attr",
+ "!doc": "This type represents a DOM element's attribute as an object. In most DOM methods, you will probably directly retrieve the attribute as a string (e.g., Element.getAttribute(), but certain functions (e.g., Element.getAttributeNode()) or means of iterating give Attr types."
+ },
+ "value": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Attr",
+ "!doc": "This type represents a DOM element's attribute as an object. In most DOM methods, you will probably directly retrieve the attribute as a string (e.g., Element.getAttribute(), but certain functions (e.g., Element.getAttributeNode()) or means of iterating give Attr types."
+ }
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Attr",
+ "!doc": "This type represents a DOM element's attribute as an object. In most DOM methods, you will probably directly retrieve the attribute as a string (e.g., Element.getAttribute(), but certain functions (e.g., Element.getAttributeNode()) or means of iterating give Attr types."
+ },
+ "NodeList": {
+ "!type": "fn()",
+ "prototype": {
+ "length": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.length",
+ "!doc": "Returns the number of items in a NodeList."
+ },
+ "item": {
+ "!type": "fn(i: number) -> +Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/NodeList.item",
+ "!doc": "Returns a node from a NodeList by index."
+ },
+ "<i>": "+Element"
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/NodeList",
+ "!doc": "NodeList objects are collections of nodes returned by getElementsByTagName, getElementsByTagNameNS, Node.childNodes, querySelectorAll, getElementsByClassName, etc."
+ },
+ "HTMLCollection": {
+ "!type": "fn()",
+ "prototype": {
+ "length": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/HTMLCollection",
+ "!doc": "The number of items in the collection."
+ },
+ "item": {
+ "!type": "fn(i: number) -> +Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/HTMLCollection",
+ "!doc": "Returns the specific node at the given zero-based index into the list. Returns null if the index is out of range."
+ },
+ "namedItem": {
+ "!type": "fn(name: string) -> +Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/HTMLCollection",
+ "!doc": "Returns the specific node whose ID or, as a fallback, name matches the string specified by name. Matching by name is only done as a last resort, only in HTML, and only if the referenced element supports the name attribute. Returns null if no node exists by the given name."
+ },
+ "<i>": "+Element"
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/HTMLCollection",
+ "!doc": "HTMLCollection is an interface representing a generic collection of elements (in document order) and offers methods and properties for traversing the list."
+ },
+ "NamedNodeMap": {
+ "!type": "fn()",
+ "prototype": {
+ "length": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/NamedNodeMap",
+ "!doc": "The number of items in the map."
+ },
+ "getNamedItem": {
+ "!type": "fn(name: string) -> +Node",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/NamedNodeMap",
+ "!doc": "Gets a node by name."
+ },
+ "setNamedItem": {
+ "!type": "fn(node: +Node) -> +Node",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/NamedNodeMap",
+ "!doc": "Adds (or replaces) a node by its nodeName."
+ },
+ "removeNamedItem": {
+ "!type": "fn(name: string) -> +Node",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/NamedNodeMap",
+ "!doc": "Removes a node (or if an attribute, may reveal a default if present)."
+ },
+ "item": {
+ "!type": "fn(i: number) -> +Node",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/NamedNodeMap",
+ "!doc": "Returns the item at the given index (or null if the index is higher or equal to the number of nodes)."
+ },
+ "getNamedItemNS": {
+ "!type": "fn(ns: string, name: string) -> +Node",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/NamedNodeMap",
+ "!doc": "Gets a node by namespace and localName."
+ },
+ "setNamedItemNS": {
+ "!type": "fn(node: +Node) -> +Node",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/NamedNodeMap",
+ "!doc": "Adds (or replaces) a node by its localName and namespaceURI."
+ },
+ "removeNamedItemNS": {
+ "!type": "fn(ns: string, name: string) -> +Node",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/NamedNodeMap",
+ "!doc": "Removes a node (or if an attribute, may reveal a default if present)."
+ },
+ "<i>": "+Node"
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/NamedNodeMap",
+ "!doc": "A collection of nodes returned by Element.attributes (also potentially for DocumentType.entities, DocumentType.notations). NamedNodeMaps are not in any particular order (unlike NodeList), although they may be accessed by an index as in an array (they may also be accessed with the item() method). A NamedNodeMap object are live and will thus be auto-updated if changes are made to their contents internally or elsewhere."
+ },
+ "DocumentFragment": {
+ "!type": "fn()",
+ "prototype": {
+ "!proto": "Node.prototype"
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.createDocumentFragment",
+ "!doc": "Creates a new empty DocumentFragment."
+ },
+ "DOMTokenList": {
+ "!type": "fn()",
+ "prototype": {
+ "length": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/DOMTokenList",
+ "!doc": "The amount of items in the list."
+ },
+ "item": {
+ "!type": "fn(i: number) -> string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/DOMTokenList",
+ "!doc": "Returns an item in the list by its index."
+ },
+ "contains": {
+ "!type": "fn(token: string) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/DOMTokenList",
+ "!doc": "Return true if the underlying string contains token, otherwise false."
+ },
+ "add": {
+ "!type": "fn(token: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/DOMTokenList",
+ "!doc": "Adds token to the underlying string."
+ },
+ "remove": {
+ "!type": "fn(token: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/DOMTokenList",
+ "!doc": "Remove token from the underlying string."
+ },
+ "toggle": {
+ "!type": "fn(token: string) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/DOMTokenList",
+ "!doc": "Removes token from string and returns false. If token doesn't exist it's added and the function returns true."
+ },
+ "<i>": "string"
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/DOMTokenList",
+ "!doc": "This type represents a set of space-separated tokens. Commonly returned by HTMLElement.classList, HTMLLinkElement.relList, HTMLAnchorElement.relList or HTMLAreaElement.relList. It is indexed beginning with 0 as with JavaScript arrays. DOMTokenList is always case-sensitive."
+ },
+ "XPathResult": {
+ "!type": "fn()",
+ "prototype": {
+ "boolValue": "bool",
+ "invalidIteratorState": {
+ "!type": "bool",
+ "!url": "https://developer.mozilla.org/en/docs/Introduction_to_using_XPath_in_JavaScript",
+ "!doc": "This document describes the interface for using XPath in JavaScript internally, in extensions, and from websites. Mozilla implements a fair amount of the DOM 3 XPath. Which means that XPath expressions can be run against both HTML and XML documents."
+ },
+ "numberValue": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/XPathResult",
+ "!doc": "Refer to nsIDOMXPathResult for more detail."
+ },
+ "resultType": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.evaluate",
+ "!doc": "Returns an XPathResult based on an XPath expression and other given parameters."
+ },
+ "singleNodeValue": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/Introduction_to_using_XPath_in_JavaScript",
+ "!doc": "This document describes the interface for using XPath in JavaScript internally, in extensions, and from websites. Mozilla implements a fair amount of the DOM 3 XPath. Which means that XPath expressions can be run against both HTML and XML documents."
+ },
+ "snapshotLength": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/XPathResult",
+ "!doc": "Refer to nsIDOMXPathResult for more detail."
+ },
+ "stringValue": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/Introduction_to_using_XPath_in_JavaScript",
+ "!doc": "This document describes the interface for using XPath in JavaScript internally, in extensions, and from websites. Mozilla implements a fair amount of the DOM 3 XPath. Which means that XPath expressions can be run against both HTML and XML documents."
+ },
+ "iterateNext": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/Introduction_to_using_XPath_in_JavaScript",
+ "!doc": "This document describes the interface for using XPath in JavaScript internally, in extensions, and from websites. Mozilla implements a fair amount of the DOM 3 XPath. Which means that XPath expressions can be run against both HTML and XML documents."
+ },
+ "snapshotItem": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en-US/docs/XPathResult#snapshotItem()"
+ },
+ "ANY_TYPE": "number",
+ "NUMBER_TYPE": "number",
+ "STRING_TYPE": "number",
+ "BOOL_TYPE": "number",
+ "UNORDERED_NODE_ITERATOR_TYPE": "number",
+ "ORDERED_NODE_ITERATOR_TYPE": "number",
+ "UNORDERED_NODE_SNAPSHOT_TYPE": "number",
+ "ORDERED_NODE_SNAPSHOT_TYPE": "number",
+ "ANY_UNORDERED_NODE_TYPE": "number",
+ "FIRST_ORDERED_NODE_TYPE": "number"
+ },
+ "!url": "https://developer.mozilla.org/en/docs/XPathResult",
+ "!doc": "Refer to nsIDOMXPathResult for more detail."
+ },
+ "ClientRect": {
+ "!type": "fn()",
+ "prototype": {
+ "top": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.getClientRects",
+ "!doc": "Top of the box, in pixels, relative to the viewport."
+ },
+ "left": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.getClientRects",
+ "!doc": "Left of the box, in pixels, relative to the viewport."
+ },
+ "bottom": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.getClientRects",
+ "!doc": "Bottom of the box, in pixels, relative to the viewport."
+ },
+ "right": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.getClientRects",
+ "!doc": "Right of the box, in pixels, relative to the viewport."
+ }
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.getClientRects",
+ "!doc": "Returns a collection of rectangles that indicate the bounding rectangles for each box in a client."
+ },
+ "Event": {
+ "!type": "fn()",
+ "prototype": {
+ "stopPropagation": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.stopPropagation",
+ "!doc": "Prevents further propagation of the current event."
+ },
+ "preventDefault": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.preventDefault",
+ "!doc": "Cancels the event if it is cancelable, without stopping further propagation of the event."
+ },
+ "initEvent": {
+ "!type": "fn(type: string, bubbles: bool, cancelable: bool)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.initEvent",
+ "!doc": "The initEvent method is used to initialize the value of an event created using document.createEvent."
+ },
+ "stopImmediatePropagation": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.stopImmediatePropagation",
+ "!doc": "Prevents other listeners of the same event to be called."
+ },
+ "NONE": "number",
+ "CAPTURING_PHASE": "number",
+ "AT_TARGET": "number",
+ "BUBBLING_PHASE": "number",
+ "MOUSEDOWN": "number",
+ "MOUSEUP": "number",
+ "MOUSEOVER": "number",
+ "MOUSEOUT": "number",
+ "MOUSEMOVE": "number",
+ "MOUSEDRAG": "number",
+ "CLICK": "number",
+ "DBLCLICK": "number",
+ "KEYDOWN": "number",
+ "KEYUP": "number",
+ "KEYPRESS": "number",
+ "DRAGDROP": "number",
+ "FOCUS": "number",
+ "BLUR": "number",
+ "SELECT": "number",
+ "CHANGE": "number",
+ "target": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/EventTarget",
+ "!doc": "An EventTarget is a DOM interface implemented by objects that can receive DOM events and have listeners for them. The most common EventTargets are DOM elements, although other objects can be EventTargets too, for example document, window, XMLHttpRequest, and others."
+ },
+ "relatedTarget": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.relatedTarget",
+ "!doc": "Identifies a secondary target for the event."
+ },
+ "pageX": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.pageX",
+ "!doc": "Returns the horizontal coordinate of the event relative to whole document."
+ },
+ "pageY": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.pageY",
+ "!doc": "Returns the vertical coordinate of the event relative to the whole document."
+ },
+ "clientX": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.clientX",
+ "!doc": "Returns the horizontal coordinate within the application's client area at which the event occurred (as opposed to the coordinates within the page). For example, clicking in the top-left corner of the client area will always result in a mouse event with a clientX value of 0, regardless of whether the page is scrolled horizontally."
+ },
+ "clientY": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.clientY",
+ "!doc": "Returns the vertical coordinate within the application's client area at which the event occurred (as opposed to the coordinates within the page). For example, clicking in the top-left corner of the client area will always result in a mouse event with a clientY value of 0, regardless of whether the page is scrolled vertically."
+ },
+ "keyCode": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.keyCode",
+ "!doc": "Returns the Unicode value of a non-character key in a keypress event or any key in any other type of keyboard event."
+ },
+ "charCode": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.charCode",
+ "!doc": "Returns the Unicode value of a character key pressed during a keypress event."
+ },
+ "which": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.which",
+ "!doc": "Returns the numeric keyCode of the key pressed, or the character code (charCode) for an alphanumeric key pressed."
+ },
+ "button": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.button",
+ "!doc": "Indicates which mouse button caused the event."
+ },
+ "shiftKey": {
+ "!type": "bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.shiftKey",
+ "!doc": "Indicates whether the SHIFT key was pressed when the event fired."
+ },
+ "ctrlKey": {
+ "!type": "bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.ctrlKey",
+ "!doc": "Indicates whether the CTRL key was pressed when the event fired."
+ },
+ "altKey": {
+ "!type": "bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.altKey",
+ "!doc": "Indicates whether the ALT key was pressed when the event fired."
+ },
+ "metaKey": {
+ "!type": "bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.metaKey",
+ "!doc": "Indicates whether the META key was pressed when the event fired."
+ },
+ "returnValue": {
+ "!type": "bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.onbeforeunload",
+ "!doc": "An event that fires when a window is about to unload its resources. The document is still visible and the event is still cancelable."
+ },
+ "cancelBubble": {
+ "!type": "bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.cancelBubble",
+ "!doc": "bool is the boolean value of true or false."
+ },
+ "dataTransfer": {
+ "dropEffect": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DragDrop/DataTransfer",
+ "!doc": "The actual effect that will be used, and should always be one of the possible values of effectAllowed."
+ },
+ "effectAllowed": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DragDrop/Drag_Operations",
+ "!doc": "Specifies the effects that are allowed for this drag."
+ },
+ "files": {
+ "!type": "+FileList",
+ "!url": "https://developer.mozilla.org/en/docs/DragDrop/DataTransfer",
+ "!doc": "Contains a list of all the local files available on the data transfer."
+ },
+ "types": {
+ "!type": "[string]",
+ "!url": "https://developer.mozilla.org/en-US/docs/DragDrop/DataTransfer",
+ "!doc": "Holds a list of the format types of the data that is stored for the first item, in the same order the data was added. An empty list will be returned if no data was added."
+ },
+ "addElement": {
+ "!type": "fn(element: +Element)",
+ "!url": "https://developer.mozilla.org/en/docs/DragDrop/DataTransfer",
+ "!doc": "Set the drag source."
+ },
+ "clearData": {
+ "!type": "fn(type?: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DragDrop/Drag_Operations",
+ "!doc": "Remove the data associated with a given type."
+ },
+ "getData": {
+ "!type": "fn(type: string) -> string",
+ "!url": "https://developer.mozilla.org/en/docs/DragDrop/Drag_Operations",
+ "!doc": "Retrieves the data for a given type, or an empty string if data for that type does not exist or the data transfer contains no data."
+ },
+ "setData": {
+ "!type": "fn(type: string, data: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DragDrop/Drag_Operations",
+ "!doc": "Set the data for a given type."
+ },
+ "setDragImage": {
+ "!type": "fn(image: +Element)",
+ "!url": "https://developer.mozilla.org/en/docs/DragDrop/Drag_Operations",
+ "!doc": "Set the image to be used for dragging if a custom one is desired."
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DragDrop/DataTransfer",
+ "!doc": "This object is available from the dataTransfer property of all drag events. It cannot be created separately."
+ }
+ },
+ "!url": "https://developer.mozilla.org/en-US/docs/DOM/event",
+ "!doc": "The DOM Event interface is accessible from within the handler function, via the event object passed as the first argument."
+ },
+ "TouchEvent": {
+ "!type": "fn()",
+ "prototype": "Event.prototype",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Touch_events",
+ "!doc": "In order to provide quality support for touch-based user interfaces, touch events offer the ability to interpret finger activity on touch screens or trackpads."
+ },
+ "WheelEvent": {
+ "!type": "fn()",
+ "prototype": "Event.prototype",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/WheelEvent",
+ "!doc": "The DOM WheelEvent represents events that occur due to the user moving a mouse wheel or similar input device."
+ },
+ "MouseEvent": {
+ "!type": "fn()",
+ "prototype": "Event.prototype",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/MouseEvent",
+ "!doc": "The DOM MouseEvent represents events that occur due to the user interacting with a pointing device (such as a mouse). It's represented by the nsINSDOMMouseEvent interface, which extends the nsIDOMMouseEvent interface."
+ },
+ "KeyboardEvent": {
+ "!type": "fn()",
+ "prototype": "Event.prototype",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/KeyboardEvent",
+ "!doc": "KeyboardEvent objects describe a user interaction with the keyboard. Each event describes a key; the event type (keydown, keypress, or keyup) identifies what kind of activity was performed."
+ },
+ "HashChangeEvent": {
+ "!type": "fn()",
+ "prototype": "Event.prototype",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.onhashchange",
+ "!doc": "The hashchange event fires when a window's hash changes."
+ },
+ "ErrorEvent": {
+ "!type": "fn()",
+ "prototype": "Event.prototype",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/DOM_event_reference/error",
+ "!doc": "The error event is fired whenever a resource fails to load."
+ },
+ "CustomEvent": {
+ "!type": "fn()",
+ "prototype": "Event.prototype",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Event/CustomEvent",
+ "!doc": "The DOM CustomEvent are events initialized by an application for any purpose."
+ },
+ "BeforeLoadEvent": {
+ "!type": "fn()",
+ "prototype": "Event.prototype",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window",
+ "!doc": "This section provides a brief reference for all of the methods, properties, and events available through the DOM window object. The window object implements the Window interface, which in turn inherits from the AbstractView interface. Some additional global functions, namespaces objects, and constructors, not typically associated with the window, but available on it, are listed in the JavaScript Reference."
+ },
+ "WebSocket": {
+ "!type": "fn(url: string)",
+ "prototype": {
+ "close": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/WebSockets/WebSockets_reference/CloseEvent",
+ "!doc": "A CloseEvent is sent to clients using WebSockets when the connection is closed. This is delivered to the listener indicated by the WebSocket object's onclose attribute."
+ },
+ "send": {
+ "!type": "fn(data: string)",
+ "!url": "https://developer.mozilla.org/en/docs/WebSockets/WebSockets_reference/WebSocket",
+ "!doc": "The WebSocket object provides the API for creating and managing a WebSocket connection to a server, as well as for sending and receiving data on the connection."
+ },
+ "binaryType": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/WebSockets/WebSockets_reference/WebSocket",
+ "!doc": "The WebSocket object provides the API for creating and managing a WebSocket connection to a server, as well as for sending and receiving data on the connection."
+ },
+ "bufferedAmount": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/WebSockets/Writing_WebSocket_client_applications",
+ "!doc": "WebSockets is a technology that makes it possible to open an interactive communication session between the user's browser and a server. Using a WebSocket connection, Web applications can perform real-time communication instead of having to poll for changes back and forth."
+ },
+ "extensions": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/WebSockets/WebSockets_reference/WebSocket",
+ "!doc": "The WebSocket object provides the API for creating and managing a WebSocket connection to a server, as well as for sending and receiving data on the connection."
+ },
+ "onclose": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/WebSockets/WebSockets_reference/CloseEvent",
+ "!doc": "A CloseEvent is sent to clients using WebSockets when the connection is closed. This is delivered to the listener indicated by the WebSocket object's onclose attribute."
+ },
+ "onerror": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/WebSockets/Writing_WebSocket_client_applications",
+ "!doc": "WebSockets is a technology that makes it possible to open an interactive communication session between the user's browser and a server. Using a WebSocket connection, Web applications can perform real-time communication instead of having to poll for changes back and forth."
+ },
+ "onmessage": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/WebSockets/WebSockets_reference/WebSocket",
+ "!doc": "The WebSocket object provides the API for creating and managing a WebSocket connection to a server, as well as for sending and receiving data on the connection."
+ },
+ "onopen": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/WebSockets/WebSockets_reference/WebSocket",
+ "!doc": "The WebSocket object provides the API for creating and managing a WebSocket connection to a server, as well as for sending and receiving data on the connection."
+ },
+ "protocol": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/WebSockets",
+ "!doc": "WebSockets is an advanced technology that makes it possible to open an interactive communication session between the user's browser and a server. With this API, you can send messages to a server and receive event-driven responses without having to poll the server for a reply."
+ },
+ "url": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/WebSockets/Writing_WebSocket_client_applications",
+ "!doc": "WebSockets is a technology that makes it possible to open an interactive communication session between the user's browser and a server. Using a WebSocket connection, Web applications can perform real-time communication instead of having to poll for changes back and forth."
+ },
+ "CONNECTING": "number",
+ "OPEN": "number",
+ "CLOSING": "number",
+ "CLOSED": "number"
+ },
+ "!url": "https://developer.mozilla.org/en/docs/WebSockets",
+ "!doc": "WebSockets is an advanced technology that makes it possible to open an interactive communication session between the user's browser and a server. With this API, you can send messages to a server and receive event-driven responses without having to poll the server for a reply."
+ },
+ "Worker": {
+ "!type": "fn(scriptURL: string)",
+ "prototype": {
+ "postMessage": {
+ "!type": "fn(message: ?)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Worker",
+ "!doc": "Sends a message to the worker's inner scope. This accepts a single parameter, which is the data to send to the worker. The data may be any value or JavaScript object handled by the structured clone algorithm, which includes cyclical references."
+ },
+ "terminate": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Worker",
+ "!doc": "Immediately terminates the worker. This does not offer the worker an opportunity to finish its operations; it is simply stopped at once."
+ },
+ "onmessage": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Worker",
+ "!doc": "An event listener that is called whenever a MessageEvent with type message bubbles through the worker. The message is stored in the event's data member."
+ },
+ "onerror": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Worker",
+ "!doc": "An event listener that is called whenever an ErrorEvent with type error bubbles through the worker."
+ }
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Worker",
+ "!doc": "Workers are background tasks that can be easily created and can send messages back to their creators. Creating a worker is as simple as calling the Worker() constructor, specifying a script to be run in the worker thread."
+ },
+ "localStorage": {
+ "setItem": {
+ "!type": "fn(name: string, value: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Storage",
+ "!doc": "Store an item in storage."
+ },
+ "getItem": {
+ "!type": "fn(name: string) -> string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Storage",
+ "!doc": "Retrieve an item from storage."
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Storage",
+ "!doc": "The DOM Storage mechanism is a means through which string key/value pairs can be securely stored and later retrieved for use."
+ },
+ "sessionStorage": {
+ "setItem": {
+ "!type": "fn(name: string, value: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Storage",
+ "!doc": "Store an item in storage."
+ },
+ "getItem": {
+ "!type": "fn(name: string) -> string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Storage",
+ "!doc": "Retrieve an item from storage."
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Storage",
+ "!doc": "This is a global object (sessionStorage) that maintains a storage area that's available for the duration of the page session. A page session lasts for as long as the browser is open and survives over page reloads and restores. Opening a page in a new tab or window will cause a new session to be initiated."
+ },
+ "FileList": {
+ "!type": "fn()",
+ "prototype": {
+ "length": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileList",
+ "!doc": "A read-only value indicating the number of files in the list."
+ },
+ "item": {
+ "!type": "fn(i: number) -> +File",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileList",
+ "!doc": "Returns a File object representing the file at the specified index in the file list."
+ },
+ "<i>": "+File"
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileList",
+ "!doc": "An object of this type is returned by the files property of the HTML input element; this lets you access the list of files selected with the <input type=\"file\"> element. It's also used for a list of files dropped into web content when using the drag and drop API."
+ },
+ "File": {
+ "!type": "fn()",
+ "prototype": {
+ "!proto": "Blob.prototype",
+ "fileName": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/File.fileName",
+ "!doc": "Returns the name of the file. For security reasons the path is excluded from this property."
+ },
+ "fileSize": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/File.fileSize",
+ "!doc": "Returns the size of a file in bytes."
+ },
+ "lastModifiedDate": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/File.lastModifiedDate",
+ "!doc": "Returns the last modified date of the file. Files without a known last modified date use the current date instead."
+ },
+ "name": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/File.name",
+ "!doc": "Returns the name of the file. For security reasons, the path is excluded from this property."
+ }
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/File",
+ "!doc": "The File object provides information about -- and access to the contents of -- files. These are generally retrieved from a FileList object returned as a result of a user selecting files using the input element, or from a drag and drop operation's DataTransfer object."
+ },
+ "Blob": {
+ "!type": "fn(parts: [?], properties?: ?)",
+ "prototype": {
+ "size": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Blob",
+ "!doc": "The size, in bytes, of the data contained in the Blob object. Read only."
+ },
+ "type": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Blob",
+ "!doc": "An ASCII-encoded string, in all lower case, indicating the MIME type of the data contained in the Blob. If the type is unknown, this string is empty. Read only."
+ },
+ "slice": {
+ "!type": "fn(start: number, end?: number, type?: string) -> +Blob",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Blob",
+ "!doc": "Returns a new Blob object containing the data in the specified range of bytes of the source Blob."
+ }
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Blob",
+ "!doc": "A Blob object represents a file-like object of immutable, raw data. Blobs represent data that isn't necessarily in a JavaScript-native format. The File interface is based on Blob, inheriting blob functionality and expanding it to support files on the user's system."
+ },
+ "FileReader": {
+ "!type": "fn()",
+ "prototype": {
+ "abort": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
+ "!doc": "Aborts the read operation. Upon return, the readyState will be DONE."
+ },
+ "readAsArrayBuffer": {
+ "!type": "fn(blob: +Blob)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
+ "!doc": "Starts reading the contents of the specified Blob, producing an ArrayBuffer."
+ },
+ "readAsBinaryString": {
+ "!type": "fn(blob: +Blob)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
+ "!doc": "Starts reading the contents of the specified Blob, producing raw binary data."
+ },
+ "readAsDataURL": {
+ "!type": "fn(blob: +Blob)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
+ "!doc": "Starts reading the contents of the specified Blob, producing a data: url."
+ },
+ "readAsText": {
+ "!type": "fn(blob: +Blob, encoding?: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
+ "!doc": "Starts reading the contents of the specified Blob, producing a string."
+ },
+ "EMPTY": "number",
+ "LOADING": "number",
+ "DONE": "number",
+ "error": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
+ "!doc": "The error that occurred while reading the file. Read only."
+ },
+ "readyState": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
+ "!doc": "Indicates the state of the FileReader. This will be one of the State constants. Read only."
+ },
+ "result": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
+ "!doc": "The file's contents. This property is only valid after the read operation is complete, and the format of the data depends on which of the methods was used to initiate the read operation. Read only."
+ },
+ "onabort": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
+ "!doc": "Called when the read operation is aborted."
+ },
+ "onerror": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
+ "!doc": "Called when an error occurs."
+ },
+ "onload": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
+ "!doc": "Called when the read operation is successfully completed."
+ },
+ "onloadend": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
+ "!doc": "Called when the read is completed, whether successful or not. This is called after either onload or onerror."
+ },
+ "onloadstart": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
+ "!doc": "Called when reading the data is about to begin."
+ },
+ "onprogress": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
+ "!doc": "Called periodically while the data is being read."
+ }
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/FileReader",
+ "!doc": "The FileReader object lets web applications asynchronously read the contents of files (or raw data buffers) stored on the user's computer, using File or Blob objects to specify the file or data to read. File objects may be obtained from a FileList object returned as a result of a user selecting files using the <input> element, from a drag and drop operation's DataTransfer object, or from the mozGetAsFile() API on an HTMLCanvasElement."
+ },
+ "Range": {
+ "!type": "fn()",
+ "prototype": {
+ "collapsed": {
+ "!type": "bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.collapsed",
+ "!doc": "Returns a boolean indicating whether the range's start and end points are at the same position."
+ },
+ "commonAncestorContainer": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.commonAncestorContainer",
+ "!doc": "Returns the deepest Node that contains the startContainer and endContainer Nodes."
+ },
+ "endContainer": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.endContainer",
+ "!doc": "Returns the Node within which the Range ends."
+ },
+ "endOffset": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.endOffset",
+ "!doc": "Returns a number representing where in the endContainer the Range ends."
+ },
+ "startContainer": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.startContainer",
+ "!doc": "Returns the Node within which the Range starts."
+ },
+ "startOffset": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.startOffset",
+ "!doc": "Returns a number representing where in the startContainer the Range starts."
+ },
+ "setStart": {
+ "!type": "fn(node: +Element, offset: number)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.setStart",
+ "!doc": "Sets the start position of a Range."
+ },
+ "setEnd": {
+ "!type": "fn(node: +Element, offset: number)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.setEnd",
+ "!doc": "Sets the end position of a Range."
+ },
+ "setStartBefore": {
+ "!type": "fn(node: +Element)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.setStartBefore",
+ "!doc": "Sets the start position of a Range relative to another Node."
+ },
+ "setStartAfter": {
+ "!type": "fn(node: +Element)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.setStartAfter",
+ "!doc": "Sets the start position of a Range relative to a Node."
+ },
+ "setEndBefore": {
+ "!type": "fn(node: +Element)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.setEndBefore",
+ "!doc": "Sets the end position of a Range relative to another Node."
+ },
+ "setEndAfter": {
+ "!type": "fn(node: +Element)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.setEndAfter",
+ "!doc": "Sets the end position of a Range relative to another Node."
+ },
+ "selectNode": {
+ "!type": "fn(node: +Element)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.selectNode",
+ "!doc": "Sets the Range to contain the Node and its contents."
+ },
+ "selectNodeContents": {
+ "!type": "fn(node: +Element)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.selectNodeContents",
+ "!doc": "Sets the Range to contain the contents of a Node."
+ },
+ "collapse": {
+ "!type": "fn(toStart: bool)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.collapse",
+ "!doc": "Collapses the Range to one of its boundary points."
+ },
+ "cloneContents": {
+ "!type": "fn() -> +DocumentFragment",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.cloneContents",
+ "!doc": "Returns a DocumentFragment copying the Nodes of a Range."
+ },
+ "deleteContents": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.deleteContents",
+ "!doc": "Removes the contents of a Range from the Document."
+ },
+ "extractContents": {
+ "!type": "fn() -> +DocumentFragment",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.extractContents",
+ "!doc": "Moves contents of a Range from the document tree into a DocumentFragment."
+ },
+ "insertNode": {
+ "!type": "fn(node: +Element)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.insertNode",
+ "!doc": "Insert a node at the start of a Range."
+ },
+ "surroundContents": {
+ "!type": "fn(node: +Element)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.surroundContents",
+ "!doc": "Moves content of a Range into a new node, placing the new node at the start of the specified range."
+ },
+ "compareBoundaryPoints": {
+ "!type": "fn(how: number, other: +Range) -> number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.compareBoundaryPoints",
+ "!doc": "Compares the boundary points of two Ranges."
+ },
+ "cloneRange": {
+ "!type": "fn() -> +Range",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.cloneRange",
+ "!doc": "Returns a Range object with boundary points identical to the cloned Range."
+ },
+ "detach": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.detach",
+ "!doc": "Releases a Range from use to improve performance. This lets the browser choose to release resources associated with this Range. Subsequent attempts to use the detached range will result in a DOMException being thrown with an error code of INVALID_STATE_ERR."
+ },
+ "END_TO_END": "number",
+ "END_TO_START": "number",
+ "START_TO_END": "number",
+ "START_TO_START": "number"
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/range.detach",
+ "!doc": "Releases a Range from use to improve performance. This lets the browser choose to release resources associated with this Range. Subsequent attempts to use the detached range will result in a DOMException being thrown with an error code of INVALID_STATE_ERR."
+ },
+ "XMLHttpRequest": {
+ "!type": "fn()",
+ "prototype": {
+ "abort": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest",
+ "!doc": "Aborts the request if it has already been sent."
+ },
+ "getAllResponseHeaders": {
+ "!type": "fn() -> string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest",
+ "!doc": "Returns all the response headers as a string, or null if no response has been received. Note: For multipart requests, this returns the headers from the current part of the request, not from the original channel."
+ },
+ "getResponseHeader": {
+ "!type": "fn(header: string) -> string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest",
+ "!doc": "Returns the string containing the text of the specified header, or null if either the response has not yet been received or the header doesn't exist in the response."
+ },
+ "open": {
+ "!type": "fn(method: string, url: string, async?: bool, user?: string, password?: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest",
+ "!doc": "Initializes a request."
+ },
+ "overrideMimeType": {
+ "!type": "fn(type: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest",
+ "!doc": "Overrides the MIME type returned by the server."
+ },
+ "send": {
+ "!type": "fn(data?: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest",
+ "!doc": "Sends the request. If the request is asynchronous (which is the default), this method returns as soon as the request is sent. If the request is synchronous, this method doesn't return until the response has arrived."
+ },
+ "setRequestHeader": {
+ "!type": "fn(header: string, value: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest",
+ "!doc": "Sets the value of an HTTP request header.You must call setRequestHeader() after open(), but before send()."
+ },
+ "onreadystatechange": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest",
+ "!doc": "A JavaScript function object that is called whenever the readyState attribute changes."
+ },
+ "readyState": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest",
+ "!doc": "The state of the request. (0=unsent, 1=opened, 2=headers_received, 3=loading, 4=done)"
+ },
+ "response": {
+ "!type": "+Document",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest",
+ "!doc": "The response entity body according to responseType, as an ArrayBuffer, Blob, Document, JavaScript object (for \"json\"), or string. This is null if the request is not complete or was not successful."
+ },
+ "responseText": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest",
+ "!doc": "The response to the request as text, or null if the request was unsuccessful or has not yet been sent."
+ },
+ "responseType": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest",
+ "!doc": "Can be set to change the response type."
+ },
+ "responseXML": {
+ "!type": "+Document",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest",
+ "!doc": "The response to the request as a DOM Document object, or null if the request was unsuccessful, has not yet been sent, or cannot be parsed as XML or HTML."
+ },
+ "status": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest",
+ "!doc": "The status of the response to the request. This is the HTTP result code"
+ },
+ "statusText": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest",
+ "!doc": "The response string returned by the HTTP server. Unlike status, this includes the entire text of the response message (\"200 OK\", for example)."
+ },
+ "timeout": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest/Synchronous_and_Asynchronous_Requests",
+ "!doc": "The number of milliseconds a request can take before automatically being terminated. A value of 0 (which is the default) means there is no timeout."
+ },
+ "UNSENT": "number",
+ "OPENED": "number",
+ "HEADERS_RECEIVED": "number",
+ "LOADING": "number",
+ "DONE": "number"
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/XMLHttpRequest",
+ "!doc": "XMLHttpRequest is a JavaScript object that was designed by Microsoft and adopted by Mozilla, Apple, and Google. It's now being standardized in the W3C. It provides an easy way to retrieve data at a URL. Despite its name, XMLHttpRequest can be used to retrieve any type of data, not just XML, and it supports protocols other than HTTP (including file and ftp)."
+ },
+ "DOMParser": {
+ "!type": "fn()",
+ "prototype": {
+ "parseFromString": {
+ "!type": "fn(data: string, mime: string) -> +Document",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/DOMParser",
+ "!doc": "DOMParser can parse XML or HTML source stored in a string into a DOM Document. DOMParser is specified in DOM Parsing and Serialization."
+ }
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/DOMParser",
+ "!doc": "DOMParser can parse XML or HTML source stored in a string into a DOM Document. DOMParser is specified in DOM Parsing and Serialization."
+ },
+ "Selection": {
+ "!type": "fn()",
+ "prototype": {
+ "anchorNode": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/anchorNode",
+ "!doc": "Returns the node in which the selection begins."
+ },
+ "anchorOffset": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/anchorOffset",
+ "!doc": "Returns the number of characters that the selection's anchor is offset within the anchorNode."
+ },
+ "focusNode": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/focusNode",
+ "!doc": "Returns the node in which the selection ends."
+ },
+ "focusOffset": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/focusOffset",
+ "!doc": "Returns the number of characters that the selection's focus is offset within the focusNode. "
+ },
+ "isCollapsed": {
+ "!type": "bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/isCollapsed",
+ "!doc": "Returns a boolean indicating whether the selection's start and end points are at the same position."
+ },
+ "rangeCount": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/rangeCount",
+ "!doc": "Returns the number of ranges in the selection."
+ },
+ "getRangeAt": {
+ "!type": "fn(i: number) -> +Range",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/getRangeAt",
+ "!doc": "Returns a range object representing one of the ranges currently selected."
+ },
+ "collapse": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/collapse",
+ "!doc": "Collapses the current selection to a single point. The document is not modified. If the content is focused and editable, the caret will blink there."
+ },
+ "extend": {
+ "!type": "fn(node: +Element, offset: number)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/extend",
+ "!doc": "Moves the focus of the selection to a specified point. The anchor of the selection does not move. The selection will be from the anchor to the new focus regardless of direction."
+ },
+ "collapseToStart": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/collapseToStart",
+ "!doc": "Collapses the selection to the start of the first range in the selection. If the content of the selection is focused and editable, the caret will blink there."
+ },
+ "collapseToEnd": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/collapseToEnd",
+ "!doc": "Collapses the selection to the end of the last range in the selection. If the content the selection is in is focused and editable, the caret will blink there."
+ },
+ "selectAllChildren": {
+ "!type": "fn(node: +Element)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/selectAllChildren",
+ "!doc": "Adds all the children of the specified node to the selection. Previous selection is lost."
+ },
+ "addRange": {
+ "!type": "fn(range: +Range)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/addRange",
+ "!doc": "Adds a Range to a Selection."
+ },
+ "removeRange": {
+ "!type": "fn(range: +Range)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/removeRange",
+ "!doc": "Removes a range from the selection."
+ },
+ "removeAllRanges": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/removeAllRanges",
+ "!doc": "Removes all ranges from the selection, leaving the anchorNode and focusNode properties equal to null and leaving nothing selected. "
+ },
+ "deleteFromDocument": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/deleteFromDocument",
+ "!doc": "Deletes the actual text being represented by a selection object from the document's DOM."
+ },
+ "containsNode": {
+ "!type": "fn(node: +Element) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection/containsNode",
+ "!doc": "Indicates if the node is part of the selection."
+ }
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Selection",
+ "!doc": "Selection is the class of the object returned by window.getSelection() and other methods. It represents the text selection in the greater page, possibly spanning multiple elements, when the user drags over static text and other parts of the page. For information about text selection in an individual text editing element."
+ },
+ "console": {
+ "error": {
+ "!type": "fn(text: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/console.error",
+ "!doc": "Outputs an error message to the Web Console."
+ },
+ "info": {
+ "!type": "fn(text: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/console.info",
+ "!doc": "Outputs an informational message to the Web Console."
+ },
+ "log": {
+ "!type": "fn(text: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/console.log",
+ "!doc": "Outputs a message to the Web Console."
+ },
+ "warn": {
+ "!type": "fn(text: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/console.warn",
+ "!doc": "Outputs a warning message to the Web Console."
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/console",
+ "!doc": "The console object provides access to the browser's debugging console. The specifics of how it works vary from browser to browser, but there is a de facto set of features that are typically provided."
+ },
+ "top": {
+ "!type": "<top>",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.top",
+ "!doc": "Returns a reference to the topmost window in the window hierarchy."
+ },
+ "parent": {
+ "!type": "<top>",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.parent",
+ "!doc": "A reference to the parent of the current window or subframe."
+ },
+ "window": {
+ "!type": "<top>",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window",
+ "!doc": "This section provides a brief reference for all of the methods, properties, and events available through the DOM window object. The window object implements the Window interface, which in turn inherits from the AbstractView interface. Some additional global functions, namespaces objects, and constructors, not typically associated with the window, but available on it, are listed in the JavaScript Reference."
+ },
+ "opener": {
+ "!type": "<top>",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.opener",
+ "!doc": "Returns a reference to the window that opened this current window."
+ },
+ "self": {
+ "!type": "<top>",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.self",
+ "!doc": "Returns an object reference to the window object. "
+ },
+ "devicePixelRatio": "number",
+ "name": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/JavaScript/Reference/Global_Objects/Function/name",
+ "!doc": "The name of the function."
+ },
+ "closed": {
+ "!type": "bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.closed",
+ "!doc": "This property indicates whether the referenced window is closed or not."
+ },
+ "pageYOffset": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.scrollY",
+ "!doc": "Returns the number of pixels that the document has already been scrolled vertically."
+ },
+ "pageXOffset": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.scrollX",
+ "!doc": "Returns the number of pixels that the document has already been scrolled vertically."
+ },
+ "scrollY": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.scrollY",
+ "!doc": "Returns the number of pixels that the document has already been scrolled vertically."
+ },
+ "scrollX": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.scrollX",
+ "!doc": "Returns the number of pixels that the document has already been scrolled vertically."
+ },
+ "screenTop": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.screen.top",
+ "!doc": "Returns the distance in pixels from the top side of the current screen."
+ },
+ "screenLeft": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.screen.left",
+ "!doc": "Returns the distance in pixels from the left side of the main screen to the left side of the current screen."
+ },
+ "screenY": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.screenY",
+ "!doc": "Returns the vertical coordinate of the event within the screen as a whole."
+ },
+ "screenX": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/event.screenX",
+ "!doc": "Returns the horizontal coordinate of the event within the screen as a whole."
+ },
+ "innerWidth": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.innerWidth",
+ "!doc": "Width (in pixels) of the browser window viewport including, if rendered, the vertical scrollbar."
+ },
+ "innerHeight": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.innerHeight",
+ "!doc": "Height (in pixels) of the browser window viewport including, if rendered, the horizontal scrollbar."
+ },
+ "outerWidth": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.outerWidth",
+ "!doc": "window.outerWidth gets the width of the outside of the browser window. It represents the width of the whole browser window including sidebar (if expanded), window chrome and window resizing borders/handles."
+ },
+ "outerHeight": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.outerHeight",
+ "!doc": "window.outerHeight gets the height in pixels of the whole browser window."
+ },
+ "frameElement": {
+ "!type": "+Element",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.frameElement",
+ "!doc": "Returns the element (such as <iframe> or <object>) in which the window is embedded, or null if the window is top-level."
+ },
+ "crypto": {
+ "getRandomValues": {
+ "!type": "fn([number])",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.crypto.getRandomValues",
+ "!doc": "This methods lets you get cryptographically random values."
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.crypto.getRandomValues",
+ "!doc": "This methods lets you get cryptographically random values."
+ },
+ "navigator": {
+ "appName": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.navigator.appName",
+ "!doc": "Returns the name of the browser. The HTML5 specification also allows any browser to return \"Netscape\" here, for compatibility reasons."
+ },
+ "appVersion": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.navigator.appVersion",
+ "!doc": "Returns the version of the browser as a string. It may be either a plain version number, like \"5.0\", or a version number followed by more detailed information. The HTML5 specification also allows any browser to return \"4.0\" here, for compatibility reasons."
+ },
+ "language": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.navigator.language",
+ "!doc": "Returns a string representing the language version of the browser."
+ },
+ "platform": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.navigator.platform",
+ "!doc": "Returns a string representing the platform of the browser."
+ },
+ "plugins": {
+ "!type": "[?]",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.navigator.plugins",
+ "!doc": "Returns a PluginArray object, listing the plugins installed in the application."
+ },
+ "userAgent": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.navigator.userAgent",
+ "!doc": "Returns the user agent string for the current browser."
+ },
+ "vendor": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.navigator.vendor",
+ "!doc": "Returns the name of the browser vendor for the current browser."
+ },
+ "javaEnabled": {
+ "!type": "bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.navigator.javaEnabled",
+ "!doc": "This method indicates whether the current browser is Java-enabled or not."
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.navigator",
+ "!doc": "Returns a reference to the navigator object, which can be queried for information about the application running the script."
+ },
+ "history": {
+ "state": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Manipulating_the_browser_history",
+ "!doc": "The DOM window object provides access to the browser's history through the history object. It exposes useful methods and properties that let you move back and forth through the user's history, as well as -- starting with HTML5 -- manipulate the contents of the history stack."
+ },
+ "length": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Manipulating_the_browser_history",
+ "!doc": "The DOM window object provides access to the browser's history through the history object. It exposes useful methods and properties that let you move back and forth through the user's history, as well as -- starting with HTML5 -- manipulate the contents of the history stack."
+ },
+ "go": {
+ "!type": "fn(delta: number)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.history",
+ "!doc": "Returns a reference to the History object, which provides an interface for manipulating the browser session history (pages visited in the tab or frame that the current page is loaded in)."
+ },
+ "forward": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Manipulating_the_browser_history",
+ "!doc": "The DOM window object provides access to the browser's history through the history object. It exposes useful methods and properties that let you move back and forth through the user's history, as well as -- starting with HTML5 -- manipulate the contents of the history stack."
+ },
+ "back": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Manipulating_the_browser_history",
+ "!doc": "The DOM window object provides access to the browser's history through the history object. It exposes useful methods and properties that let you move back and forth through the user's history, as well as -- starting with HTML5 -- manipulate the contents of the history stack."
+ },
+ "pushState": {
+ "!type": "fn(data: ?, title: string, url?: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Manipulating_the_browser_history",
+ "!doc": "The DOM window object provides access to the browser's history through the history object. It exposes useful methods and properties that let you move back and forth through the user's history, as well as -- starting with HTML5 -- manipulate the contents of the history stack."
+ },
+ "replaceState": {
+ "!type": "fn(data: ?, title: string, url?: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Manipulating_the_browser_history",
+ "!doc": "The DOM window object provides access to the browser's history through the history object. It exposes useful methods and properties that let you move back and forth through the user's history, as well as -- starting with HTML5 -- manipulate the contents of the history stack."
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Manipulating_the_browser_history",
+ "!doc": "The DOM window object provides access to the browser's history through the history object. It exposes useful methods and properties that let you move back and forth through the user's history, as well as -- starting with HTML5 -- manipulate the contents of the history stack."
+ },
+ "screen": {
+ "availWidth": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.screen.availWidth",
+ "!doc": "Returns the amount of horizontal space in pixels available to the window."
+ },
+ "availHeight": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.screen.availHeight",
+ "!doc": "Returns the amount of vertical space available to the window on the screen."
+ },
+ "availTop": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.screen.availTop",
+ "!doc": "Specifies the y-coordinate of the first pixel that is not allocated to permanent or semipermanent user interface features."
+ },
+ "availLeft": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.screen.availLeft",
+ "!doc": "Returns the first available pixel available from the left side of the screen."
+ },
+ "pixelDepth": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.screen.pixelDepth",
+ "!doc": "Returns the bit depth of the screen."
+ },
+ "colorDepth": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.screen.colorDepth",
+ "!doc": "Returns the color depth of the screen."
+ },
+ "width": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.screen.width",
+ "!doc": "Returns the width of the screen."
+ },
+ "height": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.screen.height",
+ "!doc": "Returns the height of the screen in pixels."
+ },
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.screen",
+ "!doc": "Returns a reference to the screen object associated with the window."
+ },
+ "postMessage": {
+ "!type": "fn(message: string, targetOrigin: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.postMessage",
+ "!doc": "window.postMessage, when called, causes a MessageEvent to be dispatched at the target window when any pending script that must be executed completes (e.g. remaining event handlers if window.postMessage is called from an event handler, previously-set pending timeouts, etc.). The MessageEvent has the type message, a data property which is set to the value of the first argument provided to window.postMessage, an origin property corresponding to the origin of the main document in the window calling window.postMessage at the time window.postMessage was called, and a source property which is the window from which window.postMessage is called. (Other standard properties of events are present with their expected values.)"
+ },
+ "close": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.close",
+ "!doc": "Closes the current window, or a referenced window."
+ },
+ "blur": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.blur",
+ "!doc": "The blur method removes keyboard focus from the current element."
+ },
+ "focus": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.focus",
+ "!doc": "Sets focus on the specified element, if it can be focused."
+ },
+ "onload": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.onload",
+ "!doc": "An event handler for the load event of a window."
+ },
+ "onunload": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.onunload",
+ "!doc": "The unload event is raised when the window is unloading its content and resources. The resources removal is processed after the unload event occurs."
+ },
+ "onscroll": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.onscroll",
+ "!doc": "Specifies the function to be called when the window is scrolled."
+ },
+ "onresize": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.onresize",
+ "!doc": "An event handler for the resize event on the window."
+ },
+ "ononline": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/document.ononline",
+ "!doc": ",fgh s dgkljgsdfl dfjg sdlgj sdlg sdlfj dlg jkdfkj dfjgdfkglsdfjsdlfkgj hdflkg hdlkfjgh dfkjgh"
+ },
+ "onoffline": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/Online_and_offline_events",
+ "!doc": "Some browsers implement Online/Offline events from the WHATWG Web Applications 1.0 specification."
+ },
+ "onmousewheel": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/DOM_event_reference/mousewheel",
+ "!doc": "The DOM mousewheel event is fired asynchronously when mouse wheel or similar device is operated. It's represented by the MouseWheelEvent interface."
+ },
+ "onmouseup": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.onmouseup",
+ "!doc": "An event handler for the mouseup event on the window."
+ },
+ "onmouseover": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onmouseover",
+ "!doc": "The onmouseover property returns the onMouseOver event handler code on the current element."
+ },
+ "onmouseout": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onmouseout",
+ "!doc": "The onmouseout property returns the onMouseOut event handler code on the current element."
+ },
+ "onmousemove": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onmousemove",
+ "!doc": "The onmousemove property returns the mousemove event handler code on the current element."
+ },
+ "onmousedown": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.onmousedown",
+ "!doc": "An event handler for the mousedown event on the window."
+ },
+ "onclick": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onclick",
+ "!doc": "The onclick property returns the onClick event handler code on the current element."
+ },
+ "ondblclick": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.ondblclick",
+ "!doc": "The ondblclick property returns the onDblClick event handler code on the current element."
+ },
+ "onmessage": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/Worker",
+ "!doc": "Dedicated Web Workers provide a simple means for web content to run scripts in background threads. Once created, a worker can send messages to the spawning task by posting messages to an event handler specified by the creator."
+ },
+ "onkeyup": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onkeyup",
+ "!doc": "The onkeyup property returns the onKeyUp event handler code for the current element."
+ },
+ "onkeypress": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onkeypress",
+ "!doc": "The onkeypress property sets and returns the onKeyPress event handler code for the current element."
+ },
+ "onkeydown": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.onkeydown",
+ "!doc": "An event handler for the keydown event on the window."
+ },
+ "oninput": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/DOM_event_reference/input",
+ "!doc": "The DOM input event is fired synchronously when the value of an <input> or <textarea> element is changed. Additionally, it's also fired on contenteditable editors when its contents are changed. In this case, the event target is the editing host element. If there are two or more elements which have contenteditable as true, \"editing host\" is the nearest ancestor element whose parent isn't editable. Similarly, it's also fired on root element of designMode editors."
+ },
+ "onpopstate": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.onpopstate",
+ "!doc": "An event handler for the popstate event on the window."
+ },
+ "onhashchange": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.onhashchange",
+ "!doc": "The hashchange event fires when a window's hash changes."
+ },
+ "onfocus": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onfocus",
+ "!doc": "The onfocus property returns the onFocus event handler code on the current element."
+ },
+ "onblur": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onblur",
+ "!doc": "The onblur property returns the onBlur event handler code, if any, that exists on the current element."
+ },
+ "onerror": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.onerror",
+ "!doc": "An event handler for runtime script errors."
+ },
+ "ondrop": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en-US/docs/DOM/Mozilla_event_reference/drop",
+ "!doc": "The drop event is fired when an element or text selection is dropped on a valid drop target."
+ },
+ "ondragstart": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en-US/docs/DOM/Mozilla_event_reference/dragstart",
+ "!doc": "The dragstart event is fired when the user starts dragging an element or text selection."
+ },
+ "ondragover": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en-US/docs/DOM/Mozilla_event_reference/dragover",
+ "!doc": "The dragover event is fired when an element or text selection is being dragged over a valid drop target (every few hundred milliseconds)."
+ },
+ "ondragleave": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en-US/docs/DOM/Mozilla_event_reference/dragleave",
+ "!doc": "The dragleave event is fired when a dragged element or text selection leaves a valid drop target."
+ },
+ "ondragenter": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en-US/docs/DOM/Mozilla_event_reference/dragenter",
+ "!doc": "The dragenter event is fired when a dragged element or text selection enters a valid drop target."
+ },
+ "ondragend": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en-US/docs/DOM/Mozilla_event_reference/dragend",
+ "!doc": "The dragend event is fired when a drag operation is being ended (by releasing a mouse button or hitting the escape key)."
+ },
+ "ondrag": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en-US/docs/DOM/Mozilla_event_reference/drag",
+ "!doc": "The drag event is fired when an element or text selection is being dragged (every few hundred milliseconds)."
+ },
+ "oncontextmenu": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.oncontextmenu",
+ "!doc": "An event handler property for right-click events on the window. Unless the default behavior is prevented, the browser context menu will activate (though IE8 has a bug with this and will not activate the context menu if a contextmenu event handler is defined). Note that this event will occur with any non-disabled right-click event and does not depend on an element possessing the \"contextmenu\" attribute."
+ },
+ "onchange": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/element.onchange",
+ "!doc": "The onchange property sets and returns the onChange event handler code for the current element."
+ },
+ "onbeforeunload": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.onbeforeunload",
+ "!doc": "An event that fires when a window is about to unload its resources. The document is still visible and the event is still cancelable."
+ },
+ "onabort": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.onabort",
+ "!doc": "An event handler for abort events sent to the window."
+ },
+ "getSelection": {
+ "!type": "fn() -> +Selection",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.getSelection",
+ "!doc": "Returns a selection object representing the range of text selected by the user. "
+ },
+ "alert": {
+ "!type": "fn(message: string)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.alert",
+ "!doc": "Display an alert dialog with the specified content and an OK button."
+ },
+ "confirm": {
+ "!type": "fn(message: string) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.confirm",
+ "!doc": "Displays a modal dialog with a message and two buttons, OK and Cancel."
+ },
+ "prompt": {
+ "!type": "fn(message: string, value: string) -> string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.prompt",
+ "!doc": "Displays a dialog with a message prompting the user to input some text."
+ },
+ "scrollBy": {
+ "!type": "fn(x: number, y: number)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.scrollBy",
+ "!doc": "Scrolls the document in the window by the given amount."
+ },
+ "scrollTo": {
+ "!type": "fn(x: number, y: number)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.scrollTo",
+ "!doc": "Scrolls to a particular set of coordinates in the document."
+ },
+ "scroll": {
+ "!type": "fn(x: number, y: number)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.scroll",
+ "!doc": "Scrolls the window to a particular place in the document."
+ },
+ "setTimeout": {
+ "!type": "fn(f: fn(), ms: number) -> number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.setTimeout",
+ "!doc": "Calls a function or executes a code snippet after specified delay."
+ },
+ "clearTimeout": {
+ "!type": "fn(timeout: number)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.clearTimeout",
+ "!doc": "Clears the delay set by window.setTimeout()."
+ },
+ "setInterval": {
+ "!type": "fn(f: fn(), ms: number) -> number",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.setInterval",
+ "!doc": "Calls a function or executes a code snippet repeatedly, with a fixed time delay between each call to that function."
+ },
+ "clearInterval": {
+ "!type": "fn(interval: number)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.clearInterval",
+ "!doc": "Cancels repeated action which was set up using setInterval."
+ },
+ "atob": {
+ "!type": "fn(encoded: string) -> string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.atob",
+ "!doc": "Decodes a string of data which has been encoded using base-64 encoding."
+ },
+ "btoa": {
+ "!type": "fn(data: string) -> string",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.btoa",
+ "!doc": "Creates a base-64 encoded ASCII string from a string of binary data."
+ },
+ "addEventListener": {
+ "!type": "fn(type: string, listener: fn(e: +Event), capture: bool)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/EventTarget.addEventListener",
+ "!doc": "Registers a single event listener on a single target. The event target may be a single element in a document, the document itself, a window, or an XMLHttpRequest."
+ },
+ "removeEventListener": {
+ "!type": "fn(type: string, listener: fn(), capture: bool)",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/EventTarget.removeEventListener",
+ "!doc": "Allows the removal of event listeners from the event target."
+ },
+ "dispatchEvent": {
+ "!type": "fn(event: +Event) -> bool",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/EventTarget.dispatchEvent",
+ "!doc": "Dispatches an event into the event system. The event is subject to the same capturing and bubbling behavior as directly dispatched events."
+ },
+ "getComputedStyle": {
+ "!type": "fn(node: +Element, pseudo?: string) -> Element.prototype.style",
+ "!url": "https://developer.mozilla.org/en/docs/DOM/window.getComputedStyle",
+ "!doc": "Gives the final used values of all the CSS properties of an element."
+ },
+ "CanvasRenderingContext2D": {
+ "canvas": "+Element",
+ "width": "number",
+ "height": "number",
+ "commit": "fn()",
+ "save": "fn()",
+ "restore": "fn()",
+ "currentTransform": "?",
+ "scale": "fn(x: number, y: number)",
+ "rotate": "fn(angle: number)",
+ "translate": "fn(x: number, y: number)",
+ "transform": "fn(a: number, b: number, c: number, d: number, e: number, f: number)",
+ "setTransform": "fn(a: number, b: number, c: number, d: number, e: number, f: number)",
+ "resetTransform": "fn()",
+ "globalAlpha": "number",
+ "globalCompositeOperation": "string",
+ "imageSmoothingEnabled": "bool",
+ "strokeStyle": "string",
+ "fillStyle": "string",
+ "createLinearGradient": "fn(x0: number, y0: number, x1: number, y1: number) -> ?",
+ "createPattern": "fn(image: ?, repetition: string) -> ?",
+ "shadowOffsetX": "number",
+ "shadowOffsetY": "number",
+ "shadowBlur": "number",
+ "shadowColor": "string",
+ "clearRect": "fn(x: number, y: number, w: number, h: number)",
+ "fillRect": "fn(x: number, y: number, w: number, h: number)",
+ "strokeRect": "fn(x: number, y: number, w: number, h: number)",
+ "fillRule": "string",
+ "fill": "fn()",
+ "beginPath": "fn()",
+ "stroke": "fn()",
+ "clip": "fn()",
+ "resetClip": "fn()",
+ "measureText": "fn(text: string) -> ?",
+ "drawImage": "fn(image: ?, dx: number, dy: number)",
+ "createImageData": "fn(sw: number, sh: number) -> ?",
+ "getImageData": "fn(sx: number, sy: number, sw: number, sh: number) -> ?",
+ "putImageData": "fn(imagedata: ?, dx: number, dy: number)",
+ "lineWidth": "number",
+ "lineCap": "string",
+ "lineJoin": "string",
+ "miterLimit": "number",
+ "setLineDash": "fn(segments: [number])",
+ "getLineDash": "fn() -> [number]",
+ "lineDashOffset": "number",
+ "font": "string",
+ "textAlign": "string",
+ "textBaseline": "string",
+ "direction": "string",
+ "closePath": "fn()",
+ "moveTo": "fn(x: number, y: number)",
+ "lineTo": "fn(x: number, y: number)",
+ "quadraticCurveTo": "fn(cpx: number, cpy: number, x: number, y: number)",
+ "bezierCurveTo": "fn(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number)",
+ "arcTo": "fn(x1: number, y1: number, x2: number, y2: number, radius: number)",
+ "rect": "fn(x: number, y: number, w: number, h: number)",
+ "arc": "fn(x: number, y: number, radius: number, startAngle: number, endAngle: number, anticlockwise?: bool)",
+ "ellipse": "fn(x: number, y: number, radiusX: number, radiusY: number, rotation: number, startAngle: number, endAngle: number, anticlockwise: bool)"
+ }
+}
diff --git a/devtools/client/sourceeditor/tern/comment.js b/devtools/client/sourceeditor/tern/comment.js
new file mode 100755
index 000000000..e3ff87190
--- /dev/null
+++ b/devtools/client/sourceeditor/tern/comment.js
@@ -0,0 +1,87 @@
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ return mod(exports);
+ if (typeof define == "function" && define.amd) // AMD
+ return define(["exports"], mod);
+ mod(tern.comment || (tern.comment = {}));
+})(function(exports) {
+ function isSpace(ch) {
+ return (ch < 14 && ch > 8) || ch === 32 || ch === 160;
+ }
+
+ function onOwnLine(text, pos) {
+ for (; pos > 0; --pos) {
+ var ch = text.charCodeAt(pos - 1);
+ if (ch == 10) break;
+ if (!isSpace(ch)) return false;
+ }
+ return true;
+ }
+
+ // Gather comments directly before a function
+ exports.commentsBefore = function(text, pos) {
+ var found = null, emptyLines = 0, topIsLineComment;
+ out: while (pos > 0) {
+ var prev = text.charCodeAt(pos - 1);
+ if (prev == 10) {
+ for (var scan = --pos, sawNonWS = false; scan > 0; --scan) {
+ prev = text.charCodeAt(scan - 1);
+ if (prev == 47 && text.charCodeAt(scan - 2) == 47) {
+ if (!onOwnLine(text, scan - 2)) break out;
+ var content = text.slice(scan, pos);
+ if (!emptyLines && topIsLineComment) found[0] = content + "\n" + found[0];
+ else (found || (found = [])).unshift(content);
+ topIsLineComment = true;
+ emptyLines = 0;
+ pos = scan - 2;
+ break;
+ } else if (prev == 10) {
+ if (!sawNonWS && ++emptyLines > 1) break out;
+ break;
+ } else if (!sawNonWS && !isSpace(prev)) {
+ sawNonWS = true;
+ }
+ }
+ } else if (prev == 47 && text.charCodeAt(pos - 2) == 42) {
+ for (var scan = pos - 2; scan > 1; --scan) {
+ if (text.charCodeAt(scan - 1) == 42 && text.charCodeAt(scan - 2) == 47) {
+ if (!onOwnLine(text, scan - 2)) break out;
+ (found || (found = [])).unshift(text.slice(scan, pos - 2));
+ topIsLineComment = false;
+ emptyLines = 0;
+ break;
+ }
+ }
+ pos = scan - 2;
+ } else if (isSpace(prev)) {
+ --pos;
+ } else {
+ break;
+ }
+ }
+ return found;
+ };
+
+ exports.commentAfter = function(text, pos) {
+ while (pos < text.length) {
+ var next = text.charCodeAt(pos);
+ if (next == 47) {
+ var after = text.charCodeAt(pos + 1), end;
+ if (after == 47) // line comment
+ end = text.indexOf("\n", pos + 2);
+ else if (after == 42) // block comment
+ end = text.indexOf("*/", pos + 2);
+ else
+ return;
+ return text.slice(pos + 2, end < 0 ? text.length : end);
+ } else if (isSpace(next)) {
+ ++pos;
+ }
+ }
+ };
+
+ exports.ensureCommentsBefore = function(text, node) {
+ if (node.hasOwnProperty("commentsBefore")) return node.commentsBefore;
+ return node.commentsBefore = exports.commentsBefore(text, node.start);
+ };
+});
diff --git a/devtools/client/sourceeditor/tern/condense.js b/devtools/client/sourceeditor/tern/condense.js
new file mode 100755
index 000000000..f6bf626d6
--- /dev/null
+++ b/devtools/client/sourceeditor/tern/condense.js
@@ -0,0 +1,304 @@
+// Condensing an inferred set of types to a JSON description document.
+
+// This code can be used to, after a library has been analyzed,
+// extract the types defined in that library and dump them as a JSON
+// structure (as parsed by def.js).
+
+// The idea being that big libraries can be analyzed once, dumped, and
+// then cheaply included in later analysis.
+
+(function(root, mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ return mod(exports, require("./infer"));
+ if (typeof define == "function" && define.amd) // AMD
+ return define(["exports", "./infer"], mod);
+ mod(root.tern || (root.tern = {}), tern); // Plain browser env
+})(this, function(exports, infer) {
+ "use strict";
+
+ exports.condense = function(origins, name, options) {
+ if (typeof origins == "string") origins = [origins];
+ var state = new State(origins, name || origins[0], options || {});
+
+ state.server.signal("preCondenseReach", state)
+
+ state.cx.topScope.path = "<top>";
+ state.cx.topScope.reached("", state);
+ for (var path in state.roots)
+ reach(state.roots[path], null, path, state);
+ for (var i = 0; i < state.patchUp.length; ++i)
+ patchUpSimpleInstance(state.patchUp[i], state);
+
+ state.server.signal("postCondenseReach", state)
+
+ for (var path in state.types)
+ store(createPath(path.split("."), state), state.types[path], state);
+ for (var path in state.altPaths)
+ storeAlt(path, state.altPaths[path], state);
+ var hasDef = false;
+ for (var _def in state.output["!define"]) { hasDef = true; break; }
+ if (!hasDef) delete state.output["!define"];
+
+ state.server.signal("postCondense", state)
+
+ return simplify(state.output, state.options.sortOutput);
+ };
+
+ function State(origins, name, options) {
+ this.origins = origins;
+ this.cx = infer.cx();
+ this.server = options.server || this.cx.parent || {signal: function() {}}
+ this.maxOrigin = -Infinity;
+ for (var i = 0; i < origins.length; ++i)
+ this.maxOrigin = Math.max(this.maxOrigin, this.cx.origins.indexOf(origins[i]));
+ this.output = {"!name": name, "!define": {}};
+ this.options = options;
+ this.types = Object.create(null);
+ this.altPaths = Object.create(null);
+ this.patchUp = [];
+ this.roots = Object.create(null);
+ }
+
+ State.prototype.isTarget = function(origin) {
+ return this.origins.indexOf(origin) > -1;
+ };
+
+ State.prototype.getSpan = function(node) {
+ if (this.options.spans == false || !this.isTarget(node.origin)) return null;
+ if (node.span) return node.span;
+ var srv = this.cx.parent, file;
+ if (!srv || !node.originNode || !(file = srv.findFile(node.origin))) return null;
+ var start = node.originNode.start, end = node.originNode.end;
+ var pStart = file.asLineChar(start), pEnd = file.asLineChar(end);
+ return start + "[" + pStart.line + ":" + pStart.ch + "]-" +
+ end + "[" + pEnd.line + ":" + pEnd.ch + "]";
+ };
+
+ function pathLen(path) {
+ var len = 1, pos = 0, dot;
+ while ((dot = path.indexOf(".", pos)) != -1) {
+ pos = dot + 1;
+ len += path.charAt(pos) == "!" ? 10 : 1;
+ }
+ return len;
+ }
+
+ function hop(obj, prop) {
+ return Object.prototype.hasOwnProperty.call(obj, prop);
+ }
+
+ function isSimpleInstance(o) {
+ return o.proto && !(o instanceof infer.Fn) && o.proto != infer.cx().protos.Object &&
+ o.proto.hasCtor && !o.hasCtor;
+ }
+
+ function reach(type, path, id, state, byName) {
+ var actual = type.getType(false);
+ if (!actual) return;
+ var orig = type.origin || actual.origin, relevant = false;
+ if (orig) {
+ var origPos = state.cx.origins.indexOf(orig);
+ // This is a path that is newer than the code we are interested in.
+ if (origPos > state.maxOrigin) return;
+ relevant = state.isTarget(orig);
+ }
+ var newPath = path ? path + "." + id : id, oldPath = actual.path;
+ var shorter = !oldPath || pathLen(oldPath) > pathLen(newPath);
+ if (shorter) {
+ if (!(actual instanceof infer.Prim)) actual.path = newPath;
+ if (actual.reached(newPath, state, !relevant) && relevant) {
+ var data = state.types[oldPath];
+ if (data) {
+ delete state.types[oldPath];
+ state.altPaths[oldPath] = actual;
+ } else data = {type: actual};
+ data.span = state.getSpan(type) || (actual != type && state.isTarget(actual.origin) && state.getSpan(actual)) || data.span;
+ data.doc = type.doc || (actual != type && state.isTarget(actual.origin) && actual.doc) || data.doc;
+ data.data = actual.metaData;
+ data.byName = data.byName == null ? !!byName : data.byName && byName;
+ state.types[newPath] = data;
+ }
+ } else {
+ if (relevant) state.altPaths[newPath] = actual;
+ }
+ }
+ function reachByName(aval, path, id, state) {
+ var type = aval.getType();
+ if (type) reach(type, path, id, state, true);
+ }
+
+ infer.Prim.prototype.reached = function() {return true;};
+
+ infer.Arr.prototype.reached = function(path, state, concrete) {
+ if (concrete) return true
+ if (this.tuple) {
+ for (var i = 0; i < this.tuple; i++)
+ reachByName(this.getProp(String(i)), path, String(i), state)
+ } else {
+ reachByName(this.getProp("<i>"), path, "<i>", state)
+ }
+ return true;
+ };
+
+ infer.Fn.prototype.reached = function(path, state, concrete) {
+ infer.Obj.prototype.reached.call(this, path, state, concrete);
+ if (!concrete) {
+ for (var i = 0; i < this.args.length; ++i)
+ reachByName(this.args[i], path, "!" + i, state);
+ reachByName(this.retval, path, "!ret", state);
+ }
+ return true;
+ };
+
+ infer.Obj.prototype.reached = function(path, state, concrete) {
+ if (isSimpleInstance(this) && !this.condenseForceInclude) {
+ if (state.patchUp.indexOf(this) == -1) state.patchUp.push(this);
+ return true;
+ } else if (this.proto && !concrete) {
+ reach(this.proto, path, "!proto", state);
+ }
+ var hasProps = false;
+ for (var prop in this.props) {
+ reach(this.props[prop], path, prop, state);
+ hasProps = true;
+ }
+ if (!hasProps && !this.condenseForceInclude && !(this instanceof infer.Fn)) {
+ this.nameOverride = "?";
+ return false;
+ }
+ return true;
+ };
+
+ function patchUpSimpleInstance(obj, state) {
+ var path = obj.proto.hasCtor.path;
+ if (path) {
+ obj.nameOverride = "+" + path;
+ } else {
+ path = obj.path;
+ }
+ for (var prop in obj.props)
+ reach(obj.props[prop], path, prop, state);
+ }
+
+ function createPath(parts, state) {
+ var base = state.output, defs = state.output["!define"];
+ for (var i = 0, path; i < parts.length; ++i) {
+ var part = parts[i];
+ path = path ? path + "." + part : part;
+ var me = state.types[path];
+ if (part.charAt(0) == "!" || me && me.byName) {
+ if (hop(defs, path)) base = defs[path];
+ else defs[path] = base = {};
+ } else {
+ if (hop(base, parts[i])) base = base[part];
+ else base = base[part] = {};
+ }
+ }
+ return base;
+ }
+
+ function store(out, info, state) {
+ var name = typeName(info.type);
+ if (name != info.type.path && name != "?") {
+ out["!type"] = name;
+ } else if (info.type.proto && info.type.proto != state.cx.protos.Object) {
+ var protoName = typeName(info.type.proto);
+ if (protoName != "?") out["!proto"] = protoName;
+ }
+ if (info.span) out["!span"] = info.span;
+ if (info.doc) out["!doc"] = info.doc;
+ if (info.data) out["!data"] = info.data;
+ }
+
+ function storeAlt(path, type, state) {
+ var parts = path.split("."), last = parts.pop();
+ if (last[0] == "!") return;
+ var known = state.types[parts.join(".")];
+ var base = createPath(parts, state);
+ if (known && known.type.constructor != infer.Obj) return;
+ if (!hop(base, last)) base[last] = type.nameOverride || type.path;
+ }
+
+ var typeNameStack = [];
+ function typeName(value) {
+ var isType = value instanceof infer.Type;
+ if (isType) {
+ if (typeNameStack.indexOf(value) > -1)
+ return value.path || "?";
+ typeNameStack.push(value);
+ }
+ var name = value.typeName();
+ if (isType) typeNameStack.pop();
+ return name;
+ }
+
+ infer.AVal.prototype.typeName = function() {
+ if (this.types.length == 0) return "?";
+ if (this.types.length == 1) return typeName(this.types[0]);
+ var simplified = infer.simplifyTypes(this.types);
+ if (simplified.length > 2) return "?";
+ for (var strs = [], i = 0; i < simplified.length; i++)
+ strs.push(typeName(simplified[i]));
+ return strs.join("|");
+ };
+
+ infer.ANull.typeName = function() { return "?"; };
+
+ infer.Prim.prototype.typeName = function() { return this.name; };
+
+ infer.Sym.prototype.typeName = function() { return this.asPropName }
+
+ infer.Arr.prototype.typeName = function() {
+ if (!this.tuple) return "[" + typeName(this.getProp("<i>")) + "]"
+ var content = []
+ for (var i = 0; i < this.tuple; i++)
+ content.push(typeName(this.getProp(String(i))))
+ return "[" + content.join(", ") + "]"
+ };
+
+ infer.Fn.prototype.typeName = function() {
+ var out = this.generator ? "fn*(" : "fn(";
+ for (var i = 0; i < this.args.length; ++i) {
+ if (i) out += ", ";
+ var name = this.argNames[i];
+ if (name && name != "?") out += name + ": ";
+ out += typeName(this.args[i]);
+ }
+ out += ")";
+ if (this.computeRetSource)
+ out += " -> " + this.computeRetSource;
+ else if (!this.retval.isEmpty())
+ out += " -> " + typeName(this.retval);
+ return out;
+ };
+
+ infer.Obj.prototype.typeName = function() {
+ if (this.nameOverride) return this.nameOverride;
+ if (!this.path) return "?";
+ return this.path;
+ };
+
+ function simplify(data, sort) {
+ if (typeof data != "object") return data;
+ var sawType = false, sawOther = false;
+ for (var prop in data) {
+ if (prop == "!type") sawType = true;
+ else sawOther = true;
+ if (prop != "!data")
+ data[prop] = simplify(data[prop], sort);
+ }
+ if (sawType && !sawOther) return data["!type"];
+ return sort ? sortObject(data) : data;
+ }
+
+ function sortObject(obj) {
+ var props = [], out = {};
+ for (var prop in obj) props.push(prop);
+ props.sort();
+ for (var i = 0; i < props.length; ++i) {
+ var prop = props[i];
+ out[prop] = obj[prop];
+ }
+ return out;
+ }
+});
diff --git a/devtools/client/sourceeditor/tern/def.js b/devtools/client/sourceeditor/tern/def.js
new file mode 100755
index 000000000..71f6e7991
--- /dev/null
+++ b/devtools/client/sourceeditor/tern/def.js
@@ -0,0 +1,656 @@
+// Type description parser
+//
+// Type description JSON files (such as ecma5.json and browser.json)
+// are used to
+//
+// A) describe types that come from native code
+//
+// B) to cheaply load the types for big libraries, or libraries that
+// can't be inferred well
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ return exports.init = mod;
+ if (typeof define == "function" && define.amd) // AMD
+ return define({init: mod});
+ tern.def = {init: mod};
+})(function(exports, infer) {
+ "use strict";
+
+ function hop(obj, prop) {
+ return Object.prototype.hasOwnProperty.call(obj, prop);
+ }
+
+ var TypeParser = exports.TypeParser = function(spec, start, base, forceNew) {
+ this.pos = start || 0;
+ this.spec = spec;
+ this.base = base;
+ this.forceNew = forceNew;
+ };
+
+ function unwrapType(type, self, args) {
+ return type.call ? type(self, args) : type;
+ }
+
+ function extractProp(type, prop) {
+ if (prop == "!ret") {
+ if (type.retval) return type.retval;
+ var rv = new infer.AVal;
+ type.propagate(new infer.IsCallee(infer.ANull, [], null, rv));
+ return rv;
+ } else {
+ return type.getProp(prop);
+ }
+ }
+
+ function computedFunc(name, args, retType, generator) {
+ return function(self, cArgs) {
+ var realArgs = [];
+ for (var i = 0; i < args.length; i++) realArgs.push(unwrapType(args[i], self, cArgs));
+ return new infer.Fn(name, infer.ANull, realArgs, unwrapType(retType, self, cArgs), generator);
+ };
+ }
+ function computedUnion(types) {
+ return function(self, args) {
+ var union = new infer.AVal;
+ for (var i = 0; i < types.length; i++) unwrapType(types[i], self, args).propagate(union);
+ union.maxWeight = 1e5;
+ return union;
+ };
+ }
+ function computedArray(inner) {
+ return function(self, args) {
+ return new infer.Arr(inner(self, args));
+ };
+ }
+ function computedTuple(types) {
+ return function(self, args) {
+ return new infer.Arr(types.map(function(tp) { return unwrapType(tp, self, args) }))
+ }
+ }
+
+ TypeParser.prototype = {
+ eat: function(str) {
+ if (str.length == 1 ? this.spec.charAt(this.pos) == str : this.spec.indexOf(str, this.pos) == this.pos) {
+ this.pos += str.length;
+ return true;
+ }
+ },
+ word: function(re) {
+ var word = "", ch, re = re || /[\w$]/;
+ while ((ch = this.spec.charAt(this.pos)) && re.test(ch)) { word += ch; ++this.pos; }
+ return word;
+ },
+ error: function() {
+ throw new Error("Unrecognized type spec: " + this.spec + " (at " + this.pos + ")");
+ },
+ parseFnType: function(comp, name, top, generator) {
+ var args = [], names = [], computed = false;
+ if (!this.eat(")")) for (var i = 0; ; ++i) {
+ var colon = this.spec.indexOf(": ", this.pos), argname;
+ if (colon != -1) {
+ argname = this.spec.slice(this.pos, colon);
+ if (/^[$\w?]+$/.test(argname))
+ this.pos = colon + 2;
+ else
+ argname = null;
+ }
+ names.push(argname);
+ var argType = this.parseType(comp);
+ if (argType.call) computed = true;
+ args.push(argType);
+ if (!this.eat(", ")) {
+ this.eat(")") || this.error();
+ break;
+ }
+ }
+ var retType, computeRet, computeRetStart, fn;
+ if (this.eat(" -> ")) {
+ var retStart = this.pos;
+ retType = this.parseType(true);
+ if (retType.call && !computed) {
+ computeRet = retType;
+ retType = infer.ANull;
+ computeRetStart = retStart;
+ }
+ } else {
+ retType = infer.ANull;
+ }
+ if (computed) return computedFunc(name, args, retType, generator);
+
+ if (top && (fn = this.base))
+ infer.Fn.call(this.base, name, infer.ANull, args, names, retType, generator);
+ else
+ fn = new infer.Fn(name, infer.ANull, args, names, retType, generator);
+ if (computeRet) fn.computeRet = computeRet;
+ if (computeRetStart != null) fn.computeRetSource = this.spec.slice(computeRetStart, this.pos);
+ return fn;
+ },
+ parseType: function(comp, name, top) {
+ var main = this.parseTypeMaybeProp(comp, name, top);
+ if (!this.eat("|")) return main;
+ var types = [main], computed = main.call;
+ for (;;) {
+ var next = this.parseTypeMaybeProp(comp, name, top);
+ types.push(next);
+ if (next.call) computed = true;
+ if (!this.eat("|")) break;
+ }
+ if (computed) return computedUnion(types);
+ var union = new infer.AVal;
+ for (var i = 0; i < types.length; i++) types[i].propagate(union);
+ union.maxWeight = 1e5;
+ return union;
+ },
+ parseTypeMaybeProp: function(comp, name, top) {
+ var result = this.parseTypeInner(comp, name, top);
+ while (comp && this.eat(".")) result = this.extendWithProp(result);
+ return result;
+ },
+ extendWithProp: function(base) {
+ var propName = this.word(/[\w<>$!:]/) || this.error();
+ if (base.apply) return function(self, args) {
+ return extractProp(base(self, args), propName);
+ };
+ return extractProp(base, propName);
+ },
+ parseTypeInner: function(comp, name, top) {
+ var gen
+ if (this.eat("fn(") || (gen = this.eat("fn*("))) {
+ return this.parseFnType(comp, name, top, gen);
+ } else if (this.eat("[")) {
+ var inner = this.parseType(comp), types, computed = inner.call
+ while (this.eat(", ")) {
+ if (!types) types = [inner]
+ var next = this.parseType(comp)
+ types.push(next)
+ computed = computed || next.call
+ }
+ this.eat("]") || this.error()
+ if (computed) return types ? computedTuple(types) : computedArray(inner)
+ if (top && this.base) {
+ infer.Arr.call(this.base, types || inner)
+ return this.base
+ }
+ return new infer.Arr(types || inner)
+ } else if (this.eat("+")) {
+ var path = this.word(/[\w$<>\.:!]/)
+ var base = infer.cx().localDefs[path + ".prototype"]
+ if (!base) {
+ var base = parsePath(path);
+ if (!(base instanceof infer.Obj)) return base;
+ var proto = descendProps(base, ["prototype"])
+ if (proto && (proto = proto.getObjType()))
+ base = proto
+ }
+ if (comp && this.eat("[")) return this.parsePoly(base);
+ if (top && this.forceNew) return new infer.Obj(base);
+ return infer.getInstance(base);
+ } else if (this.eat(":")) {
+ var name = this.word(/[\w$\.]/)
+ return infer.getSymbol(name)
+ } else if (comp && this.eat("!")) {
+ var arg = this.word(/\d/);
+ if (arg) {
+ arg = Number(arg);
+ return function(_self, args) {return args[arg] || infer.ANull;};
+ } else if (this.eat("this")) {
+ return function(self) {return self;};
+ } else if (this.eat("custom:")) {
+ var fname = this.word(/[\w$]/);
+ return customFunctions[fname] || function() { return infer.ANull; };
+ } else {
+ return this.fromWord("!" + this.word(/[\w$<>\.!:]/));
+ }
+ } else if (this.eat("?")) {
+ return infer.ANull;
+ } else {
+ return this.fromWord(this.word(/[\w$<>\.!:`]/));
+ }
+ },
+ fromWord: function(spec) {
+ var cx = infer.cx();
+ switch (spec) {
+ case "number": return cx.num;
+ case "string": return cx.str;
+ case "bool": return cx.bool;
+ case "<top>": return cx.topScope;
+ }
+ if (cx.localDefs && spec in cx.localDefs) return cx.localDefs[spec];
+ return parsePath(spec);
+ },
+ parsePoly: function(base) {
+ var propName = "<i>", match;
+ if (match = this.spec.slice(this.pos).match(/^\s*([\w$:]+)\s*=\s*/)) {
+ propName = match[1];
+ this.pos += match[0].length;
+ }
+ var value = this.parseType(true);
+ if (!this.eat("]")) this.error();
+ if (value.call) return function(self, args) {
+ var instance = new infer.Obj(base);
+ value(self, args).propagate(instance.defProp(propName));
+ return instance;
+ };
+ var instance = new infer.Obj(base);
+ value.propagate(instance.defProp(propName));
+ return instance;
+ }
+ };
+
+ function parseType(spec, name, base, forceNew) {
+ var type = new TypeParser(spec, null, base, forceNew).parseType(false, name, true);
+ if (/^fn\(/.test(spec)) for (var i = 0; i < type.args.length; ++i) (function(i) {
+ var arg = type.args[i];
+ if (arg instanceof infer.Fn && arg.args && arg.args.length) addEffect(type, function(_self, fArgs) {
+ var fArg = fArgs[i];
+ if (fArg) fArg.propagate(new infer.IsCallee(infer.cx().topScope, arg.args, null, infer.ANull));
+ });
+ })(i);
+ return type;
+ }
+
+ function addEffect(fn, handler, replaceRet) {
+ var oldCmp = fn.computeRet, rv = fn.retval;
+ fn.computeRet = function(self, args, argNodes) {
+ var handled = handler(self, args, argNodes);
+ var old = oldCmp ? oldCmp(self, args, argNodes) : rv;
+ return replaceRet ? handled : old;
+ };
+ }
+
+ var parseEffect = exports.parseEffect = function(effect, fn) {
+ var m;
+ if (effect.indexOf("propagate ") == 0) {
+ var p = new TypeParser(effect, 10);
+ var origin = p.parseType(true);
+ if (!p.eat(" ")) p.error();
+ var target = p.parseType(true);
+ addEffect(fn, function(self, args) {
+ unwrapType(origin, self, args).propagate(unwrapType(target, self, args));
+ });
+ } else if (effect.indexOf("call ") == 0) {
+ var andRet = effect.indexOf("and return ", 5) == 5;
+ var p = new TypeParser(effect, andRet ? 16 : 5);
+ var getCallee = p.parseType(true), getSelf = null, getArgs = [];
+ if (p.eat(" this=")) getSelf = p.parseType(true);
+ while (p.eat(" ")) getArgs.push(p.parseType(true));
+ addEffect(fn, function(self, args) {
+ var callee = unwrapType(getCallee, self, args);
+ var slf = getSelf ? unwrapType(getSelf, self, args) : infer.ANull, as = [];
+ for (var i = 0; i < getArgs.length; ++i) as.push(unwrapType(getArgs[i], self, args));
+ var result = andRet ? new infer.AVal : infer.ANull;
+ callee.propagate(new infer.IsCallee(slf, as, null, result));
+ return result;
+ }, andRet);
+ } else if (m = effect.match(/^custom (\S+)\s*(.*)/)) {
+ var customFunc = customFunctions[m[1]];
+ if (customFunc) addEffect(fn, m[2] ? customFunc(m[2]) : customFunc);
+ } else if (effect.indexOf("copy ") == 0) {
+ var p = new TypeParser(effect, 5);
+ var getFrom = p.parseType(true);
+ p.eat(" ");
+ var getTo = p.parseType(true);
+ addEffect(fn, function(self, args) {
+ var from = unwrapType(getFrom, self, args), to = unwrapType(getTo, self, args);
+ from.forAllProps(function(prop, val, local) {
+ if (local && prop != "<i>")
+ to.propagate(new infer.DefProp(prop, val));
+ });
+ });
+ } else {
+ throw new Error("Unknown effect type: " + effect);
+ }
+ };
+
+ var currentTopScope;
+
+ var parsePath = exports.parsePath = function(path, scope) {
+ var cx = infer.cx(), cached = cx.paths[path], origPath = path;
+ if (cached != null) return cached;
+ cx.paths[path] = infer.ANull;
+
+ var base = scope || currentTopScope || cx.topScope;
+
+ if (cx.localDefs) for (var name in cx.localDefs) {
+ if (path.indexOf(name) == 0) {
+ if (path == name) return cx.paths[path] = cx.localDefs[path];
+ if (path.charAt(name.length) == ".") {
+ base = cx.localDefs[name];
+ path = path.slice(name.length + 1);
+ break;
+ }
+ }
+ }
+
+ var result = descendProps(base, path.split("."))
+ // Uncomment this to get feedback on your poorly written .json files
+ // if (result == infer.ANull) console.error("bad path: " + origPath + " (" + cx.curOrigin + ")")
+ cx.paths[origPath] = result == infer.ANull ? null : result
+ return result
+ }
+
+ function descendProps(base, parts) {
+ for (var i = 0; i < parts.length && base != infer.ANull; ++i) {
+ var prop = parts[i];
+ if (prop.charAt(0) == "!") {
+ if (prop == "!proto") {
+ base = (base instanceof infer.Obj && base.proto) || infer.ANull;
+ } else {
+ var fn = base.getFunctionType();
+ if (!fn) {
+ base = infer.ANull;
+ } else if (prop == "!ret") {
+ base = fn.retval && fn.retval.getType(false) || infer.ANull;
+ } else {
+ var arg = fn.args && fn.args[Number(prop.slice(1))];
+ base = (arg && arg.getType(false)) || infer.ANull;
+ }
+ }
+ } else if (base instanceof infer.Obj) {
+ var propVal = (prop == "prototype" && base instanceof infer.Fn) ? base.getProp(prop) : base.props[prop];
+ if (!propVal || propVal.isEmpty())
+ base = infer.ANull;
+ else
+ base = propVal.types[0];
+ }
+ }
+ return base;
+ }
+
+ function emptyObj(ctor) {
+ var empty = Object.create(ctor.prototype);
+ empty.props = Object.create(null);
+ empty.isShell = true;
+ return empty;
+ }
+
+ function isSimpleAnnotation(spec) {
+ if (!spec["!type"] || /^(fn\(|\[)/.test(spec["!type"])) return false;
+ for (var prop in spec)
+ if (prop != "!type" && prop != "!doc" && prop != "!url" && prop != "!span" && prop != "!data")
+ return false;
+ return true;
+ }
+
+ function passOne(base, spec, path) {
+ if (!base) {
+ var tp = spec["!type"];
+ if (tp) {
+ if (/^fn\(/.test(tp)) base = emptyObj(infer.Fn);
+ else if (tp.charAt(0) == "[") base = emptyObj(infer.Arr);
+ else throw new Error("Invalid !type spec: " + tp);
+ } else if (spec["!stdProto"]) {
+ base = infer.cx().protos[spec["!stdProto"]];
+ } else {
+ base = emptyObj(infer.Obj);
+ }
+ base.name = path;
+ }
+
+ for (var name in spec) if (hop(spec, name) && name.charCodeAt(0) != 33) {
+ var inner = spec[name];
+ if (typeof inner == "string" || isSimpleAnnotation(inner)) continue;
+ var prop = base.defProp(name);
+ passOne(prop.getObjType(), inner, path ? path + "." + name : name).propagate(prop);
+ }
+ return base;
+ }
+
+ function passTwo(base, spec, path) {
+ if (base.isShell) {
+ delete base.isShell;
+ var tp = spec["!type"];
+ if (tp) {
+ parseType(tp, path, base);
+ } else {
+ var proto = spec["!proto"] && parseType(spec["!proto"]);
+ infer.Obj.call(base, proto instanceof infer.Obj ? proto : true, path);
+ }
+ }
+
+ var effects = spec["!effects"];
+ if (effects && base instanceof infer.Fn) for (var i = 0; i < effects.length; ++i)
+ parseEffect(effects[i], base);
+ copyInfo(spec, base);
+
+ for (var name in spec) if (hop(spec, name) && name.charCodeAt(0) != 33) {
+ var inner = spec[name], known = base.defProp(name), innerPath = path ? path + "." + name : name;
+ if (typeof inner == "string") {
+ if (known.isEmpty()) parseType(inner, innerPath).propagate(known);
+ } else {
+ if (!isSimpleAnnotation(inner))
+ passTwo(known.getObjType(), inner, innerPath);
+ else if (known.isEmpty())
+ parseType(inner["!type"], innerPath, null, true).propagate(known);
+ else
+ continue;
+ if (inner["!doc"]) known.doc = inner["!doc"];
+ if (inner["!url"]) known.url = inner["!url"];
+ if (inner["!span"]) known.span = inner["!span"];
+ }
+ }
+ return base;
+ }
+
+ function copyInfo(spec, type) {
+ if (spec["!doc"]) type.doc = spec["!doc"];
+ if (spec["!url"]) type.url = spec["!url"];
+ if (spec["!span"]) type.span = spec["!span"];
+ if (spec["!data"]) type.metaData = spec["!data"];
+ }
+
+ function doLoadEnvironment(data, scope) {
+ var cx = infer.cx(), server = cx.parent
+
+ infer.addOrigin(cx.curOrigin = data["!name"] || "env#" + cx.origins.length);
+ cx.localDefs = cx.definitions[cx.curOrigin] = Object.create(null);
+
+ if (server) server.signal("preLoadDef", data)
+
+ passOne(scope, data);
+
+ var def = data["!define"];
+ if (def) {
+ for (var name in def) {
+ var spec = def[name];
+ cx.localDefs[name] = typeof spec == "string" ? parsePath(spec) : passOne(null, spec, name);
+ }
+ for (var name in def) {
+ var spec = def[name];
+ if (typeof spec != "string") passTwo(cx.localDefs[name], def[name], name);
+ }
+ }
+
+ passTwo(scope, data);
+
+ if (server) server.signal("postLoadDef", data)
+
+ cx.curOrigin = cx.localDefs = null;
+ }
+
+ exports.load = function(data, scope) {
+ if (!scope) scope = infer.cx().topScope;
+ var oldScope = currentTopScope;
+ currentTopScope = scope;
+ try {
+ doLoadEnvironment(data, scope);
+ } finally {
+ currentTopScope = oldScope;
+ }
+ };
+
+ exports.parse = function(data, origin, path) {
+ var cx = infer.cx();
+ if (origin) {
+ cx.origin = origin;
+ cx.localDefs = cx.definitions[origin];
+ }
+
+ try {
+ if (typeof data == "string")
+ return parseType(data, path);
+ else
+ return passTwo(passOne(null, data, path), data, path);
+ } finally {
+ if (origin) cx.origin = cx.localDefs = null;
+ }
+ };
+
+ // Used to register custom logic for more involved effect or type
+ // computation.
+ var customFunctions = Object.create(null);
+ infer.registerFunction = function(name, f) { customFunctions[name] = f; };
+
+ var IsCreated = infer.constraint({
+ construct: function(created, target, spec) {
+ this.created = created;
+ this.target = target;
+ this.spec = spec;
+ },
+ addType: function(tp) {
+ if (tp instanceof infer.Obj && this.created++ < 5) {
+ var derived = new infer.Obj(tp), spec = this.spec;
+ if (spec instanceof infer.AVal) spec = spec.getObjType(false);
+ if (spec instanceof infer.Obj) for (var prop in spec.props) {
+ var cur = spec.props[prop].types[0];
+ var p = derived.defProp(prop);
+ if (cur && cur instanceof infer.Obj && cur.props.value) {
+ var vtp = cur.props.value.getType(false);
+ if (vtp) p.addType(vtp);
+ }
+ }
+ this.target.addType(derived);
+ }
+ }
+ });
+
+ infer.registerFunction("Object_create", function(_self, args, argNodes) {
+ if (argNodes && argNodes.length && argNodes[0].type == "Literal" && argNodes[0].value == null)
+ return new infer.Obj();
+
+ var result = new infer.AVal;
+ if (args[0]) args[0].propagate(new IsCreated(0, result, args[1]));
+ return result;
+ });
+
+ var PropSpec = infer.constraint({
+ construct: function(target) { this.target = target; },
+ addType: function(tp) {
+ if (!(tp instanceof infer.Obj)) return;
+ if (tp.hasProp("value"))
+ tp.getProp("value").propagate(this.target);
+ else if (tp.hasProp("get"))
+ tp.getProp("get").propagate(new infer.IsCallee(infer.ANull, [], null, this.target));
+ }
+ });
+
+ infer.registerFunction("Object_defineProperty", function(_self, args, argNodes) {
+ if (argNodes && argNodes.length >= 3 && argNodes[1].type == "Literal" &&
+ typeof argNodes[1].value == "string") {
+ var obj = args[0], connect = new infer.AVal;
+ obj.propagate(new infer.DefProp(argNodes[1].value, connect, argNodes[1]));
+ args[2].propagate(new PropSpec(connect));
+ }
+ return infer.ANull;
+ });
+
+ infer.registerFunction("Object_defineProperties", function(_self, args, argNodes) {
+ if (args.length >= 2) {
+ var obj = args[0];
+ args[1].forAllProps(function(prop, val, local) {
+ if (!local) return;
+ var connect = new infer.AVal;
+ obj.propagate(new infer.DefProp(prop, connect, argNodes && argNodes[1]));
+ val.propagate(new PropSpec(connect));
+ });
+ }
+ return infer.ANull;
+ });
+
+ var IsBound = infer.constraint({
+ construct: function(self, args, target) {
+ this.self = self; this.args = args; this.target = target;
+ },
+ addType: function(tp) {
+ if (!(tp instanceof infer.Fn)) return;
+ this.target.addType(new infer.Fn(tp.name, infer.ANull, tp.args.slice(this.args.length),
+ tp.argNames.slice(this.args.length), tp.retval, tp.generator));
+ this.self.propagate(tp.self);
+ for (var i = 0; i < Math.min(tp.args.length, this.args.length); ++i)
+ this.args[i].propagate(tp.args[i]);
+ }
+ });
+
+ infer.registerFunction("Function_bind", function(self, args) {
+ if (!args.length) return infer.ANull;
+ var result = new infer.AVal;
+ self.propagate(new IsBound(args[0], args.slice(1), result));
+ return result;
+ });
+
+ infer.registerFunction("Array_ctor", function(_self, args) {
+ var arr = new infer.Arr;
+ if (args.length != 1 || !args[0].hasType(infer.cx().num)) {
+ var content = arr.getProp("<i>");
+ for (var i = 0; i < args.length; ++i) args[i].propagate(content);
+ }
+ return arr;
+ });
+
+ infer.registerFunction("Promise_ctor", function(_self, args, argNodes) {
+ var defs6 = infer.cx().definitions.ecma6
+ if (!defs6 || args.length < 1) return infer.ANull;
+ var self = new infer.Obj(defs6["Promise.prototype"]);
+ var valProp = self.defProp(":t", argNodes && argNodes[0]);
+ var valArg = new infer.AVal;
+ valArg.propagate(valProp);
+ var exec = new infer.Fn("execute", infer.ANull, [valArg], ["value"], infer.ANull);
+ var reject = defs6.Promise_reject;
+ args[0].propagate(new infer.IsCallee(infer.ANull, [exec, reject], null, infer.ANull));
+ return self;
+ });
+
+ var PromiseResolvesTo = infer.constraint({
+ construct: function(output) { this.output = output; },
+ addType: function(tp) {
+ if (tp.constructor == infer.Obj && tp.name == "Promise" && tp.hasProp(":t"))
+ tp.getProp(":t").propagate(this.output);
+ else
+ tp.propagate(this.output);
+ }
+ });
+
+ var WG_PROMISE_KEEP_VALUE = 50;
+
+ infer.registerFunction("Promise_then", function(self, args, argNodes) {
+ var fn = args.length && args[0].getFunctionType();
+ var defs6 = infer.cx().definitions.ecma6
+ if (!fn || !defs6) return self;
+
+ var result = new infer.Obj(defs6["Promise.prototype"]);
+ var value = result.defProp(":t", argNodes && argNodes[0]), ty;
+ if (fn.retval.isEmpty() && (ty = self.getType()) instanceof infer.Obj && ty.hasProp(":t"))
+ ty.getProp(":t").propagate(value, WG_PROMISE_KEEP_VALUE);
+ fn.retval.propagate(new PromiseResolvesTo(value));
+ return result;
+ });
+
+ infer.registerFunction("getOwnPropertySymbols", function(_self, args) {
+ if (!args.length) return infer.ANull
+ var result = new infer.AVal
+ args[0].forAllProps(function(prop, _val, local) {
+ if (local && prop.charAt(0) == ":") result.addType(infer.getSymbol(prop.slice(1)))
+ })
+ return result
+ })
+
+ infer.registerFunction("getSymbol", function(_self, _args, argNodes) {
+ if (argNodes.length && argNodes[0].type == "Literal" && typeof argNodes[0].value == "string")
+ return infer.getSymbol(argNodes[0].value)
+ else
+ return infer.ANull
+ })
+
+ return exports;
+});
diff --git a/devtools/client/sourceeditor/tern/ecma5.js b/devtools/client/sourceeditor/tern/ecma5.js
new file mode 100644
index 000000000..634e414fd
--- /dev/null
+++ b/devtools/client/sourceeditor/tern/ecma5.js
@@ -0,0 +1,950 @@
+module.exports = {
+ "!name": "ecma5",
+ "!define": {"Error.prototype": "Error.prototype"},
+ "Infinity": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Infinity",
+ "!doc": "A numeric value representing infinity."
+ },
+ "undefined": {
+ "!type": "?",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/undefined",
+ "!doc": "The value undefined."
+ },
+ "NaN": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/NaN",
+ "!doc": "A value representing Not-A-Number."
+ },
+ "Object": {
+ "!type": "fn()",
+ "getPrototypeOf": {
+ "!type": "fn(obj: ?) -> ?",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/getPrototypeOf",
+ "!doc": "Returns the prototype (i.e. the internal prototype) of the specified object."
+ },
+ "create": {
+ "!type": "fn(proto: ?) -> !custom:Object_create",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/create",
+ "!doc": "Creates a new object with the specified prototype object and properties."
+ },
+ "defineProperty": {
+ "!type": "fn(obj: ?, prop: string, desc: ?)",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/defineProperty",
+ "!doc": "Defines a new property directly on an object, or modifies an existing property on an object, and returns the object. If you want to see how to use the Object.defineProperty method with a binary-flags-like syntax, see this article."
+ },
+ "defineProperties": {
+ "!type": "fn(obj: ?, props: ?)",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/defineProperty",
+ "!doc": "Defines a new property directly on an object, or modifies an existing property on an object, and returns the object. If you want to see how to use the Object.defineProperty method with a binary-flags-like syntax, see this article."
+ },
+ "getOwnPropertyDescriptor": {
+ "!type": "fn(obj: ?, prop: string) -> ?",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor",
+ "!doc": "Returns a property descriptor for an own property (that is, one directly present on an object, not present by dint of being along an object's prototype chain) of a given object."
+ },
+ "keys": {
+ "!type": "fn(obj: ?) -> [string]",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/keys",
+ "!doc": "Returns an array of a given object's own enumerable properties, in the same order as that provided by a for-in loop (the difference being that a for-in loop enumerates properties in the prototype chain as well)."
+ },
+ "getOwnPropertyNames": {
+ "!type": "fn(obj: ?) -> [string]",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/getOwnPropertyNames",
+ "!doc": "Returns an array of all properties (enumerable or not) found directly upon a given object."
+ },
+ "seal": {
+ "!type": "fn(obj: ?)",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/seal",
+ "!doc": "Seals an object, preventing new properties from being added to it and marking all existing properties as non-configurable. Values of present properties can still be changed as long as they are writable."
+ },
+ "isSealed": {
+ "!type": "fn(obj: ?) -> bool",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/isSealed",
+ "!doc": "Determine if an object is sealed."
+ },
+ "freeze": {
+ "!type": "fn(obj: ?)",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/freeze",
+ "!doc": "Freezes an object: that is, prevents new properties from being added to it; prevents existing properties from being removed; and prevents existing properties, or their enumerability, configurability, or writability, from being changed. In essence the object is made effectively immutable. The method returns the object being frozen."
+ },
+ "isFrozen": {
+ "!type": "fn(obj: ?) -> bool",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/isFrozen",
+ "!doc": "Determine if an object is frozen."
+ },
+ "prototype": {
+ "!stdProto": "Object",
+ "toString": {
+ "!type": "fn() -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/toString",
+ "!doc": "Returns a string representing the object."
+ },
+ "toLocaleString": {
+ "!type": "fn() -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/toLocaleString",
+ "!doc": "Returns a string representing the object. This method is meant to be overriden by derived objects for locale-specific purposes."
+ },
+ "valueOf": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/valueOf",
+ "!doc": "Returns the primitive value of the specified object"
+ },
+ "hasOwnProperty": {
+ "!type": "fn(prop: string) -> bool",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/hasOwnProperty",
+ "!doc": "Returns a boolean indicating whether the object has the specified property."
+ },
+ "propertyIsEnumerable": {
+ "!type": "fn(prop: string) -> bool",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/propertyIsEnumerable",
+ "!doc": "Returns a Boolean indicating whether the specified property is enumerable."
+ }
+ },
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object",
+ "!doc": "Creates an object wrapper."
+ },
+ "Function": {
+ "!type": "fn(body: string) -> fn()",
+ "prototype": {
+ "!stdProto": "Function",
+ "apply": {
+ "!type": "fn(this: ?, args: [?])",
+ "!effects": [
+ "call and return !this this=!0 !1.<i> !1.<i> !1.<i>"
+ ],
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/apply",
+ "!doc": "Calls a function with a given this value and arguments provided as an array (or an array like object)."
+ },
+ "call": {
+ "!type": "fn(this: ?, args?: ?) -> !this.!ret",
+ "!effects": [
+ "call and return !this this=!0 !1 !2 !3 !4"
+ ],
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/call",
+ "!doc": "Calls a function with a given this value and arguments provided individually."
+ },
+ "bind": {
+ "!type": "fn(this: ?, args?: ?) -> !custom:Function_bind",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind",
+ "!doc": "Creates a new function that, when called, has its this keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function was called."
+ },
+ "prototype": "?"
+ },
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function",
+ "!doc": "Every function in JavaScript is actually a Function object."
+ },
+ "Array": {
+ "!type": "fn(size: number) -> !custom:Array_ctor",
+ "isArray": {
+ "!type": "fn(value: ?) -> bool",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/isArray",
+ "!doc": "Returns true if an object is an array, false if it is not."
+ },
+ "prototype": {
+ "!stdProto": "Array",
+ "length": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/length",
+ "!doc": "An unsigned, 32-bit integer that specifies the number of elements in an array."
+ },
+ "concat": {
+ "!type": "fn(other: [?]) -> !this",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/concat",
+ "!doc": "Returns a new array comprised of this array joined with other array(s) and/or value(s)."
+ },
+ "join": {
+ "!type": "fn(separator?: string) -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/join",
+ "!doc": "Joins all elements of an array into a string."
+ },
+ "splice": {
+ "!type": "fn(pos: number, amount: number)",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/splice",
+ "!doc": "Changes the content of an array, adding new elements while removing old elements."
+ },
+ "pop": {
+ "!type": "fn() -> !this.<i>",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/pop",
+ "!doc": "Removes the last element from an array and returns that element."
+ },
+ "push": {
+ "!type": "fn(newelt: ?) -> number",
+ "!effects": [
+ "propagate !0 !this.<i>"
+ ],
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/push",
+ "!doc": "Mutates an array by appending the given elements and returning the new length of the array."
+ },
+ "shift": {
+ "!type": "fn() -> !this.<i>",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/shift",
+ "!doc": "Removes the first element from an array and returns that element. This method changes the length of the array."
+ },
+ "unshift": {
+ "!type": "fn(newelt: ?) -> number",
+ "!effects": [
+ "propagate !0 !this.<i>"
+ ],
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/unshift",
+ "!doc": "Adds one or more elements to the beginning of an array and returns the new length of the array."
+ },
+ "slice": {
+ "!type": "fn(from: number, to?: number) -> !this",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/slice",
+ "!doc": "Returns a shallow copy of a portion of an array."
+ },
+ "reverse": {
+ "!type": "fn()",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/reverse",
+ "!doc": "Reverses an array in place. The first array element becomes the last and the last becomes the first."
+ },
+ "sort": {
+ "!type": "fn(compare?: fn(a: ?, b: ?) -> number)",
+ "!effects": [
+ "call !0 !this.<i> !this.<i>"
+ ],
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/sort",
+ "!doc": "Sorts the elements of an array in place and returns the array."
+ },
+ "indexOf": {
+ "!type": "fn(elt: ?, from?: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/indexOf",
+ "!doc": "Returns the first index at which a given element can be found in the array, or -1 if it is not present."
+ },
+ "lastIndexOf": {
+ "!type": "fn(elt: ?, from?: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/lastIndexOf",
+ "!doc": "Returns the last index at which a given element can be found in the array, or -1 if it is not present. The array is searched backwards, starting at fromIndex."
+ },
+ "every": {
+ "!type": "fn(test: fn(elt: ?, i: number) -> bool, context?: ?) -> bool",
+ "!effects": [
+ "call !0 this=!1 !this.<i> number"
+ ],
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/every",
+ "!doc": "Tests whether all elements in the array pass the test implemented by the provided function."
+ },
+ "some": {
+ "!type": "fn(test: fn(elt: ?, i: number) -> bool, context?: ?) -> bool",
+ "!effects": [
+ "call !0 this=!1 !this.<i> number"
+ ],
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/some",
+ "!doc": "Tests whether some element in the array passes the test implemented by the provided function."
+ },
+ "filter": {
+ "!type": "fn(test: fn(elt: ?, i: number) -> bool, context?: ?) -> !this",
+ "!effects": [
+ "call !0 this=!1 !this.<i> number"
+ ],
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/filter",
+ "!doc": "Creates a new array with all elements that pass the test implemented by the provided function."
+ },
+ "forEach": {
+ "!type": "fn(f: fn(elt: ?, i: number), context?: ?)",
+ "!effects": [
+ "call !0 this=!1 !this.<i> number"
+ ],
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach",
+ "!doc": "Executes a provided function once per array element."
+ },
+ "map": {
+ "!type": "fn(f: fn(elt: ?, i: number) -> ?, context?: ?) -> [!0.!ret]",
+ "!effects": [
+ "call !0 this=!1 !this.<i> number"
+ ],
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/map",
+ "!doc": "Creates a new array with the results of calling a provided function on every element in this array."
+ },
+ "reduce": {
+ "!type": "fn(combine: fn(sum: ?, elt: ?, i: number) -> ?, init?: ?) -> !0.!ret",
+ "!effects": [
+ "call !0 !1 !this.<i> number"
+ ],
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/Reduce",
+ "!doc": "Apply a function against an accumulator and each value of the array (from left-to-right) as to reduce it to a single value."
+ },
+ "reduceRight": {
+ "!type": "fn(combine: fn(sum: ?, elt: ?, i: number) -> ?, init?: ?) -> !0.!ret",
+ "!effects": [
+ "call !0 !1 !this.<i> number"
+ ],
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/ReduceRight",
+ "!doc": "Apply a function simultaneously against two values of the array (from right-to-left) as to reduce it to a single value."
+ }
+ },
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array",
+ "!doc": "The JavaScript Array global object is a constructor for arrays, which are high-level, list-like objects."
+ },
+ "String": {
+ "!type": "fn(value: ?) -> string",
+ "fromCharCode": {
+ "!type": "fn(code: number) -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/fromCharCode",
+ "!doc": "Returns a string created by using the specified sequence of Unicode values."
+ },
+ "prototype": {
+ "!stdProto": "String",
+ "length": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en/docs/JavaScript/Reference/Global_Objects/String/length",
+ "!doc": "Represents the length of a string."
+ },
+ "<i>": "string",
+ "charAt": {
+ "!type": "fn(i: number) -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/charAt",
+ "!doc": "Returns the specified character from a string."
+ },
+ "charCodeAt": {
+ "!type": "fn(i: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/charCodeAt",
+ "!doc": "Returns the numeric Unicode value of the character at the given index (except for unicode codepoints > 0x10000)."
+ },
+ "indexOf": {
+ "!type": "fn(char: string, from?: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/indexOf",
+ "!doc": "Returns the index within the calling String object of the first occurrence of the specified value, starting the search at fromIndex,\nreturns -1 if the value is not found."
+ },
+ "lastIndexOf": {
+ "!type": "fn(char: string, from?: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/lastIndexOf",
+ "!doc": "Returns the index within the calling String object of the last occurrence of the specified value, or -1 if not found. The calling string is searched backward, starting at fromIndex."
+ },
+ "substring": {
+ "!type": "fn(from: number, to?: number) -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/substring",
+ "!doc": "Returns a subset of a string between one index and another, or through the end of the string."
+ },
+ "substr": {
+ "!type": "fn(from: number, length?: number) -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/substr",
+ "!doc": "Returns the characters in a string beginning at the specified location through the specified number of characters."
+ },
+ "slice": {
+ "!type": "fn(from: number, to?: number) -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/slice",
+ "!doc": "Extracts a section of a string and returns a new string."
+ },
+ "trim": {
+ "!type": "fn() -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/Trim",
+ "!doc": "Removes whitespace from both ends of the string."
+ },
+ "trimLeft": {
+ "!type": "fn() -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/TrimLeft",
+ "!doc": "Removes whitespace from the left end of the string."
+ },
+ "trimRight": {
+ "!type": "fn() -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/TrimRight",
+ "!doc": "Removes whitespace from the right end of the string."
+ },
+ "toUpperCase": {
+ "!type": "fn() -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/toUpperCase",
+ "!doc": "Returns the calling string value converted to uppercase."
+ },
+ "toLowerCase": {
+ "!type": "fn() -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/toLowerCase",
+ "!doc": "Returns the calling string value converted to lowercase."
+ },
+ "toLocaleUpperCase": {
+ "!type": "fn() -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/toLocaleUpperCase",
+ "!doc": "Returns the calling string value converted to upper case, according to any locale-specific case mappings."
+ },
+ "toLocaleLowerCase": {
+ "!type": "fn() -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/toLocaleLowerCase",
+ "!doc": "Returns the calling string value converted to lower case, according to any locale-specific case mappings."
+ },
+ "split": {
+ "!type": "fn(pattern: string) -> [string]",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/split",
+ "!doc": "Splits a String object into an array of strings by separating the string into substrings."
+ },
+ "concat": {
+ "!type": "fn(other: string) -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/concat",
+ "!doc": "Combines the text of two or more strings and returns a new string."
+ },
+ "localeCompare": {
+ "!type": "fn(other: string) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/localeCompare",
+ "!doc": "Returns a number indicating whether a reference string comes before or after or is the same as the given string in sort order."
+ },
+ "match": {
+ "!type": "fn(pattern: +RegExp) -> [string]",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/match",
+ "!doc": "Used to retrieve the matches when matching a string against a regular expression."
+ },
+ "replace": {
+ "!type": "fn(pattern: +RegExp, replacement: string) -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/replace",
+ "!doc": "Returns a new string with some or all matches of a pattern replaced by a replacement. The pattern can be a string or a RegExp, and the replacement can be a string or a function to be called for each match."
+ },
+ "search": {
+ "!type": "fn(pattern: +RegExp) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/search",
+ "!doc": "Executes the search for a match between a regular expression and this String object."
+ }
+ },
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String",
+ "!doc": "The String global object is a constructor for strings, or a sequence of characters."
+ },
+ "Number": {
+ "!type": "fn(value: ?) -> number",
+ "MAX_VALUE": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Number/MAX_VALUE",
+ "!doc": "The maximum numeric value representable in JavaScript."
+ },
+ "MIN_VALUE": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Number/MIN_VALUE",
+ "!doc": "The smallest positive numeric value representable in JavaScript."
+ },
+ "POSITIVE_INFINITY": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Number/POSITIVE_INFINITY",
+ "!doc": "A value representing the positive Infinity value."
+ },
+ "NEGATIVE_INFINITY": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Number/NEGATIVE_INFINITY",
+ "!doc": "A value representing the negative Infinity value."
+ },
+ "prototype": {
+ "!stdProto": "Number",
+ "toString": {
+ "!type": "fn(radix?: number) -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Number/toString",
+ "!doc": "Returns a string representing the specified Number object"
+ },
+ "toFixed": {
+ "!type": "fn(digits: number) -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Number/toFixed",
+ "!doc": "Formats a number using fixed-point notation"
+ },
+ "toExponential": {
+ "!type": "fn(digits: number) -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Number/toExponential",
+ "!doc": "Returns a string representing the Number object in exponential notation"
+ }
+ },
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Number",
+ "!doc": "The Number JavaScript object is a wrapper object allowing you to work with numerical values. A Number object is created using the Number() constructor."
+ },
+ "Boolean": {
+ "!type": "fn(value: ?) -> bool",
+ "prototype": {
+ "!stdProto": "Boolean"
+ },
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Boolean",
+ "!doc": "The Boolean object is an object wrapper for a boolean value."
+ },
+ "RegExp": {
+ "!type": "fn(source: string, flags?: string)",
+ "prototype": {
+ "!stdProto": "RegExp",
+ "exec": {
+ "!type": "fn(input: string) -> [string]",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/RegExp/exec",
+ "!doc": "Executes a search for a match in a specified string. Returns a result array, or null."
+ },
+ "compile": {
+ "!type": "fn(source: string, flags?: string)",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/RegExp",
+ "!doc": "Creates a regular expression object for matching text with a pattern."
+ },
+ "test": {
+ "!type": "fn(input: string) -> bool",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/RegExp/test",
+ "!doc": "Executes the search for a match between a regular expression and a specified string. Returns true or false."
+ },
+ "global": {
+ "!type": "bool",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/RegExp",
+ "!doc": "Creates a regular expression object for matching text with a pattern."
+ },
+ "ignoreCase": {
+ "!type": "bool",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/RegExp",
+ "!doc": "Creates a regular expression object for matching text with a pattern."
+ },
+ "multiline": {
+ "!type": "bool",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/RegExp/multiline",
+ "!doc": "Reflects whether or not to search in strings across multiple lines.\n"
+ },
+ "source": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/RegExp/source",
+ "!doc": "A read-only property that contains the text of the pattern, excluding the forward slashes.\n"
+ },
+ "lastIndex": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/RegExp/lastIndex",
+ "!doc": "A read/write integer property that specifies the index at which to start the next match."
+ }
+ },
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/RegExp",
+ "!doc": "Creates a regular expression object for matching text with a pattern."
+ },
+ "Date": {
+ "!type": "fn(ms: number)",
+ "parse": {
+ "!type": "fn(source: string) -> +Date",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/parse",
+ "!doc": "Parses a string representation of a date, and returns the number of milliseconds since January 1, 1970, 00:00:00 UTC."
+ },
+ "UTC": {
+ "!type": "fn(year: number, month: number, date: number, hour?: number, min?: number, sec?: number, ms?: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/UTC",
+ "!doc": "Accepts the same parameters as the longest form of the constructor, and returns the number of milliseconds in a Date object since January 1, 1970, 00:00:00, universal time."
+ },
+ "now": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/now",
+ "!doc": "Returns the number of milliseconds elapsed since 1 January 1970 00:00:00 UTC."
+ },
+ "prototype": {
+ "toUTCString": {
+ "!type": "fn() -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/toUTCString",
+ "!doc": "Converts a date to a string, using the universal time convention."
+ },
+ "toISOString": {
+ "!type": "fn() -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/toISOString",
+ "!doc": "JavaScript provides a direct way to convert a date object into a string in ISO format, the ISO 8601 Extended Format."
+ },
+ "toDateString": {
+ "!type": "fn() -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/toDateString",
+ "!doc": "Returns the date portion of a Date object in human readable form in American English."
+ },
+ "toTimeString": {
+ "!type": "fn() -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/toTimeString",
+ "!doc": "Returns the time portion of a Date object in human readable form in American English."
+ },
+ "toLocaleDateString": {
+ "!type": "fn() -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/toLocaleDateString",
+ "!doc": "Converts a date to a string, returning the \"date\" portion using the operating system's locale's conventions.\n"
+ },
+ "toLocaleTimeString": {
+ "!type": "fn() -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/toLocaleTimeString",
+ "!doc": "Converts a date to a string, returning the \"time\" portion using the current locale's conventions."
+ },
+ "getTime": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getTime",
+ "!doc": "Returns the numeric value corresponding to the time for the specified date according to universal time."
+ },
+ "getFullYear": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getFullYear",
+ "!doc": "Returns the year of the specified date according to local time."
+ },
+ "getYear": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getYear",
+ "!doc": "Returns the year in the specified date according to local time."
+ },
+ "getMonth": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getMonth",
+ "!doc": "Returns the month in the specified date according to local time."
+ },
+ "getUTCMonth": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getUTCMonth",
+ "!doc": "Returns the month of the specified date according to universal time.\n"
+ },
+ "getDate": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getDate",
+ "!doc": "Returns the day of the month for the specified date according to local time."
+ },
+ "getUTCDate": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getUTCDate",
+ "!doc": "Returns the day (date) of the month in the specified date according to universal time.\n"
+ },
+ "getDay": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getDay",
+ "!doc": "Returns the day of the week for the specified date according to local time."
+ },
+ "getUTCDay": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getUTCDay",
+ "!doc": "Returns the day of the week in the specified date according to universal time.\n"
+ },
+ "getHours": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getHours",
+ "!doc": "Returns the hour for the specified date according to local time."
+ },
+ "getUTCHours": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getUTCHours",
+ "!doc": "Returns the hours in the specified date according to universal time.\n"
+ },
+ "getMinutes": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getMinutes",
+ "!doc": "Returns the minutes in the specified date according to local time."
+ },
+ "getUTCMinutes": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date",
+ "!doc": "Creates JavaScript Date instances which let you work with dates and times."
+ },
+ "getSeconds": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getSeconds",
+ "!doc": "Returns the seconds in the specified date according to local time."
+ },
+ "getUTCSeconds": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getUTCSeconds",
+ "!doc": "Returns the seconds in the specified date according to universal time.\n"
+ },
+ "getMilliseconds": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getMilliseconds",
+ "!doc": "Returns the milliseconds in the specified date according to local time."
+ },
+ "getUTCMilliseconds": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getUTCMilliseconds",
+ "!doc": "Returns the milliseconds in the specified date according to universal time.\n"
+ },
+ "getTimezoneOffset": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/getTimezoneOffset",
+ "!doc": "Returns the time-zone offset from UTC, in minutes, for the current locale."
+ },
+ "setTime": {
+ "!type": "fn(date: +Date) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/setTime",
+ "!doc": "Sets the Date object to the time represented by a number of milliseconds since January 1, 1970, 00:00:00 UTC.\n"
+ },
+ "setFullYear": {
+ "!type": "fn(year: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/setFullYear",
+ "!doc": "Sets the full year for a specified date according to local time.\n"
+ },
+ "setUTCFullYear": {
+ "!type": "fn(year: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/setUTCFullYear",
+ "!doc": "Sets the full year for a specified date according to universal time.\n"
+ },
+ "setMonth": {
+ "!type": "fn(month: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/setMonth",
+ "!doc": "Set the month for a specified date according to local time."
+ },
+ "setUTCMonth": {
+ "!type": "fn(month: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/setUTCMonth",
+ "!doc": "Sets the month for a specified date according to universal time.\n"
+ },
+ "setDate": {
+ "!type": "fn(day: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/setDate",
+ "!doc": "Sets the day of the month for a specified date according to local time."
+ },
+ "setUTCDate": {
+ "!type": "fn(day: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/setUTCDate",
+ "!doc": "Sets the day of the month for a specified date according to universal time.\n"
+ },
+ "setHours": {
+ "!type": "fn(hour: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/setHours",
+ "!doc": "Sets the hours for a specified date according to local time, and returns the number of milliseconds since 1 January 1970 00:00:00 UTC until the time represented by the updated Date instance."
+ },
+ "setUTCHours": {
+ "!type": "fn(hour: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/setUTCHours",
+ "!doc": "Sets the hour for a specified date according to universal time.\n"
+ },
+ "setMinutes": {
+ "!type": "fn(min: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/setMinutes",
+ "!doc": "Sets the minutes for a specified date according to local time."
+ },
+ "setUTCMinutes": {
+ "!type": "fn(min: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/setUTCMinutes",
+ "!doc": "Sets the minutes for a specified date according to universal time.\n"
+ },
+ "setSeconds": {
+ "!type": "fn(sec: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/setSeconds",
+ "!doc": "Sets the seconds for a specified date according to local time."
+ },
+ "setUTCSeconds": {
+ "!type": "fn(sec: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/setUTCSeconds",
+ "!doc": "Sets the seconds for a specified date according to universal time.\n"
+ },
+ "setMilliseconds": {
+ "!type": "fn(ms: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/setMilliseconds",
+ "!doc": "Sets the milliseconds for a specified date according to local time.\n"
+ },
+ "setUTCMilliseconds": {
+ "!type": "fn(ms: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date/setUTCMilliseconds",
+ "!doc": "Sets the milliseconds for a specified date according to universal time.\n"
+ }
+ },
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Date",
+ "!doc": "Creates JavaScript Date instances which let you work with dates and times."
+ },
+ "Error": {
+ "!type": "fn(message: string)",
+ "prototype": {
+ "name": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Error/name",
+ "!doc": "A name for the type of error."
+ },
+ "message": {
+ "!type": "string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Error/message",
+ "!doc": "A human-readable description of the error."
+ }
+ },
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Error",
+ "!doc": "Creates an error object."
+ },
+ "SyntaxError": {
+ "!type": "fn(message: string)",
+ "prototype": "Error.prototype",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/SyntaxError",
+ "!doc": "Represents an error when trying to interpret syntactically invalid code."
+ },
+ "ReferenceError": {
+ "!type": "fn(message: string)",
+ "prototype": "Error.prototype",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/ReferenceError",
+ "!doc": "Represents an error when a non-existent variable is referenced."
+ },
+ "URIError": {
+ "!type": "fn(message: string)",
+ "prototype": "Error.prototype",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/URIError",
+ "!doc": "Represents an error when a malformed URI is encountered."
+ },
+ "EvalError": {
+ "!type": "fn(message: string)",
+ "prototype": "Error.prototype",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/EvalError",
+ "!doc": "Represents an error regarding the eval function."
+ },
+ "RangeError": {
+ "!type": "fn(message: string)",
+ "prototype": "Error.prototype",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/RangeError",
+ "!doc": "Represents an error when a number is not within the correct range allowed."
+ },
+ "parseInt": {
+ "!type": "fn(string: string, radix?: number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/parseInt",
+ "!doc": "Parses a string argument and returns an integer of the specified radix or base."
+ },
+ "parseFloat": {
+ "!type": "fn(string: string) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/parseFloat",
+ "!doc": "Parses a string argument and returns a floating point number."
+ },
+ "isNaN": {
+ "!type": "fn(value: number) -> bool",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/isNaN",
+ "!doc": "Determines whether a value is NaN or not. Be careful, this function is broken. You may be interested in ECMAScript 6 Number.isNaN."
+ },
+ "eval": {
+ "!type": "fn(code: string) -> ?",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/eval",
+ "!doc": "Evaluates JavaScript code represented as a string."
+ },
+ "encodeURI": {
+ "!type": "fn(uri: string) -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/encodeURI",
+ "!doc": "Encodes a Uniform Resource Identifier (URI) by replacing each instance of certain characters by one, two, three, or four escape sequences representing the UTF-8 encoding of the character (will only be four escape sequences for characters composed of two \"surrogate\" characters)."
+ },
+ "encodeURIComponent": {
+ "!type": "fn(uri: string) -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/encodeURIComponent",
+ "!doc": "Encodes a Uniform Resource Identifier (URI) component by replacing each instance of certain characters by one, two, three, or four escape sequences representing the UTF-8 encoding of the character (will only be four escape sequences for characters composed of two \"surrogate\" characters)."
+ },
+ "decodeURI": {
+ "!type": "fn(uri: string) -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/decodeURI",
+ "!doc": "Decodes a Uniform Resource Identifier (URI) previously created by encodeURI or by a similar routine."
+ },
+ "decodeURIComponent": {
+ "!type": "fn(uri: string) -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/decodeURIComponent",
+ "!doc": "Decodes a Uniform Resource Identifier (URI) component previously created by encodeURIComponent or by a similar routine."
+ },
+ "Math": {
+ "E": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/E",
+ "!doc": "The base of natural logarithms, e, approximately 2.718."
+ },
+ "LN2": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/LN2",
+ "!doc": "The natural logarithm of 2, approximately 0.693."
+ },
+ "LN10": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/LN10",
+ "!doc": "The natural logarithm of 10, approximately 2.302."
+ },
+ "LOG2E": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/LOG2E",
+ "!doc": "The base 2 logarithm of E (approximately 1.442)."
+ },
+ "LOG10E": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/LOG10E",
+ "!doc": "The base 10 logarithm of E (approximately 0.434)."
+ },
+ "SQRT1_2": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/SQRT1_2",
+ "!doc": "The square root of 1/2; equivalently, 1 over the square root of 2, approximately 0.707."
+ },
+ "SQRT2": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/SQRT2",
+ "!doc": "The square root of 2, approximately 1.414."
+ },
+ "PI": {
+ "!type": "number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/PI",
+ "!doc": "The ratio of the circumference of a circle to its diameter, approximately 3.14159."
+ },
+ "abs": {
+ "!type": "fn(number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/abs",
+ "!doc": "Returns the absolute value of a number."
+ },
+ "cos": {
+ "!type": "fn(number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/cos",
+ "!doc": "Returns the cosine of a number."
+ },
+ "sin": {
+ "!type": "fn(number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/sin",
+ "!doc": "Returns the sine of a number."
+ },
+ "tan": {
+ "!type": "fn(number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/tan",
+ "!doc": "Returns the tangent of a number."
+ },
+ "acos": {
+ "!type": "fn(number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/acos",
+ "!doc": "Returns the arccosine (in radians) of a number."
+ },
+ "asin": {
+ "!type": "fn(number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/asin",
+ "!doc": "Returns the arcsine (in radians) of a number."
+ },
+ "atan": {
+ "!type": "fn(number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/atan",
+ "!doc": "Returns the arctangent (in radians) of a number."
+ },
+ "atan2": {
+ "!type": "fn(number, number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/atan2",
+ "!doc": "Returns the arctangent of the quotient of its arguments."
+ },
+ "ceil": {
+ "!type": "fn(number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/ceil",
+ "!doc": "Returns the smallest integer greater than or equal to a number."
+ },
+ "floor": {
+ "!type": "fn(number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/floor",
+ "!doc": "Returns the largest integer less than or equal to a number."
+ },
+ "round": {
+ "!type": "fn(number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/round",
+ "!doc": "Returns the value of a number rounded to the nearest integer."
+ },
+ "exp": {
+ "!type": "fn(number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/exp",
+ "!doc": "Returns Ex, where x is the argument, and E is Euler's constant, the base of the natural logarithms."
+ },
+ "log": {
+ "!type": "fn(number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/log",
+ "!doc": "Returns the natural logarithm (base E) of a number."
+ },
+ "sqrt": {
+ "!type": "fn(number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/sqrt",
+ "!doc": "Returns the square root of a number."
+ },
+ "pow": {
+ "!type": "fn(number, number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/pow",
+ "!doc": "Returns base to the exponent power, that is, baseexponent."
+ },
+ "max": {
+ "!type": "fn(number, number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/max",
+ "!doc": "Returns the largest of zero or more numbers."
+ },
+ "min": {
+ "!type": "fn(number, number) -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/min",
+ "!doc": "Returns the smallest of zero or more numbers."
+ },
+ "random": {
+ "!type": "fn() -> number",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/random",
+ "!doc": "Returns a floating-point, pseudo-random number in the range [0, 1) that is, from 0 (inclusive) up to but not including 1 (exclusive), which you can then scale to your desired range."
+ },
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math",
+ "!doc": "A built-in object that has properties and methods for mathematical constants and functions."
+ },
+ "JSON": {
+ "parse": {
+ "!type": "fn(json: string) -> ?",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/JSON/parse",
+ "!doc": "Parse a string as JSON, optionally transforming the value produced by parsing."
+ },
+ "stringify": {
+ "!type": "fn(value: ?) -> string",
+ "!url": "https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/JSON/stringify",
+ "!doc": "Convert a value to JSON, optionally replacing values if a replacer function is specified, or optionally including only the specified properties if a replacer array is specified."
+ },
+ "!url": "https://developer.mozilla.org/en-US/docs/JSON",
+ "!doc": "JSON (JavaScript Object Notation) is a data-interchange format. It closely resembles a subset of JavaScript syntax, although it is not a strict subset. (See JSON in the JavaScript Reference for full details.) It is useful when writing any kind of JavaScript-based application, including websites and browser extensions. For example, you might store user information in JSON format in a cookie, or you might store extension preferences in JSON in a string-valued browser preference."
+ }
+}
diff --git a/devtools/client/sourceeditor/tern/infer.js b/devtools/client/sourceeditor/tern/infer.js
new file mode 100755
index 000000000..94f0a9518
--- /dev/null
+++ b/devtools/client/sourceeditor/tern/infer.js
@@ -0,0 +1,2119 @@
+// Main type inference engine
+
+// Walks an AST, building up a graph of abstract values and constraints
+// that cause types to flow from one node to another. Also defines a
+// number of utilities for accessing ASTs and scopes.
+
+// Analysis is done in a context, which is tracked by the dynamically
+// bound cx variable. Use withContext to set the current context.
+
+// For memory-saving reasons, individual types export an interface
+// similar to abstract values (which can hold multiple types), and can
+// thus be used in place abstract values that only ever contain a
+// single type.
+
+(function(root, mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ return mod(exports, require("acorn/acorn"), require("acorn/acorn_loose"), require("acorn/walk"),
+ require("./def"), require("./signal"));
+ if (typeof define == "function" && define.amd) // AMD
+ return define(["exports", "acorn/acorn", "acorn/acorn_loose", "acorn/walk", "./def", "./signal"], mod);
+ mod(root.tern || (root.tern = {}), acorn, acorn, acorn.walk, tern.def, tern.signal); // Plain browser env
+})(this, function(exports, acorn, acorn_loose, walk, def, signal) {
+ "use strict";
+
+ var toString = exports.toString = function(type, maxDepth, parent) {
+ if (!type || type == parent || maxDepth && maxDepth < -3) return "?";
+ return type.toString(maxDepth, parent);
+ };
+
+ // A variant of AVal used for unknown, dead-end values. Also serves
+ // as prototype for AVals, Types, and Constraints because it
+ // implements 'empty' versions of all the methods that the code
+ // expects.
+ var ANull = exports.ANull = signal.mixin({
+ addType: function() {},
+ propagate: function() {},
+ getProp: function() { return ANull; },
+ forAllProps: function() {},
+ hasType: function() { return false; },
+ isEmpty: function() { return true; },
+ getFunctionType: function() {},
+ getObjType: function() {},
+ getSymbolType: function() {},
+ getType: function() {},
+ gatherProperties: function() {},
+ propagatesTo: function() {},
+ typeHint: function() {},
+ propHint: function() {},
+ toString: function() { return "?"; }
+ });
+
+ function extend(proto, props) {
+ var obj = Object.create(proto);
+ if (props) for (var prop in props) obj[prop] = props[prop];
+ return obj;
+ }
+
+ // ABSTRACT VALUES
+
+ var WG_DEFAULT = 100, WG_NEW_INSTANCE = 90, WG_MADEUP_PROTO = 10,
+ WG_MULTI_MEMBER = 6, WG_CATCH_ERROR = 6,
+ WG_PHANTOM_OBJ = 1,
+ WG_GLOBAL_THIS = 90, WG_SPECULATIVE_THIS = 2, WG_SPECULATIVE_PROTO_THIS = 4;
+
+ var AVal = exports.AVal = function() {
+ this.types = [];
+ this.forward = null;
+ this.maxWeight = 0;
+ };
+ AVal.prototype = extend(ANull, {
+ addType: function(type, weight) {
+ weight = weight || WG_DEFAULT;
+ if (this.maxWeight < weight) {
+ this.maxWeight = weight;
+ if (this.types.length == 1 && this.types[0] == type) return;
+ this.types.length = 0;
+ } else if (this.maxWeight > weight || this.types.indexOf(type) > -1) {
+ return;
+ }
+
+ this.signal("addType", type);
+ this.types.push(type);
+ var forward = this.forward;
+ if (forward) withWorklist(function(add) {
+ for (var i = 0; i < forward.length; ++i) add(type, forward[i], weight);
+ });
+ },
+
+ propagate: function(target, weight) {
+ if (target == ANull || (target instanceof Type && this.forward && this.forward.length > 2)) return;
+ if (weight && weight != WG_DEFAULT) target = new Muffle(target, weight);
+ (this.forward || (this.forward = [])).push(target);
+ var types = this.types;
+ if (types.length) withWorklist(function(add) {
+ for (var i = 0; i < types.length; ++i) add(types[i], target, weight);
+ });
+ },
+
+ getProp: function(prop) {
+ if (prop == "__proto__" || prop == "✖") return ANull;
+ var found = (this.props || (this.props = Object.create(null)))[prop];
+ if (!found) {
+ found = this.props[prop] = new AVal;
+ this.propagate(new GetProp(prop, found));
+ }
+ return found;
+ },
+
+ forAllProps: function(c) {
+ this.propagate(new ForAllProps(c));
+ },
+
+ hasType: function(type) {
+ return this.types.indexOf(type) > -1;
+ },
+ isEmpty: function() { return this.types.length === 0; },
+ getFunctionType: function() {
+ for (var i = this.types.length - 1; i >= 0; --i)
+ if (this.types[i] instanceof Fn) return this.types[i];
+ },
+ getObjType: function() {
+ var seen = null;
+ for (var i = this.types.length - 1; i >= 0; --i) {
+ var type = this.types[i];
+ if (!(type instanceof Obj)) continue;
+ if (type.name) return type;
+ if (!seen) seen = type;
+ }
+ return seen;
+ },
+
+ getSymbolType: function() {
+ for (var i = this.types.length - 1; i >= 0; --i)
+ if (this.types[i] instanceof Sym) return this.types[i]
+ },
+
+ getType: function(guess) {
+ if (this.types.length === 0 && guess !== false) return this.makeupType();
+ if (this.types.length === 1) return this.types[0];
+ return canonicalType(this.types);
+ },
+
+ toString: function(maxDepth, parent) {
+ if (this.types.length == 0) return toString(this.makeupType(), maxDepth, parent);
+ if (this.types.length == 1) return toString(this.types[0], maxDepth, parent);
+ var simplified = simplifyTypes(this.types);
+ if (simplified.length > 2) return "?";
+ return simplified.map(function(tp) { return toString(tp, maxDepth, parent); }).join("|");
+ },
+
+ makeupPropType: function(obj) {
+ var propName = this.propertyName;
+
+ var protoProp = obj.proto && obj.proto.hasProp(propName);
+ if (protoProp) {
+ var fromProto = protoProp.getType();
+ if (fromProto) return fromProto;
+ }
+
+ if (propName != "<i>") {
+ var computedProp = obj.hasProp("<i>");
+ if (computedProp) return computedProp.getType();
+ } else if (obj.props["<i>"] != this) {
+ for (var prop in obj.props) {
+ var val = obj.props[prop];
+ if (!val.isEmpty()) return val.getType();
+ }
+ }
+ },
+
+ makeupType: function() {
+ var computed = this.propertyOf && this.makeupPropType(this.propertyOf);
+ if (computed) return computed;
+
+ if (!this.forward) return null;
+ for (var i = this.forward.length - 1; i >= 0; --i) {
+ var hint = this.forward[i].typeHint();
+ if (hint && !hint.isEmpty()) {guessing = true; return hint;}
+ }
+
+ var props = Object.create(null), foundProp = null;
+ for (var i = 0; i < this.forward.length; ++i) {
+ var prop = this.forward[i].propHint();
+ if (prop && prop != "length" && prop != "<i>" && prop != "✖" && prop != cx.completingProperty) {
+ props[prop] = true;
+ foundProp = prop;
+ }
+ }
+ if (!foundProp) return null;
+
+ var objs = objsWithProp(foundProp);
+ if (objs) {
+ var matches = [];
+ search: for (var i = 0; i < objs.length; ++i) {
+ var obj = objs[i];
+ for (var prop in props) if (!obj.hasProp(prop)) continue search;
+ if (obj.hasCtor) obj = getInstance(obj);
+ matches.push(obj);
+ }
+ var canon = canonicalType(matches);
+ if (canon) {guessing = true; return canon;}
+ }
+ },
+
+ typeHint: function() { return this.types.length ? this.getType() : null; },
+ propagatesTo: function() { return this; },
+
+ gatherProperties: function(f, depth) {
+ for (var i = 0; i < this.types.length; ++i)
+ this.types[i].gatherProperties(f, depth);
+ },
+
+ guessProperties: function(f) {
+ if (this.forward) for (var i = 0; i < this.forward.length; ++i) {
+ var prop = this.forward[i].propHint();
+ if (prop) f(prop, null, 0);
+ }
+ var guessed = this.makeupType();
+ if (guessed) guessed.gatherProperties(f);
+ }
+ });
+
+ function similarAVal(a, b, depth) {
+ var typeA = a.getType(false), typeB = b.getType(false);
+ if (!typeA || !typeB) return true;
+ return similarType(typeA, typeB, depth);
+ }
+
+ function similarType(a, b, depth) {
+ if (!a || depth >= 5) return b;
+ if (!a || a == b) return a;
+ if (!b) return a;
+ if (a.constructor != b.constructor) return false;
+ if (a.constructor == Arr) {
+ var innerA = a.getProp("<i>").getType(false);
+ if (!innerA) return b;
+ var innerB = b.getProp("<i>").getType(false);
+ if (!innerB || similarType(innerA, innerB, depth + 1)) return b;
+ } else if (a.constructor == Obj) {
+ var propsA = 0, propsB = 0, same = 0;
+ for (var prop in a.props) {
+ propsA++;
+ if (prop in b.props && similarAVal(a.props[prop], b.props[prop], depth + 1))
+ same++;
+ }
+ for (var prop in b.props) propsB++;
+ if (propsA && propsB && same < Math.max(propsA, propsB) / 2) return false;
+ return propsA > propsB ? a : b;
+ } else if (a.constructor == Fn) {
+ if (a.args.length != b.args.length ||
+ !a.args.every(function(tp, i) { return similarAVal(tp, b.args[i], depth + 1); }) ||
+ !similarAVal(a.retval, b.retval, depth + 1) || !similarAVal(a.self, b.self, depth + 1))
+ return false;
+ return a;
+ } else {
+ return false;
+ }
+ }
+
+ var simplifyTypes = exports.simplifyTypes = function(types) {
+ var found = [];
+ outer: for (var i = 0; i < types.length; ++i) {
+ var tp = types[i];
+ for (var j = 0; j < found.length; j++) {
+ var similar = similarType(tp, found[j], 0);
+ if (similar) {
+ found[j] = similar;
+ continue outer;
+ }
+ }
+ found.push(tp);
+ }
+ return found;
+ };
+
+ function canonicalType(types) {
+ var arrays = 0, fns = 0, objs = 0, prim = null;
+ for (var i = 0; i < types.length; ++i) {
+ var tp = types[i];
+ if (tp instanceof Arr) ++arrays;
+ else if (tp instanceof Fn) ++fns;
+ else if (tp instanceof Obj) ++objs;
+ else if (tp instanceof Prim) {
+ if (prim && tp.name != prim.name) return null;
+ prim = tp;
+ }
+ }
+ var kinds = (arrays && 1) + (fns && 1) + (objs && 1) + (prim && 1);
+ if (kinds > 1) return null;
+ if (prim) return prim;
+
+ var maxScore = 0, maxTp = null;
+ for (var i = 0; i < types.length; ++i) {
+ var tp = types[i], score = 0;
+ if (arrays) {
+ score = tp.getProp("<i>").isEmpty() ? 1 : 2;
+ } else if (fns) {
+ score = 1;
+ for (var j = 0; j < tp.args.length; ++j) if (!tp.args[j].isEmpty()) ++score;
+ if (!tp.retval.isEmpty()) ++score;
+ } else if (objs) {
+ score = tp.name ? 100 : 2;
+ }
+ if (score >= maxScore) { maxScore = score; maxTp = tp; }
+ }
+ return maxTp;
+ }
+
+ // PROPAGATION STRATEGIES
+
+ var constraint = exports.constraint = function(methods) {
+ var ctor = function() {
+ this.origin = cx.curOrigin;
+ this.construct.apply(this, arguments);
+ };
+ ctor.prototype = Object.create(ANull);
+ for (var m in methods) if (methods.hasOwnProperty(m)) ctor.prototype[m] = methods[m];
+ return ctor;
+ };
+
+ var GetProp = constraint({
+ construct: function(prop, target) {
+ this.prop = prop; this.target = target;
+ },
+ addType: function(type, weight) {
+ if (type.getProp)
+ type.getProp(this.prop).propagate(this.target, weight);
+ },
+ propHint: function() { return this.prop; },
+ propagatesTo: function() {
+ if (this.prop == "<i>" || !/[^\w_]/.test(this.prop))
+ return {target: this.target, pathExt: "." + this.prop};
+ }
+ });
+
+ var DefProp = exports.PropHasSubset = exports.DefProp = constraint({
+ construct: function(prop, type, originNode) {
+ this.prop = prop; this.type = type; this.originNode = originNode;
+ },
+ addType: function(type, weight) {
+ if (!(type instanceof Obj)) return;
+ var prop = type.defProp(this.prop, this.originNode);
+ if (!prop.origin) prop.origin = this.origin;
+ this.type.propagate(prop, weight);
+ },
+ propHint: function() { return this.prop; }
+ });
+
+ var ForAllProps = constraint({
+ construct: function(c) { this.c = c; },
+ addType: function(type) {
+ if (!(type instanceof Obj)) return;
+ type.forAllProps(this.c);
+ }
+ });
+
+ function withDisabledComputing(fn, body) {
+ cx.disabledComputing = {fn: fn, prev: cx.disabledComputing};
+ var result = body();
+ cx.disabledComputing = cx.disabledComputing.prev;
+ return result;
+ }
+ var IsCallee = exports.IsCallee = constraint({
+ construct: function(self, args, argNodes, retval) {
+ this.self = self; this.args = args; this.argNodes = argNodes; this.retval = retval;
+ this.disabled = cx.disabledComputing;
+ },
+ addType: function(fn, weight) {
+ if (!(fn instanceof Fn)) return;
+ for (var i = 0; i < this.args.length; ++i) {
+ if (i < fn.args.length) this.args[i].propagate(fn.args[i], weight);
+ if (fn.arguments) this.args[i].propagate(fn.arguments, weight);
+ }
+ this.self.propagate(fn.self, this.self == cx.topScope ? WG_GLOBAL_THIS : weight);
+ var compute = fn.computeRet, result = fn.retval
+ if (compute) for (var d = this.disabled; d; d = d.prev)
+ if (d.fn == fn || fn.originNode && d.fn.originNode == fn.originNode) compute = null;
+ if (compute) {
+ var old = cx.disabledComputing;
+ cx.disabledComputing = this.disabled;
+ result = compute(this.self, this.args, this.argNodes)
+ cx.disabledComputing = old;
+ }
+ maybeIterator(fn, result).propagate(this.retval, weight)
+ },
+ typeHint: function() {
+ var names = [];
+ for (var i = 0; i < this.args.length; ++i) names.push("?");
+ return new Fn(null, this.self, this.args, names, ANull);
+ },
+ propagatesTo: function() {
+ return {target: this.retval, pathExt: ".!ret"};
+ }
+ });
+
+ var HasMethodCall = constraint({
+ construct: function(propName, args, argNodes, retval) {
+ this.propName = propName; this.args = args; this.argNodes = argNodes; this.retval = retval;
+ this.disabled = cx.disabledComputing;
+ },
+ addType: function(obj, weight) {
+ var callee = new IsCallee(obj, this.args, this.argNodes, this.retval);
+ callee.disabled = this.disabled;
+ obj.getProp(this.propName).propagate(callee, weight);
+ },
+ propHint: function() { return this.propName; }
+ });
+
+ var IsCtor = exports.IsCtor = constraint({
+ construct: function(target, noReuse) {
+ this.target = target; this.noReuse = noReuse;
+ },
+ addType: function(f, weight) {
+ if (!(f instanceof Fn)) return;
+ if (cx.parent && !cx.parent.options.reuseInstances) this.noReuse = true;
+ f.getProp("prototype").propagate(new IsProto(this.noReuse ? false : f, this.target), weight);
+ }
+ });
+
+ var getInstance = exports.getInstance = function(obj, ctor) {
+ if (ctor === false) return new Obj(obj);
+
+ if (!ctor) ctor = obj.hasCtor;
+ if (!obj.instances) obj.instances = [];
+ for (var i = 0; i < obj.instances.length; ++i) {
+ var cur = obj.instances[i];
+ if (cur.ctor == ctor) return cur.instance;
+ }
+ var instance = new Obj(obj, ctor && ctor.name);
+ instance.origin = obj.origin;
+ obj.instances.push({ctor: ctor, instance: instance});
+ return instance;
+ };
+
+ var IsProto = exports.IsProto = constraint({
+ construct: function(ctor, target) {
+ this.ctor = ctor; this.target = target;
+ },
+ addType: function(o, _weight) {
+ if (!(o instanceof Obj)) return;
+ if ((this.count = (this.count || 0) + 1) > 8) return;
+ if (o == cx.protos.Array)
+ this.target.addType(new Arr);
+ else
+ this.target.addType(getInstance(o, this.ctor));
+ }
+ });
+
+ var FnPrototype = constraint({
+ construct: function(fn) { this.fn = fn; },
+ addType: function(o, _weight) {
+ if (o instanceof Obj && !o.hasCtor) {
+ o.hasCtor = this.fn;
+ var adder = new SpeculativeThis(o, this.fn);
+ adder.addType(this.fn);
+ o.forAllProps(function(_prop, val, local) {
+ if (local) val.propagate(adder);
+ });
+ }
+ }
+ });
+
+ var IsAdded = constraint({
+ construct: function(other, target) {
+ this.other = other; this.target = target;
+ },
+ addType: function(type, weight) {
+ if (type == cx.str)
+ this.target.addType(cx.str, weight);
+ else if (type == cx.num && this.other.hasType(cx.num))
+ this.target.addType(cx.num, weight);
+ },
+ typeHint: function() { return this.other; }
+ });
+
+ var IfObj = exports.IfObj = constraint({
+ construct: function(target) { this.target = target; },
+ addType: function(t, weight) {
+ if (t instanceof Obj) this.target.addType(t, weight);
+ },
+ propagatesTo: function() { return this.target; }
+ });
+
+ var SpeculativeThis = constraint({
+ construct: function(obj, ctor) { this.obj = obj; this.ctor = ctor; },
+ addType: function(tp) {
+ if (tp instanceof Fn && tp.self)
+ tp.self.addType(getInstance(this.obj, this.ctor), WG_SPECULATIVE_PROTO_THIS);
+ }
+ });
+
+ var HasProto = constraint({
+ construct: function(obj) { this.obj = obj },
+ addType: function(tp) {
+ if (tp instanceof Obj && this.obj.proto == cx.protos.Object)
+ this.obj.replaceProto(tp)
+ }
+ });
+
+ var Muffle = constraint({
+ construct: function(inner, weight) {
+ this.inner = inner; this.weight = weight;
+ },
+ addType: function(tp, weight) {
+ this.inner.addType(tp, Math.min(weight, this.weight));
+ },
+ propagatesTo: function() { return this.inner.propagatesTo(); },
+ typeHint: function() { return this.inner.typeHint(); },
+ propHint: function() { return this.inner.propHint(); }
+ });
+
+ // TYPE OBJECTS
+
+ var Type = exports.Type = function() {};
+ Type.prototype = extend(ANull, {
+ constructor: Type,
+ propagate: function(c, w) { c.addType(this, w); },
+ hasType: function(other) { return other == this; },
+ isEmpty: function() { return false; },
+ typeHint: function() { return this; },
+ getType: function() { return this; }
+ });
+
+ var Prim = exports.Prim = function(proto, name) { this.name = name; this.proto = proto; };
+ Prim.prototype = extend(Type.prototype, {
+ constructor: Prim,
+ toString: function() { return this.name; },
+ getProp: function(prop) {return this.proto.hasProp(prop) || ANull;},
+ gatherProperties: function(f, depth) {
+ if (this.proto) this.proto.gatherProperties(f, depth);
+ }
+ });
+
+ function isInteger(str) {
+ var c0 = str.charCodeAt(0)
+ if (c0 >= 48 && c0 <= 57) return !/\D/.test(str)
+ else return false
+ }
+
+ var Obj = exports.Obj = function(proto, name) {
+ if (!this.props) this.props = Object.create(null);
+ this.proto = proto === true ? cx.protos.Object : proto;
+ if (proto && !name && proto.name && !(this instanceof Fn)) {
+ var match = /^(.*)\.prototype$/.exec(this.proto.name);
+ if (match) name = match[1];
+ }
+ this.name = name;
+ this.maybeProps = null;
+ this.origin = cx.curOrigin;
+ };
+ Obj.prototype = extend(Type.prototype, {
+ constructor: Obj,
+ toString: function(maxDepth) {
+ if (maxDepth == null) maxDepth = 0;
+ if (maxDepth <= 0 && this.name) return this.name;
+ var props = [], etc = false;
+ for (var prop in this.props) if (prop != "<i>") {
+ if (props.length > 5) { etc = true; break; }
+ if (maxDepth)
+ props.push(prop + ": " + toString(this.props[prop], maxDepth - 1, this));
+ else
+ props.push(prop);
+ }
+ props.sort();
+ if (etc) props.push("...");
+ return "{" + props.join(", ") + "}";
+ },
+ hasProp: function(prop, searchProto) {
+ if (isInteger(prop)) prop = this.normalizeIntegerProp(prop)
+ var found = this.props[prop];
+ if (searchProto !== false)
+ for (var p = this.proto; p && !found; p = p.proto) found = p.props[prop];
+ return found;
+ },
+ defProp: function(prop, originNode) {
+ var found = this.hasProp(prop, false);
+ if (found) {
+ if (originNode && !found.originNode) found.originNode = originNode;
+ return found;
+ }
+ if (prop == "__proto__" || prop == "✖") return ANull;
+ if (isInteger(prop)) prop = this.normalizeIntegerProp(prop)
+
+ var av = this.maybeProps && this.maybeProps[prop];
+ if (av) {
+ delete this.maybeProps[prop];
+ this.maybeUnregProtoPropHandler();
+ } else {
+ av = new AVal;
+ av.propertyOf = this;
+ av.propertyName = prop;
+ }
+
+ this.props[prop] = av;
+ av.originNode = originNode;
+ av.origin = cx.curOrigin;
+ this.broadcastProp(prop, av, true);
+ return av;
+ },
+ getProp: function(prop) {
+ var found = this.hasProp(prop, true) || (this.maybeProps && this.maybeProps[prop]);
+ if (found) return found;
+ if (prop == "__proto__" || prop == "✖") return ANull;
+ if (isInteger(prop)) prop = this.normalizeIntegerProp(prop)
+ var av = this.ensureMaybeProps()[prop] = new AVal;
+ av.propertyOf = this;
+ av.propertyName = prop;
+ return av;
+ },
+ normalizeIntegerProp: function(_) { return "<i>" },
+ broadcastProp: function(prop, val, local) {
+ if (local) {
+ this.signal("addProp", prop, val);
+ // If this is a scope, it shouldn't be registered
+ if (!(this instanceof Scope)) registerProp(prop, this);
+ }
+
+ if (this.onNewProp) for (var i = 0; i < this.onNewProp.length; ++i) {
+ var h = this.onNewProp[i];
+ h.onProtoProp ? h.onProtoProp(prop, val, local) : h(prop, val, local);
+ }
+ },
+ onProtoProp: function(prop, val, _local) {
+ var maybe = this.maybeProps && this.maybeProps[prop];
+ if (maybe) {
+ delete this.maybeProps[prop];
+ this.maybeUnregProtoPropHandler();
+ this.proto.getProp(prop).propagate(maybe);
+ }
+ this.broadcastProp(prop, val, false);
+ },
+ replaceProto: function(proto) {
+ if (this.proto && this.maybeProps)
+ this.proto.unregPropHandler(this)
+ this.proto = proto
+ if (this.maybeProps)
+ this.proto.forAllProps(this)
+ },
+ ensureMaybeProps: function() {
+ if (!this.maybeProps) {
+ if (this.proto) this.proto.forAllProps(this);
+ this.maybeProps = Object.create(null);
+ }
+ return this.maybeProps;
+ },
+ removeProp: function(prop) {
+ var av = this.props[prop];
+ delete this.props[prop];
+ this.ensureMaybeProps()[prop] = av;
+ av.types.length = 0;
+ },
+ forAllProps: function(c) {
+ if (!this.onNewProp) {
+ this.onNewProp = [];
+ if (this.proto) this.proto.forAllProps(this);
+ }
+ this.onNewProp.push(c);
+ for (var o = this; o; o = o.proto) for (var prop in o.props) {
+ if (c.onProtoProp)
+ c.onProtoProp(prop, o.props[prop], o == this);
+ else
+ c(prop, o.props[prop], o == this);
+ }
+ },
+ maybeUnregProtoPropHandler: function() {
+ if (this.maybeProps) {
+ for (var _n in this.maybeProps) return;
+ this.maybeProps = null;
+ }
+ if (!this.proto || this.onNewProp && this.onNewProp.length) return;
+ this.proto.unregPropHandler(this);
+ },
+ unregPropHandler: function(handler) {
+ for (var i = 0; i < this.onNewProp.length; ++i)
+ if (this.onNewProp[i] == handler) { this.onNewProp.splice(i, 1); break; }
+ this.maybeUnregProtoPropHandler();
+ },
+ gatherProperties: function(f, depth) {
+ for (var prop in this.props) if (prop != "<i>" && prop.charAt(0) != ":")
+ f(prop, this, depth);
+ if (this.proto) this.proto.gatherProperties(f, depth + 1);
+ },
+ getObjType: function() { return this; }
+ });
+
+ var Fn = exports.Fn = function(name, self, args, argNames, retval, generator) {
+ Obj.call(this, cx.protos.Function, name);
+ this.self = self;
+ this.args = args;
+ this.argNames = argNames;
+ this.retval = retval;
+ this.generator = generator
+ };
+ Fn.prototype = extend(Obj.prototype, {
+ constructor: Fn,
+ toString: function(maxDepth) {
+ if (maxDepth == null) maxDepth = 0;
+ var str = this.generator ? "fn*(" : "fn(";
+ for (var i = 0; i < this.args.length; ++i) {
+ if (i) str += ", ";
+ var name = this.argNames[i];
+ if (name && name != "?") str += name + ": ";
+ str += maxDepth > -3 ? toString(this.args[i], maxDepth - 1, this) : "?";
+ }
+ str += ")";
+ if (!this.retval.isEmpty())
+ str += " -> " + (maxDepth > -3 ? toString(this.retval, maxDepth - 1, this) : "?");
+ return str;
+ },
+ getProp: function(prop) {
+ if (prop == "prototype") {
+ var known = this.hasProp(prop, false);
+ if (!known) {
+ known = this.defProp(prop);
+ var proto = new Obj(true, this.name && this.name + ".prototype");
+ proto.origin = this.origin;
+ known.addType(proto, WG_MADEUP_PROTO);
+ }
+ return known;
+ }
+ return Obj.prototype.getProp.call(this, prop);
+ },
+ defProp: function(prop, originNode) {
+ if (prop == "prototype") {
+ var found = this.hasProp(prop, false);
+ if (found) return found;
+ found = Obj.prototype.defProp.call(this, prop, originNode);
+ found.origin = this.origin;
+ found.propagate(new FnPrototype(this));
+ return found;
+ }
+ return Obj.prototype.defProp.call(this, prop, originNode);
+ },
+ getFunctionType: function() { return this; }
+ });
+
+ var Arr = exports.Arr = function(contentType) {
+ Obj.call(this, cx.protos.Array)
+ var content = this.defProp("<i>")
+ if (Array.isArray(contentType)) {
+ this.tuple = contentType.length
+ for (var i = 0; i < contentType.length; i++) {
+ var prop = this.defProp(String(i))
+ contentType[i].propagate(prop)
+ prop.propagate(content)
+ }
+ } else if (contentType) {
+ this.tuple = 0
+ contentType.propagate(content)
+ }
+ };
+ Arr.prototype = extend(Obj.prototype, {
+ constructor: Arr,
+ toString: function(maxDepth) {
+ if (maxDepth == null) maxDepth = 0
+ if (maxDepth <= -3) return "[?]"
+ var content = ""
+ if (this.tuple) {
+ var similar
+ for (var i = 0; i in this.props; i++) {
+ var type = toString(this.getProp(String(i)), maxDepth - 1, this)
+ if (similar == null)
+ similar = type
+ else if (similar != type)
+ similar = false
+ else
+ similar = type
+ content += (content ? ", " : "") + type
+ }
+ if (similar) content = similar
+ } else {
+ content = toString(this.getProp("<i>"), maxDepth - 1, this)
+ }
+ return "[" + content + "]"
+ },
+ normalizeIntegerProp: function(prop) {
+ if (+prop < this.tuple) return prop
+ else return "<i>"
+ }
+ });
+
+ var Sym = exports.Sym = function(name, originNode) {
+ Prim.call(this, cx.protos.Symbol, "Symbol")
+ this.symName = name
+ this.originNode = originNode
+ }
+ Sym.prototype = extend(Prim.prototype, {
+ constructor: Sym,
+ asPropName: function() { return ":" + this.symName },
+ getSymbolType: function() { return this }
+ })
+
+ exports.getSymbol = function(name, originNode) {
+ var cleanName = name.replace(/[^\w$\.]/g, "_")
+ var known = cx.symbols[cleanName]
+ if (known) {
+ if (originNode && !known.originNode) known.originNode = originNode
+ return known
+ }
+ return cx.symbols[cleanName] = new Sym(cleanName, originNode)
+ }
+
+ // THE PROPERTY REGISTRY
+
+ function registerProp(prop, obj) {
+ var data = cx.props[prop] || (cx.props[prop] = []);
+ data.push(obj);
+ }
+
+ function objsWithProp(prop) {
+ return cx.props[prop];
+ }
+
+ // INFERENCE CONTEXT
+
+ exports.Context = function(defs, parent) {
+ this.parent = parent;
+ this.props = Object.create(null);
+ this.protos = Object.create(null);
+ this.origins = [];
+ this.curOrigin = "ecma5";
+ this.paths = Object.create(null);
+ this.definitions = Object.create(null);
+ this.purgeGen = 0;
+ this.workList = null;
+ this.disabledComputing = null;
+ this.curSuperCtor = this.curSuper = null;
+ this.symbols = Object.create(null)
+
+ exports.withContext(this, function() {
+ cx.protos.Object = new Obj(null, "Object.prototype");
+ cx.topScope = new Scope();
+ cx.topScope.name = "<top>";
+ cx.protos.Array = new Obj(true, "Array.prototype");
+ cx.protos.Function = new Fn("Function.prototype", ANull, [], [], ANull);
+ cx.protos.Function.proto = cx.protos.Object;
+ cx.protos.RegExp = new Obj(true, "RegExp.prototype");
+ cx.protos.String = new Obj(true, "String.prototype");
+ cx.protos.Number = new Obj(true, "Number.prototype");
+ cx.protos.Boolean = new Obj(true, "Boolean.prototype");
+ cx.protos.Symbol = new Obj(true, "Symbol.prototype");
+ cx.str = new Prim(cx.protos.String, "string");
+ cx.bool = new Prim(cx.protos.Boolean, "bool");
+ cx.num = new Prim(cx.protos.Number, "number");
+ cx.curOrigin = null;
+
+ if (defs) for (var i = 0; i < defs.length; ++i)
+ def.load(defs[i]);
+ });
+ };
+
+ exports.Context.prototype.startAnalysis = function() {
+ this.disabledComputing = this.workList = this.curSuperCtor = this.curSuper = null;
+ };
+
+ var cx = null;
+ exports.cx = function() { return cx; };
+
+ exports.withContext = function(context, f) {
+ var old = cx;
+ cx = context;
+ try { return f(); }
+ finally { cx = old; }
+ };
+
+ exports.TimedOut = function() {
+ this.message = "Timed out";
+ this.stack = (new Error()).stack;
+ };
+ exports.TimedOut.prototype = Object.create(Error.prototype);
+ exports.TimedOut.prototype.name = "infer.TimedOut";
+
+ var timeout;
+ exports.withTimeout = function(ms, f) {
+ var end = +new Date + ms;
+ var oldEnd = timeout;
+ if (oldEnd && oldEnd < end) return f();
+ timeout = end;
+ try { return f(); }
+ finally { timeout = oldEnd; }
+ };
+
+ exports.addOrigin = function(origin) {
+ if (cx.origins.indexOf(origin) < 0) cx.origins.push(origin);
+ };
+
+ var baseMaxWorkDepth = 20, reduceMaxWorkDepth = 0.0001;
+ function withWorklist(f) {
+ if (cx.workList) return f(cx.workList);
+
+ var list = [], depth = 0;
+ var add = cx.workList = function(type, target, weight) {
+ if (depth < baseMaxWorkDepth - reduceMaxWorkDepth * list.length)
+ list.push(type, target, weight, depth);
+ };
+ var ret = f(add);
+ for (var i = 0; i < list.length; i += 4) {
+ if (timeout && +new Date >= timeout)
+ throw new exports.TimedOut();
+ depth = list[i + 3] + 1;
+ list[i + 1].addType(list[i], list[i + 2]);
+ }
+ cx.workList = null;
+ return ret;
+ }
+
+ function withSuper(ctor, obj, f) {
+ var oldCtor = cx.curSuperCtor, oldObj = cx.curSuper
+ cx.curSuperCtor = ctor; cx.curSuper = obj
+ var result = f()
+ cx.curSuperCtor = oldCtor; cx.curSuper = oldObj
+ return result
+ }
+
+ // SCOPES
+
+ var Scope = exports.Scope = function(prev, originNode, isBlock) {
+ Obj.call(this, prev || true);
+ this.prev = prev;
+ this.originNode = originNode
+ this.isBlock = !!isBlock
+ };
+ Scope.prototype = extend(Obj.prototype, {
+ constructor: Scope,
+ defVar: function(name, originNode) {
+ for (var s = this; ; s = s.proto) {
+ var found = s.props[name];
+ if (found) return found;
+ if (!s.prev) return s.defProp(name, originNode);
+ }
+ }
+ });
+
+ function functionScope(scope) {
+ while (scope.isBlock) scope = scope.prev
+ return scope
+ }
+
+
+ // RETVAL COMPUTATION HEURISTICS
+
+ function maybeInstantiate(scope, score) {
+ var fn = functionScope(scope).fnType
+ if (fn) fn.instantiateScore = (fn.instantiateScore || 0) + score;
+ }
+
+ var NotSmaller = {};
+ function nodeSmallerThan(node, n) {
+ try {
+ walk.simple(node, {Expression: function() { if (--n <= 0) throw NotSmaller; }});
+ return true;
+ } catch(e) {
+ if (e == NotSmaller) return false;
+ throw e;
+ }
+ }
+
+ function maybeTagAsInstantiated(node, fn) {
+ var score = fn.instantiateScore;
+ if (!cx.disabledComputing && score && fn.args.length && nodeSmallerThan(node, score * 5)) {
+ maybeInstantiate(functionScope(fn.originNode.scope.prev), score / 2);
+ setFunctionInstantiated(node, fn);
+ return true;
+ } else {
+ fn.instantiateScore = null;
+ }
+ }
+
+ function setFunctionInstantiated(node, fn) {
+ // Disconnect the arg avals, so that we can add info to them without side effects
+ for (var i = 0; i < fn.args.length; ++i) fn.args[i] = new AVal;
+ fn.self = new AVal;
+ fn.computeRet = function(self, args) {
+ // Prevent recursion
+ return withDisabledComputing(fn, function() {
+ var oldOrigin = cx.curOrigin;
+ cx.curOrigin = fn.origin;
+ var scope = node.scope
+ var scopeCopy = new Scope(scope.prev, scope.originNode);
+ for (var v in scope.props) {
+ var local = scopeCopy.defProp(v, scope.props[v].originNode);
+ for (var i = 0; i < args.length; ++i) if (fn.argNames[i] == v && i < args.length)
+ args[i].propagate(local);
+ }
+ var argNames = fn.argNames.length != args.length ? fn.argNames.slice(0, args.length) : fn.argNames;
+ while (argNames.length < args.length) argNames.push("?");
+ scopeCopy.fnType = new Fn(fn.name, self, args, argNames, ANull, fn.generator);
+ scopeCopy.fnType.originNode = fn.originNode;
+ if (fn.arguments) {
+ var argset = scopeCopy.fnType.arguments = new AVal;
+ scopeCopy.defProp("arguments").addType(new Arr(argset));
+ for (var i = 0; i < args.length; ++i) args[i].propagate(argset);
+ }
+ node.scope = scopeCopy;
+ walk.recursive(node.body, scopeCopy, null, scopeGatherer);
+ walk.recursive(node.body, scopeCopy, null, inferWrapper);
+ cx.curOrigin = oldOrigin;
+ return scopeCopy.fnType.retval;
+ });
+ };
+ }
+
+ function maybeTagAsGeneric(fn) {
+ var target = fn.retval;
+ if (target == ANull) return;
+ var targetInner, asArray;
+ if (!target.isEmpty() && (targetInner = target.getType()) instanceof Arr)
+ target = asArray = targetInner.getProp("<i>");
+
+ function explore(aval, path, depth) {
+ if (depth > 3 || !aval.forward) return;
+ for (var i = 0; i < aval.forward.length; ++i) {
+ var prop = aval.forward[i].propagatesTo();
+ if (!prop) continue;
+ var newPath = path, dest;
+ if (prop instanceof AVal) {
+ dest = prop;
+ } else if (prop.target instanceof AVal) {
+ newPath += prop.pathExt;
+ dest = prop.target;
+ } else continue;
+ if (dest == target) return newPath;
+ var found = explore(dest, newPath, depth + 1);
+ if (found) return found;
+ }
+ }
+
+ var foundPath = explore(fn.self, "!this", 0);
+ for (var i = 0; !foundPath && i < fn.args.length; ++i)
+ foundPath = explore(fn.args[i], "!" + i, 0);
+
+ if (foundPath) {
+ if (asArray) foundPath = "[" + foundPath + "]";
+ var p = new def.TypeParser(foundPath);
+ var parsed = p.parseType(true);
+ fn.computeRet = parsed.apply ? parsed : function() { return parsed; };
+ fn.computeRetSource = foundPath;
+ return true;
+ }
+ }
+
+ // SCOPE GATHERING PASS
+
+ function addVar(scope, nameNode) {
+ return scope.defProp(nameNode.name, nameNode);
+ }
+ function patternName(node) {
+ if (node.type == "Identifier") return node.name
+ if (node.type == "AssignmentPattern") return patternName(node.left)
+ if (node.type == "ObjectPattern") return "{" + node.properties.map(function(e) { return patternName(e.value) }).join(", ") + "}"
+ if (node.type == "ArrayPattern") return "[" + node.elements.map(patternName).join(", ") + "]"
+ if (node.type == "RestElement") return "..." + patternName(node.argument)
+ return "_"
+ }
+
+ function isBlockScopedDecl(node) {
+ return node.type == "VariableDeclaration" && node.kind != "var" ||
+ node.type == "FunctionDeclaration" ||
+ node.type == "ClassDeclaration";
+ }
+
+ function patternScopes(inner, outer) {
+ return {inner: inner, outer: outer || inner}
+ }
+
+ var scopeGatherer = exports.scopeGatherer = walk.make({
+ VariablePattern: function(node, scopes) {
+ if (scopes.inner) addVar(scopes.inner, node)
+ },
+ AssignmentPattern: function(node, scopes, c) {
+ c(node.left, scopes, "Pattern")
+ c(node.right, scopes.outer, "Expression")
+ },
+ AssignmentExpression: function(node, scope, c) {
+ if (node.left.type == "MemberExpression")
+ c(node.left, scope, "Expression")
+ else
+ c(node.left, patternScopes(false, scope), "Pattern")
+ c(node.right, scope, "Expression")
+ },
+ Function: function(node, scope, c) {
+ var inner = node.scope = new Scope(scope, node)
+ var argVals = [], argNames = []
+ for (var i = 0; i < node.params.length; ++i) {
+ var param = node.params[i]
+ argNames.push(patternName(param))
+ if (param.type == "Identifier") {
+ argVals.push(addVar(inner, param))
+ } else {
+ var arg = new AVal
+ argVals.push(arg)
+ arg.originNode = param
+ c(param, patternScopes(inner), "Pattern")
+ }
+ }
+ inner.fnType = new Fn(node.id && node.id.name, new AVal, argVals, argNames, ANull, node.generator)
+ inner.fnType.originNode = node;
+ if (node.id) {
+ var decl = node.type == "FunctionDeclaration";
+ addVar(decl ? scope : inner, node.id);
+ }
+ c(node.body, inner, node.expression ? "Expression" : "Statement");
+ },
+ BlockStatement: function(node, scope, c) {
+ if (!node.scope && node.body.some(isBlockScopedDecl))
+ scope = node.scope = new Scope(scope, node, true)
+ walk.base.BlockStatement(node, scope, c)
+ },
+ TryStatement: function(node, scope, c) {
+ c(node.block, scope, "Statement");
+ if (node.handler) {
+ if (node.handler.param.type == "Identifier") {
+ var v = addVar(scope, node.handler.param);
+ c(node.handler.body, scope, "Statement");
+ var e5 = cx.definitions.ecma5;
+ if (e5 && v.isEmpty()) getInstance(e5["Error.prototype"]).propagate(v, WG_CATCH_ERROR);
+ } else {
+ c(node.handler.param, patternScopes(scope), "Pattern")
+ }
+ }
+ if (node.finalizer) c(node.finalizer, scope, "Statement");
+ },
+ VariableDeclaration: function(node, scope, c) {
+ var targetScope = node.kind == "var" ? functionScope(scope) : scope
+ for (var i = 0; i < node.declarations.length; ++i) {
+ var decl = node.declarations[i];
+ c(decl.id, patternScopes(targetScope, scope), "Pattern")
+ if (decl.init) c(decl.init, scope, "Expression");
+ }
+ },
+ ClassDeclaration: function(node, scope, c) {
+ addVar(scope, node.id)
+ if (node.superClass) c(node.superClass, scope, "Expression")
+ for (var i = 0; i < node.body.body.length; i++)
+ c(node.body.body[i], scope)
+ },
+ ForInStatement: function(node, scope, c) {
+ if (!node.scope && isBlockScopedDecl(node.left))
+ scope = node.scope = new Scope(scope, node, true)
+ walk.base.ForInStatement(node, scope, c)
+ },
+ ForStatement: function(node, scope, c) {
+ if (!node.scope && node.init && isBlockScopedDecl(node.init))
+ scope = node.scope = new Scope(scope, node, true)
+ walk.base.ForStatement(node, scope, c)
+ },
+ ImportDeclaration: function(node, scope) {
+ for (var i = 0; i < node.specifiers.length; i++)
+ addVar(scope, node.specifiers[i].local)
+ }
+ });
+ scopeGatherer.ForOfStatement = scopeGatherer.ForInStatement
+
+ // CONSTRAINT GATHERING PASS
+
+ var propName = exports.propName = function(node, inferInScope) {
+ var key = node.property || node.key;
+ if (!node.computed && key.type == "Identifier") return key.name;
+ if (key.type == "Literal") {
+ if (typeof key.value == "string") return key.value
+ if (typeof key.value == "number") return String(key.value)
+ }
+ if (inferInScope) {
+ var symName = symbolName(infer(key, inferInScope))
+ if (symName) return node.propName = symName
+ } else if (node.propName) {
+ return node.propName
+ }
+ return "<i>";
+ }
+ function symbolName(val) {
+ var sym = val.getSymbolType()
+ if (sym) return sym.asPropName()
+ }
+
+ function unopResultType(op) {
+ switch (op) {
+ case "+": case "-": case "~": return cx.num;
+ case "!": return cx.bool;
+ case "typeof": return cx.str;
+ case "void": case "delete": return ANull;
+ }
+ }
+ function binopIsBoolean(op) {
+ switch (op) {
+ case "==": case "!=": case "===": case "!==": case "<": case ">": case ">=": case "<=":
+ case "in": case "instanceof": return true;
+ }
+ }
+ function literalType(node) {
+ if (node.regex) return getInstance(cx.protos.RegExp);
+ switch (typeof node.value) {
+ case "boolean": return cx.bool;
+ case "number": return cx.num;
+ case "string": return cx.str;
+ case "object":
+ case "function":
+ if (!node.value) return ANull;
+ return getInstance(cx.protos.RegExp);
+ }
+ }
+
+ function join(a, b) {
+ if (a == b || b == ANull) return a
+ if (a == ANull) return b
+ var joined = new AVal
+ a.propagate(joined)
+ b.propagate(joined)
+ return joined
+ }
+
+ function connectParams(node, scope) {
+ for (var i = 0; i < node.params.length; i++) {
+ var param = node.params[i]
+ if (param.type == "Identifier") continue
+ connectPattern(param, scope, node.scope.fnType.args[i])
+ }
+ }
+
+ function ensureVar(node, scope) {
+ return scope.hasProp(node.name) || cx.topScope.defProp(node.name, node)
+ }
+
+ var inferPatternVisitor = exports.inferPatternVisitor = {
+ Identifier: function(node, scope, source) {
+ source.propagate(ensureVar(node, scope))
+ },
+ MemberExpression: function(node, scope, source) {
+ var obj = infer(node.object, scope)
+ var pName = propName(node, scope)
+ obj.propagate(new DefProp(pName, source, node.property))
+ },
+ RestElement: function(node, scope, source) {
+ connectPattern(node.argument, scope, new Arr(source))
+ },
+ ObjectPattern: function(node, scope, source) {
+ for (var i = 0; i < node.properties.length; ++i) {
+ var prop = node.properties[i]
+ connectPattern(prop.value, scope, source.getProp(prop.key.name))
+ }
+ },
+ ArrayPattern: function(node, scope, source) {
+ for (var i = 0; i < node.elements.length; i++)
+ if (node.elements[i])
+ connectPattern(node.elements[i], scope, source.getProp(String(i)))
+ },
+ AssignmentPattern: function(node, scope, source) {
+ connectPattern(node.left, scope, join(source, infer(node.right, scope)))
+ }
+ }
+
+ function connectPattern(node, scope, source) {
+ var connecter = inferPatternVisitor[node.type]
+ if (connecter) connecter(node, scope, source)
+ }
+
+ function getThis(scope) {
+ var fnScope = functionScope(scope)
+ return fnScope.fnType ? fnScope.fnType.self : fnScope
+ }
+
+ function maybeAddPhantomObj(obj) {
+ if (!obj.isEmpty() || !obj.propertyOf) return
+ obj.propertyOf.getProp(obj.propertyName).addType(new Obj, WG_PHANTOM_OBJ)
+ maybeAddPhantomObj(obj.propertyOf)
+ }
+
+ function inferClass(node, scope, name) {
+ if (!name && node.id) name = node.id.name
+
+ var sup = cx.protos.Object, supCtor, delayed
+ if (node.superClass) {
+ if (node.superClass.type == "Literal" && node.superClass.value == null) {
+ sup = null
+ } else {
+ var supVal = infer(node.superClass, scope), supProto
+ supCtor = supVal.getFunctionType()
+ if (supCtor && (supProto = supCtor.getProp("prototype").getObjType())) {
+ sup = supProto
+ } else {
+ supCtor = supVal
+ delayed = supVal.getProp("prototype")
+ }
+ }
+ }
+ var proto = new Obj(sup, name && name + ".prototype")
+ if (delayed) delayed.propagate(new HasProto(proto))
+
+ return withSuper(supCtor, delayed || sup, function() {
+ var ctor, body = node.body.body
+ for (var i = 0; i < body.length; i++)
+ if (body[i].kind == "constructor") ctor = body[i].value
+ var fn = node.objType = ctor ? infer(ctor, scope) : new Fn(name, ANull, [], null, ANull)
+ fn.originNode = node.id || ctor || node
+
+ var inst = getInstance(proto, fn)
+ fn.self.addType(inst)
+ fn.defProp("prototype", node).addType(proto)
+ for (var i = 0; i < body.length; i++) {
+ var method = body[i], target
+ if (method.kind == "constructor") continue
+ var pName = propName(method, scope)
+ if (pName == "<i>" || method.kind == "set") {
+ target = ANull
+ } else {
+ target = (method.static ? fn : proto).defProp(pName, method.key)
+ target.initializer = true
+ if (method.kind == "get") target = new IsCallee(inst, [], null, target)
+ }
+ infer(method.value, scope, target)
+ var methodFn = target.getFunctionType()
+ if (methodFn) methodFn.self.addType(inst)
+ }
+ return fn
+ })
+ }
+
+ function arrayLiteralType(elements, scope, inner) {
+ var tuple = elements.length > 1 && elements.length < 6
+ if (tuple) {
+ var homogenous = true, litType
+ for (var i = 0; i < elements.length; i++) {
+ var elt = elements[i]
+ if (!elt)
+ tuple = false
+ else if (elt.type != "Literal" || (litType && litType != typeof elt.value))
+ homogenous = false
+ else
+ litType = typeof elt.value
+ }
+ if (homogenous) tuple = false
+ }
+
+ if (tuple) {
+ var types = []
+ for (var i = 0; i < elements.length; ++i)
+ types.push(inner(elements[i], scope))
+ return new Arr(types)
+ } else if (elements.length < 2) {
+ return new Arr(elements[0] && inner(elements[0], scope))
+ } else {
+ var eltVal = new AVal
+ for (var i = 0; i < elements.length; i++)
+ if (elements[i]) inner(elements[i], scope).propagate(eltVal)
+ return new Arr(eltVal)
+ }
+ }
+
+ function ret(f) {
+ return function(node, scope, out, name) {
+ var r = f(node, scope, name);
+ if (out) r.propagate(out);
+ return r;
+ };
+ }
+ function fill(f) {
+ return function(node, scope, out, name) {
+ if (!out) out = new AVal;
+ f(node, scope, out, name);
+ return out;
+ };
+ }
+
+ var inferExprVisitor = exports.inferExprVisitor = {
+ ArrayExpression: ret(function(node, scope) {
+ return arrayLiteralType(node.elements, scope, infer)
+ }),
+ ObjectExpression: ret(function(node, scope, name) {
+ var proto = true, waitForProto
+ for (var i = 0; i < node.properties.length; ++i) {
+ var prop = node.properties[i]
+ if (prop.key.name == "__proto__") {
+ if (prop.value.type == "Literal" && prop.value.value == null) {
+ proto = null
+ } else {
+ var protoVal = infer(prop.value, scope), known = protoVal.getObjType()
+ if (known) proto = known
+ else waitForProto = protoVal
+ }
+ }
+ }
+
+ var obj = node.objType = new Obj(proto, name);
+ if (waitForProto) waitForProto.propagate(new HasProto(obj))
+ obj.originNode = node;
+
+ withSuper(null, waitForProto || proto, function() {
+ for (var i = 0; i < node.properties.length; ++i) {
+ var prop = node.properties[i], key = prop.key;
+ if (prop.value.name == "✖" || prop.key.name == "__proto__") continue;
+
+ var name = propName(prop, scope), target
+ if (name == "<i>" || prop.kind == "set") {
+ target = ANull;
+ } else {
+ var val = target = obj.defProp(name, key);
+ val.initializer = true;
+ if (prop.kind == "get")
+ target = new IsCallee(obj, [], null, val);
+ }
+ infer(prop.value, scope, target, name);
+ if (prop.value.type == "FunctionExpression")
+ prop.value.scope.fnType.self.addType(obj, WG_SPECULATIVE_THIS);
+ }
+ })
+ return obj;
+ }),
+ FunctionExpression: ret(function(node, scope, name) {
+ var inner = node.scope, fn = inner.fnType;
+ if (name && !fn.name) fn.name = name;
+ connectParams(node, inner)
+ if (node.expression)
+ infer(node.body, inner, inner.fnType.retval = new AVal)
+ else
+ walk.recursive(node.body, inner, null, inferWrapper, "Statement")
+ if (node.type == "ArrowFunctionExpression") {
+ getThis(scope).propagate(fn.self)
+ fn.self = ANull
+ }
+ maybeTagAsInstantiated(node, fn) || maybeTagAsGeneric(fn);
+ if (node.id) inner.getProp(node.id.name).addType(fn);
+ return fn;
+ }),
+ ClassExpression: ret(inferClass),
+ SequenceExpression: ret(function(node, scope) {
+ for (var i = 0, l = node.expressions.length - 1; i < l; ++i)
+ infer(node.expressions[i], scope, ANull);
+ return infer(node.expressions[l], scope);
+ }),
+ UnaryExpression: ret(function(node, scope) {
+ infer(node.argument, scope, ANull);
+ return unopResultType(node.operator);
+ }),
+ UpdateExpression: ret(function(node, scope) {
+ infer(node.argument, scope, ANull);
+ return cx.num;
+ }),
+ BinaryExpression: ret(function(node, scope) {
+ if (node.operator == "+") {
+ var lhs = infer(node.left, scope);
+ var rhs = infer(node.right, scope);
+ if (lhs.hasType(cx.str) || rhs.hasType(cx.str)) return cx.str;
+ if (lhs.hasType(cx.num) && rhs.hasType(cx.num)) return cx.num;
+ var result = new AVal;
+ lhs.propagate(new IsAdded(rhs, result));
+ rhs.propagate(new IsAdded(lhs, result));
+ return result;
+ } else {
+ infer(node.left, scope, ANull);
+ infer(node.right, scope, ANull);
+ return binopIsBoolean(node.operator) ? cx.bool : cx.num;
+ }
+ }),
+ AssignmentExpression: ret(function(node, scope, name) {
+ var rhs, pName;
+ if (node.left.type == "MemberExpression") {
+ pName = propName(node.left, scope)
+ if (!name)
+ name = node.left.object.type == "Identifier" ? node.left.object.name + "." + pName : pName
+ } else if (!name && node.left.type == "Identifier") {
+ name = node.left.name
+ }
+
+ if (node.operator && node.operator != "=" && node.operator != "+=") {
+ infer(node.right, scope, ANull);
+ rhs = cx.num;
+ } else {
+ rhs = infer(node.right, scope, null, name);
+ }
+
+ if (node.left.type == "MemberExpression") {
+ var obj = infer(node.left.object, scope);
+ if (pName == "prototype") maybeInstantiate(scope, 20);
+ if (pName == "<i>") {
+ // This is a hack to recognize for/in loops that copy
+ // properties, and do the copying ourselves, insofar as we
+ // manage, because such loops tend to be relevant for type
+ // information.
+ var v = node.left.property.name, local = scope.props[v], over = local && local.iteratesOver;
+ if (over) {
+ maybeInstantiate(scope, 20);
+ var fromRight = node.right.type == "MemberExpression" && node.right.computed && node.right.property.name == v;
+ over.forAllProps(function(prop, val, local) {
+ if (local && prop != "prototype" && prop != "<i>")
+ obj.propagate(new DefProp(prop, fromRight ? val : ANull));
+ });
+ return rhs;
+ }
+ }
+
+ obj.propagate(new DefProp(pName, rhs, node.left.property));
+ maybeAddPhantomObj(obj)
+ if (node.right.type == "FunctionExpression")
+ obj.propagate(node.right.scope.fnType.self, WG_SPECULATIVE_THIS);
+ } else {
+ connectPattern(node.left, scope, rhs)
+ }
+ return rhs;
+ }),
+ LogicalExpression: fill(function(node, scope, out) {
+ infer(node.left, scope, out);
+ infer(node.right, scope, out);
+ }),
+ ConditionalExpression: fill(function(node, scope, out) {
+ infer(node.test, scope, ANull);
+ infer(node.consequent, scope, out);
+ infer(node.alternate, scope, out);
+ }),
+ NewExpression: fill(function(node, scope, out, name) {
+ if (node.callee.type == "Identifier" && node.callee.name in scope.props)
+ maybeInstantiate(scope, 20);
+
+ for (var i = 0, args = []; i < node.arguments.length; ++i)
+ args.push(infer(node.arguments[i], scope));
+ var callee = infer(node.callee, scope);
+ var self = new AVal;
+ callee.propagate(new IsCtor(self, name && /\.prototype$/.test(name)));
+ self.propagate(out, WG_NEW_INSTANCE);
+ callee.propagate(new IsCallee(self, args, node.arguments, new IfObj(out)));
+ }),
+ CallExpression: fill(function(node, scope, out) {
+ for (var i = 0, args = []; i < node.arguments.length; ++i)
+ args.push(infer(node.arguments[i], scope));
+ var outerFn = functionScope(scope).fnType
+ if (node.callee.type == "MemberExpression") {
+ var self = infer(node.callee.object, scope);
+ var pName = propName(node.callee, scope)
+ if (outerFn && (pName == "call" || pName == "apply") &&
+ outerFn.args.indexOf(self) > -1)
+ maybeInstantiate(scope, 30);
+ self.propagate(new HasMethodCall(pName, args, node.arguments, out));
+ } else if (node.callee.type == "Super" && cx.curSuperCtor) {
+ cx.curSuperCtor.propagate(new IsCallee(getThis(scope), args, node.arguments, out))
+ } else {
+ var callee = infer(node.callee, scope);
+ if (outerFn && outerFn.args.indexOf(callee) > -1)
+ maybeInstantiate(scope, 30);
+ var knownFn = callee.getFunctionType();
+ if (knownFn && knownFn.instantiateScore && outerFn)
+ maybeInstantiate(scope, knownFn.instantiateScore / 5);
+ callee.propagate(new IsCallee(cx.topScope, args, node.arguments, out));
+ }
+ }),
+ MemberExpression: fill(function(node, scope, out) {
+ var name = propName(node), wg;
+ if (name == "<i>") {
+ var propType = infer(node.property, scope)
+ var symName = symbolName(propType)
+ if (symName)
+ name = node.propName = symName
+ else if (!propType.hasType(cx.num))
+ wg = WG_MULTI_MEMBER
+ }
+ infer(node.object, scope).getProp(name).propagate(out, wg)
+ }),
+ Identifier: ret(function(node, scope) {
+ if (node.name == "arguments") {
+ var fnScope = functionScope(scope)
+ if (fnScope.fnType && !(node.name in fnScope.props))
+ scope.defProp(node.name, fnScope.fnType.originNode)
+ .addType(new Arr(fnScope.fnType.arguments = new AVal));
+ }
+ return scope.getProp(node.name);
+ }),
+ ThisExpression: ret(function(_node, scope) {
+ return getThis(scope)
+ }),
+ Super: ret(function(node) {
+ return node.superType = cx.curSuper || ANull
+ }),
+ Literal: ret(function(node) {
+ return literalType(node);
+ }),
+ TemplateLiteral: ret(function(node, scope) {
+ for (var i = 0; i < node.expressions.length; ++i)
+ infer(node.expressions[i], scope, ANull)
+ return cx.str
+ }),
+ TaggedTemplateExpression: fill(function(node, scope, out) {
+ var args = [new Arr(cx.str)]
+ for (var i = 0; i < node.quasi.expressions.length; ++i)
+ args.push(infer(node.quasi.expressions[i], scope))
+ infer(node.tag, scope, new IsCallee(cx.topScope, args, node.quasi.expressions, out))
+ }),
+ YieldExpression: ret(function(node, scope) {
+ var output = ANull, fn = functionScope(scope).fnType
+ if (fn) {
+ if (fn.retval == ANull) fn.retval = new AVal
+ if (!fn.yieldval) fn.yieldval = new AVal
+ output = fn.retval
+ }
+ if (node.argument) {
+ if (node.delegate) {
+ infer(node.argument, scope, new HasMethodCall("next", [], null,
+ new GetProp("value", output)))
+ } else {
+ infer(node.argument, scope, output)
+ }
+ }
+ return fn ? fn.yieldval : ANull
+ })
+ };
+ inferExprVisitor.ArrowFunctionExpression = inferExprVisitor.FunctionExpression
+
+ function infer(node, scope, out, name) {
+ var handler = inferExprVisitor[node.type];
+ return handler ? handler(node, scope, out, name) : ANull;
+ }
+
+ function loopPattern(init) {
+ return init.type == "VariableDeclaration" ? init.declarations[0].id : init
+ }
+
+ var inferWrapper = exports.inferWrapper = walk.make({
+ Expression: function(node, scope) {
+ infer(node, node.scope || scope, ANull);
+ },
+
+ FunctionDeclaration: function(node, scope, c) {
+ var inner = node.scope, fn = inner.fnType;
+ connectParams(node, inner)
+ c(node.body, inner, "Statement");
+ maybeTagAsInstantiated(node, fn) || maybeTagAsGeneric(fn);
+ scope.getProp(node.id.name).addType(fn)
+ },
+
+ Statement: function(node, scope, c) {
+ c(node, node.scope || scope)
+ },
+
+ VariableDeclaration: function(node, scope) {
+ for (var i = 0; i < node.declarations.length; ++i) {
+ var decl = node.declarations[i];
+ if (decl.id.type == "Identifier") {
+ var prop = scope.getProp(decl.id.name);
+ if (decl.init)
+ infer(decl.init, scope, prop, decl.id.name);
+ } else if (decl.init) {
+ connectPattern(decl.id, scope, infer(decl.init, scope))
+ }
+ }
+ },
+
+ ClassDeclaration: function(node, scope) {
+ scope.getProp(node.id.name).addType(inferClass(node, scope, node.id.name))
+ },
+
+ ReturnStatement: function(node, scope) {
+ if (!node.argument) return;
+ var output = ANull, fn = functionScope(scope).fnType
+ if (fn) {
+ if (fn.retval == ANull) fn.retval = new AVal;
+ output = fn.retval;
+ }
+ infer(node.argument, scope, output);
+ },
+
+ ForInStatement: function(node, scope, c) {
+ var source = infer(node.right, scope);
+ if ((node.right.type == "Identifier" && node.right.name in scope.props) ||
+ (node.right.type == "MemberExpression" && node.right.property.name == "prototype")) {
+ maybeInstantiate(scope, 5);
+ var pattern = loopPattern(node.left)
+ if (pattern.type == "Identifier") {
+ if (pattern.name in scope.props)
+ scope.getProp(pattern.name).iteratesOver = source
+ source.getProp("<i>").propagate(ensureVar(pattern, scope))
+ } else {
+ connectPattern(pattern, scope, source.getProp("<i>"))
+ }
+ }
+ c(node.body, scope, "Statement");
+ },
+
+ ForOfStatement: function(node, scope, c) {
+ var pattern = loopPattern(node.left), target
+ if (pattern.type == "Identifier")
+ target = ensureVar(pattern, scope)
+ else
+ connectPattern(pattern, scope, target = new AVal)
+ infer(node.right, scope, new HasMethodCall(":Symbol.iterator", [], null,
+ new HasMethodCall("next", [], null,
+ new GetProp("value", target))))
+ c(node.body, scope, "Statement")
+ }
+ });
+
+ // PARSING
+
+ var parse = exports.parse = function(text, options, thirdArg) {
+ if (!options || Array.isArray(options)) options = thirdArg
+ var ast;
+ try { ast = acorn.parse(text, options); }
+ catch(e) { ast = acorn_loose.parse_dammit(text, options); }
+ return ast;
+ };
+
+ // ANALYSIS INTERFACE
+
+ exports.analyze = function(ast, name, scope) {
+ if (typeof ast == "string") ast = parse(ast);
+
+ if (!name) name = "file#" + cx.origins.length;
+ exports.addOrigin(cx.curOrigin = name);
+
+ if (!scope) scope = cx.topScope;
+ cx.startAnalysis();
+
+ walk.recursive(ast, scope, null, scopeGatherer);
+ if (cx.parent) cx.parent.signal("preInfer", ast, scope)
+ walk.recursive(ast, scope, null, inferWrapper);
+ if (cx.parent) cx.parent.signal("postInfer", ast, scope)
+
+ cx.curOrigin = null;
+ };
+
+ // PURGING
+
+ exports.purge = function(origins, start, end) {
+ var test = makePredicate(origins, start, end);
+ ++cx.purgeGen;
+ cx.topScope.purge(test);
+ for (var prop in cx.props) {
+ var list = cx.props[prop];
+ for (var i = 0; i < list.length; ++i) {
+ var obj = list[i], av = obj.props[prop];
+ if (!av || test(av, av.originNode)) list.splice(i--, 1);
+ }
+ if (!list.length) delete cx.props[prop];
+ }
+ };
+
+ function makePredicate(origins, start, end) {
+ var arr = Array.isArray(origins);
+ if (arr && origins.length == 1) { origins = origins[0]; arr = false; }
+ if (arr) {
+ if (end == null) return function(n) { return origins.indexOf(n.origin) > -1; };
+ return function(n, pos) { return pos && pos.start >= start && pos.end <= end && origins.indexOf(n.origin) > -1; };
+ } else {
+ if (end == null) return function(n) { return n.origin == origins; };
+ return function(n, pos) { return pos && pos.start >= start && pos.end <= end && n.origin == origins; };
+ }
+ }
+
+ AVal.prototype.purge = function(test) {
+ if (this.purgeGen == cx.purgeGen) return;
+ this.purgeGen = cx.purgeGen;
+ for (var i = 0; i < this.types.length; ++i) {
+ var type = this.types[i];
+ if (test(type, type.originNode))
+ this.types.splice(i--, 1);
+ else
+ type.purge(test);
+ }
+ if (!this.types.length) this.maxWeight = 0;
+
+ if (this.forward) for (var i = 0; i < this.forward.length; ++i) {
+ var f = this.forward[i];
+ if (test(f)) {
+ this.forward.splice(i--, 1);
+ if (this.props) this.props = null;
+ } else if (f.purge) {
+ f.purge(test);
+ }
+ }
+ };
+ ANull.purge = function() {};
+ Obj.prototype.purge = function(test) {
+ if (this.purgeGen == cx.purgeGen) return true;
+ this.purgeGen = cx.purgeGen;
+ for (var p in this.props) {
+ var av = this.props[p];
+ if (test(av, av.originNode))
+ this.removeProp(p);
+ av.purge(test);
+ }
+ };
+ Fn.prototype.purge = function(test) {
+ if (Obj.prototype.purge.call(this, test)) return;
+ this.self.purge(test);
+ this.retval.purge(test);
+ for (var i = 0; i < this.args.length; ++i) this.args[i].purge(test);
+ };
+
+ // EXPRESSION TYPE DETERMINATION
+
+ function findByPropertyName(name) {
+ guessing = true;
+ var found = objsWithProp(name);
+ if (found) for (var i = 0; i < found.length; ++i) {
+ var val = found[i].getProp(name);
+ if (!val.isEmpty()) return val;
+ }
+ return ANull;
+ }
+
+ function generatorResult(input, output) {
+ var retObj = new Obj(true)
+ retObj.defProp("done").addType(cx.bool)
+ output.propagate(retObj.defProp("value"))
+ var method = new Fn(null, ANull, input ? [input] : [], input ? ["?"] : [], retObj)
+ var result = new Obj(cx.definitions.ecma6 && cx.definitions.ecma6.generator_prototype || true)
+ result.defProp("next").addType(method)
+ return result
+ }
+
+ function maybeIterator(fn, output) {
+ if (!fn.generator) return output
+ if (!fn.computeRet) { // Reuse iterator objects for non-computed return types
+ if (fn.generator === true) fn.generator = generatorResult(fn.yieldval, output)
+ return fn.generator
+ }
+ return generatorResult(fn.yieldval, output)
+ }
+
+ function computeReturnType(funcNode, argNodes, scope) {
+ var fn = findType(funcNode, scope).getFunctionType()
+ if (!fn) return ANull
+ var result = fn.retval
+ if (fn.computeRet) {
+ for (var i = 0, args = []; i < argNodes.length; ++i)
+ args.push(findType(argNodes[i], scope))
+ var self = ANull
+ if (funcNode.type == "MemberExpression")
+ self = findType(funcNode.object, scope)
+ result = fn.computeRet(self, args, argNodes);
+ }
+ return maybeIterator(fn, result)
+ }
+
+ var typeFinder = exports.typeFinder = {
+ ArrayExpression: function(node, scope) {
+ return arrayLiteralType(node.elements, scope, findType)
+ },
+ ObjectExpression: function(node) {
+ return node.objType;
+ },
+ ClassExpression: function(node) {
+ return node.objType;
+ },
+ FunctionExpression: function(node) {
+ return node.scope.fnType;
+ },
+ ArrowFunctionExpression: function(node) {
+ return node.scope.fnType;
+ },
+ SequenceExpression: function(node, scope) {
+ return findType(node.expressions[node.expressions.length-1], scope);
+ },
+ UnaryExpression: function(node) {
+ return unopResultType(node.operator);
+ },
+ UpdateExpression: function() {
+ return cx.num;
+ },
+ BinaryExpression: function(node, scope) {
+ if (binopIsBoolean(node.operator)) return cx.bool;
+ if (node.operator == "+") {
+ var lhs = findType(node.left, scope);
+ var rhs = findType(node.right, scope);
+ if (lhs.hasType(cx.str) || rhs.hasType(cx.str)) return cx.str;
+ }
+ return cx.num;
+ },
+ AssignmentExpression: function(node, scope) {
+ return findType(node.right, scope);
+ },
+ LogicalExpression: function(node, scope) {
+ var lhs = findType(node.left, scope);
+ return lhs.isEmpty() ? findType(node.right, scope) : lhs;
+ },
+ ConditionalExpression: function(node, scope) {
+ var lhs = findType(node.consequent, scope);
+ return lhs.isEmpty() ? findType(node.alternate, scope) : lhs;
+ },
+ NewExpression: function(node, scope) {
+ var f = findType(node.callee, scope).getFunctionType();
+ var proto = f && f.getProp("prototype").getObjType();
+ if (!proto) return ANull;
+ return getInstance(proto, f);
+ },
+ CallExpression: function(node, scope) {
+ return computeReturnType(node.callee, node.arguments, scope)
+ },
+ MemberExpression: function(node, scope) {
+ var propN = propName(node), obj = findType(node.object, scope).getType();
+ if (obj) return obj.getProp(propN);
+ if (propN == "<i>") return ANull;
+ return findByPropertyName(propN);
+ },
+ Identifier: function(node, scope) {
+ return scope.hasProp(node.name) || ANull;
+ },
+ ThisExpression: function(_node, scope) {
+ return getThis(scope)
+ },
+ Literal: function(node) {
+ return literalType(node);
+ },
+ Super: ret(function(node) {
+ return node.superType
+ }),
+ TemplateLiteral: function() {
+ return cx.str
+ },
+ TaggedTemplateExpression: function(node, scope) {
+ return computeReturnType(node.tag, node.quasi.expressions, scope)
+ },
+ YieldExpression: function(_node, scope) {
+ var fn = functionScope(scope).fnType
+ return fn ? fn.yieldval : ANull
+ }
+ };
+
+ function findType(node, scope) {
+ var finder = typeFinder[node.type];
+ return finder ? finder(node, scope) : ANull;
+ }
+
+ var searchVisitor = exports.searchVisitor = walk.make({
+ Function: function(node, _st, c) {
+ walk.base.Function(node, node.scope, c)
+ },
+ Property: function(node, st, c) {
+ if (node.computed) c(node.key, st, "Expression");
+ if (node.key != node.value) c(node.value, st, "Expression");
+ },
+ Statement: function(node, st, c) {
+ c(node, node.scope || st)
+ },
+ ImportSpecifier: function(node, st, c) {
+ c(node.local, st)
+ },
+ ImportDefaultSpecifier: function(node, st, c) {
+ c(node.local, st)
+ },
+ ImportNamespaceSpecifier: function(node, st, c) {
+ c(node.local, st)
+ }
+ });
+ exports.fullVisitor = walk.make({
+ MemberExpression: function(node, st, c) {
+ c(node.object, st, "Expression");
+ c(node.property, st, node.computed ? "Expression" : null);
+ },
+ ObjectExpression: function(node, st, c) {
+ for (var i = 0; i < node.properties.length; ++i) {
+ c(node.properties[i].value, st, "Expression");
+ c(node.properties[i].key, st);
+ }
+ }
+ }, searchVisitor);
+
+ exports.findExpressionAt = function(ast, start, end, defaultScope, filter) {
+ var test = filter || function(_t, node) {
+ if (node.type == "Identifier" && node.name == "✖") return false;
+ return typeFinder.hasOwnProperty(node.type);
+ };
+ return walk.findNodeAt(ast, start, end, test, searchVisitor, defaultScope || cx.topScope);
+ };
+
+ exports.findExpressionAround = function(ast, start, end, defaultScope, filter) {
+ var test = filter || function(_t, node) {
+ if (start != null && node.start > start) return false;
+ if (node.type == "Identifier" && node.name == "✖") return false;
+ return typeFinder.hasOwnProperty(node.type);
+ };
+ return walk.findNodeAround(ast, end, test, searchVisitor, defaultScope || cx.topScope);
+ };
+
+ exports.expressionType = function(found) {
+ return findType(found.node, found.state);
+ };
+
+ // Finding the expected type of something, from context
+
+ exports.parentNode = function(child, ast) {
+ var stack = [];
+ function c(node, st, override) {
+ if (node.start <= child.start && node.end >= child.end) {
+ var top = stack[stack.length - 1];
+ if (node == child) throw {found: top};
+ if (top != node) stack.push(node);
+ walk.base[override || node.type](node, st, c);
+ if (top != node) stack.pop();
+ }
+ }
+ try {
+ c(ast, null);
+ } catch (e) {
+ if (e.found) return e.found;
+ throw e;
+ }
+ };
+
+ var findTypeFromContext = exports.findTypeFromContext = {
+ ArrayExpression: function(parent, _, get) { return get(parent, true).getProp("<i>"); },
+ ObjectExpression: function(parent, node, get) {
+ for (var i = 0; i < parent.properties.length; ++i) {
+ var prop = node.properties[i];
+ if (prop.value == node)
+ return get(parent, true).getProp(prop.key.name);
+ }
+ },
+ UnaryExpression: function(parent) { return unopResultType(parent.operator); },
+ UpdateExpression: function() { return cx.num; },
+ BinaryExpression: function(parent) { return binopIsBoolean(parent.operator) ? cx.bool : cx.num; },
+ AssignmentExpression: function(parent, _, get) { return get(parent.left); },
+ LogicalExpression: function(parent, _, get) { return get(parent, true); },
+ ConditionalExpression: function(parent, node, get) {
+ if (parent.consequent == node || parent.alternate == node) return get(parent, true);
+ },
+ CallExpression: function(parent, node, get) {
+ for (var i = 0; i < parent.arguments.length; i++) {
+ var arg = parent.arguments[i];
+ if (arg == node) {
+ var calleeType = get(parent.callee).getFunctionType();
+ if (calleeType instanceof Fn)
+ return calleeType.args[i];
+ break;
+ }
+ }
+ },
+ ReturnStatement: function(_parent, node, get) {
+ var fnNode = walk.findNodeAround(node.sourceFile.ast, node.start, "Function");
+ if (fnNode) {
+ var fnType = fnNode.node.type != "FunctionDeclaration"
+ ? get(fnNode.node, true).getFunctionType()
+ : fnNode.node.scope.fnType;
+ if (fnType) return fnType.retval.getType();
+ }
+ },
+ VariableDeclarator: function(parent, node, get) {
+ if (parent.init == node) return get(parent.id)
+ }
+ };
+ findTypeFromContext.NewExpression = findTypeFromContext.CallExpression
+
+ exports.typeFromContext = function(ast, found) {
+ var parent = exports.parentNode(found.node, ast);
+ var type = null;
+ if (findTypeFromContext.hasOwnProperty(parent.type)) {
+ var finder = findTypeFromContext[parent.type];
+ type = finder && finder(parent, found.node, function(node, fromContext) {
+ var obj = {node: node, state: found.state};
+ var tp = fromContext ? exports.typeFromContext(ast, obj) : exports.expressionType(obj);
+ return tp || ANull;
+ });
+ }
+ return type || exports.expressionType(found);
+ };
+
+ // Flag used to indicate that some wild guessing was used to produce
+ // a type or set of completions.
+ var guessing = false;
+
+ exports.resetGuessing = function(val) { guessing = val; };
+ exports.didGuess = function() { return guessing; };
+
+ exports.forAllPropertiesOf = function(type, f) {
+ type.gatherProperties(f, 0);
+ };
+
+ var refFindWalker = walk.make({}, searchVisitor);
+
+ exports.findRefs = function(ast, baseScope, name, refScope, f) {
+ refFindWalker.Identifier = refFindWalker.VariablePattern = function(node, scope) {
+ if (node.name != name) return;
+ for (var s = scope; s; s = s.prev) {
+ if (s == refScope) f(node, scope);
+ if (name in s.props) return;
+ }
+ };
+ walk.recursive(ast, baseScope, null, refFindWalker);
+ };
+
+ var simpleWalker = walk.make({
+ Function: function(node, _scope, c) {
+ c(node.body, node.scope, node.expression ? "Expression" : "Statement")
+ },
+ Statement: function(node, scope, c) {
+ c(node, node.scope || scope)
+ }
+ });
+
+ exports.findPropRefs = function(ast, scope, objType, propName, f) {
+ walk.simple(ast, {
+ MemberExpression: function(node, scope) {
+ if (node.computed || node.property.name != propName) return;
+ if (findType(node.object, scope).getType() == objType) f(node.property);
+ },
+ ObjectExpression: function(node, scope) {
+ if (findType(node, scope).getType() != objType) return;
+ for (var i = 0; i < node.properties.length; ++i)
+ if (node.properties[i].key.name == propName) f(node.properties[i].key);
+ }
+ }, simpleWalker, scope);
+ };
+
+ // LOCAL-VARIABLE QUERIES
+
+ var scopeAt = exports.scopeAt = function(ast, pos, defaultScope) {
+ var found = walk.findNodeAround(ast, pos, function(_, node) {
+ return node.scope;
+ });
+ if (found) return found.node.scope;
+ else return defaultScope || cx.topScope;
+ };
+
+ exports.forAllLocalsAt = function(ast, pos, defaultScope, f) {
+ var scope = scopeAt(ast, pos, defaultScope);
+ scope.gatherProperties(f, 0);
+ };
+
+ // INIT DEF MODULE
+
+ // Delayed initialization because of cyclic dependencies.
+ def = exports.def = def.init({}, exports);
+});
diff --git a/devtools/client/sourceeditor/tern/moz.build b/devtools/client/sourceeditor/tern/moz.build
new file mode 100644
index 000000000..cba3f5a1b
--- /dev/null
+++ b/devtools/client/sourceeditor/tern/moz.build
@@ -0,0 +1,18 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
+
+DevToolsModules(
+ 'browser.js',
+ 'comment.js',
+ 'condense.js',
+ 'def.js',
+ 'ecma5.js',
+ 'infer.js',
+ 'signal.js',
+ 'tern.js',
+)
diff --git a/devtools/client/sourceeditor/tern/signal.js b/devtools/client/sourceeditor/tern/signal.js
new file mode 100755
index 000000000..5ea64a438
--- /dev/null
+++ b/devtools/client/sourceeditor/tern/signal.js
@@ -0,0 +1,51 @@
+(function(root, mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ return mod(exports);
+ if (typeof define == "function" && define.amd) // AMD
+ return define(["exports"], mod);
+ mod((root.tern || (root.tern = {})).signal = {}); // Plain browser env
+})(this, function(exports) {
+
+ function on(type, f) {
+ var handlers = this._handlers || (this._handlers = Object.create(null));
+ (handlers[type] || (handlers[type] = [])).push(f);
+ }
+
+ function off(type, f) {
+ var arr = this._handlers && this._handlers[type];
+ if (arr) for (var i = 0; i < arr.length; ++i)
+ if (arr[i] == f) { arr.splice(i, 1); break; }
+ }
+
+ var noHandlers = []
+ function getHandlers(emitter, type) {
+ var arr = emitter._handlers && emitter._handlers[type];
+ return arr && arr.length ? arr.slice() : noHandlers
+ }
+
+ function signal(type, a1, a2, a3, a4) {
+ var arr = getHandlers(this, type)
+ for (var i = 0; i < arr.length; ++i) arr[i].call(this, a1, a2, a3, a4)
+ }
+
+ function signalReturnFirst(type, a1, a2, a3, a4) {
+ var arr = getHandlers(this, type)
+ for (var i = 0; i < arr.length; ++i) {
+ var result = arr[i].call(this, a1, a2, a3, a4)
+ if (result) return result
+ }
+ }
+
+ function hasHandler(type) {
+ var arr = this._handlers && this._handlers[type]
+ return arr && arr.length > 0 && arr
+ }
+
+ exports.mixin = function(obj) {
+ obj.on = on; obj.off = off;
+ obj.signal = signal;
+ obj.signalReturnFirst = signalReturnFirst;
+ obj.hasHandler = hasHandler;
+ return obj;
+ };
+});
diff --git a/devtools/client/sourceeditor/tern/tern.js b/devtools/client/sourceeditor/tern/tern.js
new file mode 100755
index 000000000..327797174
--- /dev/null
+++ b/devtools/client/sourceeditor/tern/tern.js
@@ -0,0 +1,1056 @@
+// The Tern server object
+
+// A server is a stateful object that manages the analysis for a
+// project, and defines an interface for querying the code in the
+// project.
+
+(function(root, mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ return mod(exports, require("./infer"), require("./signal"),
+ require("acorn/acorn"), require("acorn/walk"));
+ if (typeof define == "function" && define.amd) // AMD
+ return define(["exports", "./infer", "./signal", "acorn/acorn", "acorn/walk"], mod);
+ mod(root.tern || (root.tern = {}), tern, tern.signal, acorn, acorn.walk); // Plain browser env
+})(this, function(exports, infer, signal, acorn, walk) {
+ "use strict";
+
+ var plugins = Object.create(null);
+ exports.registerPlugin = function(name, init) { plugins[name] = init; };
+
+ var defaultOptions = exports.defaultOptions = {
+ debug: false,
+ async: false,
+ getFile: function(_f, c) { if (this.async) c(null, null); },
+ normalizeFilename: function(name) { return name },
+ defs: [],
+ plugins: {},
+ fetchTimeout: 1000,
+ dependencyBudget: 20000,
+ reuseInstances: true,
+ stripCRs: false,
+ ecmaVersion: 6,
+ projectDir: "/"
+ };
+
+ var queryTypes = {
+ completions: {
+ takesFile: true,
+ run: findCompletions
+ },
+ properties: {
+ run: findProperties
+ },
+ type: {
+ takesFile: true,
+ run: findTypeAt
+ },
+ documentation: {
+ takesFile: true,
+ run: findDocs
+ },
+ definition: {
+ takesFile: true,
+ run: findDef
+ },
+ refs: {
+ takesFile: true,
+ fullFile: true,
+ run: findRefs
+ },
+ rename: {
+ takesFile: true,
+ fullFile: true,
+ run: buildRename
+ },
+ files: {
+ run: listFiles
+ }
+ };
+
+ exports.defineQueryType = function(name, desc) { queryTypes[name] = desc; };
+
+ function File(name, parent) {
+ this.name = name;
+ this.parent = parent;
+ this.scope = this.text = this.ast = this.lineOffsets = null;
+ }
+ File.prototype.asLineChar = function(pos) { return asLineChar(this, pos); };
+
+ function parseFile(srv, file) {
+ var options = {
+ directSourceFile: file,
+ allowReturnOutsideFunction: true,
+ allowImportExportEverywhere: true,
+ ecmaVersion: srv.options.ecmaVersion
+ }
+ var text = srv.signalReturnFirst("preParse", file.text, options) || file.text
+ var ast = infer.parse(text, options)
+ srv.signal("postParse", ast, text)
+ return ast
+ }
+
+ function updateText(file, text, srv) {
+ file.text = srv.options.stripCRs ? text.replace(/\r\n/g, "\n") : text;
+ infer.withContext(srv.cx, function() {
+ file.ast = parseFile(srv, file)
+ });
+ file.lineOffsets = null;
+ }
+
+ var Server = exports.Server = function(options) {
+ this.cx = null;
+ this.options = options || {};
+ for (var o in defaultOptions) if (!options.hasOwnProperty(o))
+ options[o] = defaultOptions[o];
+
+ this.projectDir = options.projectDir.replace(/\\/g, "/")
+ if (!/\/$/.test(this.projectDir)) this.projectDir += "/"
+
+ this.handlers = Object.create(null);
+ this.files = [];
+ this.fileMap = Object.create(null);
+ this.needsPurge = [];
+ this.budgets = Object.create(null);
+ this.uses = 0;
+ this.pending = 0;
+ this.asyncError = null;
+ this.mod = {}
+
+ this.defs = options.defs.slice(0)
+ this.plugins = Object.create(null)
+ for (var plugin in options.plugins) if (options.plugins.hasOwnProperty(plugin))
+ this.loadPlugin(plugin, options.plugins[plugin])
+
+ this.reset();
+ };
+ Server.prototype = signal.mixin({
+ addFile: function(name, /*optional*/ text, parent) {
+ // Don't crash when sloppy plugins pass non-existent parent ids
+ if (parent && !(parent in this.fileMap)) parent = null;
+ if (!(name in this.fileMap))
+ name = this.normalizeFilename(name)
+ ensureFile(this, name, parent, text);
+ },
+ delFile: function(name) {
+ var file = this.findFile(name);
+ if (file) {
+ this.needsPurge.push(file.name);
+ this.files.splice(this.files.indexOf(file), 1);
+ delete this.fileMap[name];
+ }
+ },
+ reset: function() {
+ this.signal("reset");
+ this.cx = new infer.Context(this.defs, this);
+ this.uses = 0;
+ this.budgets = Object.create(null);
+ for (var i = 0; i < this.files.length; ++i) {
+ var file = this.files[i];
+ file.scope = null;
+ }
+ this.signal("postReset");
+ },
+
+ request: function(doc, c) {
+ var inv = invalidDoc(doc);
+ if (inv) return c(inv);
+
+ var self = this;
+ doRequest(this, doc, function(err, data) {
+ c(err, data);
+ if (self.uses > 40) {
+ self.reset();
+ analyzeAll(self, null, function(){});
+ }
+ });
+ },
+
+ findFile: function(name) {
+ return this.fileMap[name];
+ },
+
+ flush: function(c) {
+ var cx = this.cx;
+ analyzeAll(this, null, function(err) {
+ if (err) return c(err);
+ infer.withContext(cx, c);
+ });
+ },
+
+ startAsyncAction: function() {
+ ++this.pending;
+ },
+ finishAsyncAction: function(err) {
+ if (err) this.asyncError = err;
+ if (--this.pending === 0) this.signal("everythingFetched");
+ },
+
+ addDefs: function(defs, toFront) {
+ if (toFront) this.defs.unshift(defs)
+ else this.defs.push(defs)
+
+ if (this.cx) this.reset()
+ },
+
+ loadPlugin: function(name, options) {
+ if (arguments.length == 1) options = this.options.plugins[name] || true
+ if (name in this.plugins || !(name in plugins) || !options) return
+ this.plugins[name] = true
+ var init = plugins[name](this, options)
+
+ // This is for backwards-compatibilty. Don't rely on it -- use addDef and on directly
+ if (!init) return
+ if (init.defs) this.addDefs(init.defs, init.loadFirst)
+ if (init.passes) for (var type in init.passes) if (init.passes.hasOwnProperty(type))
+ this.on(type, init.passes[type])
+ },
+
+ normalizeFilename: function(name) {
+ var norm = this.options.normalizeFilename(name).replace(/\\/g, "/")
+ if (norm.indexOf(this.projectDir) == 0) norm = norm.slice(this.projectDir.length)
+ return norm
+ }
+ });
+
+ function doRequest(srv, doc, c) {
+ if (doc.query && !queryTypes.hasOwnProperty(doc.query.type))
+ return c("No query type '" + doc.query.type + "' defined");
+
+ var query = doc.query;
+ // Respond as soon as possible when this just uploads files
+ if (!query) c(null, {});
+
+ var files = doc.files || [];
+ if (files.length) ++srv.uses;
+ for (var i = 0; i < files.length; ++i) {
+ var file = files[i];
+ if (file.type == "delete")
+ srv.delFile(file.name);
+ else
+ ensureFile(srv, file.name, null, file.type == "full" ? file.text : null);
+ }
+
+ var timeBudget = typeof doc.timeout == "number" ? [doc.timeout] : null;
+ if (!query) {
+ analyzeAll(srv, timeBudget, function(){});
+ return;
+ }
+
+ var queryType = queryTypes[query.type];
+ if (queryType.takesFile) {
+ if (typeof query.file != "string") return c(".query.file must be a string");
+ if (!/^#/.test(query.file)) ensureFile(srv, query.file, null);
+ }
+
+ analyzeAll(srv, timeBudget, function(err) {
+ if (err) return c(err);
+ var file = queryType.takesFile && resolveFile(srv, files, query.file);
+ if (queryType.fullFile && file.type == "part")
+ return c("Can't run a " + query.type + " query on a file fragment");
+
+ function run() {
+ var result;
+ try {
+ result = queryType.run(srv, query, file);
+ } catch (e) {
+ if (srv.options.debug && e.name != "TernError") console.error(e.stack);
+ return c(e);
+ }
+ c(null, result);
+ }
+ infer.resetGuessing()
+ infer.withContext(srv.cx, timeBudget ? function() { infer.withTimeout(timeBudget[0], run); } : run);
+ });
+ }
+
+ function analyzeFile(srv, file) {
+ infer.withContext(srv.cx, function() {
+ file.scope = srv.cx.topScope;
+ srv.signal("beforeLoad", file);
+ infer.analyze(file.ast, file.name, file.scope);
+ srv.signal("afterLoad", file);
+ });
+ return file;
+ }
+
+ function ensureFile(srv, name, parent, text) {
+ var known = srv.findFile(name);
+ if (known) {
+ if (text != null) {
+ if (known.scope) {
+ srv.needsPurge.push(name);
+ known.scope = null;
+ }
+ updateText(known, text, srv);
+ }
+ if (parentDepth(srv, known.parent) > parentDepth(srv, parent)) {
+ known.parent = parent;
+ if (known.excluded) known.excluded = null;
+ }
+ return;
+ }
+
+ var file = new File(name, parent);
+ srv.files.push(file);
+ srv.fileMap[name] = file;
+ if (text != null) {
+ updateText(file, text, srv);
+ } else if (srv.options.async) {
+ srv.startAsyncAction();
+ srv.options.getFile(name, function(err, text) {
+ updateText(file, text || "", srv);
+ srv.finishAsyncAction(err);
+ });
+ } else {
+ updateText(file, srv.options.getFile(name) || "", srv);
+ }
+ }
+
+ function fetchAll(srv, c) {
+ var done = true, returned = false;
+ srv.files.forEach(function(file) {
+ if (file.text != null) return;
+ if (srv.options.async) {
+ done = false;
+ srv.options.getFile(file.name, function(err, text) {
+ if (err && !returned) { returned = true; return c(err); }
+ updateText(file, text || "", srv);
+ fetchAll(srv, c);
+ });
+ } else {
+ try {
+ updateText(file, srv.options.getFile(file.name) || "", srv);
+ } catch (e) { return c(e); }
+ }
+ });
+ if (done) c();
+ }
+
+ function waitOnFetch(srv, timeBudget, c) {
+ var done = function() {
+ srv.off("everythingFetched", done);
+ clearTimeout(timeout);
+ analyzeAll(srv, timeBudget, c);
+ };
+ srv.on("everythingFetched", done);
+ var timeout = setTimeout(done, srv.options.fetchTimeout);
+ }
+
+ function analyzeAll(srv, timeBudget, c) {
+ if (srv.pending) return waitOnFetch(srv, timeBudget, c);
+
+ var e = srv.fetchError;
+ if (e) { srv.fetchError = null; return c(e); }
+
+ if (srv.needsPurge.length > 0) infer.withContext(srv.cx, function() {
+ infer.purge(srv.needsPurge);
+ srv.needsPurge.length = 0;
+ });
+
+ var done = true;
+ // The second inner loop might add new files. The outer loop keeps
+ // repeating both inner loops until all files have been looked at.
+ for (var i = 0; i < srv.files.length;) {
+ var toAnalyze = [];
+ for (; i < srv.files.length; ++i) {
+ var file = srv.files[i];
+ if (file.text == null) done = false;
+ else if (file.scope == null && !file.excluded) toAnalyze.push(file);
+ }
+ toAnalyze.sort(function(a, b) {
+ return parentDepth(srv, a.parent) - parentDepth(srv, b.parent);
+ });
+ for (var j = 0; j < toAnalyze.length; j++) {
+ var file = toAnalyze[j];
+ if (file.parent && !chargeOnBudget(srv, file)) {
+ file.excluded = true;
+ } else if (timeBudget) {
+ var startTime = +new Date;
+ infer.withTimeout(timeBudget[0], function() { analyzeFile(srv, file); });
+ timeBudget[0] -= +new Date - startTime;
+ } else {
+ analyzeFile(srv, file);
+ }
+ }
+ }
+ if (done) c();
+ else waitOnFetch(srv, timeBudget, c);
+ }
+
+ function firstLine(str) {
+ var end = str.indexOf("\n");
+ if (end < 0) return str;
+ return str.slice(0, end);
+ }
+
+ function findMatchingPosition(line, file, near) {
+ var pos = Math.max(0, near - 500), closest = null;
+ if (!/^\s*$/.test(line)) for (;;) {
+ var found = file.indexOf(line, pos);
+ if (found < 0 || found > near + 500) break;
+ if (closest == null || Math.abs(closest - near) > Math.abs(found - near))
+ closest = found;
+ pos = found + line.length;
+ }
+ return closest;
+ }
+
+ function scopeDepth(s) {
+ for (var i = 0; s; ++i, s = s.prev) {}
+ return i;
+ }
+
+ function ternError(msg) {
+ var err = new Error(msg);
+ err.name = "TernError";
+ return err;
+ }
+
+ function resolveFile(srv, localFiles, name) {
+ var isRef = name.match(/^#(\d+)$/);
+ if (!isRef) return srv.findFile(name);
+
+ var file = localFiles[isRef[1]];
+ if (!file || file.type == "delete") throw ternError("Reference to unknown file " + name);
+ if (file.type == "full") return srv.findFile(file.name);
+
+ // This is a partial file
+
+ var realFile = file.backing = srv.findFile(file.name);
+ var offset = file.offset;
+ if (file.offsetLines) offset = {line: file.offsetLines, ch: 0};
+ file.offset = offset = resolvePos(realFile, file.offsetLines == null ? file.offset : {line: file.offsetLines, ch: 0}, true);
+ var line = firstLine(file.text);
+ var foundPos = findMatchingPosition(line, realFile.text, offset);
+ var pos = foundPos == null ? Math.max(0, realFile.text.lastIndexOf("\n", offset)) : foundPos;
+ var inObject, atFunction;
+
+ infer.withContext(srv.cx, function() {
+ infer.purge(file.name, pos, pos + file.text.length);
+
+ var text = file.text, m;
+ if (m = text.match(/(?:"([^"]*)"|([\w$]+))\s*:\s*function\b/)) {
+ var objNode = walk.findNodeAround(file.backing.ast, pos, "ObjectExpression");
+ if (objNode && objNode.node.objType)
+ inObject = {type: objNode.node.objType, prop: m[2] || m[1]};
+ }
+ if (foundPos && (m = line.match(/^(.*?)\bfunction\b/))) {
+ var cut = m[1].length, white = "";
+ for (var i = 0; i < cut; ++i) white += " ";
+ file.text = white + text.slice(cut);
+ atFunction = true;
+ }
+
+ var scopeStart = infer.scopeAt(realFile.ast, pos, realFile.scope);
+ var scopeEnd = infer.scopeAt(realFile.ast, pos + text.length, realFile.scope);
+ var scope = file.scope = scopeDepth(scopeStart) < scopeDepth(scopeEnd) ? scopeEnd : scopeStart;
+ file.ast = parseFile(srv, file)
+ infer.analyze(file.ast, file.name, scope);
+
+ // This is a kludge to tie together the function types (if any)
+ // outside and inside of the fragment, so that arguments and
+ // return values have some information known about them.
+ tieTogether: if (inObject || atFunction) {
+ var newInner = infer.scopeAt(file.ast, line.length, scopeStart);
+ if (!newInner.fnType) break tieTogether;
+ if (inObject) {
+ var prop = inObject.type.getProp(inObject.prop);
+ prop.addType(newInner.fnType);
+ } else if (atFunction) {
+ var inner = infer.scopeAt(realFile.ast, pos + line.length, realFile.scope);
+ if (inner == scopeStart || !inner.fnType) break tieTogether;
+ var fOld = inner.fnType, fNew = newInner.fnType;
+ if (!fNew || (fNew.name != fOld.name && fOld.name)) break tieTogether;
+ for (var i = 0, e = Math.min(fOld.args.length, fNew.args.length); i < e; ++i)
+ fOld.args[i].propagate(fNew.args[i]);
+ fOld.self.propagate(fNew.self);
+ fNew.retval.propagate(fOld.retval);
+ }
+ }
+ });
+ return file;
+ }
+
+ // Budget management
+
+ function astSize(node) {
+ var size = 0;
+ walk.simple(node, {Expression: function() { ++size; }});
+ return size;
+ }
+
+ function parentDepth(srv, parent) {
+ var depth = 0;
+ while (parent) {
+ parent = srv.findFile(parent).parent;
+ ++depth;
+ }
+ return depth;
+ }
+
+ function budgetName(srv, file) {
+ for (;;) {
+ var parent = srv.findFile(file.parent);
+ if (!parent.parent) break;
+ file = parent;
+ }
+ return file.name;
+ }
+
+ function chargeOnBudget(srv, file) {
+ var bName = budgetName(srv, file);
+ var size = astSize(file.ast);
+ var known = srv.budgets[bName];
+ if (known == null)
+ known = srv.budgets[bName] = srv.options.dependencyBudget;
+ if (known < size) return false;
+ srv.budgets[bName] = known - size;
+ return true;
+ }
+
+ // Query helpers
+
+ function isPosition(val) {
+ return typeof val == "number" || typeof val == "object" &&
+ typeof val.line == "number" && typeof val.ch == "number";
+ }
+
+ // Baseline query document validation
+ function invalidDoc(doc) {
+ if (doc.query) {
+ if (typeof doc.query.type != "string") return ".query.type must be a string";
+ if (doc.query.start && !isPosition(doc.query.start)) return ".query.start must be a position";
+ if (doc.query.end && !isPosition(doc.query.end)) return ".query.end must be a position";
+ }
+ if (doc.files) {
+ if (!Array.isArray(doc.files)) return "Files property must be an array";
+ for (var i = 0; i < doc.files.length; ++i) {
+ var file = doc.files[i];
+ if (typeof file != "object") return ".files[n] must be objects";
+ else if (typeof file.name != "string") return ".files[n].name must be a string";
+ else if (file.type == "delete") continue;
+ else if (typeof file.text != "string") return ".files[n].text must be a string";
+ else if (file.type == "part") {
+ if (!isPosition(file.offset) && typeof file.offsetLines != "number")
+ return ".files[n].offset must be a position";
+ } else if (file.type != "full") return ".files[n].type must be \"full\" or \"part\"";
+ }
+ }
+ }
+
+ var offsetSkipLines = 25;
+
+ function findLineStart(file, line) {
+ var text = file.text, offsets = file.lineOffsets || (file.lineOffsets = [0]);
+ var pos = 0, curLine = 0;
+ var storePos = Math.min(Math.floor(line / offsetSkipLines), offsets.length - 1);
+ var pos = offsets[storePos], curLine = storePos * offsetSkipLines;
+
+ while (curLine < line) {
+ ++curLine;
+ pos = text.indexOf("\n", pos) + 1;
+ if (pos === 0) return null;
+ if (curLine % offsetSkipLines === 0) offsets.push(pos);
+ }
+ return pos;
+ }
+
+ var resolvePos = exports.resolvePos = function(file, pos, tolerant) {
+ if (typeof pos != "number") {
+ var lineStart = findLineStart(file, pos.line);
+ if (lineStart == null) {
+ if (tolerant) pos = file.text.length;
+ else throw ternError("File doesn't contain a line " + pos.line);
+ } else {
+ pos = lineStart + pos.ch;
+ }
+ }
+ if (pos > file.text.length) {
+ if (tolerant) pos = file.text.length;
+ else throw ternError("Position " + pos + " is outside of file.");
+ }
+ return pos;
+ };
+
+ function asLineChar(file, pos) {
+ if (!file) return {line: 0, ch: 0};
+ var offsets = file.lineOffsets || (file.lineOffsets = [0]);
+ var text = file.text, line, lineStart;
+ for (var i = offsets.length - 1; i >= 0; --i) if (offsets[i] <= pos) {
+ line = i * offsetSkipLines;
+ lineStart = offsets[i];
+ }
+ for (;;) {
+ var eol = text.indexOf("\n", lineStart);
+ if (eol >= pos || eol < 0) break;
+ lineStart = eol + 1;
+ ++line;
+ }
+ return {line: line, ch: pos - lineStart};
+ }
+
+ var outputPos = exports.outputPos = function(query, file, pos) {
+ if (query.lineCharPositions) {
+ var out = asLineChar(file, pos);
+ if (file.type == "part")
+ out.line += file.offsetLines != null ? file.offsetLines : asLineChar(file.backing, file.offset).line;
+ return out;
+ } else {
+ return pos + (file.type == "part" ? file.offset : 0);
+ }
+ };
+
+ // Delete empty fields from result objects
+ function clean(obj) {
+ for (var prop in obj) if (obj[prop] == null) delete obj[prop];
+ return obj;
+ }
+ function maybeSet(obj, prop, val) {
+ if (val != null) obj[prop] = val;
+ }
+
+ // Built-in query types
+
+ function compareCompletions(a, b) {
+ if (typeof a != "string") { a = a.name; b = b.name; }
+ var aUp = /^[A-Z]/.test(a), bUp = /^[A-Z]/.test(b);
+ if (aUp == bUp) return a < b ? -1 : a == b ? 0 : 1;
+ else return aUp ? 1 : -1;
+ }
+
+ function isStringAround(node, start, end) {
+ return node.type == "Literal" && typeof node.value == "string" &&
+ node.start == start - 1 && node.end <= end + 1;
+ }
+
+ function pointInProp(objNode, point) {
+ for (var i = 0; i < objNode.properties.length; i++) {
+ var curProp = objNode.properties[i];
+ if (curProp.key.start <= point && curProp.key.end >= point)
+ return curProp;
+ }
+ }
+
+ var jsKeywords = ("break do instanceof typeof case else new var " +
+ "catch finally return void continue for switch while debugger " +
+ "function this with default if throw delete in try").split(" ");
+
+ var addCompletion = exports.addCompletion = function(query, completions, name, aval, depth) {
+ var typeInfo = query.types || query.docs || query.urls || query.origins;
+ var wrapAsObjs = typeInfo || query.depths;
+
+ for (var i = 0; i < completions.length; ++i) {
+ var c = completions[i];
+ if ((wrapAsObjs ? c.name : c) == name) return;
+ }
+ var rec = wrapAsObjs ? {name: name} : name;
+ completions.push(rec);
+
+ if (aval && typeInfo) {
+ infer.resetGuessing();
+ var type = aval.getType();
+ rec.guess = infer.didGuess();
+ if (query.types)
+ rec.type = infer.toString(aval);
+ if (query.docs)
+ maybeSet(rec, "doc", parseDoc(query, aval.doc || type && type.doc));
+ if (query.urls)
+ maybeSet(rec, "url", aval.url || type && type.url);
+ if (query.origins)
+ maybeSet(rec, "origin", aval.origin || type && type.origin);
+ }
+ if (query.depths) rec.depth = depth || 0;
+ return rec;
+ };
+
+ function findCompletions(srv, query, file) {
+ if (query.end == null) throw ternError("missing .query.end field");
+ var fromPlugin = srv.signalReturnFirst("completion", file, query)
+ if (fromPlugin) return fromPlugin
+
+ var wordStart = resolvePos(file, query.end), wordEnd = wordStart, text = file.text;
+ while (wordStart && acorn.isIdentifierChar(text.charCodeAt(wordStart - 1))) --wordStart;
+ if (query.expandWordForward !== false)
+ while (wordEnd < text.length && acorn.isIdentifierChar(text.charCodeAt(wordEnd))) ++wordEnd;
+ var word = text.slice(wordStart, wordEnd), completions = [], ignoreObj;
+ if (query.caseInsensitive) word = word.toLowerCase();
+
+ function gather(prop, obj, depth, addInfo) {
+ // 'hasOwnProperty' and such are usually just noise, leave them
+ // out when no prefix is provided.
+ if ((objLit || query.omitObjectPrototype !== false) && obj == srv.cx.protos.Object && !word) return;
+ if (query.filter !== false && word &&
+ (query.caseInsensitive ? prop.toLowerCase() : prop).indexOf(word) !== 0) return;
+ if (ignoreObj && ignoreObj.props[prop]) return;
+ var result = addCompletion(query, completions, prop, obj && obj.props[prop], depth);
+ if (addInfo && result && typeof result != "string") addInfo(result);
+ }
+
+ var hookname, prop, objType, isKey;
+
+ var exprAt = infer.findExpressionAround(file.ast, null, wordStart, file.scope);
+ var memberExpr, objLit;
+ // Decide whether this is an object property, either in a member
+ // expression or an object literal.
+ if (exprAt) {
+ var exprNode = exprAt.node;
+ if (exprNode.type == "MemberExpression" && exprNode.object.end < wordStart) {
+ memberExpr = exprAt;
+ } else if (isStringAround(exprNode, wordStart, wordEnd)) {
+ var parent = infer.parentNode(exprNode, file.ast);
+ if (parent.type == "MemberExpression" && parent.property == exprNode)
+ memberExpr = {node: parent, state: exprAt.state};
+ } else if (exprNode.type == "ObjectExpression") {
+ var objProp = pointInProp(exprNode, wordEnd);
+ if (objProp) {
+ objLit = exprAt;
+ prop = isKey = objProp.key.name;
+ } else if (!word && !/:\s*$/.test(file.text.slice(0, wordStart))) {
+ objLit = exprAt;
+ prop = isKey = true;
+ }
+ }
+ }
+
+ if (objLit) {
+ // Since we can't use the type of the literal itself to complete
+ // its properties (it doesn't contain the information we need),
+ // we have to try asking the surrounding expression for type info.
+ objType = infer.typeFromContext(file.ast, objLit);
+ ignoreObj = objLit.node.objType;
+ } else if (memberExpr) {
+ prop = memberExpr.node.property;
+ prop = prop.type == "Literal" ? prop.value.slice(1) : prop.name;
+ memberExpr.node = memberExpr.node.object;
+ objType = infer.expressionType(memberExpr);
+ } else if (text.charAt(wordStart - 1) == ".") {
+ var pathStart = wordStart - 1;
+ while (pathStart && (text.charAt(pathStart - 1) == "." || acorn.isIdentifierChar(text.charCodeAt(pathStart - 1)))) pathStart--;
+ var path = text.slice(pathStart, wordStart - 1);
+ if (path) {
+ objType = infer.def.parsePath(path, file.scope).getObjType();
+ prop = word;
+ }
+ }
+
+ if (prop != null) {
+ srv.cx.completingProperty = prop;
+
+ if (objType) infer.forAllPropertiesOf(objType, gather);
+
+ if (!completions.length && query.guess !== false && objType && objType.guessProperties)
+ objType.guessProperties(function(p, o, d) {if (p != prop && p != "✖") gather(p, o, d);});
+ if (!completions.length && word.length >= 2 && query.guess !== false)
+ for (var prop in srv.cx.props) gather(prop, srv.cx.props[prop][0], 0);
+ hookname = "memberCompletion";
+ } else {
+ infer.forAllLocalsAt(file.ast, wordStart, file.scope, gather);
+ if (query.includeKeywords) jsKeywords.forEach(function(kw) {
+ gather(kw, null, 0, function(rec) { rec.isKeyword = true; });
+ });
+ hookname = "variableCompletion";
+ }
+ srv.signal(hookname, file, wordStart, wordEnd, gather)
+
+ if (query.sort !== false) completions.sort(compareCompletions);
+ srv.cx.completingProperty = null;
+
+ return {start: outputPos(query, file, wordStart),
+ end: outputPos(query, file, wordEnd),
+ isProperty: !!prop,
+ isObjectKey: !!isKey,
+ completions: completions};
+ }
+
+ function findProperties(srv, query) {
+ var prefix = query.prefix, found = [];
+ for (var prop in srv.cx.props)
+ if (prop != "<i>" && (!prefix || prop.indexOf(prefix) === 0)) found.push(prop);
+ if (query.sort !== false) found.sort(compareCompletions);
+ return {completions: found};
+ }
+
+ var findExpr = exports.findQueryExpr = function(file, query, wide) {
+ if (query.end == null) throw ternError("missing .query.end field");
+
+ if (query.variable) {
+ var scope = infer.scopeAt(file.ast, resolvePos(file, query.end), file.scope);
+ return {node: {type: "Identifier", name: query.variable, start: query.end, end: query.end + 1},
+ state: scope};
+ } else {
+ var start = query.start && resolvePos(file, query.start), end = resolvePos(file, query.end);
+ var expr = infer.findExpressionAt(file.ast, start, end, file.scope);
+ if (expr) return expr;
+ expr = infer.findExpressionAround(file.ast, start, end, file.scope);
+ if (expr && (expr.node.type == "ObjectExpression" || wide ||
+ (start == null ? end : start) - expr.node.start < 20 || expr.node.end - end < 20))
+ return expr;
+ return null;
+ }
+ };
+
+ function findExprOrThrow(file, query, wide) {
+ var expr = findExpr(file, query, wide);
+ if (expr) return expr;
+ throw ternError("No expression at the given position.");
+ }
+
+ function ensureObj(tp) {
+ if (!tp || !(tp = tp.getType()) || !(tp instanceof infer.Obj)) return null;
+ return tp;
+ }
+
+ function findExprType(srv, query, file, expr) {
+ var type;
+ if (expr) {
+ infer.resetGuessing();
+ type = infer.expressionType(expr);
+ }
+ var typeHandlers = srv.hasHandler("typeAt")
+ if (typeHandlers) {
+ var pos = resolvePos(file, query.end)
+ for (var i = 0; i < typeHandlers.length; i++)
+ type = typeHandlers[i](file, pos, expr, type)
+ }
+ if (!type) throw ternError("No type found at the given position.");
+
+ var objProp;
+ if (expr.node.type == "ObjectExpression" && query.end != null &&
+ (objProp = pointInProp(expr.node, resolvePos(file, query.end)))) {
+ var name = objProp.key.name;
+ var fromCx = ensureObj(infer.typeFromContext(file.ast, expr));
+ if (fromCx && fromCx.hasProp(name)) {
+ type = fromCx.hasProp(name);
+ } else {
+ var fromLocal = ensureObj(type);
+ if (fromLocal && fromLocal.hasProp(name))
+ type = fromLocal.hasProp(name);
+ }
+ }
+ return type;
+ };
+
+ function findTypeAt(srv, query, file) {
+ var expr = findExpr(file, query), exprName;
+ var type = findExprType(srv, query, file, expr), exprType = type;
+ if (query.preferFunction)
+ type = type.getFunctionType() || type.getType();
+ else
+ type = type.getType();
+
+ if (expr) {
+ if (expr.node.type == "Identifier")
+ exprName = expr.node.name;
+ else if (expr.node.type == "MemberExpression" && !expr.node.computed)
+ exprName = expr.node.property.name;
+ }
+
+ if (query.depth != null && typeof query.depth != "number")
+ throw ternError(".query.depth must be a number");
+
+ var result = {guess: infer.didGuess(),
+ type: infer.toString(exprType, query.depth),
+ name: type && type.name,
+ exprName: exprName,
+ doc: exprType.doc,
+ url: exprType.url};
+ if (type) storeTypeDocs(query, type, result);
+
+ return clean(result);
+ }
+
+ function parseDoc(query, doc) {
+ if (!doc) return null;
+ if (query.docFormat == "full") return doc;
+ var parabreak = /.\n[\s@\n]/.exec(doc);
+ if (parabreak) doc = doc.slice(0, parabreak.index + 1);
+ doc = doc.replace(/\n\s*/g, " ");
+ if (doc.length < 100) return doc;
+ var sentenceEnd = /[\.!?] [A-Z]/g;
+ sentenceEnd.lastIndex = 80;
+ var found = sentenceEnd.exec(doc);
+ if (found) doc = doc.slice(0, found.index + 1);
+ return doc;
+ }
+
+ function findDocs(srv, query, file) {
+ var expr = findExpr(file, query);
+ var type = findExprType(srv, query, file, expr);
+ var result = {url: type.url, doc: parseDoc(query, type.doc), type: infer.toString(type)};
+ var inner = type.getType();
+ if (inner) storeTypeDocs(query, inner, result);
+ return clean(result);
+ }
+
+ function storeTypeDocs(query, type, out) {
+ if (!out.url) out.url = type.url;
+ if (!out.doc) out.doc = parseDoc(query, type.doc);
+ if (!out.origin) out.origin = type.origin;
+ var ctor, boring = infer.cx().protos;
+ if (!out.url && !out.doc && type.proto && (ctor = type.proto.hasCtor) &&
+ type.proto != boring.Object && type.proto != boring.Function && type.proto != boring.Array) {
+ out.url = ctor.url;
+ out.doc = parseDoc(query, ctor.doc);
+ }
+ }
+
+ var getSpan = exports.getSpan = function(obj) {
+ if (!obj.origin) return;
+ if (obj.originNode) {
+ var node = obj.originNode;
+ if (/^Function/.test(node.type) && node.id) node = node.id;
+ return {origin: obj.origin, node: node};
+ }
+ if (obj.span) return {origin: obj.origin, span: obj.span};
+ };
+
+ var storeSpan = exports.storeSpan = function(srv, query, span, target) {
+ target.origin = span.origin;
+ if (span.span) {
+ var m = /^(\d+)\[(\d+):(\d+)\]-(\d+)\[(\d+):(\d+)\]$/.exec(span.span);
+ target.start = query.lineCharPositions ? {line: Number(m[2]), ch: Number(m[3])} : Number(m[1]);
+ target.end = query.lineCharPositions ? {line: Number(m[5]), ch: Number(m[6])} : Number(m[4]);
+ } else {
+ var file = srv.findFile(span.origin);
+ target.start = outputPos(query, file, span.node.start);
+ target.end = outputPos(query, file, span.node.end);
+ }
+ };
+
+ function findDef(srv, query, file) {
+ var expr = findExpr(file, query);
+ var type = findExprType(srv, query, file, expr);
+ if (infer.didGuess()) return {};
+
+ var span = getSpan(type);
+ var result = {url: type.url, doc: parseDoc(query, type.doc), origin: type.origin};
+
+ if (type.types) for (var i = type.types.length - 1; i >= 0; --i) {
+ var tp = type.types[i];
+ storeTypeDocs(query, tp, result);
+ if (!span) span = getSpan(tp);
+ }
+
+ if (span && span.node) { // refers to a loaded file
+ var spanFile = span.node.sourceFile || srv.findFile(span.origin);
+ var start = outputPos(query, spanFile, span.node.start), end = outputPos(query, spanFile, span.node.end);
+ result.start = start; result.end = end;
+ result.file = span.origin;
+ var cxStart = Math.max(0, span.node.start - 50);
+ result.contextOffset = span.node.start - cxStart;
+ result.context = spanFile.text.slice(cxStart, cxStart + 50);
+ } else if (span) { // external
+ result.file = span.origin;
+ storeSpan(srv, query, span, result);
+ }
+ return clean(result);
+ }
+
+ function findRefsToVariable(srv, query, file, expr, checkShadowing) {
+ var name = expr.node.name;
+
+ for (var scope = expr.state; scope && !(name in scope.props); scope = scope.prev) {}
+ if (!scope) throw ternError("Could not find a definition for " + name);
+
+ var type, refs = [];
+ function storeRef(file) {
+ return function(node, scopeHere) {
+ if (checkShadowing) for (var s = scopeHere; s != scope; s = s.prev) {
+ var exists = s.hasProp(checkShadowing);
+ if (exists)
+ throw ternError("Renaming `" + name + "` to `" + checkShadowing + "` would make a variable at line " +
+ (asLineChar(file, node.start).line + 1) + " point to the definition at line " +
+ (asLineChar(file, exists.name.start).line + 1));
+ }
+ refs.push({file: file.name,
+ start: outputPos(query, file, node.start),
+ end: outputPos(query, file, node.end)});
+ };
+ }
+
+ if (scope.originNode) {
+ type = "local";
+ if (checkShadowing) {
+ for (var prev = scope.prev; prev; prev = prev.prev)
+ if (checkShadowing in prev.props) break;
+ if (prev) infer.findRefs(scope.originNode, scope, checkShadowing, prev, function(node) {
+ throw ternError("Renaming `" + name + "` to `" + checkShadowing + "` would shadow the definition used at line " +
+ (asLineChar(file, node.start).line + 1));
+ });
+ }
+ infer.findRefs(scope.originNode, scope, name, scope, storeRef(file));
+ } else {
+ type = "global";
+ for (var i = 0; i < srv.files.length; ++i) {
+ var cur = srv.files[i];
+ infer.findRefs(cur.ast, cur.scope, name, scope, storeRef(cur));
+ }
+ }
+
+ return {refs: refs, type: type, name: name};
+ }
+
+ function findRefsToProperty(srv, query, expr, prop) {
+ var objType = infer.expressionType(expr).getObjType();
+ if (!objType) throw ternError("Couldn't determine type of base object.");
+
+ var refs = [];
+ function storeRef(file) {
+ return function(node) {
+ refs.push({file: file.name,
+ start: outputPos(query, file, node.start),
+ end: outputPos(query, file, node.end)});
+ };
+ }
+ for (var i = 0; i < srv.files.length; ++i) {
+ var cur = srv.files[i];
+ infer.findPropRefs(cur.ast, cur.scope, objType, prop.name, storeRef(cur));
+ }
+
+ return {refs: refs, name: prop.name};
+ }
+
+ function findRefs(srv, query, file) {
+ var expr = findExprOrThrow(file, query, true);
+ if (expr && expr.node.type == "Identifier") {
+ return findRefsToVariable(srv, query, file, expr);
+ } else if (expr && expr.node.type == "MemberExpression" && !expr.node.computed) {
+ var p = expr.node.property;
+ expr.node = expr.node.object;
+ return findRefsToProperty(srv, query, expr, p);
+ } else if (expr && expr.node.type == "ObjectExpression") {
+ var pos = resolvePos(file, query.end);
+ for (var i = 0; i < expr.node.properties.length; ++i) {
+ var k = expr.node.properties[i].key;
+ if (k.start <= pos && k.end >= pos)
+ return findRefsToProperty(srv, query, expr, k);
+ }
+ }
+ throw ternError("Not at a variable or property name.");
+ }
+
+ function buildRename(srv, query, file) {
+ if (typeof query.newName != "string") throw ternError(".query.newName should be a string");
+ var expr = findExprOrThrow(file, query);
+ if (!expr || expr.node.type != "Identifier") throw ternError("Not at a variable.");
+
+ var data = findRefsToVariable(srv, query, file, expr, query.newName), refs = data.refs;
+ delete data.refs;
+ data.files = srv.files.map(function(f){return f.name;});
+
+ var changes = data.changes = [];
+ for (var i = 0; i < refs.length; ++i) {
+ var use = refs[i];
+ use.text = query.newName;
+ changes.push(use);
+ }
+
+ return data;
+ }
+
+ function listFiles(srv) {
+ return {files: srv.files.map(function(f){return f.name;})};
+ }
+
+ exports.version = "0.16.0";
+});
diff --git a/devtools/client/sourceeditor/tern/tests/unit/head_tern.js b/devtools/client/sourceeditor/tern/tests/unit/head_tern.js
new file mode 100644
index 000000000..1ab102685
--- /dev/null
+++ b/devtools/client/sourceeditor/tern/tests/unit/head_tern.js
@@ -0,0 +1,3 @@
+"use strict";
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
diff --git a/devtools/client/sourceeditor/tern/tests/unit/test_autocompletion.js b/devtools/client/sourceeditor/tern/tests/unit/test_autocompletion.js
new file mode 100644
index 000000000..493d6fb18
--- /dev/null
+++ b/devtools/client/sourceeditor/tern/tests/unit/test_autocompletion.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that tern autocompletions work.
+ */
+
+const tern = require("devtools/client/sourceeditor/tern/tern");
+const ecma5 = require("devtools/client/sourceeditor/tern/ecma5");
+
+function run_test() {
+ do_test_pending();
+
+ const server = new tern.Server({ defs: [ecma5] });
+ const code = "[].";
+ const query = { type: "completions", file: "test", end: code.length };
+ const files = [{ type: "full", name: "test", text: code }];
+
+ server.request({ query: query, files: files }, (error, response) => {
+ do_check_eq(error, null);
+ do_check_true(!!response);
+ do_check_true(Array.isArray(response.completions));
+ do_check_true(response.completions.indexOf("concat") != -1);
+ do_test_finished();
+ });
+}
diff --git a/devtools/client/sourceeditor/tern/tests/unit/test_import_tern.js b/devtools/client/sourceeditor/tern/tests/unit/test_import_tern.js
new file mode 100644
index 000000000..74f68fe60
--- /dev/null
+++ b/devtools/client/sourceeditor/tern/tests/unit/test_import_tern.js
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that we can require tern.
+ */
+
+function run_test() {
+ const tern = require("devtools/client/sourceeditor/tern/tern");
+ const ecma5 = require("devtools/client/sourceeditor/tern/ecma5");
+ const browser = require("devtools/client/sourceeditor/tern/browser");
+ do_check_true(!!tern);
+ do_check_true(!!ecma5);
+ do_check_true(!!browser);
+ do_check_eq(typeof tern.Server, "function");
+}
diff --git a/devtools/client/sourceeditor/tern/tests/unit/xpcshell.ini b/devtools/client/sourceeditor/tern/tests/unit/xpcshell.ini
new file mode 100644
index 000000000..567d8524d
--- /dev/null
+++ b/devtools/client/sourceeditor/tern/tests/unit/xpcshell.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+tags = devtools
+head = head_tern.js
+tail =
+firefox-appdir = browser
+
+[test_autocompletion.js]
+[test_import_tern.js]
diff --git a/devtools/client/sourceeditor/test/.eslintrc.js b/devtools/client/sourceeditor/test/.eslintrc.js
new file mode 100644
index 000000000..8d15a76d9
--- /dev/null
+++ b/devtools/client/sourceeditor/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/sourceeditor/test/browser.ini b/devtools/client/sourceeditor/test/browser.ini
new file mode 100644
index 000000000..c084d1d80
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser.ini
@@ -0,0 +1,48 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ codemirror/comment_test.js
+ codemirror/doc_test.js
+ codemirror/driver.js
+ codemirror/emacs_test.js
+ codemirror/mode_test.css
+ codemirror/mode_test.js
+ codemirror/multi_test.js
+ codemirror/search_test.js
+ codemirror/sublime_test.js
+ codemirror/test.js
+ codemirror/vim_test.js
+ codemirror/codemirror.html
+ codemirror/vimemacs.html
+ codemirror/mode/javascript/test.js
+ css_statemachine_testcases.css
+ css_statemachine_tests.json
+ css_autocompletion_tests.json
+ head.js
+ helper_codemirror_runner.js
+ cm_mode_ruby.js
+ cm_script_injection_test.js
+ !/devtools/client/framework/test/shared-head.js
+
+[browser_editor_autocomplete_basic.js]
+[browser_editor_autocomplete_events.js]
+[browser_editor_autocomplete_js.js]
+[browser_editor_basic.js]
+[browser_editor_cursor.js]
+[browser_editor_find_again.js]
+[browser_editor_goto_line.js]
+[browser_editor_history.js]
+[browser_editor_markers.js]
+[browser_editor_movelines.js]
+[browser_editor_prefs.js]
+[browser_editor_script_injection.js]
+[browser_editor_addons.js]
+[browser_codemirror.js]
+[browser_css_autocompletion.js]
+[browser_css_getInfo.js]
+[browser_css_statemachine.js]
+[browser_detectindent.js]
+[browser_vimemacs.js]
+skip-if = os == 'linux'&&debug # bug 981707
+
diff --git a/devtools/client/sourceeditor/test/browser_codemirror.js b/devtools/client/sourceeditor/test/browser_codemirror.js
new file mode 100644
index 000000000..381a6530f
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_codemirror.js
@@ -0,0 +1,18 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const URI = "chrome://mochitests/content/browser/devtools/client/" +
+ "sourceeditor/test/codemirror/codemirror.html";
+loadHelperScript("helper_codemirror_runner.js");
+
+function test() {
+ requestLongerTimeout(3);
+ waitForExplicitFinish();
+
+ addTab(URI).then(function (tab) {
+ runCodeMirrorTest(tab.linkedBrowser);
+ });
+}
diff --git a/devtools/client/sourceeditor/test/browser_css_autocompletion.js b/devtools/client/sourceeditor/test/browser_css_autocompletion.js
new file mode 100644
index 000000000..e98803113
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_css_autocompletion.js
@@ -0,0 +1,145 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const CSSCompleter = require("devtools/client/sourceeditor/css-autocompleter");
+const {InspectorFront} = require("devtools/shared/fronts/inspector");
+
+const CSS_URI = "http://mochi.test:8888/browser/devtools/client/sourceeditor" +
+ "/test/css_statemachine_testcases.css";
+const TESTS_URI = "http://mochi.test:8888/browser/devtools/client" +
+ "/sourceeditor/test/css_autocompletion_tests.json";
+
+const source = read(CSS_URI);
+const tests = eval(read(TESTS_URI));
+
+const TEST_URI = "data:text/html;charset=UTF-8," + encodeURIComponent(
+ ["<!DOCTYPE html>",
+ "<html>",
+ " <head>",
+ " <title>CSS State machine tests.</title>",
+ " <style type='text/css'>",
+ "#progress {",
+ " width: 500px; height: 30px;",
+ " border: 1px solid black;",
+ " position: relative",
+ "}",
+ "#progress div {",
+ " width: 0%; height: 100%;",
+ " background: green;",
+ " position: absolute;",
+ " z-index: -1; top: 0",
+ "}",
+ "#progress.failed div {",
+ " background: red !important;",
+ "}",
+ "#progress.failed:after {",
+ " content: 'Some tests failed';",
+ " color: white",
+ "}",
+ "#progress:before {",
+ " content: 'Running test ' attr(data-progress) ' of " + tests.length + "';",
+ " color: white;",
+ " text-shadow: 0 0 2px darkgreen;",
+ "}",
+ " </style>",
+ " </head>",
+ " <body>",
+ " <h2>State machine tests for CSS autocompleter.</h2><br>",
+ " <div id='progress' data-progress='0'>",
+ " <div></div>",
+ " </div>",
+ " <div id='devtools-menu' class='devtools-toolbarbutton'></div>",
+ " <div id='devtools-toolbarbutton' class='devtools-menulist'></div>",
+ " <div id='devtools-anotherone'></div>",
+ " <div id='devtools-yetagain'></div>",
+ " <div id='devtools-itjustgoeson'></div>",
+ " <div id='devtools-okstopitnow'></div>",
+ " <div class='hidden-labels-box devtools-toolbarbutton devtools-menulist'></div>",
+ " <div class='devtools-menulist'></div>",
+ " <div class='devtools-menulist'></div>",
+ " <tabs class='devtools-toolbarbutton'><tab></tab><tab></tab><tab></tab></tabs><tabs></tabs>",
+ " <button class='category-name visible'></button>",
+ " <div class='devtools-toolbarbutton' label='true'>",
+ " <hbox class='toolbarbutton-menubutton-button'></hbox></div>",
+ " </body>",
+ " </html>"
+ ].join("\n"));
+
+let doc = null;
+let index = 0;
+let completer = null;
+let progress;
+let progressDiv;
+let inspector;
+
+function test() {
+ waitForExplicitFinish();
+ addTab(TEST_URI).then(function () {
+ doc = content.document;
+ runTests();
+ });
+}
+
+function runTests() {
+ progress = doc.getElementById("progress");
+ progressDiv = doc.querySelector("#progress > div");
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ target.makeRemote().then(() => {
+ inspector = InspectorFront(target.client, target.form);
+ inspector.getWalker().then(walker => {
+ completer = new CSSCompleter({walker: walker,
+ cssProperties: getClientCssProperties()});
+ checkStateAndMoveOn();
+ });
+ });
+}
+
+function checkStateAndMoveOn() {
+ if (index == tests.length) {
+ finishUp();
+ return;
+ }
+
+ let [lineCh, expectedSuggestions] = tests[index];
+ let [line, ch] = lineCh;
+
+ progress.dataset.progress = ++index;
+ progressDiv.style.width = 100 * index / tests.length + "%";
+
+ completer.complete(limit(source, lineCh), {line, ch})
+ .then(actualSuggestions => checkState(expectedSuggestions, actualSuggestions))
+ .then(checkStateAndMoveOn);
+}
+
+function checkState(expected, actual) {
+ if (expected.length != actual.length) {
+ ok(false, "Number of suggestions did not match up for state " + index +
+ ". Expected: " + expected.length + ", Actual: " + actual.length);
+ progress.classList.add("failed");
+ return;
+ }
+
+ for (let i = 0; i < actual.length; i++) {
+ if (expected[i] != actual[i].label) {
+ ok(false, "Suggestion " + i + " of state " + index + " did not match up" +
+ ". Expected: " + expected[i] + ". Actual: " + actual[i].label);
+ return;
+ }
+ }
+ ok(true, "Test " + index + " passed. ");
+}
+
+function finishUp() {
+ completer.walker.release().then(() => {
+ inspector.destroy();
+ inspector = null;
+ completer = null;
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+ progress = null;
+ progressDiv = null;
+}
diff --git a/devtools/client/sourceeditor/test/browser_css_getInfo.js b/devtools/client/sourceeditor/test/browser_css_getInfo.js
new file mode 100644
index 000000000..21b0b92b9
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_css_getInfo.js
@@ -0,0 +1,176 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const CSSCompleter =
+ require("devtools/client/sourceeditor/css-autocompleter");
+
+const source = [
+ ".devtools-toolbar {",
+ " -moz-appearance: none;",
+ " padding:4px 3px;border-bottom-width: 1px;",
+ " border-bottom-style: solid;",
+ "}",
+ "",
+ "#devtools-menu.devtools-menulist,",
+ ".devtools-toolbarbutton#devtools-menu {",
+ " -moz-appearance: none;",
+ " -moz-box-align: center;",
+ " min-width: 78px;",
+ " min-height: 22px;",
+ " text-shadow: 0 -1px 0 hsla(210,8%,5%,.45);",
+ " border: 1px solid hsla(210,8%,5%,.45);",
+ " border-radius: 3px;",
+ " background: linear-gradient(hsla(212,7%,57%,.35),",
+ " hsla(212,7%,57%,.1)) padding-box;",
+ " margin: 0 3px;",
+ " color: inherit;",
+ "}",
+ "",
+ ".devtools-toolbarbutton > hbox.toolbarbutton-menubutton-button {",
+ " -moz-box-orient: horizontal;",
+ "}",
+ "",
+ ".devtools-menulist:active,",
+ "#devtools-toolbarbutton:focus {",
+ " outline: 1px dotted hsla(210,30%,85%,0.7);",
+ " outline-offset : -4px;",
+ "}",
+ "",
+ ".devtools-toolbarbutton:not([label]) {",
+ " min-width: 32px;",
+ "}",
+ "",
+ ".devtools-toolbarbutton:not([label]) > .toolbarbutton-text, .devtools-toolbar {",
+ " display: none;",
+ "}",
+].join("\n");
+
+// Format of test cases :
+// [
+// {line, ch}, - The caret position at which the getInfo call should be made
+// expectedState, - The expected state at the caret
+// expectedSelector, - The expected selector for the state
+// expectedProperty, - The expected property name for states value and property
+// expectedValue, - If state is value, then the expected value
+// ]
+const tests = [
+ [{line: 0, ch: 13}, "selector", ".devtools-toolbar"],
+ [{line: 8, ch: 13}, "property", ["#devtools-menu.devtools-menulist",
+ ".devtools-toolbarbutton#devtools-menu "], "-moz-appearance"],
+ [{line: 28, ch: 25}, "value", [".devtools-menulist:active",
+ "#devtools-toolbarbutton:focus "], "outline-offset", "-4px"],
+ [{line: 4, ch: 1}, "null"],
+ [{line: 5, ch: 0}, "null"],
+ [{line: 31, ch: 13}, "selector", ".devtools-toolbarbutton:not([label])"],
+ [{line: 35, ch: 23}, "selector", ".devtools-toolbarbutton:not([label]) > .toolbarbutton-text"],
+ [{line: 35, ch: 70}, "selector", ".devtools-toolbar"],
+ [{line: 27, ch: 14}, "value", [".devtools-menulist:active",
+ "#devtools-toolbarbutton:focus "], "outline", "1px dotted hsla(210,30%,85%,0.7)"],
+ [{line: 16, ch: 16}, "value", ["#devtools-menu.devtools-menulist",
+ ".devtools-toolbarbutton#devtools-menu "], "background",
+ "linear-gradient(hsla(212,7%,57%,.35),\n hsla(212,7%,57%,.1)) padding-box"],
+ [{line: 16, ch: 3}, "value", ["#devtools-menu.devtools-menulist",
+ ".devtools-toolbarbutton#devtools-menu "], "background",
+ "linear-gradient(hsla(212,7%,57%,.35),\n hsla(212,7%,57%,.1)) padding-box"],
+ [{line: 15, ch: 25}, "value", ["#devtools-menu.devtools-menulist",
+ ".devtools-toolbarbutton#devtools-menu "], "background",
+ "linear-gradient(hsla(212,7%,57%,.35),\n hsla(212,7%,57%,.1)) padding-box"],
+];
+
+const TEST_URI = "data:text/html;charset=UTF-8," + encodeURIComponent(
+ ["<!DOCTYPE html>",
+ "<html>",
+ " <head>",
+ " <title>CSS contextual information tests.</title>",
+ " <style type='text/css'>",
+ "#progress {",
+ " width: 500px; height: 30px;",
+ " border: 1px solid black;",
+ " position: relative",
+ "}",
+ "#progress div {",
+ " width: 0%; height: 100%;",
+ " background: green;",
+ " position: absolute;",
+ " z-index: -1; top: 0",
+ "}",
+ "#progress.failed div {",
+ " background: red !important;",
+ "}",
+ "#progress.failed:after {",
+ " content: 'Some tests failed';",
+ " color: white",
+ "}",
+ "#progress:before {",
+ " content: 'Running test ' attr(data-progress) ' of " + tests.length + "';",
+ " color: white;",
+ " text-shadow: 0 0 2px darkgreen;",
+ "}",
+ " </style>",
+ " </head>",
+ " <body>",
+ " <h2>State machine tests for CSS autocompleter.</h2><br>",
+ " <div id='progress' data-progress='0'>",
+ " <div></div>",
+ " </div>",
+ " </body>",
+ " </html>"
+ ].join("\n"));
+
+let doc = null;
+function test() {
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab();
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => {
+ doc = content.document;
+ runTests();
+ });
+ gBrowser.loadURI(TEST_URI);
+}
+
+function runTests() {
+ let completer = new CSSCompleter({cssProperties: getClientCssProperties()});
+ let matches = (arr, toCheck) => !arr.some((x, i) => x != toCheck[i]);
+ let checkState = (expected, actual) => {
+ if (expected[0] == "null" && actual == null) {
+ return true;
+ } else if (expected[0] == actual.state && expected[0] == "selector" &&
+ expected[1] == actual.selector) {
+ return true;
+ } else if (expected[0] == actual.state && expected[0] == "property" &&
+ matches(expected[1], actual.selectors) &&
+ expected[2] == actual.propertyName) {
+ return true;
+ } else if (expected[0] == actual.state && expected[0] == "value" &&
+ matches(expected[1], actual.selectors) &&
+ expected[2] == actual.propertyName &&
+ expected[3] == actual.value) {
+ return true;
+ }
+ return false;
+ };
+
+ let progress = doc.getElementById("progress");
+ let progressDiv = doc.querySelector("#progress > div");
+ let i = 0;
+ for (let expected of tests) {
+ let caret = expected.splice(0, 1)[0];
+ progress.dataset.progress = ++i;
+ progressDiv.style.width = 100 * i / tests.length + "%";
+ let actual = completer.getInfoAt(source, caret);
+ if (checkState(expected, actual)) {
+ ok(true, "Test " + i + " passed. ");
+ } else {
+ ok(false, "Test " + i + " failed. Expected state : [" + expected + "] " +
+ "but found [" + actual.state + ", " +
+ (actual.selector || actual.selectors) + ", " +
+ actual.propertyName + ", " + actual.value + "].");
+ progress.classList.add("failed");
+ }
+ }
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/devtools/client/sourceeditor/test/browser_css_statemachine.js b/devtools/client/sourceeditor/test/browser_css_statemachine.js
new file mode 100644
index 000000000..f69d60278
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_css_statemachine.js
@@ -0,0 +1,109 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const CSSCompleter = require("devtools/client/sourceeditor/css-autocompleter");
+
+const CSS_URI = "http://mochi.test:8888/browser/devtools/client/sourceeditor" +
+ "/test/css_statemachine_testcases.css";
+const TESTS_URI = "http://mochi.test:8888/browser/devtools/client" +
+ "/sourceeditor/test/css_statemachine_tests.json";
+
+const source = read(CSS_URI);
+const tests = eval(read(TESTS_URI));
+
+const TEST_URI = "data:text/html;charset=UTF-8," + encodeURIComponent(
+ ["<!DOCTYPE html>",
+ "<html>",
+ " <head>",
+ " <title>CSS State machine tests.</title>",
+ " <style type='text/css'>",
+ "#progress {",
+ " width: 500px; height: 30px;",
+ " border: 1px solid black;",
+ " position: relative",
+ "}",
+ "#progress div {",
+ " width: 0%; height: 100%;",
+ " background: green;",
+ " position: absolute;",
+ " z-index: -1; top: 0",
+ "}",
+ "#progress.failed div {",
+ " background: red !important;",
+ "}",
+ "#progress.failed:after {",
+ " content: 'Some tests failed';",
+ " color: white",
+ "}",
+ "#progress:before {",
+ " content: 'Running test ' attr(data-progress) ' of " + tests.length + "';",
+ " color: white;",
+ " text-shadow: 0 0 2px darkgreen;",
+ "}",
+ " </style>",
+ " </head>",
+ " <body>",
+ " <h2>State machine tests for CSS autocompleter.</h2><br>",
+ " <div id='progress' data-progress='0'>",
+ " <div></div>",
+ " </div>",
+ " </body>",
+ " </html>"
+ ].join("\n"));
+
+var doc = null;
+function test() {
+ waitForExplicitFinish();
+ gBrowser.selectedTab = gBrowser.addTab(TEST_URI);
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => {
+ doc = content.document;
+ runTests();
+ });
+}
+
+function runTests() {
+ let completer = new CSSCompleter({cssProperties: getClientCssProperties()});
+ let checkState = state => {
+ if (state[0] == "null" && (!completer.state || completer.state == "null")) {
+ return true;
+ } else if (state[0] == completer.state && state[0] == "selector" &&
+ state[1] == completer.selectorState &&
+ state[2] == completer.completing &&
+ state[3] == completer.selector) {
+ return true;
+ } else if (state[0] == completer.state && state[0] == "value" &&
+ state[2] == completer.completing &&
+ state[3] == completer.propertyName) {
+ return true;
+ } else if (state[0] == completer.state &&
+ state[2] == completer.completing &&
+ state[0] != "selector" && state[0] != "value") {
+ return true;
+ }
+ return false;
+ };
+
+ let progress = doc.getElementById("progress");
+ let progressDiv = doc.querySelector("#progress > div");
+ let i = 0;
+ for (let test of tests) {
+ progress.dataset.progress = ++i;
+ progressDiv.style.width = 100 * i / tests.length + "%";
+ completer.resolveState(limit(source, test[0]),
+ {line: test[0][0], ch: test[0][1]});
+ if (checkState(test[1])) {
+ ok(true, "Test " + i + " passed. ");
+ } else {
+ ok(false, "Test " + i + " failed. Expected state : [" + test[1] + "] " +
+ "but found [" + completer.state + ", " + completer.selectorState +
+ ", " + completer.completing + ", " +
+ (completer.propertyName || completer.selector) + "].");
+ progress.classList.add("failed");
+ }
+ }
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/devtools/client/sourceeditor/test/browser_detectindent.js b/devtools/client/sourceeditor/test/browser_detectindent.js
new file mode 100644
index 000000000..89a3898d1
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_detectindent.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TWO_SPACES_CODE = [
+ "/*",
+ " * tricky comment block",
+ " */",
+ "div {",
+ " color: red;",
+ " background: blue;",
+ "}",
+ " ",
+ "span {",
+ " padding-left: 10px;",
+ "}"
+].join("\n");
+
+const FOUR_SPACES_CODE = [
+ "var obj = {",
+ " addNumbers: function() {",
+ " var x = 5;",
+ " var y = 18;",
+ " return x + y;",
+ " },",
+ " ",
+ " /*",
+ " * Do some stuff to two numbers",
+ " * ",
+ " * @param x",
+ " * @param y",
+ " * ",
+ " * @return the result of doing stuff",
+ " */",
+ " subtractNumbers: function(x, y) {",
+ " var x += 7;",
+ " var y += 18;",
+ " var result = x - y;",
+ " result %= 2;",
+ " }",
+ "}"
+].join("\n");
+
+const TABS_CODE = [
+ "/*",
+ " * tricky comment block",
+ " */",
+ "div {",
+ "\tcolor: red;",
+ "\tbackground: blue;",
+ "}",
+ "",
+ "span {",
+ "\tpadding-left: 10px;",
+ "}"
+].join("\n");
+
+const NONE_CODE = [
+ "var x = 0;",
+ " // stray thing",
+ "var y = 9;",
+ " ",
+ ""
+].join("\n");
+
+function test() {
+ waitForExplicitFinish();
+
+ setup((ed, win) => {
+ is(ed.getOption("indentUnit"), 2,
+ "2 spaces before code added");
+ is(ed.getOption("indentWithTabs"), false,
+ "spaces is default");
+
+ ed.setText(NONE_CODE);
+ is(ed.getOption("indentUnit"), 2,
+ "2 spaces after un-detectable code");
+ is(ed.getOption("indentWithTabs"), false,
+ "spaces still set after un-detectable code");
+
+ ed.setText(FOUR_SPACES_CODE);
+ is(ed.getOption("indentUnit"), 4,
+ "4 spaces detected in 4 space code");
+ is(ed.getOption("indentWithTabs"), false,
+ "spaces detected in 4 space code");
+
+ ed.setText(TWO_SPACES_CODE);
+ is(ed.getOption("indentUnit"), 2,
+ "2 spaces detected in 2 space code");
+ is(ed.getOption("indentWithTabs"), false,
+ "spaces detected in 2 space code");
+
+ ed.setText(TABS_CODE);
+ is(ed.getOption("indentUnit"), 2,
+ "2 space indentation unit");
+ is(ed.getOption("indentWithTabs"), true,
+ "tabs detected in majority tabs code");
+
+ teardown(ed, win);
+ });
+}
diff --git a/devtools/client/sourceeditor/test/browser_editor_addons.js b/devtools/client/sourceeditor/test/browser_editor_addons.js
new file mode 100644
index 000000000..6a7e9ca42
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_editor_addons.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function test() {
+ waitForExplicitFinish();
+
+ setup((ed, win) => {
+ let doc = win.document.querySelector("iframe").contentWindow.document;
+
+ // trailingspace.js
+ ed.setText("Hello ");
+ ed.setOption("showTrailingSpace", false);
+ ok(!doc.querySelector(".cm-trailingspace"));
+ ed.setOption("showTrailingSpace", true);
+ ok(doc.querySelector(".cm-trailingspace"));
+
+ // foldcode.js and foldgutter.js
+ ed.setMode(Editor.modes.js);
+ ed.setText("function main() {\nreturn 'Hello, World!';\n}");
+ executeSoon(() => testFold(doc, ed, win));
+ });
+}
+
+function testFold(doc, ed, win) {
+ // Wait until folding arrow is there.
+ if (!doc.querySelector(".CodeMirror-foldgutter-open")) {
+ executeSoon(() => testFold(doc, ed, win));
+ return;
+ }
+
+ teardown(ed, win);
+}
diff --git a/devtools/client/sourceeditor/test/browser_editor_autocomplete_basic.js b/devtools/client/sourceeditor/test/browser_editor_autocomplete_basic.js
new file mode 100644
index 000000000..03cdc2a4a
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_editor_autocomplete_basic.js
@@ -0,0 +1,59 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const AUTOCOMPLETION_PREF = "devtools.editor.autocomplete";
+
+// Test to make sure that different autocompletion modes can be created,
+// switched, and destroyed. This doesn't test the actual autocompletion
+// popups, only their integration with the editor.
+function test() {
+ waitForExplicitFinish();
+ setup((ed, win) => {
+ let edWin = ed.container.contentWindow.wrappedJSObject;
+ testJS(ed, edWin);
+ testCSS(ed, edWin);
+ testPref(ed, edWin);
+ teardown(ed, win);
+ });
+}
+
+function testJS(ed, win) {
+ ok(!ed.getOption("autocomplete"), "Autocompletion is not set");
+ ok(!win.tern, "Tern is not defined on the window");
+
+ ed.setMode(Editor.modes.js);
+ ed.setOption("autocomplete", true);
+
+ ok(ed.getOption("autocomplete"), "Autocompletion is set");
+ ok(win.tern, "Tern is defined on the window");
+}
+
+function testCSS(ed, win) {
+ ok(ed.getOption("autocomplete"), "Autocompletion is set");
+ ok(win.tern, "Tern is currently defined on the window");
+
+ ed.setMode(Editor.modes.css);
+ ed.setOption("autocomplete", true);
+
+ ok(ed.getOption("autocomplete"), "Autocompletion is still set");
+ ok(!win.tern, "Tern is no longer defined on the window");
+}
+
+function testPref(ed, win) {
+ ed.setMode(Editor.modes.js);
+ ed.setOption("autocomplete", true);
+
+ ok(ed.getOption("autocomplete"), "Autocompletion is set");
+ ok(win.tern, "Tern is defined on the window");
+
+ info("Preffing autocompletion off");
+ Services.prefs.setBoolPref(AUTOCOMPLETION_PREF, false);
+
+ ok(ed.getOption("autocomplete"), "Autocompletion is still set");
+ ok(!win.tern, "Tern is no longer defined on the window");
+
+ Services.prefs.clearUserPref(AUTOCOMPLETION_PREF);
+}
diff --git a/devtools/client/sourceeditor/test/browser_editor_autocomplete_events.js b/devtools/client/sourceeditor/test/browser_editor_autocomplete_events.js
new file mode 100644
index 000000000..e2f976afe
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_editor_autocomplete_events.js
@@ -0,0 +1,126 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {InspectorFront} = require("devtools/shared/fronts/inspector");
+const AUTOCOMPLETION_PREF = "devtools.editor.autocomplete";
+const TEST_URI = "data:text/html;charset=UTF-8,<html><body><bar></bar>" +
+ "<div id='baz'></div><body></html>";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ yield runTests();
+});
+
+function* runTests() {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ yield target.makeRemote();
+ let inspector = InspectorFront(target.client, target.form);
+ let walker = yield inspector.getWalker();
+ let {ed, win, edWin} = yield setup(null, {
+ autocomplete: true,
+ mode: Editor.modes.css,
+ autocompleteOpts: {walker: walker, cssProperties: getClientCssProperties()}
+ });
+ yield testMouse(ed, edWin);
+ yield testKeyboard(ed, edWin);
+ yield testKeyboardCycle(ed, edWin);
+ yield testKeyboardCycleForPrefixedString(ed, edWin);
+ yield testKeyboardCSSComma(ed, edWin);
+ teardown(ed, win);
+}
+
+function* testKeyboard(ed, win) {
+ ed.focus();
+ ed.setText("b");
+ ed.setCursor({line: 1, ch: 1});
+
+ let popupOpened = ed.getAutocompletionPopup().once("popup-opened");
+
+ let autocompleteKey =
+ Editor.keyFor("autocompletion", { noaccel: true }).toUpperCase();
+ EventUtils.synthesizeKey("VK_" + autocompleteKey, { ctrlKey: true }, win);
+
+ info("Waiting for popup to be opened");
+ yield popupOpened;
+
+ EventUtils.synthesizeKey("VK_RETURN", { }, win);
+ is(ed.getText(), "bar", "Editor text has been updated");
+}
+
+function* testKeyboardCycle(ed, win) {
+ ed.focus();
+ ed.setText("b");
+ ed.setCursor({line: 1, ch: 1});
+
+ let popupOpened = ed.getAutocompletionPopup().once("popup-opened");
+
+ let autocompleteKey =
+ Editor.keyFor("autocompletion", { noaccel: true }).toUpperCase();
+ EventUtils.synthesizeKey("VK_" + autocompleteKey, { ctrlKey: true }, win);
+
+ info("Waiting for popup to be opened");
+ yield popupOpened;
+
+ EventUtils.synthesizeKey("VK_DOWN", { }, win);
+ is(ed.getText(), "bar", "Editor text has been updated");
+
+ EventUtils.synthesizeKey("VK_DOWN", { }, win);
+ is(ed.getText(), "body", "Editor text has been updated");
+
+ EventUtils.synthesizeKey("VK_DOWN", { }, win);
+ is(ed.getText(), "#baz", "Editor text has been updated");
+}
+
+function* testKeyboardCycleForPrefixedString(ed, win) {
+ ed.focus();
+ ed.setText("#b");
+ ed.setCursor({line: 1, ch: 2});
+
+ let popupOpened = ed.getAutocompletionPopup().once("popup-opened");
+
+ let autocompleteKey =
+ Editor.keyFor("autocompletion", { noaccel: true }).toUpperCase();
+ EventUtils.synthesizeKey("VK_" + autocompleteKey, { ctrlKey: true }, win);
+
+ info("Waiting for popup to be opened");
+ yield popupOpened;
+
+ EventUtils.synthesizeKey("VK_DOWN", { }, win);
+ is(ed.getText(), "#baz", "Editor text has been updated");
+}
+
+function* testKeyboardCSSComma(ed, win) {
+ ed.focus();
+ ed.setText("b");
+ ed.setCursor({line: 1, ch: 1});
+
+ let isPopupOpened = false;
+ let popupOpened = ed.getAutocompletionPopup().once("popup-opened");
+ popupOpened.then(() => isPopupOpened = true);
+
+ EventUtils.synthesizeKey(",", { }, win);
+
+ yield wait(500);
+
+ ok(!isPopupOpened, "Autocompletion shouldn't be opened");
+}
+
+function* testMouse(ed, win) {
+ ed.focus();
+ ed.setText("b");
+ ed.setCursor({line: 1, ch: 1});
+
+ let popupOpened = ed.getAutocompletionPopup().once("popup-opened");
+
+ let autocompleteKey =
+ Editor.keyFor("autocompletion", { noaccel: true }).toUpperCase();
+ EventUtils.synthesizeKey("VK_" + autocompleteKey, { ctrlKey: true }, win);
+
+ info("Waiting for popup to be opened");
+ yield popupOpened;
+ ed.getAutocompletionPopup()._list.children[2].click();
+ is(ed.getText(), "#baz", "Editor text has been updated");
+}
diff --git a/devtools/client/sourceeditor/test/browser_editor_autocomplete_js.js b/devtools/client/sourceeditor/test/browser_editor_autocomplete_js.js
new file mode 100644
index 000000000..31fa6878f
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_editor_autocomplete_js.js
@@ -0,0 +1,45 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test to make sure that JS autocompletion is opening popups.
+function test() {
+ waitForExplicitFinish();
+ setup((ed, win) => {
+ let edWin = ed.container.contentWindow.wrappedJSObject;
+ testJS(ed, edWin).then(() => {
+ teardown(ed, win);
+ });
+ });
+}
+
+function testJS(ed, win) {
+ ok(!ed.getOption("autocomplete"), "Autocompletion is not set");
+ ok(!win.tern, "Tern is not defined on the window");
+
+ ed.setMode(Editor.modes.js);
+ ed.setOption("autocomplete", true);
+
+ ok(ed.getOption("autocomplete"), "Autocompletion is set");
+ ok(win.tern, "Tern is defined on the window");
+
+ ed.focus();
+ ed.setText("document.");
+ ed.setCursor({line: 0, ch: 9});
+
+ let waitForSuggestion = promise.defer();
+
+ ed.on("before-suggest", () => {
+ info("before-suggest has been triggered");
+ EventUtils.synthesizeKey("VK_ESCAPE", { }, win);
+ waitForSuggestion.resolve();
+ });
+
+ let autocompleteKey =
+ Editor.keyFor("autocompletion", { noaccel: true }).toUpperCase();
+ EventUtils.synthesizeKey("VK_" + autocompleteKey, { ctrlKey: true }, win);
+
+ return waitForSuggestion.promise;
+}
diff --git a/devtools/client/sourceeditor/test/browser_editor_basic.js b/devtools/client/sourceeditor/test/browser_editor_basic.js
new file mode 100644
index 000000000..503b06afe
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_editor_basic.js
@@ -0,0 +1,62 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function test() {
+ waitForExplicitFinish();
+ setup((ed, win) => {
+ // appendTo
+ let src = win.document.querySelector("iframe").getAttribute("src");
+ ok(~src.indexOf(".CodeMirror"), "correct iframe is there");
+
+ // getOption/setOption
+ ok(ed.getOption("styleActiveLine"), "getOption works");
+ ed.setOption("styleActiveLine", false);
+ ok(!ed.getOption("styleActiveLine"), "setOption works");
+
+ // Language modes
+ is(ed.getMode(), Editor.modes.text, "getMode");
+ ed.setMode(Editor.modes.js);
+ is(ed.getMode(), Editor.modes.js, "setMode");
+
+ // Content
+ is(ed.getText(), "Hello.", "getText");
+ ed.setText("Hi.\nHow are you?");
+ is(ed.getText(), "Hi.\nHow are you?", "setText");
+ is(ed.getText(1), "How are you?", "getText(num)");
+ is(ed.getText(5), "", "getText(num) when num is out of scope");
+
+ ed.replaceText("YOU", { line: 1, ch: 8 }, { line: 1, ch: 11 });
+ is(ed.getText(1), "How are YOU?", "replaceText(str, from, to)");
+ ed.replaceText("you?", { line: 1, ch: 8 });
+ is(ed.getText(1), "How are you?", "replaceText(str, from)");
+ ed.replaceText("Hello.");
+ is(ed.getText(), "Hello.", "replaceText(str)");
+
+ ed.insertText(", sir/madam", { line: 0, ch: 5});
+ is(ed.getText(), "Hello, sir/madam.", "insertText");
+
+ // Add-ons
+ ed.extend({ whoami: () => "Anton", whereami: () => "Mozilla" });
+ is(ed.whoami(), "Anton", "extend/1");
+ is(ed.whereami(), "Mozilla", "extend/2");
+
+ // Line classes
+ ed.setText("Hello!\nHow are you?");
+ ok(!ed.hasLineClass(0, "test"), "no test line class");
+ ed.addLineClass(0, "test");
+ ok(ed.hasLineClass(0, "test"), "test line class is there");
+ ed.removeLineClass(0, "test");
+ ok(!ed.hasLineClass(0, "test"), "test line class is gone");
+
+ // Font size
+ let size = ed.getFontSize();
+ is("number", typeof size, "we have the default font size");
+ ed.setFontSize(ed.getFontSize() + 1);
+ is(ed.getFontSize(), size + 1, "new font size was set");
+
+ teardown(ed, win);
+ });
+}
diff --git a/devtools/client/sourceeditor/test/browser_editor_cursor.js b/devtools/client/sourceeditor/test/browser_editor_cursor.js
new file mode 100644
index 000000000..236b3d152
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_editor_cursor.js
@@ -0,0 +1,44 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function test() {
+ waitForExplicitFinish();
+ setup((ed, win) => {
+ ch(ed.getCursor(), { line: 0, ch: 0 }, "default cursor position is ok");
+ ed.setText("Hello.\nHow are you?");
+
+ ed.setCursor({ line: 1, ch: 5 });
+ ch(ed.getCursor(), { line: 1, ch: 5 }, "setCursor({ line, ch })");
+
+ ch(ed.getPosition(7), { line: 1, ch: 0}, "getPosition(num)");
+ ch(ed.getPosition(7, 1)[0], { line: 1, ch: 0}, "getPosition(num, num)[0]");
+ ch(ed.getPosition(7, 1)[1], { line: 0, ch: 1}, "getPosition(num, num)[1]");
+
+ ch(ed.getOffset({ line: 1, ch: 0 }), 7, "getOffset(num)");
+ ch(ed.getOffset({ line: 1, ch: 0 }, { line: 0, ch: 1 })[0], 7,
+ "getOffset(num, num)[0]");
+ ch(ed.getOffset({ line: 1, ch: 0 }, { line: 0, ch: 1 })[0], 2,
+ "getOffset(num, num)[1]");
+
+ is(ed.getSelection(), "", "nothing is selected");
+ ed.setSelection({ line: 0, ch: 0 }, { line: 0, ch: 5 });
+ is(ed.getSelection(), "Hello", "setSelection");
+
+ ed.dropSelection();
+ is(ed.getSelection(), "", "dropSelection");
+
+ // Check that shift-click on a gutter selects the whole line (bug 919707)
+ let iframe = win.document.querySelector("iframe");
+ let gutter =
+ iframe.contentWindow.document.querySelector(".CodeMirror-gutters");
+
+ EventUtils.sendMouseEvent({ type: "mousedown", shiftKey: true }, gutter,
+ iframe.contentWindow);
+ is(ed.getSelection(), "Hello.", "shift-click");
+
+ teardown(ed, win);
+ });
+}
diff --git a/devtools/client/sourceeditor/test/browser_editor_find_again.js b/devtools/client/sourceeditor/test/browser_editor_find_again.js
new file mode 100644
index 000000000..f3d80095b
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_editor_find_again.js
@@ -0,0 +1,215 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2; fill-column: 80 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/sourceeditor.properties");
+
+const { OS } = Services.appinfo;
+
+// On linux, getting immediately the selection's range here fails, returning
+const FIND_KEY = L10N.getStr("find.key");
+const FINDNEXT_KEY = L10N.getStr("findNext.key");
+const FINDPREV_KEY = L10N.getStr("findPrev.key");
+// the replace's key with the appropriate modifiers based on OS
+const REPLACE_KEY = OS == "Darwin" ? L10N.getStr("replaceAllMac.key") : L10N.getStr("replaceAll.key");
+
+// values like it's not selected – even if the selection is visible.
+// For the record, setting the selection's range immediately doesn't have
+// any effect.
+// It's like the <input> is not ready yet.
+// Therefore, we trigger the UI focus event to the <input>, waiting for the
+// response.
+// Using a timeout could also work, but that is more precise, ensuring also
+// the execution of the listeners added to the <input>'s focus.
+const dispatchAndWaitForFocus = (target) => new Promise((resolve) => {
+ target.addEventListener("focus", function listener() {
+ target.removeEventListener("focus", listener);
+ resolve(target);
+ });
+
+ target.dispatchEvent(new UIEvent("focus"));
+});
+
+function openSearchBox(ed) {
+ let edDoc = ed.container.contentDocument;
+ let edWin = edDoc.defaultView;
+
+ let input = edDoc.querySelector("input[type=search]");
+ ok(!input, "search box closed");
+
+ // The editor needs the focus to properly receive the `synthesizeKey`
+ ed.focus();
+
+ synthesizeKeyShortcut(FINDNEXT_KEY, edWin);
+ input = edDoc.querySelector("input[type=search]");
+ ok(input, "find again command key opens the search box");
+}
+
+function testFindAgain(ed, inputLine, expectCursor, isFindPrev = false) {
+ let edDoc = ed.container.contentDocument;
+ let edWin = edDoc.defaultView;
+
+ let input = edDoc.querySelector("input[type=search]");
+ input.value = inputLine;
+
+ // Ensure the input has the focus before send the key – necessary on Linux,
+ // it seems that during the tests can be lost
+ input.focus();
+
+ if (isFindPrev) {
+ synthesizeKeyShortcut(FINDPREV_KEY, edWin);
+ } else {
+ synthesizeKeyShortcut(FINDNEXT_KEY, edWin);
+ }
+
+ ch(ed.getCursor(), expectCursor,
+ "find: " + inputLine + " expects cursor: " + expectCursor.toSource());
+}
+
+const testSearchBoxTextIsSelected = Task.async(function* (ed) {
+ let edDoc = ed.container.contentDocument;
+ let edWin = edDoc.defaultView;
+
+ let input = edDoc.querySelector("input[type=search]");
+ ok(input, "search box is opened");
+
+ // Ensure the input has the focus before send the key – necessary on Linux,
+ // it seems that during the tests can be lost
+ input.focus();
+
+ // Close search box
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, edWin);
+
+ input = edDoc.querySelector("input[type=search]");
+ ok(!input, "search box is closed");
+
+ // Re-open the search box
+ synthesizeKeyShortcut(FIND_KEY, edWin);
+
+ input = edDoc.querySelector("input[type=search]");
+ ok(input, "find command key opens the search box");
+
+ yield dispatchAndWaitForFocus(input);
+
+ let { selectionStart, selectionEnd, value } = input;
+
+ ok(selectionStart === 0 && selectionEnd === value.length,
+ "search box's text is selected when re-opened");
+
+ // Removing selection
+ input.setSelectionRange(0, 0);
+
+ synthesizeKeyShortcut(FIND_KEY, edWin);
+
+ ({ selectionStart, selectionEnd } = input);
+
+ ok(selectionStart === 0 && selectionEnd === value.length,
+ "search box's text is selected when find key is pressed");
+
+ // Close search box
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, edWin);
+});
+
+const testReplaceBoxTextIsSelected = Task.async(function* (ed) {
+ let edDoc = ed.container.contentDocument;
+ let edWin = edDoc.defaultView;
+
+ let input = edDoc.querySelector(".CodeMirror-dialog > input");
+ ok(!input, "dialog box with replace is closed");
+
+ // The editor needs the focus to properly receive the `synthesizeKey`
+ ed.focus();
+
+ synthesizeKeyShortcut(REPLACE_KEY, edWin);
+
+ input = edDoc.querySelector(".CodeMirror-dialog > input");
+ ok(input, "dialog box with replace is opened");
+
+ input.value = "line 5";
+
+ // Ensure the input has the focus before send the key – necessary on Linux,
+ // it seems that during the tests can be lost
+ input.focus();
+
+ yield dispatchAndWaitForFocus(input);
+
+ let { selectionStart, selectionEnd, value } = input;
+
+ ok(!(selectionStart === 0 && selectionEnd === value.length),
+ "Text in dialog box is not selected");
+
+ synthesizeKeyShortcut(REPLACE_KEY, edWin);
+
+ ({ selectionStart, selectionEnd } = input);
+
+ ok(selectionStart === 0 && selectionEnd === value.length,
+ "dialog box's text is selected when replace key is pressed");
+
+ // Close dialog box
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, edWin);
+});
+
+add_task(function* () {
+ let { ed, win } = yield setup();
+
+ ed.setText([
+ "// line 1",
+ "// line 2",
+ "// line 3",
+ "// line 4",
+ "// line 5"
+ ].join("\n"));
+
+ yield promiseWaitForFocus();
+
+ openSearchBox(ed);
+
+ let testVectors = [
+ // Starting here expect data needs to get updated for length changes to
+ // "textLines" above.
+ ["line",
+ {line: 0, ch: 7}],
+ ["line",
+ {line: 1, ch: 8}],
+ ["line",
+ {line: 2, ch: 9}],
+ ["line",
+ {line: 3, ch: 10}],
+ ["line",
+ {line: 4, ch: 11}],
+ ["ne 3",
+ {line: 2, ch: 11}],
+ ["line 1",
+ {line: 0, ch: 9}],
+ // Testing find prev
+ ["line",
+ {line: 4, ch: 11},
+ true],
+ ["line",
+ {line: 3, ch: 10},
+ true],
+ ["line",
+ {line: 2, ch: 9},
+ true],
+ ["line",
+ {line: 1, ch: 8},
+ true],
+ ["line",
+ {line: 0, ch: 7},
+ true]
+ ];
+
+ for (let v of testVectors) {
+ yield testFindAgain(ed, ...v);
+ }
+
+ yield testSearchBoxTextIsSelected(ed);
+
+ yield testReplaceBoxTextIsSelected(ed);
+
+ teardown(ed, win);
+});
diff --git a/devtools/client/sourceeditor/test/browser_editor_goto_line.js b/devtools/client/sourceeditor/test/browser_editor_goto_line.js
new file mode 100644
index 000000000..eedde41dd
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_editor_goto_line.js
@@ -0,0 +1,131 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2; fill-column: 80 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function testJumpToLine(ed, inputLine, expectCursor) {
+ ed.jumpToLine();
+ let editorDoc = ed.container.contentDocument;
+ let lineInput = editorDoc.querySelector("input");
+ lineInput.value = inputLine;
+ EventUtils.synthesizeKey("VK_RETURN", { }, editorDoc.defaultView);
+ // CodeMirror lines and columns are 0-based, Scratchpad UI is 1-based.
+ ch(ed.getCursor(), expectCursor,
+ "jumpToLine " + inputLine + " expects cursor " + expectCursor.toSource());
+}
+
+function test() {
+ waitForExplicitFinish();
+ setup((ed, win) => {
+ let textLines = [
+ "// line 1",
+ "// line 2",
+ "// line 3",
+ "// line 4",
+ "// line 5",
+ ""];
+ ed.setText(textLines.join("\n"));
+ waitForFocus(function () {
+ let testVectors = [
+ // Various useless inputs go to line 0, column 0 or do nothing.
+ ["",
+ {line: 0, ch: 0}],
+ [":",
+ {line: 0, ch: 0}],
+ [" ",
+ {line: 0, ch: 0}],
+ [" : ",
+ {line: 0, ch: 0}],
+ ["a:b",
+ {line: 0, ch: 0}],
+ ["LINE: COLUMN ",
+ {line: 0, ch: 0}],
+ ["-1",
+ {line: 0, ch: 0}],
+ [":-1",
+ {line: 0, ch: 0}],
+ ["-1:-1",
+ {line: 0, ch: 0}],
+ ["0",
+ {line: 0, ch: 0}],
+ [":0",
+ {line: 0, ch: 0}],
+ ["0:0",
+ {line: 0, ch: 0}],
+ // Starting here expect data needs to get updated for length changes to
+ // "textLines" above.
+ // Just jump to line
+ ["1",
+ {line: 0, ch: 0}],
+ // Jump to second character in line
+ ["1:2",
+ {line: 0, ch: 1}],
+ // Jump to last character on line
+ ["1:9",
+ {line: 0, ch: 8}],
+ // Jump just after last character on line (end of line)
+ ["1:10",
+ {line: 0, ch: 9}],
+ // Jump one character past end of line (gets clamped to end of line)
+ ["1:11",
+ {line: 0, ch: 9}],
+ ["2",
+ {line: 1, ch: 0}],
+ ["2:2",
+ {line: 1, ch: 1}],
+ ["2:10",
+ {line: 1, ch: 9}],
+ ["2:11",
+ {line: 1, ch: 10}],
+ ["2:12",
+ {line: 1, ch: 10}],
+ ["3",
+ {line: 2, ch: 0}],
+ ["3:2",
+ {line: 2, ch: 1}],
+ ["3:11",
+ {line: 2, ch: 10}],
+ ["3:12",
+ {line: 2, ch: 11}],
+ ["3:13",
+ {line: 2, ch: 11}],
+ ["4",
+ {line: 3, ch: 0}],
+ ["4:2",
+ {line: 3, ch: 1}],
+ ["4:12",
+ {line: 3, ch: 11}],
+ ["4:13",
+ {line: 3, ch: 12}],
+ ["4:14",
+ {line: 3, ch: 12}],
+ ["5",
+ {line: 4, ch: 0}],
+ ["5:2",
+ {line: 4, ch: 1}],
+ ["5:13",
+ {line: 4, ch: 12}],
+ ["5:14",
+ {line: 4, ch: 13}],
+ ["5:15",
+ {line: 4, ch: 13}],
+ // One line beyond last newline in editor text:
+ ["6",
+ {line: 5, ch: 0}],
+ ["6:2",
+ {line: 5, ch: 0}],
+ // Two line beyond last newline in editor text (gets clamped):
+ ["7",
+ {line: 5, ch: 0}],
+ ["7:2",
+ {line: 5, ch: 0}]
+ ];
+ testVectors.forEach(vector => {
+ testJumpToLine(ed, vector[0], vector[1]);
+ });
+ teardown(ed, win);
+ });
+ });
+}
diff --git a/devtools/client/sourceeditor/test/browser_editor_history.js b/devtools/client/sourceeditor/test/browser_editor_history.js
new file mode 100644
index 000000000..9098afdf4
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_editor_history.js
@@ -0,0 +1,32 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function test() {
+ waitForExplicitFinish();
+ setup((ed, win) => {
+ ok(ed.isClean(), "default isClean");
+ ok(!ed.canUndo(), "default canUndo");
+ ok(!ed.canRedo(), "default canRedo");
+
+ ed.setText("Hello, World!");
+ ok(!ed.isClean(), "isClean");
+ ok(ed.canUndo(), "canUndo");
+ ok(!ed.canRedo(), "canRedo");
+
+ ed.undo();
+ ok(ed.isClean(), "isClean after undo");
+ ok(!ed.canUndo(), "canUndo after undo");
+ ok(ed.canRedo(), "canRedo after undo");
+
+ ed.setText("What's up?");
+ ed.setClean();
+ ok(ed.isClean(), "isClean after setClean");
+ ok(ed.canUndo(), "canUndo after setClean");
+ ok(!ed.canRedo(), "canRedo after setClean");
+
+ teardown(ed, win);
+ });
+}
diff --git a/devtools/client/sourceeditor/test/browser_editor_markers.js b/devtools/client/sourceeditor/test/browser_editor_markers.js
new file mode 100644
index 000000000..64a78d510
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_editor_markers.js
@@ -0,0 +1,39 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function test() {
+ waitForExplicitFinish();
+ setup((ed, win) => {
+ ok(!ed.hasMarker(0, "breakpoints", "test"), "default is ok");
+ ed.addMarker(0, "breakpoints", "test");
+ ed.addMarker(0, "breakpoints", "test2");
+ ok(ed.hasMarker(0, "breakpoints", "test"), "addMarker/1");
+ ok(ed.hasMarker(0, "breakpoints", "test2"), "addMarker/2");
+
+ ed.removeMarker(0, "breakpoints", "test");
+ ok(!ed.hasMarker(0, "breakpoints", "test"), "removeMarker/1");
+ ok(ed.hasMarker(0, "breakpoints", "test2"), "removeMarker/2");
+
+ ed.removeAllMarkers("breakpoints");
+ ok(!ed.hasMarker(0, "breakpoints", "test"), "removeAllMarkers/1");
+ ok(!ed.hasMarker(0, "breakpoints", "test2"), "removeAllMarkers/2");
+
+ ed.addMarker(0, "breakpoints", "breakpoint");
+ ed.setMarkerListeners(0, "breakpoints", "breakpoint", {
+ "click": (line, marker, param) => {
+ is(line, 0, "line is ok");
+ is(marker.className, "breakpoint", "marker is ok");
+ ok(param, "click is ok");
+
+ teardown(ed, win);
+ }
+ }, [ true ]);
+
+ const env = win.document.querySelector("iframe").contentWindow;
+ const div = env.document.querySelector("div.breakpoint");
+ div.click();
+ });
+}
diff --git a/devtools/client/sourceeditor/test/browser_editor_movelines.js b/devtools/client/sourceeditor/test/browser_editor_movelines.js
new file mode 100644
index 000000000..60a7f6865
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_editor_movelines.js
@@ -0,0 +1,63 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function test() {
+ waitForExplicitFinish();
+ setup((ed, win) => {
+ let simpleProg = "function foo() {\n let i = 1;\n let j = 2;\n " +
+ "return bar;\n}";
+ ed.setText(simpleProg);
+
+ // Move first line up
+ ed.setCursor({ line: 0, ch: 0 });
+ ed.moveLineUp();
+ is(ed.getText(0), "function foo() {", "getText(num)");
+ ch(ed.getCursor(), { line: 0, ch: 0 }, "getCursor");
+
+ // Move last line down
+ ed.setCursor({ line: 4, ch: 0 });
+ ed.moveLineDown();
+ is(ed.getText(4), "}", "getText(num)");
+ ch(ed.getCursor(), { line: 4, ch: 0 }, "getCursor");
+
+ // Move line 2 up
+ ed.setCursor({ line: 1, ch: 5});
+ ed.moveLineUp();
+ is(ed.getText(0), " let i = 1;", "getText(num)");
+ is(ed.getText(1), "function foo() {", "getText(num)");
+ ch(ed.getCursor(), { line: 0, ch: 5 }, "getCursor");
+
+ // Undo previous move by moving line 1 down
+ ed.moveLineDown();
+ is(ed.getText(0), "function foo() {", "getText(num)");
+ is(ed.getText(1), " let i = 1;", "getText(num)");
+ ch(ed.getCursor(), { line: 1, ch: 5 }, "getCursor");
+
+ // Move line 2 and 3 up
+ ed.setSelection({ line: 1, ch: 0 }, { line: 2, ch: 0 });
+ ed.moveLineUp();
+ is(ed.getText(0), " let i = 1;", "getText(num)");
+ is(ed.getText(1), " let j = 2;", "getText(num)");
+ is(ed.getText(2), "function foo() {", "getText(num)");
+ ch(ed.getCursor("start"), { line: 0, ch: 0 }, "getCursor(string)");
+ ch(ed.getCursor("end"), { line: 1, ch: 0 }, "getCursor(string)");
+
+ // Move line 1 to 3 down twice
+ ed.dropSelection();
+ ed.setSelection({ line: 0, ch: 7 }, { line: 2, ch: 5 });
+ ed.moveLineDown();
+ ed.moveLineDown();
+ is(ed.getText(0), " return bar;", "getText(num)");
+ is(ed.getText(1), "}", "getText(num)");
+ is(ed.getText(2), " let i = 1;", "getText(num)");
+ is(ed.getText(3), " let j = 2;", "getText(num)");
+ is(ed.getText(4), "function foo() {", "getText(num)");
+ ch(ed.getCursor("start"), { line: 2, ch: 7 }, "getCursor(string)");
+ ch(ed.getCursor("end"), { line: 4, ch: 5 }, "getCursor(string)");
+
+ teardown(ed, win);
+ });
+}
diff --git a/devtools/client/sourceeditor/test/browser_editor_prefs.js b/devtools/client/sourceeditor/test/browser_editor_prefs.js
new file mode 100644
index 000000000..0c8c7782f
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_editor_prefs.js
@@ -0,0 +1,121 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test to make sure that the editor reacts to preference changes
+
+const TAB_SIZE = "devtools.editor.tabsize";
+const ENABLE_CODE_FOLDING = "devtools.editor.enableCodeFolding";
+const EXPAND_TAB = "devtools.editor.expandtab";
+const KEYMAP = "devtools.editor.keymap";
+const AUTO_CLOSE = "devtools.editor.autoclosebrackets";
+const AUTOCOMPLETE = "devtools.editor.autocomplete";
+const DETECT_INDENT = "devtools.editor.detectindentation";
+
+function test() {
+ waitForExplicitFinish();
+ setup((ed, win) => {
+ Assert.deepEqual(ed.getOption("gutters"), [
+ "CodeMirror-linenumbers",
+ "breakpoints",
+ "CodeMirror-foldgutter"], "gutters is correct");
+
+ ed.setText("Checking preferences.");
+
+ info("Turning prefs off");
+
+ ed.setOption("autocomplete", true);
+
+ Services.prefs.setIntPref(TAB_SIZE, 2);
+ Services.prefs.setBoolPref(ENABLE_CODE_FOLDING, false);
+ Services.prefs.setBoolPref(EXPAND_TAB, false);
+ Services.prefs.setCharPref(KEYMAP, "default");
+ Services.prefs.setBoolPref(AUTO_CLOSE, false);
+ Services.prefs.setBoolPref(AUTOCOMPLETE, false);
+ Services.prefs.setBoolPref(DETECT_INDENT, false);
+
+ Assert.deepEqual(ed.getOption("gutters"), [
+ "CodeMirror-linenumbers",
+ "breakpoints"], "gutters is correct");
+
+ is(ed.getOption("tabSize"), 2, "tabSize is correct");
+ is(ed.getOption("indentUnit"), 2, "indentUnit is correct");
+ is(ed.getOption("foldGutter"), false, "foldGutter is correct");
+ is(ed.getOption("enableCodeFolding"), undefined,
+ "enableCodeFolding is correct");
+ is(ed.getOption("indentWithTabs"), true, "indentWithTabs is correct");
+ is(ed.getOption("keyMap"), "default", "keyMap is correct");
+ is(ed.getOption("autoCloseBrackets"), "", "autoCloseBrackets is correct");
+ is(ed.getOption("autocomplete"), true, "autocomplete is correct");
+ ok(!ed.isAutocompletionEnabled(), "Autocompletion is not enabled");
+
+ info("Turning prefs on");
+
+ Services.prefs.setIntPref(TAB_SIZE, 4);
+ Services.prefs.setBoolPref(ENABLE_CODE_FOLDING, true);
+ Services.prefs.setBoolPref(EXPAND_TAB, true);
+ Services.prefs.setCharPref(KEYMAP, "sublime");
+ Services.prefs.setBoolPref(AUTO_CLOSE, true);
+ Services.prefs.setBoolPref(AUTOCOMPLETE, true);
+
+ Assert.deepEqual(ed.getOption("gutters"), [
+ "CodeMirror-linenumbers",
+ "breakpoints",
+ "CodeMirror-foldgutter"], "gutters is correct");
+
+ is(ed.getOption("tabSize"), 4, "tabSize is correct");
+ is(ed.getOption("indentUnit"), 4, "indentUnit is correct");
+ is(ed.getOption("foldGutter"), true, "foldGutter is correct");
+ is(ed.getOption("enableCodeFolding"), undefined,
+ "enableCodeFolding is correct");
+ is(ed.getOption("indentWithTabs"), false, "indentWithTabs is correct");
+ is(ed.getOption("keyMap"), "sublime", "keyMap is correct");
+ is(ed.getOption("autoCloseBrackets"), "()[]{}''\"\"``",
+ "autoCloseBrackets is correct");
+ is(ed.getOption("autocomplete"), true, "autocomplete is correct");
+ ok(ed.isAutocompletionEnabled(), "Autocompletion is enabled");
+
+ info("Forcing foldGutter off using enableCodeFolding");
+ ed.setOption("enableCodeFolding", false);
+
+ is(ed.getOption("foldGutter"), false, "foldGutter is correct");
+ is(ed.getOption("enableCodeFolding"), false,
+ "enableCodeFolding is correct");
+ Assert.deepEqual(ed.getOption("gutters"), [
+ "CodeMirror-linenumbers",
+ "breakpoints"], "gutters is correct");
+
+ info("Forcing foldGutter on using enableCodeFolding");
+ ed.setOption("enableCodeFolding", true);
+
+ is(ed.getOption("foldGutter"), true, "foldGutter is correct");
+ is(ed.getOption("enableCodeFolding"), true, "enableCodeFolding is correct");
+ Assert.deepEqual(ed.getOption("gutters"), [
+ "CodeMirror-linenumbers",
+ "breakpoints",
+ "CodeMirror-foldgutter"], "gutters is correct");
+
+ info("Checking indentation detection");
+
+ Services.prefs.setBoolPref(DETECT_INDENT, true);
+
+ ed.setText("Detecting\n\tTabs");
+ is(ed.getOption("indentWithTabs"), true, "indentWithTabs is correct");
+ is(ed.getOption("indentUnit"), 4, "indentUnit is correct");
+
+ ed.setText("body {\n color:red;\n a:b;\n}");
+ is(ed.getOption("indentWithTabs"), false, "indentWithTabs is correct");
+ is(ed.getOption("indentUnit"), 2, "indentUnit is correct");
+
+ Services.prefs.clearUserPref(TAB_SIZE);
+ Services.prefs.clearUserPref(EXPAND_TAB);
+ Services.prefs.clearUserPref(KEYMAP);
+ Services.prefs.clearUserPref(AUTO_CLOSE);
+ Services.prefs.clearUserPref(AUTOCOMPLETE);
+ Services.prefs.clearUserPref(DETECT_INDENT);
+
+ teardown(ed, win);
+ });
+}
diff --git a/devtools/client/sourceeditor/test/browser_editor_script_injection.js b/devtools/client/sourceeditor/test/browser_editor_script_injection.js
new file mode 100644
index 000000000..05487b4f2
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_editor_script_injection.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test the externalScripts option, which allows custom language modes or
+// other scripts to be injected into the editor window. See Bug 1089428.
+
+"use strict";
+
+add_task(function* () {
+ yield runTest();
+});
+
+function* runTest() {
+ const baseURL =
+ "chrome://mochitests/content/browser/devtools/client/sourceeditor/test";
+ const injectedText = "Script successfully injected!";
+
+ let {ed, win} = yield setup(null, {
+ mode: "ruby",
+ externalScripts: [`${baseURL}/cm_script_injection_test.js`,
+ `${baseURL}/cm_mode_ruby.js`]
+ });
+
+ is(ed.getText(), injectedText, "The text has been injected");
+ is(ed.getOption("mode"), "ruby", "The ruby mode is correctly set");
+ teardown(ed, win);
+}
diff --git a/devtools/client/sourceeditor/test/browser_vimemacs.js b/devtools/client/sourceeditor/test/browser_vimemacs.js
new file mode 100644
index 000000000..46ff02b5e
--- /dev/null
+++ b/devtools/client/sourceeditor/test/browser_vimemacs.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const URI = "chrome://mochitests/content/browser/devtools/client" +
+ "/sourceeditor/test/codemirror/vimemacs.html";
+loadHelperScript("helper_codemirror_runner.js");
+
+function test() {
+ requestLongerTimeout(4);
+ waitForExplicitFinish();
+
+ addTab(URI).then(function (tab) {
+ runCodeMirrorTest(tab.linkedBrowser);
+ });
+}
diff --git a/devtools/client/sourceeditor/test/cm_mode_ruby.js b/devtools/client/sourceeditor/test/cm_mode_ruby.js
new file mode 100644
index 000000000..01f3f9a46
--- /dev/null
+++ b/devtools/client/sourceeditor/test/cm_mode_ruby.js
@@ -0,0 +1,285 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function (mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function (CodeMirror) {
+ "use strict";
+
+ CodeMirror.defineMode("ruby", function (config) {
+ function wordObj(words) {
+ var o = {};
+ for (var i = 0, e = words.length; i < e; ++i) o[words[i]] = true;
+ return o;
+ }
+ var keywords = wordObj([
+ "alias", "and", "BEGIN", "begin", "break", "case", "class", "def", "defined?", "do", "else",
+ "elsif", "END", "end", "ensure", "false", "for", "if", "in", "module", "next", "not", "or",
+ "redo", "rescue", "retry", "return", "self", "super", "then", "true", "undef", "unless",
+ "until", "when", "while", "yield", "nil", "raise", "throw", "catch", "fail", "loop", "callcc",
+ "caller", "lambda", "proc", "public", "protected", "private", "require", "load",
+ "require_relative", "extend", "autoload", "__END__", "__FILE__", "__LINE__", "__dir__"
+ ]);
+ var indentWords = wordObj(["def", "class", "case", "for", "while", "module", "then",
+ "catch", "loop", "proc", "begin"]);
+ var dedentWords = wordObj(["end", "until"]);
+ var matching = {"[": "]", "{": "}", "(": ")"};
+ var curPunc;
+
+ function chain(newtok, stream, state) {
+ state.tokenize.push(newtok);
+ return newtok(stream, state);
+ }
+
+ function tokenBase(stream, state) {
+ curPunc = null;
+ if (stream.sol() && stream.match("=begin") && stream.eol()) {
+ state.tokenize.push(readBlockComment);
+ return "comment";
+ }
+ if (stream.eatSpace()) return null;
+ var ch = stream.next(), m;
+ if (ch == "`" || ch == "'" || ch == '"') {
+ return chain(readQuoted(ch, "string", ch == '"' || ch == "`"), stream, state);
+ } else if (ch == "/") {
+ var currentIndex = stream.current().length;
+ if (stream.skipTo("/")) {
+ var search_till = stream.current().length;
+ stream.backUp(stream.current().length - currentIndex);
+ var balance = 0; // balance brackets
+ while (stream.current().length < search_till) {
+ var chchr = stream.next();
+ if (chchr == "(") balance += 1;
+ else if (chchr == ")") balance -= 1;
+ if (balance < 0) break;
+ }
+ stream.backUp(stream.current().length - currentIndex);
+ if (balance == 0)
+ return chain(readQuoted(ch, "string-2", true), stream, state);
+ }
+ return "operator";
+ } else if (ch == "%") {
+ var style = "string", embed = true;
+ if (stream.eat("s")) style = "atom";
+ else if (stream.eat(/[WQ]/)) style = "string";
+ else if (stream.eat(/[r]/)) style = "string-2";
+ else if (stream.eat(/[wxq]/)) { style = "string"; embed = false; }
+ var delim = stream.eat(/[^\w\s=]/);
+ if (!delim) return "operator";
+ if (matching.propertyIsEnumerable(delim)) delim = matching[delim];
+ return chain(readQuoted(delim, style, embed, true), stream, state);
+ } else if (ch == "#") {
+ stream.skipToEnd();
+ return "comment";
+ } else if (ch == "<" && (m = stream.match(/^<-?[\`\"\']?([a-zA-Z_?]\w*)[\`\"\']?(?:;|$)/))) {
+ return chain(readHereDoc(m[1]), stream, state);
+ } else if (ch == "0") {
+ if (stream.eat("x")) stream.eatWhile(/[\da-fA-F]/);
+ else if (stream.eat("b")) stream.eatWhile(/[01]/);
+ else stream.eatWhile(/[0-7]/);
+ return "number";
+ } else if (/\d/.test(ch)) {
+ stream.match(/^[\d_]*(?:\.[\d_]+)?(?:[eE][+\-]?[\d_]+)?/);
+ return "number";
+ } else if (ch == "?") {
+ while (stream.match(/^\\[CM]-/)) {}
+ if (stream.eat("\\")) stream.eatWhile(/\w/);
+ else stream.next();
+ return "string";
+ } else if (ch == ":") {
+ if (stream.eat("'")) return chain(readQuoted("'", "atom", false), stream, state);
+ if (stream.eat('"')) return chain(readQuoted('"', "atom", true), stream, state);
+
+ // :> :>> :< :<< are valid symbols
+ if (stream.eat(/[\<\>]/)) {
+ stream.eat(/[\<\>]/);
+ return "atom";
+ }
+
+ // :+ :- :/ :* :| :& :! are valid symbols
+ if (stream.eat(/[\+\-\*\/\&\|\:\!]/)) {
+ return "atom";
+ }
+
+ // Symbols can't start by a digit
+ if (stream.eat(/[a-zA-Z$@_\xa1-\uffff]/)) {
+ stream.eatWhile(/[\w$\xa1-\uffff]/);
+ // Only one ? ! = is allowed and only as the last character
+ stream.eat(/[\?\!\=]/);
+ return "atom";
+ }
+ return "operator";
+ } else if (ch == "@" && stream.match(/^@?[a-zA-Z_\xa1-\uffff]/)) {
+ stream.eat("@");
+ stream.eatWhile(/[\w\xa1-\uffff]/);
+ return "variable-2";
+ } else if (ch == "$") {
+ if (stream.eat(/[a-zA-Z_]/)) {
+ stream.eatWhile(/[\w]/);
+ } else if (stream.eat(/\d/)) {
+ stream.eat(/\d/);
+ } else {
+ stream.next(); // Must be a special global like $: or $!
+ }
+ return "variable-3";
+ } else if (/[a-zA-Z_\xa1-\uffff]/.test(ch)) {
+ stream.eatWhile(/[\w\xa1-\uffff]/);
+ stream.eat(/[\?\!]/);
+ if (stream.eat(":")) return "atom";
+ return "ident";
+ } else if (ch == "|" && (state.varList || state.lastTok == "{" || state.lastTok == "do")) {
+ curPunc = "|";
+ return null;
+ } else if (/[\(\)\[\]{}\\;]/.test(ch)) {
+ curPunc = ch;
+ return null;
+ } else if (ch == "-" && stream.eat(">")) {
+ return "arrow";
+ } else if (/[=+\-\/*:\.^%<>~|]/.test(ch)) {
+ var more = stream.eatWhile(/[=+\-\/*:\.^%<>~|]/);
+ if (ch == "." && !more) curPunc = ".";
+ return "operator";
+ } else {
+ return null;
+ }
+ }
+
+ function tokenBaseUntilBrace(depth) {
+ if (!depth) depth = 1;
+ return function (stream, state) {
+ if (stream.peek() == "}") {
+ if (depth == 1) {
+ state.tokenize.pop();
+ return state.tokenize[state.tokenize.length - 1](stream, state);
+ } else {
+ state.tokenize[state.tokenize.length - 1] = tokenBaseUntilBrace(depth - 1);
+ }
+ } else if (stream.peek() == "{") {
+ state.tokenize[state.tokenize.length - 1] = tokenBaseUntilBrace(depth + 1);
+ }
+ return tokenBase(stream, state);
+ };
+ }
+ function tokenBaseOnce() {
+ var alreadyCalled = false;
+ return function (stream, state) {
+ if (alreadyCalled) {
+ state.tokenize.pop();
+ return state.tokenize[state.tokenize.length - 1](stream, state);
+ }
+ alreadyCalled = true;
+ return tokenBase(stream, state);
+ };
+ }
+ function readQuoted(quote, style, embed, unescaped) {
+ return function (stream, state) {
+ var escaped = false, ch;
+
+ if (state.context.type === "read-quoted-paused") {
+ state.context = state.context.prev;
+ stream.eat("}");
+ }
+
+ while ((ch = stream.next()) != null) {
+ if (ch == quote && (unescaped || !escaped)) {
+ state.tokenize.pop();
+ break;
+ }
+ if (embed && ch == "#" && !escaped) {
+ if (stream.eat("{")) {
+ if (quote == "}") {
+ state.context = {prev: state.context, type: "read-quoted-paused"};
+ }
+ state.tokenize.push(tokenBaseUntilBrace());
+ break;
+ } else if (/[@\$]/.test(stream.peek())) {
+ state.tokenize.push(tokenBaseOnce());
+ break;
+ }
+ }
+ escaped = !escaped && ch == "\\";
+ }
+ return style;
+ };
+ }
+ function readHereDoc(phrase) {
+ return function (stream, state) {
+ if (stream.match(phrase)) state.tokenize.pop();
+ else stream.skipToEnd();
+ return "string";
+ };
+ }
+ function readBlockComment(stream, state) {
+ if (stream.sol() && stream.match("=end") && stream.eol())
+ state.tokenize.pop();
+ stream.skipToEnd();
+ return "comment";
+ }
+
+ return {
+ startState: function () {
+ return {tokenize: [tokenBase],
+ indented: 0,
+ context: {type: "top", indented: -config.indentUnit},
+ continuedLine: false,
+ lastTok: null,
+ varList: false};
+ },
+
+ token: function (stream, state) {
+ if (stream.sol()) state.indented = stream.indentation();
+ var style = state.tokenize[state.tokenize.length - 1](stream, state), kwtype;
+ var thisTok = curPunc;
+ if (style == "ident") {
+ var word = stream.current();
+ style = state.lastTok == "." ? "property"
+ : keywords.propertyIsEnumerable(stream.current()) ? "keyword"
+ : /^[A-Z]/.test(word) ? "tag"
+ : (state.lastTok == "def" || state.lastTok == "class" || state.varList) ? "def"
+ : "variable";
+ if (style == "keyword") {
+ thisTok = word;
+ if (indentWords.propertyIsEnumerable(word)) kwtype = "indent";
+ else if (dedentWords.propertyIsEnumerable(word)) kwtype = "dedent";
+ else if ((word == "if" || word == "unless") && stream.column() == stream.indentation())
+ kwtype = "indent";
+ else if (word == "do" && state.context.indented < state.indented)
+ kwtype = "indent";
+ }
+ }
+ if (curPunc || (style && style != "comment")) state.lastTok = thisTok;
+ if (curPunc == "|") state.varList = !state.varList;
+
+ if (kwtype == "indent" || /[\(\[\{]/.test(curPunc))
+ state.context = {prev: state.context, type: curPunc || style, indented: state.indented};
+ else if ((kwtype == "dedent" || /[\)\]\}]/.test(curPunc)) && state.context.prev)
+ state.context = state.context.prev;
+
+ if (stream.eol())
+ state.continuedLine = (curPunc == "\\" || style == "operator");
+ return style;
+ },
+
+ indent: function (state, textAfter) {
+ if (state.tokenize[state.tokenize.length - 1] != tokenBase) return 0;
+ var firstChar = textAfter && textAfter.charAt(0);
+ var ct = state.context;
+ var closing = ct.type == matching[firstChar] ||
+ ct.type == "keyword" && /^(?:end|until|else|elsif|when|rescue)\b/.test(textAfter);
+ return ct.indented + (closing ? 0 : config.indentUnit) +
+ (state.continuedLine ? config.indentUnit : 0);
+ },
+
+ electricChars: "}de", // enD and rescuE
+ lineComment: "#"
+ };
+ });
+
+ CodeMirror.defineMIME("text/x-ruby", "ruby");
+
+});
diff --git a/devtools/client/sourceeditor/test/cm_script_injection_test.js b/devtools/client/sourceeditor/test/cm_script_injection_test.js
new file mode 100644
index 000000000..3d4a5a359
--- /dev/null
+++ b/devtools/client/sourceeditor/test/cm_script_injection_test.js
@@ -0,0 +1,8 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+window.addEventListener("editorReady", function () {
+ editor.setText("Script successfully injected!");
+});
diff --git a/devtools/client/sourceeditor/test/codemirror/codemirror.html b/devtools/client/sourceeditor/test/codemirror/codemirror.html
new file mode 100644
index 000000000..ada04a2d0
--- /dev/null
+++ b/devtools/client/sourceeditor/test/codemirror/codemirror.html
@@ -0,0 +1,210 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>CodeMirror: Basic Tests</title>
+ <link rel="stylesheet" href="chrome://devtools/content/sourceeditor/codemirror/lib/codemirror.css">
+ <link rel="stylesheet" href="cm_mode_test.css">
+ <!--<link rel="stylesheet" href="../doc/docs.css">-->
+
+ <script src="chrome://devtools/content/sourceeditor/codemirror/codemirror.bundle.js"></script>
+
+ <style type="text/css">
+ .ok {color: #090;}
+ .fail {color: #e00;}
+ .error {color: #c90;}
+ .done {font-weight: bold;}
+ #progress {
+ background: #45d;
+ color: white;
+ text-shadow: 0 0 1px #45d, 0 0 2px #45d, 0 0 3px #45d;
+ font-weight: bold;
+ white-space: pre;
+ }
+ #testground {
+ visibility: hidden;
+ }
+ #testground.offscreen {
+ visibility: visible;
+ position: absolute;
+ left: -10000px;
+ top: -10000px;
+ }
+ .CodeMirror { border: 1px solid black; }
+ </style>
+ </head>
+ <body>
+ <h1>CodeMirror: Basic Tests</h1>
+
+ <p>A limited set of programmatic sanity tests for CodeMirror.</p>
+
+ <div style="border: 1px solid black; padding: 1px; max-width: 700px;">
+ <div style="width: 0px;" id=progress><div style="padding: 3px;">Ran <span id="progress_ran">0</span><span id="progress_total"> of 0</span> tests</div></div>
+ </div>
+ <p id=status>Please enable JavaScript...</p>
+ <div id=output></div>
+
+ <div id=testground></div>
+
+ <script src="driver.js"></script>
+ <script src="test.js"></script>
+ <script src="comment_test.js"></script>
+ <script src="doc_test.js"></script>
+ <script src="driver.js"></script>
+ <script src="emacs_test.js"></script>
+ <script src="mode_test.js"></script>
+ <script src="mode/javascript/test.js"></script>
+ <script src="multi_test.js"></script>
+ <script src="search_test.js"></script>
+
+ <!-- VIM and Emacs mode tests are in vimemacs.html
+ <script src="cm_sublime_test.js"></script>
+ <script src="cm_vim_test.js"></script>
+ <script src="cm_emacs_test.js"></script>
+ -->
+
+ <!-- These modes/addons are not used by Editor
+ <script src="doc_test.js"></script>
+ <script src="../mode/css/css.js"></script>
+ <script src="../mode/css/test.js"></script>
+ <script src="../mode/css/scss_test.js"></script>
+ <script src="../mode/xml/xml.js"></script>
+ <script src="../mode/htmlmixed/htmlmixed.js"></script>
+ <script src="../mode/ruby/ruby.js"></script>
+ <script src="../mode/haml/haml.js"></script>
+ <script src="../mode/haml/test.js"></script>
+ <script src="../mode/markdown/markdown.js"></script>
+ <script src="../mode/markdown/test.js"></script>
+ <script src="../mode/gfm/gfm.js"></script>
+ <script src="../mode/gfm/test.js"></script>
+ <script src="../mode/stex/stex.js"></script>
+ <script src="../mode/stex/test.js"></script>
+ <script src="../mode/xquery/xquery.js"></script>
+ <script src="../mode/xquery/test.js"></script>
+ <script src="../addon/mode/multiplex_test.js"></script>-->
+
+ <script>
+ window.onload = runHarness;
+ CodeMirror.on(window, 'hashchange', runHarness);
+
+ function esc(str) {
+ return str.replace(/[<&]/, function(ch) { return ch == "<" ? "&lt;" : "&amp;"; });
+ }
+
+ var output = document.getElementById("output"),
+ progress = document.getElementById("progress"),
+ progressRan = document.getElementById("progress_ran").childNodes[0],
+ progressTotal = document.getElementById("progress_total").childNodes[0];
+
+ var count = 0,
+ failed = 0,
+ skipped = 0,
+ bad = "",
+ running = false, // Flag that states tests are running
+ quit = false, // Flag to quit tests ASAP
+ verbose = false, // Adds message for *every* test to output
+ phantom = false;
+
+ function runHarness(){
+ if (running) {
+ quit = true;
+ setStatus("Restarting tests...", '', true);
+ setTimeout(function(){runHarness();}, 500);
+ return;
+ }
+ filters = [];
+ verbose = false;
+ if (window.location.hash.substr(1)){
+ var strings = window.location.hash.substr(1).split(",");
+ while (strings.length) {
+ var s = strings.shift();
+ if (s === "verbose")
+ verbose = true;
+ else
+ filters.push(parseTestFilter(decodeURIComponent(s)));
+ }
+ }
+ quit = false;
+ running = true;
+ setStatus("Loading tests...");
+ count = 0;
+ failed = 0;
+ skipped = 0;
+ bad = "";
+ totalTests = countTests();
+ progressTotal.nodeValue = " of " + totalTests;
+ progressRan.nodeValue = count;
+ output.innerHTML = '';
+ document.getElementById("testground").innerHTML = "<form>" +
+ "<textarea id=\"code\" name=\"code\"></textarea>" +
+ "<input type=submit value=ok name=submit>" +
+ "</form>";
+ runTests(displayTest);
+ }
+
+ function setStatus(message, className, force){
+ if (quit && !force) return;
+ if (!message) throw("must provide message");
+ var status = document.getElementById("status").childNodes[0];
+ status.nodeValue = message;
+ status.parentNode.className = className;
+ }
+ function addOutput(name, className, code){
+ var newOutput = document.createElement("dl");
+ var newTitle = document.createElement("dt");
+ newTitle.className = className;
+ newTitle.appendChild(document.createTextNode(name));
+ newOutput.appendChild(newTitle);
+ var newMessage = document.createElement("dd");
+ newMessage.innerHTML = code;
+ newOutput.appendChild(newTitle);
+ newOutput.appendChild(newMessage);
+ output.appendChild(newOutput);
+ }
+ function displayTest(type, name, customMessage) {
+ var message = "???";
+ if (type != "done" && type != "skipped") ++count;
+ progress.style.width = (count * (progress.parentNode.clientWidth - 2) / totalTests) + "px";
+ progressRan.nodeValue = count;
+ if (type == "ok") {
+ message = "Test '" + name + "' succeeded";
+ if (!verbose) customMessage = false;
+ } else if (type == "skipped") {
+ message = "Test '" + name + "' skipped";
+ ++skipped;
+ if (!verbose) customMessage = false;
+ } else if (type == "expected") {
+ message = "Test '" + name + "' failed as expected";
+ if (!verbose) customMessage = false;
+ } else if (type == "error" || type == "fail") {
+ ++failed;
+ message = "Test '" + name + "' failed";
+ } else if (type == "done") {
+ if (failed) {
+ type += " fail";
+ message = failed + " failure" + (failed > 1 ? "s" : "");
+ } else if (count < totalTests) {
+ failed = totalTests - count;
+ type += " fail";
+ message = failed + " failure" + (failed > 1 ? "s" : "");
+ } else {
+ type += " ok";
+ message = "All passed";
+ if (skipped) {
+ message += " (" + skipped + " skipped)";
+ }
+ }
+ progressTotal.nodeValue = '';
+ customMessage = true; // Hack to avoid adding to output
+ }
+ if (window.mozilla_setStatus)
+ mozilla_setStatus(message, type, customMessage);
+ if (verbose && !customMessage) customMessage = message;
+ setStatus(message, type);
+ if (customMessage && customMessage.length > 0) {
+ addOutput(name, type, customMessage);
+ }
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/sourceeditor/test/codemirror/comment_test.js b/devtools/client/sourceeditor/test/codemirror/comment_test.js
new file mode 100644
index 000000000..26e474493
--- /dev/null
+++ b/devtools/client/sourceeditor/test/codemirror/comment_test.js
@@ -0,0 +1,100 @@
+namespace = "comment_";
+
+(function() {
+ function test(name, mode, run, before, after) {
+ return testCM(name, function(cm) {
+ run(cm);
+ eq(cm.getValue(), after);
+ }, {value: before, mode: mode});
+ }
+
+ var simpleProg = "function foo() {\n return bar;\n}";
+ var inlineBlock = "foo(/* bar */ true);";
+ var inlineBlocks = "foo(/* bar */ true, /* baz */ false);";
+ var multiLineInlineBlock = ["above();", "foo(/* bar */ true);", "below();"];
+
+ test("block", "javascript", function(cm) {
+ cm.blockComment(Pos(0, 3), Pos(3, 0), {blockCommentLead: " *"});
+ }, simpleProg + "\n", "/* function foo() {\n * return bar;\n * }\n */");
+
+ test("blockToggle", "javascript", function(cm) {
+ cm.blockComment(Pos(0, 3), Pos(2, 0), {blockCommentLead: " *"});
+ cm.uncomment(Pos(0, 3), Pos(2, 0), {blockCommentLead: " *"});
+ }, simpleProg, simpleProg);
+
+ test("blockToggle2", "javascript", function(cm) {
+ cm.setCursor({line: 0, ch: 7 /* inside the block comment */});
+ cm.execCommand("toggleComment");
+ }, inlineBlock, "foo(bar true);");
+
+ // This test should work but currently fails.
+ // test("blockToggle3", "javascript", function(cm) {
+ // cm.setCursor({line: 0, ch: 7 /* inside the first block comment */});
+ // cm.execCommand("toggleComment");
+ // }, inlineBlocks, "foo(bar true, /* baz */ false);");
+
+ test("line", "javascript", function(cm) {
+ cm.lineComment(Pos(1, 1), Pos(1, 1));
+ }, simpleProg, "function foo() {\n// return bar;\n}");
+
+ test("lineToggle", "javascript", function(cm) {
+ cm.lineComment(Pos(0, 0), Pos(2, 1));
+ cm.uncomment(Pos(0, 0), Pos(2, 1));
+ }, simpleProg, simpleProg);
+
+ test("fallbackToBlock", "css", function(cm) {
+ cm.lineComment(Pos(0, 0), Pos(2, 1));
+ }, "html {\n border: none;\n}", "/* html {\n border: none;\n} */");
+
+ test("fallbackToLine", "ruby", function(cm) {
+ cm.blockComment(Pos(0, 0), Pos(1));
+ }, "def blah()\n return hah\n", "# def blah()\n# return hah\n");
+
+ test("ignoreExternalBlockComments", "javascript", function(cm) {
+ cm.execCommand("toggleComment");
+ }, inlineBlocks, "// " + inlineBlocks);
+
+ test("ignoreExternalBlockComments2", "javascript", function(cm) {
+ cm.setCursor({line: 0, ch: null /* eol */});
+ cm.execCommand("toggleComment");
+ }, inlineBlocks, "// " + inlineBlocks);
+
+ test("ignoreExternalBlockCommentsMultiLineAbove", "javascript", function(cm) {
+ cm.setSelection({line: 0, ch: 0}, {line: 1, ch: 1});
+ cm.execCommand("toggleComment");
+ }, multiLineInlineBlock.join("\n"), ["// " + multiLineInlineBlock[0],
+ "// " + multiLineInlineBlock[1],
+ multiLineInlineBlock[2]].join("\n"));
+
+ test("ignoreExternalBlockCommentsMultiLineBelow", "javascript", function(cm) {
+ cm.setSelection({line: 1, ch: 13 /* after end of block comment */}, {line: 2, ch: 1});
+ cm.execCommand("toggleComment");
+ }, multiLineInlineBlock.join("\n"), [multiLineInlineBlock[0],
+ "// " + multiLineInlineBlock[1],
+ "// " + multiLineInlineBlock[2]].join("\n"));
+
+ test("commentRange", "javascript", function(cm) {
+ cm.blockComment(Pos(1, 2), Pos(1, 13), {fullLines: false});
+ }, simpleProg, "function foo() {\n /*return bar;*/\n}");
+
+ test("indented", "javascript", function(cm) {
+ cm.lineComment(Pos(1, 0), Pos(2), {indent: true});
+ }, simpleProg, "function foo() {\n// return bar;\n// }");
+
+ test("singleEmptyLine", "javascript", function(cm) {
+ cm.setCursor(1);
+ cm.execCommand("toggleComment");
+ }, "a;\n\nb;", "a;\n// \nb;");
+
+ test("dontMessWithStrings", "javascript", function(cm) {
+ cm.execCommand("toggleComment");
+ }, "console.log(\"/*string*/\");", "// console.log(\"/*string*/\");");
+
+ test("dontMessWithStrings2", "javascript", function(cm) {
+ cm.execCommand("toggleComment");
+ }, "console.log(\"// string\");", "// console.log(\"// string\");");
+
+ test("dontMessWithStrings3", "javascript", function(cm) {
+ cm.execCommand("toggleComment");
+ }, "// console.log(\"// string\");", "console.log(\"// string\");");
+})();
diff --git a/devtools/client/sourceeditor/test/codemirror/doc_test.js b/devtools/client/sourceeditor/test/codemirror/doc_test.js
new file mode 100644
index 000000000..5f242f658
--- /dev/null
+++ b/devtools/client/sourceeditor/test/codemirror/doc_test.js
@@ -0,0 +1,371 @@
+(function() {
+ // A minilanguage for instantiating linked CodeMirror instances and Docs
+ function instantiateSpec(spec, place, opts) {
+ var names = {}, pos = 0, l = spec.length, editors = [];
+ while (spec) {
+ var m = spec.match(/^(\w+)(\*?)(?:='([^\']*)'|<(~?)(\w+)(?:\/(\d+)-(\d+))?)\s*/);
+ var name = m[1], isDoc = m[2], cur;
+ if (m[3]) {
+ cur = isDoc ? CodeMirror.Doc(m[3]) : CodeMirror(place, clone(opts, {value: m[3]}));
+ } else {
+ var other = m[5];
+ if (!names.hasOwnProperty(other)) {
+ names[other] = editors.length;
+ editors.push(CodeMirror(place, opts));
+ }
+ var doc = editors[names[other]].linkedDoc({
+ sharedHist: !m[4],
+ from: m[6] ? Number(m[6]) : null,
+ to: m[7] ? Number(m[7]) : null
+ });
+ cur = isDoc ? doc : CodeMirror(place, clone(opts, {value: doc}));
+ }
+ names[name] = editors.length;
+ editors.push(cur);
+ spec = spec.slice(m[0].length);
+ }
+ return editors;
+ }
+
+ function clone(obj, props) {
+ if (!obj) return;
+ clone.prototype = obj;
+ var inst = new clone();
+ if (props) for (var n in props) if (props.hasOwnProperty(n))
+ inst[n] = props[n];
+ return inst;
+ }
+
+ function eqAll(val) {
+ var end = arguments.length, msg = null;
+ if (typeof arguments[end-1] == "string")
+ msg = arguments[--end];
+ if (i == end) throw new Error("No editors provided to eqAll");
+ for (var i = 1; i < end; ++i)
+ eq(arguments[i].getValue(), val, msg)
+ }
+
+ function testDoc(name, spec, run, opts, expectFail) {
+ if (!opts) opts = {};
+
+ return test("doc_" + name, function() {
+ var place = document.getElementById("testground");
+ var editors = instantiateSpec(spec, place, opts);
+ var successful = false;
+
+ try {
+ run.apply(null, editors);
+ successful = true;
+ } finally {
+ if (!successful || verbose) {
+ place.style.visibility = "visible";
+ } else {
+ for (var i = 0; i < editors.length; ++i)
+ if (editors[i] instanceof CodeMirror)
+ place.removeChild(editors[i].getWrapperElement());
+ }
+ }
+ }, expectFail);
+ }
+
+ var ie_lt8 = /MSIE [1-7]\b/.test(navigator.userAgent);
+
+ function testBasic(a, b) {
+ eqAll("x", a, b);
+ a.setValue("hey");
+ eqAll("hey", a, b);
+ b.setValue("wow");
+ eqAll("wow", a, b);
+ a.replaceRange("u\nv\nw", Pos(0, 3));
+ b.replaceRange("i", Pos(0, 4));
+ b.replaceRange("j", Pos(2, 1));
+ eqAll("wowui\nv\nwj", a, b);
+ }
+
+ testDoc("basic", "A='x' B<A", testBasic);
+ testDoc("basicSeparate", "A='x' B<~A", testBasic);
+
+ testDoc("sharedHist", "A='ab\ncd\nef' B<A", function(a, b) {
+ a.replaceRange("x", Pos(0));
+ b.replaceRange("y", Pos(1));
+ a.replaceRange("z", Pos(2));
+ eqAll("abx\ncdy\nefz", a, b);
+ a.undo();
+ a.undo();
+ eqAll("abx\ncd\nef", a, b);
+ a.redo();
+ eqAll("abx\ncdy\nef", a, b);
+ b.redo();
+ eqAll("abx\ncdy\nefz", a, b);
+ a.undo(); b.undo(); a.undo(); a.undo();
+ eqAll("ab\ncd\nef", a, b);
+ }, null, ie_lt8);
+
+ testDoc("undoIntact", "A='ab\ncd\nef' B<~A", function(a, b) {
+ a.replaceRange("x", Pos(0));
+ b.replaceRange("y", Pos(1));
+ a.replaceRange("z", Pos(2));
+ a.replaceRange("q", Pos(0));
+ eqAll("abxq\ncdy\nefz", a, b);
+ a.undo();
+ a.undo();
+ eqAll("abx\ncdy\nef", a, b);
+ b.undo();
+ eqAll("abx\ncd\nef", a, b);
+ a.redo();
+ eqAll("abx\ncd\nefz", a, b);
+ a.redo();
+ eqAll("abxq\ncd\nefz", a, b);
+ a.undo(); a.undo(); a.undo(); a.undo();
+ eqAll("ab\ncd\nef", a, b);
+ b.redo();
+ eqAll("ab\ncdy\nef", a, b);
+ });
+
+ testDoc("undoConflict", "A='ab\ncd\nef' B<~A", function(a, b) {
+ a.replaceRange("x", Pos(0));
+ a.replaceRange("z", Pos(2));
+ // This should clear the first undo event in a, but not the second
+ b.replaceRange("y", Pos(0));
+ a.undo(); a.undo();
+ eqAll("abxy\ncd\nef", a, b);
+ a.replaceRange("u", Pos(2));
+ a.replaceRange("v", Pos(0));
+ // This should clear both events in a
+ b.replaceRange("w", Pos(0));
+ a.undo(); a.undo();
+ eqAll("abxyvw\ncd\nefu", a, b);
+ });
+
+ testDoc("doubleRebase", "A='ab\ncd\nef\ng' B<~A C<B", function(a, b, c) {
+ c.replaceRange("u", Pos(3));
+ a.replaceRange("", Pos(0, 0), Pos(1, 0));
+ c.undo();
+ eqAll("cd\nef\ng", a, b, c);
+ });
+
+ testDoc("undoUpdate", "A='ab\ncd\nef' B<~A", function(a, b) {
+ a.replaceRange("x", Pos(2));
+ b.replaceRange("u\nv\nw\n", Pos(0, 0));
+ a.undo();
+ eqAll("u\nv\nw\nab\ncd\nef", a, b);
+ a.redo();
+ eqAll("u\nv\nw\nab\ncd\nefx", a, b);
+ a.undo();
+ eqAll("u\nv\nw\nab\ncd\nef", a, b);
+ b.undo();
+ a.redo();
+ eqAll("ab\ncd\nefx", a, b);
+ a.undo();
+ eqAll("ab\ncd\nef", a, b);
+ });
+
+ testDoc("undoKeepRanges", "A='abcdefg' B<A", function(a, b) {
+ var m = a.markText(Pos(0, 1), Pos(0, 3), {className: "foo"});
+ b.replaceRange("x", Pos(0, 0));
+ eqPos(m.find().from, Pos(0, 2));
+ b.replaceRange("yzzy", Pos(0, 1), Pos(0));
+ eq(m.find(), null);
+ b.undo();
+ eqPos(m.find().from, Pos(0, 2));
+ b.undo();
+ eqPos(m.find().from, Pos(0, 1));
+ });
+
+ testDoc("longChain", "A='uv' B<A C<B D<C", function(a, b, c, d) {
+ a.replaceSelection("X");
+ eqAll("Xuv", a, b, c, d);
+ d.replaceRange("Y", Pos(0));
+ eqAll("XuvY", a, b, c, d);
+ });
+
+ testDoc("broadCast", "B<A C<A D<A E<A", function(a, b, c, d, e) {
+ b.setValue("uu");
+ eqAll("uu", a, b, c, d, e);
+ a.replaceRange("v", Pos(0, 1));
+ eqAll("uvu", a, b, c, d, e);
+ });
+
+ // A and B share a history, C and D share a separate one
+ testDoc("islands", "A='x\ny\nz' B<A C<~A D<C", function(a, b, c, d) {
+ a.replaceRange("u", Pos(0));
+ d.replaceRange("v", Pos(2));
+ b.undo();
+ eqAll("x\ny\nzv", a, b, c, d);
+ c.undo();
+ eqAll("x\ny\nz", a, b, c, d);
+ a.redo();
+ eqAll("xu\ny\nz", a, b, c, d);
+ d.redo();
+ eqAll("xu\ny\nzv", a, b, c, d);
+ });
+
+ testDoc("unlink", "B<A C<A D<B", function(a, b, c, d) {
+ a.setValue("hi");
+ b.unlinkDoc(a);
+ d.setValue("aye");
+ eqAll("hi", a, c);
+ eqAll("aye", b, d);
+ a.setValue("oo");
+ eqAll("oo", a, c);
+ eqAll("aye", b, d);
+ });
+
+ testDoc("bareDoc", "A*='foo' B*<A C<B", function(a, b, c) {
+ is(a instanceof CodeMirror.Doc);
+ is(b instanceof CodeMirror.Doc);
+ is(c instanceof CodeMirror);
+ eqAll("foo", a, b, c);
+ a.replaceRange("hey", Pos(0, 0), Pos(0));
+ c.replaceRange("!", Pos(0));
+ eqAll("hey!", a, b, c);
+ b.unlinkDoc(a);
+ b.setValue("x");
+ eqAll("x", b, c);
+ eqAll("hey!", a);
+ });
+
+ testDoc("swapDoc", "A='a' B*='b' C<A", function(a, b, c) {
+ var d = a.swapDoc(b);
+ d.setValue("x");
+ eqAll("x", c, d);
+ eqAll("b", a, b);
+ });
+
+ testDoc("docKeepsScroll", "A='x' B*='y'", function(a, b) {
+ addDoc(a, 200, 200);
+ a.scrollIntoView(Pos(199, 200));
+ var c = a.swapDoc(b);
+ a.swapDoc(c);
+ var pos = a.getScrollInfo();
+ is(pos.left > 0, "not at left");
+ is(pos.top > 0, "not at top");
+ });
+
+ testDoc("copyDoc", "A='u'", function(a) {
+ var copy = a.getDoc().copy(true);
+ a.setValue("foo");
+ copy.setValue("bar");
+ var old = a.swapDoc(copy);
+ eq(a.getValue(), "bar");
+ a.undo();
+ eq(a.getValue(), "u");
+ a.swapDoc(old);
+ eq(a.getValue(), "foo");
+ eq(old.historySize().undo, 1);
+ eq(old.copy(false).historySize().undo, 0);
+ });
+
+ testDoc("docKeepsMode", "A='1+1'", function(a) {
+ var other = CodeMirror.Doc("hi", "text/x-markdown");
+ a.setOption("mode", "text/javascript");
+ var old = a.swapDoc(other);
+ eq(a.getOption("mode"), "text/x-markdown");
+ eq(a.getMode().name, "markdown");
+ a.swapDoc(old);
+ eq(a.getOption("mode"), "text/javascript");
+ eq(a.getMode().name, "javascript");
+ });
+
+ testDoc("subview", "A='1\n2\n3\n4\n5' B<~A/1-3", function(a, b) {
+ eq(b.getValue(), "2\n3");
+ eq(b.firstLine(), 1);
+ b.setCursor(Pos(4));
+ eqPos(b.getCursor(), Pos(2, 1));
+ a.replaceRange("-1\n0\n", Pos(0, 0));
+ eq(b.firstLine(), 3);
+ eqPos(b.getCursor(), Pos(4, 1));
+ a.undo();
+ eqPos(b.getCursor(), Pos(2, 1));
+ b.replaceRange("oyoy\n", Pos(2, 0));
+ eq(a.getValue(), "1\n2\noyoy\n3\n4\n5");
+ b.undo();
+ eq(a.getValue(), "1\n2\n3\n4\n5");
+ });
+
+ testDoc("subviewEditOnBoundary", "A='11\n22\n33\n44\n55' B<~A/1-4", function(a, b) {
+ a.replaceRange("x\nyy\nz", Pos(0, 1), Pos(2, 1));
+ eq(b.firstLine(), 2);
+ eq(b.lineCount(), 2);
+ eq(b.getValue(), "z3\n44");
+ a.replaceRange("q\nrr\ns", Pos(3, 1), Pos(4, 1));
+ eq(b.firstLine(), 2);
+ eq(b.getValue(), "z3\n4q");
+ eq(a.getValue(), "1x\nyy\nz3\n4q\nrr\ns5");
+ a.execCommand("selectAll");
+ a.replaceSelection("!");
+ eqAll("!", a, b);
+ });
+
+
+ testDoc("sharedMarker", "A='ab\ncd\nef\ngh' B<A C<~A/1-2", function(a, b, c) {
+ var mark = b.markText(Pos(0, 1), Pos(3, 1),
+ {className: "cm-searching", shared: true});
+ var found = a.findMarksAt(Pos(0, 2));
+ eq(found.length, 1);
+ eq(found[0], mark);
+ eq(c.findMarksAt(Pos(1, 1)).length, 1);
+ eqPos(mark.find().from, Pos(0, 1));
+ eqPos(mark.find().to, Pos(3, 1));
+ b.replaceRange("x\ny\n", Pos(0, 0));
+ eqPos(mark.find().from, Pos(2, 1));
+ eqPos(mark.find().to, Pos(5, 1));
+ var cleared = 0;
+ CodeMirror.on(mark, "clear", function() {++cleared;});
+ b.operation(function(){mark.clear();});
+ eq(a.findMarksAt(Pos(3, 1)).length, 0);
+ eq(b.findMarksAt(Pos(3, 1)).length, 0);
+ eq(c.findMarksAt(Pos(3, 1)).length, 0);
+ eq(mark.find(), null);
+ eq(cleared, 1);
+ });
+
+ testDoc("sharedMarkerCopy", "A='abcde'", function(a) {
+ var shared = a.markText(Pos(0, 1), Pos(0, 3), {shared: true});
+ var b = a.linkedDoc();
+ var found = b.findMarksAt(Pos(0, 2));
+ eq(found.length, 1);
+ eq(found[0], shared);
+ shared.clear();
+ eq(b.findMarksAt(Pos(0, 2)), 0);
+ });
+
+ testDoc("sharedMarkerDetach", "A='abcde' B<A C<B", function(a, b, c) {
+ var shared = a.markText(Pos(0, 1), Pos(0, 3), {shared: true});
+ a.unlinkDoc(b);
+ var inB = b.findMarksAt(Pos(0, 2));
+ eq(inB.length, 1);
+ is(inB[0] != shared);
+ var inC = c.findMarksAt(Pos(0, 2));
+ eq(inC.length, 1);
+ is(inC[0] != shared);
+ inC[0].clear();
+ is(shared.find());
+ });
+
+ testDoc("sharedBookmark", "A='ab\ncd\nef\ngh' B<A C<~A/1-2", function(a, b, c) {
+ var mark = b.setBookmark(Pos(1, 1), {shared: true});
+ var found = a.findMarksAt(Pos(1, 1));
+ eq(found.length, 1);
+ eq(found[0], mark);
+ eq(c.findMarksAt(Pos(1, 1)).length, 1);
+ eqPos(mark.find(), Pos(1, 1));
+ b.replaceRange("x\ny\n", Pos(0, 0));
+ eqPos(mark.find(), Pos(3, 1));
+ var cleared = 0;
+ CodeMirror.on(mark, "clear", function() {++cleared;});
+ b.operation(function() {mark.clear();});
+ eq(a.findMarks(Pos(0, 0), Pos(5)).length, 0);
+ eq(b.findMarks(Pos(0, 0), Pos(5)).length, 0);
+ eq(c.findMarks(Pos(0, 0), Pos(5)).length, 0);
+ eq(mark.find(), null);
+ eq(cleared, 1);
+ });
+
+ testDoc("undoInSubview", "A='line 0\nline 1\nline 2\nline 3\nline 4' B<A/1-4", function(a, b) {
+ b.replaceRange("x", Pos(2, 0));
+ a.undo();
+ eq(a.getValue(), "line 0\nline 1\nline 2\nline 3\nline 4");
+ eq(b.getValue(), "line 1\nline 2\nline 3");
+ });
+})();
diff --git a/devtools/client/sourceeditor/test/codemirror/driver.js b/devtools/client/sourceeditor/test/codemirror/driver.js
new file mode 100644
index 000000000..c61d4c1f3
--- /dev/null
+++ b/devtools/client/sourceeditor/test/codemirror/driver.js
@@ -0,0 +1,138 @@
+var tests = [], filters = [], allNames = [];
+
+function Failure(why) {this.message = why;}
+Failure.prototype.toString = function() { return this.message; };
+
+function indexOf(collection, elt) {
+ if (collection.indexOf) return collection.indexOf(elt);
+ for (var i = 0, e = collection.length; i < e; ++i)
+ if (collection[i] == elt) return i;
+ return -1;
+}
+
+function test(name, run, expectedFail) {
+ // Force unique names
+ var originalName = name;
+ var i = 2; // Second function would be NAME_2
+ while (indexOf(allNames, name) !== -1){
+ name = originalName + "_" + i;
+ i++;
+ }
+ allNames.push(name);
+ // Add test
+ tests.push({name: name, func: run, expectedFail: expectedFail});
+ return name;
+}
+var namespace = "";
+function testCM(name, run, opts, expectedFail) {
+ return test(namespace + name, function() {
+ var place = document.getElementById("testground"), cm = window.cm = CodeMirror(place, opts);
+ var successful = false;
+ try {
+ run(cm);
+ successful = true;
+ } finally {
+ if (!successful || verbose) {
+ place.style.visibility = "visible";
+ } else {
+ place.removeChild(cm.getWrapperElement());
+ }
+ }
+ }, expectedFail);
+}
+
+function runTests(callback) {
+ var totalTime = 0;
+ function step(i) {
+ for (;;) {
+ if (i === tests.length) {
+ running = false;
+ return callback("done");
+ }
+ var test = tests[i], skip = false;
+ if (filters.length) {
+ skip = true;
+ for (var j = 0; j < filters.length; j++)
+ if (test.name.match(filters[j])) skip = false;
+ }
+ if (skip) {
+ callback("skipped", test.name, message);
+ i++;
+ } else {
+ break;
+ }
+ }
+ var expFail = test.expectedFail, startTime = +new Date, threw = false;
+ try {
+ var message = test.func();
+ } catch(e) {
+ threw = true;
+ if (expFail) callback("expected", test.name);
+ else if (e instanceof Failure) callback("fail", test.name, e.message);
+ else {
+ var pos = /(?:\bat |@).*?([^\/:]+):(\d+)/.exec(e.stack);
+ if (pos) console["log"](e.stack);
+ callback("error", test.name, e.toString() + (pos ? " (" + pos[1] + ":" + pos[2] + ")" : ""));
+ }
+ }
+ if (!threw) {
+ if (expFail) callback("fail", test.name, message || "expected failure, but passed");
+ else callback("ok", test.name, message);
+ }
+ if (!quit) { // Run next test
+ var delay = 0;
+ totalTime += (+new Date) - startTime;
+ if (totalTime > 500){
+ totalTime = 0;
+ delay = 50;
+ }
+ setTimeout(function(){step(i + 1);}, delay);
+ } else { // Quit tests
+ running = false;
+ return null;
+ }
+ }
+ step(0);
+}
+
+function label(str, msg) {
+ if (msg) return str + " (" + msg + ")";
+ return str;
+}
+function eq(a, b, msg) {
+ if (a != b) throw new Failure(label(a + " != " + b, msg));
+}
+function near(a, b, margin, msg) {
+ if (Math.abs(a - b) > margin)
+ throw new Failure(label(a + " is not close to " + b + " (" + margin + ")", msg));
+}
+function eqPos(a, b, msg) {
+ function str(p) { return "{line:" + p.line + ",ch:" + p.ch + "}"; }
+ if (a == b) return;
+ if (a == null) throw new Failure(label("comparing null to " + str(b), msg));
+ if (b == null) throw new Failure(label("comparing " + str(a) + " to null", msg));
+ if (a.line != b.line || a.ch != b.ch) throw new Failure(label(str(a) + " != " + str(b), msg));
+}
+function is(a, msg) {
+ if (!a) throw new Failure(label("assertion failed", msg));
+}
+
+function countTests() {
+ if (!filters.length) return tests.length;
+ var sum = 0;
+ for (var i = 0; i < tests.length; ++i) {
+ var name = tests[i].name;
+ for (var j = 0; j < filters.length; j++) {
+ if (name.match(filters[j])) {
+ ++sum;
+ break;
+ }
+ }
+ }
+ return sum;
+}
+
+function parseTestFilter(s) {
+ if (/_\*$/.test(s)) return new RegExp("^" + s.slice(0, s.length - 2), "i");
+ else return new RegExp(s, "i");
+}
diff --git a/devtools/client/sourceeditor/test/codemirror/emacs_test.js b/devtools/client/sourceeditor/test/codemirror/emacs_test.js
new file mode 100644
index 000000000..124575c72
--- /dev/null
+++ b/devtools/client/sourceeditor/test/codemirror/emacs_test.js
@@ -0,0 +1,147 @@
+(function() {
+ "use strict";
+
+ var Pos = CodeMirror.Pos;
+ namespace = "emacs_";
+
+ var eventCache = {};
+ function fakeEvent(keyName) {
+ var event = eventCache[key];
+ if (event) return event;
+
+ var ctrl, shift, alt;
+ var key = keyName.replace(/\w+-/g, function(type) {
+ if (type == "Ctrl-") ctrl = true;
+ else if (type == "Alt-") alt = true;
+ else if (type == "Shift-") shift = true;
+ return "";
+ });
+ var code;
+ for (var c in CodeMirror.keyNames)
+ if (CodeMirror.keyNames[c] == key) { code = c; break; }
+ if (code == null) throw new Error("Unknown key: " + key);
+
+ return eventCache[keyName] = {
+ type: "keydown", keyCode: code, ctrlKey: ctrl, shiftKey: shift, altKey: alt,
+ preventDefault: function(){}, stopPropagation: function(){}
+ };
+ }
+
+ function sim(name, start /*, actions... */) {
+ var keys = Array.prototype.slice.call(arguments, 2);
+ testCM(name, function(cm) {
+ for (var i = 0; i < keys.length; ++i) {
+ var cur = keys[i];
+ if (cur instanceof Pos) cm.setCursor(cur);
+ else if (cur.call) cur(cm);
+ else cm.triggerOnKeyDown(fakeEvent(cur));
+ }
+ }, {keyMap: "emacs", value: start, mode: "javascript"});
+ }
+
+ function at(line, ch) { return function(cm) { eqPos(cm.getCursor(), Pos(line, ch)); }; }
+ function txt(str) { return function(cm) { eq(cm.getValue(), str); }; }
+
+ sim("motionHSimple", "abc", "Ctrl-F", "Ctrl-F", "Ctrl-B", at(0, 1));
+ sim("motionHMulti", "abcde",
+ "Ctrl-4", "Ctrl-F", at(0, 4), "Ctrl--", "Ctrl-2", "Ctrl-F", at(0, 2),
+ "Ctrl-5", "Ctrl-B", at(0, 0));
+
+ sim("motionHWord", "abc. def ghi",
+ "Alt-F", at(0, 3), "Alt-F", at(0, 8),
+ "Ctrl-B", "Alt-B", at(0, 5), "Alt-B", at(0, 0));
+ sim("motionHWordMulti", "abc. def ghi ",
+ "Ctrl-3", "Alt-F", at(0, 12), "Ctrl-2", "Alt-B", at(0, 5),
+ "Ctrl--", "Alt-B", at(0, 8));
+
+ sim("motionVSimple", "a\nb\nc\n", "Ctrl-N", "Ctrl-N", "Ctrl-P", at(1, 0));
+ sim("motionVMulti", "a\nb\nc\nd\ne\n",
+ "Ctrl-2", "Ctrl-N", at(2, 0), "Ctrl-F", "Ctrl--", "Ctrl-N", at(1, 1),
+ "Ctrl--", "Ctrl-3", "Ctrl-P", at(4, 1));
+
+ sim("killYank", "abc\ndef\nghi",
+ "Ctrl-F", "Ctrl-Space", "Ctrl-N", "Ctrl-N", "Ctrl-W", "Ctrl-E", "Ctrl-Y",
+ txt("ahibc\ndef\ng"));
+ sim("killRing", "abcdef",
+ "Ctrl-Space", "Ctrl-F", "Ctrl-W", "Ctrl-Space", "Ctrl-F", "Ctrl-W",
+ "Ctrl-Y", "Alt-Y",
+ txt("acdef"));
+ sim("copyYank", "abcd",
+ "Ctrl-Space", "Ctrl-E", "Alt-W", "Ctrl-Y",
+ txt("abcdabcd"));
+
+ sim("killLineSimple", "foo\nbar", "Ctrl-F", "Ctrl-K", txt("f\nbar"));
+ sim("killLineEmptyLine", "foo\n \nbar", "Ctrl-N", "Ctrl-K", txt("foo\nbar"));
+ sim("killLineMulti", "foo\nbar\nbaz",
+ "Ctrl-F", "Ctrl-F", "Ctrl-K", "Ctrl-K", "Ctrl-K", "Ctrl-A", "Ctrl-Y",
+ txt("o\nbarfo\nbaz"));
+
+ sim("moveByParagraph", "abc\ndef\n\n\nhij\nklm\n\n",
+ "Ctrl-F", "Ctrl-Down", at(2, 0), "Ctrl-Down", at(6, 0),
+ "Ctrl-N", "Ctrl-Up", at(3, 0), "Ctrl-Up", at(0, 0),
+ Pos(1, 2), "Ctrl-Down", at(2, 0), Pos(4, 2), "Ctrl-Up", at(3, 0));
+ sim("moveByParagraphMulti", "abc\n\ndef\n\nhij\n\nklm",
+ "Ctrl-U", "2", "Ctrl-Down", at(3, 0),
+ "Shift-Alt-.", "Ctrl-3", "Ctrl-Up", at(1, 0));
+
+ sim("moveBySentence", "sentence one! sentence\ntwo\n\nparagraph two",
+ "Alt-E", at(0, 13), "Alt-E", at(1, 3), "Ctrl-F", "Alt-A", at(0, 13));
+
+ sim("moveByExpr", "function foo(a, b) {}",
+ "Ctrl-Alt-F", at(0, 8), "Ctrl-Alt-F", at(0, 12), "Ctrl-Alt-F", at(0, 18),
+ "Ctrl-Alt-B", at(0, 12), "Ctrl-Alt-B", at(0, 9));
+ sim("moveByExprMulti", "foo bar baz bug",
+ "Ctrl-2", "Ctrl-Alt-F", at(0, 7),
+ "Ctrl--", "Ctrl-Alt-F", at(0, 4),
+ "Ctrl--", "Ctrl-2", "Ctrl-Alt-B", at(0, 11));
+ sim("delExpr", "var x = [\n a,\n b\n c\n];",
+ Pos(0, 8), "Ctrl-Alt-K", txt("var x = ;"), "Ctrl-/",
+ Pos(4, 1), "Ctrl-Alt-Backspace", txt("var x = ;"));
+ sim("delExprMulti", "foo bar baz",
+ "Ctrl-2", "Ctrl-Alt-K", txt(" baz"),
+ "Ctrl-/", "Ctrl-E", "Ctrl-2", "Ctrl-Alt-Backspace", txt("foo "));
+
+ sim("justOneSpace", "hi bye ",
+ Pos(0, 4), "Alt-Space", txt("hi bye "),
+ Pos(0, 4), "Alt-Space", txt("hi b ye "),
+ "Ctrl-A", "Alt-Space", "Ctrl-E", "Alt-Space", txt(" hi b ye "));
+
+ sim("openLine", "foo bar", "Alt-F", "Ctrl-O", txt("foo\n bar"))
+
+ sim("transposeChar", "abcd\ne",
+ "Ctrl-F", "Ctrl-T", "Ctrl-T", txt("bcad\ne"), at(0, 3),
+ "Ctrl-F", "Ctrl-T", "Ctrl-T", "Ctrl-T", txt("bcda\ne"), at(0, 4),
+ "Ctrl-F", "Ctrl-T", txt("bcde\na"), at(1, 0));
+
+ sim("manipWordCase", "foo BAR bAZ",
+ "Alt-C", "Alt-L", "Alt-U", txt("Foo bar BAZ"),
+ "Ctrl-A", "Alt-U", "Alt-L", "Alt-C", txt("FOO bar Baz"));
+ sim("manipWordCaseMulti", "foo Bar bAz",
+ "Ctrl-2", "Alt-U", txt("FOO BAR bAz"),
+ "Ctrl-A", "Ctrl-3", "Alt-C", txt("Foo Bar Baz"));
+
+ sim("upExpr", "foo {\n bar[];\n baz(blah);\n}",
+ Pos(2, 7), "Ctrl-Alt-U", at(2, 5), "Ctrl-Alt-U", at(0, 4));
+ sim("transposeExpr", "do foo[bar] dah",
+ Pos(0, 6), "Ctrl-Alt-T", txt("do [bar]foo dah"));
+
+ sim("clearMark", "abcde", Pos(0, 2), "Ctrl-Space", "Ctrl-F", "Ctrl-F",
+ "Ctrl-G", "Ctrl-W", txt("abcde"));
+
+ sim("delRegion", "abcde", "Ctrl-Space", "Ctrl-F", "Ctrl-F", "Delete", txt("cde"));
+ sim("backspaceRegion", "abcde", "Ctrl-Space", "Ctrl-F", "Ctrl-F", "Backspace", txt("cde"));
+
+ testCM("save", function(cm) {
+ var saved = false;
+ CodeMirror.commands.save = function(cm) { saved = cm.getValue(); };
+ cm.triggerOnKeyDown(fakeEvent("Ctrl-X"));
+ cm.triggerOnKeyDown(fakeEvent("Ctrl-S"));
+ is(saved, "hi");
+ }, {value: "hi", keyMap: "emacs"});
+
+ testCM("gotoInvalidLineFloat", function(cm) {
+ cm.openDialog = function(_, cb) { cb("2.2"); };
+ cm.triggerOnKeyDown(fakeEvent("Alt-G"));
+ cm.triggerOnKeyDown(fakeEvent("G"));
+ }, {value: "1\n2\n3\n4", keyMap: "emacs"});
+})();
diff --git a/devtools/client/sourceeditor/test/codemirror/mode/javascript/test.js b/devtools/client/sourceeditor/test/codemirror/mode/javascript/test.js
new file mode 100644
index 000000000..cb43d0894
--- /dev/null
+++ b/devtools/client/sourceeditor/test/codemirror/mode/javascript/test.js
@@ -0,0 +1,210 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function() {
+ var mode = CodeMirror.getMode({indentUnit: 2}, "javascript");
+ function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1)); }
+
+ MT("locals",
+ "[keyword function] [def foo]([def a], [def b]) { [keyword var] [def c] [operator =] [number 10]; [keyword return] [variable-2 a] [operator +] [variable-2 c] [operator +] [variable d]; }");
+
+ MT("comma-and-binop",
+ "[keyword function](){ [keyword var] [def x] [operator =] [number 1] [operator +] [number 2], [def y]; }");
+
+ MT("destructuring",
+ "([keyword function]([def a], [[[def b], [def c] ]]) {",
+ " [keyword let] {[def d], [property foo]: [def c][operator =][number 10], [def x]} [operator =] [variable foo]([variable-2 a]);",
+ " [[[variable-2 c], [variable y] ]] [operator =] [variable-2 c];",
+ "})();");
+
+ MT("destructure_trailing_comma",
+ "[keyword let] {[def a], [def b],} [operator =] [variable foo];",
+ "[keyword let] [def c];"); // Parser still in good state?
+
+ MT("class_body",
+ "[keyword class] [def Foo] {",
+ " [property constructor]() {}",
+ " [property sayName]() {",
+ " [keyword return] [string-2 `foo${][variable foo][string-2 }oo`];",
+ " }",
+ "}");
+
+ MT("class",
+ "[keyword class] [def Point] [keyword extends] [variable SuperThing] {",
+ " [property get] [property prop]() { [keyword return] [number 24]; }",
+ " [property constructor]([def x], [def y]) {",
+ " [keyword super]([string 'something']);",
+ " [keyword this].[property x] [operator =] [variable-2 x];",
+ " }",
+ "}");
+
+ MT("import",
+ "[keyword function] [def foo]() {",
+ " [keyword import] [def $] [keyword from] [string 'jquery'];",
+ " [keyword import] { [def encrypt], [def decrypt] } [keyword from] [string 'crypto'];",
+ "}");
+
+ MT("const",
+ "[keyword function] [def f]() {",
+ " [keyword const] [[ [def a], [def b] ]] [operator =] [[ [number 1], [number 2] ]];",
+ "}");
+
+ MT("for/of",
+ "[keyword for]([keyword let] [def of] [keyword of] [variable something]) {}");
+
+ MT("generator",
+ "[keyword function*] [def repeat]([def n]) {",
+ " [keyword for]([keyword var] [def i] [operator =] [number 0]; [variable-2 i] [operator <] [variable-2 n]; [operator ++][variable-2 i])",
+ " [keyword yield] [variable-2 i];",
+ "}");
+
+ MT("quotedStringAddition",
+ "[keyword let] [def f] [operator =] [variable a] [operator +] [string 'fatarrow'] [operator +] [variable c];");
+
+ MT("quotedFatArrow",
+ "[keyword let] [def f] [operator =] [variable a] [operator +] [string '=>'] [operator +] [variable c];");
+
+ MT("fatArrow",
+ "[variable array].[property filter]([def a] [operator =>] [variable-2 a] [operator +] [number 1]);",
+ "[variable a];", // No longer in scope
+ "[keyword let] [def f] [operator =] ([[ [def a], [def b] ]], [def c]) [operator =>] [variable-2 a] [operator +] [variable-2 c];",
+ "[variable c];");
+
+ MT("spread",
+ "[keyword function] [def f]([def a], [meta ...][def b]) {",
+ " [variable something]([variable-2 a], [meta ...][variable-2 b]);",
+ "}");
+
+ MT("comprehension",
+ "[keyword function] [def f]() {",
+ " [[([variable x] [operator +] [number 1]) [keyword for] ([keyword var] [def x] [keyword in] [variable y]) [keyword if] [variable pred]([variable-2 x]) ]];",
+ " ([variable u] [keyword for] ([keyword var] [def u] [keyword of] [variable generateValues]()) [keyword if] ([variable-2 u].[property color] [operator ===] [string 'blue']));",
+ "}");
+
+ MT("quasi",
+ "[variable re][string-2 `fofdlakj${][variable x] [operator +] ([variable re][string-2 `foo`]) [operator +] [number 1][string-2 }fdsa`] [operator +] [number 2]");
+
+ MT("quasi_no_function",
+ "[variable x] [operator =] [string-2 `fofdlakj${][variable x] [operator +] [string-2 `foo`] [operator +] [number 1][string-2 }fdsa`] [operator +] [number 2]");
+
+ MT("indent_statement",
+ "[keyword var] [def x] [operator =] [number 10]",
+ "[variable x] [operator +=] [variable y] [operator +]",
+ " [atom Infinity]",
+ "[keyword debugger];");
+
+ MT("indent_if",
+ "[keyword if] ([number 1])",
+ " [keyword break];",
+ "[keyword else] [keyword if] ([number 2])",
+ " [keyword continue];",
+ "[keyword else]",
+ " [number 10];",
+ "[keyword if] ([number 1]) {",
+ " [keyword break];",
+ "} [keyword else] [keyword if] ([number 2]) {",
+ " [keyword continue];",
+ "} [keyword else] {",
+ " [number 10];",
+ "}");
+
+ MT("indent_for",
+ "[keyword for] ([keyword var] [def i] [operator =] [number 0];",
+ " [variable i] [operator <] [number 100];",
+ " [variable i][operator ++])",
+ " [variable doSomething]([variable i]);",
+ "[keyword debugger];");
+
+ MT("indent_c_style",
+ "[keyword function] [def foo]()",
+ "{",
+ " [keyword debugger];",
+ "}");
+
+ MT("indent_else",
+ "[keyword for] (;;)",
+ " [keyword if] ([variable foo])",
+ " [keyword if] ([variable bar])",
+ " [number 1];",
+ " [keyword else]",
+ " [number 2];",
+ " [keyword else]",
+ " [number 3];");
+
+ MT("indent_funarg",
+ "[variable foo]([number 10000],",
+ " [keyword function]([def a]) {",
+ " [keyword debugger];",
+ "};");
+
+ MT("indent_below_if",
+ "[keyword for] (;;)",
+ " [keyword if] ([variable foo])",
+ " [number 1];",
+ "[number 2];");
+
+ MT("multilinestring",
+ "[keyword var] [def x] [operator =] [string 'foo\\]",
+ "[string bar'];");
+
+ MT("scary_regexp",
+ "[string-2 /foo[[/]]bar/];");
+
+ MT("indent_strange_array",
+ "[keyword var] [def x] [operator =] [[",
+ " [number 1],,",
+ " [number 2],",
+ "]];",
+ "[number 10];");
+
+ MT("param_default",
+ "[keyword function] [def foo]([def x] [operator =] [string-2 `foo${][number 10][string-2 }bar`]) {",
+ " [keyword return] [variable-2 x];",
+ "}");
+
+ MT("new_target",
+ "[keyword function] [def F]([def target]) {",
+ " [keyword if] ([variable-2 target] [operator &&] [keyword new].[keyword target].[property name]) {",
+ " [keyword return] [keyword new]",
+ " .[keyword target];",
+ " }",
+ "}");
+
+ var jsonld_mode = CodeMirror.getMode(
+ {indentUnit: 2},
+ {name: "javascript", jsonld: true}
+ );
+ function LD(name) {
+ test.mode(name, jsonld_mode, Array.prototype.slice.call(arguments, 1));
+ }
+
+ LD("json_ld_keywords",
+ '{',
+ ' [meta "@context"]: {',
+ ' [meta "@base"]: [string "http://example.com"],',
+ ' [meta "@vocab"]: [string "http://xmlns.com/foaf/0.1/"],',
+ ' [property "likesFlavor"]: {',
+ ' [meta "@container"]: [meta "@list"]',
+ ' [meta "@reverse"]: [string "@beFavoriteOf"]',
+ ' },',
+ ' [property "nick"]: { [meta "@container"]: [meta "@set"] },',
+ ' [property "nick"]: { [meta "@container"]: [meta "@index"] }',
+ ' },',
+ ' [meta "@graph"]: [[ {',
+ ' [meta "@id"]: [string "http://dbpedia.org/resource/John_Lennon"],',
+ ' [property "name"]: [string "John Lennon"],',
+ ' [property "modified"]: {',
+ ' [meta "@value"]: [string "2010-05-29T14:17:39+02:00"],',
+ ' [meta "@type"]: [string "http://www.w3.org/2001/XMLSchema#dateTime"]',
+ ' }',
+ ' } ]]',
+ '}');
+
+ LD("json_ld_fake",
+ '{',
+ ' [property "@fake"]: [string "@fake"],',
+ ' [property "@contextual"]: [string "@identifier"],',
+ ' [property "user@domain.com"]: [string "@graphical"],',
+ ' [property "@ID"]: [string "@@ID"]',
+ '}');
+})();
diff --git a/devtools/client/sourceeditor/test/codemirror/mode_test.css b/devtools/client/sourceeditor/test/codemirror/mode_test.css
new file mode 100644
index 000000000..f83271b4e
--- /dev/null
+++ b/devtools/client/sourceeditor/test/codemirror/mode_test.css
@@ -0,0 +1,23 @@
+.mt-output .mt-token {
+ border: 1px solid #ddd;
+ white-space: pre;
+ font-family: "Consolas", monospace;
+ text-align: center;
+}
+
+.mt-output .mt-style {
+ font-size: x-small;
+}
+
+.mt-output .mt-state {
+ font-size: x-small;
+ vertical-align: top;
+}
+
+.mt-output .mt-state-row {
+ display: none;
+}
+
+.mt-state-unhide .mt-output .mt-state-row {
+ display: table-row;
+}
diff --git a/devtools/client/sourceeditor/test/codemirror/mode_test.js b/devtools/client/sourceeditor/test/codemirror/mode_test.js
new file mode 100644
index 000000000..0aed50f7d
--- /dev/null
+++ b/devtools/client/sourceeditor/test/codemirror/mode_test.js
@@ -0,0 +1,192 @@
+/**
+ * Helper to test CodeMirror highlighting modes. It pretty prints output of the
+ * highlighter and can check against expected styles.
+ *
+ * Mode tests are registered by calling test.mode(testName, mode,
+ * tokens), where mode is a mode object as returned by
+ * CodeMirror.getMode, and tokens is an array of lines that make up
+ * the test.
+ *
+ * These lines are strings, in which styled stretches of code are
+ * enclosed in brackets `[]`, and prefixed by their style. For
+ * example, `[keyword if]`. Brackets in the code itself must be
+ * duplicated to prevent them from being interpreted as token
+ * boundaries. For example `a[[i]]` for `a[i]`. If a token has
+ * multiple styles, the styles must be separated by ampersands, for
+ * example `[tag&error </hmtl>]`.
+ *
+ * See the test.js files in the css, markdown, gfm, and stex mode
+ * directories for examples.
+ */
+(function() {
+ function findSingle(str, pos, ch) {
+ for (;;) {
+ var found = str.indexOf(ch, pos);
+ if (found == -1) return null;
+ if (str.charAt(found + 1) != ch) return found;
+ pos = found + 2;
+ }
+ }
+
+ var styleName = /[\w&-_]+/g;
+ function parseTokens(strs) {
+ var tokens = [], plain = "";
+ for (var i = 0; i < strs.length; ++i) {
+ if (i) plain += "\n";
+ var str = strs[i], pos = 0;
+ while (pos < str.length) {
+ var style = null, text;
+ if (str.charAt(pos) == "[" && str.charAt(pos+1) != "[") {
+ styleName.lastIndex = pos + 1;
+ var m = styleName.exec(str);
+ style = m[0].replace(/&/g, " ");
+ var textStart = pos + style.length + 2;
+ var end = findSingle(str, textStart, "]");
+ if (end == null) throw new Error("Unterminated token at " + pos + " in '" + str + "'" + style);
+ text = str.slice(textStart, end);
+ pos = end + 1;
+ } else {
+ var end = findSingle(str, pos, "[");
+ if (end == null) end = str.length;
+ text = str.slice(pos, end);
+ pos = end;
+ }
+ text = text.replace(/\[\[|\]\]/g, function(s) {return s.charAt(0);});
+ tokens.push({style: style, text: text});
+ plain += text;
+ }
+ }
+ return {tokens: tokens, plain: plain};
+ }
+
+ test.mode = function(name, mode, tokens, modeName) {
+ var data = parseTokens(tokens);
+ return test((modeName || mode.name) + "_" + name, function() {
+ return compare(data.plain, data.tokens, mode);
+ });
+ };
+
+ function esc(str) {
+ return str.replace('&', '&amp;').replace('<', '&lt;').replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
+;
+ }
+
+ function compare(text, expected, mode) {
+
+ var expectedOutput = [];
+ for (var i = 0; i < expected.length; ++i) {
+ var sty = expected[i].style;
+ if (sty && sty.indexOf(" ")) sty = sty.split(' ').sort().join(' ');
+ expectedOutput.push({style: sty, text: expected[i].text});
+ }
+
+ var observedOutput = highlight(text, mode);
+
+ var s = "";
+ var diff = highlightOutputsDifferent(expectedOutput, observedOutput);
+ if (diff != null) {
+ s += '<div class="mt-test mt-fail">';
+ s += '<pre>' + esc(text) + '</pre>';
+ s += '<div class="cm-s-default">';
+ s += 'expected:';
+ s += prettyPrintOutputTable(expectedOutput, diff);
+ s += 'observed: [<a onclick="this.parentElement.className+=\' mt-state-unhide\'">display states</a>]';
+ s += prettyPrintOutputTable(observedOutput, diff);
+ s += '</div>';
+ s += '</div>';
+ }
+ if (observedOutput.indentFailures) {
+ for (var i = 0; i < observedOutput.indentFailures.length; i++)
+ s += "<div class='mt-test mt-fail'>" + esc(observedOutput.indentFailures[i]) + "</div>";
+ }
+ if (s) throw new Failure(s);
+ }
+
+ function stringify(obj) {
+ function replacer(key, obj) {
+ if (typeof obj == "function") {
+ var m = obj.toString().match(/function\s*[^\s(]*/);
+ return m ? m[0] : "function";
+ }
+ return obj;
+ }
+ if (window.JSON && JSON.stringify)
+ return JSON.stringify(obj, replacer, 2);
+ return "[unsupported]"; // Fail safely if no native JSON.
+ }
+
+ function highlight(string, mode) {
+ var state = mode.startState();
+
+ var lines = string.replace(/\r\n/g,'\n').split('\n');
+ var st = [], pos = 0;
+ for (var i = 0; i < lines.length; ++i) {
+ var line = lines[i], newLine = true;
+ if (mode.indent) {
+ var ws = line.match(/^\s*/)[0];
+ var indent = mode.indent(state, line.slice(ws.length));
+ if (indent != CodeMirror.Pass && indent != ws.length)
+ (st.indentFailures || (st.indentFailures = [])).push(
+ "Indentation of line " + (i + 1) + " is " + indent + " (expected " + ws.length + ")");
+ }
+ var stream = new CodeMirror.StringStream(line);
+ if (line == "" && mode.blankLine) mode.blankLine(state);
+ /* Start copied code from CodeMirror.highlight */
+ while (!stream.eol()) {
+ for (var j = 0; j < 10 && stream.start >= stream.pos; j++)
+ var compare = mode.token(stream, state);
+ if (j == 10)
+ throw new Failure("Failed to advance the stream." + stream.string + " " + stream.pos);
+ var substr = stream.current();
+ if (compare && compare.indexOf(" ") > -1) compare = compare.split(' ').sort().join(' ');
+ stream.start = stream.pos;
+ if (pos && st[pos-1].style == compare && !newLine) {
+ st[pos-1].text += substr;
+ } else if (substr) {
+ st[pos++] = {style: compare, text: substr, state: stringify(state)};
+ }
+ // Give up when line is ridiculously long
+ if (stream.pos > 5000) {
+ st[pos++] = {style: null, text: this.text.slice(stream.pos)};
+ break;
+ }
+ newLine = false;
+ }
+ }
+
+ return st;
+ }
+
+ function highlightOutputsDifferent(o1, o2) {
+ var minLen = Math.min(o1.length, o2.length);
+ for (var i = 0; i < minLen; ++i)
+ if (o1[i].style != o2[i].style || o1[i].text != o2[i].text) return i;
+ if (o1.length > minLen || o2.length > minLen) return minLen;
+ }
+
+ function prettyPrintOutputTable(output, diffAt) {
+ var s = '<table class="mt-output">';
+ s += '<tr>';
+ for (var i = 0; i < output.length; ++i) {
+ var style = output[i].style, val = output[i].text;
+ s +=
+ '<td class="mt-token"' + (i == diffAt ? " style='background: pink'" : "") + '>' +
+ '<span class="cm-' + esc(String(style)) + '">' +
+ esc(val.replace(/ /g,'\xb7')) + // · MIDDLE DOT
+ '</span>' +
+ '</td>';
+ }
+ s += '</tr><tr>';
+ for (var i = 0; i < output.length; ++i) {
+ s += '<td class="mt-style"><span>' + (output[i].style || null) + '</span></td>';
+ }
+ if(output[0].state) {
+ s += '</tr><tr class="mt-state-row" title="State AFTER each token">';
+ for (var i = 0; i < output.length; ++i) {
+ s += '<td class="mt-state"><pre>' + esc(output[i].state) + '</pre></td>';
+ }
+ }
+ s += '</tr></table>';
+ return s;
+ }
+})();
diff --git a/devtools/client/sourceeditor/test/codemirror/multi_test.js b/devtools/client/sourceeditor/test/codemirror/multi_test.js
new file mode 100644
index 000000000..a8e760d27
--- /dev/null
+++ b/devtools/client/sourceeditor/test/codemirror/multi_test.js
@@ -0,0 +1,285 @@
+(function() {
+ namespace = "multi_";
+
+ function hasSelections(cm) {
+ var sels = cm.listSelections();
+ var given = (arguments.length - 1) / 4;
+ if (sels.length != given)
+ throw new Failure("expected " + given + " selections, found " + sels.length);
+ for (var i = 0, p = 1; i < given; i++, p += 4) {
+ var anchor = Pos(arguments[p], arguments[p + 1]);
+ var head = Pos(arguments[p + 2], arguments[p + 3]);
+ eqPos(sels[i].anchor, anchor, "anchor of selection " + i);
+ eqPos(sels[i].head, head, "head of selection " + i);
+ }
+ }
+ function hasCursors(cm) {
+ var sels = cm.listSelections();
+ var given = (arguments.length - 1) / 2;
+ if (sels.length != given)
+ throw new Failure("expected " + given + " selections, found " + sels.length);
+ for (var i = 0, p = 1; i < given; i++, p += 2) {
+ eqPos(sels[i].anchor, sels[i].head, "something selected for " + i);
+ var head = Pos(arguments[p], arguments[p + 1]);
+ eqPos(sels[i].head, head, "selection " + i);
+ }
+ }
+
+ testCM("getSelection", function(cm) {
+ select(cm, {anchor: Pos(0, 0), head: Pos(1, 2)}, {anchor: Pos(2, 2), head: Pos(2, 0)});
+ eq(cm.getSelection(), "1234\n56\n90");
+ eq(cm.getSelection(false).join("|"), "1234|56|90");
+ eq(cm.getSelections().join("|"), "1234\n56|90");
+ }, {value: "1234\n5678\n90"});
+
+ testCM("setSelection", function(cm) {
+ select(cm, Pos(3, 0), Pos(0, 0), {anchor: Pos(2, 5), head: Pos(1, 0)});
+ hasSelections(cm, 0, 0, 0, 0,
+ 2, 5, 1, 0,
+ 3, 0, 3, 0);
+ cm.setSelection(Pos(1, 2), Pos(1, 1));
+ hasSelections(cm, 1, 2, 1, 1);
+ select(cm, {anchor: Pos(1, 1), head: Pos(2, 4)},
+ {anchor: Pos(0, 0), head: Pos(1, 3)},
+ Pos(3, 0), Pos(2, 2));
+ hasSelections(cm, 0, 0, 2, 4,
+ 3, 0, 3, 0);
+ cm.setSelections([{anchor: Pos(0, 1), head: Pos(0, 2)},
+ {anchor: Pos(1, 1), head: Pos(1, 2)},
+ {anchor: Pos(2, 1), head: Pos(2, 2)}], 1);
+ eqPos(cm.getCursor("head"), Pos(1, 2));
+ eqPos(cm.getCursor("anchor"), Pos(1, 1));
+ eqPos(cm.getCursor("from"), Pos(1, 1));
+ eqPos(cm.getCursor("to"), Pos(1, 2));
+ cm.setCursor(Pos(1, 1));
+ hasCursors(cm, 1, 1);
+ }, {value: "abcde\nabcde\nabcde\n"});
+
+ testCM("somethingSelected", function(cm) {
+ select(cm, Pos(0, 1), {anchor: Pos(0, 3), head: Pos(0, 5)});
+ eq(cm.somethingSelected(), true);
+ select(cm, Pos(0, 1), Pos(0, 3), Pos(0, 5));
+ eq(cm.somethingSelected(), false);
+ }, {value: "123456789"});
+
+ testCM("extendSelection", function(cm) {
+ select(cm, Pos(0, 1), Pos(1, 1), Pos(2, 1));
+ cm.setExtending(true);
+ cm.extendSelections([Pos(0, 2), Pos(1, 0), Pos(2, 3)]);
+ hasSelections(cm, 0, 1, 0, 2,
+ 1, 1, 1, 0,
+ 2, 1, 2, 3);
+ cm.extendSelection(Pos(2, 4), Pos(2, 0));
+ hasSelections(cm, 2, 4, 2, 0);
+ }, {value: "1234\n1234\n1234"});
+
+ testCM("addSelection", function(cm) {
+ select(cm, Pos(0, 1), Pos(1, 1));
+ cm.addSelection(Pos(0, 0), Pos(0, 4));
+ hasSelections(cm, 0, 0, 0, 4,
+ 1, 1, 1, 1);
+ cm.addSelection(Pos(2, 2));
+ hasSelections(cm, 0, 0, 0, 4,
+ 1, 1, 1, 1,
+ 2, 2, 2, 2);
+ }, {value: "1234\n1234\n1234"});
+
+ testCM("replaceSelection", function(cm) {
+ var selections = [{anchor: Pos(0, 0), head: Pos(0, 1)},
+ {anchor: Pos(0, 2), head: Pos(0, 3)},
+ {anchor: Pos(0, 4), head: Pos(0, 5)},
+ {anchor: Pos(2, 1), head: Pos(2, 4)},
+ {anchor: Pos(2, 5), head: Pos(2, 6)}];
+ var val = "123456\n123456\n123456";
+ cm.setValue(val);
+ cm.setSelections(selections);
+ cm.replaceSelection("ab", "around");
+ eq(cm.getValue(), "ab2ab4ab6\n123456\n1ab5ab");
+ hasSelections(cm, 0, 0, 0, 2,
+ 0, 3, 0, 5,
+ 0, 6, 0, 8,
+ 2, 1, 2, 3,
+ 2, 4, 2, 6);
+ cm.setValue(val);
+ cm.setSelections(selections);
+ cm.replaceSelection("", "around");
+ eq(cm.getValue(), "246\n123456\n15");
+ hasSelections(cm, 0, 0, 0, 0,
+ 0, 1, 0, 1,
+ 0, 2, 0, 2,
+ 2, 1, 2, 1,
+ 2, 2, 2, 2);
+ cm.setValue(val);
+ cm.setSelections(selections);
+ cm.replaceSelection("X\nY\nZ", "around");
+ hasSelections(cm, 0, 0, 2, 1,
+ 2, 2, 4, 1,
+ 4, 2, 6, 1,
+ 8, 1, 10, 1,
+ 10, 2, 12, 1);
+ cm.replaceSelection("a", "around");
+ hasSelections(cm, 0, 0, 0, 1,
+ 0, 2, 0, 3,
+ 0, 4, 0, 5,
+ 2, 1, 2, 2,
+ 2, 3, 2, 4);
+ cm.replaceSelection("xy", "start");
+ hasSelections(cm, 0, 0, 0, 0,
+ 0, 3, 0, 3,
+ 0, 6, 0, 6,
+ 2, 1, 2, 1,
+ 2, 4, 2, 4);
+ cm.replaceSelection("z\nf");
+ hasSelections(cm, 1, 1, 1, 1,
+ 2, 1, 2, 1,
+ 3, 1, 3, 1,
+ 6, 1, 6, 1,
+ 7, 1, 7, 1);
+ eq(cm.getValue(), "z\nfxy2z\nfxy4z\nfxy6\n123456\n1z\nfxy5z\nfxy");
+ });
+
+ function select(cm) {
+ var sels = [];
+ for (var i = 1; i < arguments.length; i++) {
+ var arg = arguments[i];
+ if (arg.head) sels.push(arg);
+ else sels.push({head: arg, anchor: arg});
+ }
+ cm.setSelections(sels, sels.length - 1);
+ }
+
+ testCM("indentSelection", function(cm) {
+ select(cm, Pos(0, 1), Pos(1, 1));
+ cm.indentSelection(4);
+ eq(cm.getValue(), " foo\n bar\nbaz");
+
+ select(cm, Pos(0, 2), Pos(0, 3), Pos(0, 4));
+ cm.indentSelection(-2);
+ eq(cm.getValue(), " foo\n bar\nbaz");
+
+ select(cm, {anchor: Pos(0, 0), head: Pos(1, 2)},
+ {anchor: Pos(1, 3), head: Pos(2, 0)});
+ cm.indentSelection(-2);
+ eq(cm.getValue(), "foo\n bar\nbaz");
+ }, {value: "foo\nbar\nbaz"});
+
+ testCM("killLine", function(cm) {
+ select(cm, Pos(0, 1), Pos(0, 2), Pos(1, 1));
+ cm.execCommand("killLine");
+ eq(cm.getValue(), "f\nb\nbaz");
+ cm.execCommand("killLine");
+ eq(cm.getValue(), "fbbaz");
+ cm.setValue("foo\nbar\nbaz");
+ select(cm, Pos(0, 1), {anchor: Pos(0, 2), head: Pos(2, 1)});
+ cm.execCommand("killLine");
+ eq(cm.getValue(), "faz");
+ }, {value: "foo\nbar\nbaz"});
+
+ testCM("deleteLine", function(cm) {
+ select(cm, Pos(0, 0),
+ {head: Pos(0, 1), anchor: Pos(2, 0)},
+ Pos(4, 0));
+ cm.execCommand("deleteLine");
+ eq(cm.getValue(), "4\n6\n7");
+ select(cm, Pos(2, 1));
+ cm.execCommand("deleteLine");
+ eq(cm.getValue(), "4\n6\n");
+ }, {value: "1\n2\n3\n4\n5\n6\n7"});
+
+ testCM("deleteH", function(cm) {
+ select(cm, Pos(0, 4), {anchor: Pos(1, 4), head: Pos(1, 5)});
+ cm.execCommand("delWordAfter");
+ eq(cm.getValue(), "foo bar baz\nabc ef ghi\n");
+ cm.execCommand("delWordAfter");
+ eq(cm.getValue(), "foo baz\nabc ghi\n");
+ cm.execCommand("delCharBefore");
+ cm.execCommand("delCharBefore");
+ eq(cm.getValue(), "fo baz\nab ghi\n");
+ select(cm, Pos(0, 3), Pos(0, 4), Pos(0, 5));
+ cm.execCommand("delWordAfter");
+ eq(cm.getValue(), "fo \nab ghi\n");
+ }, {value: "foo bar baz\nabc def ghi\n"});
+
+ testCM("goLineStart", function(cm) {
+ select(cm, Pos(0, 2), Pos(0, 3), Pos(1, 1));
+ cm.execCommand("goLineStart");
+ hasCursors(cm, 0, 0, 1, 0);
+ select(cm, Pos(1, 1), Pos(0, 1));
+ cm.setExtending(true);
+ cm.execCommand("goLineStart");
+ hasSelections(cm, 0, 1, 0, 0,
+ 1, 1, 1, 0);
+ }, {value: "foo\nbar\nbaz"});
+
+ testCM("moveV", function(cm) {
+ select(cm, Pos(0, 2), Pos(1, 2));
+ cm.execCommand("goLineDown");
+ hasCursors(cm, 1, 2, 2, 2);
+ cm.execCommand("goLineUp");
+ hasCursors(cm, 0, 2, 1, 2);
+ cm.execCommand("goLineUp");
+ hasCursors(cm, 0, 0, 0, 2);
+ cm.execCommand("goLineUp");
+ hasCursors(cm, 0, 0);
+ select(cm, Pos(0, 2), Pos(1, 2));
+ cm.setExtending(true);
+ cm.execCommand("goLineDown");
+ hasSelections(cm, 0, 2, 2, 2);
+ }, {value: "12345\n12345\n12345"});
+
+ testCM("moveH", function(cm) {
+ select(cm, Pos(0, 1), Pos(0, 3), Pos(0, 5), Pos(2, 3));
+ cm.execCommand("goCharRight");
+ hasCursors(cm, 0, 2, 0, 4, 1, 0, 2, 4);
+ cm.execCommand("goCharLeft");
+ hasCursors(cm, 0, 1, 0, 3, 0, 5, 2, 3);
+ for (var i = 0; i < 15; i++)
+ cm.execCommand("goCharRight");
+ hasCursors(cm, 2, 4, 2, 5);
+ }, {value: "12345\n12345\n12345"});
+
+ testCM("newlineAndIndent", function(cm) {
+ select(cm, Pos(0, 5), Pos(1, 5));
+ cm.execCommand("newlineAndIndent");
+ hasCursors(cm, 1, 2, 3, 2);
+ eq(cm.getValue(), "x = [\n 1];\ny = [\n 2];");
+ cm.undo();
+ eq(cm.getValue(), "x = [1];\ny = [2];");
+ hasCursors(cm, 0, 5, 1, 5);
+ select(cm, Pos(0, 5), Pos(0, 6));
+ cm.execCommand("newlineAndIndent");
+ hasCursors(cm, 1, 2, 2, 0);
+ eq(cm.getValue(), "x = [\n 1\n];\ny = [2];");
+ }, {value: "x = [1];\ny = [2];", mode: "javascript"});
+
+ testCM("goDocStartEnd", function(cm) {
+ select(cm, Pos(0, 1), Pos(1, 1));
+ cm.execCommand("goDocStart");
+ hasCursors(cm, 0, 0);
+ select(cm, Pos(0, 1), Pos(1, 1));
+ cm.execCommand("goDocEnd");
+ hasCursors(cm, 1, 3);
+ select(cm, Pos(0, 1), Pos(1, 1));
+ cm.setExtending(true);
+ cm.execCommand("goDocEnd");
+ hasSelections(cm, 1, 1, 1, 3);
+ }, {value: "abc\ndef"});
+
+ testCM("selectionHistory", function(cm) {
+ for (var i = 0; i < 3; ++i)
+ cm.addSelection(Pos(0, i * 2), Pos(0, i * 2 + 1));
+ cm.execCommand("undoSelection");
+ eq(cm.getSelection(), "1\n2");
+ cm.execCommand("undoSelection");
+ eq(cm.getSelection(), "1");
+ cm.execCommand("undoSelection");
+ eq(cm.getSelection(), "");
+ eqPos(cm.getCursor(), Pos(0, 0));
+ cm.execCommand("redoSelection");
+ eq(cm.getSelection(), "1");
+ cm.execCommand("redoSelection");
+ eq(cm.getSelection(), "1\n2");
+ cm.execCommand("redoSelection");
+ eq(cm.getSelection(), "1\n2\n3");
+ }, {value: "1 2 3"});
+})();
diff --git a/devtools/client/sourceeditor/test/codemirror/search_test.js b/devtools/client/sourceeditor/test/codemirror/search_test.js
new file mode 100644
index 000000000..04a1e685a
--- /dev/null
+++ b/devtools/client/sourceeditor/test/codemirror/search_test.js
@@ -0,0 +1,62 @@
+(function() {
+ "use strict";
+
+ function test(name) {
+ var text = Array.prototype.slice.call(arguments, 1, arguments.length - 1).join("\n");
+ var body = arguments[arguments.length - 1];
+ return window.test("search_" + name, function() {
+ body(new CodeMirror.Doc(text));
+ });
+ }
+
+ function run(doc, query, insensitive) {
+ var cursor = doc.getSearchCursor(query, null, insensitive);
+ for (var i = 3; i < arguments.length; i += 4) {
+ var found = cursor.findNext();
+ is(found, "not enough results (forward)");
+ eqPos(Pos(arguments[i], arguments[i + 1]), cursor.from(), "from, forward, " + (i - 3) / 4);
+ eqPos(Pos(arguments[i + 2], arguments[i + 3]), cursor.to(), "to, forward, " + (i - 3) / 4);
+ }
+ is(!cursor.findNext(), "too many matches (forward)");
+ for (var i = arguments.length - 4; i >= 3; i -= 4) {
+ var found = cursor.findPrevious();
+ is(found, "not enough results (backwards)");
+ eqPos(Pos(arguments[i], arguments[i + 1]), cursor.from(), "from, backwards, " + (i - 3) / 4);
+ eqPos(Pos(arguments[i + 2], arguments[i + 3]), cursor.to(), "to, backwards, " + (i - 3) / 4);
+ }
+ is(!cursor.findPrevious(), "too many matches (backwards)");
+ }
+
+ test("simple", "abcdefg", "abcdefg", function(doc) {
+ run(doc, "cde", false, 0, 2, 0, 5, 1, 2, 1, 5);
+ });
+
+ test("multiline", "hallo", "goodbye", function(doc) {
+ run(doc, "llo\ngoo", false, 0, 2, 1, 3);
+ run(doc, "blah\nhall", false);
+ run(doc, "bye\neye", false);
+ });
+
+ test("regexp", "abcde", "abcde", function(doc) {
+ run(doc, /bcd/, false, 0, 1, 0, 4, 1, 1, 1, 4);
+ run(doc, /BCD/, false);
+ run(doc, /BCD/i, false, 0, 1, 0, 4, 1, 1, 1, 4);
+ });
+
+ test("insensitive", "hallo", "HALLO", "oink", "hAllO", function(doc) {
+ run(doc, "All", false, 3, 1, 3, 4);
+ run(doc, "All", true, 0, 1, 0, 4, 1, 1, 1, 4, 3, 1, 3, 4);
+ });
+
+ test("multilineInsensitive", "zie ginds komT", "De Stoomboot", "uit Spanje weer aan", function(doc) {
+ run(doc, "komt\nde stoomboot\nuit", false);
+ run(doc, "komt\nde stoomboot\nuit", true, 0, 10, 2, 3);
+ run(doc, "kOMt\ndE stOOmboot\nuiT", true, 0, 10, 2, 3);
+ });
+
+ test("expandingCaseFold", "<b>İİ İİ</b>", "<b>uu uu</b>", function(doc) {
+ if (phantom) return; // A Phantom bug makes this hang
+ run(doc, "</b>", true, 0, 8, 0, 12, 1, 8, 1, 12);
+ run(doc, "İİ", true, 0, 3, 0, 5, 0, 6, 0, 8);
+ });
+})();
diff --git a/devtools/client/sourceeditor/test/codemirror/sublime_test.js b/devtools/client/sourceeditor/test/codemirror/sublime_test.js
new file mode 100644
index 000000000..c5c19c0a2
--- /dev/null
+++ b/devtools/client/sourceeditor/test/codemirror/sublime_test.js
@@ -0,0 +1,307 @@
+(function() {
+ "use strict";
+
+ var Pos = CodeMirror.Pos;
+ namespace = "sublime_";
+
+ function stTest(name) {
+ var actions = Array.prototype.slice.call(arguments, 1);
+ testCM(name, function(cm) {
+ for (var i = 0; i < actions.length; i++) {
+ var action = actions[i];
+ if (typeof action == "string" && i == 0)
+ cm.setValue(action);
+ else if (typeof action == "string")
+ cm.execCommand(action);
+ else if (action instanceof Pos)
+ cm.setCursor(action);
+ else
+ action(cm);
+ }
+ });
+ }
+
+ function at(line, ch, msg) {
+ return function(cm) {
+ eq(cm.listSelections().length, 1);
+ eqPos(cm.getCursor("head"), Pos(line, ch), msg);
+ eqPos(cm.getCursor("anchor"), Pos(line, ch), msg);
+ };
+ }
+
+ function val(content, msg) {
+ return function(cm) { eq(cm.getValue(), content, msg); };
+ }
+
+ function argsToRanges(args) {
+ if (args.length % 4) throw new Error("Wrong number of arguments for ranges.");
+ var ranges = [];
+ for (var i = 0; i < args.length; i += 4)
+ ranges.push({anchor: Pos(args[i], args[i + 1]),
+ head: Pos(args[i + 2], args[i + 3])});
+ return ranges;
+ }
+
+ function setSel() {
+ var ranges = argsToRanges(arguments);
+ return function(cm) { cm.setSelections(ranges, 0); };
+ }
+
+ function hasSel() {
+ var ranges = argsToRanges(arguments);
+ return function(cm) {
+ var sels = cm.listSelections();
+ if (sels.length != ranges.length)
+ throw new Failure("Expected " + ranges.length + " selections, but found " + sels.length);
+ for (var i = 0; i < sels.length; i++) {
+ eqPos(sels[i].anchor, ranges[i].anchor, "anchor " + i);
+ eqPos(sels[i].head, ranges[i].head, "head " + i);
+ }
+ };
+ }
+
+ stTest("bySubword", "the foo_bar DooDahBah \n a",
+ "goSubwordLeft", at(0, 0),
+ "goSubwordRight", at(0, 3),
+ "goSubwordRight", at(0, 7),
+ "goSubwordRight", at(0, 11),
+ "goSubwordRight", at(0, 15),
+ "goSubwordRight", at(0, 18),
+ "goSubwordRight", at(0, 21),
+ "goSubwordRight", at(0, 22),
+ "goSubwordRight", at(1, 0),
+ "goSubwordRight", at(1, 2),
+ "goSubwordRight", at(1, 2),
+ "goSubwordLeft", at(1, 1),
+ "goSubwordLeft", at(1, 0),
+ "goSubwordLeft", at(0, 22),
+ "goSubwordLeft", at(0, 18),
+ "goSubwordLeft", at(0, 15),
+ "goSubwordLeft", at(0, 12),
+ "goSubwordLeft", at(0, 8),
+ "goSubwordLeft", at(0, 4),
+ "goSubwordLeft", at(0, 0));
+
+ stTest("splitSelectionByLine", "abc\ndef\nghi",
+ setSel(0, 1, 2, 2),
+ "splitSelectionByLine",
+ hasSel(0, 1, 0, 3,
+ 1, 0, 1, 3,
+ 2, 0, 2, 2));
+
+ stTest("splitSelectionByLineMulti", "abc\ndef\nghi\njkl",
+ setSel(0, 1, 1, 1,
+ 1, 2, 3, 2,
+ 3, 3, 3, 3),
+ "splitSelectionByLine",
+ hasSel(0, 1, 0, 3,
+ 1, 0, 1, 1,
+ 1, 2, 1, 3,
+ 2, 0, 2, 3,
+ 3, 0, 3, 2,
+ 3, 3, 3, 3));
+
+ stTest("selectLine", "abc\ndef\nghi",
+ setSel(0, 1, 0, 1,
+ 2, 0, 2, 1),
+ "selectLine",
+ hasSel(0, 0, 1, 0,
+ 2, 0, 2, 3),
+ setSel(0, 1, 1, 0),
+ "selectLine",
+ hasSel(0, 0, 2, 0));
+
+ stTest("insertLineAfter", "abcde\nfghijkl\nmn",
+ setSel(0, 1, 0, 1,
+ 0, 3, 0, 3,
+ 1, 2, 1, 2,
+ 1, 3, 1, 5), "insertLineAfter",
+ hasSel(1, 0, 1, 0,
+ 3, 0, 3, 0), val("abcde\n\nfghijkl\n\nmn"));
+
+ stTest("insertLineBefore", "abcde\nfghijkl\nmn",
+ setSel(0, 1, 0, 1,
+ 0, 3, 0, 3,
+ 1, 2, 1, 2,
+ 1, 3, 1, 5), "insertLineBefore",
+ hasSel(0, 0, 0, 0,
+ 2, 0, 2, 0), val("\nabcde\n\nfghijkl\nmn"));
+
+ stTest("selectNextOccurrence", "a foo bar\nfoobar foo",
+ setSel(0, 2, 0, 5),
+ "selectNextOccurrence", hasSel(0, 2, 0, 5,
+ 1, 0, 1, 3),
+ "selectNextOccurrence", hasSel(0, 2, 0, 5,
+ 1, 0, 1, 3,
+ 1, 7, 1, 10),
+ "selectNextOccurrence", hasSel(0, 2, 0, 5,
+ 1, 0, 1, 3,
+ 1, 7, 1, 10),
+ Pos(0, 3), "selectNextOccurrence", hasSel(0, 2, 0, 5),
+ "selectNextOccurrence", hasSel(0, 2, 0, 5,
+ 1, 7, 1, 10),
+ setSel(0, 6, 0, 9),
+ "selectNextOccurrence", hasSel(0, 6, 0, 9,
+ 1, 3, 1, 6));
+
+ stTest("selectScope", "foo(a) {\n bar[1, 2];\n}",
+ "selectScope", hasSel(0, 0, 2, 1),
+ Pos(0, 4), "selectScope", hasSel(0, 4, 0, 5),
+ Pos(0, 5), "selectScope", hasSel(0, 4, 0, 5),
+ Pos(0, 6), "selectScope", hasSel(0, 0, 2, 1),
+ Pos(0, 8), "selectScope", hasSel(0, 8, 2, 0),
+ Pos(1, 2), "selectScope", hasSel(0, 8, 2, 0),
+ Pos(1, 6), "selectScope", hasSel(1, 6, 1, 10),
+ Pos(1, 9), "selectScope", hasSel(1, 6, 1, 10));
+
+ stTest("goToBracket", "foo(a) {\n bar[1, 2];\n}",
+ Pos(0, 0), "goToBracket", at(0, 0),
+ Pos(0, 4), "goToBracket", at(0, 5), "goToBracket", at(0, 4),
+ Pos(0, 8), "goToBracket", at(2, 0), "goToBracket", at(0, 8),
+ Pos(1, 2), "goToBracket", at(2, 0),
+ Pos(1, 7), "goToBracket", at(1, 10), "goToBracket", at(1, 6));
+
+ stTest("swapLine", "1\n2\n3---\n4\n5",
+ "swapLineDown", val("2\n1\n3---\n4\n5"),
+ "swapLineUp", val("1\n2\n3---\n4\n5"),
+ "swapLineUp", val("1\n2\n3---\n4\n5"),
+ Pos(4, 1), "swapLineDown", val("1\n2\n3---\n4\n5"),
+ setSel(0, 1, 0, 1,
+ 1, 0, 2, 0,
+ 2, 2, 2, 2),
+ "swapLineDown", val("4\n1\n2\n3---\n5"),
+ hasSel(1, 1, 1, 1,
+ 2, 0, 3, 0,
+ 3, 2, 3, 2),
+ "swapLineUp", val("1\n2\n3---\n4\n5"),
+ hasSel(0, 1, 0, 1,
+ 1, 0, 2, 0,
+ 2, 2, 2, 2));
+
+ stTest("swapLineEmptyBottomSel", "1\n2\n3",
+ setSel(0, 1, 1, 0),
+ "swapLineDown", val("2\n1\n3"), hasSel(1, 1, 2, 0),
+ "swapLineUp", val("1\n2\n3"), hasSel(0, 1, 1, 0),
+ "swapLineUp", val("1\n2\n3"), hasSel(0, 0, 0, 0));
+
+ stTest("swapLineUpFromEnd", "a\nb\nc",
+ Pos(2, 1), "swapLineUp",
+ hasSel(1, 1, 1, 1), val("a\nc\nb"));
+
+ stTest("joinLines", "abc\ndef\nghi\njkl",
+ "joinLines", val("abc def\nghi\njkl"), at(0, 4),
+ "undo",
+ setSel(0, 2, 1, 1), "joinLines",
+ val("abc def ghi\njkl"), hasSel(0, 2, 0, 8),
+ "undo",
+ setSel(0, 1, 0, 1,
+ 1, 1, 1, 1,
+ 3, 1, 3, 1), "joinLines",
+ val("abc def ghi\njkl"), hasSel(0, 4, 0, 4,
+ 0, 8, 0, 8,
+ 1, 3, 1, 3));
+
+ stTest("duplicateLine", "abc\ndef\nghi",
+ Pos(1, 0), "duplicateLine", val("abc\ndef\ndef\nghi"), at(2, 0),
+ "undo",
+ setSel(0, 1, 0, 1,
+ 1, 1, 1, 1,
+ 2, 1, 2, 1), "duplicateLine",
+ val("abc\nabc\ndef\ndef\nghi\nghi"), hasSel(1, 1, 1, 1,
+ 3, 1, 3, 1,
+ 5, 1, 5, 1));
+ stTest("duplicateLineSelection", "abcdef",
+ setSel(0, 1, 0, 1,
+ 0, 2, 0, 4,
+ 0, 5, 0, 5),
+ "duplicateLine",
+ val("abcdef\nabcdcdef\nabcdcdef"), hasSel(2, 1, 2, 1,
+ 2, 4, 2, 6,
+ 2, 7, 2, 7));
+
+ stTest("selectLinesUpward", "123\n345\n789\n012",
+ setSel(0, 1, 0, 1,
+ 1, 1, 1, 3,
+ 2, 0, 2, 0,
+ 3, 0, 3, 0),
+ "selectLinesUpward",
+ hasSel(0, 1, 0, 1,
+ 0, 3, 0, 3,
+ 1, 0, 1, 0,
+ 1, 1, 1, 3,
+ 2, 0, 2, 0,
+ 3, 0, 3, 0));
+
+ stTest("selectLinesDownward", "123\n345\n789\n012",
+ setSel(0, 1, 0, 1,
+ 1, 1, 1, 3,
+ 2, 0, 2, 0,
+ 3, 0, 3, 0),
+ "selectLinesDownward",
+ hasSel(0, 1, 0, 1,
+ 1, 1, 1, 3,
+ 2, 0, 2, 0,
+ 2, 3, 2, 3,
+ 3, 0, 3, 0));
+
+ stTest("sortLines", "c\nb\na\nC\nB\nA",
+ "sortLines", val("A\nB\nC\na\nb\nc"),
+ "undo",
+ setSel(0, 0, 2, 0,
+ 3, 0, 5, 0),
+ "sortLines", val("a\nb\nc\nA\nB\nC"),
+ hasSel(0, 0, 2, 1,
+ 3, 0, 5, 1),
+ "undo",
+ setSel(1, 0, 4, 0), "sortLinesInsensitive", val("c\na\nB\nb\nC\nA"));
+
+ stTest("bookmarks", "abc\ndef\nghi\njkl",
+ Pos(0, 1), "toggleBookmark",
+ setSel(1, 1, 1, 2), "toggleBookmark",
+ setSel(2, 1, 2, 2), "toggleBookmark",
+ "nextBookmark", hasSel(0, 1, 0, 1),
+ "nextBookmark", hasSel(1, 1, 1, 2),
+ "nextBookmark", hasSel(2, 1, 2, 2),
+ "prevBookmark", hasSel(1, 1, 1, 2),
+ "prevBookmark", hasSel(0, 1, 0, 1),
+ "prevBookmark", hasSel(2, 1, 2, 2),
+ "prevBookmark", hasSel(1, 1, 1, 2),
+ "toggleBookmark",
+ "prevBookmark", hasSel(2, 1, 2, 2),
+ "prevBookmark", hasSel(0, 1, 0, 1),
+ "selectBookmarks", hasSel(0, 1, 0, 1,
+ 2, 1, 2, 2),
+ "clearBookmarks",
+ Pos(0, 0), "selectBookmarks", at(0, 0));
+
+ stTest("smartBackspace", " foo\n bar",
+ setSel(0, 2, 0, 2, 1, 4, 1, 4, 1, 6, 1, 6), "smartBackspace",
+ val("foo\n br"))
+
+ stTest("upAndDowncaseAtCursor", "abc\ndef x\nghI",
+ setSel(0, 1, 0, 3,
+ 1, 1, 1, 1,
+ 1, 4, 1, 4), "upcaseAtCursor",
+ val("aBC\nDEF x\nghI"), hasSel(0, 1, 0, 3,
+ 1, 3, 1, 3,
+ 1, 4, 1, 4),
+ "downcaseAtCursor",
+ val("abc\ndef x\nghI"), hasSel(0, 1, 0, 3,
+ 1, 3, 1, 3,
+ 1, 4, 1, 4));
+
+ stTest("mark", "abc\ndef\nghi",
+ Pos(1, 1), "setSublimeMark",
+ Pos(2, 1), "selectToSublimeMark", hasSel(2, 1, 1, 1),
+ Pos(0, 1), "swapWithSublimeMark", at(1, 1), "swapWithSublimeMark", at(0, 1),
+ "deleteToSublimeMark", val("aef\nghi"),
+ "sublimeYank", val("abc\ndef\nghi"), at(1, 1));
+
+ stTest("findUnder", "foo foobar a",
+ "findUnder", hasSel(0, 4, 0, 7),
+ "findUnder", hasSel(0, 0, 0, 3),
+ "findUnderPrevious", hasSel(0, 4, 0, 7),
+ "findUnderPrevious", hasSel(0, 0, 0, 3),
+ Pos(0, 4), "findUnder", hasSel(0, 4, 0, 10),
+ Pos(0, 11), "findUnder", hasSel(0, 11, 0, 11));
+})();
diff --git a/devtools/client/sourceeditor/test/codemirror/test.js b/devtools/client/sourceeditor/test/codemirror/test.js
new file mode 100644
index 000000000..82ee231e3
--- /dev/null
+++ b/devtools/client/sourceeditor/test/codemirror/test.js
@@ -0,0 +1,2151 @@
+var Pos = CodeMirror.Pos;
+
+CodeMirror.defaults.rtlMoveVisually = true;
+
+function forEach(arr, f) {
+ for (var i = 0, e = arr.length; i < e; ++i) f(arr[i], i);
+}
+
+function addDoc(cm, width, height) {
+ var content = [], line = "";
+ for (var i = 0; i < width; ++i) line += "x";
+ for (var i = 0; i < height; ++i) content.push(line);
+ cm.setValue(content.join("\n"));
+}
+
+function byClassName(elt, cls) {
+ if (elt.getElementsByClassName) return elt.getElementsByClassName(cls);
+ var found = [], re = new RegExp("\\b" + cls + "\\b");
+ function search(elt) {
+ if (elt.nodeType == 3) return;
+ if (re.test(elt.className)) found.push(elt);
+ for (var i = 0, e = elt.childNodes.length; i < e; ++i)
+ search(elt.childNodes[i]);
+ }
+ search(elt);
+ return found;
+}
+
+var ie_lt8 = /MSIE [1-7]\b/.test(navigator.userAgent);
+var ie_lt9 = /MSIE [1-8]\b/.test(navigator.userAgent);
+var mac = /Mac/.test(navigator.platform);
+var phantom = /PhantomJS/.test(navigator.userAgent);
+var opera = /Opera\/\./.test(navigator.userAgent);
+var opera_version = opera && navigator.userAgent.match(/Version\/(\d+\.\d+)/);
+if (opera_version) opera_version = Number(opera_version);
+var opera_lt10 = opera && (!opera_version || opera_version < 10);
+
+namespace = "core_";
+
+test("core_fromTextArea", function() {
+ var te = document.getElementById("code");
+ te.value = "CONTENT";
+ var cm = CodeMirror.fromTextArea(te);
+ is(!te.offsetHeight);
+ eq(cm.getValue(), "CONTENT");
+ cm.setValue("foo\nbar");
+ eq(cm.getValue(), "foo\nbar");
+ cm.save();
+ is(/^foo\r?\nbar$/.test(te.value));
+ cm.setValue("xxx");
+ cm.toTextArea();
+ is(te.offsetHeight);
+ eq(te.value, "xxx");
+});
+
+testCM("getRange", function(cm) {
+ eq(cm.getLine(0), "1234");
+ eq(cm.getLine(1), "5678");
+ eq(cm.getLine(2), null);
+ eq(cm.getLine(-1), null);
+ eq(cm.getRange(Pos(0, 0), Pos(0, 3)), "123");
+ eq(cm.getRange(Pos(0, -1), Pos(0, 200)), "1234");
+ eq(cm.getRange(Pos(0, 2), Pos(1, 2)), "34\n56");
+ eq(cm.getRange(Pos(1, 2), Pos(100, 0)), "78");
+}, {value: "1234\n5678"});
+
+testCM("replaceRange", function(cm) {
+ eq(cm.getValue(), "");
+ cm.replaceRange("foo\n", Pos(0, 0));
+ eq(cm.getValue(), "foo\n");
+ cm.replaceRange("a\nb", Pos(0, 1));
+ eq(cm.getValue(), "fa\nboo\n");
+ eq(cm.lineCount(), 3);
+ cm.replaceRange("xyzzy", Pos(0, 0), Pos(1, 1));
+ eq(cm.getValue(), "xyzzyoo\n");
+ cm.replaceRange("abc", Pos(0, 0), Pos(10, 0));
+ eq(cm.getValue(), "abc");
+ eq(cm.lineCount(), 1);
+});
+
+testCM("selection", function(cm) {
+ cm.setSelection(Pos(0, 4), Pos(2, 2));
+ is(cm.somethingSelected());
+ eq(cm.getSelection(), "11\n222222\n33");
+ eqPos(cm.getCursor(false), Pos(2, 2));
+ eqPos(cm.getCursor(true), Pos(0, 4));
+ cm.setSelection(Pos(1, 0));
+ is(!cm.somethingSelected());
+ eq(cm.getSelection(), "");
+ eqPos(cm.getCursor(true), Pos(1, 0));
+ cm.replaceSelection("abc", "around");
+ eq(cm.getSelection(), "abc");
+ eq(cm.getValue(), "111111\nabc222222\n333333");
+ cm.replaceSelection("def", "end");
+ eq(cm.getSelection(), "");
+ eqPos(cm.getCursor(true), Pos(1, 3));
+ cm.setCursor(Pos(2, 1));
+ eqPos(cm.getCursor(true), Pos(2, 1));
+ cm.setCursor(1, 2);
+ eqPos(cm.getCursor(true), Pos(1, 2));
+}, {value: "111111\n222222\n333333"});
+
+testCM("extendSelection", function(cm) {
+ cm.setExtending(true);
+ addDoc(cm, 10, 10);
+ cm.setSelection(Pos(3, 5));
+ eqPos(cm.getCursor("head"), Pos(3, 5));
+ eqPos(cm.getCursor("anchor"), Pos(3, 5));
+ cm.setSelection(Pos(2, 5), Pos(5, 5));
+ eqPos(cm.getCursor("head"), Pos(5, 5));
+ eqPos(cm.getCursor("anchor"), Pos(2, 5));
+ eqPos(cm.getCursor("start"), Pos(2, 5));
+ eqPos(cm.getCursor("end"), Pos(5, 5));
+ cm.setSelection(Pos(5, 5), Pos(2, 5));
+ eqPos(cm.getCursor("head"), Pos(2, 5));
+ eqPos(cm.getCursor("anchor"), Pos(5, 5));
+ eqPos(cm.getCursor("start"), Pos(2, 5));
+ eqPos(cm.getCursor("end"), Pos(5, 5));
+ cm.extendSelection(Pos(3, 2));
+ eqPos(cm.getCursor("head"), Pos(3, 2));
+ eqPos(cm.getCursor("anchor"), Pos(5, 5));
+ cm.extendSelection(Pos(6, 2));
+ eqPos(cm.getCursor("head"), Pos(6, 2));
+ eqPos(cm.getCursor("anchor"), Pos(5, 5));
+ cm.extendSelection(Pos(6, 3), Pos(6, 4));
+ eqPos(cm.getCursor("head"), Pos(6, 4));
+ eqPos(cm.getCursor("anchor"), Pos(5, 5));
+ cm.extendSelection(Pos(0, 3), Pos(0, 4));
+ eqPos(cm.getCursor("head"), Pos(0, 3));
+ eqPos(cm.getCursor("anchor"), Pos(5, 5));
+ cm.extendSelection(Pos(4, 5), Pos(6, 5));
+ eqPos(cm.getCursor("head"), Pos(6, 5));
+ eqPos(cm.getCursor("anchor"), Pos(4, 5));
+ cm.setExtending(false);
+ cm.extendSelection(Pos(0, 3), Pos(0, 4));
+ eqPos(cm.getCursor("head"), Pos(0, 3));
+ eqPos(cm.getCursor("anchor"), Pos(0, 4));
+});
+
+testCM("lines", function(cm) {
+ eq(cm.getLine(0), "111111");
+ eq(cm.getLine(1), "222222");
+ eq(cm.getLine(-1), null);
+ cm.replaceRange("", Pos(1, 0), Pos(2, 0))
+ cm.replaceRange("abc", Pos(1, 0), Pos(1));
+ eq(cm.getValue(), "111111\nabc");
+}, {value: "111111\n222222\n333333"});
+
+testCM("indent", function(cm) {
+ cm.indentLine(1);
+ eq(cm.getLine(1), " blah();");
+ cm.setOption("indentUnit", 8);
+ cm.indentLine(1);
+ eq(cm.getLine(1), "\tblah();");
+ cm.setOption("indentUnit", 10);
+ cm.setOption("tabSize", 4);
+ cm.indentLine(1);
+ eq(cm.getLine(1), "\t\t blah();");
+}, {value: "if (x) {\nblah();\n}", indentUnit: 3, indentWithTabs: true, tabSize: 8});
+
+testCM("indentByNumber", function(cm) {
+ cm.indentLine(0, 2);
+ eq(cm.getLine(0), " foo");
+ cm.indentLine(0, -200);
+ eq(cm.getLine(0), "foo");
+ cm.setSelection(Pos(0, 0), Pos(1, 2));
+ cm.indentSelection(3);
+ eq(cm.getValue(), " foo\n bar\nbaz");
+}, {value: "foo\nbar\nbaz"});
+
+test("core_defaults", function() {
+ var defsCopy = {}, defs = CodeMirror.defaults;
+ for (var opt in defs) defsCopy[opt] = defs[opt];
+ defs.indentUnit = 5;
+ defs.value = "uu";
+ defs.indentWithTabs = true;
+ defs.tabindex = 55;
+ var place = document.getElementById("testground"), cm = CodeMirror(place);
+ try {
+ eq(cm.getOption("indentUnit"), 5);
+ cm.setOption("indentUnit", 10);
+ eq(defs.indentUnit, 5);
+ eq(cm.getValue(), "uu");
+ eq(cm.getOption("indentWithTabs"), true);
+ eq(cm.getInputField().tabIndex, 55);
+ }
+ finally {
+ for (var opt in defsCopy) defs[opt] = defsCopy[opt];
+ place.removeChild(cm.getWrapperElement());
+ }
+});
+
+testCM("lineInfo", function(cm) {
+ eq(cm.lineInfo(-1), null);
+ var mark = document.createElement("span");
+ var lh = cm.setGutterMarker(1, "FOO", mark);
+ var info = cm.lineInfo(1);
+ eq(info.text, "222222");
+ eq(info.gutterMarkers.FOO, mark);
+ eq(info.line, 1);
+ eq(cm.lineInfo(2).gutterMarkers, null);
+ cm.setGutterMarker(lh, "FOO", null);
+ eq(cm.lineInfo(1).gutterMarkers, null);
+ cm.setGutterMarker(1, "FOO", mark);
+ cm.setGutterMarker(0, "FOO", mark);
+ cm.clearGutter("FOO");
+ eq(cm.lineInfo(0).gutterMarkers, null);
+ eq(cm.lineInfo(1).gutterMarkers, null);
+}, {value: "111111\n222222\n333333"});
+
+testCM("coords", function(cm) {
+ cm.setSize(null, 100);
+ addDoc(cm, 32, 200);
+ var top = cm.charCoords(Pos(0, 0));
+ var bot = cm.charCoords(Pos(200, 30));
+ is(top.left < bot.left);
+ is(top.top < bot.top);
+ is(top.top < top.bottom);
+ cm.scrollTo(null, 100);
+ var top2 = cm.charCoords(Pos(0, 0));
+ is(top.top > top2.top);
+ eq(top.left, top2.left);
+});
+
+testCM("coordsChar", function(cm) {
+ addDoc(cm, 35, 70);
+ for (var i = 0; i < 2; ++i) {
+ var sys = i ? "local" : "page";
+ for (var ch = 0; ch <= 35; ch += 5) {
+ for (var line = 0; line < 70; line += 5) {
+ cm.setCursor(line, ch);
+ var coords = cm.charCoords(Pos(line, ch), sys);
+ var pos = cm.coordsChar({left: coords.left + 1, top: coords.top + 1}, sys);
+ eqPos(pos, Pos(line, ch));
+ }
+ }
+ }
+}, {lineNumbers: true});
+
+testCM("posFromIndex", function(cm) {
+ cm.setValue(
+ "This function should\n" +
+ "convert a zero based index\n" +
+ "to line and ch."
+ );
+
+ var examples = [
+ { index: -1, line: 0, ch: 0 }, // <- Tests clipping
+ { index: 0, line: 0, ch: 0 },
+ { index: 10, line: 0, ch: 10 },
+ { index: 39, line: 1, ch: 18 },
+ { index: 55, line: 2, ch: 7 },
+ { index: 63, line: 2, ch: 15 },
+ { index: 64, line: 2, ch: 15 } // <- Tests clipping
+ ];
+
+ for (var i = 0; i < examples.length; i++) {
+ var example = examples[i];
+ var pos = cm.posFromIndex(example.index);
+ eq(pos.line, example.line);
+ eq(pos.ch, example.ch);
+ if (example.index >= 0 && example.index < 64)
+ eq(cm.indexFromPos(pos), example.index);
+ }
+});
+
+testCM("undo", function(cm) {
+ cm.replaceRange("def", Pos(0, 0), Pos(0));
+ eq(cm.historySize().undo, 1);
+ cm.undo();
+ eq(cm.getValue(), "abc");
+ eq(cm.historySize().undo, 0);
+ eq(cm.historySize().redo, 1);
+ cm.redo();
+ eq(cm.getValue(), "def");
+ eq(cm.historySize().undo, 1);
+ eq(cm.historySize().redo, 0);
+ cm.setValue("1\n\n\n2");
+ cm.clearHistory();
+ eq(cm.historySize().undo, 0);
+ for (var i = 0; i < 20; ++i) {
+ cm.replaceRange("a", Pos(0, 0));
+ cm.replaceRange("b", Pos(3, 0));
+ }
+ eq(cm.historySize().undo, 40);
+ for (var i = 0; i < 40; ++i)
+ cm.undo();
+ eq(cm.historySize().redo, 40);
+ eq(cm.getValue(), "1\n\n\n2");
+}, {value: "abc"});
+
+testCM("undoDepth", function(cm) {
+ cm.replaceRange("d", Pos(0));
+ cm.replaceRange("e", Pos(0));
+ cm.replaceRange("f", Pos(0));
+ cm.undo(); cm.undo(); cm.undo();
+ eq(cm.getValue(), "abcd");
+}, {value: "abc", undoDepth: 4});
+
+testCM("undoDoesntClearValue", function(cm) {
+ cm.undo();
+ eq(cm.getValue(), "x");
+}, {value: "x"});
+
+testCM("undoMultiLine", function(cm) {
+ cm.operation(function() {
+ cm.replaceRange("x", Pos(0, 0));
+ cm.replaceRange("y", Pos(1, 0));
+ });
+ cm.undo();
+ eq(cm.getValue(), "abc\ndef\nghi");
+ cm.operation(function() {
+ cm.replaceRange("y", Pos(1, 0));
+ cm.replaceRange("x", Pos(0, 0));
+ });
+ cm.undo();
+ eq(cm.getValue(), "abc\ndef\nghi");
+ cm.operation(function() {
+ cm.replaceRange("y", Pos(2, 0));
+ cm.replaceRange("x", Pos(1, 0));
+ cm.replaceRange("z", Pos(2, 0));
+ });
+ cm.undo();
+ eq(cm.getValue(), "abc\ndef\nghi", 3);
+}, {value: "abc\ndef\nghi"});
+
+testCM("undoComposite", function(cm) {
+ cm.replaceRange("y", Pos(1));
+ cm.operation(function() {
+ cm.replaceRange("x", Pos(0));
+ cm.replaceRange("z", Pos(2));
+ });
+ eq(cm.getValue(), "ax\nby\ncz\n");
+ cm.undo();
+ eq(cm.getValue(), "a\nby\nc\n");
+ cm.undo();
+ eq(cm.getValue(), "a\nb\nc\n");
+ cm.redo(); cm.redo();
+ eq(cm.getValue(), "ax\nby\ncz\n");
+}, {value: "a\nb\nc\n"});
+
+testCM("undoSelection", function(cm) {
+ cm.setSelection(Pos(0, 2), Pos(0, 4));
+ cm.replaceSelection("");
+ cm.setCursor(Pos(1, 0));
+ cm.undo();
+ eqPos(cm.getCursor(true), Pos(0, 2));
+ eqPos(cm.getCursor(false), Pos(0, 4));
+ cm.setCursor(Pos(1, 0));
+ cm.redo();
+ eqPos(cm.getCursor(true), Pos(0, 2));
+ eqPos(cm.getCursor(false), Pos(0, 2));
+}, {value: "abcdefgh\n"});
+
+testCM("undoSelectionAsBefore", function(cm) {
+ cm.replaceSelection("abc", "around");
+ cm.undo();
+ cm.redo();
+ eq(cm.getSelection(), "abc");
+});
+
+testCM("selectionChangeConfusesHistory", function(cm) {
+ cm.replaceSelection("abc", null, "dontmerge");
+ cm.operation(function() {
+ cm.setCursor(Pos(0, 0));
+ cm.replaceSelection("abc", null, "dontmerge");
+ });
+ eq(cm.historySize().undo, 2);
+});
+
+testCM("markTextSingleLine", function(cm) {
+ forEach([{a: 0, b: 1, c: "", f: 2, t: 5},
+ {a: 0, b: 4, c: "", f: 0, t: 2},
+ {a: 1, b: 2, c: "x", f: 3, t: 6},
+ {a: 4, b: 5, c: "", f: 3, t: 5},
+ {a: 4, b: 5, c: "xx", f: 3, t: 7},
+ {a: 2, b: 5, c: "", f: 2, t: 3},
+ {a: 2, b: 5, c: "abcd", f: 6, t: 7},
+ {a: 2, b: 6, c: "x", f: null, t: null},
+ {a: 3, b: 6, c: "", f: null, t: null},
+ {a: 0, b: 9, c: "hallo", f: null, t: null},
+ {a: 4, b: 6, c: "x", f: 3, t: 4},
+ {a: 4, b: 8, c: "", f: 3, t: 4},
+ {a: 6, b: 6, c: "a", f: 3, t: 6},
+ {a: 8, b: 9, c: "", f: 3, t: 6}], function(test) {
+ cm.setValue("1234567890");
+ var r = cm.markText(Pos(0, 3), Pos(0, 6), {className: "foo"});
+ cm.replaceRange(test.c, Pos(0, test.a), Pos(0, test.b));
+ var f = r.find();
+ eq(f && f.from.ch, test.f); eq(f && f.to.ch, test.t);
+ });
+});
+
+testCM("markTextMultiLine", function(cm) {
+ function p(v) { return v && Pos(v[0], v[1]); }
+ forEach([{a: [0, 0], b: [0, 5], c: "", f: [0, 0], t: [2, 5]},
+ {a: [0, 0], b: [0, 5], c: "foo\n", f: [1, 0], t: [3, 5]},
+ {a: [0, 1], b: [0, 10], c: "", f: [0, 1], t: [2, 5]},
+ {a: [0, 5], b: [0, 6], c: "x", f: [0, 6], t: [2, 5]},
+ {a: [0, 0], b: [1, 0], c: "", f: [0, 0], t: [1, 5]},
+ {a: [0, 6], b: [2, 4], c: "", f: [0, 5], t: [0, 7]},
+ {a: [0, 6], b: [2, 4], c: "aa", f: [0, 5], t: [0, 9]},
+ {a: [1, 2], b: [1, 8], c: "", f: [0, 5], t: [2, 5]},
+ {a: [0, 5], b: [2, 5], c: "xx", f: null, t: null},
+ {a: [0, 0], b: [2, 10], c: "x", f: null, t: null},
+ {a: [1, 5], b: [2, 5], c: "", f: [0, 5], t: [1, 5]},
+ {a: [2, 0], b: [2, 3], c: "", f: [0, 5], t: [2, 2]},
+ {a: [2, 5], b: [3, 0], c: "a\nb", f: [0, 5], t: [2, 5]},
+ {a: [2, 3], b: [3, 0], c: "x", f: [0, 5], t: [2, 3]},
+ {a: [1, 1], b: [1, 9], c: "1\n2\n3", f: [0, 5], t: [4, 5]}], function(test) {
+ cm.setValue("aaaaaaaaaa\nbbbbbbbbbb\ncccccccccc\ndddddddd\n");
+ var r = cm.markText(Pos(0, 5), Pos(2, 5),
+ {className: "CodeMirror-matchingbracket"});
+ cm.replaceRange(test.c, p(test.a), p(test.b));
+ var f = r.find();
+ eqPos(f && f.from, p(test.f)); eqPos(f && f.to, p(test.t));
+ });
+});
+
+testCM("markTextUndo", function(cm) {
+ var marker1, marker2, bookmark;
+ marker1 = cm.markText(Pos(0, 1), Pos(0, 3),
+ {className: "CodeMirror-matchingbracket"});
+ marker2 = cm.markText(Pos(0, 0), Pos(2, 1),
+ {className: "CodeMirror-matchingbracket"});
+ bookmark = cm.setBookmark(Pos(1, 5));
+ cm.operation(function(){
+ cm.replaceRange("foo", Pos(0, 2));
+ cm.replaceRange("bar\nbaz\nbug\n", Pos(2, 0), Pos(3, 0));
+ });
+ var v1 = cm.getValue();
+ cm.setValue("");
+ eq(marker1.find(), null); eq(marker2.find(), null); eq(bookmark.find(), null);
+ cm.undo();
+ eqPos(bookmark.find(), Pos(1, 5), "still there");
+ cm.undo();
+ var m1Pos = marker1.find(), m2Pos = marker2.find();
+ eqPos(m1Pos.from, Pos(0, 1)); eqPos(m1Pos.to, Pos(0, 3));
+ eqPos(m2Pos.from, Pos(0, 0)); eqPos(m2Pos.to, Pos(2, 1));
+ eqPos(bookmark.find(), Pos(1, 5));
+ cm.redo(); cm.redo();
+ eq(bookmark.find(), null);
+ cm.undo();
+ eqPos(bookmark.find(), Pos(1, 5));
+ eq(cm.getValue(), v1);
+}, {value: "1234\n56789\n00\n"});
+
+testCM("markTextStayGone", function(cm) {
+ var m1 = cm.markText(Pos(0, 0), Pos(0, 1));
+ cm.replaceRange("hi", Pos(0, 2));
+ m1.clear();
+ cm.undo();
+ eq(m1.find(), null);
+}, {value: "hello"});
+
+testCM("markTextAllowEmpty", function(cm) {
+ var m1 = cm.markText(Pos(0, 1), Pos(0, 2), {clearWhenEmpty: false});
+ is(m1.find());
+ cm.replaceRange("x", Pos(0, 0));
+ is(m1.find());
+ cm.replaceRange("y", Pos(0, 2));
+ is(m1.find());
+ cm.replaceRange("z", Pos(0, 3), Pos(0, 4));
+ is(!m1.find());
+ var m2 = cm.markText(Pos(0, 1), Pos(0, 2), {clearWhenEmpty: false,
+ inclusiveLeft: true,
+ inclusiveRight: true});
+ cm.replaceRange("q", Pos(0, 1), Pos(0, 2));
+ is(m2.find());
+ cm.replaceRange("", Pos(0, 0), Pos(0, 3));
+ is(!m2.find());
+ var m3 = cm.markText(Pos(0, 1), Pos(0, 1), {clearWhenEmpty: false});
+ cm.replaceRange("a", Pos(0, 3));
+ is(m3.find());
+ cm.replaceRange("b", Pos(0, 1));
+ is(!m3.find());
+}, {value: "abcde"});
+
+testCM("markTextStacked", function(cm) {
+ var m1 = cm.markText(Pos(0, 0), Pos(0, 0), {clearWhenEmpty: false});
+ var m2 = cm.markText(Pos(0, 0), Pos(0, 0), {clearWhenEmpty: false});
+ cm.replaceRange("B", Pos(0, 1));
+ is(m1.find() && m2.find());
+}, {value: "A"});
+
+testCM("undoPreservesNewMarks", function(cm) {
+ cm.markText(Pos(0, 3), Pos(0, 4));
+ cm.markText(Pos(1, 1), Pos(1, 3));
+ cm.replaceRange("", Pos(0, 3), Pos(3, 1));
+ var mBefore = cm.markText(Pos(0, 0), Pos(0, 1));
+ var mAfter = cm.markText(Pos(0, 5), Pos(0, 6));
+ var mAround = cm.markText(Pos(0, 2), Pos(0, 4));
+ cm.undo();
+ eqPos(mBefore.find().from, Pos(0, 0));
+ eqPos(mBefore.find().to, Pos(0, 1));
+ eqPos(mAfter.find().from, Pos(3, 3));
+ eqPos(mAfter.find().to, Pos(3, 4));
+ eqPos(mAround.find().from, Pos(0, 2));
+ eqPos(mAround.find().to, Pos(3, 2));
+ var found = cm.findMarksAt(Pos(2, 2));
+ eq(found.length, 1);
+ eq(found[0], mAround);
+}, {value: "aaaa\nbbbb\ncccc\ndddd"});
+
+testCM("markClearBetween", function(cm) {
+ cm.setValue("aaa\nbbb\nccc\nddd\n");
+ cm.markText(Pos(0, 0), Pos(2));
+ cm.replaceRange("aaa\nbbb\nccc", Pos(0, 0), Pos(2));
+ eq(cm.findMarksAt(Pos(1, 1)).length, 0);
+});
+
+testCM("findMarksMiddle", function(cm) {
+ var mark = cm.markText(Pos(1, 1), Pos(3, 1));
+ var found = cm.findMarks(Pos(2, 1), Pos(2, 2));
+ eq(found.length, 1);
+ eq(found[0], mark);
+}, {value: "line 0\nline 1\nline 2\nline 3"});
+
+testCM("deleteSpanCollapsedInclusiveLeft", function(cm) {
+ var from = Pos(1, 0), to = Pos(1, 1);
+ var m = cm.markText(from, to, {collapsed: true, inclusiveLeft: true});
+ // Delete collapsed span.
+ cm.replaceRange("", from, to);
+}, {value: "abc\nX\ndef"});
+
+testCM("markTextCSS", function(cm) {
+ function present() {
+ var spans = cm.display.lineDiv.getElementsByTagName("span");
+ for (var i = 0; i < spans.length; i++)
+ if (spans[i].style.color == "cyan" && span[i].textContent == "cdefg") return true;
+ }
+ var m = cm.markText(Pos(0, 2), Pos(0, 6), {css: "color: cyan"});
+ m.clear();
+ is(!present());
+}, {value: "abcdefgh"});
+
+testCM("bookmark", function(cm) {
+ function p(v) { return v && Pos(v[0], v[1]); }
+ forEach([{a: [1, 0], b: [1, 1], c: "", d: [1, 4]},
+ {a: [1, 1], b: [1, 1], c: "xx", d: [1, 7]},
+ {a: [1, 4], b: [1, 5], c: "ab", d: [1, 6]},
+ {a: [1, 4], b: [1, 6], c: "", d: null},
+ {a: [1, 5], b: [1, 6], c: "abc", d: [1, 5]},
+ {a: [1, 6], b: [1, 8], c: "", d: [1, 5]},
+ {a: [1, 4], b: [1, 4], c: "\n\n", d: [3, 1]},
+ {bm: [1, 9], a: [1, 1], b: [1, 1], c: "\n", d: [2, 8]}], function(test) {
+ cm.setValue("1234567890\n1234567890\n1234567890");
+ var b = cm.setBookmark(p(test.bm) || Pos(1, 5));
+ cm.replaceRange(test.c, p(test.a), p(test.b));
+ eqPos(b.find(), p(test.d));
+ });
+});
+
+testCM("bookmarkInsertLeft", function(cm) {
+ var br = cm.setBookmark(Pos(0, 2), {insertLeft: false});
+ var bl = cm.setBookmark(Pos(0, 2), {insertLeft: true});
+ cm.setCursor(Pos(0, 2));
+ cm.replaceSelection("hi");
+ eqPos(br.find(), Pos(0, 2));
+ eqPos(bl.find(), Pos(0, 4));
+ cm.replaceRange("", Pos(0, 4), Pos(0, 5));
+ cm.replaceRange("", Pos(0, 2), Pos(0, 4));
+ cm.replaceRange("", Pos(0, 1), Pos(0, 2));
+ // Verify that deleting next to bookmarks doesn't kill them
+ eqPos(br.find(), Pos(0, 1));
+ eqPos(bl.find(), Pos(0, 1));
+}, {value: "abcdef"});
+
+testCM("bookmarkCursor", function(cm) {
+ var pos01 = cm.cursorCoords(Pos(0, 1)), pos11 = cm.cursorCoords(Pos(1, 1)),
+ pos20 = cm.cursorCoords(Pos(2, 0)), pos30 = cm.cursorCoords(Pos(3, 0)),
+ pos41 = cm.cursorCoords(Pos(4, 1));
+ cm.setBookmark(Pos(0, 1), {widget: document.createTextNode("←"), insertLeft: true});
+ cm.setBookmark(Pos(2, 0), {widget: document.createTextNode("←"), insertLeft: true});
+ cm.setBookmark(Pos(1, 1), {widget: document.createTextNode("→")});
+ cm.setBookmark(Pos(3, 0), {widget: document.createTextNode("→")});
+ var new01 = cm.cursorCoords(Pos(0, 1)), new11 = cm.cursorCoords(Pos(1, 1)),
+ new20 = cm.cursorCoords(Pos(2, 0)), new30 = cm.cursorCoords(Pos(3, 0));
+ near(new01.left, pos01.left, 1);
+ near(new01.top, pos01.top, 1);
+ is(new11.left > pos11.left, "at right, middle of line");
+ near(new11.top == pos11.top, 1);
+ near(new20.left, pos20.left, 1);
+ near(new20.top, pos20.top, 1);
+ is(new30.left > pos30.left, "at right, empty line");
+ near(new30.top, pos30, 1);
+ cm.setBookmark(Pos(4, 0), {widget: document.createTextNode("→")});
+ is(cm.cursorCoords(Pos(4, 1)).left > pos41.left, "single-char bug");
+}, {value: "foo\nbar\n\n\nx\ny"});
+
+testCM("multiBookmarkCursor", function(cm) {
+ if (phantom) return;
+ var ms = [], m;
+ function add(insertLeft) {
+ for (var i = 0; i < 3; ++i) {
+ var node = document.createElement("span");
+ node.innerHTML = "X";
+ ms.push(cm.setBookmark(Pos(0, 1), {widget: node, insertLeft: insertLeft}));
+ }
+ }
+ var base1 = cm.cursorCoords(Pos(0, 1)).left, base4 = cm.cursorCoords(Pos(0, 4)).left;
+ add(true);
+ near(base1, cm.cursorCoords(Pos(0, 1)).left, 1);
+ while (m = ms.pop()) m.clear();
+ add(false);
+ near(base4, cm.cursorCoords(Pos(0, 1)).left, 1);
+}, {value: "abcdefg"});
+
+testCM("getAllMarks", function(cm) {
+ addDoc(cm, 10, 10);
+ var m1 = cm.setBookmark(Pos(0, 2));
+ var m2 = cm.markText(Pos(0, 2), Pos(3, 2));
+ var m3 = cm.markText(Pos(1, 2), Pos(1, 8));
+ var m4 = cm.markText(Pos(8, 0), Pos(9, 0));
+ eq(cm.getAllMarks().length, 4);
+ m1.clear();
+ m3.clear();
+ eq(cm.getAllMarks().length, 2);
+});
+
+testCM("setValueClears", function(cm) {
+ cm.addLineClass(0, "wrap", "foo");
+ var mark = cm.markText(Pos(0, 0), Pos(1, 1), {inclusiveLeft: true, inclusiveRight: true});
+ cm.setValue("foo");
+ is(!cm.lineInfo(0).wrapClass);
+ is(!mark.find());
+}, {value: "a\nb"});
+
+testCM("bug577", function(cm) {
+ cm.setValue("a\nb");
+ cm.clearHistory();
+ cm.setValue("fooooo");
+ cm.undo();
+});
+
+testCM("scrollSnap", function(cm) {
+ cm.setSize(100, 100);
+ addDoc(cm, 200, 200);
+ cm.setCursor(Pos(100, 180));
+ var info = cm.getScrollInfo();
+ is(info.left > 0 && info.top > 0);
+ cm.setCursor(Pos(0, 0));
+ info = cm.getScrollInfo();
+ is(info.left == 0 && info.top == 0, "scrolled clean to top");
+ cm.setCursor(Pos(100, 180));
+ cm.setCursor(Pos(199, 0));
+ info = cm.getScrollInfo();
+ is(info.left == 0 && info.top + 2 > info.height - cm.getScrollerElement().clientHeight, "scrolled clean to bottom");
+});
+
+testCM("scrollIntoView", function(cm) {
+ if (phantom) return;
+ var outer = cm.getWrapperElement().getBoundingClientRect();
+ function test(line, ch, msg) {
+ var pos = Pos(line, ch);
+ cm.scrollIntoView(pos);
+ var box = cm.charCoords(pos, "window");
+ is(box.left >= outer.left, msg + " (left)");
+ is(box.right <= outer.right, msg + " (right)");
+ is(box.top >= outer.top, msg + " (top)");
+ is(box.bottom <= outer.bottom, msg + " (bottom)");
+ }
+ addDoc(cm, 200, 200);
+ test(199, 199, "bottom right");
+ test(0, 0, "top left");
+ test(100, 100, "center");
+ test(199, 0, "bottom left");
+ test(0, 199, "top right");
+ test(100, 100, "center again");
+});
+
+testCM("scrollBackAndForth", function(cm) {
+ addDoc(cm, 1, 200);
+ cm.operation(function() {
+ cm.scrollIntoView(Pos(199, 0));
+ cm.scrollIntoView(Pos(4, 0));
+ });
+ is(cm.getScrollInfo().top > 0);
+});
+
+testCM("selectAllNoScroll", function(cm) {
+ addDoc(cm, 1, 200);
+ cm.execCommand("selectAll");
+ eq(cm.getScrollInfo().top, 0);
+ cm.setCursor(199);
+ cm.execCommand("selectAll");
+ is(cm.getScrollInfo().top > 0);
+});
+
+testCM("selectionPos", function(cm) {
+ if (phantom || cm.getOption("inputStyle") != "textarea") return;
+ cm.setSize(100, 100);
+ addDoc(cm, 200, 100);
+ cm.setSelection(Pos(1, 100), Pos(98, 100));
+ var lineWidth = cm.charCoords(Pos(0, 200), "local").left;
+ var lineHeight = (cm.charCoords(Pos(99)).top - cm.charCoords(Pos(0)).top) / 100;
+ cm.scrollTo(0, 0);
+ var selElt = byClassName(cm.getWrapperElement(), "CodeMirror-selected");
+ var outer = cm.getWrapperElement().getBoundingClientRect();
+ var sawMiddle, sawTop, sawBottom;
+ for (var i = 0, e = selElt.length; i < e; ++i) {
+ var box = selElt[i].getBoundingClientRect();
+ var atLeft = box.left - outer.left < 30;
+ var width = box.right - box.left;
+ var atRight = box.right - outer.left > .8 * lineWidth;
+ if (atLeft && atRight) {
+ sawMiddle = true;
+ is(box.bottom - box.top > 90 * lineHeight, "middle high");
+ is(width > .9 * lineWidth, "middle wide");
+ } else {
+ is(width > .4 * lineWidth, "top/bot wide enough");
+ is(width < .6 * lineWidth, "top/bot slim enough");
+ if (atLeft) {
+ sawBottom = true;
+ is(box.top - outer.top > 96 * lineHeight, "bot below");
+ } else if (atRight) {
+ sawTop = true;
+ is(box.top - outer.top < 2.1 * lineHeight, "top above");
+ }
+ }
+ }
+ is(sawTop && sawBottom && sawMiddle, "all parts");
+}, null);
+
+testCM("restoreHistory", function(cm) {
+ cm.setValue("abc\ndef");
+ cm.replaceRange("hello", Pos(1, 0), Pos(1));
+ cm.replaceRange("goop", Pos(0, 0), Pos(0));
+ cm.undo();
+ var storedVal = cm.getValue(), storedHist = cm.getHistory();
+ if (window.JSON) storedHist = JSON.parse(JSON.stringify(storedHist));
+ eq(storedVal, "abc\nhello");
+ cm.setValue("");
+ cm.clearHistory();
+ eq(cm.historySize().undo, 0);
+ cm.setValue(storedVal);
+ cm.setHistory(storedHist);
+ cm.redo();
+ eq(cm.getValue(), "goop\nhello");
+ cm.undo(); cm.undo();
+ eq(cm.getValue(), "abc\ndef");
+});
+
+testCM("doubleScrollbar", function(cm) {
+ var dummy = document.body.appendChild(document.createElement("p"));
+ dummy.style.cssText = "height: 50px; overflow: scroll; width: 50px";
+ var scrollbarWidth = dummy.offsetWidth + 1 - dummy.clientWidth;
+ document.body.removeChild(dummy);
+ if (scrollbarWidth < 2) return;
+ cm.setSize(null, 100);
+ addDoc(cm, 1, 300);
+ var wrap = cm.getWrapperElement();
+ is(wrap.offsetWidth - byClassName(wrap, "CodeMirror-lines")[0].offsetWidth <= scrollbarWidth * 1.5);
+});
+
+testCM("weirdLinebreaks", function(cm) {
+ cm.setValue("foo\nbar\rbaz\r\nquux\n\rplop");
+ is(cm.getValue(), "foo\nbar\nbaz\nquux\n\nplop");
+ is(cm.lineCount(), 6);
+ cm.setValue("\n\n");
+ is(cm.lineCount(), 3);
+});
+
+testCM("setSize", function(cm) {
+ cm.setSize(100, 100);
+ var wrap = cm.getWrapperElement();
+ is(wrap.offsetWidth, 100);
+ is(wrap.offsetHeight, 100);
+ cm.setSize("100%", "3em");
+ is(wrap.style.width, "100%");
+ is(wrap.style.height, "3em");
+ cm.setSize(null, 40);
+ is(wrap.style.width, "100%");
+ is(wrap.style.height, "40px");
+});
+
+function foldLines(cm, start, end, autoClear) {
+ return cm.markText(Pos(start, 0), Pos(end - 1), {
+ inclusiveLeft: true,
+ inclusiveRight: true,
+ collapsed: true,
+ clearOnEnter: autoClear
+ });
+}
+
+testCM("collapsedLines", function(cm) {
+ addDoc(cm, 4, 10);
+ var range = foldLines(cm, 4, 5), cleared = 0;
+ CodeMirror.on(range, "clear", function() {cleared++;});
+ cm.setCursor(Pos(3, 0));
+ CodeMirror.commands.goLineDown(cm);
+ eqPos(cm.getCursor(), Pos(5, 0));
+ cm.replaceRange("abcdefg", Pos(3, 0), Pos(3));
+ cm.setCursor(Pos(3, 6));
+ CodeMirror.commands.goLineDown(cm);
+ eqPos(cm.getCursor(), Pos(5, 4));
+ cm.replaceRange("ab", Pos(3, 0), Pos(3));
+ cm.setCursor(Pos(3, 2));
+ CodeMirror.commands.goLineDown(cm);
+ eqPos(cm.getCursor(), Pos(5, 2));
+ cm.operation(function() {range.clear(); range.clear();});
+ eq(cleared, 1);
+});
+
+testCM("collapsedRangeCoordsChar", function(cm) {
+ var pos_1_3 = cm.charCoords(Pos(1, 3));
+ pos_1_3.left += 2; pos_1_3.top += 2;
+ var opts = {collapsed: true, inclusiveLeft: true, inclusiveRight: true};
+ var m1 = cm.markText(Pos(0, 0), Pos(2, 0), opts);
+ eqPos(cm.coordsChar(pos_1_3), Pos(3, 3));
+ m1.clear();
+ var m1 = cm.markText(Pos(0, 0), Pos(1, 1), {collapsed: true, inclusiveLeft: true});
+ var m2 = cm.markText(Pos(1, 1), Pos(2, 0), {collapsed: true, inclusiveRight: true});
+ eqPos(cm.coordsChar(pos_1_3), Pos(3, 3));
+ m1.clear(); m2.clear();
+ var m1 = cm.markText(Pos(0, 0), Pos(1, 6), opts);
+ eqPos(cm.coordsChar(pos_1_3), Pos(3, 3));
+}, {value: "123456\nabcdef\nghijkl\nmnopqr\n"});
+
+testCM("collapsedRangeBetweenLinesSelected", function(cm) {
+ if (cm.getOption("inputStyle") != "textarea") return;
+ var widget = document.createElement("span");
+ widget.textContent = "\u2194";
+ cm.markText(Pos(0, 3), Pos(1, 0), {replacedWith: widget});
+ cm.setSelection(Pos(0, 3), Pos(1, 0));
+ var selElts = byClassName(cm.getWrapperElement(), "CodeMirror-selected");
+ for (var i = 0, w = 0; i < selElts.length; i++)
+ w += selElts[i].offsetWidth;
+ is(w > 0);
+}, {value: "one\ntwo"});
+
+testCM("randomCollapsedRanges", function(cm) {
+ addDoc(cm, 20, 500);
+ cm.operation(function() {
+ for (var i = 0; i < 200; i++) {
+ var start = Pos(Math.floor(Math.random() * 500), Math.floor(Math.random() * 20));
+ if (i % 4)
+ try { cm.markText(start, Pos(start.line + 2, 1), {collapsed: true}); }
+ catch(e) { if (!/overlapping/.test(String(e))) throw e; }
+ else
+ cm.markText(start, Pos(start.line, start.ch + 4), {"className": "foo"});
+ }
+ });
+});
+
+testCM("hiddenLinesAutoUnfold", function(cm) {
+ var range = foldLines(cm, 1, 3, true), cleared = 0;
+ CodeMirror.on(range, "clear", function() {cleared++;});
+ cm.setCursor(Pos(3, 0));
+ eq(cleared, 0);
+ cm.execCommand("goCharLeft");
+ eq(cleared, 1);
+ range = foldLines(cm, 1, 3, true);
+ CodeMirror.on(range, "clear", function() {cleared++;});
+ eqPos(cm.getCursor(), Pos(3, 0));
+ cm.setCursor(Pos(0, 3));
+ cm.execCommand("goCharRight");
+ eq(cleared, 2);
+}, {value: "abc\ndef\nghi\njkl"});
+
+testCM("hiddenLinesSelectAll", function(cm) { // Issue #484
+ addDoc(cm, 4, 20);
+ foldLines(cm, 0, 10);
+ foldLines(cm, 11, 20);
+ CodeMirror.commands.selectAll(cm);
+ eqPos(cm.getCursor(true), Pos(10, 0));
+ eqPos(cm.getCursor(false), Pos(10, 4));
+});
+
+
+testCM("everythingFolded", function(cm) {
+ addDoc(cm, 2, 2);
+ function enterPress() {
+ cm.triggerOnKeyDown({type: "keydown", keyCode: 13, preventDefault: function(){}, stopPropagation: function(){}});
+ }
+ var fold = foldLines(cm, 0, 2);
+ enterPress();
+ eq(cm.getValue(), "xx\nxx");
+ fold.clear();
+ fold = foldLines(cm, 0, 2, true);
+ eq(fold.find(), null);
+ enterPress();
+ eq(cm.getValue(), "\nxx\nxx");
+});
+
+testCM("structuredFold", function(cm) {
+ if (phantom) return;
+ addDoc(cm, 4, 8);
+ var range = cm.markText(Pos(1, 2), Pos(6, 2), {
+ replacedWith: document.createTextNode("Q")
+ });
+ cm.setCursor(0, 3);
+ CodeMirror.commands.goLineDown(cm);
+ eqPos(cm.getCursor(), Pos(6, 2));
+ CodeMirror.commands.goCharLeft(cm);
+ eqPos(cm.getCursor(), Pos(1, 2));
+ CodeMirror.commands.delCharAfter(cm);
+ eq(cm.getValue(), "xxxx\nxxxx\nxxxx");
+ addDoc(cm, 4, 8);
+ range = cm.markText(Pos(1, 2), Pos(6, 2), {
+ replacedWith: document.createTextNode("M"),
+ clearOnEnter: true
+ });
+ var cleared = 0;
+ CodeMirror.on(range, "clear", function(){++cleared;});
+ cm.setCursor(0, 3);
+ CodeMirror.commands.goLineDown(cm);
+ eqPos(cm.getCursor(), Pos(6, 2));
+ CodeMirror.commands.goCharLeft(cm);
+ eqPos(cm.getCursor(), Pos(6, 1));
+ eq(cleared, 1);
+ range.clear();
+ eq(cleared, 1);
+ range = cm.markText(Pos(1, 2), Pos(6, 2), {
+ replacedWith: document.createTextNode("Q"),
+ clearOnEnter: true
+ });
+ range.clear();
+ cm.setCursor(1, 2);
+ CodeMirror.commands.goCharRight(cm);
+ eqPos(cm.getCursor(), Pos(1, 3));
+ range = cm.markText(Pos(2, 0), Pos(4, 4), {
+ replacedWith: document.createTextNode("M")
+ });
+ cm.setCursor(1, 0);
+ CodeMirror.commands.goLineDown(cm);
+ eqPos(cm.getCursor(), Pos(2, 0));
+}, null);
+
+testCM("nestedFold", function(cm) {
+ addDoc(cm, 10, 3);
+ function fold(ll, cl, lr, cr) {
+ return cm.markText(Pos(ll, cl), Pos(lr, cr), {collapsed: true});
+ }
+ var inner1 = fold(0, 6, 1, 3), inner2 = fold(0, 2, 1, 8), outer = fold(0, 1, 2, 3), inner0 = fold(0, 5, 0, 6);
+ cm.setCursor(0, 1);
+ CodeMirror.commands.goCharRight(cm);
+ eqPos(cm.getCursor(), Pos(2, 3));
+ inner0.clear();
+ CodeMirror.commands.goCharLeft(cm);
+ eqPos(cm.getCursor(), Pos(0, 1));
+ outer.clear();
+ CodeMirror.commands.goCharRight(cm);
+ eqPos(cm.getCursor(), Pos(0, 2));
+ CodeMirror.commands.goCharRight(cm);
+ eqPos(cm.getCursor(), Pos(1, 8));
+ inner2.clear();
+ CodeMirror.commands.goCharLeft(cm);
+ eqPos(cm.getCursor(), Pos(1, 7));
+ cm.setCursor(0, 5);
+ CodeMirror.commands.goCharRight(cm);
+ eqPos(cm.getCursor(), Pos(0, 6));
+ CodeMirror.commands.goCharRight(cm);
+ eqPos(cm.getCursor(), Pos(1, 3));
+});
+
+testCM("badNestedFold", function(cm) {
+ addDoc(cm, 4, 4);
+ cm.markText(Pos(0, 2), Pos(3, 2), {collapsed: true});
+ var caught;
+ try {cm.markText(Pos(0, 1), Pos(0, 3), {collapsed: true});}
+ catch(e) {caught = e;}
+ is(caught instanceof Error, "no error");
+ is(/overlap/i.test(caught.message), "wrong error");
+});
+
+testCM("nestedFoldOnSide", function(cm) {
+ var m1 = cm.markText(Pos(0, 1), Pos(2, 1), {collapsed: true, inclusiveRight: true});
+ var m2 = cm.markText(Pos(0, 1), Pos(0, 2), {collapsed: true});
+ cm.markText(Pos(0, 1), Pos(0, 2), {collapsed: true}).clear();
+ try { cm.markText(Pos(0, 1), Pos(0, 2), {collapsed: true, inclusiveLeft: true}); }
+ catch(e) { var caught = e; }
+ is(caught && /overlap/i.test(caught.message));
+ var m3 = cm.markText(Pos(2, 0), Pos(2, 1), {collapsed: true});
+ var m4 = cm.markText(Pos(2, 0), Pos(2, 1), {collapse: true, inclusiveRight: true});
+ m1.clear(); m4.clear();
+ m1 = cm.markText(Pos(0, 1), Pos(2, 1), {collapsed: true});
+ cm.markText(Pos(2, 0), Pos(2, 1), {collapsed: true}).clear();
+ try { cm.markText(Pos(2, 0), Pos(2, 1), {collapsed: true, inclusiveRight: true}); }
+ catch(e) { var caught = e; }
+ is(caught && /overlap/i.test(caught.message));
+}, {value: "ab\ncd\ef"});
+
+testCM("editInFold", function(cm) {
+ addDoc(cm, 4, 6);
+ var m = cm.markText(Pos(1, 2), Pos(3, 2), {collapsed: true});
+ cm.replaceRange("", Pos(0, 0), Pos(1, 3));
+ cm.replaceRange("", Pos(2, 1), Pos(3, 3));
+ cm.replaceRange("a\nb\nc\nd", Pos(0, 1), Pos(1, 0));
+ cm.cursorCoords(Pos(0, 0));
+});
+
+testCM("wrappingInlineWidget", function(cm) {
+ cm.setSize("11em");
+ var w = document.createElement("span");
+ w.style.color = "red";
+ w.innerHTML = "one two three four";
+ cm.markText(Pos(0, 6), Pos(0, 9), {replacedWith: w});
+ var cur0 = cm.cursorCoords(Pos(0, 0)), cur1 = cm.cursorCoords(Pos(0, 10));
+ is(cur0.top < cur1.top);
+ is(cur0.bottom < cur1.bottom);
+ var curL = cm.cursorCoords(Pos(0, 6)), curR = cm.cursorCoords(Pos(0, 9));
+ eq(curL.top, cur0.top);
+ eq(curL.bottom, cur0.bottom);
+ eq(curR.top, cur1.top);
+ eq(curR.bottom, cur1.bottom);
+ cm.replaceRange("", Pos(0, 9), Pos(0));
+ curR = cm.cursorCoords(Pos(0, 9));
+ if (phantom) return;
+ eq(curR.top, cur1.top);
+ eq(curR.bottom, cur1.bottom);
+}, {value: "1 2 3 xxx 4", lineWrapping: true});
+
+testCM("showEmptyWidgetSpan", function(cm) {
+ var marker = cm.markText(Pos(0, 2), Pos(0, 2), {
+ clearWhenEmpty: false,
+ replacedWith: document.createTextNode("X")
+ });
+ eq(cm.display.view[0].text.textContent, "abXc");
+}, {value: "abc"});
+
+testCM("changedInlineWidget", function(cm) {
+ cm.setSize("10em");
+ var w = document.createElement("span");
+ w.innerHTML = "x";
+ var m = cm.markText(Pos(0, 4), Pos(0, 5), {replacedWith: w});
+ w.innerHTML = "and now the widget is really really long all of a sudden and a scrollbar is needed";
+ m.changed();
+ var hScroll = byClassName(cm.getWrapperElement(), "CodeMirror-hscrollbar")[0];
+ is(hScroll.scrollWidth > hScroll.clientWidth);
+}, {value: "hello there"});
+
+testCM("changedBookmark", function(cm) {
+ cm.setSize("10em");
+ var w = document.createElement("span");
+ w.innerHTML = "x";
+ var m = cm.setBookmark(Pos(0, 4), {widget: w});
+ w.innerHTML = "and now the widget is really really long all of a sudden and a scrollbar is needed";
+ m.changed();
+ var hScroll = byClassName(cm.getWrapperElement(), "CodeMirror-hscrollbar")[0];
+ is(hScroll.scrollWidth > hScroll.clientWidth);
+}, {value: "abcdefg"});
+
+testCM("inlineWidget", function(cm) {
+ var w = cm.setBookmark(Pos(0, 2), {widget: document.createTextNode("uu")});
+ cm.setCursor(0, 2);
+ CodeMirror.commands.goLineDown(cm);
+ eqPos(cm.getCursor(), Pos(1, 4));
+ cm.setCursor(0, 2);
+ cm.replaceSelection("hi");
+ eqPos(w.find(), Pos(0, 2));
+ cm.setCursor(0, 1);
+ cm.replaceSelection("ay");
+ eqPos(w.find(), Pos(0, 4));
+ eq(cm.getLine(0), "uayuhiuu");
+}, {value: "uuuu\nuuuuuu"});
+
+testCM("wrappingAndResizing", function(cm) {
+ cm.setSize(null, "auto");
+ cm.setOption("lineWrapping", true);
+ var wrap = cm.getWrapperElement(), h0 = wrap.offsetHeight;
+ var doc = "xxx xxx xxx xxx xxx";
+ cm.setValue(doc);
+ for (var step = 10, w = cm.charCoords(Pos(0, 18), "div").right;; w += step) {
+ cm.setSize(w);
+ if (wrap.offsetHeight <= h0 * (opera_lt10 ? 1.2 : 1.5)) {
+ if (step == 10) { w -= 10; step = 1; }
+ else break;
+ }
+ }
+ // Ensure that putting the cursor at the end of the maximally long
+ // line doesn't cause wrapping to happen.
+ cm.setCursor(Pos(0, doc.length));
+ eq(wrap.offsetHeight, h0);
+ cm.replaceSelection("x");
+ is(wrap.offsetHeight > h0, "wrapping happens");
+ // Now add a max-height and, in a document consisting of
+ // almost-wrapped lines, go over it so that a scrollbar appears.
+ cm.setValue(doc + "\n" + doc + "\n");
+ cm.getScrollerElement().style.maxHeight = "100px";
+ cm.replaceRange("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n!\n", Pos(2, 0));
+ forEach([Pos(0, doc.length), Pos(0, doc.length - 1),
+ Pos(0, 0), Pos(1, doc.length), Pos(1, doc.length - 1)],
+ function(pos) {
+ var coords = cm.charCoords(pos);
+ eqPos(pos, cm.coordsChar({left: coords.left + 2, top: coords.top + 5}));
+ });
+}, null, ie_lt8);
+
+testCM("measureEndOfLine", function(cm) {
+ cm.setSize(null, "auto");
+ var inner = byClassName(cm.getWrapperElement(), "CodeMirror-lines")[0].firstChild;
+ var lh = inner.offsetHeight;
+ for (var step = 10, w = cm.charCoords(Pos(0, 7), "div").right;; w += step) {
+ cm.setSize(w);
+ if (inner.offsetHeight < 2.5 * lh) {
+ if (step == 10) { w -= 10; step = 1; }
+ else break;
+ }
+ }
+ cm.setValue(cm.getValue() + "\n\n");
+ var endPos = cm.charCoords(Pos(0, 18), "local");
+ is(endPos.top > lh * .8, "not at top");
+ is(endPos.left > w - 20, "not at right");
+ endPos = cm.charCoords(Pos(0, 18));
+ eqPos(cm.coordsChar({left: endPos.left, top: endPos.top + 5}), Pos(0, 18));
+}, {mode: "text/html", value: "<!-- foo barrr -->", lineWrapping: true}, ie_lt8 || opera_lt10);
+
+testCM("scrollVerticallyAndHorizontally", function(cm) {
+ if (cm.getOption("inputStyle") != "textarea") return;
+ cm.setSize(100, 100);
+ addDoc(cm, 40, 40);
+ cm.setCursor(39);
+ var wrap = cm.getWrapperElement(), bar = byClassName(wrap, "CodeMirror-vscrollbar")[0];
+ is(bar.offsetHeight < wrap.offsetHeight, "vertical scrollbar limited by horizontal one");
+ var cursorBox = byClassName(wrap, "CodeMirror-cursor")[0].getBoundingClientRect();
+ var editorBox = wrap.getBoundingClientRect();
+ is(cursorBox.bottom < editorBox.top + cm.getScrollerElement().clientHeight,
+ "bottom line visible");
+}, {lineNumbers: true});
+
+testCM("moveVstuck", function(cm) {
+ var lines = byClassName(cm.getWrapperElement(), "CodeMirror-lines")[0].firstChild, h0 = lines.offsetHeight;
+ var val = "fooooooooooooooooooooooooo baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaar\n";
+ cm.setValue(val);
+ for (var w = cm.charCoords(Pos(0, 26), "div").right * 2.8;; w += 5) {
+ cm.setSize(w);
+ if (lines.offsetHeight <= 3.5 * h0) break;
+ }
+ cm.setCursor(Pos(0, val.length - 1));
+ cm.moveV(-1, "line");
+ eqPos(cm.getCursor(), Pos(0, 26));
+}, {lineWrapping: true}, ie_lt8 || opera_lt10);
+
+testCM("collapseOnMove", function(cm) {
+ cm.setSelection(Pos(0, 1), Pos(2, 4));
+ cm.execCommand("goLineUp");
+ is(!cm.somethingSelected());
+ eqPos(cm.getCursor(), Pos(0, 1));
+ cm.setSelection(Pos(0, 1), Pos(2, 4));
+ cm.execCommand("goPageDown");
+ is(!cm.somethingSelected());
+ eqPos(cm.getCursor(), Pos(2, 4));
+ cm.execCommand("goLineUp");
+ cm.execCommand("goLineUp");
+ eqPos(cm.getCursor(), Pos(0, 4));
+ cm.setSelection(Pos(0, 1), Pos(2, 4));
+ cm.execCommand("goCharLeft");
+ is(!cm.somethingSelected());
+ eqPos(cm.getCursor(), Pos(0, 1));
+}, {value: "aaaaa\nb\nccccc"});
+
+testCM("clickTab", function(cm) {
+ var p0 = cm.charCoords(Pos(0, 0));
+ eqPos(cm.coordsChar({left: p0.left + 5, top: p0.top + 5}), Pos(0, 0));
+ eqPos(cm.coordsChar({left: p0.right - 5, top: p0.top + 5}), Pos(0, 1));
+}, {value: "\t\n\n", lineWrapping: true, tabSize: 8});
+
+testCM("verticalScroll", function(cm) {
+ cm.setSize(100, 200);
+ cm.setValue("foo\nbar\nbaz\n");
+ var sc = cm.getScrollerElement(), baseWidth = sc.scrollWidth;
+ cm.replaceRange("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaah", Pos(0, 0), Pos(0));
+ is(sc.scrollWidth > baseWidth, "scrollbar present");
+ cm.replaceRange("foo", Pos(0, 0), Pos(0));
+ if (!phantom) eq(sc.scrollWidth, baseWidth, "scrollbar gone");
+ cm.replaceRange("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaah", Pos(0, 0), Pos(0));
+ cm.replaceRange("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbh", Pos(1, 0), Pos(1));
+ is(sc.scrollWidth > baseWidth, "present again");
+ var curWidth = sc.scrollWidth;
+ cm.replaceRange("foo", Pos(0, 0), Pos(0));
+ is(sc.scrollWidth < curWidth, "scrollbar smaller");
+ is(sc.scrollWidth > baseWidth, "but still present");
+});
+
+testCM("extraKeys", function(cm) {
+ var outcome;
+ function fakeKey(expected, code, props) {
+ if (typeof code == "string") code = code.charCodeAt(0);
+ var e = {type: "keydown", keyCode: code, preventDefault: function(){}, stopPropagation: function(){}};
+ if (props) for (var n in props) e[n] = props[n];
+ outcome = null;
+ cm.triggerOnKeyDown(e);
+ eq(outcome, expected);
+ }
+ CodeMirror.commands.testCommand = function() {outcome = "tc";};
+ CodeMirror.commands.goTestCommand = function() {outcome = "gtc";};
+ cm.setOption("extraKeys", {"Shift-X": function() {outcome = "sx";},
+ "X": function() {outcome = "x";},
+ "Ctrl-Alt-U": function() {outcome = "cau";},
+ "End": "testCommand",
+ "Home": "goTestCommand",
+ "Tab": false});
+ fakeKey(null, "U");
+ fakeKey("cau", "U", {ctrlKey: true, altKey: true});
+ fakeKey(null, "U", {shiftKey: true, ctrlKey: true, altKey: true});
+ fakeKey("x", "X");
+ fakeKey("sx", "X", {shiftKey: true});
+ fakeKey("tc", 35);
+ fakeKey(null, 35, {shiftKey: true});
+ fakeKey("gtc", 36);
+ fakeKey("gtc", 36, {shiftKey: true});
+ fakeKey(null, 9);
+}, null, window.opera && mac);
+
+testCM("wordMovementCommands", function(cm) {
+ cm.execCommand("goWordLeft");
+ eqPos(cm.getCursor(), Pos(0, 0));
+ cm.execCommand("goWordRight"); cm.execCommand("goWordRight");
+ eqPos(cm.getCursor(), Pos(0, 7));
+ cm.execCommand("goWordLeft");
+ eqPos(cm.getCursor(), Pos(0, 5));
+ cm.execCommand("goWordRight"); cm.execCommand("goWordRight");
+ eqPos(cm.getCursor(), Pos(0, 12));
+ cm.execCommand("goWordLeft");
+ eqPos(cm.getCursor(), Pos(0, 9));
+ cm.execCommand("goWordRight"); cm.execCommand("goWordRight"); cm.execCommand("goWordRight");
+ eqPos(cm.getCursor(), Pos(0, 24));
+ cm.execCommand("goWordRight"); cm.execCommand("goWordRight");
+ eqPos(cm.getCursor(), Pos(1, 9));
+ cm.execCommand("goWordRight");
+ eqPos(cm.getCursor(), Pos(1, 13));
+ cm.execCommand("goWordRight"); cm.execCommand("goWordRight");
+ eqPos(cm.getCursor(), Pos(2, 0));
+}, {value: "this is (the) firstline.\na foo12\u00e9\u00f8\u00d7bar\n"});
+
+testCM("groupMovementCommands", function(cm) {
+ cm.execCommand("goGroupLeft");
+ eqPos(cm.getCursor(), Pos(0, 0));
+ cm.execCommand("goGroupRight");
+ eqPos(cm.getCursor(), Pos(0, 4));
+ cm.execCommand("goGroupRight");
+ eqPos(cm.getCursor(), Pos(0, 7));
+ cm.execCommand("goGroupRight");
+ eqPos(cm.getCursor(), Pos(0, 10));
+ cm.execCommand("goGroupLeft");
+ eqPos(cm.getCursor(), Pos(0, 7));
+ cm.execCommand("goGroupRight"); cm.execCommand("goGroupRight"); cm.execCommand("goGroupRight");
+ eqPos(cm.getCursor(), Pos(0, 15));
+ cm.setCursor(Pos(0, 17));
+ cm.execCommand("goGroupLeft");
+ eqPos(cm.getCursor(), Pos(0, 16));
+ cm.execCommand("goGroupLeft");
+ eqPos(cm.getCursor(), Pos(0, 14));
+ cm.execCommand("goGroupRight"); cm.execCommand("goGroupRight");
+ eqPos(cm.getCursor(), Pos(0, 20));
+ cm.execCommand("goGroupRight");
+ eqPos(cm.getCursor(), Pos(1, 0));
+ cm.execCommand("goGroupRight");
+ eqPos(cm.getCursor(), Pos(1, 2));
+ cm.execCommand("goGroupRight");
+ eqPos(cm.getCursor(), Pos(1, 5));
+ cm.execCommand("goGroupLeft"); cm.execCommand("goGroupLeft");
+ eqPos(cm.getCursor(), Pos(1, 0));
+ cm.execCommand("goGroupLeft");
+ eqPos(cm.getCursor(), Pos(0, 20));
+ cm.execCommand("goGroupLeft");
+ eqPos(cm.getCursor(), Pos(0, 16));
+}, {value: "booo ba---quux. ffff\n abc d"});
+
+testCM("groupsAndWhitespace", function(cm) {
+ var positions = [Pos(0, 0), Pos(0, 2), Pos(0, 5), Pos(0, 9), Pos(0, 11),
+ Pos(1, 0), Pos(1, 2), Pos(1, 5)];
+ for (var i = 1; i < positions.length; i++) {
+ cm.execCommand("goGroupRight");
+ eqPos(cm.getCursor(), positions[i]);
+ }
+ for (var i = positions.length - 2; i >= 0; i--) {
+ cm.execCommand("goGroupLeft");
+ eqPos(cm.getCursor(), i == 2 ? Pos(0, 6) : positions[i]);
+ }
+}, {value: " foo +++ \n bar"});
+
+testCM("charMovementCommands", function(cm) {
+ cm.execCommand("goCharLeft"); cm.execCommand("goColumnLeft");
+ eqPos(cm.getCursor(), Pos(0, 0));
+ cm.execCommand("goCharRight"); cm.execCommand("goCharRight");
+ eqPos(cm.getCursor(), Pos(0, 2));
+ cm.setCursor(Pos(1, 0));
+ cm.execCommand("goColumnLeft");
+ eqPos(cm.getCursor(), Pos(1, 0));
+ cm.execCommand("goCharLeft");
+ eqPos(cm.getCursor(), Pos(0, 5));
+ cm.execCommand("goColumnRight");
+ eqPos(cm.getCursor(), Pos(0, 5));
+ cm.execCommand("goCharRight");
+ eqPos(cm.getCursor(), Pos(1, 0));
+ cm.execCommand("goLineEnd");
+ eqPos(cm.getCursor(), Pos(1, 5));
+ cm.execCommand("goLineStartSmart");
+ eqPos(cm.getCursor(), Pos(1, 1));
+ cm.execCommand("goLineStartSmart");
+ eqPos(cm.getCursor(), Pos(1, 0));
+ cm.setCursor(Pos(2, 0));
+ cm.execCommand("goCharRight"); cm.execCommand("goColumnRight");
+ eqPos(cm.getCursor(), Pos(2, 0));
+}, {value: "line1\n ine2\n"});
+
+testCM("verticalMovementCommands", function(cm) {
+ cm.execCommand("goLineUp");
+ eqPos(cm.getCursor(), Pos(0, 0));
+ cm.execCommand("goLineDown");
+ if (!phantom) // This fails in PhantomJS, though not in a real Webkit
+ eqPos(cm.getCursor(), Pos(1, 0));
+ cm.setCursor(Pos(1, 12));
+ cm.execCommand("goLineDown");
+ eqPos(cm.getCursor(), Pos(2, 5));
+ cm.execCommand("goLineDown");
+ eqPos(cm.getCursor(), Pos(3, 0));
+ cm.execCommand("goLineUp");
+ eqPos(cm.getCursor(), Pos(2, 5));
+ cm.execCommand("goLineUp");
+ eqPos(cm.getCursor(), Pos(1, 12));
+ cm.execCommand("goPageDown");
+ eqPos(cm.getCursor(), Pos(5, 0));
+ cm.execCommand("goPageDown"); cm.execCommand("goLineDown");
+ eqPos(cm.getCursor(), Pos(5, 0));
+ cm.execCommand("goPageUp");
+ eqPos(cm.getCursor(), Pos(0, 0));
+}, {value: "line1\nlong long line2\nline3\n\nline5\n"});
+
+testCM("verticalMovementCommandsWrapping", function(cm) {
+ cm.setSize(120);
+ cm.setCursor(Pos(0, 5));
+ cm.execCommand("goLineDown");
+ eq(cm.getCursor().line, 0);
+ is(cm.getCursor().ch > 5, "moved beyond wrap");
+ for (var i = 0; ; ++i) {
+ is(i < 20, "no endless loop");
+ cm.execCommand("goLineDown");
+ var cur = cm.getCursor();
+ if (cur.line == 1) eq(cur.ch, 5);
+ if (cur.line == 2) { eq(cur.ch, 1); break; }
+ }
+}, {value: "a very long line that wraps around somehow so that we can test cursor movement\nshortone\nk",
+ lineWrapping: true});
+
+testCM("rtlMovement", function(cm) {
+ if (cm.getOption("inputStyle") != "textarea") return;
+ forEach(["خحج", "خحabcخحج", "abخحخحجcd", "abخde", "abخح2342خ1حج", "خ1ح2خح3حxج",
+ "خحcd", "1خحcd", "abcdeح1ج", "خمرحبها مها!", "foobarر", "خ ة ق",
+ "<img src=\"/בדיקה3.jpg\">", "يتم السحب في 05 فبراير 2014"], function(line) {
+ var inv = line.charCodeAt(0) > 128;
+ cm.setValue(line + "\n"); cm.execCommand(inv ? "goLineEnd" : "goLineStart");
+ var cursors = byClassName(cm.getWrapperElement(), "CodeMirror-cursors")[0];
+ var cursor = cursors.firstChild;
+ var prevX = cursor.offsetLeft, prevY = cursor.offsetTop;
+ for (var i = 0; i <= line.length; ++i) {
+ cm.execCommand("goCharRight");
+ cursor = cursors.firstChild;
+ if (i == line.length) is(cursor.offsetTop > prevY, "next line");
+ else is(cursor.offsetLeft > prevX, "moved right");
+ prevX = cursor.offsetLeft; prevY = cursor.offsetTop;
+ }
+ cm.setCursor(0, 0); cm.execCommand(inv ? "goLineStart" : "goLineEnd");
+ prevX = cursors.firstChild.offsetLeft;
+ for (var i = 0; i < line.length; ++i) {
+ cm.execCommand("goCharLeft");
+ cursor = cursors.firstChild;
+ is(cursor.offsetLeft < prevX, "moved left");
+ prevX = cursor.offsetLeft;
+ }
+ });
+}, null, ie_lt9);
+
+// Verify that updating a line clears its bidi ordering
+testCM("bidiUpdate", function(cm) {
+ cm.setCursor(Pos(0, 2));
+ cm.replaceSelection("خحج", "start");
+ cm.execCommand("goCharRight");
+ eqPos(cm.getCursor(), Pos(0, 4));
+}, {value: "abcd\n"});
+
+testCM("movebyTextUnit", function(cm) {
+ cm.setValue("בְּרֵאשִ\nééé́\n");
+ cm.execCommand("goLineEnd");
+ for (var i = 0; i < 4; ++i) cm.execCommand("goCharRight");
+ eqPos(cm.getCursor(), Pos(0, 0));
+ cm.execCommand("goCharRight");
+ eqPos(cm.getCursor(), Pos(1, 0));
+ cm.execCommand("goCharRight");
+ cm.execCommand("goCharRight");
+ eqPos(cm.getCursor(), Pos(1, 4));
+ cm.execCommand("goCharRight");
+ eqPos(cm.getCursor(), Pos(1, 7));
+});
+
+testCM("lineChangeEvents", function(cm) {
+ addDoc(cm, 3, 5);
+ var log = [], want = ["ch 0", "ch 1", "del 2", "ch 0", "ch 0", "del 1", "del 3", "del 4"];
+ for (var i = 0; i < 5; ++i) {
+ CodeMirror.on(cm.getLineHandle(i), "delete", function(i) {
+ return function() {log.push("del " + i);};
+ }(i));
+ CodeMirror.on(cm.getLineHandle(i), "change", function(i) {
+ return function() {log.push("ch " + i);};
+ }(i));
+ }
+ cm.replaceRange("x", Pos(0, 1));
+ cm.replaceRange("xy", Pos(1, 1), Pos(2));
+ cm.replaceRange("foo\nbar", Pos(0, 1));
+ cm.replaceRange("", Pos(0, 0), Pos(cm.lineCount()));
+ eq(log.length, want.length, "same length");
+ for (var i = 0; i < log.length; ++i)
+ eq(log[i], want[i]);
+});
+
+testCM("scrollEntirelyToRight", function(cm) {
+ if (phantom || cm.getOption("inputStyle") != "textarea") return;
+ addDoc(cm, 500, 2);
+ cm.setCursor(Pos(0, 500));
+ var wrap = cm.getWrapperElement(), cur = byClassName(wrap, "CodeMirror-cursor")[0];
+ is(wrap.getBoundingClientRect().right > cur.getBoundingClientRect().left);
+});
+
+testCM("lineWidgets", function(cm) {
+ addDoc(cm, 500, 3);
+ var last = cm.charCoords(Pos(2, 0));
+ var node = document.createElement("div");
+ node.innerHTML = "hi";
+ var widget = cm.addLineWidget(1, node);
+ is(last.top < cm.charCoords(Pos(2, 0)).top, "took up space");
+ cm.setCursor(Pos(1, 1));
+ cm.execCommand("goLineDown");
+ eqPos(cm.getCursor(), Pos(2, 1));
+ cm.execCommand("goLineUp");
+ eqPos(cm.getCursor(), Pos(1, 1));
+});
+
+testCM("lineWidgetFocus", function(cm) {
+ var place = document.getElementById("testground");
+ place.className = "offscreen";
+ try {
+ addDoc(cm, 500, 10);
+ var node = document.createElement("input");
+ var widget = cm.addLineWidget(1, node);
+ node.focus();
+ eq(document.activeElement, node);
+ cm.replaceRange("new stuff", Pos(1, 0));
+ eq(document.activeElement, node);
+ } finally {
+ place.className = "";
+ }
+});
+
+testCM("lineWidgetCautiousRedraw", function(cm) {
+ var node = document.createElement("div");
+ node.innerHTML = "hahah";
+ var w = cm.addLineWidget(0, node);
+ var redrawn = false;
+ w.on("redraw", function() { redrawn = true; });
+ cm.replaceSelection("0");
+ is(!redrawn);
+}, {value: "123\n456"});
+
+
+var knownScrollbarWidth;
+function scrollbarWidth(measure) {
+ if (knownScrollbarWidth != null) return knownScrollbarWidth;
+ var div = document.createElement('div');
+ div.style.cssText = "width: 50px; height: 50px; overflow-x: scroll";
+ document.body.appendChild(div);
+ knownScrollbarWidth = div.offsetHeight - div.clientHeight;
+ document.body.removeChild(div);
+ return knownScrollbarWidth || 0;
+}
+
+testCM("lineWidgetChanged", function(cm) {
+ addDoc(cm, 2, 300);
+ var halfScrollbarWidth = scrollbarWidth(cm.display.measure)/2;
+ cm.setOption('lineNumbers', true);
+ cm.setSize(600, cm.defaultTextHeight() * 50);
+ cm.scrollTo(null, cm.heightAtLine(125, "local"));
+
+ var expectedWidgetHeight = 60;
+ var expectedLinesInWidget = 3;
+ function w() {
+ var node = document.createElement("div");
+ // we use these children with just under half width of the line to check measurements are made with correct width
+ // when placed in the measure div.
+ // If the widget is measured at a width much narrower than it is displayed at, the underHalf children will span two lines and break the test.
+ // If the widget is measured at a width much wider than it is displayed at, the overHalf children will combine and break the test.
+ // Note that this test only checks widgets where coverGutter is true, because these require extra styling to get the width right.
+ // It may also be worthwhile to check this for non-coverGutter widgets.
+ // Visually:
+ // Good:
+ // | ------------- display width ------------- |
+ // | ------- widget-width when measured ------ |
+ // | | -- under-half -- | | -- under-half -- | |
+ // | | --- over-half --- | |
+ // | | --- over-half --- | |
+ // Height: measured as 3 lines, same as it will be when actually displayed
+
+ // Bad (too narrow):
+ // | ------------- display width ------------- |
+ // | ------ widget-width when measured ----- | < -- uh oh
+ // | | -- under-half -- | |
+ // | | -- under-half -- | | < -- when measured, shoved to next line
+ // | | --- over-half --- | |
+ // | | --- over-half --- | |
+ // Height: measured as 4 lines, more than expected . Will be displayed as 3 lines!
+
+ // Bad (too wide):
+ // | ------------- display width ------------- |
+ // | -------- widget-width when measured ------- | < -- uh oh
+ // | | -- under-half -- | | -- under-half -- | |
+ // | | --- over-half --- | | --- over-half --- | | < -- when measured, combined on one line
+ // Height: measured as 2 lines, less than expected. Will be displayed as 3 lines!
+
+ var barelyUnderHalfWidthHtml = '<div style="display: inline-block; height: 1px; width: '+(285 - halfScrollbarWidth)+'px;"></div>';
+ var barelyOverHalfWidthHtml = '<div style="display: inline-block; height: 1px; width: '+(305 - halfScrollbarWidth)+'px;"></div>';
+ node.innerHTML = new Array(3).join(barelyUnderHalfWidthHtml) + new Array(3).join(barelyOverHalfWidthHtml);
+ node.style.cssText = "background: yellow;font-size:0;line-height: " + (expectedWidgetHeight/expectedLinesInWidget) + "px;";
+ return node;
+ }
+ var info0 = cm.getScrollInfo();
+ var w0 = cm.addLineWidget(0, w(), { coverGutter: true });
+ var w150 = cm.addLineWidget(150, w(), { coverGutter: true });
+ var w300 = cm.addLineWidget(300, w(), { coverGutter: true });
+ var info1 = cm.getScrollInfo();
+ eq(info0.height + (3 * expectedWidgetHeight), info1.height);
+ eq(info0.top + expectedWidgetHeight, info1.top);
+ expectedWidgetHeight = 12;
+ w0.node.style.lineHeight = w150.node.style.lineHeight = w300.node.style.lineHeight = (expectedWidgetHeight/expectedLinesInWidget) + "px";
+ w0.changed(); w150.changed(); w300.changed();
+ var info2 = cm.getScrollInfo();
+ eq(info0.height + (3 * expectedWidgetHeight), info2.height);
+ eq(info0.top + expectedWidgetHeight, info2.top);
+});
+
+testCM("getLineNumber", function(cm) {
+ addDoc(cm, 2, 20);
+ var h1 = cm.getLineHandle(1);
+ eq(cm.getLineNumber(h1), 1);
+ cm.replaceRange("hi\nbye\n", Pos(0, 0));
+ eq(cm.getLineNumber(h1), 3);
+ cm.setValue("");
+ eq(cm.getLineNumber(h1), null);
+});
+
+testCM("jumpTheGap", function(cm) {
+ if (phantom) return;
+ var longLine = "abcdef ghiklmnop qrstuvw xyz ";
+ longLine += longLine; longLine += longLine; longLine += longLine;
+ cm.replaceRange(longLine, Pos(2, 0), Pos(2));
+ cm.setSize("200px", null);
+ cm.getWrapperElement().style.lineHeight = 2;
+ cm.refresh();
+ cm.setCursor(Pos(0, 1));
+ cm.execCommand("goLineDown");
+ eqPos(cm.getCursor(), Pos(1, 1));
+ cm.execCommand("goLineDown");
+ eqPos(cm.getCursor(), Pos(2, 1));
+ cm.execCommand("goLineDown");
+ eq(cm.getCursor().line, 2);
+ is(cm.getCursor().ch > 1);
+ cm.execCommand("goLineUp");
+ eqPos(cm.getCursor(), Pos(2, 1));
+ cm.execCommand("goLineUp");
+ eqPos(cm.getCursor(), Pos(1, 1));
+ var node = document.createElement("div");
+ node.innerHTML = "hi"; node.style.height = "30px";
+ cm.addLineWidget(0, node);
+ cm.addLineWidget(1, node.cloneNode(true), {above: true});
+ cm.setCursor(Pos(0, 2));
+ cm.execCommand("goLineDown");
+ eqPos(cm.getCursor(), Pos(1, 2));
+ cm.execCommand("goLineUp");
+ eqPos(cm.getCursor(), Pos(0, 2));
+}, {lineWrapping: true, value: "abc\ndef\nghi\njkl\n"});
+
+testCM("addLineClass", function(cm) {
+ function cls(line, text, bg, wrap, gutter) {
+ var i = cm.lineInfo(line);
+ eq(i.textClass, text);
+ eq(i.bgClass, bg);
+ eq(i.wrapClass, wrap);
+ if (typeof i.handle.gutterClass !== 'undefined') {
+ eq(i.handle.gutterClass, gutter);
+ }
+ }
+ cm.addLineClass(0, "text", "foo");
+ cm.addLineClass(0, "text", "bar");
+ cm.addLineClass(1, "background", "baz");
+ cm.addLineClass(1, "wrap", "foo");
+ cm.addLineClass(1, "gutter", "gutter-class");
+ cls(0, "foo bar", null, null, null);
+ cls(1, null, "baz", "foo", "gutter-class");
+ var lines = cm.display.lineDiv;
+ eq(byClassName(lines, "foo").length, 2);
+ eq(byClassName(lines, "bar").length, 1);
+ eq(byClassName(lines, "baz").length, 1);
+ eq(byClassName(lines, "gutter-class").length, 2); // Gutter classes are reflected in 2 nodes
+ cm.removeLineClass(0, "text", "foo");
+ cls(0, "bar", null, null, null);
+ cm.removeLineClass(0, "text", "foo");
+ cls(0, "bar", null, null, null);
+ cm.removeLineClass(0, "text", "bar");
+ cls(0, null, null, null);
+
+ cm.addLineClass(1, "wrap", "quux");
+ cls(1, null, "baz", "foo quux", "gutter-class");
+ cm.removeLineClass(1, "wrap");
+ cls(1, null, "baz", null, "gutter-class");
+ cm.removeLineClass(1, "gutter", "gutter-class");
+ eq(byClassName(lines, "gutter-class").length, 0);
+ cls(1, null, "baz", null, null);
+
+ cm.addLineClass(1, "gutter", "gutter-class");
+ cls(1, null, "baz", null, "gutter-class");
+ cm.removeLineClass(1, "gutter", "gutter-class");
+ cls(1, null, "baz", null, null);
+
+}, {value: "hohoho\n", lineNumbers: true});
+
+testCM("atomicMarker", function(cm) {
+ addDoc(cm, 10, 10);
+ function atom(ll, cl, lr, cr, li, ri) {
+ return cm.markText(Pos(ll, cl), Pos(lr, cr),
+ {atomic: true, inclusiveLeft: li, inclusiveRight: ri});
+ }
+ var m = atom(0, 1, 0, 5);
+ cm.setCursor(Pos(0, 1));
+ cm.execCommand("goCharRight");
+ eqPos(cm.getCursor(), Pos(0, 5));
+ cm.execCommand("goCharLeft");
+ eqPos(cm.getCursor(), Pos(0, 1));
+ m.clear();
+ m = atom(0, 0, 0, 5, true);
+ eqPos(cm.getCursor(), Pos(0, 5), "pushed out");
+ cm.execCommand("goCharLeft");
+ eqPos(cm.getCursor(), Pos(0, 5));
+ m.clear();
+ m = atom(8, 4, 9, 10, false, true);
+ cm.setCursor(Pos(9, 8));
+ eqPos(cm.getCursor(), Pos(8, 4), "set");
+ cm.execCommand("goCharRight");
+ eqPos(cm.getCursor(), Pos(8, 4), "char right");
+ cm.execCommand("goLineDown");
+ eqPos(cm.getCursor(), Pos(8, 4), "line down");
+ cm.execCommand("goCharLeft");
+ eqPos(cm.getCursor(), Pos(8, 3));
+ m.clear();
+ m = atom(1, 1, 3, 8);
+ cm.setCursor(Pos(0, 0));
+ cm.setCursor(Pos(2, 0));
+ eqPos(cm.getCursor(), Pos(3, 8));
+ cm.execCommand("goCharLeft");
+ eqPos(cm.getCursor(), Pos(1, 1));
+ cm.execCommand("goCharRight");
+ eqPos(cm.getCursor(), Pos(3, 8));
+ cm.execCommand("goLineUp");
+ eqPos(cm.getCursor(), Pos(1, 1));
+ cm.execCommand("goLineDown");
+ eqPos(cm.getCursor(), Pos(3, 8));
+ cm.execCommand("delCharBefore");
+ eq(cm.getValue().length, 80, "del chunk");
+ m = atom(3, 0, 5, 5);
+ cm.setCursor(Pos(3, 0));
+ cm.execCommand("delWordAfter");
+ eq(cm.getValue().length, 53, "del chunk");
+});
+
+testCM("selectionBias", function(cm) {
+ cm.markText(Pos(0, 1), Pos(0, 3), {atomic: true});
+ cm.setCursor(Pos(0, 2));
+ eqPos(cm.getCursor(), Pos(0, 1));
+ cm.setCursor(Pos(0, 2));
+ eqPos(cm.getCursor(), Pos(0, 3));
+ cm.setCursor(Pos(0, 2));
+ eqPos(cm.getCursor(), Pos(0, 1));
+ cm.setCursor(Pos(0, 2), null, {bias: -1});
+ eqPos(cm.getCursor(), Pos(0, 1));
+ cm.setCursor(Pos(0, 4));
+ cm.setCursor(Pos(0, 2), null, {bias: 1});
+ eqPos(cm.getCursor(), Pos(0, 3));
+}, {value: "12345"});
+
+testCM("selectionHomeEnd", function(cm) {
+ cm.markText(Pos(1, 0), Pos(1, 1), {atomic: true, inclusiveLeft: true});
+ cm.markText(Pos(1, 3), Pos(1, 4), {atomic: true, inclusiveRight: true});
+ cm.setCursor(Pos(1, 2));
+ cm.execCommand("goLineStart");
+ eqPos(cm.getCursor(), Pos(1, 1));
+ cm.execCommand("goLineEnd");
+ eqPos(cm.getCursor(), Pos(1, 3));
+}, {value: "ab\ncdef\ngh"});
+
+testCM("readOnlyMarker", function(cm) {
+ function mark(ll, cl, lr, cr, at) {
+ return cm.markText(Pos(ll, cl), Pos(lr, cr),
+ {readOnly: true, atomic: at});
+ }
+ var m = mark(0, 1, 0, 4);
+ cm.setCursor(Pos(0, 2));
+ cm.replaceSelection("hi", "end");
+ eqPos(cm.getCursor(), Pos(0, 2));
+ eq(cm.getLine(0), "abcde");
+ cm.execCommand("selectAll");
+ cm.replaceSelection("oops", "around");
+ eq(cm.getValue(), "oopsbcd");
+ cm.undo();
+ eqPos(m.find().from, Pos(0, 1));
+ eqPos(m.find().to, Pos(0, 4));
+ m.clear();
+ cm.setCursor(Pos(0, 2));
+ cm.replaceSelection("hi", "around");
+ eq(cm.getLine(0), "abhicde");
+ eqPos(cm.getCursor(), Pos(0, 4));
+ m = mark(0, 2, 2, 2, true);
+ cm.setSelection(Pos(1, 1), Pos(2, 4));
+ cm.replaceSelection("t", "end");
+ eqPos(cm.getCursor(), Pos(2, 3));
+ eq(cm.getLine(2), "klto");
+ cm.execCommand("goCharLeft");
+ cm.execCommand("goCharLeft");
+ eqPos(cm.getCursor(), Pos(0, 2));
+ cm.setSelection(Pos(0, 1), Pos(0, 3));
+ cm.replaceSelection("xx", "around");
+ eqPos(cm.getCursor(), Pos(0, 3));
+ eq(cm.getLine(0), "axxhicde");
+}, {value: "abcde\nfghij\nklmno\n"});
+
+testCM("dirtyBit", function(cm) {
+ eq(cm.isClean(), true);
+ cm.replaceSelection("boo", null, "test");
+ eq(cm.isClean(), false);
+ cm.undo();
+ eq(cm.isClean(), true);
+ cm.replaceSelection("boo", null, "test");
+ cm.replaceSelection("baz", null, "test");
+ cm.undo();
+ eq(cm.isClean(), false);
+ cm.markClean();
+ eq(cm.isClean(), true);
+ cm.undo();
+ eq(cm.isClean(), false);
+ cm.redo();
+ eq(cm.isClean(), true);
+});
+
+testCM("changeGeneration", function(cm) {
+ cm.replaceSelection("x");
+ var softGen = cm.changeGeneration();
+ cm.replaceSelection("x");
+ cm.undo();
+ eq(cm.getValue(), "");
+ is(!cm.isClean(softGen));
+ cm.replaceSelection("x");
+ var hardGen = cm.changeGeneration(true);
+ cm.replaceSelection("x");
+ cm.undo();
+ eq(cm.getValue(), "x");
+ is(cm.isClean(hardGen));
+});
+
+testCM("addKeyMap", function(cm) {
+ function sendKey(code) {
+ cm.triggerOnKeyDown({type: "keydown", keyCode: code,
+ preventDefault: function(){}, stopPropagation: function(){}});
+ }
+
+ sendKey(39);
+ eqPos(cm.getCursor(), Pos(0, 1));
+ var test = 0;
+ var map1 = {Right: function() { ++test; }}, map2 = {Right: function() { test += 10; }}
+ cm.addKeyMap(map1);
+ sendKey(39);
+ eqPos(cm.getCursor(), Pos(0, 1));
+ eq(test, 1);
+ cm.addKeyMap(map2, true);
+ sendKey(39);
+ eq(test, 2);
+ cm.removeKeyMap(map1);
+ sendKey(39);
+ eq(test, 12);
+ cm.removeKeyMap(map2);
+ sendKey(39);
+ eq(test, 12);
+ eqPos(cm.getCursor(), Pos(0, 2));
+ cm.addKeyMap({Right: function() { test = 55; }, name: "mymap"});
+ sendKey(39);
+ eq(test, 55);
+ cm.removeKeyMap("mymap");
+ sendKey(39);
+ eqPos(cm.getCursor(), Pos(0, 3));
+}, {value: "abc"});
+
+testCM("findPosH", function(cm) {
+ forEach([{from: Pos(0, 0), to: Pos(0, 1), by: 1},
+ {from: Pos(0, 0), to: Pos(0, 0), by: -1, hitSide: true},
+ {from: Pos(0, 0), to: Pos(0, 4), by: 1, unit: "word"},
+ {from: Pos(0, 0), to: Pos(0, 8), by: 2, unit: "word"},
+ {from: Pos(0, 0), to: Pos(2, 0), by: 20, unit: "word", hitSide: true},
+ {from: Pos(0, 7), to: Pos(0, 5), by: -1, unit: "word"},
+ {from: Pos(0, 4), to: Pos(0, 8), by: 1, unit: "word"},
+ {from: Pos(1, 0), to: Pos(1, 18), by: 3, unit: "word"},
+ {from: Pos(1, 22), to: Pos(1, 5), by: -3, unit: "word"},
+ {from: Pos(1, 15), to: Pos(1, 10), by: -5},
+ {from: Pos(1, 15), to: Pos(1, 10), by: -5, unit: "column"},
+ {from: Pos(1, 15), to: Pos(1, 0), by: -50, unit: "column", hitSide: true},
+ {from: Pos(1, 15), to: Pos(1, 24), by: 50, unit: "column", hitSide: true},
+ {from: Pos(1, 15), to: Pos(2, 0), by: 50, hitSide: true}], function(t) {
+ var r = cm.findPosH(t.from, t.by, t.unit || "char");
+ eqPos(r, t.to);
+ eq(!!r.hitSide, !!t.hitSide);
+ });
+}, {value: "line one\nline two.something.other\n"});
+
+testCM("beforeChange", function(cm) {
+ cm.on("beforeChange", function(cm, change) {
+ var text = [];
+ for (var i = 0; i < change.text.length; ++i)
+ text.push(change.text[i].replace(/\s/g, "_"));
+ change.update(null, null, text);
+ });
+ cm.setValue("hello, i am a\nnew document\n");
+ eq(cm.getValue(), "hello,_i_am_a\nnew_document\n");
+ CodeMirror.on(cm.getDoc(), "beforeChange", function(doc, change) {
+ if (change.from.line == 0) change.cancel();
+ });
+ cm.setValue("oops"); // Canceled
+ eq(cm.getValue(), "hello,_i_am_a\nnew_document\n");
+ cm.replaceRange("hey hey hey", Pos(1, 0), Pos(2, 0));
+ eq(cm.getValue(), "hello,_i_am_a\nhey_hey_hey");
+}, {value: "abcdefghijk"});
+
+testCM("beforeChangeUndo", function(cm) {
+ cm.replaceRange("hi", Pos(0, 0), Pos(0));
+ cm.replaceRange("bye", Pos(0, 0), Pos(0));
+ eq(cm.historySize().undo, 2);
+ cm.on("beforeChange", function(cm, change) {
+ is(!change.update);
+ change.cancel();
+ });
+ cm.undo();
+ eq(cm.historySize().undo, 0);
+ eq(cm.getValue(), "bye\ntwo");
+}, {value: "one\ntwo"});
+
+testCM("beforeSelectionChange", function(cm) {
+ function notAtEnd(cm, pos) {
+ var len = cm.getLine(pos.line).length;
+ if (!len || pos.ch == len) return Pos(pos.line, pos.ch - 1);
+ return pos;
+ }
+ cm.on("beforeSelectionChange", function(cm, obj) {
+ obj.update([{anchor: notAtEnd(cm, obj.ranges[0].anchor),
+ head: notAtEnd(cm, obj.ranges[0].head)}]);
+ });
+
+ addDoc(cm, 10, 10);
+ cm.execCommand("goLineEnd");
+ eqPos(cm.getCursor(), Pos(0, 9));
+ cm.execCommand("selectAll");
+ eqPos(cm.getCursor("start"), Pos(0, 0));
+ eqPos(cm.getCursor("end"), Pos(9, 9));
+});
+
+testCM("change_removedText", function(cm) {
+ cm.setValue("abc\ndef");
+
+ var removedText = [];
+ cm.on("change", function(cm, change) {
+ removedText.push(change.removed);
+ });
+
+ cm.operation(function() {
+ cm.replaceRange("xyz", Pos(0, 0), Pos(1,1));
+ cm.replaceRange("123", Pos(0,0));
+ });
+
+ eq(removedText.length, 2);
+ eq(removedText[0].join("\n"), "abc\nd");
+ eq(removedText[1].join("\n"), "");
+
+ var removedText = [];
+ cm.undo();
+ eq(removedText.length, 2);
+ eq(removedText[0].join("\n"), "123");
+ eq(removedText[1].join("\n"), "xyz");
+
+ var removedText = [];
+ cm.redo();
+ eq(removedText.length, 2);
+ eq(removedText[0].join("\n"), "abc\nd");
+ eq(removedText[1].join("\n"), "");
+});
+
+testCM("lineStyleFromMode", function(cm) {
+ CodeMirror.defineMode("test_mode", function() {
+ return {token: function(stream) {
+ if (stream.match(/^\[[^\]]*\]/)) return " line-brackets ";
+ if (stream.match(/^\([^\)]*\)/)) return " line-background-parens ";
+ if (stream.match(/^<[^>]*>/)) return " span line-line line-background-bg ";
+ stream.match(/^\s+|^\S+/);
+ }};
+ });
+ cm.setOption("mode", "test_mode");
+ var bracketElts = byClassName(cm.getWrapperElement(), "brackets");
+ eq(bracketElts.length, 1, "brackets count");
+ eq(bracketElts[0].nodeName, "PRE");
+ is(!/brackets.*brackets/.test(bracketElts[0].className));
+ var parenElts = byClassName(cm.getWrapperElement(), "parens");
+ eq(parenElts.length, 1, "parens count");
+ eq(parenElts[0].nodeName, "DIV");
+ is(!/parens.*parens/.test(parenElts[0].className));
+ eq(parenElts[0].parentElement.nodeName, "DIV");
+
+ eq(byClassName(cm.getWrapperElement(), "bg").length, 1);
+ eq(byClassName(cm.getWrapperElement(), "line").length, 1);
+ var spanElts = byClassName(cm.getWrapperElement(), "cm-span");
+ eq(spanElts.length, 2);
+ is(/^\s*cm-span\s*$/.test(spanElts[0].className));
+}, {value: "line1: [br] [br]\nline2: (par) (par)\nline3: <tag> <tag>"});
+
+testCM("lineStyleFromBlankLine", function(cm) {
+ CodeMirror.defineMode("lineStyleFromBlankLine_mode", function() {
+ return {token: function(stream) { stream.skipToEnd(); return "comment"; },
+ blankLine: function() { return "line-blank"; }};
+ });
+ cm.setOption("mode", "lineStyleFromBlankLine_mode");
+ var blankElts = byClassName(cm.getWrapperElement(), "blank");
+ eq(blankElts.length, 1);
+ eq(blankElts[0].nodeName, "PRE");
+ cm.replaceRange("x", Pos(1, 0));
+ blankElts = byClassName(cm.getWrapperElement(), "blank");
+ eq(blankElts.length, 0);
+}, {value: "foo\n\nbar"});
+
+CodeMirror.registerHelper("xxx", "a", "A");
+CodeMirror.registerHelper("xxx", "b", "B");
+CodeMirror.defineMode("yyy", function() {
+ return {
+ token: function(stream) { stream.skipToEnd(); },
+ xxx: ["a", "b", "q"]
+ };
+});
+CodeMirror.registerGlobalHelper("xxx", "c", function(m) { return m.enableC; }, "C");
+
+testCM("helpers", function(cm) {
+ cm.setOption("mode", "yyy");
+ eq(cm.getHelpers(Pos(0, 0), "xxx").join("/"), "A/B");
+ cm.setOption("mode", {name: "yyy", modeProps: {xxx: "b", enableC: true}});
+ eq(cm.getHelpers(Pos(0, 0), "xxx").join("/"), "B/C");
+ cm.setOption("mode", "javascript");
+ eq(cm.getHelpers(Pos(0, 0), "xxx").join("/"), "");
+});
+
+testCM("selectionHistory", function(cm) {
+ for (var i = 0; i < 3; i++) {
+ cm.setExtending(true);
+ cm.execCommand("goCharRight");
+ cm.setExtending(false);
+ cm.execCommand("goCharRight");
+ cm.execCommand("goCharRight");
+ }
+ cm.execCommand("undoSelection");
+ eq(cm.getSelection(), "c");
+ cm.execCommand("undoSelection");
+ eq(cm.getSelection(), "");
+ eqPos(cm.getCursor(), Pos(0, 4));
+ cm.execCommand("undoSelection");
+ eq(cm.getSelection(), "b");
+ cm.execCommand("redoSelection");
+ eq(cm.getSelection(), "");
+ eqPos(cm.getCursor(), Pos(0, 4));
+ cm.execCommand("redoSelection");
+ eq(cm.getSelection(), "c");
+ cm.execCommand("redoSelection");
+ eq(cm.getSelection(), "");
+ eqPos(cm.getCursor(), Pos(0, 6));
+}, {value: "a b c d"});
+
+testCM("selectionChangeReducesRedo", function(cm) {
+ cm.replaceSelection("X");
+ cm.execCommand("goCharRight");
+ cm.undoSelection();
+ cm.execCommand("selectAll");
+ cm.undoSelection();
+ eq(cm.getValue(), "Xabc");
+ eqPos(cm.getCursor(), Pos(0, 1));
+ cm.undoSelection();
+ eq(cm.getValue(), "abc");
+}, {value: "abc"});
+
+testCM("selectionHistoryNonOverlapping", function(cm) {
+ cm.setSelection(Pos(0, 0), Pos(0, 1));
+ cm.setSelection(Pos(0, 2), Pos(0, 3));
+ cm.execCommand("undoSelection");
+ eqPos(cm.getCursor("anchor"), Pos(0, 0));
+ eqPos(cm.getCursor("head"), Pos(0, 1));
+}, {value: "1234"});
+
+testCM("cursorMotionSplitsHistory", function(cm) {
+ cm.replaceSelection("a");
+ cm.execCommand("goCharRight");
+ cm.replaceSelection("b");
+ cm.replaceSelection("c");
+ cm.undo();
+ eq(cm.getValue(), "a1234");
+ eqPos(cm.getCursor(), Pos(0, 2));
+ cm.undo();
+ eq(cm.getValue(), "1234");
+ eqPos(cm.getCursor(), Pos(0, 0));
+}, {value: "1234"});
+
+testCM("selChangeInOperationDoesNotSplit", function(cm) {
+ for (var i = 0; i < 4; i++) {
+ cm.operation(function() {
+ cm.replaceSelection("x");
+ cm.setCursor(Pos(0, cm.getCursor().ch - 1));
+ });
+ }
+ eqPos(cm.getCursor(), Pos(0, 0));
+ eq(cm.getValue(), "xxxxa");
+ cm.undo();
+ eq(cm.getValue(), "a");
+}, {value: "a"});
+
+testCM("alwaysMergeSelEventWithChangeOrigin", function(cm) {
+ cm.replaceSelection("U", null, "foo");
+ cm.setSelection(Pos(0, 0), Pos(0, 1), {origin: "foo"});
+ cm.undoSelection();
+ eq(cm.getValue(), "a");
+ cm.replaceSelection("V", null, "foo");
+ cm.setSelection(Pos(0, 0), Pos(0, 1), {origin: "bar"});
+ cm.undoSelection();
+ eq(cm.getValue(), "Va");
+}, {value: "a"});
+
+testCM("getTokenAt", function(cm) {
+ var tokPlus = cm.getTokenAt(Pos(0, 2));
+ eq(tokPlus.type, "operator");
+ eq(tokPlus.string, "+");
+ var toks = cm.getLineTokens(0);
+ eq(toks.length, 3);
+ forEach([["number", "1"], ["operator", "+"], ["number", "2"]], function(expect, i) {
+ eq(toks[i].type, expect[0]);
+ eq(toks[i].string, expect[1]);
+ });
+}, {value: "1+2", mode: "javascript"});
+
+testCM("getTokenTypeAt", function(cm) {
+ eq(cm.getTokenTypeAt(Pos(0, 0)), "number");
+ eq(cm.getTokenTypeAt(Pos(0, 6)), "string");
+ cm.addOverlay({
+ token: function(stream) {
+ if (stream.match("foo")) return "foo";
+ else stream.next();
+ }
+ });
+ eq(byClassName(cm.getWrapperElement(), "cm-foo").length, 1);
+ eq(cm.getTokenTypeAt(Pos(0, 6)), "string");
+}, {value: "1 + 'foo'", mode: "javascript"});
+
+testCM("resizeLineWidget", function(cm) {
+ addDoc(cm, 200, 3);
+ var widget = document.createElement("pre");
+ widget.innerHTML = "imwidget";
+ widget.style.background = "yellow";
+ cm.addLineWidget(1, widget, {noHScroll: true});
+ cm.setSize(40);
+ is(widget.parentNode.offsetWidth < 42);
+});
+
+testCM("combinedOperations", function(cm) {
+ var place = document.getElementById("testground");
+ var other = CodeMirror(place, {value: "123"});
+ try {
+ cm.operation(function() {
+ cm.addLineClass(0, "wrap", "foo");
+ other.addLineClass(0, "wrap", "foo");
+ });
+ eq(byClassName(cm.getWrapperElement(), "foo").length, 1);
+ eq(byClassName(other.getWrapperElement(), "foo").length, 1);
+ cm.operation(function() {
+ cm.removeLineClass(0, "wrap", "foo");
+ other.removeLineClass(0, "wrap", "foo");
+ });
+ eq(byClassName(cm.getWrapperElement(), "foo").length, 0);
+ eq(byClassName(other.getWrapperElement(), "foo").length, 0);
+ } finally {
+ place.removeChild(other.getWrapperElement());
+ }
+}, {value: "abc"});
+
+testCM("eventOrder", function(cm) {
+ var seen = [];
+ cm.on("change", function() {
+ if (!seen.length) cm.replaceSelection(".");
+ seen.push("change");
+ });
+ cm.on("cursorActivity", function() {
+ cm.replaceSelection("!");
+ seen.push("activity");
+ });
+ cm.replaceSelection("/");
+ eq(seen.join(","), "change,change,activity,change");
+});
+
+testCM("splitSpaces_nonspecial", function(cm) {
+ eq(byClassName(cm.getWrapperElement(), "cm-invalidchar").length, 0);
+}, {
+ specialChars: /[\u00a0]/,
+ value: "spaces -> <- between"
+});
+
+test("core_rmClass", function() {
+ var node = document.createElement("div");
+ node.className = "foo-bar baz-quux yadda";
+ CodeMirror.rmClass(node, "quux");
+ eq(node.className, "foo-bar baz-quux yadda");
+ CodeMirror.rmClass(node, "baz-quux");
+ eq(node.className, "foo-bar yadda");
+ CodeMirror.rmClass(node, "yadda");
+ eq(node.className, "foo-bar");
+ CodeMirror.rmClass(node, "foo-bar");
+ eq(node.className, "");
+ node.className = " foo ";
+ CodeMirror.rmClass(node, "foo");
+ eq(node.className, "");
+});
+
+test("core_addClass", function() {
+ var node = document.createElement("div");
+ CodeMirror.addClass(node, "a");
+ eq(node.className, "a");
+ CodeMirror.addClass(node, "a");
+ eq(node.className, "a");
+ CodeMirror.addClass(node, "b");
+ eq(node.className, "a b");
+ CodeMirror.addClass(node, "a");
+ CodeMirror.addClass(node, "b");
+ eq(node.className, "a b");
+});
+
+testCM("lineSeparator", function(cm) {
+ eq(cm.lineCount(), 3);
+ eq(cm.getLine(1), "bar\r");
+ eq(cm.getLine(2), "baz\rquux");
+ cm.setOption("lineSeparator", "\r");
+ eq(cm.lineCount(), 5);
+ eq(cm.getLine(4), "quux");
+ eq(cm.getValue(), "foo\rbar\r\rbaz\rquux");
+ eq(cm.getValue("\n"), "foo\nbar\n\nbaz\nquux");
+ cm.setOption("lineSeparator", null);
+ cm.setValue("foo\nbar\r\nbaz\rquux");
+ eq(cm.lineCount(), 4);
+}, {value: "foo\nbar\r\nbaz\rquux",
+ lineSeparator: "\n"});
diff --git a/devtools/client/sourceeditor/test/codemirror/vim_test.js b/devtools/client/sourceeditor/test/codemirror/vim_test.js
new file mode 100644
index 000000000..fb612b140
--- /dev/null
+++ b/devtools/client/sourceeditor/test/codemirror/vim_test.js
@@ -0,0 +1,4011 @@
+CodeMirror.Vim.suppressErrorLogging = true;
+
+var code = '' +
+' wOrd1 (#%\n' +
+' word3] \n' +
+'aopop pop 0 1 2 3 4\n' +
+' (a) [b] {c} \n' +
+'int getchar(void) {\n' +
+' static char buf[BUFSIZ];\n' +
+' static char *bufp = buf;\n' +
+' if (n == 0) { /* buffer is empty */\n' +
+' n = read(0, buf, sizeof buf);\n' +
+' bufp = buf;\n' +
+' }\n' +
+'\n' +
+' return (--n >= 0) ? (unsigned char) *bufp++ : EOF;\n' +
+' \n' +
+'}\n';
+
+var lines = (function() {
+ lineText = code.split('\n');
+ var ret = [];
+ for (var i = 0; i < lineText.length; i++) {
+ ret[i] = {
+ line: i,
+ length: lineText[i].length,
+ lineText: lineText[i],
+ textStart: /^\s*/.exec(lineText[i])[0].length
+ };
+ }
+ return ret;
+})();
+var endOfDocument = makeCursor(lines.length - 1,
+ lines[lines.length - 1].length);
+var wordLine = lines[0];
+var bigWordLine = lines[1];
+var charLine = lines[2];
+var bracesLine = lines[3];
+var seekBraceLine = lines[4];
+
+var word1 = {
+ start: { line: wordLine.line, ch: 1 },
+ end: { line: wordLine.line, ch: 5 }
+};
+var word2 = {
+ start: { line: wordLine.line, ch: word1.end.ch + 2 },
+ end: { line: wordLine.line, ch: word1.end.ch + 4 }
+};
+var word3 = {
+ start: { line: bigWordLine.line, ch: 1 },
+ end: { line: bigWordLine.line, ch: 5 }
+};
+var bigWord1 = word1;
+var bigWord2 = word2;
+var bigWord3 = {
+ start: { line: bigWordLine.line, ch: 1 },
+ end: { line: bigWordLine.line, ch: 7 }
+};
+var bigWord4 = {
+ start: { line: bigWordLine.line, ch: bigWord1.end.ch + 3 },
+ end: { line: bigWordLine.line, ch: bigWord1.end.ch + 7 }
+};
+
+var oChars = [ { line: charLine.line, ch: 1 },
+ { line: charLine.line, ch: 3 },
+ { line: charLine.line, ch: 7 } ];
+var pChars = [ { line: charLine.line, ch: 2 },
+ { line: charLine.line, ch: 4 },
+ { line: charLine.line, ch: 6 },
+ { line: charLine.line, ch: 8 } ];
+var numChars = [ { line: charLine.line, ch: 10 },
+ { line: charLine.line, ch: 12 },
+ { line: charLine.line, ch: 14 },
+ { line: charLine.line, ch: 16 },
+ { line: charLine.line, ch: 18 }];
+var parens1 = {
+ start: { line: bracesLine.line, ch: 1 },
+ end: { line: bracesLine.line, ch: 3 }
+};
+var squares1 = {
+ start: { line: bracesLine.line, ch: 5 },
+ end: { line: bracesLine.line, ch: 7 }
+};
+var curlys1 = {
+ start: { line: bracesLine.line, ch: 9 },
+ end: { line: bracesLine.line, ch: 11 }
+};
+var seekOutside = {
+ start: { line: seekBraceLine.line, ch: 1 },
+ end: { line: seekBraceLine.line, ch: 16 }
+};
+var seekInside = {
+ start: { line: seekBraceLine.line, ch: 14 },
+ end: { line: seekBraceLine.line, ch: 11 }
+};
+
+function copyCursor(cur) {
+ return { ch: cur.ch, line: cur.line };
+}
+
+function forEach(arr, func) {
+ for (var i = 0; i < arr.length; i++) {
+ func(arr[i], i, arr);
+ }
+}
+
+function testVim(name, run, opts, expectedFail) {
+ var vimOpts = {
+ lineNumbers: true,
+ vimMode: true,
+ showCursorWhenSelecting: true,
+ value: code
+ };
+ for (var prop in opts) {
+ if (opts.hasOwnProperty(prop)) {
+ vimOpts[prop] = opts[prop];
+ }
+ }
+ return test('vim_' + name, function() {
+ var place = document.getElementById("testground");
+ var cm = CodeMirror(place, vimOpts);
+ var vim = CodeMirror.Vim.maybeInitVimState_(cm);
+
+ function doKeysFn(cm) {
+ return function(args) {
+ if (args instanceof Array) {
+ arguments = args;
+ }
+ for (var i = 0; i < arguments.length; i++) {
+ CodeMirror.Vim.handleKey(cm, arguments[i]);
+ }
+ }
+ }
+ function doInsertModeKeysFn(cm) {
+ return function(args) {
+ if (args instanceof Array) { arguments = args; }
+ function executeHandler(handler) {
+ if (typeof handler == 'string') {
+ CodeMirror.commands[handler](cm);
+ } else {
+ handler(cm);
+ }
+ return true;
+ }
+ for (var i = 0; i < arguments.length; i++) {
+ var key = arguments[i];
+ // Find key in keymap and handle.
+ var handled = CodeMirror.lookupKey(key, 'vim-insert', executeHandler);
+ // Record for insert mode.
+ if (handled == "handled" && cm.state.vim.insertMode && arguments[i] != 'Esc') {
+ var lastChange = CodeMirror.Vim.getVimGlobalState_().macroModeState.lastInsertModeChanges;
+ if (lastChange) {
+ lastChange.changes.push(new CodeMirror.Vim.InsertModeKey(key));
+ }
+ }
+ }
+ }
+ }
+ function doExFn(cm) {
+ return function(command) {
+ cm.openDialog = helpers.fakeOpenDialog(command);
+ helpers.doKeys(':');
+ }
+ }
+ function assertCursorAtFn(cm) {
+ return function(line, ch) {
+ var pos;
+ if (ch == null && typeof line.line == 'number') {
+ pos = line;
+ } else {
+ pos = makeCursor(line, ch);
+ }
+ eqPos(pos, cm.getCursor());
+ }
+ }
+ function fakeOpenDialog(result) {
+ return function(text, callback) {
+ return callback(result);
+ }
+ }
+ function fakeOpenNotification(matcher) {
+ return function(text) {
+ matcher(text);
+ }
+ }
+ var helpers = {
+ doKeys: doKeysFn(cm),
+ // Warning: Only emulates keymap events, not character insertions. Use
+ // replaceRange to simulate character insertions.
+ // Keys are in CodeMirror format, NOT vim format.
+ doInsertModeKeys: doInsertModeKeysFn(cm),
+ doEx: doExFn(cm),
+ assertCursorAt: assertCursorAtFn(cm),
+ fakeOpenDialog: fakeOpenDialog,
+ fakeOpenNotification: fakeOpenNotification,
+ getRegisterController: function() {
+ return CodeMirror.Vim.getRegisterController();
+ }
+ }
+ CodeMirror.Vim.resetVimGlobalState_();
+ var successful = false;
+ var savedOpenNotification = cm.openNotification;
+ var savedOpenDialog = cm.openDialog;
+ try {
+ run(cm, vim, helpers);
+ successful = true;
+ } finally {
+ cm.openNotification = savedOpenNotification;
+ cm.openDialog = savedOpenDialog;
+ if (!successful || verbose) {
+ place.style.visibility = "visible";
+ } else {
+ place.removeChild(cm.getWrapperElement());
+ }
+ }
+ }, expectedFail);
+};
+testVim('qq@q', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'q', 'l', 'l', 'q');
+ helpers.assertCursorAt(0,2);
+ helpers.doKeys('@', 'q');
+ helpers.assertCursorAt(0,4);
+}, { value: ' '});
+testVim('@@', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'q', 'l', 'l', 'q');
+ helpers.assertCursorAt(0,2);
+ helpers.doKeys('@', 'q');
+ helpers.assertCursorAt(0,4);
+ helpers.doKeys('@', '@');
+ helpers.assertCursorAt(0,6);
+}, { value: ' '});
+var jumplistScene = ''+
+ 'word\n'+
+ '(word)\n'+
+ '{word\n'+
+ 'word.\n'+
+ '\n'+
+ 'word search\n'+
+ '}word\n'+
+ 'word\n'+
+ 'word\n';
+function testJumplist(name, keys, endPos, startPos, dialog) {
+ endPos = makeCursor(endPos[0], endPos[1]);
+ startPos = makeCursor(startPos[0], startPos[1]);
+ testVim(name, function(cm, vim, helpers) {
+ CodeMirror.Vim.resetVimGlobalState_();
+ if(dialog)cm.openDialog = helpers.fakeOpenDialog('word');
+ cm.setCursor(startPos);
+ helpers.doKeys.apply(null, keys);
+ helpers.assertCursorAt(endPos);
+ }, {value: jumplistScene});
+};
+testJumplist('jumplist_H', ['H', '<C-o>'], [5,2], [5,2]);
+testJumplist('jumplist_M', ['M', '<C-o>'], [2,2], [2,2]);
+testJumplist('jumplist_L', ['L', '<C-o>'], [2,2], [2,2]);
+testJumplist('jumplist_[[', ['[', '[', '<C-o>'], [5,2], [5,2]);
+testJumplist('jumplist_]]', [']', ']', '<C-o>'], [2,2], [2,2]);
+testJumplist('jumplist_G', ['G', '<C-o>'], [5,2], [5,2]);
+testJumplist('jumplist_gg', ['g', 'g', '<C-o>'], [5,2], [5,2]);
+testJumplist('jumplist_%', ['%', '<C-o>'], [1,5], [1,5]);
+testJumplist('jumplist_{', ['{', '<C-o>'], [1,5], [1,5]);
+testJumplist('jumplist_}', ['}', '<C-o>'], [1,5], [1,5]);
+testJumplist('jumplist_\'', ['m', 'a', 'h', '\'', 'a', 'h', '<C-i>'], [1,0], [1,5]);
+testJumplist('jumplist_`', ['m', 'a', 'h', '`', 'a', 'h', '<C-i>'], [1,5], [1,5]);
+testJumplist('jumplist_*_cachedCursor', ['*', '<C-o>'], [1,3], [1,3]);
+testJumplist('jumplist_#_cachedCursor', ['#', '<C-o>'], [1,3], [1,3]);
+testJumplist('jumplist_n', ['#', 'n', '<C-o>'], [1,1], [2,3]);
+testJumplist('jumplist_N', ['#', 'N', '<C-o>'], [1,1], [2,3]);
+testJumplist('jumplist_repeat_<c-o>', ['*', '*', '*', '3', '<C-o>'], [2,3], [2,3]);
+testJumplist('jumplist_repeat_<c-i>', ['*', '*', '*', '3', '<C-o>', '2', '<C-i>'], [5,0], [2,3]);
+testJumplist('jumplist_repeated_motion', ['3', '*', '<C-o>'], [2,3], [2,3]);
+testJumplist('jumplist_/', ['/', '<C-o>'], [2,3], [2,3], 'dialog');
+testJumplist('jumplist_?', ['?', '<C-o>'], [2,3], [2,3], 'dialog');
+testJumplist('jumplist_skip_deleted_mark<c-o>',
+ ['*', 'n', 'n', 'k', 'd', 'k', '<C-o>', '<C-o>', '<C-o>'],
+ [0,2], [0,2]);
+testJumplist('jumplist_skip_deleted_mark<c-i>',
+ ['*', 'n', 'n', 'k', 'd', 'k', '<C-o>', '<C-i>', '<C-i>'],
+ [1,0], [0,2]);
+
+/**
+ * @param name Name of the test
+ * @param keys An array of keys or a string with a single key to simulate.
+ * @param endPos The expected end position of the cursor.
+ * @param startPos The position the cursor should start at, defaults to 0, 0.
+ */
+function testMotion(name, keys, endPos, startPos) {
+ testVim(name, function(cm, vim, helpers) {
+ if (!startPos) {
+ startPos = { line: 0, ch: 0 };
+ }
+ cm.setCursor(startPos);
+ helpers.doKeys(keys);
+ helpers.assertCursorAt(endPos);
+ });
+};
+
+function makeCursor(line, ch) {
+ return { line: line, ch: ch };
+};
+
+function offsetCursor(cur, offsetLine, offsetCh) {
+ return { line: cur.line + offsetLine, ch: cur.ch + offsetCh };
+};
+
+// Motion tests
+testMotion('|', '|', makeCursor(0, 0), makeCursor(0,4));
+testMotion('|_repeat', ['3', '|'], makeCursor(0, 2), makeCursor(0,4));
+testMotion('h', 'h', makeCursor(0, 0), word1.start);
+testMotion('h_repeat', ['3', 'h'], offsetCursor(word1.end, 0, -3), word1.end);
+testMotion('l', 'l', makeCursor(0, 1));
+testMotion('l_repeat', ['2', 'l'], makeCursor(0, 2));
+testMotion('j', 'j', offsetCursor(word1.end, 1, 0), word1.end);
+testMotion('j_repeat', ['2', 'j'], offsetCursor(word1.end, 2, 0), word1.end);
+testMotion('j_repeat_clip', ['1000', 'j'], endOfDocument);
+testMotion('k', 'k', offsetCursor(word3.end, -1, 0), word3.end);
+testMotion('k_repeat', ['2', 'k'], makeCursor(0, 4), makeCursor(2, 4));
+testMotion('k_repeat_clip', ['1000', 'k'], makeCursor(0, 4), makeCursor(2, 4));
+testMotion('w', 'w', word1.start);
+testMotion('w_multiple_newlines_no_space', 'w', makeCursor(12, 2), makeCursor(11, 2));
+testMotion('w_multiple_newlines_with_space', 'w', makeCursor(14, 0), makeCursor(12, 51));
+testMotion('w_repeat', ['2', 'w'], word2.start);
+testMotion('w_wrap', ['w'], word3.start, word2.start);
+testMotion('w_endOfDocument', 'w', endOfDocument, endOfDocument);
+testMotion('w_start_to_end', ['1000', 'w'], endOfDocument, makeCursor(0, 0));
+testMotion('W', 'W', bigWord1.start);
+testMotion('W_repeat', ['2', 'W'], bigWord3.start, bigWord1.start);
+testMotion('e', 'e', word1.end);
+testMotion('e_repeat', ['2', 'e'], word2.end);
+testMotion('e_wrap', 'e', word3.end, word2.end);
+testMotion('e_endOfDocument', 'e', endOfDocument, endOfDocument);
+testMotion('e_start_to_end', ['1000', 'e'], endOfDocument, makeCursor(0, 0));
+testMotion('b', 'b', word3.start, word3.end);
+testMotion('b_repeat', ['2', 'b'], word2.start, word3.end);
+testMotion('b_wrap', 'b', word2.start, word3.start);
+testMotion('b_startOfDocument', 'b', makeCursor(0, 0), makeCursor(0, 0));
+testMotion('b_end_to_start', ['1000', 'b'], makeCursor(0, 0), endOfDocument);
+testMotion('ge', ['g', 'e'], word2.end, word3.end);
+testMotion('ge_repeat', ['2', 'g', 'e'], word1.end, word3.start);
+testMotion('ge_wrap', ['g', 'e'], word2.end, word3.start);
+testMotion('ge_startOfDocument', ['g', 'e'], makeCursor(0, 0),
+ makeCursor(0, 0));
+testMotion('ge_end_to_start', ['1000', 'g', 'e'], makeCursor(0, 0), endOfDocument);
+testMotion('gg', ['g', 'g'], makeCursor(lines[0].line, lines[0].textStart),
+ makeCursor(3, 1));
+testMotion('gg_repeat', ['3', 'g', 'g'],
+ makeCursor(lines[2].line, lines[2].textStart));
+testMotion('G', 'G',
+ makeCursor(lines[lines.length - 1].line, lines[lines.length - 1].textStart),
+ makeCursor(3, 1));
+testMotion('G_repeat', ['3', 'G'], makeCursor(lines[2].line,
+ lines[2].textStart));
+// TODO: Make the test code long enough to test Ctrl-F and Ctrl-B.
+testMotion('0', '0', makeCursor(0, 0), makeCursor(0, 8));
+testMotion('^', '^', makeCursor(0, lines[0].textStart), makeCursor(0, 8));
+testMotion('+', '+', makeCursor(1, lines[1].textStart), makeCursor(0, 8));
+testMotion('-', '-', makeCursor(0, lines[0].textStart), makeCursor(1, 4));
+testMotion('_', ['6','_'], makeCursor(5, lines[5].textStart), makeCursor(0, 8));
+testMotion('$', '$', makeCursor(0, lines[0].length - 1), makeCursor(0, 1));
+testMotion('$_repeat', ['2', '$'], makeCursor(1, lines[1].length - 1),
+ makeCursor(0, 3));
+testMotion('f', ['f', 'p'], pChars[0], makeCursor(charLine.line, 0));
+testMotion('f_repeat', ['2', 'f', 'p'], pChars[2], pChars[0]);
+testMotion('f_num', ['f', '2'], numChars[2], makeCursor(charLine.line, 0));
+testMotion('t', ['t','p'], offsetCursor(pChars[0], 0, -1),
+ makeCursor(charLine.line, 0));
+testMotion('t_repeat', ['2', 't', 'p'], offsetCursor(pChars[2], 0, -1),
+ pChars[0]);
+testMotion('F', ['F', 'p'], pChars[0], pChars[1]);
+testMotion('F_repeat', ['2', 'F', 'p'], pChars[0], pChars[2]);
+testMotion('T', ['T', 'p'], offsetCursor(pChars[0], 0, 1), pChars[1]);
+testMotion('T_repeat', ['2', 'T', 'p'], offsetCursor(pChars[0], 0, 1), pChars[2]);
+testMotion('%_parens', ['%'], parens1.end, parens1.start);
+testMotion('%_squares', ['%'], squares1.end, squares1.start);
+testMotion('%_braces', ['%'], curlys1.end, curlys1.start);
+testMotion('%_seek_outside', ['%'], seekOutside.end, seekOutside.start);
+testMotion('%_seek_inside', ['%'], seekInside.end, seekInside.start);
+testVim('%_seek_skip', function(cm, vim, helpers) {
+ cm.setCursor(0,0);
+ helpers.doKeys(['%']);
+ helpers.assertCursorAt(0,9);
+}, {value:'01234"("()'});
+testVim('%_skip_string', function(cm, vim, helpers) {
+ cm.setCursor(0,0);
+ helpers.doKeys(['%']);
+ helpers.assertCursorAt(0,4);
+ cm.setCursor(0,2);
+ helpers.doKeys(['%']);
+ helpers.assertCursorAt(0,0);
+}, {value:'(")")'});
+testVim('%_skip_comment', function(cm, vim, helpers) {
+ cm.setCursor(0,0);
+ helpers.doKeys(['%']);
+ helpers.assertCursorAt(0,6);
+ cm.setCursor(0,3);
+ helpers.doKeys(['%']);
+ helpers.assertCursorAt(0,0);
+}, {value:'(/*)*/)'});
+// Make sure that moving down after going to the end of a line always leaves you
+// at the end of a line, but preserves the offset in other cases
+testVim('Changing lines after Eol operation', function(cm, vim, helpers) {
+ cm.setCursor(0,0);
+ helpers.doKeys(['$']);
+ helpers.doKeys(['j']);
+ // After moving to Eol and then down, we should be at Eol of line 2
+ helpers.assertCursorAt({ line: 1, ch: lines[1].length - 1 });
+ helpers.doKeys(['j']);
+ // After moving down, we should be at Eol of line 3
+ helpers.assertCursorAt({ line: 2, ch: lines[2].length - 1 });
+ helpers.doKeys(['h']);
+ helpers.doKeys(['j']);
+ // After moving back one space and then down, since line 4 is shorter than line 2, we should
+ // be at Eol of line 2 - 1
+ helpers.assertCursorAt({ line: 3, ch: lines[3].length - 1 });
+ helpers.doKeys(['j']);
+ helpers.doKeys(['j']);
+ // After moving down again, since line 3 has enough characters, we should be back to the
+ // same place we were at on line 1
+ helpers.assertCursorAt({ line: 5, ch: lines[2].length - 2 });
+});
+//making sure gj and gk recover from clipping
+testVim('gj_gk_clipping', function(cm,vim,helpers){
+ cm.setCursor(0, 1);
+ helpers.doKeys('g','j','g','j');
+ helpers.assertCursorAt(2, 1);
+ helpers.doKeys('g','k','g','k');
+ helpers.assertCursorAt(0, 1);
+},{value: 'line 1\n\nline 2'});
+//testing a mix of j/k and gj/gk
+testVim('j_k_and_gj_gk', function(cm,vim,helpers){
+ cm.setSize(120);
+ cm.setCursor(0, 0);
+ //go to the last character on the first line
+ helpers.doKeys('$');
+ //move up/down on the column within the wrapped line
+ //side-effect: cursor is not locked to eol anymore
+ helpers.doKeys('g','k');
+ var cur=cm.getCursor();
+ eq(cur.line,0);
+ is((cur.ch<176),'gk didn\'t move cursor back (1)');
+ helpers.doKeys('g','j');
+ helpers.assertCursorAt(0, 176);
+ //should move to character 177 on line 2 (j/k preserve character index within line)
+ helpers.doKeys('j');
+ //due to different line wrapping, the cursor can be on a different screen-x now
+ //gj and gk preserve screen-x on movement, much like moveV
+ helpers.doKeys('3','g','k');
+ cur=cm.getCursor();
+ eq(cur.line,1);
+ is((cur.ch<176),'gk didn\'t move cursor back (2)');
+ helpers.doKeys('g','j','2','g','j');
+ //should return to the same character-index
+ helpers.doKeys('k');
+ helpers.assertCursorAt(0, 176);
+},{ lineWrapping:true, value: 'This line is intentially long to test movement of gj and gk over wrapped lines. I will start on the end of this line, then make a step up and back to set the origin for j and k.\nThis line is supposed to be even longer than the previous. I will jump here and make another wiggle with gj and gk, before I jump back to the line above. Both wiggles should not change my cursor\'s target character but both j/k and gj/gk change each other\'s reference position.'});
+testVim('gj_gk', function(cm, vim, helpers) {
+ if (phantom) return;
+ cm.setSize(120);
+ // Test top of document edge case.
+ cm.setCursor(0, 4);
+ helpers.doKeys('g', 'j');
+ helpers.doKeys('10', 'g', 'k');
+ helpers.assertCursorAt(0, 4);
+
+ // Test moving down preserves column position.
+ helpers.doKeys('g', 'j');
+ var pos1 = cm.getCursor();
+ var expectedPos2 = { line: 0, ch: (pos1.ch - 4) * 2 + 4};
+ helpers.doKeys('g', 'j');
+ helpers.assertCursorAt(expectedPos2);
+
+ // Move to the last character
+ cm.setCursor(0, 0);
+ // Move left to reset HSPos
+ helpers.doKeys('h');
+ // Test bottom of document edge case.
+ helpers.doKeys('100', 'g', 'j');
+ var endingPos = cm.getCursor();
+ is(endingPos != 0, 'gj should not be on wrapped line 0');
+ var topLeftCharCoords = cm.charCoords(makeCursor(0, 0));
+ var endingCharCoords = cm.charCoords(endingPos);
+ is(topLeftCharCoords.left == endingCharCoords.left, 'gj should end up on column 0');
+},{ lineNumbers: false, lineWrapping:true, value: 'Thislineisintentionallylongtotestmovementofgjandgkoverwrappedlines.' });
+testVim('}', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('}');
+ helpers.assertCursorAt(1, 0);
+ cm.setCursor(0, 0);
+ helpers.doKeys('2', '}');
+ helpers.assertCursorAt(4, 0);
+ cm.setCursor(0, 0);
+ helpers.doKeys('6', '}');
+ helpers.assertCursorAt(5, 0);
+}, { value: 'a\n\nb\nc\n\nd' });
+testVim('{', function(cm, vim, helpers) {
+ cm.setCursor(5, 0);
+ helpers.doKeys('{');
+ helpers.assertCursorAt(4, 0);
+ cm.setCursor(5, 0);
+ helpers.doKeys('2', '{');
+ helpers.assertCursorAt(1, 0);
+ cm.setCursor(5, 0);
+ helpers.doKeys('6', '{');
+ helpers.assertCursorAt(0, 0);
+}, { value: 'a\n\nb\nc\n\nd' });
+testVim('paragraph_motions', function(cm, vim, helpers) {
+ cm.setCursor(10, 0);
+ helpers.doKeys('{');
+ helpers.assertCursorAt(4, 0);
+ helpers.doKeys('{');
+ helpers.assertCursorAt(0, 0);
+ helpers.doKeys('2', '}');
+ helpers.assertCursorAt(7, 0);
+ helpers.doKeys('2', '}');
+ helpers.assertCursorAt(16, 0);
+
+ cm.setCursor(9, 0);
+ helpers.doKeys('}');
+ helpers.assertCursorAt(14, 0);
+
+ cm.setCursor(6, 0);
+ helpers.doKeys('}');
+ helpers.assertCursorAt(7, 0);
+
+ // ip inside empty space
+ cm.setCursor(10, 0);
+ helpers.doKeys('v', 'i', 'p');
+ eqPos(Pos(7, 0), cm.getCursor('anchor'));
+ eqPos(Pos(12, 0), cm.getCursor('head'));
+ helpers.doKeys('i', 'p');
+ eqPos(Pos(7, 0), cm.getCursor('anchor'));
+ eqPos(Pos(13, 1), cm.getCursor('head'));
+ helpers.doKeys('2', 'i', 'p');
+ eqPos(Pos(7, 0), cm.getCursor('anchor'));
+ eqPos(Pos(16, 1), cm.getCursor('head'));
+
+ // should switch to visualLine mode
+ cm.setCursor(14, 0);
+ helpers.doKeys('<Esc>', 'v', 'i', 'p');
+ helpers.assertCursorAt(14, 0);
+
+ cm.setCursor(14, 0);
+ helpers.doKeys('<Esc>', 'V', 'i', 'p');
+ eqPos(Pos(16, 1), cm.getCursor('head'));
+
+ // ap inside empty space
+ cm.setCursor(10, 0);
+ helpers.doKeys('<Esc>', 'v', 'a', 'p');
+ eqPos(Pos(7, 0), cm.getCursor('anchor'));
+ eqPos(Pos(13, 1), cm.getCursor('head'));
+ helpers.doKeys('a', 'p');
+ eqPos(Pos(7, 0), cm.getCursor('anchor'));
+ eqPos(Pos(16, 1), cm.getCursor('head'));
+
+ cm.setCursor(13, 0);
+ helpers.doKeys('v', 'a', 'p');
+ eqPos(Pos(13, 0), cm.getCursor('anchor'));
+ eqPos(Pos(14, 0), cm.getCursor('head'));
+
+ cm.setCursor(16, 0);
+ helpers.doKeys('v', 'a', 'p');
+ eqPos(Pos(14, 0), cm.getCursor('anchor'));
+ eqPos(Pos(16, 1), cm.getCursor('head'));
+
+ cm.setCursor(0, 0);
+ helpers.doKeys('v', 'a', 'p');
+ eqPos(Pos(0, 0), cm.getCursor('anchor'));
+ eqPos(Pos(4, 0), cm.getCursor('head'));
+
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', 'i', 'p');
+ var register = helpers.getRegisterController().getRegister();
+ eq('a\na\n', register.toString());
+ is(register.linewise);
+ helpers.doKeys('3', 'j', 'p');
+ helpers.doKeys('y', 'i', 'p');
+ is(register.linewise);
+ eq('b\na\na\nc\n', register.toString());
+}, { value: 'a\na\n\n\n\nb\nc\n\n\n\n\n\n\nd\n\ne\nf' });
+
+// Operator tests
+testVim('dl', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 0);
+ cm.setCursor(curStart);
+ helpers.doKeys('d', 'l');
+ eq('word1 ', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq(' ', register.toString());
+ is(!register.linewise);
+ eqPos(curStart, cm.getCursor());
+}, { value: ' word1 ' });
+testVim('dl_eol', function(cm, vim, helpers) {
+ cm.setCursor(0, 6);
+ helpers.doKeys('d', 'l');
+ eq(' word1', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq(' ', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 5);
+}, { value: ' word1 ' });
+testVim('dl_repeat', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 0);
+ cm.setCursor(curStart);
+ helpers.doKeys('2', 'd', 'l');
+ eq('ord1 ', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq(' w', register.toString());
+ is(!register.linewise);
+ eqPos(curStart, cm.getCursor());
+}, { value: ' word1 ' });
+testVim('dh', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 3);
+ cm.setCursor(curStart);
+ helpers.doKeys('d', 'h');
+ eq(' wrd1 ', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('o', register.toString());
+ is(!register.linewise);
+ eqPos(offsetCursor(curStart, 0 , -1), cm.getCursor());
+}, { value: ' word1 ' });
+testVim('dj', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 3);
+ cm.setCursor(curStart);
+ helpers.doKeys('d', 'j');
+ eq(' word3', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq(' word1\nword2\n', register.toString());
+ is(register.linewise);
+ helpers.assertCursorAt(0, 1);
+}, { value: ' word1\nword2\n word3' });
+testVim('dj_end_of_document', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 3);
+ cm.setCursor(curStart);
+ helpers.doKeys('d', 'j');
+ eq('', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq(' word1 \n', register.toString());
+ is(register.linewise);
+ helpers.assertCursorAt(0, 0);
+}, { value: ' word1 ' });
+testVim('dk', function(cm, vim, helpers) {
+ var curStart = makeCursor(1, 3);
+ cm.setCursor(curStart);
+ helpers.doKeys('d', 'k');
+ eq(' word3', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq(' word1\nword2\n', register.toString());
+ is(register.linewise);
+ helpers.assertCursorAt(0, 1);
+}, { value: ' word1\nword2\n word3' });
+testVim('dk_start_of_document', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 3);
+ cm.setCursor(curStart);
+ helpers.doKeys('d', 'k');
+ eq('', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq(' word1 \n', register.toString());
+ is(register.linewise);
+ helpers.assertCursorAt(0, 0);
+}, { value: ' word1 ' });
+testVim('dw_space', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 0);
+ cm.setCursor(curStart);
+ helpers.doKeys('d', 'w');
+ eq('word1 ', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq(' ', register.toString());
+ is(!register.linewise);
+ eqPos(curStart, cm.getCursor());
+}, { value: ' word1 ' });
+testVim('dw_word', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 1);
+ cm.setCursor(curStart);
+ helpers.doKeys('d', 'w');
+ eq(' word2', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('word1 ', register.toString());
+ is(!register.linewise);
+ eqPos(curStart, cm.getCursor());
+}, { value: ' word1 word2' });
+testVim('dw_unicode_word', function(cm, vim, helpers) {
+ helpers.doKeys('d', 'w');
+ eq(cm.getValue().length, 10);
+ helpers.doKeys('d', 'w');
+ eq(cm.getValue().length, 6);
+ helpers.doKeys('d', 'w');
+ eq(cm.getValue().length, 5);
+ helpers.doKeys('d', 'e');
+ eq(cm.getValue().length, 2);
+}, { value: ' \u0562\u0561\u0580\u0587\xbbe\xb5g ' });
+testVim('dw_only_word', function(cm, vim, helpers) {
+ // Test that if there is only 1 word left, dw deletes till the end of the
+ // line.
+ cm.setCursor(0, 1);
+ helpers.doKeys('d', 'w');
+ eq(' ', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('word1 ', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 0);
+}, { value: ' word1 ' });
+testVim('dw_eol', function(cm, vim, helpers) {
+ // Assert that dw does not delete the newline if last word to delete is at end
+ // of line.
+ cm.setCursor(0, 1);
+ helpers.doKeys('d', 'w');
+ eq(' \nword2', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('word1', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 0);
+}, { value: ' word1\nword2' });
+testVim('dw_eol_with_multiple_newlines', function(cm, vim, helpers) {
+ // Assert that dw does not delete the newline if last word to delete is at end
+ // of line and it is followed by multiple newlines.
+ cm.setCursor(0, 1);
+ helpers.doKeys('d', 'w');
+ eq(' \n\nword2', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('word1', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 0);
+}, { value: ' word1\n\nword2' });
+testVim('dw_empty_line_followed_by_whitespace', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', 'w');
+ eq(' \nword', cm.getValue());
+}, { value: '\n \nword' });
+testVim('dw_empty_line_followed_by_word', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', 'w');
+ eq('word', cm.getValue());
+}, { value: '\nword' });
+testVim('dw_empty_line_followed_by_empty_line', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', 'w');
+ eq('\n', cm.getValue());
+}, { value: '\n\n' });
+testVim('dw_whitespace_followed_by_whitespace', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', 'w');
+ eq('\n \n', cm.getValue());
+}, { value: ' \n \n' });
+testVim('dw_whitespace_followed_by_empty_line', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', 'w');
+ eq('\n\n', cm.getValue());
+}, { value: ' \n\n' });
+testVim('dw_word_whitespace_word', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', 'w');
+ eq('\n \nword2', cm.getValue());
+}, { value: 'word1\n \nword2'})
+testVim('dw_end_of_document', function(cm, vim, helpers) {
+ cm.setCursor(1, 2);
+ helpers.doKeys('d', 'w');
+ eq('\nab', cm.getValue());
+}, { value: '\nabc' });
+testVim('dw_repeat', function(cm, vim, helpers) {
+ // Assert that dw does delete newline if it should go to the next line, and
+ // that repeat works properly.
+ cm.setCursor(0, 1);
+ helpers.doKeys('d', '2', 'w');
+ eq(' ', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('word1\nword2', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 0);
+}, { value: ' word1\nword2' });
+testVim('de_word_start_and_empty_lines', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', 'e');
+ eq('\n\n', cm.getValue());
+}, { value: 'word\n\n' });
+testVim('de_word_end_and_empty_lines', function(cm, vim, helpers) {
+ cm.setCursor(0, 3);
+ helpers.doKeys('d', 'e');
+ eq('wor', cm.getValue());
+}, { value: 'word\n\n\n' });
+testVim('de_whitespace_and_empty_lines', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', 'e');
+ eq('', cm.getValue());
+}, { value: ' \n\n\n' });
+testVim('de_end_of_document', function(cm, vim, helpers) {
+ cm.setCursor(1, 2);
+ helpers.doKeys('d', 'e');
+ eq('\nab', cm.getValue());
+}, { value: '\nabc' });
+testVim('db_empty_lines', function(cm, vim, helpers) {
+ cm.setCursor(2, 0);
+ helpers.doKeys('d', 'b');
+ eq('\n\n', cm.getValue());
+}, { value: '\n\n\n' });
+testVim('db_word_start_and_empty_lines', function(cm, vim, helpers) {
+ cm.setCursor(2, 0);
+ helpers.doKeys('d', 'b');
+ eq('\nword', cm.getValue());
+}, { value: '\n\nword' });
+testVim('db_word_end_and_empty_lines', function(cm, vim, helpers) {
+ cm.setCursor(2, 3);
+ helpers.doKeys('d', 'b');
+ eq('\n\nd', cm.getValue());
+}, { value: '\n\nword' });
+testVim('db_whitespace_and_empty_lines', function(cm, vim, helpers) {
+ cm.setCursor(2, 0);
+ helpers.doKeys('d', 'b');
+ eq('', cm.getValue());
+}, { value: '\n \n' });
+testVim('db_start_of_document', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', 'b');
+ eq('abc\n', cm.getValue());
+}, { value: 'abc\n' });
+testVim('dge_empty_lines', function(cm, vim, helpers) {
+ cm.setCursor(1, 0);
+ helpers.doKeys('d', 'g', 'e');
+ // Note: In real VIM the result should be '', but it's not quite consistent,
+ // since 2 newlines are deleted. But in the similar case of word\n\n, only
+ // 1 newline is deleted. We'll diverge from VIM's behavior since it's much
+ // easier this way.
+ eq('\n', cm.getValue());
+}, { value: '\n\n' });
+testVim('dge_word_and_empty_lines', function(cm, vim, helpers) {
+ cm.setCursor(1, 0);
+ helpers.doKeys('d', 'g', 'e');
+ eq('wor\n', cm.getValue());
+}, { value: 'word\n\n'});
+testVim('dge_whitespace_and_empty_lines', function(cm, vim, helpers) {
+ cm.setCursor(2, 0);
+ helpers.doKeys('d', 'g', 'e');
+ eq('', cm.getValue());
+}, { value: '\n \n' });
+testVim('dge_start_of_document', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', 'g', 'e');
+ eq('bc\n', cm.getValue());
+}, { value: 'abc\n' });
+testVim('d_inclusive', function(cm, vim, helpers) {
+ // Assert that when inclusive is set, the character the cursor is on gets
+ // deleted too.
+ var curStart = makeCursor(0, 1);
+ cm.setCursor(curStart);
+ helpers.doKeys('d', 'e');
+ eq(' ', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('word1', register.toString());
+ is(!register.linewise);
+ eqPos(curStart, cm.getCursor());
+}, { value: ' word1 ' });
+testVim('d_reverse', function(cm, vim, helpers) {
+ // Test that deleting in reverse works.
+ cm.setCursor(1, 0);
+ helpers.doKeys('d', 'b');
+ eq(' word2 ', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('word1\n', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 1);
+}, { value: ' word1\nword2 ' });
+testVim('dd', function(cm, vim, helpers) {
+ cm.setCursor(0, 3);
+ var expectedBuffer = cm.getRange({ line: 0, ch: 0 },
+ { line: 1, ch: 0 });
+ var expectedLineCount = cm.lineCount() - 1;
+ helpers.doKeys('d', 'd');
+ eq(expectedLineCount, cm.lineCount());
+ var register = helpers.getRegisterController().getRegister();
+ eq(expectedBuffer, register.toString());
+ is(register.linewise);
+ helpers.assertCursorAt(0, lines[1].textStart);
+});
+testVim('dd_prefix_repeat', function(cm, vim, helpers) {
+ cm.setCursor(0, 3);
+ var expectedBuffer = cm.getRange({ line: 0, ch: 0 },
+ { line: 2, ch: 0 });
+ var expectedLineCount = cm.lineCount() - 2;
+ helpers.doKeys('2', 'd', 'd');
+ eq(expectedLineCount, cm.lineCount());
+ var register = helpers.getRegisterController().getRegister();
+ eq(expectedBuffer, register.toString());
+ is(register.linewise);
+ helpers.assertCursorAt(0, lines[2].textStart);
+});
+testVim('dd_motion_repeat', function(cm, vim, helpers) {
+ cm.setCursor(0, 3);
+ var expectedBuffer = cm.getRange({ line: 0, ch: 0 },
+ { line: 2, ch: 0 });
+ var expectedLineCount = cm.lineCount() - 2;
+ helpers.doKeys('d', '2', 'd');
+ eq(expectedLineCount, cm.lineCount());
+ var register = helpers.getRegisterController().getRegister();
+ eq(expectedBuffer, register.toString());
+ is(register.linewise);
+ helpers.assertCursorAt(0, lines[2].textStart);
+});
+testVim('dd_multiply_repeat', function(cm, vim, helpers) {
+ cm.setCursor(0, 3);
+ var expectedBuffer = cm.getRange({ line: 0, ch: 0 },
+ { line: 6, ch: 0 });
+ var expectedLineCount = cm.lineCount() - 6;
+ helpers.doKeys('2', 'd', '3', 'd');
+ eq(expectedLineCount, cm.lineCount());
+ var register = helpers.getRegisterController().getRegister();
+ eq(expectedBuffer, register.toString());
+ is(register.linewise);
+ helpers.assertCursorAt(0, lines[6].textStart);
+});
+testVim('dd_lastline', function(cm, vim, helpers) {
+ cm.setCursor(cm.lineCount(), 0);
+ var expectedLineCount = cm.lineCount() - 1;
+ helpers.doKeys('d', 'd');
+ eq(expectedLineCount, cm.lineCount());
+ helpers.assertCursorAt(cm.lineCount() - 1, 0);
+});
+testVim('dd_only_line', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ var expectedRegister = cm.getValue() + "\n";
+ helpers.doKeys('d','d');
+ eq(1, cm.lineCount());
+ eq('', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq(expectedRegister, register.toString());
+}, { value: "thisistheonlyline" });
+// Yank commands should behave the exact same as d commands, expect that nothing
+// gets deleted.
+testVim('yw_repeat', function(cm, vim, helpers) {
+ // Assert that yw does yank newline if it should go to the next line, and
+ // that repeat works properly.
+ var curStart = makeCursor(0, 1);
+ cm.setCursor(curStart);
+ helpers.doKeys('y', '2', 'w');
+ eq(' word1\nword2', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('word1\nword2', register.toString());
+ is(!register.linewise);
+ eqPos(curStart, cm.getCursor());
+}, { value: ' word1\nword2' });
+testVim('yy_multiply_repeat', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 3);
+ cm.setCursor(curStart);
+ var expectedBuffer = cm.getRange({ line: 0, ch: 0 },
+ { line: 6, ch: 0 });
+ var expectedLineCount = cm.lineCount();
+ helpers.doKeys('2', 'y', '3', 'y');
+ eq(expectedLineCount, cm.lineCount());
+ var register = helpers.getRegisterController().getRegister();
+ eq(expectedBuffer, register.toString());
+ is(register.linewise);
+ eqPos(curStart, cm.getCursor());
+});
+// Change commands behave like d commands except that it also enters insert
+// mode. In addition, when the change is linewise, an additional newline is
+// inserted so that insert mode starts on that line.
+testVim('cw', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('c', '2', 'w');
+ eq(' word3', cm.getValue());
+ helpers.assertCursorAt(0, 0);
+}, { value: 'word1 word2 word3'});
+testVim('cw_repeat', function(cm, vim, helpers) {
+ // Assert that cw does delete newline if it should go to the next line, and
+ // that repeat works properly.
+ var curStart = makeCursor(0, 1);
+ cm.setCursor(curStart);
+ helpers.doKeys('c', '2', 'w');
+ eq(' ', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('word1\nword2', register.toString());
+ is(!register.linewise);
+ eqPos(curStart, cm.getCursor());
+ eq('vim-insert', cm.getOption('keyMap'));
+}, { value: ' word1\nword2' });
+testVim('cc_multiply_repeat', function(cm, vim, helpers) {
+ cm.setCursor(0, 3);
+ var expectedBuffer = cm.getRange({ line: 0, ch: 0 },
+ { line: 6, ch: 0 });
+ var expectedLineCount = cm.lineCount() - 5;
+ helpers.doKeys('2', 'c', '3', 'c');
+ eq(expectedLineCount, cm.lineCount());
+ var register = helpers.getRegisterController().getRegister();
+ eq(expectedBuffer, register.toString());
+ is(register.linewise);
+ eq('vim-insert', cm.getOption('keyMap'));
+});
+testVim('ct', function(cm, vim, helpers) {
+ cm.setCursor(0, 9);
+ helpers.doKeys('c', 't', 'w');
+ eq(' word1 word3', cm.getValue());
+ helpers.doKeys('<Esc>', 'c', '|');
+ eq(' word3', cm.getValue());
+ helpers.assertCursorAt(0, 0);
+ helpers.doKeys('<Esc>', '2', 'u', 'w', 'h');
+ helpers.doKeys('c', '2', 'g', 'e');
+ eq(' wordword3', cm.getValue());
+}, { value: ' word1 word2 word3'});
+testVim('cc_should_not_append_to_document', function(cm, vim, helpers) {
+ var expectedLineCount = cm.lineCount();
+ cm.setCursor(cm.lastLine(), 0);
+ helpers.doKeys('c', 'c');
+ eq(expectedLineCount, cm.lineCount());
+});
+function fillArray(val, times) {
+ var arr = [];
+ for (var i = 0; i < times; i++) {
+ arr.push(val);
+ }
+ return arr;
+}
+testVim('c_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('<C-v>', '2', 'j', 'l', 'l', 'l', 'c');
+ var replacement = fillArray('hello', 3);
+ cm.replaceSelections(replacement);
+ eq('1hello\n5hello\nahellofg', cm.getValue());
+ helpers.doKeys('<Esc>');
+ cm.setCursor(2, 3);
+ helpers.doKeys('<C-v>', '2', 'k', 'h', 'C');
+ replacement = fillArray('world', 3);
+ cm.replaceSelections(replacement);
+ eq('1hworld\n5hworld\nahworld', cm.getValue());
+}, {value: '1234\n5678\nabcdefg'});
+testVim('c_visual_block_replay', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('<C-v>', '2', 'j', 'l', 'c');
+ var replacement = fillArray('fo', 3);
+ cm.replaceSelections(replacement);
+ eq('1fo4\n5fo8\nafodefg', cm.getValue());
+ helpers.doKeys('<Esc>');
+ cm.setCursor(0, 0);
+ helpers.doKeys('.');
+ eq('foo4\nfoo8\nfoodefg', cm.getValue());
+}, {value: '1234\n5678\nabcdefg'});
+
+testVim('d_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('<C-v>', '2', 'j', 'l', 'l', 'l', 'd');
+ eq('1\n5\nafg', cm.getValue());
+}, {value: '1234\n5678\nabcdefg'});
+testVim('D_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('<C-v>', '2', 'j', 'l', 'D');
+ eq('1\n5\na', cm.getValue());
+}, {value: '1234\n5678\nabcdefg'});
+
+testVim('s_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('<C-v>', '2', 'j', 'l', 'l', 'l', 's');
+ var replacement = fillArray('hello{', 3);
+ cm.replaceSelections(replacement);
+ eq('1hello{\n5hello{\nahello{fg\n', cm.getValue());
+ helpers.doKeys('<Esc>');
+ cm.setCursor(2, 3);
+ helpers.doKeys('<C-v>', '1', 'k', 'h', 'S');
+ replacement = fillArray('world', 1);
+ cm.replaceSelections(replacement);
+ eq('1hello{\n world\n', cm.getValue());
+}, {value: '1234\n5678\nabcdefg\n'});
+
+// Swapcase commands edit in place and do not modify registers.
+testVim('g~w_repeat', function(cm, vim, helpers) {
+ // Assert that dw does delete newline if it should go to the next line, and
+ // that repeat works properly.
+ var curStart = makeCursor(0, 1);
+ cm.setCursor(curStart);
+ helpers.doKeys('g', '~', '2', 'w');
+ eq(' WORD1\nWORD2', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('', register.toString());
+ is(!register.linewise);
+ eqPos(curStart, cm.getCursor());
+}, { value: ' word1\nword2' });
+testVim('g~g~', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 3);
+ cm.setCursor(curStart);
+ var expectedLineCount = cm.lineCount();
+ var expectedValue = cm.getValue().toUpperCase();
+ helpers.doKeys('2', 'g', '~', '3', 'g', '~');
+ eq(expectedValue, cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('', register.toString());
+ is(!register.linewise);
+ eqPos(curStart, cm.getCursor());
+}, { value: ' word1\nword2\nword3\nword4\nword5\nword6' });
+testVim('gu_and_gU', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 7);
+ var value = cm.getValue();
+ cm.setCursor(curStart);
+ helpers.doKeys('2', 'g', 'U', 'w');
+ eq(cm.getValue(), 'wa wb xX WC wd');
+ eqPos(curStart, cm.getCursor());
+ helpers.doKeys('2', 'g', 'u', 'w');
+ eq(cm.getValue(), value);
+
+ helpers.doKeys('2', 'g', 'U', 'B');
+ eq(cm.getValue(), 'wa WB Xx wc wd');
+ eqPos(makeCursor(0, 3), cm.getCursor());
+
+ cm.setCursor(makeCursor(0, 4));
+ helpers.doKeys('g', 'u', 'i', 'w');
+ eq(cm.getValue(), 'wa wb Xx wc wd');
+ eqPos(makeCursor(0, 3), cm.getCursor());
+
+ // TODO: support gUgU guu
+ // eqPos(makeCursor(0, 0), cm.getCursor());
+
+ var register = helpers.getRegisterController().getRegister();
+ eq('', register.toString());
+ is(!register.linewise);
+}, { value: 'wa wb xx wc wd' });
+testVim('visual_block_~', function(cm, vim, helpers) {
+ cm.setCursor(1, 1);
+ helpers.doKeys('<C-v>', 'l', 'l', 'j', '~');
+ helpers.assertCursorAt(1, 1);
+ eq('hello\nwoRLd\naBCDe', cm.getValue());
+ cm.setCursor(2, 0);
+ helpers.doKeys('v', 'l', 'l', '~');
+ helpers.assertCursorAt(2, 0);
+ eq('hello\nwoRLd\nAbcDe', cm.getValue());
+},{value: 'hello\nwOrld\nabcde' });
+testVim('._swapCase_visualBlock', function(cm, vim, helpers) {
+ helpers.doKeys('<C-v>', 'j', 'j', 'l', '~');
+ cm.setCursor(0, 3);
+ helpers.doKeys('.');
+ eq('HelLO\nWorLd\nAbcdE', cm.getValue());
+},{value: 'hEllo\nwOrlD\naBcDe' });
+testVim('._delete_visualBlock', function(cm, vim, helpers) {
+ helpers.doKeys('<C-v>', 'j', 'x');
+ eq('ive\ne\nsome\nsugar', cm.getValue());
+ helpers.doKeys('.');
+ eq('ve\n\nsome\nsugar', cm.getValue());
+ helpers.doKeys('j', 'j', '.');
+ eq('ve\n\nome\nugar', cm.getValue());
+ helpers.doKeys('u', '<C-r>', '.');
+ eq('ve\n\nme\ngar', cm.getValue());
+},{value: 'give\nme\nsome\nsugar' });
+testVim('>{motion}', function(cm, vim, helpers) {
+ cm.setCursor(1, 3);
+ var expectedLineCount = cm.lineCount();
+ var expectedValue = ' word1\n word2\nword3 ';
+ helpers.doKeys('>', 'k');
+ eq(expectedValue, cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 3);
+}, { value: ' word1\nword2\nword3 ', indentUnit: 2 });
+testVim('>>', function(cm, vim, helpers) {
+ cm.setCursor(0, 3);
+ var expectedLineCount = cm.lineCount();
+ var expectedValue = ' word1\n word2\nword3 ';
+ helpers.doKeys('2', '>', '>');
+ eq(expectedValue, cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 3);
+}, { value: ' word1\nword2\nword3 ', indentUnit: 2 });
+testVim('<{motion}', function(cm, vim, helpers) {
+ cm.setCursor(1, 3);
+ var expectedLineCount = cm.lineCount();
+ var expectedValue = ' word1\nword2\nword3 ';
+ helpers.doKeys('<', 'k');
+ eq(expectedValue, cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 1);
+}, { value: ' word1\n word2\nword3 ', indentUnit: 2 });
+testVim('<<', function(cm, vim, helpers) {
+ cm.setCursor(0, 3);
+ var expectedLineCount = cm.lineCount();
+ var expectedValue = ' word1\nword2\nword3 ';
+ helpers.doKeys('2', '<', '<');
+ eq(expectedValue, cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 1);
+}, { value: ' word1\n word2\nword3 ', indentUnit: 2 });
+
+// Edit tests
+function testEdit(name, before, pos, edit, after) {
+ return testVim(name, function(cm, vim, helpers) {
+ var ch = before.search(pos)
+ var line = before.substring(0, ch).split('\n').length - 1;
+ if (line) {
+ ch = before.substring(0, ch).split('\n').pop().length;
+ }
+ cm.setCursor(line, ch);
+ helpers.doKeys.apply(this, edit.split(''));
+ eq(after, cm.getValue());
+ }, {value: before});
+}
+
+// These Delete tests effectively cover word-wise Change, Visual & Yank.
+// Tabs are used as differentiated whitespace to catch edge cases.
+// Normal word:
+testEdit('diw_mid_spc', 'foo \tbAr\t baz', /A/, 'diw', 'foo \t\t baz');
+testEdit('daw_mid_spc', 'foo \tbAr\t baz', /A/, 'daw', 'foo \tbaz');
+testEdit('diw_mid_punct', 'foo \tbAr.\t baz', /A/, 'diw', 'foo \t.\t baz');
+testEdit('daw_mid_punct', 'foo \tbAr.\t baz', /A/, 'daw', 'foo.\t baz');
+testEdit('diw_mid_punct2', 'foo \t,bAr.\t baz', /A/, 'diw', 'foo \t,.\t baz');
+testEdit('daw_mid_punct2', 'foo \t,bAr.\t baz', /A/, 'daw', 'foo \t,.\t baz');
+testEdit('diw_start_spc', 'bAr \tbaz', /A/, 'diw', ' \tbaz');
+testEdit('daw_start_spc', 'bAr \tbaz', /A/, 'daw', 'baz');
+testEdit('diw_start_punct', 'bAr. \tbaz', /A/, 'diw', '. \tbaz');
+testEdit('daw_start_punct', 'bAr. \tbaz', /A/, 'daw', '. \tbaz');
+testEdit('diw_end_spc', 'foo \tbAr', /A/, 'diw', 'foo \t');
+testEdit('daw_end_spc', 'foo \tbAr', /A/, 'daw', 'foo');
+testEdit('diw_end_punct', 'foo \tbAr.', /A/, 'diw', 'foo \t.');
+testEdit('daw_end_punct', 'foo \tbAr.', /A/, 'daw', 'foo.');
+// Big word:
+testEdit('diW_mid_spc', 'foo \tbAr\t baz', /A/, 'diW', 'foo \t\t baz');
+testEdit('daW_mid_spc', 'foo \tbAr\t baz', /A/, 'daW', 'foo \tbaz');
+testEdit('diW_mid_punct', 'foo \tbAr.\t baz', /A/, 'diW', 'foo \t\t baz');
+testEdit('daW_mid_punct', 'foo \tbAr.\t baz', /A/, 'daW', 'foo \tbaz');
+testEdit('diW_mid_punct2', 'foo \t,bAr.\t baz', /A/, 'diW', 'foo \t\t baz');
+testEdit('daW_mid_punct2', 'foo \t,bAr.\t baz', /A/, 'daW', 'foo \tbaz');
+testEdit('diW_start_spc', 'bAr\t baz', /A/, 'diW', '\t baz');
+testEdit('daW_start_spc', 'bAr\t baz', /A/, 'daW', 'baz');
+testEdit('diW_start_punct', 'bAr.\t baz', /A/, 'diW', '\t baz');
+testEdit('daW_start_punct', 'bAr.\t baz', /A/, 'daW', 'baz');
+testEdit('diW_end_spc', 'foo \tbAr', /A/, 'diW', 'foo \t');
+testEdit('daW_end_spc', 'foo \tbAr', /A/, 'daW', 'foo');
+testEdit('diW_end_punct', 'foo \tbAr.', /A/, 'diW', 'foo \t');
+testEdit('daW_end_punct', 'foo \tbAr.', /A/, 'daW', 'foo');
+// Deleting text objects
+// Open and close on same line
+testEdit('di(_open_spc', 'foo (bAr) baz', /\(/, 'di(', 'foo () baz');
+testEdit('di)_open_spc', 'foo (bAr) baz', /\(/, 'di)', 'foo () baz');
+testEdit('dib_open_spc', 'foo (bAr) baz', /\(/, 'dib', 'foo () baz');
+testEdit('da(_open_spc', 'foo (bAr) baz', /\(/, 'da(', 'foo baz');
+testEdit('da)_open_spc', 'foo (bAr) baz', /\(/, 'da)', 'foo baz');
+
+testEdit('di(_middle_spc', 'foo (bAr) baz', /A/, 'di(', 'foo () baz');
+testEdit('di)_middle_spc', 'foo (bAr) baz', /A/, 'di)', 'foo () baz');
+testEdit('da(_middle_spc', 'foo (bAr) baz', /A/, 'da(', 'foo baz');
+testEdit('da)_middle_spc', 'foo (bAr) baz', /A/, 'da)', 'foo baz');
+
+testEdit('di(_close_spc', 'foo (bAr) baz', /\)/, 'di(', 'foo () baz');
+testEdit('di)_close_spc', 'foo (bAr) baz', /\)/, 'di)', 'foo () baz');
+testEdit('da(_close_spc', 'foo (bAr) baz', /\)/, 'da(', 'foo baz');
+testEdit('da)_close_spc', 'foo (bAr) baz', /\)/, 'da)', 'foo baz');
+
+// delete around and inner b.
+testEdit('dab_on_(_should_delete_around_()block', 'o( in(abc) )', /\(a/, 'dab', 'o( in )');
+
+// delete around and inner B.
+testEdit('daB_on_{_should_delete_around_{}block', 'o{ in{abc} }', /{a/, 'daB', 'o{ in }');
+testEdit('diB_on_{_should_delete_inner_{}block', 'o{ in{abc} }', /{a/, 'diB', 'o{ in{} }');
+
+testEdit('da{_on_{_should_delete_inner_block', 'o{ in{abc} }', /{a/, 'da{', 'o{ in }');
+testEdit('di[_on_(_should_not_delete', 'foo (bAr) baz', /\(/, 'di[', 'foo (bAr) baz');
+testEdit('di[_on_)_should_not_delete', 'foo (bAr) baz', /\)/, 'di[', 'foo (bAr) baz');
+testEdit('da[_on_(_should_not_delete', 'foo (bAr) baz', /\(/, 'da[', 'foo (bAr) baz');
+testEdit('da[_on_)_should_not_delete', 'foo (bAr) baz', /\)/, 'da[', 'foo (bAr) baz');
+testMotion('di(_outside_should_stay', ['d', 'i', '('], { line: 0, ch: 0}, { line: 0, ch: 0});
+
+// Open and close on different lines, equally indented
+testEdit('di{_middle_spc', 'a{\n\tbar\n}b', /r/, 'di{', 'a{}b');
+testEdit('di}_middle_spc', 'a{\n\tbar\n}b', /r/, 'di}', 'a{}b');
+testEdit('da{_middle_spc', 'a{\n\tbar\n}b', /r/, 'da{', 'ab');
+testEdit('da}_middle_spc', 'a{\n\tbar\n}b', /r/, 'da}', 'ab');
+testEdit('daB_middle_spc', 'a{\n\tbar\n}b', /r/, 'daB', 'ab');
+
+// open and close on diff lines, open indented less than close
+testEdit('di{_middle_spc', 'a{\n\tbar\n\t}b', /r/, 'di{', 'a{}b');
+testEdit('di}_middle_spc', 'a{\n\tbar\n\t}b', /r/, 'di}', 'a{}b');
+testEdit('da{_middle_spc', 'a{\n\tbar\n\t}b', /r/, 'da{', 'ab');
+testEdit('da}_middle_spc', 'a{\n\tbar\n\t}b', /r/, 'da}', 'ab');
+
+// open and close on diff lines, open indented more than close
+testEdit('di[_middle_spc', 'a\t[\n\tbar\n]b', /r/, 'di[', 'a\t[]b');
+testEdit('di]_middle_spc', 'a\t[\n\tbar\n]b', /r/, 'di]', 'a\t[]b');
+testEdit('da[_middle_spc', 'a\t[\n\tbar\n]b', /r/, 'da[', 'a\tb');
+testEdit('da]_middle_spc', 'a\t[\n\tbar\n]b', /r/, 'da]', 'a\tb');
+
+function testSelection(name, before, pos, keys, sel) {
+ return testVim(name, function(cm, vim, helpers) {
+ var ch = before.search(pos)
+ var line = before.substring(0, ch).split('\n').length - 1;
+ if (line) {
+ ch = before.substring(0, ch).split('\n').pop().length;
+ }
+ cm.setCursor(line, ch);
+ helpers.doKeys.apply(this, keys.split(''));
+ eq(sel, cm.getSelection());
+ }, {value: before});
+}
+testSelection('viw_middle_spc', 'foo \tbAr\t baz', /A/, 'viw', 'bAr');
+testSelection('vaw_middle_spc', 'foo \tbAr\t baz', /A/, 'vaw', 'bAr\t ');
+testSelection('viw_middle_punct', 'foo \tbAr,\t baz', /A/, 'viw', 'bAr');
+testSelection('vaW_middle_punct', 'foo \tbAr,\t baz', /A/, 'vaW', 'bAr,\t ');
+testSelection('viw_start_spc', 'foo \tbAr\t baz', /b/, 'viw', 'bAr');
+testSelection('viw_end_spc', 'foo \tbAr\t baz', /r/, 'viw', 'bAr');
+testSelection('viw_eol', 'foo \tbAr', /r/, 'viw', 'bAr');
+testSelection('vi{_middle_spc', 'a{\n\tbar\n\t}b', /r/, 'vi{', '\n\tbar\n\t');
+testSelection('va{_middle_spc', 'a{\n\tbar\n\t}b', /r/, 'va{', '{\n\tbar\n\t}');
+
+testVim('mouse_select', function(cm, vim, helpers) {
+ cm.setSelection(Pos(0, 2), Pos(0, 4), {origin: '*mouse'});
+ is(cm.state.vim.visualMode);
+ is(!cm.state.vim.visualLine);
+ is(!cm.state.vim.visualBlock);
+ helpers.doKeys('<Esc>');
+ is(!cm.somethingSelected());
+ helpers.doKeys('g', 'v');
+ eq('cd', cm.getSelection());
+}, {value: 'abcdef'});
+
+// Operator-motion tests
+testVim('D', function(cm, vim, helpers) {
+ cm.setCursor(0, 3);
+ helpers.doKeys('D');
+ eq(' wo\nword2\n word3', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('rd1', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 2);
+}, { value: ' word1\nword2\n word3' });
+testVim('C', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 3);
+ cm.setCursor(curStart);
+ helpers.doKeys('C');
+ eq(' wo\nword2\n word3', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('rd1', register.toString());
+ is(!register.linewise);
+ eqPos(curStart, cm.getCursor());
+ eq('vim-insert', cm.getOption('keyMap'));
+}, { value: ' word1\nword2\n word3' });
+testVim('Y', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 3);
+ cm.setCursor(curStart);
+ helpers.doKeys('Y');
+ eq(' word1\nword2\n word3', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('rd1', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 3);
+}, { value: ' word1\nword2\n word3' });
+testVim('~', function(cm, vim, helpers) {
+ helpers.doKeys('3', '~');
+ eq('ABCdefg', cm.getValue());
+ helpers.assertCursorAt(0, 3);
+}, { value: 'abcdefg' });
+
+// Action tests
+testVim('ctrl-a', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('<C-a>');
+ eq('-9', cm.getValue());
+ helpers.assertCursorAt(0, 1);
+ helpers.doKeys('2','<C-a>');
+ eq('-7', cm.getValue());
+}, {value: '-10'});
+testVim('ctrl-x', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('<C-x>');
+ eq('-1', cm.getValue());
+ helpers.assertCursorAt(0, 1);
+ helpers.doKeys('2','<C-x>');
+ eq('-3', cm.getValue());
+}, {value: '0'});
+testVim('<C-x>/<C-a> search forward', function(cm, vim, helpers) {
+ forEach(['<C-x>', '<C-a>'], function(key) {
+ cm.setCursor(0, 0);
+ helpers.doKeys(key);
+ helpers.assertCursorAt(0, 5);
+ helpers.doKeys('l');
+ helpers.doKeys(key);
+ helpers.assertCursorAt(0, 10);
+ cm.setCursor(0, 11);
+ helpers.doKeys(key);
+ helpers.assertCursorAt(0, 11);
+ });
+}, {value: '__jmp1 jmp2 jmp'});
+testVim('a', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('a');
+ helpers.assertCursorAt(0, 2);
+ eq('vim-insert', cm.getOption('keyMap'));
+});
+testVim('a_eol', function(cm, vim, helpers) {
+ cm.setCursor(0, lines[0].length - 1);
+ helpers.doKeys('a');
+ helpers.assertCursorAt(0, lines[0].length);
+ eq('vim-insert', cm.getOption('keyMap'));
+});
+testVim('A_endOfSelectedArea', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('v', 'j', 'l');
+ helpers.doKeys('A');
+ helpers.assertCursorAt(1, 2);
+ eq('vim-insert', cm.getOption('keyMap'));
+}, {value: 'foo\nbar'});
+testVim('i', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('i');
+ helpers.assertCursorAt(0, 1);
+ eq('vim-insert', cm.getOption('keyMap'));
+});
+testVim('i_repeat', function(cm, vim, helpers) {
+ helpers.doKeys('3', 'i');
+ cm.replaceRange('test', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ eq('testtesttest', cm.getValue());
+ helpers.assertCursorAt(0, 11);
+}, { value: '' });
+testVim('i_repeat_delete', function(cm, vim, helpers) {
+ cm.setCursor(0, 4);
+ helpers.doKeys('2', 'i');
+ cm.replaceRange('z', cm.getCursor());
+ helpers.doInsertModeKeys('Backspace', 'Backspace');
+ helpers.doKeys('<Esc>');
+ eq('abe', cm.getValue());
+ helpers.assertCursorAt(0, 1);
+}, { value: 'abcde' });
+testVim('A', function(cm, vim, helpers) {
+ helpers.doKeys('A');
+ helpers.assertCursorAt(0, lines[0].length);
+ eq('vim-insert', cm.getOption('keyMap'));
+});
+testVim('A_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('<C-v>', '2', 'j', 'l', 'l', 'A');
+ var replacement = new Array(cm.listSelections().length+1).join('hello ').split(' ');
+ replacement.pop();
+ cm.replaceSelections(replacement);
+ eq('testhello\nmehello\npleahellose', cm.getValue());
+ helpers.doKeys('<Esc>');
+ cm.setCursor(0, 0);
+ helpers.doKeys('.');
+ // TODO this doesn't work yet
+ // eq('teshellothello\nme hello hello\nplehelloahellose', cm.getValue());
+}, {value: 'test\nme\nplease'});
+testVim('I', function(cm, vim, helpers) {
+ cm.setCursor(0, 4);
+ helpers.doKeys('I');
+ helpers.assertCursorAt(0, lines[0].textStart);
+ eq('vim-insert', cm.getOption('keyMap'));
+});
+testVim('I_repeat', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('3', 'I');
+ cm.replaceRange('test', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ eq('testtesttestblah', cm.getValue());
+ helpers.assertCursorAt(0, 11);
+}, { value: 'blah' });
+testVim('I_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('<C-v>', '2', 'j', 'l', 'l', 'I');
+ var replacement = new Array(cm.listSelections().length+1).join('hello ').split(' ');
+ replacement.pop();
+ cm.replaceSelections(replacement);
+ eq('hellotest\nhellome\nhelloplease', cm.getValue());
+}, {value: 'test\nme\nplease'});
+testVim('o', function(cm, vim, helpers) {
+ cm.setCursor(0, 4);
+ helpers.doKeys('o');
+ eq('word1\n\nword2', cm.getValue());
+ helpers.assertCursorAt(1, 0);
+ eq('vim-insert', cm.getOption('keyMap'));
+}, { value: 'word1\nword2' });
+testVim('o_repeat', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('3', 'o');
+ cm.replaceRange('test', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ eq('\ntest\ntest\ntest', cm.getValue());
+ helpers.assertCursorAt(3, 3);
+}, { value: '' });
+testVim('O', function(cm, vim, helpers) {
+ cm.setCursor(0, 4);
+ helpers.doKeys('O');
+ eq('\nword1\nword2', cm.getValue());
+ helpers.assertCursorAt(0, 0);
+ eq('vim-insert', cm.getOption('keyMap'));
+}, { value: 'word1\nword2' });
+testVim('J', function(cm, vim, helpers) {
+ cm.setCursor(0, 4);
+ helpers.doKeys('J');
+ var expectedValue = 'word1 word2\nword3\n word4';
+ eq(expectedValue, cm.getValue());
+ helpers.assertCursorAt(0, expectedValue.indexOf('word2') - 1);
+}, { value: 'word1 \n word2\nword3\n word4' });
+testVim('J_repeat', function(cm, vim, helpers) {
+ cm.setCursor(0, 4);
+ helpers.doKeys('3', 'J');
+ var expectedValue = 'word1 word2 word3\n word4';
+ eq(expectedValue, cm.getValue());
+ helpers.assertCursorAt(0, expectedValue.indexOf('word3') - 1);
+}, { value: 'word1 \n word2\nword3\n word4' });
+testVim('p', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.getRegisterController().pushText('"', 'yank', 'abc\ndef', false);
+ helpers.doKeys('p');
+ eq('__abc\ndef_', cm.getValue());
+ helpers.assertCursorAt(1, 2);
+}, { value: '___' });
+testVim('p_register', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.getRegisterController().getRegister('a').setText('abc\ndef', false);
+ helpers.doKeys('"', 'a', 'p');
+ eq('__abc\ndef_', cm.getValue());
+ helpers.assertCursorAt(1, 2);
+}, { value: '___' });
+testVim('p_wrong_register', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.getRegisterController().getRegister('a').setText('abc\ndef', false);
+ helpers.doKeys('p');
+ eq('___', cm.getValue());
+ helpers.assertCursorAt(0, 1);
+}, { value: '___' });
+testVim('p_line', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.getRegisterController().pushText('"', 'yank', ' a\nd\n', true);
+ helpers.doKeys('2', 'p');
+ eq('___\n a\nd\n a\nd', cm.getValue());
+ helpers.assertCursorAt(1, 2);
+}, { value: '___' });
+testVim('p_lastline', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.getRegisterController().pushText('"', 'yank', ' a\nd', true);
+ helpers.doKeys('2', 'p');
+ eq('___\n a\nd\n a\nd', cm.getValue());
+ helpers.assertCursorAt(1, 2);
+}, { value: '___' });
+testVim(']p_first_indent_is_smaller', function(cm, vim, helpers) {
+ helpers.getRegisterController().pushText('"', 'yank', ' abc\n def\n', true);
+ helpers.doKeys(']', 'p');
+ eq(' ___\n abc\n def', cm.getValue());
+}, { value: ' ___' });
+testVim(']p_first_indent_is_larger', function(cm, vim, helpers) {
+ helpers.getRegisterController().pushText('"', 'yank', ' abc\n def\n', true);
+ helpers.doKeys(']', 'p');
+ eq(' ___\n abc\ndef', cm.getValue());
+}, { value: ' ___' });
+testVim(']p_with_tab_indents', function(cm, vim, helpers) {
+ helpers.getRegisterController().pushText('"', 'yank', '\t\tabc\n\t\t\tdef\n', true);
+ helpers.doKeys(']', 'p');
+ eq('\t___\n\tabc\n\t\tdef', cm.getValue());
+}, { value: '\t___', indentWithTabs: true});
+testVim(']p_with_spaces_translated_to_tabs', function(cm, vim, helpers) {
+ helpers.getRegisterController().pushText('"', 'yank', ' abc\n def\n', true);
+ helpers.doKeys(']', 'p');
+ eq('\t___\n\tabc\n\t\tdef', cm.getValue());
+}, { value: '\t___', indentWithTabs: true, tabSize: 2 });
+testVim('[p', function(cm, vim, helpers) {
+ helpers.getRegisterController().pushText('"', 'yank', ' abc\n def\n', true);
+ helpers.doKeys('[', 'p');
+ eq(' abc\n def\n ___', cm.getValue());
+}, { value: ' ___' });
+testVim('P', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.getRegisterController().pushText('"', 'yank', 'abc\ndef', false);
+ helpers.doKeys('P');
+ eq('_abc\ndef__', cm.getValue());
+ helpers.assertCursorAt(1, 3);
+}, { value: '___' });
+testVim('P_line', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.getRegisterController().pushText('"', 'yank', ' a\nd\n', true);
+ helpers.doKeys('2', 'P');
+ eq(' a\nd\n a\nd\n___', cm.getValue());
+ helpers.assertCursorAt(0, 2);
+}, { value: '___' });
+testVim('r', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('3', 'r', 'u');
+ eq('wuuuet\nanother', cm.getValue(),'3r failed');
+ helpers.assertCursorAt(0, 3);
+ cm.setCursor(0, 4);
+ helpers.doKeys('v', 'j', 'h', 'r', '<Space>');
+ eq('wuuu \n her', cm.getValue(),'Replacing selection by space-characters failed');
+}, { value: 'wordet\nanother' });
+testVim('r_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(2, 3);
+ helpers.doKeys('<C-v>', 'k', 'k', 'h', 'h', 'r', 'l');
+ eq('1lll\n5lll\nalllefg', cm.getValue());
+ helpers.doKeys('<C-v>', 'l', 'j', 'r', '<Space>');
+ eq('1 l\n5 l\nalllefg', cm.getValue());
+ cm.setCursor(2, 0);
+ helpers.doKeys('o');
+ helpers.doKeys('<Esc>');
+ cm.replaceRange('\t\t', cm.getCursor());
+ helpers.doKeys('<C-v>', 'h', 'h', 'r', 'r');
+ eq('1 l\n5 l\nalllefg\nrrrrrrrr', cm.getValue());
+}, {value: '1234\n5678\nabcdefg'});
+testVim('R', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('R');
+ helpers.assertCursorAt(0, 1);
+ eq('vim-replace', cm.getOption('keyMap'));
+ is(cm.state.overwrite, 'Setting overwrite state failed');
+});
+testVim('mark', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 't');
+ cm.setCursor(0, 0);
+ helpers.doKeys('`', 't');
+ helpers.assertCursorAt(2, 2);
+ cm.setCursor(2, 0);
+ cm.replaceRange(' h', cm.getCursor());
+ cm.setCursor(0, 0);
+ helpers.doKeys('\'', 't');
+ helpers.assertCursorAt(2, 3);
+});
+testVim('jumpToMark_next', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 't');
+ cm.setCursor(0, 0);
+ helpers.doKeys(']', '`');
+ helpers.assertCursorAt(2, 2);
+ cm.setCursor(0, 0);
+ helpers.doKeys(']', '\'');
+ helpers.assertCursorAt(2, 0);
+});
+testVim('jumpToMark_next_repeat', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(3, 2);
+ helpers.doKeys('m', 'b');
+ cm.setCursor(4, 2);
+ helpers.doKeys('m', 'c');
+ cm.setCursor(0, 0);
+ helpers.doKeys('2', ']', '`');
+ helpers.assertCursorAt(3, 2);
+ cm.setCursor(0, 0);
+ helpers.doKeys('2', ']', '\'');
+ helpers.assertCursorAt(3, 1);
+});
+testVim('jumpToMark_next_sameline', function(cm, vim, helpers) {
+ cm.setCursor(2, 0);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(2, 4);
+ helpers.doKeys('m', 'b');
+ cm.setCursor(2, 2);
+ helpers.doKeys(']', '`');
+ helpers.assertCursorAt(2, 4);
+});
+testVim('jumpToMark_next_onlyprev', function(cm, vim, helpers) {
+ cm.setCursor(2, 0);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(4, 0);
+ helpers.doKeys(']', '`');
+ helpers.assertCursorAt(4, 0);
+});
+testVim('jumpToMark_next_nomark', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys(']', '`');
+ helpers.assertCursorAt(2, 2);
+ helpers.doKeys(']', '\'');
+ helpers.assertCursorAt(2, 0);
+});
+testVim('jumpToMark_next_linewise_over', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(3, 4);
+ helpers.doKeys('m', 'b');
+ cm.setCursor(2, 1);
+ helpers.doKeys(']', '\'');
+ helpers.assertCursorAt(3, 1);
+});
+testVim('jumpToMark_next_action', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 't');
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', ']', '`');
+ helpers.assertCursorAt(0, 0);
+ var actual = cm.getLine(0);
+ var expected = 'pop pop 0 1 2 3 4';
+ eq(actual, expected, "Deleting while jumping to the next mark failed.");
+});
+testVim('jumpToMark_next_line_action', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 't');
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', ']', '\'');
+ helpers.assertCursorAt(0, 1);
+ var actual = cm.getLine(0);
+ var expected = ' (a) [b] {c} '
+ eq(actual, expected, "Deleting while jumping to the next mark line failed.");
+});
+testVim('jumpToMark_prev', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 't');
+ cm.setCursor(4, 0);
+ helpers.doKeys('[', '`');
+ helpers.assertCursorAt(2, 2);
+ cm.setCursor(4, 0);
+ helpers.doKeys('[', '\'');
+ helpers.assertCursorAt(2, 0);
+});
+testVim('jumpToMark_prev_repeat', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(3, 2);
+ helpers.doKeys('m', 'b');
+ cm.setCursor(4, 2);
+ helpers.doKeys('m', 'c');
+ cm.setCursor(5, 0);
+ helpers.doKeys('2', '[', '`');
+ helpers.assertCursorAt(3, 2);
+ cm.setCursor(5, 0);
+ helpers.doKeys('2', '[', '\'');
+ helpers.assertCursorAt(3, 1);
+});
+testVim('jumpToMark_prev_sameline', function(cm, vim, helpers) {
+ cm.setCursor(2, 0);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(2, 4);
+ helpers.doKeys('m', 'b');
+ cm.setCursor(2, 2);
+ helpers.doKeys('[', '`');
+ helpers.assertCursorAt(2, 0);
+});
+testVim('jumpToMark_prev_onlynext', function(cm, vim, helpers) {
+ cm.setCursor(4, 4);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(2, 0);
+ helpers.doKeys('[', '`');
+ helpers.assertCursorAt(2, 0);
+});
+testVim('jumpToMark_prev_nomark', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys('[', '`');
+ helpers.assertCursorAt(2, 2);
+ helpers.doKeys('[', '\'');
+ helpers.assertCursorAt(2, 0);
+});
+testVim('jumpToMark_prev_linewise_over', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(3, 4);
+ helpers.doKeys('m', 'b');
+ cm.setCursor(3, 6);
+ helpers.doKeys('[', '\'');
+ helpers.assertCursorAt(2, 0);
+});
+testVim('delmark_single', function(cm, vim, helpers) {
+ cm.setCursor(1, 2);
+ helpers.doKeys('m', 't');
+ helpers.doEx('delmarks t');
+ cm.setCursor(0, 0);
+ helpers.doKeys('`', 't');
+ helpers.assertCursorAt(0, 0);
+});
+testVim('delmark_range', function(cm, vim, helpers) {
+ cm.setCursor(1, 2);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 'b');
+ cm.setCursor(3, 2);
+ helpers.doKeys('m', 'c');
+ cm.setCursor(4, 2);
+ helpers.doKeys('m', 'd');
+ cm.setCursor(5, 2);
+ helpers.doKeys('m', 'e');
+ helpers.doEx('delmarks b-d');
+ cm.setCursor(0, 0);
+ helpers.doKeys('`', 'a');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'b');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'c');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'd');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'e');
+ helpers.assertCursorAt(5, 2);
+});
+testVim('delmark_multi', function(cm, vim, helpers) {
+ cm.setCursor(1, 2);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 'b');
+ cm.setCursor(3, 2);
+ helpers.doKeys('m', 'c');
+ cm.setCursor(4, 2);
+ helpers.doKeys('m', 'd');
+ cm.setCursor(5, 2);
+ helpers.doKeys('m', 'e');
+ helpers.doEx('delmarks bcd');
+ cm.setCursor(0, 0);
+ helpers.doKeys('`', 'a');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'b');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'c');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'd');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'e');
+ helpers.assertCursorAt(5, 2);
+});
+testVim('delmark_multi_space', function(cm, vim, helpers) {
+ cm.setCursor(1, 2);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 'b');
+ cm.setCursor(3, 2);
+ helpers.doKeys('m', 'c');
+ cm.setCursor(4, 2);
+ helpers.doKeys('m', 'd');
+ cm.setCursor(5, 2);
+ helpers.doKeys('m', 'e');
+ helpers.doEx('delmarks b c d');
+ cm.setCursor(0, 0);
+ helpers.doKeys('`', 'a');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'b');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'c');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'd');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'e');
+ helpers.assertCursorAt(5, 2);
+});
+testVim('delmark_all', function(cm, vim, helpers) {
+ cm.setCursor(1, 2);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 'b');
+ cm.setCursor(3, 2);
+ helpers.doKeys('m', 'c');
+ cm.setCursor(4, 2);
+ helpers.doKeys('m', 'd');
+ cm.setCursor(5, 2);
+ helpers.doKeys('m', 'e');
+ helpers.doEx('delmarks a b-de');
+ cm.setCursor(0, 0);
+ helpers.doKeys('`', 'a');
+ helpers.assertCursorAt(0, 0);
+ helpers.doKeys('`', 'b');
+ helpers.assertCursorAt(0, 0);
+ helpers.doKeys('`', 'c');
+ helpers.assertCursorAt(0, 0);
+ helpers.doKeys('`', 'd');
+ helpers.assertCursorAt(0, 0);
+ helpers.doKeys('`', 'e');
+ helpers.assertCursorAt(0, 0);
+});
+testVim('visual', function(cm, vim, helpers) {
+ helpers.doKeys('l', 'v', 'l', 'l');
+ helpers.assertCursorAt(0, 4);
+ eqPos(makeCursor(0, 1), cm.getCursor('anchor'));
+ helpers.doKeys('d');
+ eq('15', cm.getValue());
+}, { value: '12345' });
+testVim('visual_yank', function(cm, vim, helpers) {
+ helpers.doKeys('v', '3', 'l', 'y');
+ helpers.assertCursorAt(0, 0);
+ helpers.doKeys('p');
+ eq('aa te test for yank', cm.getValue());
+}, { value: 'a test for yank' })
+testVim('visual_w', function(cm, vim, helpers) {
+ helpers.doKeys('v', 'w');
+ eq(cm.getSelection(), 'motion t');
+}, { value: 'motion test'});
+testVim('visual_initial_selection', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('v');
+ cm.getSelection('n');
+}, { value: 'init'});
+testVim('visual_crossover_left', function(cm, vim, helpers) {
+ cm.setCursor(0, 2);
+ helpers.doKeys('v', 'l', 'h', 'h');
+ cm.getSelection('ro');
+}, { value: 'cross'});
+testVim('visual_crossover_left', function(cm, vim, helpers) {
+ cm.setCursor(0, 2);
+ helpers.doKeys('v', 'h', 'l', 'l');
+ cm.getSelection('os');
+}, { value: 'cross'});
+testVim('visual_crossover_up', function(cm, vim, helpers) {
+ cm.setCursor(3, 2);
+ helpers.doKeys('v', 'j', 'k', 'k');
+ eqPos(Pos(2, 2), cm.getCursor('head'));
+ eqPos(Pos(3, 3), cm.getCursor('anchor'));
+ helpers.doKeys('k');
+ eqPos(Pos(1, 2), cm.getCursor('head'));
+ eqPos(Pos(3, 3), cm.getCursor('anchor'));
+}, { value: 'cross\ncross\ncross\ncross\ncross\n'});
+testVim('visual_crossover_down', function(cm, vim, helpers) {
+ cm.setCursor(1, 2);
+ helpers.doKeys('v', 'k', 'j', 'j');
+ eqPos(Pos(2, 3), cm.getCursor('head'));
+ eqPos(Pos(1, 2), cm.getCursor('anchor'));
+ helpers.doKeys('j');
+ eqPos(Pos(3, 3), cm.getCursor('head'));
+ eqPos(Pos(1, 2), cm.getCursor('anchor'));
+}, { value: 'cross\ncross\ncross\ncross\ncross\n'});
+testVim('visual_exit', function(cm, vim, helpers) {
+ helpers.doKeys('<C-v>', 'l', 'j', 'j', '<Esc>');
+ eqPos(cm.getCursor('anchor'), cm.getCursor('head'));
+ eq(vim.visualMode, false);
+}, { value: 'hello\nworld\nfoo' });
+testVim('visual_line', function(cm, vim, helpers) {
+ helpers.doKeys('l', 'V', 'l', 'j', 'j', 'd');
+ eq(' 4\n 5', cm.getValue());
+}, { value: ' 1\n 2\n 3\n 4\n 5' });
+testVim('visual_block_move_to_eol', function(cm, vim, helpers) {
+ // moveToEol should move all block cursors to end of line
+ cm.setCursor(0, 0);
+ helpers.doKeys('<C-v>', 'G', '$');
+ var selections = cm.getSelections().join();
+ eq('123,45,6', selections);
+ // Checks that with cursor at Infinity, finding words backwards still works.
+ helpers.doKeys('2', 'k', 'b');
+ selections = cm.getSelections().join();
+ eq('1', selections);
+}, {value: '123\n45\n6'});
+testVim('visual_block_different_line_lengths', function(cm, vim, helpers) {
+ // test the block selection with lines of different length
+ // i.e. extending the selection
+ // till the end of the longest line.
+ helpers.doKeys('<C-v>', 'l', 'j', 'j', '6', 'l', 'd');
+ helpers.doKeys('d', 'd', 'd', 'd');
+ eq('', cm.getValue());
+}, {value: '1234\n5678\nabcdefg'});
+testVim('visual_block_truncate_on_short_line', function(cm, vim, helpers) {
+ // check for left side selection in case
+ // of moving up to a shorter line.
+ cm.replaceRange('', cm.getCursor());
+ cm.setCursor(3, 4);
+ helpers.doKeys('<C-v>', 'l', 'k', 'k', 'd');
+ eq('hello world\n{\ntis\nsa!', cm.getValue());
+}, {value: 'hello world\n{\nthis is\nsparta!'});
+testVim('visual_block_corners', function(cm, vim, helpers) {
+ cm.setCursor(1, 2);
+ helpers.doKeys('<C-v>', '2', 'l', 'k');
+ // circle around the anchor
+ // and check the selections
+ var selections = cm.getSelections();
+ eq('345891', selections.join(''));
+ helpers.doKeys('4', 'h');
+ selections = cm.getSelections();
+ eq('123678', selections.join(''));
+ helpers.doKeys('j', 'j');
+ selections = cm.getSelections();
+ eq('678abc', selections.join(''));
+ helpers.doKeys('4', 'l');
+ selections = cm.getSelections();
+ eq('891cde', selections.join(''));
+}, {value: '12345\n67891\nabcde'});
+testVim('visual_block_mode_switch', function(cm, vim, helpers) {
+ // switch between visual modes
+ cm.setCursor(1, 1);
+ // blockwise to characterwise visual
+ helpers.doKeys('<C-v>', 'j', 'l', 'v');
+ selections = cm.getSelections();
+ eq('7891\nabc', selections.join(''));
+ // characterwise to blockwise
+ helpers.doKeys('<C-v>');
+ selections = cm.getSelections();
+ eq('78bc', selections.join(''));
+ // blockwise to linewise visual
+ helpers.doKeys('V');
+ selections = cm.getSelections();
+ eq('67891\nabcde', selections.join(''));
+}, {value: '12345\n67891\nabcde'});
+testVim('visual_block_crossing_short_line', function(cm, vim, helpers) {
+ // visual block with long and short lines
+ cm.setCursor(0, 3);
+ helpers.doKeys('<C-v>', 'j', 'j', 'j');
+ var selections = cm.getSelections().join();
+ eq('4,,d,b', selections);
+ helpers.doKeys('3', 'k');
+ selections = cm.getSelections().join();
+ eq('4', selections);
+ helpers.doKeys('5', 'j', 'k');
+ selections = cm.getSelections().join("");
+ eq(10, selections.length);
+}, {value: '123456\n78\nabcdefg\nfoobar\n}\n'});
+testVim('visual_block_curPos_on_exit', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('<C-v>', '3' , 'l', '<Esc>');
+ eqPos(makeCursor(0, 3), cm.getCursor());
+ helpers.doKeys('h', '<C-v>', '2' , 'j' ,'3' , 'l');
+ eq(cm.getSelections().join(), "3456,,cdef");
+ helpers.doKeys('4' , 'h');
+ eq(cm.getSelections().join(), "23,8,bc");
+ helpers.doKeys('2' , 'l');
+ eq(cm.getSelections().join(), "34,,cd");
+}, {value: '123456\n78\nabcdefg\nfoobar'});
+
+testVim('visual_marks', function(cm, vim, helpers) {
+ helpers.doKeys('l', 'v', 'l', 'l', 'j', 'j', 'v');
+ // Test visual mode marks
+ cm.setCursor(2, 1);
+ helpers.doKeys('\'', '<');
+ helpers.assertCursorAt(0, 1);
+ helpers.doKeys('\'', '>');
+ helpers.assertCursorAt(2, 0);
+});
+testVim('visual_join', function(cm, vim, helpers) {
+ helpers.doKeys('l', 'V', 'l', 'j', 'j', 'J');
+ eq(' 1 2 3\n 4\n 5', cm.getValue());
+ is(!vim.visualMode);
+}, { value: ' 1\n 2\n 3\n 4\n 5' });
+testVim('visual_join_2', function(cm, vim, helpers) {
+ helpers.doKeys('G', 'V', 'g', 'g', 'J');
+ eq('1 2 3 4 5 6 ', cm.getValue());
+ is(!vim.visualMode);
+}, { value: '1\n2\n3\n4\n5\n6\n'});
+testVim('visual_blank', function(cm, vim, helpers) {
+ helpers.doKeys('v', 'k');
+ eq(vim.visualMode, true);
+}, { value: '\n' });
+testVim('reselect_visual', function(cm, vim, helpers) {
+ helpers.doKeys('l', 'v', 'l', 'l', 'l', 'y', 'g', 'v');
+ helpers.assertCursorAt(0, 5);
+ eqPos(makeCursor(0, 1), cm.getCursor('anchor'));
+ helpers.doKeys('v');
+ cm.setCursor(1, 0);
+ helpers.doKeys('v', 'l', 'l', 'p');
+ eq('123456\n2345\nbar', cm.getValue());
+ cm.setCursor(0, 0);
+ helpers.doKeys('g', 'v');
+ // here the fake cursor is at (1, 3)
+ helpers.assertCursorAt(1, 4);
+ eqPos(makeCursor(1, 0), cm.getCursor('anchor'));
+ helpers.doKeys('v');
+ cm.setCursor(2, 0);
+ helpers.doKeys('v', 'l', 'l', 'g', 'v');
+ helpers.assertCursorAt(1, 4);
+ eqPos(makeCursor(1, 0), cm.getCursor('anchor'));
+ helpers.doKeys('g', 'v');
+ helpers.assertCursorAt(2, 3);
+ eqPos(makeCursor(2, 0), cm.getCursor('anchor'));
+ eq('123456\n2345\nbar', cm.getValue());
+}, { value: '123456\nfoo\nbar' });
+testVim('reselect_visual_line', function(cm, vim, helpers) {
+ helpers.doKeys('l', 'V', 'j', 'j', 'V', 'g', 'v', 'd');
+ eq('foo\nand\nbar', cm.getValue());
+ cm.setCursor(1, 0);
+ helpers.doKeys('V', 'y', 'j');
+ helpers.doKeys('V', 'p' , 'g', 'v', 'd');
+ eq('foo\nand', cm.getValue());
+}, { value: 'hello\nthis\nis\nfoo\nand\nbar' });
+testVim('reselect_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(1, 2);
+ helpers.doKeys('<C-v>', 'k', 'h', '<C-v>');
+ cm.setCursor(2, 1);
+ helpers.doKeys('v', 'l', 'g', 'v');
+ eqPos(Pos(1, 2), vim.sel.anchor);
+ eqPos(Pos(0, 1), vim.sel.head);
+ // Ensure selection is done with visual block mode rather than one
+ // continuous range.
+ eq(cm.getSelections().join(''), '23oo')
+ helpers.doKeys('g', 'v');
+ eqPos(Pos(2, 1), vim.sel.anchor);
+ eqPos(Pos(2, 2), vim.sel.head);
+ helpers.doKeys('<Esc>');
+ // Ensure selection of deleted range
+ cm.setCursor(1, 1);
+ helpers.doKeys('v', '<C-v>', 'j', 'd', 'g', 'v');
+ eq(cm.getSelections().join(''), 'or');
+}, { value: '123456\nfoo\nbar' });
+testVim('s_normal', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('s');
+ helpers.doKeys('<Esc>');
+ eq('ac', cm.getValue());
+}, { value: 'abc'});
+testVim('s_visual', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('v', 's');
+ helpers.doKeys('<Esc>');
+ helpers.assertCursorAt(0, 0);
+ eq('ac', cm.getValue());
+}, { value: 'abc'});
+testVim('o_visual', function(cm, vim, helpers) {
+ cm.setCursor(0,0);
+ helpers.doKeys('v','l','l','l','o');
+ helpers.assertCursorAt(0,0);
+ helpers.doKeys('v','v','j','j','j','o');
+ helpers.assertCursorAt(0,0);
+ helpers.doKeys('O');
+ helpers.doKeys('l','l')
+ helpers.assertCursorAt(3, 3);
+ helpers.doKeys('d');
+ eq('p',cm.getValue());
+}, { value: 'abcd\nefgh\nijkl\nmnop'});
+testVim('o_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('<C-v>','3','j','l','l', 'o');
+ eqPos(Pos(3, 3), vim.sel.anchor);
+ eqPos(Pos(0, 1), vim.sel.head);
+ helpers.doKeys('O');
+ eqPos(Pos(3, 1), vim.sel.anchor);
+ eqPos(Pos(0, 3), vim.sel.head);
+ helpers.doKeys('o');
+ eqPos(Pos(0, 3), vim.sel.anchor);
+ eqPos(Pos(3, 1), vim.sel.head);
+}, { value: 'abcd\nefgh\nijkl\nmnop'});
+testVim('changeCase_visual', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('v', 'l', 'l');
+ helpers.doKeys('U');
+ helpers.assertCursorAt(0, 0);
+ helpers.doKeys('v', 'l', 'l');
+ helpers.doKeys('u');
+ helpers.assertCursorAt(0, 0);
+ helpers.doKeys('l', 'l', 'l', '.');
+ helpers.assertCursorAt(0, 3);
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'a', 'v', 'j', 'U', 'q');
+ helpers.assertCursorAt(0, 0);
+ helpers.doKeys('j', '@', 'a');
+ helpers.assertCursorAt(1, 0);
+ cm.setCursor(3, 0);
+ helpers.doKeys('V', 'U', 'j', '.');
+ eq('ABCDEF\nGHIJKL\nMnopq\nSHORT LINE\nLONG LINE OF TEXT', cm.getValue());
+}, { value: 'abcdef\nghijkl\nmnopq\nshort line\nlong line of text'});
+testVim('changeCase_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(2, 1);
+ helpers.doKeys('<C-v>', 'k', 'k', 'h', 'U');
+ eq('ABcdef\nGHijkl\nMNopq\nfoo', cm.getValue());
+ cm.setCursor(0, 2);
+ helpers.doKeys('.');
+ eq('ABCDef\nGHIJkl\nMNOPq\nfoo', cm.getValue());
+ // check when last line is shorter.
+ cm.setCursor(2, 2);
+ helpers.doKeys('.');
+ eq('ABCDef\nGHIJkl\nMNOPq\nfoO', cm.getValue());
+}, { value: 'abcdef\nghijkl\nmnopq\nfoo'});
+testVim('visual_paste', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('v', 'l', 'l', 'y');
+ helpers.assertCursorAt(0, 0);
+ helpers.doKeys('3', 'l', 'j', 'v', 'l', 'p');
+ helpers.assertCursorAt(1, 5);
+ eq('this is a\nunithitest for visual paste', cm.getValue());
+ cm.setCursor(0, 0);
+ // in case of pasting whole line
+ helpers.doKeys('y', 'y');
+ cm.setCursor(1, 6);
+ helpers.doKeys('v', 'l', 'l', 'l', 'p');
+ helpers.assertCursorAt(2, 0);
+ eq('this is a\nunithi\nthis is a\n for visual paste', cm.getValue());
+}, { value: 'this is a\nunit test for visual paste'});
+
+// This checks the contents of the register used to paste the text
+testVim('v_paste_from_register', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('"', 'a', 'y', 'w');
+ cm.setCursor(1, 0);
+ helpers.doKeys('v', 'p');
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/a\s+register/.test(text));
+ });
+}, { value: 'register contents\nare not erased'});
+testVim('S_normal', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('j', 'S');
+ helpers.doKeys('<Esc>');
+ helpers.assertCursorAt(1, 1);
+ eq('aa{\n \ncc', cm.getValue());
+ helpers.doKeys('j', 'S');
+ eq('aa{\n \n ', cm.getValue());
+ helpers.assertCursorAt(2, 2);
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('d', 'd', 'd', 'd');
+ helpers.assertCursorAt(0, 0);
+ helpers.doKeys('S');
+ is(vim.insertMode);
+ eq('', cm.getValue());
+}, { value: 'aa{\nbb\ncc'});
+testVim('blockwise_paste', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('<C-v>', '3', 'j', 'l', 'y');
+ cm.setCursor(0, 2);
+ // paste one char after the current cursor position
+ helpers.doKeys('p');
+ eq('helhelo\nworwold\nfoofo\nbarba', cm.getValue());
+ cm.setCursor(0, 0);
+ helpers.doKeys('v', '4', 'l', 'y');
+ cm.setCursor(0, 0);
+ helpers.doKeys('<C-v>', '3', 'j', 'p');
+ eq('helheelhelo\norwold\noofo\narba', cm.getValue());
+}, { value: 'hello\nworld\nfoo\nbar'});
+testVim('blockwise_paste_long/short_line', function(cm, vim, helpers) {
+ // extend short lines in case of different line lengths.
+ cm.setCursor(0, 0);
+ helpers.doKeys('<C-v>', 'j', 'j', 'y');
+ cm.setCursor(0, 3);
+ helpers.doKeys('p');
+ eq('hellho\nfoo f\nbar b', cm.getValue());
+}, { value: 'hello\nfoo\nbar'});
+testVim('blockwise_paste_cut_paste', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('<C-v>', '2', 'j', 'x');
+ cm.setCursor(0, 0);
+ helpers.doKeys('P');
+ eq('cut\nand\npaste\nme', cm.getValue());
+}, { value: 'cut\nand\npaste\nme'});
+testVim('blockwise_paste_from_register', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('<C-v>', '2', 'j', '"', 'a', 'y');
+ cm.setCursor(0, 3);
+ helpers.doKeys('"', 'a', 'p');
+ eq('foobfar\nhellho\nworlwd', cm.getValue());
+}, { value: 'foobar\nhello\nworld'});
+testVim('blockwise_paste_last_line', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('<C-v>', '2', 'j', 'l', 'y');
+ cm.setCursor(3, 0);
+ helpers.doKeys('p');
+ eq('cut\nand\npaste\nmcue\n an\n pa', cm.getValue());
+}, { value: 'cut\nand\npaste\nme'});
+
+testVim('S_visual', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('v', 'j', 'S');
+ helpers.doKeys('<Esc>');
+ helpers.assertCursorAt(0, 0);
+ eq('\ncc', cm.getValue());
+}, { value: 'aa\nbb\ncc'});
+
+testVim('d_/', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('match');
+ helpers.doKeys('2', 'd', '/');
+ helpers.assertCursorAt(0, 0);
+ eq('match \n next', cm.getValue());
+ cm.openDialog = helpers.fakeOpenDialog('2');
+ helpers.doKeys('d', ':');
+ // TODO eq(' next', cm.getValue());
+}, { value: 'text match match \n next' });
+testVim('/ and n/N', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('match');
+ helpers.doKeys('/');
+ helpers.assertCursorAt(0, 11);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(1, 6);
+ helpers.doKeys('N');
+ helpers.assertCursorAt(0, 11);
+
+ cm.setCursor(0, 0);
+ helpers.doKeys('2', '/');
+ helpers.assertCursorAt(1, 6);
+}, { value: 'match nope match \n nope Match' });
+testVim('/_case', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('Match');
+ helpers.doKeys('/');
+ helpers.assertCursorAt(1, 6);
+}, { value: 'match nope match \n nope Match' });
+testVim('/_2_pcre', function(cm, vim, helpers) {
+ CodeMirror.Vim.setOption('pcre', true);
+ cm.openDialog = helpers.fakeOpenDialog('(word){2}');
+ helpers.doKeys('/');
+ helpers.assertCursorAt(1, 9);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(2, 1);
+}, { value: 'word\n another wordword\n wordwordword\n' });
+testVim('/_2_nopcre', function(cm, vim, helpers) {
+ CodeMirror.Vim.setOption('pcre', false);
+ cm.openDialog = helpers.fakeOpenDialog('\\(word\\)\\{2}');
+ helpers.doKeys('/');
+ helpers.assertCursorAt(1, 9);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(2, 1);
+}, { value: 'word\n another wordword\n wordwordword\n' });
+testVim('/_nongreedy', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('aa');
+ helpers.doKeys('/');
+ helpers.assertCursorAt(0, 4);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(1, 3);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 0);
+}, { value: 'aaa aa \n a aa'});
+testVim('?_nongreedy', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('aa');
+ helpers.doKeys('?');
+ helpers.assertCursorAt(1, 3);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 4);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 0);
+}, { value: 'aaa aa \n a aa'});
+testVim('/_greedy', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('a+');
+ helpers.doKeys('/');
+ helpers.assertCursorAt(0, 4);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(1, 1);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(1, 3);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 0);
+}, { value: 'aaa aa \n a aa'});
+testVim('?_greedy', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('a+');
+ helpers.doKeys('?');
+ helpers.assertCursorAt(1, 3);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(1, 1);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 4);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 0);
+}, { value: 'aaa aa \n a aa'});
+testVim('/_greedy_0_or_more', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('a*');
+ helpers.doKeys('/');
+ helpers.assertCursorAt(0, 3);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 4);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 5);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(1, 0);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(1, 1);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 0);
+}, { value: 'aaa aa\n aa'});
+testVim('?_greedy_0_or_more', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('a*');
+ helpers.doKeys('?');
+ helpers.assertCursorAt(1, 1);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(1, 0);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 5);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 4);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 3);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 0);
+}, { value: 'aaa aa\n aa'});
+testVim('? and n/N', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('match');
+ helpers.doKeys('?');
+ helpers.assertCursorAt(1, 6);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 11);
+ helpers.doKeys('N');
+ helpers.assertCursorAt(1, 6);
+
+ cm.setCursor(0, 0);
+ helpers.doKeys('2', '?');
+ helpers.assertCursorAt(0, 11);
+}, { value: 'match nope match \n nope Match' });
+testVim('*', function(cm, vim, helpers) {
+ cm.setCursor(0, 9);
+ helpers.doKeys('*');
+ helpers.assertCursorAt(0, 22);
+
+ cm.setCursor(0, 9);
+ helpers.doKeys('2', '*');
+ helpers.assertCursorAt(1, 8);
+}, { value: 'nomatch match nomatch match \nnomatch Match' });
+testVim('*_no_word', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('*');
+ helpers.assertCursorAt(0, 0);
+}, { value: ' \n match \n' });
+testVim('*_symbol', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('*');
+ helpers.assertCursorAt(1, 0);
+}, { value: ' /}\n/} match \n' });
+testVim('#', function(cm, vim, helpers) {
+ cm.setCursor(0, 9);
+ helpers.doKeys('#');
+ helpers.assertCursorAt(1, 8);
+
+ cm.setCursor(0, 9);
+ helpers.doKeys('2', '#');
+ helpers.assertCursorAt(0, 22);
+}, { value: 'nomatch match nomatch match \nnomatch Match' });
+testVim('*_seek', function(cm, vim, helpers) {
+ // Should skip over space and symbols.
+ cm.setCursor(0, 3);
+ helpers.doKeys('*');
+ helpers.assertCursorAt(0, 22);
+}, { value: ' := match nomatch match \nnomatch Match' });
+testVim('#', function(cm, vim, helpers) {
+ // Should skip over space and symbols.
+ cm.setCursor(0, 3);
+ helpers.doKeys('#');
+ helpers.assertCursorAt(1, 8);
+}, { value: ' := match nomatch match \nnomatch Match' });
+testVim('g*', function(cm, vim, helpers) {
+ cm.setCursor(0, 8);
+ helpers.doKeys('g', '*');
+ helpers.assertCursorAt(0, 18);
+ cm.setCursor(0, 8);
+ helpers.doKeys('3', 'g', '*');
+ helpers.assertCursorAt(1, 8);
+}, { value: 'matches match alsoMatch\nmatchme matching' });
+testVim('g#', function(cm, vim, helpers) {
+ cm.setCursor(0, 8);
+ helpers.doKeys('g', '#');
+ helpers.assertCursorAt(0, 0);
+ cm.setCursor(0, 8);
+ helpers.doKeys('3', 'g', '#');
+ helpers.assertCursorAt(1, 0);
+}, { value: 'matches match alsoMatch\nmatchme matching' });
+testVim('macro_insert', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'a', '0', 'i');
+ cm.replaceRange('foo', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('q', '@', 'a');
+ eq('foofoo', cm.getValue());
+}, { value: ''});
+testVim('macro_insert_repeat', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'a', '$', 'a');
+ cm.replaceRange('larry.', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('a');
+ cm.replaceRange('curly.', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('q');
+ helpers.doKeys('a');
+ cm.replaceRange('moe.', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('@', 'a');
+ // At this point, the most recent edit should be the 2nd insert change
+ // inside the macro, i.e. "curly.".
+ helpers.doKeys('.');
+ eq('larry.curly.moe.larry.curly.curly.', cm.getValue());
+}, { value: ''});
+testVim('macro_space', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('<Space>', '<Space>');
+ helpers.assertCursorAt(0, 2);
+ helpers.doKeys('q', 'a', '<Space>', '<Space>', 'q');
+ helpers.assertCursorAt(0, 4);
+ helpers.doKeys('@', 'a');
+ helpers.assertCursorAt(0, 6);
+ helpers.doKeys('@', 'a');
+ helpers.assertCursorAt(0, 8);
+}, { value: 'one line of text.'});
+testVim('macro_t_search', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'a', 't', 'e', 'q');
+ helpers.assertCursorAt(0, 1);
+ helpers.doKeys('l', '@', 'a');
+ helpers.assertCursorAt(0, 6);
+ helpers.doKeys('l', ';');
+ helpers.assertCursorAt(0, 12);
+}, { value: 'one line of text.'});
+testVim('macro_f_search', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'b', 'f', 'e', 'q');
+ helpers.assertCursorAt(0, 2);
+ helpers.doKeys('@', 'b');
+ helpers.assertCursorAt(0, 7);
+ helpers.doKeys(';');
+ helpers.assertCursorAt(0, 13);
+}, { value: 'one line of text.'});
+testVim('macro_slash_search', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'c');
+ cm.openDialog = helpers.fakeOpenDialog('e');
+ helpers.doKeys('/', 'q');
+ helpers.assertCursorAt(0, 2);
+ helpers.doKeys('@', 'c');
+ helpers.assertCursorAt(0, 7);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 13);
+}, { value: 'one line of text.'});
+testVim('macro_multislash_search', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'd');
+ cm.openDialog = helpers.fakeOpenDialog('e');
+ helpers.doKeys('/');
+ cm.openDialog = helpers.fakeOpenDialog('t');
+ helpers.doKeys('/', 'q');
+ helpers.assertCursorAt(0, 12);
+ helpers.doKeys('@', 'd');
+ helpers.assertCursorAt(0, 15);
+}, { value: 'one line of text to rule them all.'});
+testVim('macro_last_ex_command_register', function (cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doEx('s/a/b');
+ helpers.doKeys('2', '@', ':');
+ eq('bbbaa', cm.getValue());
+ helpers.assertCursorAt(0, 2);
+}, { value: 'aaaaa'});
+testVim('macro_parens', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'z', 'i');
+ cm.replaceRange('(', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('e', 'a');
+ cm.replaceRange(')', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('q');
+ helpers.doKeys('w', '@', 'z');
+ helpers.doKeys('w', '@', 'z');
+ eq('(see) (spot) (run)', cm.getValue());
+}, { value: 'see spot run'});
+testVim('macro_overwrite', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'z', '0', 'i');
+ cm.replaceRange('I ', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('q');
+ helpers.doKeys('e');
+ // Now replace the macro with something else.
+ helpers.doKeys('q', 'z', 'a');
+ cm.replaceRange('.', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('q');
+ helpers.doKeys('e', '@', 'z');
+ helpers.doKeys('e', '@', 'z');
+ eq('I see. spot. run.', cm.getValue());
+}, { value: 'see spot run'});
+testVim('macro_search_f', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'a', 'f', ' ');
+ helpers.assertCursorAt(0,3);
+ helpers.doKeys('q', '0');
+ helpers.assertCursorAt(0,0);
+ helpers.doKeys('@', 'a');
+ helpers.assertCursorAt(0,3);
+}, { value: 'The quick brown fox jumped over the lazy dog.'});
+testVim('macro_search_2f', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'a', '2', 'f', ' ');
+ helpers.assertCursorAt(0,9);
+ helpers.doKeys('q', '0');
+ helpers.assertCursorAt(0,0);
+ helpers.doKeys('@', 'a');
+ helpers.assertCursorAt(0,9);
+}, { value: 'The quick brown fox jumped over the lazy dog.'});
+testVim('yank_register', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('"', 'a', 'y', 'y');
+ helpers.doKeys('j', '"', 'b', 'y', 'y');
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/a\s+foo/.test(text));
+ is(/b\s+bar/.test(text));
+ });
+ helpers.doKeys(':');
+}, { value: 'foo\nbar'});
+testVim('yank_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('<C-v>', 'l', 'j', '"', 'a', 'y');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/a\s+oo\nar/.test(text));
+ });
+ helpers.doKeys(':');
+}, { value: 'foo\nbar'});
+testVim('yank_append_line_to_line_register', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('"', 'a', 'y', 'y');
+ helpers.doKeys('j', '"', 'A', 'y', 'y');
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/a\s+foo\nbar/.test(text));
+ is(/"\s+foo\nbar/.test(text));
+ });
+ helpers.doKeys(':');
+}, { value: 'foo\nbar'});
+testVim('yank_append_word_to_word_register', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('"', 'a', 'y', 'w');
+ helpers.doKeys('j', '"', 'A', 'y', 'w');
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/a\s+foobar/.test(text));
+ is(/"\s+foobar/.test(text));
+ });
+ helpers.doKeys(':');
+}, { value: 'foo\nbar'});
+testVim('yank_append_line_to_word_register', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('"', 'a', 'y', 'w');
+ helpers.doKeys('j', '"', 'A', 'y', 'y');
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/a\s+foo\nbar/.test(text));
+ is(/"\s+foo\nbar/.test(text));
+ });
+ helpers.doKeys(':');
+}, { value: 'foo\nbar'});
+testVim('yank_append_word_to_line_register', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('"', 'a', 'y', 'y');
+ helpers.doKeys('j', '"', 'A', 'y', 'w');
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/a\s+foo\nbar/.test(text));
+ is(/"\s+foo\nbar/.test(text));
+ });
+ helpers.doKeys(':');
+}, { value: 'foo\nbar'});
+testVim('macro_register', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'a', 'i');
+ cm.replaceRange('gangnam', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('q');
+ helpers.doKeys('q', 'b', 'o');
+ cm.replaceRange('style', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('q');
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/a\s+i/.test(text));
+ is(/b\s+o/.test(text));
+ });
+ helpers.doKeys(':');
+}, { value: ''});
+testVim('._register', function(cm,vim,helpers) {
+ cm.setCursor(0,0);
+ helpers.doKeys('i');
+ cm.replaceRange('foo',cm.getCursor());
+ helpers.doKeys('<Esc>');
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/\.\s+foo/.test(text));
+ });
+ helpers.doKeys(':');
+}, {value: ''});
+testVim(':_register', function(cm,vim,helpers) {
+ helpers.doEx('bar');
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/:\s+bar/.test(text));
+ });
+ helpers.doKeys(':');
+}, {value: ''});
+testVim('search_register_escape', function(cm, vim, helpers) {
+ // Check that the register is restored if the user escapes rather than confirms.
+ cm.openDialog = helpers.fakeOpenDialog('waldo');
+ helpers.doKeys('/');
+ var onKeyDown;
+ var onKeyUp;
+ var KEYCODES = {
+ f: 70,
+ o: 79,
+ Esc: 27
+ };
+ cm.openDialog = function(template, callback, options) {
+ onKeyDown = options.onKeyDown;
+ onKeyUp = options.onKeyUp;
+ };
+ var close = function() {};
+ helpers.doKeys('/');
+ // Fake some keyboard events coming in.
+ onKeyDown({keyCode: KEYCODES.f}, '', close);
+ onKeyUp({keyCode: KEYCODES.f}, '', close);
+ onKeyDown({keyCode: KEYCODES.o}, 'f', close);
+ onKeyUp({keyCode: KEYCODES.o}, 'f', close);
+ onKeyDown({keyCode: KEYCODES.o}, 'fo', close);
+ onKeyUp({keyCode: KEYCODES.o}, 'fo', close);
+ onKeyDown({keyCode: KEYCODES.Esc}, 'foo', close);
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/waldo/.test(text));
+ is(!/foo/.test(text));
+ });
+ helpers.doKeys(':');
+}, {value: ''});
+testVim('search_register', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('foo');
+ helpers.doKeys('/');
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/\/\s+foo/.test(text));
+ });
+ helpers.doKeys(':');
+}, {value: ''});
+testVim('search_history', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('this');
+ helpers.doKeys('/');
+ cm.openDialog = helpers.fakeOpenDialog('checks');
+ helpers.doKeys('/');
+ cm.openDialog = helpers.fakeOpenDialog('search');
+ helpers.doKeys('/');
+ cm.openDialog = helpers.fakeOpenDialog('history');
+ helpers.doKeys('/');
+ cm.openDialog = helpers.fakeOpenDialog('checks');
+ helpers.doKeys('/');
+ var onKeyDown;
+ var onKeyUp;
+ var query = '';
+ var keyCodes = {
+ Up: 38,
+ Down: 40
+ };
+ cm.openDialog = function(template, callback, options) {
+ onKeyUp = options.onKeyUp;
+ onKeyDown = options.onKeyDown;
+ };
+ var close = function(newVal) {
+ if (typeof newVal == 'string') query = newVal;
+ }
+ helpers.doKeys('/');
+ onKeyDown({keyCode: keyCodes.Up}, query, close);
+ onKeyUp({keyCode: keyCodes.Up}, query, close);
+ eq(query, 'checks');
+ onKeyDown({keyCode: keyCodes.Up}, query, close);
+ onKeyUp({keyCode: keyCodes.Up}, query, close);
+ eq(query, 'history');
+ onKeyDown({keyCode: keyCodes.Up}, query, close);
+ onKeyUp({keyCode: keyCodes.Up}, query, close);
+ eq(query, 'search');
+ onKeyDown({keyCode: keyCodes.Up}, query, close);
+ onKeyUp({keyCode: keyCodes.Up}, query, close);
+ eq(query, 'this');
+ onKeyDown({keyCode: keyCodes.Down}, query, close);
+ onKeyUp({keyCode: keyCodes.Down}, query, close);
+ eq(query, 'search');
+}, {value: ''});
+testVim('exCommand_history', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ helpers.doKeys(':');
+ cm.openDialog = helpers.fakeOpenDialog('sort');
+ helpers.doKeys(':');
+ cm.openDialog = helpers.fakeOpenDialog('map');
+ helpers.doKeys(':');
+ cm.openDialog = helpers.fakeOpenDialog('invalid');
+ helpers.doKeys(':');
+ var onKeyDown;
+ var onKeyUp;
+ var input = '';
+ var keyCodes = {
+ Up: 38,
+ Down: 40,
+ s: 115
+ };
+ cm.openDialog = function(template, callback, options) {
+ onKeyUp = options.onKeyUp;
+ onKeyDown = options.onKeyDown;
+ };
+ var close = function(newVal) {
+ if (typeof newVal == 'string') input = newVal;
+ }
+ helpers.doKeys(':');
+ onKeyDown({keyCode: keyCodes.Up}, input, close);
+ eq(input, 'invalid');
+ onKeyDown({keyCode: keyCodes.Up}, input, close);
+ eq(input, 'map');
+ onKeyDown({keyCode: keyCodes.Up}, input, close);
+ eq(input, 'sort');
+ onKeyDown({keyCode: keyCodes.Up}, input, close);
+ eq(input, 'registers');
+ onKeyDown({keyCode: keyCodes.s}, '', close);
+ input = 's';
+ onKeyDown({keyCode: keyCodes.Up}, input, close);
+ eq(input, 'sort');
+}, {value: ''});
+testVim('search_clear', function(cm, vim, helpers) {
+ var onKeyDown;
+ var input = '';
+ var keyCodes = {
+ Ctrl: 17,
+ u: 85
+ };
+ cm.openDialog = function(template, callback, options) {
+ onKeyDown = options.onKeyDown;
+ };
+ var close = function(newVal) {
+ if (typeof newVal == 'string') input = newVal;
+ }
+ helpers.doKeys('/');
+ input = 'foo';
+ onKeyDown({keyCode: keyCodes.Ctrl}, input, close);
+ onKeyDown({keyCode: keyCodes.u, ctrlKey: true}, input, close);
+ eq(input, '');
+});
+testVim('exCommand_clear', function(cm, vim, helpers) {
+ var onKeyDown;
+ var input = '';
+ var keyCodes = {
+ Ctrl: 17,
+ u: 85
+ };
+ cm.openDialog = function(template, callback, options) {
+ onKeyDown = options.onKeyDown;
+ };
+ var close = function(newVal) {
+ if (typeof newVal == 'string') input = newVal;
+ }
+ helpers.doKeys(':');
+ input = 'foo';
+ onKeyDown({keyCode: keyCodes.Ctrl}, input, close);
+ onKeyDown({keyCode: keyCodes.u, ctrlKey: true}, input, close);
+ eq(input, '');
+});
+testVim('.', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('2', 'd', 'w');
+ helpers.doKeys('.');
+ eq('5 6', cm.getValue());
+}, { value: '1 2 3 4 5 6'});
+testVim('._repeat', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('2', 'd', 'w');
+ helpers.doKeys('3', '.');
+ eq('6', cm.getValue());
+}, { value: '1 2 3 4 5 6'});
+testVim('._insert', function(cm, vim, helpers) {
+ helpers.doKeys('i');
+ cm.replaceRange('test', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('.');
+ eq('testestt', cm.getValue());
+ helpers.assertCursorAt(0, 6);
+}, { value: ''});
+testVim('._insert_repeat', function(cm, vim, helpers) {
+ helpers.doKeys('i');
+ cm.replaceRange('test', cm.getCursor());
+ cm.setCursor(0, 4);
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('2', '.');
+ eq('testesttestt', cm.getValue());
+ helpers.assertCursorAt(0, 10);
+}, { value: ''});
+testVim('._repeat_insert', function(cm, vim, helpers) {
+ helpers.doKeys('3', 'i');
+ cm.replaceRange('te', cm.getCursor());
+ cm.setCursor(0, 2);
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('.');
+ eq('tetettetetee', cm.getValue());
+ helpers.assertCursorAt(0, 10);
+}, { value: ''});
+testVim('._insert_o', function(cm, vim, helpers) {
+ helpers.doKeys('o');
+ cm.replaceRange('z', cm.getCursor());
+ cm.setCursor(1, 1);
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('.');
+ eq('\nz\nz', cm.getValue());
+ helpers.assertCursorAt(2, 0);
+}, { value: ''});
+testVim('._insert_o_repeat', function(cm, vim, helpers) {
+ helpers.doKeys('o');
+ cm.replaceRange('z', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ cm.setCursor(1, 0);
+ helpers.doKeys('2', '.');
+ eq('\nz\nz\nz', cm.getValue());
+ helpers.assertCursorAt(3, 0);
+}, { value: ''});
+testVim('._insert_o_indent', function(cm, vim, helpers) {
+ helpers.doKeys('o');
+ cm.replaceRange('z', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ cm.setCursor(1, 2);
+ helpers.doKeys('.');
+ eq('{\n z\n z', cm.getValue());
+ helpers.assertCursorAt(2, 2);
+}, { value: '{'});
+testVim('._insert_cw', function(cm, vim, helpers) {
+ helpers.doKeys('c', 'w');
+ cm.replaceRange('test', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ cm.setCursor(0, 3);
+ helpers.doKeys('2', 'l');
+ helpers.doKeys('.');
+ eq('test test word3', cm.getValue());
+ helpers.assertCursorAt(0, 8);
+}, { value: 'word1 word2 word3' });
+testVim('._insert_cw_repeat', function(cm, vim, helpers) {
+ // For some reason, repeat cw in desktop VIM will does not repeat insert mode
+ // changes. Will conform to that behavior.
+ helpers.doKeys('c', 'w');
+ cm.replaceRange('test', cm.getCursor());
+ helpers.doKeys('<Esc>');
+ cm.setCursor(0, 4);
+ helpers.doKeys('l');
+ helpers.doKeys('2', '.');
+ eq('test test', cm.getValue());
+ helpers.assertCursorAt(0, 8);
+}, { value: 'word1 word2 word3' });
+testVim('._delete', function(cm, vim, helpers) {
+ cm.setCursor(0, 5);
+ helpers.doKeys('i');
+ helpers.doInsertModeKeys('Backspace');
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('.');
+ eq('zace', cm.getValue());
+ helpers.assertCursorAt(0, 1);
+}, { value: 'zabcde'});
+testVim('._delete_repeat', function(cm, vim, helpers) {
+ cm.setCursor(0, 6);
+ helpers.doKeys('i');
+ helpers.doInsertModeKeys('Backspace');
+ helpers.doKeys('<Esc>');
+ helpers.doKeys('2', '.');
+ eq('zzce', cm.getValue());
+ helpers.assertCursorAt(0, 1);
+}, { value: 'zzabcde'});
+testVim('._visual_>', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('V', 'j', '>');
+ cm.setCursor(2, 0)
+ helpers.doKeys('.');
+ eq(' 1\n 2\n 3\n 4', cm.getValue());
+ helpers.assertCursorAt(2, 2);
+}, { value: '1\n2\n3\n4'});
+testVim('f;', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('f', 'x');
+ helpers.doKeys(';');
+ helpers.doKeys('2', ';');
+ eq(9, cm.getCursor().ch);
+}, { value: '01x3xx678x'});
+testVim('F;', function(cm, vim, helpers) {
+ cm.setCursor(0, 8);
+ helpers.doKeys('F', 'x');
+ helpers.doKeys(';');
+ helpers.doKeys('2', ';');
+ eq(2, cm.getCursor().ch);
+}, { value: '01x3xx6x8x'});
+testVim('t;', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('t', 'x');
+ helpers.doKeys(';');
+ helpers.doKeys('2', ';');
+ eq(8, cm.getCursor().ch);
+}, { value: '01x3xx678x'});
+testVim('T;', function(cm, vim, helpers) {
+ cm.setCursor(0, 9);
+ helpers.doKeys('T', 'x');
+ helpers.doKeys(';');
+ helpers.doKeys('2', ';');
+ eq(2, cm.getCursor().ch);
+}, { value: '0xx3xx678x'});
+testVim('f,', function(cm, vim, helpers) {
+ cm.setCursor(0, 6);
+ helpers.doKeys('f', 'x');
+ helpers.doKeys(',');
+ helpers.doKeys('2', ',');
+ eq(2, cm.getCursor().ch);
+}, { value: '01x3xx678x'});
+testVim('F,', function(cm, vim, helpers) {
+ cm.setCursor(0, 3);
+ helpers.doKeys('F', 'x');
+ helpers.doKeys(',');
+ helpers.doKeys('2', ',');
+ eq(9, cm.getCursor().ch);
+}, { value: '01x3xx678x'});
+testVim('t,', function(cm, vim, helpers) {
+ cm.setCursor(0, 6);
+ helpers.doKeys('t', 'x');
+ helpers.doKeys(',');
+ helpers.doKeys('2', ',');
+ eq(3, cm.getCursor().ch);
+}, { value: '01x3xx678x'});
+testVim('T,', function(cm, vim, helpers) {
+ cm.setCursor(0, 4);
+ helpers.doKeys('T', 'x');
+ helpers.doKeys(',');
+ helpers.doKeys('2', ',');
+ eq(8, cm.getCursor().ch);
+}, { value: '01x3xx67xx'});
+testVim('fd,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('f', '4');
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', ';');
+ eq('56789', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 9);
+ helpers.doKeys('d', ',');
+ eq('01239', cm.getValue());
+}, { value: '0123456789'});
+testVim('Fd,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 9);
+ helpers.doKeys('F', '4');
+ cm.setCursor(0, 9);
+ helpers.doKeys('d', ';');
+ eq('01239', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', ',');
+ eq('56789', cm.getValue());
+}, { value: '0123456789'});
+testVim('td,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('t', '4');
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', ';');
+ eq('456789', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 9);
+ helpers.doKeys('d', ',');
+ eq('012349', cm.getValue());
+}, { value: '0123456789'});
+testVim('Td,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 9);
+ helpers.doKeys('T', '4');
+ cm.setCursor(0, 9);
+ helpers.doKeys('d', ';');
+ eq('012349', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', ',');
+ eq('456789', cm.getValue());
+}, { value: '0123456789'});
+testVim('fc,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('f', '4');
+ cm.setCursor(0, 0);
+ helpers.doKeys('c', ';', '<Esc>');
+ eq('56789', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 9);
+ helpers.doKeys('c', ',');
+ eq('01239', cm.getValue());
+}, { value: '0123456789'});
+testVim('Fc,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 9);
+ helpers.doKeys('F', '4');
+ cm.setCursor(0, 9);
+ helpers.doKeys('c', ';', '<Esc>');
+ eq('01239', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 0);
+ helpers.doKeys('c', ',');
+ eq('56789', cm.getValue());
+}, { value: '0123456789'});
+testVim('tc,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('t', '4');
+ cm.setCursor(0, 0);
+ helpers.doKeys('c', ';', '<Esc>');
+ eq('456789', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 9);
+ helpers.doKeys('c', ',');
+ eq('012349', cm.getValue());
+}, { value: '0123456789'});
+testVim('Tc,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 9);
+ helpers.doKeys('T', '4');
+ cm.setCursor(0, 9);
+ helpers.doKeys('c', ';', '<Esc>');
+ eq('012349', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 0);
+ helpers.doKeys('c', ',');
+ eq('456789', cm.getValue());
+}, { value: '0123456789'});
+testVim('fy,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('f', '4');
+ cm.setCursor(0, 0);
+ helpers.doKeys('y', ';', 'P');
+ eq('012340123456789', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 9);
+ helpers.doKeys('y', ',', 'P');
+ eq('012345678456789', cm.getValue());
+}, { value: '0123456789'});
+testVim('Fy,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 9);
+ helpers.doKeys('F', '4');
+ cm.setCursor(0, 9);
+ helpers.doKeys('y', ';', 'p');
+ eq('012345678945678', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 0);
+ helpers.doKeys('y', ',', 'P');
+ eq('012340123456789', cm.getValue());
+}, { value: '0123456789'});
+testVim('ty,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('t', '4');
+ cm.setCursor(0, 0);
+ helpers.doKeys('y', ';', 'P');
+ eq('01230123456789', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 9);
+ helpers.doKeys('y', ',', 'p');
+ eq('01234567895678', cm.getValue());
+}, { value: '0123456789'});
+testVim('Ty,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 9);
+ helpers.doKeys('T', '4');
+ cm.setCursor(0, 9);
+ helpers.doKeys('y', ';', 'p');
+ eq('01234567895678', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 0);
+ helpers.doKeys('y', ',', 'P');
+ eq('01230123456789', cm.getValue());
+}, { value: '0123456789'});
+testVim('HML', function(cm, vim, helpers) {
+ var lines = 35;
+ var textHeight = cm.defaultTextHeight();
+ cm.setSize(600, lines*textHeight);
+ cm.setCursor(120, 0);
+ helpers.doKeys('H');
+ helpers.assertCursorAt(86, 2);
+ helpers.doKeys('L');
+ helpers.assertCursorAt(120, 4);
+ helpers.doKeys('M');
+ helpers.assertCursorAt(103,4);
+}, { value: (function(){
+ var lines = new Array(100);
+ var upper = ' xx\n';
+ var lower = ' xx\n';
+ upper = lines.join(upper);
+ lower = lines.join(lower);
+ return upper + lower;
+})()});
+
+var zVals = [];
+forEach(['zb','zz','zt','z-','z.','z<CR>'], function(e, idx){
+ var lineNum = 250;
+ var lines = 35;
+ testVim(e, function(cm, vim, helpers) {
+ var k1 = e[0];
+ var k2 = e.substring(1);
+ var textHeight = cm.defaultTextHeight();
+ cm.setSize(600, lines*textHeight);
+ cm.setCursor(lineNum, 0);
+ helpers.doKeys(k1, k2);
+ zVals[idx] = cm.getScrollInfo().top;
+ }, { value: (function(){
+ return new Array(500).join('\n');
+ })()});
+});
+testVim('zb_to_bottom', function(cm, vim, helpers){
+ var lineNum = 250;
+ cm.setSize(600, 35*cm.defaultTextHeight());
+ cm.setCursor(lineNum, 0);
+ helpers.doKeys('z', 'b');
+ var scrollInfo = cm.getScrollInfo();
+ eq(scrollInfo.top + scrollInfo.clientHeight, cm.charCoords(Pos(lineNum, 0), 'local').bottom);
+}, { value: (function(){
+ return new Array(500).join('\n');
+})()});
+testVim('zt_to_top', function(cm, vim, helpers){
+ var lineNum = 250;
+ cm.setSize(600, 35*cm.defaultTextHeight());
+ cm.setCursor(lineNum, 0);
+ helpers.doKeys('z', 't');
+ eq(cm.getScrollInfo().top, cm.charCoords(Pos(lineNum, 0), 'local').top);
+}, { value: (function(){
+ return new Array(500).join('\n');
+})()});
+testVim('zb<zz', function(cm, vim, helpers){
+ eq(zVals[0]<zVals[1], true);
+});
+testVim('zz<zt', function(cm, vim, helpers){
+ eq(zVals[1]<zVals[2], true);
+});
+testVim('zb==z-', function(cm, vim, helpers){
+ eq(zVals[0], zVals[3]);
+});
+testVim('zz==z.', function(cm, vim, helpers){
+ eq(zVals[1], zVals[4]);
+});
+testVim('zt==z<CR>', function(cm, vim, helpers){
+ eq(zVals[2], zVals[5]);
+});
+
+var moveTillCharacterSandbox =
+ 'The quick brown fox \n';
+testVim('moveTillCharacter', function(cm, vim, helpers){
+ cm.setCursor(0, 0);
+ // Search for the 'q'.
+ cm.openDialog = helpers.fakeOpenDialog('q');
+ helpers.doKeys('/');
+ eq(4, cm.getCursor().ch);
+ // Jump to just before the first o in the list.
+ helpers.doKeys('t');
+ helpers.doKeys('o');
+ eq('The quick brown fox \n', cm.getValue());
+ // Delete that one character.
+ helpers.doKeys('d');
+ helpers.doKeys('t');
+ helpers.doKeys('o');
+ eq('The quick bown fox \n', cm.getValue());
+ // Delete everything until the next 'o'.
+ helpers.doKeys('.');
+ eq('The quick box \n', cm.getValue());
+ // An unmatched character should have no effect.
+ helpers.doKeys('d');
+ helpers.doKeys('t');
+ helpers.doKeys('q');
+ eq('The quick box \n', cm.getValue());
+ // Matches should only be possible on single lines.
+ helpers.doKeys('d');
+ helpers.doKeys('t');
+ helpers.doKeys('z');
+ eq('The quick box \n', cm.getValue());
+ // After all that, the search for 'q' should still be active, so the 'N' command
+ // can run it again in reverse. Use that to delete everything back to the 'q'.
+ helpers.doKeys('d');
+ helpers.doKeys('N');
+ eq('The ox \n', cm.getValue());
+ eq(4, cm.getCursor().ch);
+}, { value: moveTillCharacterSandbox});
+testVim('searchForPipe', function(cm, vim, helpers){
+ CodeMirror.Vim.setOption('pcre', false);
+ cm.setCursor(0, 0);
+ // Search for the '|'.
+ cm.openDialog = helpers.fakeOpenDialog('|');
+ helpers.doKeys('/');
+ eq(4, cm.getCursor().ch);
+}, { value: 'this|that'});
+
+
+var scrollMotionSandbox =
+ '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n';
+testVim('scrollMotion', function(cm, vim, helpers){
+ var prevCursor, prevScrollInfo;
+ cm.setCursor(0, 0);
+ // ctrl-y at the top of the file should have no effect.
+ helpers.doKeys('<C-y>');
+ eq(0, cm.getCursor().line);
+ prevScrollInfo = cm.getScrollInfo();
+ helpers.doKeys('<C-e>');
+ eq(1, cm.getCursor().line);
+ is(prevScrollInfo.top < cm.getScrollInfo().top);
+ // Jump to the end of the sandbox.
+ cm.setCursor(1000, 0);
+ prevCursor = cm.getCursor();
+ // ctrl-e at the bottom of the file should have no effect.
+ helpers.doKeys('<C-e>');
+ eq(prevCursor.line, cm.getCursor().line);
+ prevScrollInfo = cm.getScrollInfo();
+ helpers.doKeys('<C-y>');
+ eq(prevCursor.line - 1, cm.getCursor().line, "Y");
+ is(prevScrollInfo.top > cm.getScrollInfo().top);
+}, { value: scrollMotionSandbox});
+
+var squareBracketMotionSandbox = ''+
+ '({\n'+//0
+ ' ({\n'+//11
+ ' /*comment {\n'+//2
+ ' */(\n'+//3
+ '#else \n'+//4
+ ' /* )\n'+//5
+ '#if }\n'+//6
+ ' )}*/\n'+//7
+ ')}\n'+//8
+ '{}\n'+//9
+ '#else {{\n'+//10
+ '{}\n'+//11
+ '}\n'+//12
+ '{\n'+//13
+ '#endif\n'+//14
+ '}\n'+//15
+ '}\n'+//16
+ '#else';//17
+testVim('[[, ]]', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys(']', ']');
+ helpers.assertCursorAt(9,0);
+ helpers.doKeys('2', ']', ']');
+ helpers.assertCursorAt(13,0);
+ helpers.doKeys(']', ']');
+ helpers.assertCursorAt(17,0);
+ helpers.doKeys('[', '[');
+ helpers.assertCursorAt(13,0);
+ helpers.doKeys('2', '[', '[');
+ helpers.assertCursorAt(9,0);
+ helpers.doKeys('[', '[');
+ helpers.assertCursorAt(0,0);
+}, { value: squareBracketMotionSandbox});
+testVim('[], ][', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys(']', '[');
+ helpers.assertCursorAt(12,0);
+ helpers.doKeys('2', ']', '[');
+ helpers.assertCursorAt(16,0);
+ helpers.doKeys(']', '[');
+ helpers.assertCursorAt(17,0);
+ helpers.doKeys('[', ']');
+ helpers.assertCursorAt(16,0);
+ helpers.doKeys('2', '[', ']');
+ helpers.assertCursorAt(12,0);
+ helpers.doKeys('[', ']');
+ helpers.assertCursorAt(0,0);
+}, { value: squareBracketMotionSandbox});
+testVim('[{, ]}', function(cm, vim, helpers) {
+ cm.setCursor(4, 10);
+ helpers.doKeys('[', '{');
+ helpers.assertCursorAt(2,12);
+ helpers.doKeys('2', '[', '{');
+ helpers.assertCursorAt(0,1);
+ cm.setCursor(4, 10);
+ helpers.doKeys(']', '}');
+ helpers.assertCursorAt(6,11);
+ helpers.doKeys('2', ']', '}');
+ helpers.assertCursorAt(8,1);
+ cm.setCursor(0,1);
+ helpers.doKeys(']', '}');
+ helpers.assertCursorAt(8,1);
+ helpers.doKeys('[', '{');
+ helpers.assertCursorAt(0,1);
+}, { value: squareBracketMotionSandbox});
+testVim('[(, ])', function(cm, vim, helpers) {
+ cm.setCursor(4, 10);
+ helpers.doKeys('[', '(');
+ helpers.assertCursorAt(3,14);
+ helpers.doKeys('2', '[', '(');
+ helpers.assertCursorAt(0,0);
+ cm.setCursor(4, 10);
+ helpers.doKeys(']', ')');
+ helpers.assertCursorAt(5,11);
+ helpers.doKeys('2', ']', ')');
+ helpers.assertCursorAt(8,0);
+ helpers.doKeys('[', '(');
+ helpers.assertCursorAt(0,0);
+ helpers.doKeys(']', ')');
+ helpers.assertCursorAt(8,0);
+}, { value: squareBracketMotionSandbox});
+testVim('[*, ]*, [/, ]/', function(cm, vim, helpers) {
+ forEach(['*', '/'], function(key){
+ cm.setCursor(7, 0);
+ helpers.doKeys('2', '[', key);
+ helpers.assertCursorAt(2,2);
+ helpers.doKeys('2', ']', key);
+ helpers.assertCursorAt(7,5);
+ });
+}, { value: squareBracketMotionSandbox});
+testVim('[#, ]#', function(cm, vim, helpers) {
+ cm.setCursor(10, 3);
+ helpers.doKeys('2', '[', '#');
+ helpers.assertCursorAt(4,0);
+ helpers.doKeys('5', ']', '#');
+ helpers.assertCursorAt(17,0);
+ cm.setCursor(10, 3);
+ helpers.doKeys(']', '#');
+ helpers.assertCursorAt(14,0);
+}, { value: squareBracketMotionSandbox});
+testVim('[m, ]m, [M, ]M', function(cm, vim, helpers) {
+ cm.setCursor(11, 0);
+ helpers.doKeys('[', 'm');
+ helpers.assertCursorAt(10,7);
+ helpers.doKeys('4', '[', 'm');
+ helpers.assertCursorAt(1,3);
+ helpers.doKeys('5', ']', 'm');
+ helpers.assertCursorAt(11,0);
+ helpers.doKeys('[', 'M');
+ helpers.assertCursorAt(9,1);
+ helpers.doKeys('3', ']', 'M');
+ helpers.assertCursorAt(15,0);
+ helpers.doKeys('5', '[', 'M');
+ helpers.assertCursorAt(7,3);
+}, { value: squareBracketMotionSandbox});
+
+// Ex mode tests
+testVim('ex_go_to_line', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doEx('4');
+ helpers.assertCursorAt(3, 0);
+}, { value: 'a\nb\nc\nd\ne\n'});
+testVim('ex_write', function(cm, vim, helpers) {
+ var tmp = CodeMirror.commands.save;
+ var written;
+ var actualCm;
+ CodeMirror.commands.save = function(cm) {
+ written = true;
+ actualCm = cm;
+ };
+ // Test that w, wr, wri ... write all trigger :write.
+ var command = 'write';
+ for (var i = 1; i < command.length; i++) {
+ written = false;
+ actualCm = null;
+ helpers.doEx(command.substring(0, i));
+ eq(written, true);
+ eq(actualCm, cm);
+ }
+ CodeMirror.commands.save = tmp;
+});
+testVim('ex_sort', function(cm, vim, helpers) {
+ helpers.doEx('sort');
+ eq('Z\na\nb\nc\nd', cm.getValue());
+}, { value: 'b\nZ\nd\nc\na'});
+testVim('ex_sort_reverse', function(cm, vim, helpers) {
+ helpers.doEx('sort!');
+ eq('d\nc\nb\na', cm.getValue());
+}, { value: 'b\nd\nc\na'});
+testVim('ex_sort_range', function(cm, vim, helpers) {
+ helpers.doEx('2,3sort');
+ eq('b\nc\nd\na', cm.getValue());
+}, { value: 'b\nd\nc\na'});
+testVim('ex_sort_oneline', function(cm, vim, helpers) {
+ helpers.doEx('2sort');
+ // Expect no change.
+ eq('b\nd\nc\na', cm.getValue());
+}, { value: 'b\nd\nc\na'});
+testVim('ex_sort_ignoreCase', function(cm, vim, helpers) {
+ helpers.doEx('sort i');
+ eq('a\nb\nc\nd\nZ', cm.getValue());
+}, { value: 'b\nZ\nd\nc\na'});
+testVim('ex_sort_unique', function(cm, vim, helpers) {
+ helpers.doEx('sort u');
+ eq('Z\na\nb\nc\nd', cm.getValue());
+}, { value: 'b\nZ\na\na\nd\na\nc\na'});
+testVim('ex_sort_decimal', function(cm, vim, helpers) {
+ helpers.doEx('sort d');
+ eq('d3\n s5\n6\n.9', cm.getValue());
+}, { value: '6\nd3\n s5\n.9'});
+testVim('ex_sort_decimal_negative', function(cm, vim, helpers) {
+ helpers.doEx('sort d');
+ eq('z-9\nd3\n s5\n6\n.9', cm.getValue());
+}, { value: '6\nd3\n s5\n.9\nz-9'});
+testVim('ex_sort_decimal_reverse', function(cm, vim, helpers) {
+ helpers.doEx('sort! d');
+ eq('.9\n6\n s5\nd3', cm.getValue());
+}, { value: '6\nd3\n s5\n.9'});
+testVim('ex_sort_hex', function(cm, vim, helpers) {
+ helpers.doEx('sort x');
+ eq(' s5\n6\n.9\n&0xB\nd3', cm.getValue());
+}, { value: '6\nd3\n s5\n&0xB\n.9'});
+testVim('ex_sort_octal', function(cm, vim, helpers) {
+ helpers.doEx('sort o');
+ eq('.8\n.9\nd3\n s5\n6', cm.getValue());
+}, { value: '6\nd3\n s5\n.9\n.8'});
+testVim('ex_sort_decimal_mixed', function(cm, vim, helpers) {
+ helpers.doEx('sort d');
+ eq('y\nz\nc1\nb2\na3', cm.getValue());
+}, { value: 'a3\nz\nc1\ny\nb2'});
+testVim('ex_sort_decimal_mixed_reverse', function(cm, vim, helpers) {
+ helpers.doEx('sort! d');
+ eq('a3\nb2\nc1\nz\ny', cm.getValue());
+}, { value: 'a3\nz\nc1\ny\nb2'});
+testVim('ex_sort_patterns_not_supported', function(cm, vim, helpers) {
+ var notified = false;
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ notified = /patterns not supported/.test(text);
+ });
+ helpers.doEx('sort /abc/');
+ is(notified, 'No notification.');
+});
+// test for :global command
+testVim('ex_global', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doEx('g/one/s//two');
+ eq('two two\n two two\n two two', cm.getValue());
+ helpers.doEx('1,2g/two/s//one');
+ eq('one one\n one one\n two two', cm.getValue());
+}, {value: 'one one\n one one\n one one'});
+testVim('ex_global_confirm', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ var onKeyDown;
+ var openDialogSave = cm.openDialog;
+ var KEYCODES = {
+ a: 65,
+ n: 78,
+ q: 81,
+ y: 89
+ };
+ // Intercept the ex command, 'global'
+ cm.openDialog = function(template, callback, options) {
+ // Intercept the prompt for the embedded ex command, 'substitute'
+ cm.openDialog = function(template, callback, options) {
+ onKeyDown = options.onKeyDown;
+ };
+ callback('g/one/s//two/gc');
+ };
+ helpers.doKeys(':');
+ var close = function() {};
+ onKeyDown({keyCode: KEYCODES.n}, '', close);
+ onKeyDown({keyCode: KEYCODES.y}, '', close);
+ onKeyDown({keyCode: KEYCODES.a}, '', close);
+ onKeyDown({keyCode: KEYCODES.q}, '', close);
+ onKeyDown({keyCode: KEYCODES.y}, '', close);
+ eq('one two\n two two\n one one\n two one\n one one', cm.getValue());
+}, {value: 'one one\n one one\n one one\n one one\n one one'});
+// Basic substitute tests.
+testVim('ex_substitute_same_line', function(cm, vim, helpers) {
+ cm.setCursor(1, 0);
+ helpers.doEx('s/one/two/g');
+ eq('one one\n two two', cm.getValue());
+}, { value: 'one one\n one one'});
+testVim('ex_substitute_full_file', function(cm, vim, helpers) {
+ cm.setCursor(1, 0);
+ helpers.doEx('%s/one/two/g');
+ eq('two two\n two two', cm.getValue());
+}, { value: 'one one\n one one'});
+testVim('ex_substitute_input_range', function(cm, vim, helpers) {
+ cm.setCursor(1, 0);
+ helpers.doEx('1,3s/\\d/0/g');
+ eq('0\n0\n0\n4', cm.getValue());
+}, { value: '1\n2\n3\n4' });
+testVim('ex_substitute_visual_range', function(cm, vim, helpers) {
+ cm.setCursor(1, 0);
+ // Set last visual mode selection marks '< and '> at lines 2 and 4
+ helpers.doKeys('V', '2', 'j', 'v');
+ helpers.doEx('\'<,\'>s/\\d/0/g');
+ eq('1\n0\n0\n0\n5', cm.getValue());
+}, { value: '1\n2\n3\n4\n5' });
+testVim('ex_substitute_empty_query', function(cm, vim, helpers) {
+ // If the query is empty, use last query.
+ cm.setCursor(1, 0);
+ cm.openDialog = helpers.fakeOpenDialog('1');
+ helpers.doKeys('/');
+ helpers.doEx('s//b/g');
+ eq('abb ab2 ab3', cm.getValue());
+}, { value: 'a11 a12 a13' });
+testVim('ex_substitute_javascript', function(cm, vim, helpers) {
+ CodeMirror.Vim.setOption('pcre', false);
+ cm.setCursor(1, 0);
+ // Throw all the things that javascript likes to treat as special values
+ // into the replace part. All should be literal (this is VIM).
+ helpers.doEx('s/\\(\\d+\\)/$$ $\' $` $& \\1/g')
+ eq('a $$ $\' $` $& 0 b', cm.getValue());
+}, { value: 'a 0 b' });
+testVim('ex_substitute_empty_arguments', function(cm,vim,helpers) {
+ cm.setCursor(0, 0);
+ helpers.doEx('s/a/b/g');
+ cm.setCursor(1, 0);
+ helpers.doEx('s');
+ eq('b b\nb a', cm.getValue());
+}, {value: 'a a\na a'});
+
+// More complex substitute tests that test both pcre and nopcre options.
+function testSubstitute(name, options) {
+ testVim(name + '_pcre', function(cm, vim, helpers) {
+ cm.setCursor(1, 0);
+ CodeMirror.Vim.setOption('pcre', true);
+ helpers.doEx(options.expr);
+ eq(options.expectedValue, cm.getValue());
+ }, options);
+ // If no noPcreExpr is defined, assume that it's the same as the expr.
+ var noPcreExpr = options.noPcreExpr ? options.noPcreExpr : options.expr;
+ testVim(name + '_nopcre', function(cm, vim, helpers) {
+ cm.setCursor(1, 0);
+ CodeMirror.Vim.setOption('pcre', false);
+ helpers.doEx(noPcreExpr);
+ eq(options.expectedValue, cm.getValue());
+ }, options);
+}
+testSubstitute('ex_substitute_capture', {
+ value: 'a11 a12 a13',
+ expectedValue: 'a1111 a1212 a1313',
+ // $n is a backreference
+ expr: 's/(\\d+)/$1$1/g',
+ // \n is a backreference.
+ noPcreExpr: 's/\\(\\d+\\)/\\1\\1/g'});
+testSubstitute('ex_substitute_capture2', {
+ value: 'a 0 b',
+ expectedValue: 'a $00 b',
+ expr: 's/(\\d+)/$$$1$1/g',
+ noPcreExpr: 's/\\(\\d+\\)/$\\1\\1/g'});
+testSubstitute('ex_substitute_nocapture', {
+ value: 'a11 a12 a13',
+ expectedValue: 'a$1$1 a$1$1 a$1$1',
+ expr: 's/(\\d+)/$$1$$1/g',
+ noPcreExpr: 's/\\(\\d+\\)/$1$1/g'});
+testSubstitute('ex_substitute_nocapture2', {
+ value: 'a 0 b',
+ expectedValue: 'a $10 b',
+ expr: 's/(\\d+)/$$1$1/g',
+ noPcreExpr: 's/\\(\\d+\\)/\\$1\\1/g'});
+testSubstitute('ex_substitute_nocapture', {
+ value: 'a b c',
+ expectedValue: 'a $ c',
+ expr: 's/b/$$/',
+ noPcreExpr: 's/b/$/'});
+testSubstitute('ex_substitute_slash_regex', {
+ value: 'one/two \n three/four',
+ expectedValue: 'one|two \n three|four',
+ expr: '%s/\\//|'});
+testSubstitute('ex_substitute_pipe_regex', {
+ value: 'one|two \n three|four',
+ expectedValue: 'one,two \n three,four',
+ expr: '%s/\\|/,/',
+ noPcreExpr: '%s/|/,/'});
+testSubstitute('ex_substitute_or_regex', {
+ value: 'one|two \n three|four',
+ expectedValue: 'ana|twa \n thraa|faar',
+ expr: '%s/o|e|u/a/g',
+ noPcreExpr: '%s/o\\|e\\|u/a/g'});
+testSubstitute('ex_substitute_or_word_regex', {
+ value: 'one|two \n three|four',
+ expectedValue: 'five|five \n three|four',
+ expr: '%s/(one|two)/five/g',
+ noPcreExpr: '%s/\\(one\\|two\\)/five/g'});
+testSubstitute('ex_substitute_backslashslash_regex', {
+ value: 'one\\two \n three\\four',
+ expectedValue: 'one,two \n three,four',
+ expr: '%s/\\\\/,'});
+testSubstitute('ex_substitute_slash_replacement', {
+ value: 'one,two \n three,four',
+ expectedValue: 'one/two \n three/four',
+ expr: '%s/,/\\/'});
+testSubstitute('ex_substitute_backslash_replacement', {
+ value: 'one,two \n three,four',
+ expectedValue: 'one\\two \n three\\four',
+ expr: '%s/,/\\\\/g'});
+testSubstitute('ex_substitute_multibackslash_replacement', {
+ value: 'one,two \n three,four',
+ expectedValue: 'one\\\\\\\\two \n three\\\\\\\\four', // 2*8 backslashes.
+ expr: '%s/,/\\\\\\\\\\\\\\\\/g'}); // 16 backslashes.
+testSubstitute('ex_substitute_newline_replacement', {
+ value: 'one,two \n three,four',
+ expectedValue: 'one\ntwo \n three\nfour',
+ expr: '%s/,/\\n/g'});
+testSubstitute('ex_substitute_braces_word', {
+ value: 'ababab abb ab{2}',
+ expectedValue: 'ab abb ab{2}',
+ expr: '%s/(ab){2}//g',
+ noPcreExpr: '%s/\\(ab\\)\\{2\\}//g'});
+testSubstitute('ex_substitute_braces_range', {
+ value: 'a aa aaa aaaa',
+ expectedValue: 'a a',
+ expr: '%s/a{2,3}//g',
+ noPcreExpr: '%s/a\\{2,3\\}//g'});
+testSubstitute('ex_substitute_braces_literal', {
+ value: 'ababab abb ab{2}',
+ expectedValue: 'ababab abb ',
+ expr: '%s/ab\\{2\\}//g',
+ noPcreExpr: '%s/ab{2}//g'});
+testSubstitute('ex_substitute_braces_char', {
+ value: 'ababab abb ab{2}',
+ expectedValue: 'ababab ab{2}',
+ expr: '%s/ab{2}//g',
+ noPcreExpr: '%s/ab\\{2\\}//g'});
+testSubstitute('ex_substitute_braces_no_escape', {
+ value: 'ababab abb ab{2}',
+ expectedValue: 'ababab ab{2}',
+ expr: '%s/ab{2}//g',
+ noPcreExpr: '%s/ab\\{2}//g'});
+testSubstitute('ex_substitute_count', {
+ value: '1\n2\n3\n4',
+ expectedValue: '1\n0\n0\n4',
+ expr: 's/\\d/0/i 2'});
+testSubstitute('ex_substitute_count_with_range', {
+ value: '1\n2\n3\n4',
+ expectedValue: '1\n2\n0\n0',
+ expr: '1,3s/\\d/0/ 3'});
+testSubstitute('ex_substitute_not_global', {
+ value: 'aaa\nbaa\ncaa',
+ expectedValue: 'xaa\nbxa\ncxa',
+ expr: '%s/a/x/'});
+function testSubstituteConfirm(name, command, initialValue, expectedValue, keys, finalPos) {
+ testVim(name, function(cm, vim, helpers) {
+ var savedOpenDialog = cm.openDialog;
+ var savedKeyName = CodeMirror.keyName;
+ var onKeyDown;
+ var recordedCallback;
+ var closed = true; // Start out closed, set false on second openDialog.
+ function close() {
+ closed = true;
+ }
+ // First openDialog should save callback.
+ cm.openDialog = function(template, callback, options) {
+ recordedCallback = callback;
+ }
+ // Do first openDialog.
+ helpers.doKeys(':');
+ // Second openDialog should save keyDown handler.
+ cm.openDialog = function(template, callback, options) {
+ onKeyDown = options.onKeyDown;
+ closed = false;
+ };
+ // Return the command to Vim and trigger second openDialog.
+ recordedCallback(command);
+ // The event should really use keyCode, but here just mock it out and use
+ // key and replace keyName to just return key.
+ CodeMirror.keyName = function (e) { return e.key; }
+ keys = keys.toUpperCase();
+ for (var i = 0; i < keys.length; i++) {
+ is(!closed);
+ onKeyDown({ key: keys.charAt(i) }, '', close);
+ }
+ try {
+ eq(expectedValue, cm.getValue());
+ helpers.assertCursorAt(finalPos);
+ is(closed);
+ } catch(e) {
+ throw e
+ } finally {
+ // Restore overridden functions.
+ CodeMirror.keyName = savedKeyName;
+ cm.openDialog = savedOpenDialog;
+ }
+ }, { value: initialValue });
+};
+testSubstituteConfirm('ex_substitute_confirm_emptydoc',
+ '%s/x/b/c', '', '', '', makeCursor(0, 0));
+testSubstituteConfirm('ex_substitute_confirm_nomatch',
+ '%s/x/b/c', 'ba a\nbab', 'ba a\nbab', '', makeCursor(0, 0));
+testSubstituteConfirm('ex_substitute_confirm_accept',
+ '%s/a/b/cg', 'ba a\nbab', 'bb b\nbbb', 'yyy', makeCursor(1, 1));
+testSubstituteConfirm('ex_substitute_confirm_random_keys',
+ '%s/a/b/cg', 'ba a\nbab', 'bb b\nbbb', 'ysdkywerty', makeCursor(1, 1));
+testSubstituteConfirm('ex_substitute_confirm_some',
+ '%s/a/b/cg', 'ba a\nbab', 'bb a\nbbb', 'yny', makeCursor(1, 1));
+testSubstituteConfirm('ex_substitute_confirm_all',
+ '%s/a/b/cg', 'ba a\nbab', 'bb b\nbbb', 'a', makeCursor(1, 1));
+testSubstituteConfirm('ex_substitute_confirm_accept_then_all',
+ '%s/a/b/cg', 'ba a\nbab', 'bb b\nbbb', 'ya', makeCursor(1, 1));
+testSubstituteConfirm('ex_substitute_confirm_quit',
+ '%s/a/b/cg', 'ba a\nbab', 'bb a\nbab', 'yq', makeCursor(0, 3));
+testSubstituteConfirm('ex_substitute_confirm_last',
+ '%s/a/b/cg', 'ba a\nbab', 'bb b\nbab', 'yl', makeCursor(0, 3));
+testSubstituteConfirm('ex_substitute_confirm_oneline',
+ '1s/a/b/cg', 'ba a\nbab', 'bb b\nbab', 'yl', makeCursor(0, 3));
+testSubstituteConfirm('ex_substitute_confirm_range_accept',
+ '1,2s/a/b/cg', 'aa\na \na\na', 'bb\nb \na\na', 'yyy', makeCursor(1, 0));
+testSubstituteConfirm('ex_substitute_confirm_range_some',
+ '1,3s/a/b/cg', 'aa\na \na\na', 'ba\nb \nb\na', 'ynyy', makeCursor(2, 0));
+testSubstituteConfirm('ex_substitute_confirm_range_all',
+ '1,3s/a/b/cg', 'aa\na \na\na', 'bb\nb \nb\na', 'a', makeCursor(2, 0));
+testSubstituteConfirm('ex_substitute_confirm_range_last',
+ '1,3s/a/b/cg', 'aa\na \na\na', 'bb\nb \na\na', 'yyl', makeCursor(1, 0));
+//:noh should clear highlighting of search-results but allow to resume search through n
+testVim('ex_noh_clearSearchHighlight', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('match');
+ helpers.doKeys('?');
+ helpers.doEx('noh');
+ eq(vim.searchState_.getOverlay(),null,'match-highlighting wasn\'t cleared');
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 11,'can\'t resume search after clearing highlighting');
+}, { value: 'match nope match \n nope Match' });
+testVim('ex_yank', function (cm, vim, helpers) {
+ var curStart = makeCursor(3, 0);
+ cm.setCursor(curStart);
+ helpers.doEx('y');
+ var register = helpers.getRegisterController().getRegister();
+ var line = cm.getLine(3);
+ eq(line + '\n', register.toString());
+});
+testVim('set_boolean', function(cm, vim, helpers) {
+ CodeMirror.Vim.defineOption('testoption', true, 'boolean');
+ // Test default value is set.
+ is(CodeMirror.Vim.getOption('testoption'));
+ try {
+ // Test fail to set to non-boolean
+ CodeMirror.Vim.setOption('testoption', '5');
+ fail();
+ } catch (expected) {};
+ // Test setOption
+ CodeMirror.Vim.setOption('testoption', false);
+ is(!CodeMirror.Vim.getOption('testoption'));
+});
+testVim('ex_set_boolean', function(cm, vim, helpers) {
+ CodeMirror.Vim.defineOption('testoption', true, 'boolean');
+ // Test default value is set.
+ is(CodeMirror.Vim.getOption('testoption'));
+ try {
+ // Test fail to set to non-boolean
+ helpers.doEx('set testoption=22');
+ fail();
+ } catch (expected) {};
+ // Test setOption
+ helpers.doEx('set notestoption');
+ is(!CodeMirror.Vim.getOption('testoption'));
+});
+testVim('set_string', function(cm, vim, helpers) {
+ CodeMirror.Vim.defineOption('testoption', 'a', 'string');
+ // Test default value is set.
+ eq('a', CodeMirror.Vim.getOption('testoption'));
+ try {
+ // Test fail to set non-string.
+ CodeMirror.Vim.setOption('testoption', true);
+ fail();
+ } catch (expected) {};
+ try {
+ // Test fail to set 'notestoption'
+ CodeMirror.Vim.setOption('notestoption', 'b');
+ fail();
+ } catch (expected) {};
+ // Test setOption
+ CodeMirror.Vim.setOption('testoption', 'c');
+ eq('c', CodeMirror.Vim.getOption('testoption'));
+});
+testVim('ex_set_string', function(cm, vim, helpers) {
+ CodeMirror.Vim.defineOption('testopt', 'a', 'string');
+ // Test default value is set.
+ eq('a', CodeMirror.Vim.getOption('testopt'));
+ try {
+ // Test fail to set 'notestopt'
+ helpers.doEx('set notestopt=b');
+ fail();
+ } catch (expected) {};
+ // Test setOption
+ helpers.doEx('set testopt=c')
+ eq('c', CodeMirror.Vim.getOption('testopt'));
+ helpers.doEx('set testopt=c')
+ eq('c', CodeMirror.Vim.getOption('testopt', cm)); //local || global
+ eq('c', CodeMirror.Vim.getOption('testopt', cm, {scope: 'local'})); // local
+ eq('c', CodeMirror.Vim.getOption('testopt', cm, {scope: 'global'})); // global
+ eq('c', CodeMirror.Vim.getOption('testopt')); // global
+ // Test setOption global
+ helpers.doEx('setg testopt=d')
+ eq('c', CodeMirror.Vim.getOption('testopt', cm));
+ eq('c', CodeMirror.Vim.getOption('testopt', cm, {scope: 'local'}));
+ eq('d', CodeMirror.Vim.getOption('testopt', cm, {scope: 'global'}));
+ eq('d', CodeMirror.Vim.getOption('testopt'));
+ // Test setOption local
+ helpers.doEx('setl testopt=e')
+ eq('e', CodeMirror.Vim.getOption('testopt', cm));
+ eq('e', CodeMirror.Vim.getOption('testopt', cm, {scope: 'local'}));
+ eq('d', CodeMirror.Vim.getOption('testopt', cm, {scope: 'global'}));
+ eq('d', CodeMirror.Vim.getOption('testopt'));
+});
+testVim('ex_set_callback', function(cm, vim, helpers) {
+ var global;
+
+ function cb(val, cm, cfg) {
+ if (val === undefined) {
+ // Getter
+ if (cm) {
+ return cm._local;
+ } else {
+ return global;
+ }
+ } else {
+ // Setter
+ if (cm) {
+ cm._local = val;
+ } else {
+ global = val;
+ }
+ }
+ }
+
+ CodeMirror.Vim.defineOption('testopt', 'a', 'string', cb);
+ // Test default value is set.
+ eq('a', CodeMirror.Vim.getOption('testopt'));
+ try {
+ // Test fail to set 'notestopt'
+ helpers.doEx('set notestopt=b');
+ fail();
+ } catch (expected) {};
+ // Test setOption (Identical to the string tests, but via callback instead)
+ helpers.doEx('set testopt=c')
+ eq('c', CodeMirror.Vim.getOption('testopt', cm)); //local || global
+ eq('c', CodeMirror.Vim.getOption('testopt', cm, {scope: 'local'})); // local
+ eq('c', CodeMirror.Vim.getOption('testopt', cm, {scope: 'global'})); // global
+ eq('c', CodeMirror.Vim.getOption('testopt')); // global
+ // Test setOption global
+ helpers.doEx('setg testopt=d')
+ eq('c', CodeMirror.Vim.getOption('testopt', cm));
+ eq('c', CodeMirror.Vim.getOption('testopt', cm, {scope: 'local'}));
+ eq('d', CodeMirror.Vim.getOption('testopt', cm, {scope: 'global'}));
+ eq('d', CodeMirror.Vim.getOption('testopt'));
+ // Test setOption local
+ helpers.doEx('setl testopt=e')
+ eq('e', CodeMirror.Vim.getOption('testopt', cm));
+ eq('e', CodeMirror.Vim.getOption('testopt', cm, {scope: 'local'}));
+ eq('d', CodeMirror.Vim.getOption('testopt', cm, {scope: 'global'}));
+ eq('d', CodeMirror.Vim.getOption('testopt'));
+})
+testVim('ex_set_filetype', function(cm, vim, helpers) {
+ CodeMirror.defineMode('test_mode', function() {
+ return {token: function(stream) {
+ stream.match(/^\s+|^\S+/);
+ }};
+ });
+ CodeMirror.defineMode('test_mode_2', function() {
+ return {token: function(stream) {
+ stream.match(/^\s+|^\S+/);
+ }};
+ });
+ // Test mode is set.
+ helpers.doEx('set filetype=test_mode');
+ eq('test_mode', cm.getMode().name);
+ // Test 'ft' alias also sets mode.
+ helpers.doEx('set ft=test_mode_2');
+ eq('test_mode_2', cm.getMode().name);
+});
+testVim('ex_set_filetype_null', function(cm, vim, helpers) {
+ CodeMirror.defineMode('test_mode', function() {
+ return {token: function(stream) {
+ stream.match(/^\s+|^\S+/);
+ }};
+ });
+ cm.setOption('mode', 'test_mode');
+ // Test mode is set to null.
+ helpers.doEx('set filetype=');
+ eq('null', cm.getMode().name);
+});
+// TODO: Reset key maps after each test.
+testVim('ex_map_key2key', function(cm, vim, helpers) {
+ helpers.doEx('map a x');
+ helpers.doKeys('a');
+ helpers.assertCursorAt(0, 0);
+ eq('bc', cm.getValue());
+}, { value: 'abc' });
+testVim('ex_unmap_key2key', function(cm, vim, helpers) {
+ helpers.doEx('unmap a');
+ helpers.doKeys('a');
+ eq('vim-insert', cm.getOption('keyMap'));
+}, { value: 'abc' });
+testVim('ex_unmap_key2key_does_not_remove_default', function(cm, vim, helpers) {
+ try {
+ helpers.doEx('unmap a');
+ fail();
+ } catch (expected) {}
+ helpers.doKeys('a');
+ eq('vim-insert', cm.getOption('keyMap'));
+}, { value: 'abc' });
+testVim('ex_map_key2key_to_colon', function(cm, vim, helpers) {
+ helpers.doEx('map ; :');
+ var dialogOpened = false;
+ cm.openDialog = function() {
+ dialogOpened = true;
+ }
+ helpers.doKeys(';');
+ eq(dialogOpened, true);
+});
+testVim('ex_map_ex2key:', function(cm, vim, helpers) {
+ helpers.doEx('map :del x');
+ helpers.doEx('del');
+ helpers.assertCursorAt(0, 0);
+ eq('bc', cm.getValue());
+}, { value: 'abc' });
+testVim('ex_map_ex2ex', function(cm, vim, helpers) {
+ helpers.doEx('map :del :w');
+ var tmp = CodeMirror.commands.save;
+ var written = false;
+ var actualCm;
+ CodeMirror.commands.save = function(cm) {
+ written = true;
+ actualCm = cm;
+ };
+ helpers.doEx('del');
+ CodeMirror.commands.save = tmp;
+ eq(written, true);
+ eq(actualCm, cm);
+});
+testVim('ex_map_key2ex', function(cm, vim, helpers) {
+ helpers.doEx('map a :w');
+ var tmp = CodeMirror.commands.save;
+ var written = false;
+ var actualCm;
+ CodeMirror.commands.save = function(cm) {
+ written = true;
+ actualCm = cm;
+ };
+ helpers.doKeys('a');
+ CodeMirror.commands.save = tmp;
+ eq(written, true);
+ eq(actualCm, cm);
+});
+testVim('ex_map_key2key_visual_api', function(cm, vim, helpers) {
+ CodeMirror.Vim.map('b', ':w', 'visual');
+ var tmp = CodeMirror.commands.save;
+ var written = false;
+ var actualCm;
+ CodeMirror.commands.save = function(cm) {
+ written = true;
+ actualCm = cm;
+ };
+ // Mapping should not work in normal mode.
+ helpers.doKeys('b');
+ eq(written, false);
+ // Mapping should work in visual mode.
+ helpers.doKeys('v', 'b');
+ eq(written, true);
+ eq(actualCm, cm);
+
+ CodeMirror.commands.save = tmp;
+});
+testVim('ex_imap', function(cm, vim, helpers) {
+ CodeMirror.Vim.map('jk', '<Esc>', 'insert');
+ helpers.doKeys('i');
+ is(vim.insertMode);
+ helpers.doKeys('j', 'k');
+ is(!vim.insertMode);
+});
+testVim('ex_unmap_api', function(cm, vim, helpers) {
+ CodeMirror.Vim.map('<Alt-X>', 'gg', 'normal');
+ is(CodeMirror.Vim.handleKey(cm, "<Alt-X>", "normal"), "Alt-X key is mapped");
+ CodeMirror.Vim.unmap("<Alt-X>", "normal");
+ is(!CodeMirror.Vim.handleKey(cm, "<Alt-X>", "normal"), "Alt-X key is unmapped");
+});
+
+// Testing registration of functions as ex-commands and mapping to <Key>-keys
+testVim('ex_api_test', function(cm, vim, helpers) {
+ var res=false;
+ var val='from';
+ CodeMirror.Vim.defineEx('extest','ext',function(cm,params){
+ if(params.args)val=params.args[0];
+ else res=true;
+ });
+ helpers.doEx(':ext to');
+ eq(val,'to','Defining ex-command failed');
+ CodeMirror.Vim.map('<C-CR><Space>',':ext');
+ helpers.doKeys('<C-CR>','<Space>');
+ is(res,'Mapping to key failed');
+});
+// For now, this test needs to be last because it messes up : for future tests.
+testVim('ex_map_key2key_from_colon', function(cm, vim, helpers) {
+ helpers.doEx('map : x');
+ helpers.doKeys(':');
+ helpers.assertCursorAt(0, 0);
+ eq('bc', cm.getValue());
+}, { value: 'abc' });
+
+// Test event handlers
+testVim('beforeSelectionChange', function(cm, vim, helpers) {
+ cm.setCursor(0, 100);
+ eqPos(cm.getCursor('head'), cm.getCursor('anchor'));
+}, { value: 'abc' });
+
+
diff --git a/devtools/client/sourceeditor/test/codemirror/vimemacs.html b/devtools/client/sourceeditor/test/codemirror/vimemacs.html
new file mode 100644
index 000000000..c12236a7e
--- /dev/null
+++ b/devtools/client/sourceeditor/test/codemirror/vimemacs.html
@@ -0,0 +1,212 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>CodeMirror: VIM/Emacs tests</title>
+ <link rel="stylesheet" href="chrome://devtools/content/sourceeditor/codemirror/lib/codemirror.css">
+ <link rel="stylesheet" href="cm_mode_test.css">
+ <!--<link rel="stylesheet" href="../doc/docs.css">-->
+
+ <script src="chrome://devtools/content/sourceeditor/codemirror/codemirror.bundle.js"></script>
+
+ <style type="text/css">
+ .ok {color: #090;}
+ .fail {color: #e00;}
+ .error {color: #c90;}
+ .done {font-weight: bold;}
+ #progress {
+ background: #45d;
+ color: white;
+ text-shadow: 0 0 1px #45d, 0 0 2px #45d, 0 0 3px #45d;
+ font-weight: bold;
+ white-space: pre;
+ }
+ #testground {
+ visibility: hidden;
+ }
+ #testground.offscreen {
+ visibility: visible;
+ position: absolute;
+ left: -10000px;
+ top: -10000px;
+ }
+ .CodeMirror { border: 1px solid black; }
+ </style>
+ </head>
+ <body>
+ <h1>CodeMirror: VIM/Emacs tests</h1>
+
+ <p>A limited set of programmatic sanity tests for CodeMirror.</p>
+
+ <div style="border: 1px solid black; padding: 1px; max-width: 700px;">
+ <div style="width: 0px;" id=progress><div style="padding: 3px;">Ran <span id="progress_ran">0</span><span id="progress_total"> of 0</span> tests</div></div>
+ </div>
+ <p id=status>Please enable JavaScript...</p>
+ <div id=output></div>
+
+ <div id=testground></div>
+
+ <script src="driver.js"></script>
+ <script src="sublime_test.js"></script>
+ <script src="vim_test.js"></script>
+ <script src="emacs_test.js"></script>
+
+ <!-- Basic tests are in codemirror.html
+ <script src="cm_driver.js"></script>
+ <script src="cm_test.js"></script>
+ <script src="cm_comment_test.js"></script>
+ <script src="cm_doc_test.js"></script>
+ <script src="cm_driver.js"></script>
+ <script src="cm_emacs_test.js"></script>
+ <script src="cm_mode_test.js"></script>
+ <script src="cm_mode_javascript_test.js"></script>
+ <script src="cm_multi_test.js"></script>
+ <script src="cm_search_test.js"></script>
+ -->
+
+ <!-- These modes/addons are not used by Editor
+ <script src="doc_test.js"></script>
+ <script src="../mode/css/css.js"></script>
+ <script src="../mode/css/test.js"></script>
+ <script src="../mode/css/scss_test.js"></script>
+ <script src="../mode/xml/xml.js"></script>
+ <script src="../mode/htmlmixed/htmlmixed.js"></script>
+ <script src="../mode/ruby/ruby.js"></script>
+ <script src="../mode/haml/haml.js"></script>
+ <script src="../mode/haml/test.js"></script>
+ <script src="../mode/markdown/markdown.js"></script>
+ <script src="../mode/markdown/test.js"></script>
+ <script src="../mode/gfm/gfm.js"></script>
+ <script src="../mode/gfm/test.js"></script>
+ <script src="../mode/stex/stex.js"></script>
+ <script src="../mode/stex/test.js"></script>
+ <script src="../mode/xquery/xquery.js"></script>
+ <script src="../mode/xquery/test.js"></script>
+ <script src="../addon/mode/multiplex_test.js"></script>-->
+
+ <script>
+ window.onload = runHarness;
+ CodeMirror.on(window, 'hashchange', runHarness);
+
+ function esc(str) {
+ return str.replace(/[<&]/, function(ch) { return ch == "<" ? "&lt;" : "&amp;"; });
+ }
+
+ var output = document.getElementById("output"),
+ progress = document.getElementById("progress"),
+ progressRan = document.getElementById("progress_ran").childNodes[0],
+ progressTotal = document.getElementById("progress_total").childNodes[0];
+
+ var count = 0,
+ failed = 0,
+ skipped = 0,
+ bad = "",
+ running = false, // Flag that states tests are running
+ quit = false, // Flag to quit tests ASAP
+ verbose = false, // Adds message for *every* test to output
+ phantom = false,
+ Pos = CodeMirror.Pos; // Required for VIM tests
+
+ function runHarness(){
+ if (running) {
+ quit = true;
+ setStatus("Restarting tests...", '', true);
+ setTimeout(function(){runHarness();}, 500);
+ return;
+ }
+ filters = [];
+ verbose = false;
+ if (window.location.hash.substr(1)){
+ var strings = window.location.hash.substr(1).split(",");
+ while (strings.length) {
+ var s = strings.shift();
+ if (s === "verbose")
+ verbose = true;
+ else
+ filters.push(parseTestFilter(decodeURIComponent(s)));
+ }
+ }
+ quit = false;
+ running = true;
+ setStatus("Loading tests...");
+ count = 0;
+ failed = 0;
+ skipped = 0;
+ bad = "";
+ totalTests = countTests();
+ progressTotal.nodeValue = " of " + totalTests;
+ progressRan.nodeValue = count;
+ output.innerHTML = '';
+ document.getElementById("testground").innerHTML = "<form>" +
+ "<textarea id=\"code\" name=\"code\"></textarea>" +
+ "<input type=submit value=ok name=submit>" +
+ "</form>";
+ runTests(displayTest);
+ }
+
+ function setStatus(message, className, force){
+ if (quit && !force) return;
+ if (!message) throw("must provide message");
+ var status = document.getElementById("status").childNodes[0];
+ status.nodeValue = message;
+ status.parentNode.className = className;
+ }
+ function addOutput(name, className, code){
+ var newOutput = document.createElement("dl");
+ var newTitle = document.createElement("dt");
+ newTitle.className = className;
+ newTitle.appendChild(document.createTextNode(name));
+ newOutput.appendChild(newTitle);
+ var newMessage = document.createElement("dd");
+ newMessage.innerHTML = code;
+ newOutput.appendChild(newTitle);
+ newOutput.appendChild(newMessage);
+ output.appendChild(newOutput);
+ }
+ function displayTest(type, name, customMessage) {
+ var message = "???";
+ if (type != "done" && type != "skipped") ++count;
+ progress.style.width = (count * (progress.parentNode.clientWidth - 2) / totalTests) + "px";
+ progressRan.nodeValue = count;
+ if (type == "ok") {
+ message = "Test '" + name + "' succeeded";
+ if (!verbose) customMessage = false;
+ } else if (type == "skipped") {
+ message = "Test '" + name + "' skipped";
+ ++skipped;
+ if (!verbose) customMessage = false;
+ } else if (type == "expected") {
+ message = "Test '" + name + "' failed as expected";
+ if (!verbose) customMessage = false;
+ } else if (type == "error" || type == "fail") {
+ ++failed;
+ message = "Test '" + name + "' failed";
+ } else if (type == "done") {
+ if (failed) {
+ type += " fail";
+ message = failed + " failure" + (failed > 1 ? "s" : "");
+ } else if (count < totalTests) {
+ failed = totalTests - count;
+ type += " fail";
+ message = failed + " failure" + (failed > 1 ? "s" : "");
+ } else {
+ type += " ok";
+ message = "All passed";
+ if (skipped) {
+ message += " (" + skipped + " skipped)";
+ }
+ }
+ progressTotal.nodeValue = '';
+ customMessage = true; // Hack to avoid adding to output
+ }
+ if (window.mozilla_setStatus)
+ mozilla_setStatus(message, type, customMessage);
+ if (verbose && !customMessage) customMessage = message;
+ setStatus(message, type);
+ if (customMessage && customMessage.length > 0) {
+ addOutput(name, type, customMessage);
+ }
+ }
+ </script>
+ </body>
+</html>
diff --git a/devtools/client/sourceeditor/test/css_autocompletion_tests.json b/devtools/client/sourceeditor/test/css_autocompletion_tests.json
new file mode 100644
index 000000000..70ec5be0e
--- /dev/null
+++ b/devtools/client/sourceeditor/test/css_autocompletion_tests.json
@@ -0,0 +1,39 @@
+// Test states to be tested for css state machine in css-autocompelter.js file.
+// Test cases are of the following format:
+// [
+// [
+// line, // The line location of the cursor
+// ch // The column locaiton of the cursor
+// ],
+// suggestions // Array of expected results
+// ]
+[
+ [[0, 10], []],
+ [[4, 7], ['.devtools-menulist', '.devtools-toolbarbutton']],
+ [[5, 8], ['-moz-animation', '-moz-animation-delay', '-moz-animation-direction',
+ '-moz-animation-duration', '-moz-animation-fill-mode',
+ '-moz-animation-iteration-count', '-moz-animation-name',
+ '-moz-animation-play-state', '-moz-animation-timing-function',
+ '-moz-appearance']],
+ [[12, 20], ['none', 'number-input']],
+ [[12, 22], ['none']],
+ [[17, 22], ['hsl', 'hsla']],
+ [[19, 10], ['background', 'background-attachment', 'background-blend-mode',
+ 'background-clip', 'background-color', 'background-image',
+ 'background-origin', 'background-position', 'background-position-x',
+ 'background-position-y', 'background-repeat', 'background-size']],
+ [[21, 9], ["-moz-calc", "auto", "calc", "inherit", "initial","unset"]],
+ [[25, 26], ['.devtools-toolbarbutton > tab',
+ '.devtools-toolbarbutton > hbox',
+ '.devtools-toolbarbutton > .toolbarbutton-menubutton-button']],
+ [[25, 31], ['.devtools-toolbarbutton > hbox.toolbarbutton-menubutton-button']],
+ [[29, 20], ['.devtools-menulist:after', '.devtools-menulist:active']],
+ [[30, 10], ['#devtools-anotherone', '#devtools-itjustgoeson', '#devtools-menu',
+ '#devtools-okstopitnow', '#devtools-toolbarbutton', '#devtools-yetagain']],
+ [[39, 39], ['.devtools-toolbarbutton:not([label]) > tab']],
+ [[43, 51], ['.devtools-toolbarbutton:not([checked=true]):hover:after',
+ '.devtools-toolbarbutton:not([checked=true]):hover:active']],
+ [[58, 36], ['!important;']],
+ [[73, 42], [':lang(', ':last-of-type', ':link', ':last-child']],
+ [[77, 25], ['.visible']],
+]
diff --git a/devtools/client/sourceeditor/test/css_statemachine_testcases.css b/devtools/client/sourceeditor/test/css_statemachine_testcases.css
new file mode 100644
index 000000000..b3149030f
--- /dev/null
+++ b/devtools/client/sourceeditor/test/css_statemachine_testcases.css
@@ -0,0 +1,121 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.devtools-toolbar {
+ -moz-appearance: none;
+ padding:4px 3px;border-bottom-width: 1px;
+ border-bottom-style: solid;
+}
+
+#devtools-menu.devtools-menulist,
+.devtools-toolbarbutton#devtools-menu {
+ -moz-appearance: none;
+ -moz-box-align: center;
+ min-width: 78px;
+ min-height: 22px;
+ text-shadow: 0 -1px 0 hsla(210,8%,5%,.45);
+ border: 1px solid hsla(210,8%,5%,.45);
+ border-radius: 3px;
+ background: linear-gradient(hsla(212,7%,57%,.35), hsla(212,7%,57%,.1)) padding-box;
+ box-shadow: 0 1px 0 hsla(210,16%,76%,.15) inset, 0 0 0 1px hsla(210,16%,76%,.15) inset, 0 1px 0 hsla(210,16%,76%,.15);
+ margin: 0 3px;
+ color: inherit;
+}
+
+.devtools-toolbarbutton > hbox.toolbarbutton-menubutton-button {
+ -moz-box-orient: horizontal;
+}
+
+.devtools-menulist:active,
+#devtools-toolbarbutton:focus {
+ outline: 1px dotted hsla(210,30%,85%,0.7);
+ outline-offset : -4px;
+}
+
+.devtools-toolbarbutton:not([label]) {
+ min-width: 32px;
+}
+
+.devtools-toolbarbutton:not([label]) > .toolbarbutton-text {
+ display: none;
+}
+
+.devtools-toolbarbutton:not([checked=true]):hover:active {
+ border-color: hsla(210,8%,5%,.6);
+}
+
+.devtools-menulist["open" ="true"],
+.devtools-toolbarbutton["open" = true],
+.devtools-toolbarbutton[checked= "true"] {
+ border-color: hsla(210,8%,5%,.6) !important;
+}
+
+.devtools-toolbarbutton["checked"="true"] {
+ color: hsl(208,100%,60%);
+}
+
+.devtools-toolbarbutton[checked=true]:hover {
+ background-color: transparent !important;
+}
+
+.devtools-toolbarbutton[checked=true]:hover:active {
+ background-color: hsla(210,8%,5%,.2) !important;
+}
+
+.devtools-toolbarbutton[type=menu-button] > .toolbarbutton-menubutton-button {
+ -moz-appearance: none;
+}
+
+.devtools-sidebar-tabs > tabs > tab:first-of-type {
+ margin-inline-start: -3px;
+}
+
+.devtools-sidebar-tabs > tabs > tab:not(:last-of-type) {
+ background-size: calc(100% - 2px) 100%, 1px 100%;
+}
+
+.hidden-labels-box:not(.visible) > label,
+.hidden-labels-box.visible ~ .hidden-labels-box > label:last-child {
+ display: none;
+}
+
+/* Maximize the size of the viewport when the window is small */
+@media (max-width: 800px) {
+ .category-name {
+ display: none;
+ }
+}
+
+@media all and (min-width: 300px) {
+ #error-box {
+ max-width: 50%;
+ margin: 0 auto;
+ background-image: url('chrome://global/skin/icons/information-32.png');
+ min-height: 36px;
+ padding-inline-start: 38px;
+ }
+
+ button {
+ width: auto !important;
+ min-width: 150px;
+ }
+
+ @keyframes downloadsIndicatorNotificationFinish {
+ from { opacity: 0; transform: scale(1); }
+ 20% {
+ opacity: .65;
+ animation-timing-function: ease-in;
+ } to { opacity: 0;
+ transform: scale(8); }
+ }
+}
+
+@keyframes smooth {
+ from { opacity: 0; transform: scale(1); }
+ 20% { opacity: .65; animation-timing-function: ease-in; }
+ to {
+ opacity : 0;
+ transform: scale(8);
+ }
+}
diff --git a/devtools/client/sourceeditor/test/css_statemachine_tests.json b/devtools/client/sourceeditor/test/css_statemachine_tests.json
new file mode 100644
index 000000000..2e2574b36
--- /dev/null
+++ b/devtools/client/sourceeditor/test/css_statemachine_tests.json
@@ -0,0 +1,84 @@
+// Test states to be tested for css state machine in css-autocompelter.js file.
+// Test cases are of the following format:
+// [
+// [
+// line, // The line location of the cursor
+// ch // The column locaiton of the cursor
+// ],
+// [
+// state, // one of CSS_STATES
+// selectorState, // one of SELECTOR_STATES
+// completing, // what is being completed
+// propertyName, // what property is being completed in case of value state
+// // or the current selector that is being completed
+// ]
+// ]
+[
+ [[0, 10], ['null', '', '', '']],
+ [[4, 3], ['selector', 'class', 'de', '.de']],
+ [[5, 8], ['property', 'null', '-moz-a']],
+ [[5, 21], ['value', 'null', 'no', '-moz-appearance']],
+ [[6, 18], ['property', 'null', 'padding']],
+ [[6, 24], ['value', 'null', '3', 'padding']],
+ [[6, 29], ['property', 'null', 'bo']],
+ [[6, 50], ['value', 'null', '1p', 'border-bottom-width']],
+ [[7, 24], ['value', 'null', 's', 'border-bottom-style']],
+ [[9, 0], ['null', 'null', '', '']],
+ [[10, 6], ['selector', 'id', 'devto', '#devto']],
+ [[10, 17], ['selector', 'class', 'de', '#devtools-menu.de']],
+ [[11, 5], ['selector', 'class', 'devt', '.devt']],
+ [[11, 30], ['selector', 'id', 'devtoo', '.devtools-toolbarbutton#devtoo']],
+ [[12, 10], ['property', 'null', '-moz-app']],
+ [[16, 27], ['value', 'null', 'hsl', 'text-shadow']],
+ [[19, 24], ['value', 'null', 'linear-gra', 'background']],
+ [[19, 55], ['value', 'null', 'hsl', 'background']],
+ [[19, 79], ['value', 'null', 'paddin', 'background']],
+ [[20, 47], ['value', 'null', 'ins', 'box-shadow']],
+ [[22, 15], ['value', 'null', 'inheri', 'color']],
+ [[25, 26], ['selector', 'null', '', '.devtools-toolbarbutton > ']],
+ [[25, 28], ['selector', 'tag', 'hb', '.devtools-toolbarbutton > hb']],
+ [[25, 41], ['selector', 'class', 'toolbarbut', '.devtools-toolbarbutton > hbox.toolbarbut']],
+ [[29, 21], ['selector', 'pseudo', 'ac', '.devtools-menulist:ac']],
+ [[30, 27], ['selector', 'pseudo', 'foc', '#devtools-toolbarbutton:foc']],
+ [[31, 18], ['value', 'null', 'dot', 'outline']],
+ [[32, 25], ['value', 'null', '-4p', 'outline-offset']],
+ [[35, 26], ['selector', 'pseudo', 'no', '.devtools-toolbarbutton:no']],
+ [[35, 28], ['selector', 'null', 'not', '']],
+ [[35, 30], ['selector', 'attribute', 'l', '[l']],
+ [[39, 46], ['selector', 'class', 'toolba', '.devtools-toolbarbutton:not([label]) > .toolba']],
+ [[43, 39], ['selector', 'value', 'tr', '[checked=tr']],
+ [[43, 47], ['selector', 'pseudo', 'hov', '.devtools-toolbarbutton:not([checked=true]):hov']],
+ [[43, 53], ['selector', 'pseudo', 'act', '.devtools-toolbarbutton:not([checked=true]):hover:act']],
+ [[47, 22], ['selector', 'attribute', 'op', '.devtools-menulist[op']],
+ [[47, 33], ['selector', 'value', 'tr', '.devtools-menulist[open =tr']],
+ [[48, 38], ['selector', 'value', 'tr', '.devtools-toolbarbutton[open = tr']],
+ [[49, 40], ['selector', 'value', 'true', '.devtools-toolbarbutton[checked= true']],
+ [[53, 34], ['selector', 'value', '=', '.devtools-toolbarbutton[checked=']],
+ [[58, 38], ['value', 'null', '!impor', 'background-color']],
+ [[61, 41], ['selector', 'pseudo', 'hov', '.devtools-toolbarbutton[checked=true]:hov']],
+ [[65, 47], ['selector', 'class', 'to', '.devtools-toolbarbutton[type=menu-button] > .to']],
+ [[69, 44], ['selector', 'pseudo', 'first-of', '.devtools-sidebar-tabs > tabs > tab:first-of']],
+ [[73, 45], ['selector', 'pseudo', 'last', ':last']],
+ [[77, 27], ['selector', 'class', 'vis', '.vis']],
+ [[78, 34], ['selector', 'class', 'hidd', '.hidden-labels-box.visible ~ .hidd']],
+ [[83, 5], ['media', 'null', 'medi']],
+ [[83, 22], ['media', 'null', '800']],
+ [[84, 9], ['selector', 'class', 'catego', '.catego']],
+ [[89, 9], ['media', 'null', 'al']],
+ [[90, 6], ['selector', 'id', 'err', '#err']],
+ [[93, 11], ['property', 'null', 'backgro']],
+ [[98, 6], ['selector', 'tag', 'butt', 'butt']],
+ [[99, 22], ['value', 'null', '!impor', 'width']],
+ [[103, 5], ['keyframes', 'null', 'ke']],
+ [[104, 7], ['frame', 'null', 'fro']],
+ [[104, 15], ['property', 'null', 'opac']],
+ [[104, 29], ['property', 'null', 'transf']],
+ [[104, 38], ['value', 'null', 'scal', 'transform']],
+ [[105, 8], ['frame', 'null', '']],
+ [[113, 6], ['keyframes', 'null', 'keyfr']],
+ [[114, 4], ['frame', 'null', 'fr']],
+ [[115, 3], ['frame', 'null', '2']],
+ [[117, 8], ['property', 'null', 'opac']],
+ [[117, 16], ['value', 'null', '0', 'opacity']],
+ [[121, 0], ['null', '', '']],
+]
diff --git a/devtools/client/sourceeditor/test/head.js b/devtools/client/sourceeditor/test/head.js
new file mode 100644
index 000000000..4f8473eaf
--- /dev/null
+++ b/devtools/client/sourceeditor/test/head.js
@@ -0,0 +1,163 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from ../../framework/test/shared-head.js */
+"use strict";
+
+// shared-head.js handles imports, constants, and utility functions
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
+ this);
+
+const { NetUtil } = require("resource://gre/modules/NetUtil.jsm");
+const Editor = require("devtools/client/sourceeditor/editor");
+const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
+
+flags.testing = true;
+SimpleTest.registerCleanupFunction(() => {
+ flags.testing = false;
+});
+
+function promiseWaitForFocus() {
+ return new Promise(resolve =>
+ waitForFocus(resolve));
+}
+
+function setup(cb, additionalOpts = {}) {
+ cb = cb || function () {};
+ let def = promise.defer();
+ const opt = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+ const url = "data:application/vnd.mozilla.xul+xml;charset=UTF-8," +
+ "<?xml version='1.0'?>" +
+ "<?xml-stylesheet href='chrome://global/skin/global.css'?>" +
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper" +
+ "/there.is.only.xul' title='Editor' width='600' height='500'>" +
+ "<box flex='1'/></window>";
+
+ let win = Services.ww.openWindow(null, url, "_blank", opt, null);
+ let opts = {
+ value: "Hello.",
+ lineNumbers: true,
+ foldGutter: true,
+ gutters: ["CodeMirror-linenumbers", "breakpoints", "CodeMirror-foldgutter"],
+ cssProperties: getClientCssProperties()
+ };
+
+ for (let o in additionalOpts) {
+ opts[o] = additionalOpts[o];
+ }
+
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad, false);
+
+ waitForFocus(function () {
+ let box = win.document.querySelector("box");
+ let editor = new Editor(opts);
+
+ editor.appendTo(box)
+ .then(() => {
+ def.resolve({
+ ed: editor,
+ win: win,
+ edWin: editor.container.contentWindow.wrappedJSObject
+ });
+ cb(editor, win);
+ }, err => ok(false, err.message));
+ }, win);
+ }, false);
+
+ return def.promise;
+}
+
+function ch(exp, act, label) {
+ is(exp.line, act.line, label + " (line)");
+ is(exp.ch, act.ch, label + " (ch)");
+}
+
+function teardown(ed, win) {
+ ed.destroy();
+ win.close();
+
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+ finish();
+}
+
+/**
+ * Some tests may need to import one or more of the test helper scripts.
+ * A test helper script is simply a js file that contains common test code that
+ * is either not common-enough to be in head.js, or that is located in a
+ * separate directory.
+ * The script will be loaded synchronously and in the test's scope.
+ * @param {String} filePath The file path, relative to the current directory.
+ * Examples:
+ * - "helper_attributes_test_runner.js"
+ * - "../../../commandline/test/helpers.js"
+ */
+function loadHelperScript(filePath) {
+ let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
+ Services.scriptloader.loadSubScript(testDir + "/" + filePath, this);
+}
+
+/**
+ * This method returns the portion of the input string `source` up to the
+ * [line, ch] location.
+ */
+function limit(source, [line, ch]) {
+ line++;
+ let list = source.split("\n");
+ if (list.length < line) {
+ return source;
+ }
+ if (line == 1) {
+ return list[0].slice(0, ch);
+ }
+ return [...list.slice(0, line - 1), list[line - 1].slice(0, ch)].join("\n");
+}
+
+function read(url) {
+ let scriptableStream = Cc["@mozilla.org/scriptableinputstream;1"]
+ .getService(Ci.nsIScriptableInputStream);
+
+ let channel = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true
+ });
+ let input = channel.open2();
+ scriptableStream.init(input);
+
+ let data = "";
+ while (input.available()) {
+ data = data.concat(scriptableStream.read(input.available()));
+ }
+ scriptableStream.close();
+ input.close();
+
+ return data;
+}
+
+/**
+ * This function is called by the CodeMirror test runner to report status
+ * messages from the CM tests.
+ * @see codemirror.html
+ */
+function codemirrorSetStatus(statusMsg, type, customMsg) {
+ switch (type) {
+ case "expected":
+ case "ok":
+ ok(1, statusMsg);
+ break;
+ case "error":
+ case "fail":
+ ok(0, statusMsg);
+ break;
+ default:
+ info(statusMsg);
+ break;
+ }
+
+ if (customMsg && typeof customMsg == "string" && customMsg != statusMsg) {
+ info(customMsg);
+ }
+}
diff --git a/devtools/client/sourceeditor/test/helper_codemirror_runner.js b/devtools/client/sourceeditor/test/helper_codemirror_runner.js
new file mode 100644
index 000000000..b9e458472
--- /dev/null
+++ b/devtools/client/sourceeditor/test/helper_codemirror_runner.js
@@ -0,0 +1,38 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* globals codemirrorSetStatus */
+
+"use strict";
+
+function runCodeMirrorTest(browser) {
+ let mm = browser.messageManager;
+ mm.addMessageListener("setStatus", function listener({data}) {
+ let {statusMsg, type, customMsg} = data;
+ codemirrorSetStatus(statusMsg, type, customMsg);
+ });
+ mm.addMessageListener("done", function listener({data}) {
+ ok(!data.failed, "CodeMirror tests all passed");
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+ mm = null;
+ finish();
+ });
+
+ // Interact with the content iframe, giving it a function to
+ // 1) Proxy CM test harness calls into ok() calls
+ // 2) Detecting when it finishes by checking the DOM and
+ // setting a timeout to check again if not.
+ mm.loadFrameScript("data:," +
+ "content.wrappedJSObject.mozilla_setStatus = function(statusMsg, type, customMsg) {" +
+ " sendSyncMessage('setStatus', {statusMsg: statusMsg, type: type, customMsg: customMsg});" +
+ "};" +
+ "function check() { " +
+ " var doc = content.document; var out = doc.getElementById('status'); " +
+ " if (!out || !out.classList.contains('done')) { return setTimeout(check, 100); }" +
+ " sendAsyncMessage('done', { failed: content.wrappedJSObject.failed });" +
+ "}" +
+ "check();"
+ , true);
+}