Wed, 31 Dec 2014 06:09:35 +0100
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 };
1001 }
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;
1023 }
1024 let width = 0;
1025 while (text[width] === " ") {
1026 width++;
1027 }
1028 // don't count lines that are all spaces
1029 if (width == text.length) {
1030 last = 0;
1031 return;
1032 }
1033 if (width > 1) {
1034 total++;
1035 }
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;
1041 }
1042 last = width;
1043 });
1045 // this file is not indented at all
1046 if (total == 0) {
1047 return null;
1048 }
1050 // mark as tabs if they start more than half the lines
1051 if (tabs >= total / 2) {
1052 return { tabs: true };
1053 }
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;
1063 }
1064 }
1065 if (!freqIndent) {
1066 return null;
1067 }
1069 return { tabs: false, spaces: freqIndent };
1070 }
1072 module.exports = Editor;