browser/devtools/sourceeditor/editor.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* vim:set ts=2 sw=2 sts=2 et tw=80:
     2  * This Source Code Form is subject to the terms of the Mozilla Public
     3  * License, v. 2.0. If a copy of the MPL was not distributed with this
     4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     6 "use strict";
     8 const { Cu, Cc, Ci, components } = require("chrome");
    10 const TAB_SIZE    = "devtools.editor.tabsize";
    11 const EXPAND_TAB  = "devtools.editor.expandtab";
    12 const KEYMAP      = "devtools.editor.keymap";
    13 const AUTO_CLOSE  = "devtools.editor.autoclosebrackets";
    14 const DETECT_INDENT = "devtools.editor.detectindentation";
    15 const DETECT_INDENT_MAX_LINES = 500;
    16 const L10N_BUNDLE = "chrome://browser/locale/devtools/sourceeditor.properties";
    17 const XUL_NS      = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
    19 // Maximum allowed margin (in number of lines) from top or bottom of the editor
    20 // while shifting to a line which was initially out of view.
    21 const MAX_VERTICAL_OFFSET = 3;
    23 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
    24 const events  = require("devtools/toolkit/event-emitter");
    26 Cu.import("resource://gre/modules/Services.jsm");
    27 const L10N = Services.strings.createBundle(L10N_BUNDLE);
    29 // CM_STYLES, CM_SCRIPTS and CM_IFRAME represent the HTML,
    30 // JavaScript and CSS that is injected into an iframe in
    31 // order to initialize a CodeMirror instance.
    33 const CM_STYLES   = [
    34   "chrome://browser/skin/devtools/common.css",
    35   "chrome://browser/content/devtools/codemirror/codemirror.css",
    36   "chrome://browser/content/devtools/codemirror/dialog.css",
    37   "chrome://browser/content/devtools/codemirror/mozilla.css"
    38 ];
    40 const CM_SCRIPTS  = [
    41   "chrome://browser/content/devtools/theme-switching.js",
    42   "chrome://browser/content/devtools/codemirror/codemirror.js",
    43   "chrome://browser/content/devtools/codemirror/dialog.js",
    44   "chrome://browser/content/devtools/codemirror/searchcursor.js",
    45   "chrome://browser/content/devtools/codemirror/search.js",
    46   "chrome://browser/content/devtools/codemirror/matchbrackets.js",
    47   "chrome://browser/content/devtools/codemirror/closebrackets.js",
    48   "chrome://browser/content/devtools/codemirror/comment.js",
    49   "chrome://browser/content/devtools/codemirror/javascript.js",
    50   "chrome://browser/content/devtools/codemirror/xml.js",
    51   "chrome://browser/content/devtools/codemirror/css.js",
    52   "chrome://browser/content/devtools/codemirror/htmlmixed.js",
    53   "chrome://browser/content/devtools/codemirror/clike.js",
    54   "chrome://browser/content/devtools/codemirror/activeline.js",
    55   "chrome://browser/content/devtools/codemirror/trailingspace.js",
    56   "chrome://browser/content/devtools/codemirror/emacs.js",
    57   "chrome://browser/content/devtools/codemirror/vim.js",
    58   "chrome://browser/content/devtools/codemirror/sublime.js",
    59   "chrome://browser/content/devtools/codemirror/foldcode.js",
    60   "chrome://browser/content/devtools/codemirror/brace-fold.js",
    61   "chrome://browser/content/devtools/codemirror/comment-fold.js",
    62   "chrome://browser/content/devtools/codemirror/xml-fold.js",
    63   "chrome://browser/content/devtools/codemirror/foldgutter.js"
    64 ];
    66 const CM_IFRAME   =
    67   "data:text/html;charset=utf8,<!DOCTYPE html>" +
    68   "<html dir='ltr'>" +
    69   "  <head>" +
    70   "    <style>" +
    71   "      html, body { height: 100%; }" +
    72   "      body { margin: 0; overflow: hidden; }" +
    73   "      .CodeMirror { width: 100%; height: 100% !important; line-height: 1.25 !important;}" +
    74   "    </style>" +
    75 [ "    <link rel='stylesheet' href='" + style + "'>" for (style of CM_STYLES) ].join("\n") +
    76   "  </head>" +
    77   "  <body class='theme-body devtools-monospace'></body>" +
    78   "</html>";
    80 const CM_MAPPING = [
    81   "focus",
    82   "hasFocus",
    83   "lineCount",
    84   "somethingSelected",
    85   "getCursor",
    86   "setSelection",
    87   "getSelection",
    88   "replaceSelection",
    89   "extendSelection",
    90   "undo",
    91   "redo",
    92   "clearHistory",
    93   "openDialog",
    94   "refresh",
    95   "getScrollInfo",
    96   "getOption",
    97   "setOption"
    98 ];
   100 const { cssProperties, cssValues, cssColors } = getCSSKeywords();
   102 const editors = new WeakMap();
   104 Editor.modes = {
   105   text: { name: "text" },
   106   html: { name: "htmlmixed" },
   107   css:  { name: "css" },
   108   js:   { name: "javascript" },
   109   vs:   { name: "x-shader/x-vertex" },
   110   fs:   { name: "x-shader/x-fragment" }
   111 };
   113 /**
   114  * A very thin wrapper around CodeMirror. Provides a number
   115  * of helper methods to make our use of CodeMirror easier and
   116  * another method, appendTo, to actually create and append
   117  * the CodeMirror instance.
   118  *
   119  * Note that Editor doesn't expose CodeMirror instance to the
   120  * outside world.
   121  *
   122  * Constructor accepts one argument, config. It is very
   123  * similar to the CodeMirror configuration object so for most
   124  * properties go to CodeMirror's documentation (see below).
   125  *
   126  * Other than that, it accepts one additional and optional
   127  * property contextMenu. This property should be an ID of
   128  * an element we can use as a context menu.
   129  *
   130  * This object is also an event emitter.
   131  *
   132  * CodeMirror docs: http://codemirror.net/doc/manual.html
   133  */
   134 function Editor(config) {
   135   const tabSize = Services.prefs.getIntPref(TAB_SIZE);
   136   const useTabs = !Services.prefs.getBoolPref(EXPAND_TAB);
   137   const keyMap = Services.prefs.getCharPref(KEYMAP);
   138   const useAutoClose = Services.prefs.getBoolPref(AUTO_CLOSE);
   140   this.version = null;
   141   this.config = {
   142     value:             "",
   143     mode:              Editor.modes.text,
   144     indentUnit:        tabSize,
   145     tabSize:           tabSize,
   146     contextMenu:       null,
   147     matchBrackets:     true,
   148     extraKeys:         {},
   149     indentWithTabs:    useTabs,
   150     styleActiveLine:   true,
   151     autoCloseBrackets: "()[]{}''\"\"",
   152     autoCloseEnabled:  useAutoClose,
   153     theme:             "mozilla"
   154   };
   156   // Additional shortcuts.
   157   this.config.extraKeys[Editor.keyFor("jumpToLine")] = () => this.jumpToLine();
   158   this.config.extraKeys[Editor.keyFor("moveLineUp", { noaccel: true })] = () => this.moveLineUp();
   159   this.config.extraKeys[Editor.keyFor("moveLineDown", { noaccel: true })] = () => this.moveLineDown();
   160   this.config.extraKeys[Editor.keyFor("toggleComment")] = "toggleComment";
   162   // Disable ctrl-[ and ctrl-] because toolbox uses those shortcuts.
   163   this.config.extraKeys[Editor.keyFor("indentLess")] = false;
   164   this.config.extraKeys[Editor.keyFor("indentMore")] = false;
   166   // If alternative keymap is provided, use it.
   167   if (keyMap === "emacs" || keyMap === "vim" || keyMap === "sublime")
   168     this.config.keyMap = keyMap;
   170   // Overwrite default config with user-provided, if needed.
   171   Object.keys(config).forEach((k) => {
   172     if (k != "extraKeys") {
   173       this.config[k] = config[k];
   174       return;
   175     }
   177     if (!config.extraKeys)
   178       return;
   180     Object.keys(config.extraKeys).forEach((key) => {
   181       this.config.extraKeys[key] = config.extraKeys[key];
   182     });
   183   });
   185   // Set the code folding gutter, if needed.
   186   if (this.config.enableCodeFolding) {
   187     this.config.foldGutter = true;
   189     if (!this.config.gutters) {
   190       this.config.gutters = this.config.lineNumbers ? ["CodeMirror-linenumbers"] : [];
   191       this.config.gutters.push("CodeMirror-foldgutter");
   192     }
   193   }
   195   // Configure automatic bracket closing.
   196   if (!this.config.autoCloseEnabled)
   197     this.config.autoCloseBrackets = false;
   199   // Overwrite default tab behavior. If something is selected,
   200   // indent those lines. If nothing is selected and we're
   201   // indenting with tabs, insert one tab. Otherwise insert N
   202   // whitespaces where N == indentUnit option.
   203   this.config.extraKeys.Tab = (cm) => {
   204     if (cm.somethingSelected()) {
   205       cm.indentSelection("add");
   206       return;
   207     }
   209     if (this.config.indentWithTabs) {
   210       cm.replaceSelection("\t", "end", "+input");
   211       return;
   212     }
   214     var num = cm.getOption("indentUnit");
   215     if (cm.getCursor().ch !== 0) num -= 1;
   216     cm.replaceSelection(" ".repeat(num), "end", "+input");
   217   };
   219   events.decorate(this);
   220 }
   222 Editor.prototype = {
   223   container: null,
   224   version: null,
   225   config: null,
   227   /**
   228    * Appends the current Editor instance to the element specified by
   229    * 'el'. You can also provide your won iframe to host the editor as
   230    * an optional second parameter. This method actually creates and
   231    * loads CodeMirror and all its dependencies.
   232    *
   233    * This method is asynchronous and returns a promise.
   234    */
   235   appendTo: function (el, env) {
   236     let def = promise.defer();
   237     let cm  = editors.get(this);
   239     if (!env)
   240       env = el.ownerDocument.createElementNS(XUL_NS, "iframe");
   242     env.flex = 1;
   244     if (cm)
   245       throw new Error("You can append an editor only once.");
   247     let onLoad = () => {
   248       // Once the iframe is loaded, we can inject CodeMirror
   249       // and its dependencies into its DOM.
   251       env.removeEventListener("load", onLoad, true);
   252       let win = env.contentWindow.wrappedJSObject;
   254       CM_SCRIPTS.forEach((url) =>
   255         Services.scriptloader.loadSubScript(url, win, "utf8"));
   257       // Replace the propertyKeywords, colorKeywords and valueKeywords
   258       // properties of the CSS MIME type with the values provided by Gecko.
   259       let cssSpec = win.CodeMirror.resolveMode("text/css");
   260       cssSpec.propertyKeywords = cssProperties;
   261       cssSpec.colorKeywords = cssColors;
   262       cssSpec.valueKeywords = cssValues;
   263       win.CodeMirror.defineMIME("text/css", cssSpec);
   265       let scssSpec = win.CodeMirror.resolveMode("text/x-scss");
   266       scssSpec.propertyKeywords = cssProperties;
   267       scssSpec.colorKeywords = cssColors;
   268       scssSpec.valueKeywords = cssValues;
   269       win.CodeMirror.defineMIME("text/x-scss", scssSpec);
   271       win.CodeMirror.commands.save = () => this.emit("save");
   273       // Create a CodeMirror instance add support for context menus,
   274       // overwrite the default controller (otherwise items in the top and
   275       // context menus won't work).
   277       cm = win.CodeMirror(win.document.body, this.config);
   278       cm.getWrapperElement().addEventListener("contextmenu", (ev) => {
   279         ev.preventDefault();
   280         if (!this.config.contextMenu) return;
   281         let popup = el.ownerDocument.getElementById(this.config.contextMenu);
   282         popup.openPopupAtScreen(ev.screenX, ev.screenY, true);
   283       }, false);
   285       cm.on("focus", () => this.emit("focus"));
   286       cm.on("scroll", () => this.emit("scroll"));
   287       cm.on("change", () => {
   288         this.emit("change");
   289         if (!this._lastDirty) {
   290           this._lastDirty = true;
   291           this.emit("dirty-change");
   292         }
   293       });
   294       cm.on("cursorActivity", (cm) => this.emit("cursorActivity"));
   296       cm.on("gutterClick", (cm, line, gutter, ev) => {
   297         let head = { line: line, ch: 0 };
   298         let tail = { line: line, ch: this.getText(line).length };
   300         // Shift-click on a gutter selects the whole line.
   301         if (ev.shiftKey) {
   302           cm.setSelection(head, tail);
   303           return;
   304         }
   306         this.emit("gutterClick", line);
   307       });
   309       win.CodeMirror.defineExtension("l10n", (name) => {
   310         return L10N.GetStringFromName(name);
   311       });
   313       cm.getInputField().controllers.insertControllerAt(0, controller(this));
   315       this.container = env;
   316       editors.set(this, cm);
   318       this.resetIndentUnit();
   320       def.resolve();
   321     };
   323     env.addEventListener("load", onLoad, true);
   324     env.setAttribute("src", CM_IFRAME);
   325     el.appendChild(env);
   327     this.once("destroy", () => el.removeChild(env));
   328     return def.promise;
   329   },
   331   /**
   332    * Returns the currently active highlighting mode.
   333    * See Editor.modes for the list of all suppoert modes.
   334    */
   335   getMode: function () {
   336     return this.getOption("mode");
   337   },
   339   /**
   340    * Changes the value of a currently used highlighting mode.
   341    * See Editor.modes for the list of all suppoert modes.
   342    */
   343   setMode: function (value) {
   344     this.setOption("mode", value);
   345   },
   347   /**
   348    * Returns text from the text area. If line argument is provided
   349    * the method returns only that line.
   350    */
   351   getText: function (line) {
   352     let cm = editors.get(this);
   354     if (line == null)
   355       return cm.getValue();
   357     let info = cm.lineInfo(line);
   358     return info ? cm.lineInfo(line).text : "";
   359   },
   361   /**
   362    * Replaces whatever is in the text area with the contents of
   363    * the 'value' argument.
   364    */
   365   setText: function (value) {
   366     let cm = editors.get(this);
   367     cm.setValue(value);
   369     this.resetIndentUnit();
   370   },
   372   /**
   373    * Set the editor's indentation based on the current prefs and
   374    * re-detect indentation if we should.
   375    */
   376   resetIndentUnit: function() {
   377     let cm = editors.get(this);
   379     let indentWithTabs = !Services.prefs.getBoolPref(EXPAND_TAB);
   380     let indentUnit = Services.prefs.getIntPref(TAB_SIZE);
   381     let shouldDetect = Services.prefs.getBoolPref(DETECT_INDENT);
   383     cm.setOption("tabSize", indentUnit);
   385     if (shouldDetect) {
   386       let indent = detectIndentation(this);
   387       if (indent != null) {
   388         indentWithTabs = indent.tabs;
   389         indentUnit = indent.spaces ? indent.spaces : indentUnit;
   390       }
   391     }
   393     cm.setOption("indentUnit", indentUnit);
   394     cm.setOption("indentWithTabs", indentWithTabs);
   395   },
   397   /**
   398    * Replaces contents of a text area within the from/to {line, ch}
   399    * range. If neither from nor to arguments are provided works
   400    * exactly like setText. If only from object is provided, inserts
   401    * text at that point, *overwriting* as many characters as needed.
   402    */
   403   replaceText: function (value, from, to) {
   404     let cm = editors.get(this);
   406     if (!from) {
   407       this.setText(value);
   408       return;
   409     }
   411     if (!to) {
   412       let text = cm.getRange({ line: 0, ch: 0 }, from);
   413       this.setText(text + value);
   414       return;
   415     }
   417     cm.replaceRange(value, from, to);
   418   },
   420   /**
   421    * Inserts text at the specified {line, ch} position, shifting existing
   422    * contents as necessary.
   423    */
   424   insertText: function (value, at) {
   425     let cm = editors.get(this);
   426     cm.replaceRange(value, at, at);
   427   },
   429   /**
   430    * Deselects contents of the text area.
   431    */
   432   dropSelection: function () {
   433     if (!this.somethingSelected())
   434       return;
   436     this.setCursor(this.getCursor());
   437   },
   439   /**
   440    * Returns true if there is more than one selection in the editor.
   441    */
   442   hasMultipleSelections: function () {
   443     let cm = editors.get(this);
   444     return cm.listSelections().length > 1;
   445   },
   447   /**
   448    * Gets the first visible line number in the editor.
   449    */
   450   getFirstVisibleLine: function () {
   451     let cm = editors.get(this);
   452     return cm.lineAtHeight(0, "local");
   453   },
   455   /**
   456    * Scrolls the view such that the given line number is the first visible line.
   457    */
   458   setFirstVisibleLine: function (line) {
   459     let cm = editors.get(this);
   460     let { top } = cm.charCoords({line: line, ch: 0}, "local");
   461     cm.scrollTo(0, top);
   462   },
   464   /**
   465    * Sets the cursor to the specified {line, ch} position with an additional
   466    * option to align the line at the "top", "center" or "bottom" of the editor
   467    * with "top" being default value.
   468    */
   469   setCursor: function ({line, ch}, align) {
   470     let cm = editors.get(this);
   471     this.alignLine(line, align);
   472     cm.setCursor({line: line, ch: ch});
   473   },
   475   /**
   476    * Aligns the provided line to either "top", "center" or "bottom" of the
   477    * editor view with a maximum margin of MAX_VERTICAL_OFFSET lines from top or
   478    * bottom.
   479    */
   480   alignLine: function(line, align) {
   481     let cm = editors.get(this);
   482     let from = cm.lineAtHeight(0, "page");
   483     let to = cm.lineAtHeight(cm.getWrapperElement().clientHeight, "page");
   484     let linesVisible = to - from;
   485     let halfVisible = Math.round(linesVisible/2);
   487     // If the target line is in view, skip the vertical alignment part.
   488     if (line <= to && line >= from) {
   489       return;
   490     }
   492     // Setting the offset so that the line always falls in the upper half
   493     // of visible lines (lower half for bottom aligned).
   494     // MAX_VERTICAL_OFFSET is the maximum allowed value.
   495     let offset = Math.min(halfVisible, MAX_VERTICAL_OFFSET);
   497     let topLine = {
   498       "center": Math.max(line - halfVisible, 0),
   499       "bottom": Math.max(line - linesVisible + offset, 0),
   500       "top": Math.max(line - offset, 0)
   501     }[align || "top"] || offset;
   503     // Bringing down the topLine to total lines in the editor if exceeding.
   504     topLine = Math.min(topLine, this.lineCount());
   505     this.setFirstVisibleLine(topLine);
   506   },
   508   /**
   509    * Returns whether a marker of a specified class exists in a line's gutter.
   510    */
   511   hasMarker: function (line, gutterName, markerClass) {
   512     let cm = editors.get(this);
   513     let info = cm.lineInfo(line);
   514     if (!info)
   515       return false;
   517     let gutterMarkers = info.gutterMarkers;
   518     if (!gutterMarkers)
   519       return false;
   521     let marker = gutterMarkers[gutterName];
   522     if (!marker)
   523       return false;
   525     return marker.classList.contains(markerClass);
   526   },
   528   /**
   529    * Adds a marker with a specified class to a line's gutter. If another marker
   530    * exists on that line, the new marker class is added to its class list.
   531    */
   532   addMarker: function (line, gutterName, markerClass) {
   533     let cm = editors.get(this);
   534     let info = cm.lineInfo(line);
   535     if (!info)
   536       return;
   538     let gutterMarkers = info.gutterMarkers;
   539     if (gutterMarkers) {
   540       let marker = gutterMarkers[gutterName];
   541       if (marker) {
   542         marker.classList.add(markerClass);
   543         return;
   544       }
   545     }
   547     let marker = cm.getWrapperElement().ownerDocument.createElement("div");
   548     marker.className = markerClass;
   549     cm.setGutterMarker(info.line, gutterName, marker);
   550   },
   552   /**
   553    * The reverse of addMarker. Removes a marker of a specified class from a
   554    * line's gutter.
   555    */
   556   removeMarker: function (line, gutterName, markerClass) {
   557     if (!this.hasMarker(line, gutterName, markerClass))
   558       return;
   560     let cm = editors.get(this);
   561     cm.lineInfo(line).gutterMarkers[gutterName].classList.remove(markerClass);
   562   },
   564   /**
   565    * Remove all gutter markers in the gutter with the given name.
   566    */
   567   removeAllMarkers: function (gutterName) {
   568     let cm = editors.get(this);
   569     cm.clearGutter(gutterName);
   570   },
   572   /**
   573    * Handles attaching a set of events listeners on a marker. They should
   574    * be passed as an object literal with keys as event names and values as
   575    * function listeners. The line number, marker node and optional data
   576    * will be passed as arguments to the function listener.
   577    *
   578    * You don't need to worry about removing these event listeners.
   579    * They're automatically orphaned when clearing markers.
   580    */
   581   setMarkerListeners: function(line, gutterName, markerClass, events, data) {
   582     if (!this.hasMarker(line, gutterName, markerClass))
   583       return;
   585     let cm = editors.get(this);
   586     let marker = cm.lineInfo(line).gutterMarkers[gutterName];
   588     for (let name in events) {
   589       let listener = events[name].bind(this, line, marker, data);
   590       marker.addEventListener(name, listener);
   591     }
   592   },
   594   /**
   595    * Returns whether a line is decorated using the specified class name.
   596    */
   597   hasLineClass: function (line, className) {
   598     let cm = editors.get(this);
   599     let info = cm.lineInfo(line);
   601     if (!info || !info.wrapClass)
   602       return false;
   604     return info.wrapClass.split(" ").indexOf(className) != -1;
   605   },
   607   /**
   608    * Set a CSS class name for the given line, including the text and gutter.
   609    */
   610   addLineClass: function (line, className) {
   611     let cm = editors.get(this);
   612     cm.addLineClass(line, "wrap", className);
   613   },
   615   /**
   616    * The reverse of addLineClass.
   617    */
   618   removeLineClass: function (line, className) {
   619     let cm = editors.get(this);
   620     cm.removeLineClass(line, "wrap", className);
   621   },
   623   /**
   624    * Mark a range of text inside the two {line, ch} bounds. Since the range may
   625    * be modified, for example, when typing text, this method returns a function
   626    * that can be used to remove the mark.
   627    */
   628   markText: function(from, to, className = "marked-text") {
   629     let cm = editors.get(this);
   630     let text = cm.getRange(from, to);
   631     let span = cm.getWrapperElement().ownerDocument.createElement("span");
   632     span.className = className;
   633     span.textContent = text;
   635     let mark = cm.markText(from, to, { replacedWith: span });
   636     return {
   637       anchor: span,
   638       clear: () => mark.clear()
   639     };
   640   },
   642   /**
   643    * Calculates and returns one or more {line, ch} objects for
   644    * a zero-based index who's value is relative to the start of
   645    * the editor's text.
   646    *
   647    * If only one argument is given, this method returns a single
   648    * {line,ch} object. Otherwise it returns an array.
   649    */
   650   getPosition: function (...args) {
   651     let cm = editors.get(this);
   652     let res = args.map((ind) => cm.posFromIndex(ind));
   653     return args.length === 1 ? res[0] : res;
   654   },
   656   /**
   657    * The reverse of getPosition. Similarly to getPosition this
   658    * method returns a single value if only one argument was given
   659    * and an array otherwise.
   660    */
   661   getOffset: function (...args) {
   662     let cm = editors.get(this);
   663     let res = args.map((pos) => cm.indexFromPos(pos));
   664     return args.length > 1 ? res : res[0];
   665   },
   667   /**
   668    * Returns a {line, ch} object that corresponds to the
   669    * left, top coordinates.
   670    */
   671   getPositionFromCoords: function ({left, top}) {
   672     let cm = editors.get(this);
   673     return cm.coordsChar({ left: left, top: top });
   674   },
   676   /**
   677    * The reverse of getPositionFromCoords. Similarly, returns a {left, top}
   678    * object that corresponds to the specified line and character number.
   679    */
   680   getCoordsFromPosition: function ({line, ch}) {
   681     let cm = editors.get(this);
   682     return cm.charCoords({ line: ~~line, ch: ~~ch });
   683   },
   685   /**
   686    * Returns true if there's something to undo and false otherwise.
   687    */
   688   canUndo: function () {
   689     let cm = editors.get(this);
   690     return cm.historySize().undo > 0;
   691   },
   693   /**
   694    * Returns true if there's something to redo and false otherwise.
   695    */
   696   canRedo: function () {
   697     let cm = editors.get(this);
   698     return cm.historySize().redo > 0;
   699   },
   701   /**
   702    * Marks the contents as clean and returns the current
   703    * version number.
   704    */
   705   setClean: function () {
   706     let cm = editors.get(this);
   707     this.version = cm.changeGeneration();
   708     this._lastDirty = false;
   709     this.emit("dirty-change");
   710     return this.version;
   711   },
   713   /**
   714    * Returns true if contents of the text area are
   715    * clean i.e. no changes were made since the last version.
   716    */
   717   isClean: function () {
   718     let cm = editors.get(this);
   719     return cm.isClean(this.version);
   720   },
   722   /**
   723    * This method opens an in-editor dialog asking for a line to
   724    * jump to. Once given, it changes cursor to that line.
   725    */
   726   jumpToLine: function () {
   727     let doc = editors.get(this).getWrapperElement().ownerDocument;
   728     let div = doc.createElement("div");
   729     let inp = doc.createElement("input");
   730     let txt = doc.createTextNode(L10N.GetStringFromName("gotoLineCmd.promptTitle"));
   732     inp.type = "text";
   733     inp.style.width = "10em";
   734     inp.style.MozMarginStart = "1em";
   736     div.appendChild(txt);
   737     div.appendChild(inp);
   739     this.openDialog(div, (line) => this.setCursor({ line: line - 1, ch: 0 }));
   740   },
   742   /**
   743    * Moves the content of the current line or the lines selected up a line.
   744    */
   745   moveLineUp: function () {
   746     let cm = editors.get(this);
   747     let start = cm.getCursor("start");
   748     let end = cm.getCursor("end");
   750     if (start.line === 0)
   751       return;
   753     // Get the text in the lines selected or the current line of the cursor
   754     // and append the text of the previous line.
   755     let value;
   756     if (start.line !== end.line) {
   757       value = cm.getRange({ line: start.line, ch: 0 },
   758         { line: end.line, ch: cm.getLine(end.line).length }) + "\n";
   759     } else {
   760       value = cm.getLine(start.line) + "\n";
   761     }
   762     value += cm.getLine(start.line - 1);
   764     // Replace the previous line and the currently selected lines with the new
   765     // value and maintain the selection of the text.
   766     cm.replaceRange(value, { line: start.line - 1, ch: 0 },
   767       { line: end.line, ch: cm.getLine(end.line).length });
   768     cm.setSelection({ line: start.line - 1, ch: start.ch },
   769       { line: end.line - 1, ch: end.ch });
   770   },
   772   /**
   773    * Moves the content of the current line or the lines selected down a line.
   774    */
   775   moveLineDown: function () {
   776     let cm = editors.get(this);
   777     let start = cm.getCursor("start");
   778     let end = cm.getCursor("end");
   780     if (end.line + 1 === cm.lineCount())
   781       return;
   783     // Get the text of next line and append the text in the lines selected
   784     // or the current line of the cursor.
   785     let value = cm.getLine(end.line + 1) + "\n";
   786     if (start.line !== end.line) {
   787       value += cm.getRange({ line: start.line, ch: 0 },
   788         { line: end.line, ch: cm.getLine(end.line).length });
   789     } else {
   790       value += cm.getLine(start.line);
   791     }
   793     // Replace the currently selected lines and the next line with the new
   794     // value and maintain the selection of the text.
   795     cm.replaceRange(value, { line: start.line, ch: 0 },
   796       { line: end.line + 1, ch: cm.getLine(end.line + 1).length});
   797     cm.setSelection({ line: start.line + 1, ch: start.ch },
   798       { line: end.line + 1, ch: end.ch });
   799   },
   801   /**
   802    * Returns current font size for the editor area, in pixels.
   803    */
   804   getFontSize: function () {
   805     let cm  = editors.get(this);
   806     let el  = cm.getWrapperElement();
   807     let win = el.ownerDocument.defaultView;
   809     return parseInt(win.getComputedStyle(el).getPropertyValue("font-size"), 10);
   810   },
   812   /**
   813    * Sets font size for the editor area.
   814    */
   815   setFontSize: function (size) {
   816     let cm = editors.get(this);
   817     cm.getWrapperElement().style.fontSize = parseInt(size, 10) + "px";
   818     cm.refresh();
   819   },
   821   /**
   822    * Extends an instance of the Editor object with additional
   823    * functions. Each function will be called with context as
   824    * the first argument. Context is a {ed, cm} object where
   825    * 'ed' is an instance of the Editor object and 'cm' is an
   826    * instance of the CodeMirror object. Example:
   827    *
   828    * function hello(ctx, name) {
   829    *   let { cm, ed } = ctx;
   830    *   cm;   // CodeMirror instance
   831    *   ed;   // Editor instance
   832    *   name; // 'Mozilla'
   833    * }
   834    *
   835    * editor.extend({ hello: hello });
   836    * editor.hello('Mozilla');
   837    */
   838   extend: function (funcs) {
   839     Object.keys(funcs).forEach((name) => {
   840       let cm  = editors.get(this);
   841       let ctx = { ed: this, cm: cm, Editor: Editor};
   843       if (name === "initialize") {
   844         funcs[name](ctx);
   845         return;
   846       }
   848       this[name] = funcs[name].bind(null, ctx);
   849     });
   850   },
   852   destroy: function () {
   853     this.container = null;
   854     this.config = null;
   855     this.version = null;
   856     this.emit("destroy");
   857   }
   858 };
   860 // Since Editor is a thin layer over CodeMirror some methods
   861 // are mapped directly—without any changes.
   863 CM_MAPPING.forEach(function (name) {
   864   Editor.prototype[name] = function (...args) {
   865     let cm = editors.get(this);
   866     return cm[name].apply(cm, args);
   867   };
   868 });
   870 // Static methods on the Editor object itself.
   872 /**
   873  * Returns a string representation of a shortcut 'key' with
   874  * a OS specific modifier. Cmd- for Macs, Ctrl- for other
   875  * platforms. Useful with extraKeys configuration option.
   876  *
   877  * CodeMirror defines all keys with modifiers in the following
   878  * order: Shift - Ctrl/Cmd - Alt - Key
   879  */
   880 Editor.accel = function (key, modifiers={}) {
   881   return (modifiers.shift ? "Shift-" : "") +
   882          (Services.appinfo.OS == "Darwin" ? "Cmd-" : "Ctrl-") +
   883          (modifiers.alt ? "Alt-" : "") + key;
   884 };
   886 /**
   887  * Returns a string representation of a shortcut for a
   888  * specified command 'cmd'. Append Cmd- for macs, Ctrl- for other
   889  * platforms unless noaccel is specified in the options. Useful when overwriting
   890  * or disabling default shortcuts.
   891  */
   892 Editor.keyFor = function (cmd, opts={ noaccel: false }) {
   893   let key = L10N.GetStringFromName(cmd + ".commandkey");
   894   return opts.noaccel ? key : Editor.accel(key);
   895 };
   897 // Since Gecko already provide complete and up to date list of CSS property
   898 // names, values and color names, we compute them so that they can replace
   899 // the ones used in CodeMirror while initiating an editor object. This is done
   900 // here instead of the file codemirror/css.js so as to leave that file untouched
   901 // and easily upgradable.
   902 function getCSSKeywords() {
   903   function keySet(array) {
   904     var keys = {};
   905     for (var i = 0; i < array.length; ++i) {
   906       keys[array[i]] = true;
   907     }
   908     return keys;
   909   }
   911   let domUtils = Cc["@mozilla.org/inspector/dom-utils;1"]
   912                    .getService(Ci.inIDOMUtils);
   913   let cssProperties = domUtils.getCSSPropertyNames(domUtils.INCLUDE_ALIASES);
   914   let cssColors = {};
   915   let cssValues = {};
   916   cssProperties.forEach(property => {
   917     if (property.contains("color")) {
   918       domUtils.getCSSValuesForProperty(property).forEach(value => {
   919         cssColors[value] = true;
   920       });
   921     }
   922     else {
   923       domUtils.getCSSValuesForProperty(property).forEach(value => {
   924         cssValues[value] = true;
   925       });
   926     }
   927   });
   928   return {
   929     cssProperties: keySet(cssProperties),
   930     cssValues: cssValues,
   931     cssColors: cssColors
   932   };
   933 }
   935 /**
   936  * Returns a controller object that can be used for
   937  * editor-specific commands such as find, jump to line,
   938  * copy/paste, etc.
   939  */
   940 function controller(ed) {
   941   return {
   942     supportsCommand: function (cmd) {
   943       switch (cmd) {
   944         case "cmd_find":
   945         case "cmd_findAgain":
   946         case "cmd_findPrevious":
   947         case "cmd_gotoLine":
   948         case "cmd_undo":
   949         case "cmd_redo":
   950         case "cmd_delete":
   951         case "cmd_selectAll":
   952           return true;
   953       }
   955       return false;
   956     },
   958     isCommandEnabled: function (cmd) {
   959       let cm = editors.get(ed);
   961       switch (cmd) {
   962         case "cmd_find":
   963         case "cmd_gotoLine":
   964         case "cmd_selectAll":
   965           return true;
   966         case "cmd_findAgain":
   967           return cm.state.search != null && cm.state.search.query != null;
   968         case "cmd_undo":
   969           return ed.canUndo();
   970         case "cmd_redo":
   971           return ed.canRedo();
   972         case "cmd_delete":
   973           return ed.somethingSelected();
   974       }
   976       return false;
   977     },
   979     doCommand: function (cmd) {
   980       let cm  = editors.get(ed);
   981       let map = {
   982         "cmd_selectAll": "selectAll",
   983         "cmd_find": "find",
   984         "cmd_undo": "undo",
   985         "cmd_redo": "redo",
   986         "cmd_delete": "delCharAfter",
   987         "cmd_findAgain": "findNext"
   988       };
   990       if (map[cmd]) {
   991         cm.execCommand(map[cmd]);
   992         return;
   993       }
   995       if (cmd == "cmd_gotoLine")
   996         ed.jumpToLine();
   997     },
   999     onEvent: function () {}
  1000   };
  1003 /**
  1004  * Detect the indentation used in an editor. Returns an object
  1005  * with 'tabs' - whether this is tab-indented and 'spaces' - the
  1006  * width of one indent in spaces. Or `null` if it's inconclusive.
  1007  */
  1008 function detectIndentation(ed) {
  1009   let cm = editors.get(ed);
  1011   let spaces = {};  // # spaces indent -> # lines with that indent
  1012   let last = 0;     // indentation width of the last line we saw
  1013   let tabs = 0;     // # of lines that start with a tab
  1014   let total = 0;    // # of indented lines (non-zero indent)
  1016   cm.eachLine(0, DETECT_INDENT_MAX_LINES, (line) => {
  1017     let text = line.text;
  1019     if (text.startsWith("\t")) {
  1020       tabs++;
  1021       total++;
  1022       return;
  1024     let width = 0;
  1025     while (text[width] === " ") {
  1026       width++;
  1028     // don't count lines that are all spaces
  1029     if (width == text.length) {
  1030       last = 0;
  1031       return;
  1033     if (width > 1) {
  1034       total++;
  1037     // see how much this line is offset from the line above it
  1038     let indent = Math.abs(width - last);
  1039     if (indent > 1 && indent <= 8) {
  1040       spaces[indent] = (spaces[indent] || 0) + 1;
  1042     last = width;
  1043   });
  1045   // this file is not indented at all
  1046   if (total == 0) {
  1047     return null;
  1050   // mark as tabs if they start more than half the lines
  1051   if (tabs >= total / 2) {
  1052     return { tabs: true };
  1055   // find most frequent non-zero width difference between adjacent lines
  1056   let freqIndent = null, max = 1;
  1057   for (let width in spaces) {
  1058     width = parseInt(width, 10);
  1059     let tally = spaces[width];
  1060     if (tally > max) {
  1061       max = tally;
  1062       freqIndent = width;
  1065   if (!freqIndent) {
  1066     return null;
  1069   return { tabs: false, spaces: freqIndent };
  1072 module.exports = Editor;

mercurial