1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
11 getIndentationFromIteration,
12 } = require("resource://devtools/shared/indentation.js");
14 const ENABLE_CODE_FOLDING = "devtools.editor.enableCodeFolding";
15 const KEYMAP_PREF = "devtools.editor.keymap";
16 const AUTO_CLOSE = "devtools.editor.autoclosebrackets";
17 const AUTOCOMPLETE = "devtools.editor.autocomplete";
18 const CARET_BLINK_TIME = "ui.caretBlinkTime";
19 const XHTML_NS = "http://www.w3.org/1999/xhtml";
21 const VALID_KEYMAPS = new Map([
24 "chrome://devtools/content/shared/sourceeditor/codemirror/keymap/emacs.js",
28 "chrome://devtools/content/shared/sourceeditor/codemirror/keymap/vim.js",
32 "chrome://devtools/content/shared/sourceeditor/codemirror/keymap/sublime.js",
36 // Maximum allowed margin (in number of lines) from top or bottom of the editor
37 // while shifting to a line which was initially out of view.
38 const MAX_VERTICAL_OFFSET = 3;
40 const RE_JUMP_TO_LINE = /^(\d+):?(\d+)?/;
41 const AUTOCOMPLETE_MARK_CLASSNAME = "cm-auto-complete-shadow-text";
43 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
44 const { PrefObserver } = require("resource://devtools/client/shared/prefs.js");
45 const KeyShortcuts = require("resource://devtools/client/shared/key-shortcuts.js");
47 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
48 const L10N = new LocalizationHelper(
49 "devtools/client/locales/sourceeditor.properties"
52 loader.lazyRequireGetter(
55 "resource://devtools/client/shared/sourceeditor/wasm.js"
58 const { OS } = Services.appinfo;
60 // CM_BUNDLE and CM_IFRAME represent the HTML and JavaScript that is
61 // injected into an iframe in order to initialize a CodeMirror instance.
64 "chrome://devtools/content/shared/sourceeditor/codemirror/codemirror.bundle.js";
67 "chrome://devtools/content/shared/sourceeditor/codemirror/cmiframe.html";
90 const editors = new WeakMap();
93 * A very thin wrapper around CodeMirror. Provides a number
94 * of helper methods to make our use of CodeMirror easier and
95 * another method, appendTo, to actually create and append
96 * the CodeMirror instance.
98 * Note that Editor doesn't expose CodeMirror instance to the
101 * Constructor accepts one argument, config. It is very
102 * similar to the CodeMirror configuration object so for most
103 * properties go to CodeMirror's documentation (see below).
105 * Other than that, it accepts one additional and optional
106 * property contextMenu. This property should be an element, or
107 * an ID of an element that we can use as a context menu.
109 * This object is also an event emitter.
111 * CodeMirror docs: http://codemirror.net/doc/manual.html
113 class Editor extends EventEmitter {
114 // Static methods on the Editor object itself.
117 * Returns a string representation of a shortcut 'key' with
118 * a OS specific modifier. Cmd- for Macs, Ctrl- for other
119 * platforms. Useful with extraKeys configuration option.
121 * CodeMirror defines all keys with modifiers in the following
122 * order: Shift - Ctrl/Cmd - Alt - Key
124 static accel(key, modifiers = {}) {
126 (modifiers.shift ? "Shift-" : "") +
127 (Services.appinfo.OS == "Darwin" ? "Cmd-" : "Ctrl-") +
128 (modifiers.alt ? "Alt-" : "") +
134 * Returns a string representation of a shortcut for a
135 * specified command 'cmd'. Append Cmd- for macs, Ctrl- for other
136 * platforms unless noaccel is specified in the options. Useful when overwriting
137 * or disabling default shortcuts.
139 static keyFor(cmd, opts = { noaccel: false }) {
140 const key = L10N.getStr(cmd + ".commandkey");
141 return opts.noaccel ? key : Editor.accel(key);
145 cljs: { name: "text/x-clojure" },
146 css: { name: "css" },
147 fs: { name: "x-shader/x-fragment" },
148 haxe: { name: "haxe" },
149 http: { name: "http" },
150 html: { name: "htmlmixed" },
151 js: { name: "javascript" },
152 text: { name: "text" },
153 vs: { name: "x-shader/x-vertex" },
154 wasm: { name: "wasm" },
169 #lineGutterMarkers = new Map();
170 #lineContentMarkers = new Map();
172 #updateListener = null;
174 constructor(config) {
177 const tabSize = Services.prefs.getIntPref(TAB_SIZE);
178 const useTabs = !Services.prefs.getBoolPref(EXPAND_TAB);
179 const useAutoClose = Services.prefs.getBoolPref(AUTO_CLOSE);
185 mode: Editor.modes.text,
190 highlightSelectionMatches: {
194 indentWithTabs: useTabs,
195 inputStyle: "accessibleTextArea",
196 // This is set to the biggest value for setTimeout (See https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout#Maximum_delay_value)
197 // This is because codeMirror queries the underlying textArea for some things that
198 // can't be retrieved with events in some browser (but we're fine in Firefox).
199 pollInterval: Math.pow(2, 31) - 1,
200 styleActiveLine: true,
201 autoCloseBrackets: "()[]{}''\"\"``",
202 autoCloseEnabled: useAutoClose,
204 themeSwitching: true,
206 autocompleteOpts: {},
207 // Expect a CssProperties object (see devtools/client/fronts/css-properties.js)
209 // Set to true to prevent the search addon to be activated.
210 disableSearchAddon: false,
211 maxHighlightLength: 1000,
212 // Disable codeMirror setTimeout-based cursor blinking (will be replaced by a CSS animation)
214 // List of non-printable chars that will be displayed in the editor, showing their
215 // unicode version. We only add a few characters to the default list:
216 // - \u202d LEFT-TO-RIGHT OVERRIDE
217 // - \u202e RIGHT-TO-LEFT OVERRIDE
218 // - \u2066 LEFT-TO-RIGHT ISOLATE
219 // - \u2067 RIGHT-TO-LEFT ISOLATE
220 // - \u2069 POP DIRECTIONAL ISOLATE
222 // eslint-disable-next-line no-control-regex
223 /[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b-\u200f\u2028\u2029\u202d\u202e\u2066\u2067\u2069\ufeff\ufff9-\ufffc]/,
224 specialCharPlaceholder: char => {
225 // Use the doc provided to the setup function if we don't have a reference to a codeMirror
226 // editor yet (this can happen when an Editor is being created with existing content)
227 const doc = this.#ownerDoc;
228 const el = doc.createElement("span");
229 el.classList.add("cm-non-printable-char");
230 el.append(doc.createTextNode(`\\u${char.codePointAt(0).toString(16)}`));
235 // Additional shortcuts.
236 this.config.extraKeys[Editor.keyFor("jumpToLine")] = () =>
238 this.config.extraKeys[Editor.keyFor("moveLineUp", { noaccel: true })] =
239 () => this.moveLineUp();
240 this.config.extraKeys[Editor.keyFor("moveLineDown", { noaccel: true })] =
241 () => this.moveLineDown();
242 this.config.extraKeys[Editor.keyFor("toggleComment")] = "toggleComment";
244 // Disable ctrl-[ and ctrl-] because toolbox uses those shortcuts.
245 this.config.extraKeys[Editor.keyFor("indentLess")] = false;
246 this.config.extraKeys[Editor.keyFor("indentMore")] = false;
248 // Disable Alt-B and Alt-F to navigate groups (respectively previous and next) since:
249 // - it's not standard in input fields
250 // - it also inserts a character which feels weird
251 this.config.extraKeys["Alt-B"] = false;
252 this.config.extraKeys["Alt-F"] = false;
254 // Disable Ctrl/Cmd + U as it's used for "View Source". It's okay to disable Ctrl+U as
255 // the underlying command, `undoSelection`, isn't standard in input fields and isn't
257 this.config.extraKeys[Editor.accel("U")] = false;
259 // Disable keys that trigger events with a null-string `which` property.
260 // It looks like some of those (e.g. the Function key), can trigger a poll
261 // which fails to see that there's a selection, which end up replacing the
262 // selected text with an empty string.
263 // TODO: We should investigate the root cause.
264 this.config.extraKeys["'\u0000'"] = false;
266 // Overwrite default config with user-provided, if needed.
267 Object.keys(config).forEach(k => {
268 if (k != "extraKeys") {
269 this.config[k] = config[k];
273 if (!config.extraKeys) {
277 Object.keys(config.extraKeys).forEach(key => {
278 this.config.extraKeys[key] = config.extraKeys[key];
282 if (!this.config.gutters) {
283 this.config.gutters = [];
286 this.config.lineNumbers &&
287 !this.config.gutters.includes("CodeMirror-linenumbers")
289 this.config.gutters.push("CodeMirror-linenumbers");
292 // Remember the initial value of autoCloseBrackets.
293 this.config.autoCloseBracketsSaved = this.config.autoCloseBrackets;
295 // Overwrite default tab behavior. If something is selected,
296 // indent those lines. If nothing is selected and we're
297 // indenting with tabs, insert one tab. Otherwise insert N
298 // whitespaces where N == indentUnit option.
299 this.config.extraKeys.Tab = cm => {
300 if (config.extraKeys?.Tab) {
301 // If a consumer registers its own extraKeys.Tab, we execute it before doing
302 // anything else. If it returns false, that mean that all the key handling work is
303 // done, so we can do an early return.
304 const res = config.extraKeys.Tab(cm);
310 if (cm.somethingSelected()) {
311 cm.indentSelection("add");
315 if (this.config.indentWithTabs) {
316 cm.replaceSelection("\t", "end", "+input");
320 let num = cm.getOption("indentUnit");
321 if (cm.getCursor().ch !== 0) {
322 num -= cm.getCursor().ch % num;
324 cm.replaceSelection(" ".repeat(num), "end", "+input");
327 if (this.config.cssProperties) {
328 // Ensure that autocompletion has cssProperties if it's passed in via the options.
329 this.config.autocompleteOpts.cssProperties = this.config.cssProperties;
334 * Exposes the CodeMirror class. We want to be able to
335 * invoke static commands such as runMode for syntax highlighting.
338 const codeMirror = editors.get(this);
339 return codeMirror?.constructor;
343 * Exposes the CodeMirror instance. We want to get away from trying to
344 * abstract away the API entirely, and this makes it easier to integrate in
345 * various environments and do complex things.
348 if (!editors.has(this)) {
350 "CodeMirror instance does not exist. You must wait " +
351 "for it to be appended to the DOM."
354 return editors.get(this);
358 * Return whether there is a CodeMirror instance associated with this Editor.
360 get hasCodeMirror() {
361 return editors.has(this);
365 * Appends the current Editor instance to the element specified by
366 * 'el'. You can also provide your own iframe to host the editor as
367 * an optional second parameter. This method actually creates and
368 * loads CodeMirror and all its dependencies.
370 * This method is asynchronous and returns a promise.
373 return new Promise(resolve => {
374 const cm = editors.get(this);
377 env = el.ownerDocument.createElementNS(XHTML_NS, "iframe");
378 env.className = "source-editor-frame";
382 throw new Error("You can append an editor only once.");
385 const onLoad = () => {
386 // Prevent flickering by showing the iframe once loaded.
387 // See https://github.com/w3c/csswg-drafts/issues/9624
388 env.style.visibility = "";
389 const win = env.contentWindow.wrappedJSObject;
390 this.container = env;
392 const editorEl = win.document.body;
393 const editorDoc = el.ownerDocument;
394 if (this.config.cm6) {
395 this.#setupCm6(editorEl, editorDoc);
397 this.#setup(editorEl, editorDoc);
402 env.style.visibility = "hidden";
403 env.addEventListener("load", onLoad, { capture: true, once: true });
407 this.once("destroy", () => el.removeChild(env));
411 appendToLocalElement(el) {
412 if (this.config.cm6) {
419 // This update listener allows listening to the changes
420 // to the codemiror editor.
421 setUpdateListener(listener = null) {
422 this.#updateListener = listener;
426 * Do the actual appending and configuring of the CodeMirror instance. This is
427 * used by both append functions above, and does all the hard work to
428 * configure CodeMirror with all the right options/modes/etc.
431 this.#ownerDoc = doc || el.ownerDocument;
432 const win = el.ownerDocument.defaultView;
434 Services.scriptloader.loadSubScript(CM_BUNDLE, win);
437 if (this.config.cssProperties) {
438 // Replace the propertyKeywords, colorKeywords and valueKeywords
439 // properties of the CSS MIME type with the values provided by the CSS properties
441 const { propertyKeywords, colorKeywords, valueKeywords } = getCSSKeywords(
442 this.config.cssProperties
445 const cssSpec = win.CodeMirror.resolveMode("text/css");
446 cssSpec.propertyKeywords = propertyKeywords;
447 cssSpec.colorKeywords = colorKeywords;
448 cssSpec.valueKeywords = valueKeywords;
449 win.CodeMirror.defineMIME("text/css", cssSpec);
451 const scssSpec = win.CodeMirror.resolveMode("text/x-scss");
452 scssSpec.propertyKeywords = propertyKeywords;
453 scssSpec.colorKeywords = colorKeywords;
454 scssSpec.valueKeywords = valueKeywords;
455 win.CodeMirror.defineMIME("text/x-scss", scssSpec);
458 win.CodeMirror.commands.save = () => this.emit("saveRequested");
460 // Create a CodeMirror instance add support for context menus,
461 // overwrite the default controller (otherwise items in the top and
462 // context menus won't work).
464 const cm = win.CodeMirror(el, this.config);
465 this.Doc = win.CodeMirror.Doc;
467 // Disable APZ for source editors. It currently causes the line numbers to
468 // "tear off" and swim around on top of the content. Bug 1160601 tracks
469 // finding a solution that allows APZ to work with CodeMirror.
470 cm.getScrollerElement().addEventListener("wheel", ev => {
471 // By handling the wheel events ourselves, we force the platform to
472 // scroll synchronously, like it did before APZ. However, we lose smooth
473 // scrolling for users with mouse wheels. This seems acceptible vs.
474 // doing nothing and letting the gutter slide around.
477 let { deltaX, deltaY } = ev;
479 if (ev.deltaMode == ev.DOM_DELTA_LINE) {
480 deltaX *= cm.defaultCharWidth();
481 deltaY *= cm.defaultTextHeight();
482 } else if (ev.deltaMode == ev.DOM_DELTA_PAGE) {
483 deltaX *= cm.getWrapperElement().clientWidth;
484 deltaY *= cm.getWrapperElement().clientHeight;
487 cm.getScrollerElement().scrollBy(deltaX, deltaY);
490 cm.getWrapperElement().addEventListener("contextmenu", ev => {
491 if (!this.config.contextMenu) {
495 ev.stopPropagation();
498 let popup = this.config.contextMenu;
499 if (typeof popup == "string") {
500 popup = this.#ownerDoc.getElementById(this.config.contextMenu);
503 this.emit("popupOpen", ev, popup);
504 popup.openPopupAtScreen(ev.screenX, ev.screenY, true);
507 const pipedEvents = [
516 for (const eventName of pipedEvents) {
517 cm.on(eventName, (...args) => this.emit(eventName, ...args));
520 cm.on("change", () => {
522 if (!this.#lastDirty) {
523 this.#lastDirty = true;
524 this.emit("dirty-change");
528 cm.on("gutterClick", (cmArg, line, gutter, ev) => {
529 const lineOrOffset = !this.isWasm ? line : this.lineToWasmOffset(line);
530 this.emit("gutterClick", lineOrOffset, ev.button);
533 win.CodeMirror.defineExtension("l10n", name => {
534 return L10N.getStr(name);
537 if (!this.config.disableSearchAddon) {
538 this.#initSearchShortcuts(win);
540 // Hotfix for Bug 1527898. We should remove those overrides as part of Bug 1527903.
541 Object.assign(win.CodeMirror.commands, {
543 findPersistent: null,
544 findPersistentNext: null,
545 findPersistentPrev: null,
554 // Retrieve the cursor blink rate from user preference, or fall back to CodeMirror's
556 let cursorBlinkingRate = win.CodeMirror.defaults.cursorBlinkRate;
557 if (Services.prefs.prefHasUserValue(CARET_BLINK_TIME)) {
558 cursorBlinkingRate = Services.prefs.getIntPref(
563 // This will be used in the animation-duration property we set on the cursor to
564 // implement the blinking animation. If cursorBlinkingRate is 0 or less, the cursor
566 cm.getWrapperElement().style.setProperty(
567 "--caret-blink-time",
568 `${Math.max(0, cursorBlinkingRate)}ms`
571 editors.set(this, cm);
573 this.reloadPreferences = this.reloadPreferences.bind(this);
574 this.setKeyMap = this.setKeyMap.bind(this, win);
576 this.#prefObserver = new PrefObserver("devtools.editor.");
577 this.#prefObserver.on(TAB_SIZE, this.reloadPreferences);
578 this.#prefObserver.on(EXPAND_TAB, this.reloadPreferences);
579 this.#prefObserver.on(AUTO_CLOSE, this.reloadPreferences);
580 this.#prefObserver.on(AUTOCOMPLETE, this.reloadPreferences);
581 this.#prefObserver.on(DETECT_INDENT, this.reloadPreferences);
582 this.#prefObserver.on(ENABLE_CODE_FOLDING, this.reloadPreferences);
584 this.reloadPreferences();
586 // Init a map of the loaded keymap files. Should be of the form Map<String->Boolean>.
587 this.#loadedKeyMaps = new Set();
588 this.#prefObserver.on(KEYMAP_PREF, this.setKeyMap);
592 const editorReadyEvent = new win.CustomEvent("editorReady");
593 win.dispatchEvent(editorReadyEvent);
597 * Do the actual appending and configuring of the CodeMirror 6 instance.
598 * This is used by appendTo and appendToLocalElement, and does all the hard work to
599 * configure CodeMirror 6 with all the right options/modes/etc.
600 * This should be kept in sync with #setup.
602 * @param {Element} el: Element into which the codeMirror editor should be appended.
603 * @param {Document} document: Optional document, if not set, will default to el.ownerDocument
606 this.#ownerDoc = doc || el.ownerDocument;
607 const win = el.ownerDocument.defaultView;
610 this.#CodeMirror6 = this.#win.ChromeUtils.importESModule(
611 "resource://devtools/client/shared/sourceeditor/codemirror6/codemirror6.bundle.mjs",
612 { global: "current" }
617 codemirrorView: { EditorView, lineNumbers },
618 codemirrorState: { EditorState, Compartment },
620 codemirrorLangJavascript,
622 } = this.#CodeMirror6;
624 const tabSizeCompartment = new Compartment();
625 const indentCompartment = new Compartment();
626 const lineWrapCompartment = new Compartment();
627 const lineNumberCompartment = new Compartment();
628 const lineNumberMarkersCompartment = new Compartment();
629 const lineContentMarkerCompartment = new Compartment();
631 this.#compartments = {
635 lineNumberCompartment,
636 lineNumberMarkersCompartment,
637 lineContentMarkerCompartment,
640 const indentStr = (this.config.indentWithTabs ? "\t" : " ").repeat(
641 this.config.indentUnit || 2
645 indentCompartment.of(codemirrorLanguage.indentUnit.of(indentStr)),
646 tabSizeCompartment.of(EditorState.tabSize.of(this.config.tabSize)),
647 lineWrapCompartment.of(
648 this.config.lineWrapping ? EditorView.lineWrapping : []
650 EditorState.readOnly.of(this.config.readOnly),
651 lineNumberCompartment.of(this.config.lineNumbers ? lineNumbers() : []),
652 codemirrorLanguage.codeFolding({
653 placeholderText: "↔",
655 codemirrorLanguage.foldGutter({
656 class: "cm6-dt-foldgutter",
658 const button = this.#ownerDoc.createElement("button");
659 button.classList.add("cm6-dt-foldgutter__toggle-button");
660 button.setAttribute("aria-expanded", open);
664 codemirrorLanguage.syntaxHighlighting(lezerHighlight.classHighlighter),
665 EditorView.updateListener.of(v => {
666 if (v.viewportChanged || v.docChanged) {
667 // reset line gutter markers for the new visible ranges
668 // when the viewport changes(e.g when the page is scrolled).
669 if (this.#lineGutterMarkers.size > 0) {
670 this.setLineGutterMarkers();
673 // Any custom defined update listener should be called
674 if (typeof this.#updateListener == "function") {
675 this.#updateListener(v);
678 lineNumberMarkersCompartment.of([]),
679 lineContentMarkerCompartment.of(this.#lineContentMarkersExtension([])),
680 // keep last so other extension take precedence
681 codemirror.minimalSetup,
684 if (this.config.mode === Editor.modes.js) {
685 extensions.push(codemirrorLangJavascript.javascript());
688 const cm = new EditorView({
693 editors.set(this, cm);
697 * This creates the extension used to manage the rendering of markers
698 * for in editor line content.
699 * @param {Array} markers - The current list of markers
700 * @returns {Array<ViewPlugin>} showLineContentDecorations - An extension which is an array containing the view
701 * which manages the rendering of the line content markers.
703 #lineContentMarkersExtension(markers) {
705 codemirrorView: { Decoration, ViewPlugin },
706 codemirrorState: { RangeSetBuilder },
707 } = this.#CodeMirror6;
709 // Build and return the decoration set
710 function buildDecorations(view) {
711 const builder = new RangeSetBuilder();
712 for (const { from, to } of view.visibleRanges) {
713 for (let pos = from; pos <= to; ) {
714 const line = view.state.doc.lineAt(pos);
715 for (const { lineClassName, condition } of markers) {
716 if (condition(line.number)) {
720 Decoration.line({ class: lineClassName })
727 return builder.finish();
730 // The view which handles rendering and updating the
731 // markers decorations
732 const showLineContentDecorations = ViewPlugin.fromClass(
736 this.decorations = buildDecorations(view);
739 if (update.docChanged || update.viewportChanged) {
740 this.decorations = buildDecorations(update.view);
744 { decorations: v => v.decorations }
747 return [showLineContentDecorations];
751 * This adds a marker used to add classes to editor line based on a condition.
752 * @property {object} marker - The rule rendering a marker or class.
753 * @property {object} marker.id - The unique identifier for this marker
754 * @property {string} marker.lineClassName - The css class to add to the line
755 * @property {function} marker.condition - The condition that decides if the marker/class gets added or removed.
756 * The line is passed as an argument.
758 setLineContentMarker(marker) {
759 const cm = editors.get(this);
760 this.#lineContentMarkers.set(marker.id, marker);
763 effects: this.#compartments.lineContentMarkerCompartment.reconfigure(
764 this.#lineContentMarkersExtension(
765 Array.from(this.#lineContentMarkers.values())
772 * This removes the marker which has the specified className
773 * @param {string} markerId - The unique identifier for this marker
775 removeLineContentMarker(markerId) {
776 const cm = editors.get(this);
777 this.#lineContentMarkers.delete(markerId);
780 effects: this.#compartments.lineContentMarkerCompartment.reconfigure(
781 this.#lineContentMarkersExtension(
782 Array.from(this.#lineContentMarkers.values())
789 * Set event listeners for the line gutter
790 * @param {Object} domEventHandlers
793 * const domEventHandlers = { click(event) { console.log(event);} }
795 setGutterEventListeners(domEventHandlers) {
796 const cm = editors.get(this);
798 codemirrorView: { lineNumbers },
799 } = this.#CodeMirror6;
801 for (const eventName in domEventHandlers) {
802 const handler = domEventHandlers[eventName];
803 domEventHandlers[eventName] = (view, line, event) => {
804 line = view.state.doc.lineAt(line.from);
805 handler(event, view, line.number);
810 effects: this.#compartments.lineWrapCompartment.reconfigure(
811 lineNumbers({ domEventHandlers })
817 * This supports adding/removing of line classes or markers on the
818 * line number gutter based on the defined conditions. This only supports codemirror 6.
820 * @param {Array<Marker>} markers - The list of marker objects which defines the rules
821 * for rendering each marker.
822 * @property {object} marker - The rule rendering a marker or class. This is required.
823 * @property {string} marker.id - The unique identifier for this marker.
824 * @property {string} marker.lineClassName - The css class to add to the line. This is required.
825 * @property {function} marker.condition - The condition that decides if the marker/class gets added or removed.
826 * @property {function=} marker.createLineElementNode - This gets the line as an argument and should return the DOM element which
827 * is used for the marker. This is optional.
829 setLineGutterMarkers(markers) {
830 const cm = editors.get(this);
833 // Cache the markers for use later. See next comment
834 for (const marker of markers) {
836 throw new Error("Marker has no unique identifier");
838 this.#lineGutterMarkers.set(marker.id, marker);
841 // When no markers are passed, the cached markers are used to update the line gutters.
842 // This is useful for re-rendering the line gutters when the viewport changes
843 // (note: the visible ranges will be different) in this case, mainly when the editor is scrolled.
844 else if (!this.#lineGutterMarkers.size) {
847 markers = Array.from(this.#lineGutterMarkers.values());
850 codemirrorView: { lineNumberMarkers, GutterMarker },
851 codemirrorState: { RangeSetBuilder },
852 } = this.#CodeMirror6;
854 // This creates a new GutterMarker https://codemirror.net/docs/ref/#view.GutterMarker
855 // to represents how each line gutter is rendered in the view.
856 // This is set as the value for the Range https://codemirror.net/docs/ref/#state.Range
857 // which represents the line.
858 class LineGutterMarker extends GutterMarker {
859 constructor(className, lineNumber, createElementNode) {
861 this.elementClass = className || null;
862 this.toDOM = createElementNode
863 ? () => createElementNode(lineNumber)
868 // Loop through the visible ranges https://codemirror.net/docs/ref/#view.EditorView.visibleRanges
869 // (representing the lines in the current viewport) and generate a new rangeset for updating the line gutter
870 // based on the conditions defined in the markers(for each line) provided.
871 const builder = new RangeSetBuilder();
872 for (const { from, to } of cm.visibleRanges) {
873 for (let pos = from; pos <= to; ) {
874 const line = cm.state.doc.lineAt(pos);
878 createLineElementNode,
880 if (typeof condition !== "function") {
881 throw new Error("The `condition` is not a valid function");
883 if (condition(line.number)) {
887 new LineGutterMarker(
890 createLineElementNode
899 // To update the state with the newly generated marker range set, a dispatch is called on the view
900 // with an transaction effect created by the lineNumberMarkersCompartment, which is used to update the
901 // lineNumberMarkers extension configuration.
903 effects: this.#compartments.lineNumberMarkersCompartment.reconfigure(
904 lineNumberMarkers.of(builder.finish())
910 * Returns a boolean indicating whether the editor is ready to
911 * use. Use appendTo(el).then(() => {}) for most cases
914 return editors.has(this);
918 * Returns the currently active highlighting mode.
919 * See Editor.modes for the list of all suppoert modes.
922 return this.getOption("mode");
926 * Loads a script into editor's containing window.
929 if (!this.container) {
930 throw new Error("Can't load a script until the editor is loaded.");
932 const win = this.container.contentWindow.wrappedJSObject;
933 Services.scriptloader.loadSubScript(url, win);
937 * Creates a CodeMirror Document
939 * @param {String} text: Initial text of the document
940 * @param {Object|String} mode: Mode of the document. See https://codemirror.net/5/doc/manual.html#option_mode
941 * @returns CodeMirror.Doc
943 createDocument(text = "", mode) {
944 return new this.Doc(text, mode);
948 * Replaces the current document with a new source document
950 replaceDocument(doc) {
951 const cm = editors.get(this);
956 * Changes the value of a currently used highlighting mode.
957 * See Editor.modes for the list of all supported modes.
960 this.setOption("mode", value);
962 // If autocomplete was set up and the mode is changing, then
963 // turn it off and back on again so the proper mode can be used.
964 if (this.config.autocomplete) {
965 this.setOption("autocomplete", false);
966 this.setOption("autocomplete", true);
971 * The source editor can expose several commands linked from system and context menus.
972 * Kept for backward compatibility with styleeditor.
974 insertCommandsController() {
976 insertCommandsController,
977 } = require("resource://devtools/client/shared/sourceeditor/editor-commands-controller.js");
978 insertCommandsController(this);
982 * Returns text from the text area. If line argument is provided
983 * the method returns only that line.
986 const cm = editors.get(this);
989 return this.config.cm6 ? cm.state.doc.toString() : cm.getValue();
992 const info = this.lineInfo(line);
993 return info ? info.text : "";
997 const cm = editors.get(this);
1002 return wasm.isWasm(this.getDoc());
1005 wasmOffsetToLine(offset) {
1006 return wasm.wasmOffsetToLine(this.getDoc(), offset);
1009 lineToWasmOffset(number) {
1010 return wasm.lineToWasmOffset(this.getDoc(), number);
1013 toLineIfWasmOffset(maybeOffset) {
1014 if (typeof maybeOffset !== "number" || !this.isWasm) {
1017 return this.wasmOffsetToLine(maybeOffset);
1020 lineInfo(lineOrOffset) {
1021 const line = this.toLineIfWasmOffset(lineOrOffset);
1022 if (line == undefined) {
1025 const cm = editors.get(this);
1027 if (this.config.cm6) {
1029 // cm6 lines are 1-based, while cm5 are 0-based
1030 text: cm.state.doc.lineAt(line + 1)?.text,
1031 // TODO: Expose those, or see usage for those and do things differently
1034 gutterMarkers: null,
1042 return cm.lineInfo(line);
1045 getLineOrOffset(line) {
1046 return this.isWasm ? this.lineToWasmOffset(line) : line;
1050 * Replaces whatever is in the text area with the contents of
1051 * the 'value' argument.
1054 const cm = editors.get(this);
1056 if (typeof value !== "string" && "binary" in value) {
1058 // binary does not survive as Uint8Array, converting from string
1059 const binary = value.binary;
1060 const data = new Uint8Array(binary.length);
1061 for (let i = 0; i < data.length; i++) {
1062 data[i] = binary.charCodeAt(i);
1064 const { lines, done } = wasm.getWasmText(this.getDoc(), data);
1065 const MAX_LINES = 10000000;
1066 if (lines.length > MAX_LINES) {
1067 lines.splice(MAX_LINES, lines.length - MAX_LINES);
1068 lines.push(";; .... text is truncated due to the size");
1071 lines.push(";; .... possible error during wast conversion");
1073 // cm will try to split into lines anyway, saving memory
1074 value = { split: () => lines };
1077 if (this.config.cm6) {
1079 changes: { from: 0, to: cm.state.doc.length, insert: value },
1085 this.resetIndentUnit();
1089 * Reloads the state of the editor based on all current preferences.
1090 * This is called automatically when any of the relevant preferences
1093 reloadPreferences() {
1094 // Restore the saved autoCloseBrackets value if it is preffed on.
1095 const useAutoClose = Services.prefs.getBoolPref(AUTO_CLOSE);
1097 "autoCloseBrackets",
1098 useAutoClose ? this.config.autoCloseBracketsSaved : false
1101 this.updateCodeFoldingGutter();
1103 this.resetIndentUnit();
1104 this.setupAutoCompletion();
1108 * Set the current keyMap for CodeMirror, and load the support file if needed.
1110 * @param {Window} win: The window on which the keymap files should be loaded.
1113 if (this.config.isReadOnly) {
1117 const keyMap = Services.prefs.getCharPref(KEYMAP_PREF);
1119 // If alternative keymap is provided, use it.
1120 if (VALID_KEYMAPS.has(keyMap)) {
1121 if (!this.#loadedKeyMaps.has(keyMap)) {
1122 Services.scriptloader.loadSubScript(VALID_KEYMAPS.get(keyMap), win);
1123 this.#loadedKeyMaps.add(keyMap);
1125 this.setOption("keyMap", keyMap);
1127 this.setOption("keyMap", "default");
1132 * Sets the editor's indentation based on the current prefs and
1133 * re-detect indentation if we should.
1136 const cm = editors.get(this);
1138 const iterFn = (start, maxEnd, callback) => {
1139 if (!this.config.cm6) {
1140 cm.eachLine(start, maxEnd, line => {
1141 return callback(line.text);
1144 const iterator = cm.state.doc.iterLines(
1146 Math.min(cm.state.doc.lines, maxEnd) + 1
1151 callbackRes = callback(iterator.value);
1152 } while (iterator.done !== true && !callbackRes);
1156 const { indentUnit, indentWithTabs } = getIndentationFromIteration(iterFn);
1158 if (!this.config.cm6) {
1159 cm.setOption("tabSize", indentUnit);
1160 cm.setOption("indentUnit", indentUnit);
1161 cm.setOption("indentWithTabs", indentWithTabs);
1164 codemirrorState: { EditorState },
1166 } = this.#CodeMirror6;
1169 effects: this.#compartments.tabSizeCompartment.reconfigure(
1170 EditorState.tabSize.of(indentUnit)
1174 effects: this.#compartments.indentCompartment.reconfigure(
1175 codemirrorLanguage.indentUnit.of(
1176 (indentWithTabs ? "\t" : " ").repeat(indentUnit)
1184 * Replaces contents of a text area within the from/to {line, ch}
1185 * range. If neither `from` nor `to` arguments are provided works
1186 * exactly like setText. If only `from` object is provided, inserts
1187 * text at that point, *overwriting* as many characters as needed.
1189 replaceText(value, from, to) {
1190 const cm = editors.get(this);
1193 this.setText(value);
1198 const text = cm.getRange({ line: 0, ch: 0 }, from);
1199 this.setText(text + value);
1203 cm.replaceRange(value, from, to);
1207 * Inserts text at the specified {line, ch} position, shifting existing
1208 * contents as necessary.
1210 insertText(value, at) {
1211 const cm = editors.get(this);
1212 cm.replaceRange(value, at, at);
1216 * Deselects contents of the text area.
1219 if (!this.somethingSelected()) {
1223 this.setCursor(this.getCursor());
1227 * Returns true if there is more than one selection in the editor.
1229 hasMultipleSelections() {
1230 const cm = editors.get(this);
1231 return cm.listSelections().length > 1;
1235 * Gets the first visible line number in the editor.
1237 getFirstVisibleLine() {
1238 const cm = editors.get(this);
1239 return cm.lineAtHeight(0, "local");
1243 * Scrolls the view such that the given line number is the first visible line.
1245 setFirstVisibleLine(line) {
1246 const cm = editors.get(this);
1247 const { top } = cm.charCoords({ line, ch: 0 }, "local");
1248 cm.scrollTo(0, top);
1252 * Sets the cursor to the specified {line, ch} position with an additional
1253 * option to align the line at the "top", "center" or "bottom" of the editor
1254 * with "top" being default value.
1256 setCursor({ line, ch }, align) {
1257 const cm = editors.get(this);
1258 this.alignLine(line, align);
1259 cm.setCursor({ line, ch });
1260 this.emit("cursorActivity");
1264 * Aligns the provided line to either "top", "center" or "bottom" of the
1265 * editor view with a maximum margin of MAX_VERTICAL_OFFSET lines from top or
1268 alignLine(line, align) {
1269 const cm = editors.get(this);
1270 const from = cm.lineAtHeight(0, "page");
1271 const to = cm.lineAtHeight(cm.getWrapperElement().clientHeight, "page");
1272 const linesVisible = to - from;
1273 const halfVisible = Math.round(linesVisible / 2);
1275 // If the target line is in view, skip the vertical alignment part.
1276 if (line <= to && line >= from) {
1280 // Setting the offset so that the line always falls in the upper half
1281 // of visible lines (lower half for bottom aligned).
1282 // MAX_VERTICAL_OFFSET is the maximum allowed value.
1283 const offset = Math.min(halfVisible, MAX_VERTICAL_OFFSET);
1287 center: Math.max(line - halfVisible, 0),
1288 bottom: Math.max(line - linesVisible + offset, 0),
1289 top: Math.max(line - offset, 0),
1290 }[align || "top"] || offset;
1292 // Bringing down the topLine to total lines in the editor if exceeding.
1293 topLine = Math.min(topLine, this.lineCount());
1294 this.setFirstVisibleLine(topLine);
1298 * Returns whether a marker of a specified class exists in a line's gutter.
1300 hasMarker(line, gutterName, markerClass) {
1301 const marker = this.getMarker(line, gutterName);
1306 return marker.classList.contains(markerClass);
1310 * Adds a marker with a specified class to a line's gutter. If another marker
1311 * exists on that line, the new marker class is added to its class list.
1313 addMarker(line, gutterName, markerClass) {
1314 const cm = editors.get(this);
1315 const info = this.lineInfo(line);
1320 const gutterMarkers = info.gutterMarkers;
1322 if (gutterMarkers) {
1323 marker = gutterMarkers[gutterName];
1325 marker.classList.add(markerClass);
1330 marker = cm.getWrapperElement().ownerDocument.createElement("div");
1331 marker.className = markerClass;
1332 cm.setGutterMarker(info.line, gutterName, marker);
1336 * The reverse of addMarker. Removes a marker of a specified class from a
1339 removeMarker(line, gutterName, markerClass) {
1340 if (!this.hasMarker(line, gutterName, markerClass)) {
1344 this.lineInfo(line).gutterMarkers[gutterName].classList.remove(markerClass);
1348 * Adds a marker with a specified class and an HTML content to a line's
1349 * gutter. If another marker exists on that line, it is overwritten by a new
1352 addContentMarker(line, gutterName, markerClass, content) {
1353 const cm = editors.get(this);
1354 const info = this.lineInfo(line);
1359 const marker = cm.getWrapperElement().ownerDocument.createElement("div");
1360 marker.className = markerClass;
1361 // eslint-disable-next-line no-unsanitized/property
1362 marker.innerHTML = content;
1363 cm.setGutterMarker(info.line, gutterName, marker);
1367 * The reverse of addContentMarker. Removes any line's markers in the
1370 removeContentMarker(line, gutterName) {
1371 const cm = editors.get(this);
1372 const info = this.lineInfo(line);
1377 cm.setGutterMarker(info.line, gutterName, null);
1380 getMarker(line, gutterName) {
1381 const info = this.lineInfo(line);
1386 const gutterMarkers = info.gutterMarkers;
1387 if (!gutterMarkers) {
1391 return gutterMarkers[gutterName];
1395 * Removes all gutter markers in the gutter with the given name.
1397 removeAllMarkers(gutterName) {
1398 const cm = editors.get(this);
1399 cm.clearGutter(gutterName);
1403 * Handles attaching a set of events listeners on a marker. They should
1404 * be passed as an object literal with keys as event names and values as
1405 * function listeners. The line number, marker node and optional data
1406 * will be passed as arguments to the function listener.
1408 * You don't need to worry about removing these event listeners.
1409 * They're automatically orphaned when clearing markers.
1411 setMarkerListeners(line, gutterName, markerClass, eventsArg, data) {
1412 if (!this.hasMarker(line, gutterName, markerClass)) {
1416 const cm = editors.get(this);
1417 const marker = cm.lineInfo(line).gutterMarkers[gutterName];
1419 for (const name in eventsArg) {
1420 const listener = eventsArg[name].bind(this, line, marker, data);
1421 marker.addEventListener(name, listener);
1426 * Returns whether a line is decorated using the specified class name.
1428 hasLineClass(line, className) {
1429 const info = this.lineInfo(line);
1431 if (!info || !info.wrapClass) {
1435 return info.wrapClass.split(" ").includes(className);
1439 * Sets a CSS class name for the given line, including the text and gutter.
1441 addLineClass(lineOrOffset, className) {
1442 const cm = editors.get(this);
1443 const line = this.toLineIfWasmOffset(lineOrOffset);
1444 cm.addLineClass(line, "wrap", className);
1448 * The reverse of addLineClass.
1450 removeLineClass(lineOrOffset, className) {
1451 const cm = editors.get(this);
1452 const line = this.toLineIfWasmOffset(lineOrOffset);
1453 cm.removeLineClass(line, "wrap", className);
1457 * Mark a range of text inside the two {line, ch} bounds. Since the range may
1458 * be modified, for example, when typing text, this method returns a function
1459 * that can be used to remove the mark.
1461 markText(from, to, className = "marked-text") {
1462 const cm = editors.get(this);
1463 const text = cm.getRange(from, to);
1464 const span = cm.getWrapperElement().ownerDocument.createElement("span");
1465 span.className = className;
1466 span.textContent = text;
1468 const mark = cm.markText(from, to, { replacedWith: span });
1471 clear: () => mark.clear(),
1476 * Calculates and returns one or more {line, ch} objects for
1477 * a zero-based index who's value is relative to the start of
1478 * the editor's text.
1480 * If only one argument is given, this method returns a single
1481 * {line,ch} object. Otherwise it returns an array.
1483 getPosition(...args) {
1484 const cm = editors.get(this);
1485 const res = args.map(ind => cm.posFromIndex(ind));
1486 return args.length === 1 ? res[0] : res;
1490 * The reverse of getPosition. Similarly to getPosition this
1491 * method returns a single value if only one argument was given
1492 * and an array otherwise.
1494 getOffset(...args) {
1495 const cm = editors.get(this);
1496 const res = args.map(pos => cm.indexFromPos(pos));
1497 return args.length > 1 ? res : res[0];
1501 * Returns a {line, ch} object that corresponds to the
1502 * left, top coordinates.
1504 getPositionFromCoords({ left, top }) {
1505 const cm = editors.get(this);
1506 return cm.coordsChar({ left, top });
1510 * The reverse of getPositionFromCoords. Similarly, returns a {left, top}
1511 * object that corresponds to the specified line and character number.
1513 getCoordsFromPosition({ line, ch }) {
1514 const cm = editors.get(this);
1515 return cm.charCoords({ line: ~~line, ch: ~~ch });
1519 * Returns true if there's something to undo and false otherwise.
1522 const cm = editors.get(this);
1523 return cm.historySize().undo > 0;
1527 * Returns true if there's something to redo and false otherwise.
1530 const cm = editors.get(this);
1531 return cm.historySize().redo > 0;
1535 * Marks the contents as clean and returns the current
1539 const cm = editors.get(this);
1540 this.version = cm.changeGeneration();
1541 this.#lastDirty = false;
1542 this.emit("dirty-change");
1543 return this.version;
1547 * Returns true if contents of the text area are
1548 * clean i.e. no changes were made since the last version.
1551 const cm = editors.get(this);
1552 return cm.isClean(this.version);
1556 * This method opens an in-editor dialog asking for a line to
1557 * jump to. Once given, it changes cursor to that line.
1560 const doc = editors.get(this).getWrapperElement().ownerDocument;
1561 const div = doc.createElement("div");
1562 const inp = doc.createElement("input");
1563 const txt = doc.createTextNode(L10N.getStr("gotoLineCmd.promptTitle"));
1566 inp.style.width = "10em";
1567 inp.style.marginInlineStart = "1em";
1569 div.appendChild(txt);
1570 div.appendChild(inp);
1572 this.openDialog(div, line => {
1573 // Handle LINE:COLUMN as well as LINE
1574 const match = line.toString().match(RE_JUMP_TO_LINE);
1576 const [, matchLine, column] = match;
1577 this.setCursor({ line: matchLine - 1, ch: column ? column - 1 : 0 });
1583 * Moves the content of the current line or the lines selected up a line.
1586 const cm = editors.get(this);
1587 const start = cm.getCursor("start");
1588 const end = cm.getCursor("end");
1590 if (start.line === 0) {
1594 // Get the text in the lines selected or the current line of the cursor
1595 // and append the text of the previous line.
1597 if (start.line !== end.line) {
1600 { line: start.line, ch: 0 },
1601 { line: end.line, ch: cm.getLine(end.line).length }
1604 value = cm.getLine(start.line) + "\n";
1606 value += cm.getLine(start.line - 1);
1608 // Replace the previous line and the currently selected lines with the new
1609 // value and maintain the selection of the text.
1612 { line: start.line - 1, ch: 0 },
1613 { line: end.line, ch: cm.getLine(end.line).length }
1616 { line: start.line - 1, ch: start.ch },
1617 { line: end.line - 1, ch: end.ch }
1622 * Moves the content of the current line or the lines selected down a line.
1625 const cm = editors.get(this);
1626 const start = cm.getCursor("start");
1627 const end = cm.getCursor("end");
1629 if (end.line + 1 === cm.lineCount()) {
1633 // Get the text of next line and append the text in the lines selected
1634 // or the current line of the cursor.
1635 let value = cm.getLine(end.line + 1) + "\n";
1636 if (start.line !== end.line) {
1637 value += cm.getRange(
1638 { line: start.line, ch: 0 },
1639 { line: end.line, ch: cm.getLine(end.line).length }
1642 value += cm.getLine(start.line);
1645 // Replace the currently selected lines and the next line with the new
1646 // value and maintain the selection of the text.
1649 { line: start.line, ch: 0 },
1650 { line: end.line + 1, ch: cm.getLine(end.line + 1).length }
1653 { line: start.line + 1, ch: start.ch },
1654 { line: end.line + 1, ch: end.ch }
1659 * Intercept CodeMirror's Find and replace key shortcut to select the search input
1661 findOrReplace(node, isReplaceAll) {
1662 const cm = editors.get(this);
1663 const isInput = node.tagName === "INPUT";
1664 const isSearchInput = isInput && node.type === "search";
1665 // replace box is a different input instance than search, and it is
1666 // located in a code mirror dialog
1667 const isDialogInput =
1670 node.parentNode.classList.contains("CodeMirror-dialog");
1671 if (!(isSearchInput || isDialogInput)) {
1675 if (isSearchInput || isReplaceAll) {
1676 // select the search input
1677 // it's the precise reason why we reimplement these key shortcuts
1681 // need to call it since we prevent the propagation of the event and
1682 // cancel codemirror's key handling
1683 cm.execCommand("find");
1687 * Intercept CodeMirror's findNext and findPrev key shortcut to allow
1688 * immediately search for next occurance after typing a word to search.
1690 findNextOrPrev(node, isFindPrev) {
1691 const cm = editors.get(this);
1692 const isInput = node.tagName === "INPUT";
1693 const isSearchInput = isInput && node.type === "search";
1694 if (!isSearchInput) {
1697 const query = node.value;
1698 // cm.state.search allows to automatically start searching for the next occurance
1699 // it's the precise reason why we reimplement these key shortcuts
1700 if (!cm.state.search || cm.state.search.query !== query) {
1709 // need to call it since we prevent the propagation of the event and
1710 // cancel codemirror's key handling
1712 cm.execCommand("findPrev");
1714 cm.execCommand("findNext");
1719 * Returns current font size for the editor area, in pixels.
1722 const cm = editors.get(this);
1723 const el = cm.getWrapperElement();
1724 const win = el.ownerDocument.defaultView;
1726 return parseInt(win.getComputedStyle(el).getPropertyValue("font-size"), 10);
1730 * Sets font size for the editor area.
1733 const cm = editors.get(this);
1734 cm.getWrapperElement().style.fontSize = parseInt(size, 10) + "px";
1738 setLineWrapping(value) {
1739 const cm = editors.get(this);
1740 if (this.config.cm6) {
1742 codemirrorView: { EditorView },
1743 } = this.#CodeMirror6;
1745 effects: this.#compartments.lineWrapCompartment.reconfigure(
1746 value ? EditorView.lineWrapping : []
1750 cm.setOption("lineWrapping", value);
1752 this.config.lineWrapping = value;
1756 * Sets an option for the editor. For most options it just defers to
1757 * CodeMirror.setOption, but certain ones are maintained within the editor
1761 const cm = editors.get(this);
1763 // Save the state of a valid autoCloseBrackets string, so we can reset
1764 // it if it gets preffed off and back on.
1765 if (o === "autoCloseBrackets" && v) {
1766 this.config.autoCloseBracketsSaved = v;
1769 if (o === "autocomplete") {
1770 this.config.autocomplete = v;
1771 this.setupAutoCompletion();
1777 if (o === "enableCodeFolding") {
1778 // The new value maybe explicitly force foldGUtter on or off, ignoring
1779 // the prefs service.
1780 this.updateCodeFoldingGutter();
1785 * Gets an option for the editor. For most options it just defers to
1786 * CodeMirror.getOption, but certain ones are maintained within the editor
1790 const cm = editors.get(this);
1791 if (o === "autocomplete") {
1792 return this.config.autocomplete;
1795 return cm.getOption(o);
1799 * Sets up autocompletion for the editor. Lazily imports the required
1800 * dependencies because they vary by editor mode.
1802 * Autocompletion is special, because we don't want to automatically use
1803 * it just because it is preffed on (it still needs to be requested by the
1804 * editor), but we do want to always disable it if it is preffed off.
1806 setupAutoCompletion() {
1807 if (!this.config.autocomplete && !this.initializeAutoCompletion) {
1808 // Do nothing since there is no autocomplete config and no autocompletion have
1809 // been initialized.
1812 // The autocomplete module will overwrite this.initializeAutoCompletion
1813 // with a mode specific autocompletion handler.
1814 if (!this.initializeAutoCompletion) {
1816 require("resource://devtools/client/shared/sourceeditor/autocomplete.js")
1820 if (this.config.autocomplete && Services.prefs.getBoolPref(AUTOCOMPLETE)) {
1821 this.initializeAutoCompletion(this.config.autocompleteOpts);
1823 this.destroyAutoCompletion();
1827 getAutoCompletionText() {
1828 const cm = editors.get(this);
1831 .find(m => m.className === AUTOCOMPLETE_MARK_CLASSNAME);
1836 return mark.attributes["data-completion"] || "";
1839 setAutoCompletionText(text) {
1840 const cursor = this.getCursor();
1841 const cm = editors.get(this);
1842 const className = AUTOCOMPLETE_MARK_CLASSNAME;
1844 cm.operation(() => {
1845 cm.getAllMarks().forEach(mark => {
1846 if (mark.className === className) {
1852 cm.markText({ ...cursor, ch: cursor.ch - 1 }, cursor, {
1855 "data-completion": text,
1863 * Extends an instance of the Editor object with additional
1864 * functions. Each function will be called with context as
1865 * the first argument. Context is a {ed, cm} object where
1866 * 'ed' is an instance of the Editor object and 'cm' is an
1867 * instance of the CodeMirror object. Example:
1869 * function hello(ctx, name) {
1870 * let { cm, ed } = ctx;
1871 * cm; // CodeMirror instance
1872 * ed; // Editor instance
1873 * name; // 'Mozilla'
1876 * editor.extend({ hello: hello });
1877 * editor.hello('Mozilla');
1880 Object.keys(funcs).forEach(name => {
1881 const cm = editors.get(this);
1882 const ctx = { ed: this, cm, Editor };
1884 if (name === "initialize") {
1889 this[name] = funcs[name].bind(null, ctx);
1894 return !editors.get(this);
1898 this.container = null;
1900 this.version = null;
1901 this.#ownerDoc = null;
1902 this.#updateListener = null;
1903 this.#lineGutterMarkers.clear();
1905 if (this.#prefObserver) {
1906 this.#prefObserver.off(KEYMAP_PREF, this.setKeyMap);
1907 this.#prefObserver.off(TAB_SIZE, this.reloadPreferences);
1908 this.#prefObserver.off(EXPAND_TAB, this.reloadPreferences);
1909 this.#prefObserver.off(AUTO_CLOSE, this.reloadPreferences);
1910 this.#prefObserver.off(AUTOCOMPLETE, this.reloadPreferences);
1911 this.#prefObserver.off(DETECT_INDENT, this.reloadPreferences);
1912 this.#prefObserver.off(ENABLE_CODE_FOLDING, this.reloadPreferences);
1913 this.#prefObserver.destroy();
1916 // Remove the link between the document and code-mirror.
1917 const cm = editors.get(this);
1922 this.emit("destroy");
1925 updateCodeFoldingGutter() {
1926 let shouldFoldGutter = this.config.enableCodeFolding;
1927 const foldGutterIndex = this.config.gutters.indexOf(
1928 "CodeMirror-foldgutter"
1930 const cm = editors.get(this);
1932 if (shouldFoldGutter === undefined) {
1933 shouldFoldGutter = Services.prefs.getBoolPref(ENABLE_CODE_FOLDING);
1936 if (shouldFoldGutter) {
1937 // Add the gutter before enabling foldGutter
1938 if (foldGutterIndex === -1) {
1939 const gutters = this.config.gutters.slice();
1940 gutters.push("CodeMirror-foldgutter");
1941 this.setOption("gutters", gutters);
1944 this.setOption("foldGutter", true);
1946 // No code should remain folded when folding is off.
1948 cm.execCommand("unfoldAll");
1951 // Remove the gutter so it doesn't take up space
1952 if (foldGutterIndex !== -1) {
1953 const gutters = this.config.gutters.slice();
1954 gutters.splice(foldGutterIndex, 1);
1955 this.setOption("gutters", gutters);
1958 this.setOption("foldGutter", false);
1963 * Register all key shortcuts.
1965 #initSearchShortcuts(win) {
1966 const shortcuts = new KeyShortcuts({
1969 const keys = ["find.key", "findNext.key", "findPrev.key"];
1971 if (OS === "Darwin") {
1972 keys.push("replaceAllMac.key");
1974 keys.push("replaceAll.key");
1976 // Process generic keys:
1977 keys.forEach(name => {
1978 const key = L10N.getStr(name);
1979 shortcuts.on(key, event => this.#onSearchShortcut(name, event));
1983 * Key shortcut listener.
1985 #onSearchShortcut = (name, event) => {
1986 if (!this.#isInputOrTextarea(event.target)) {
1989 const node = event.originalTarget;
1992 // replaceAll.key is Alt + find.key
1993 case "replaceAllMac.key":
1994 this.findOrReplace(node, true);
1996 // replaceAll.key is Shift + find.key
1997 case "replaceAll.key":
1998 this.findOrReplace(node, true);
2001 this.findOrReplace(node, false);
2003 // findPrev.key is Shift + findNext.key
2004 case "findPrev.key":
2005 this.findNextOrPrev(node, true);
2007 case "findNext.key":
2008 this.findNextOrPrev(node, false);
2011 console.error("Unexpected editor key shortcut", name);
2014 // Prevent default for this action
2015 event.stopPropagation();
2016 event.preventDefault();
2020 * Check if a node is an input or textarea
2022 #isInputOrTextarea(element) {
2023 const name = element.tagName.toLowerCase();
2024 return name === "input" || name === "textarea";
2028 // Since Editor is a thin layer over CodeMirror some methods
2029 // are mapped directly—without any changes.
2031 CM_MAPPING.forEach(name => {
2032 Editor.prototype[name] = function (...args) {
2033 const cm = editors.get(this);
2034 return cm[name].apply(cm, args);
2039 * We compute the CSS property names, values, and color names to be used with
2040 * CodeMirror to more closely reflect what is supported by the target platform.
2041 * The database is used to replace the values used in CodeMirror while initiating
2042 * an editor object. This is done here instead of the file codemirror/css.js so
2043 * as to leave that file untouched and easily upgradable.
2045 function getCSSKeywords(cssProperties) {
2046 function keySet(array) {
2048 for (let i = 0; i < array.length; ++i) {
2049 keys[array[i]] = true;
2054 const propertyKeywords = cssProperties.getNames();
2055 const colorKeywords = {};
2056 const valueKeywords = {};
2058 propertyKeywords.forEach(property => {
2059 if (property.includes("color")) {
2060 cssProperties.getValues(property).forEach(value => {
2061 colorKeywords[value] = true;
2064 cssProperties.getValues(property).forEach(value => {
2065 valueKeywords[value] = true;
2071 propertyKeywords: keySet(propertyKeywords),
2077 module.exports = Editor;