tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

editor.js (130044B)


      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/. */
      4 
      5 "use strict";
      6 
      7 const {
      8  EXPAND_TAB,
      9  TAB_SIZE,
     10  DETECT_INDENT,
     11  getIndentationFromIteration,
     12 } = require("resource://devtools/shared/indentation.js");
     13 
     14 const { debounce } = require("resource://devtools/shared/debounce.js");
     15 const nodeConstants = require("resource://devtools/shared/dom-node-constants.js");
     16 
     17 const ENABLE_CODE_FOLDING = "devtools.editor.enableCodeFolding";
     18 const KEYMAP_PREF = "devtools.editor.keymap";
     19 const AUTO_CLOSE = "devtools.editor.autoclosebrackets";
     20 const AUTOCOMPLETE = "devtools.editor.autocomplete";
     21 const CARET_BLINK_TIME = "ui.caretBlinkTime";
     22 const XHTML_NS = "http://www.w3.org/1999/xhtml";
     23 
     24 const VALID_KEYMAPS = new Map([
     25  [
     26    "emacs",
     27    "chrome://devtools/content/shared/sourceeditor/codemirror/keymap/emacs.js",
     28  ],
     29  [
     30    "vim",
     31    "chrome://devtools/content/shared/sourceeditor/codemirror/keymap/vim.js",
     32  ],
     33  [
     34    "sublime",
     35    "chrome://devtools/content/shared/sourceeditor/codemirror/keymap/sublime.js",
     36  ],
     37 ]);
     38 
     39 // Maximum allowed margin (in number of lines) from top or bottom of the editor
     40 // while shifting to a line which was initially out of view.
     41 const MAX_VERTICAL_OFFSET = 3;
     42 
     43 const RE_JUMP_TO_LINE = /^(\d+):?(\d+)?/;
     44 const AUTOCOMPLETE_MARK_CLASSNAME = "cm-auto-complete-shadow-text";
     45 
     46 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
     47 const { PrefObserver } = require("resource://devtools/client/shared/prefs.js");
     48 const KeyShortcuts = require("resource://devtools/client/shared/key-shortcuts.js");
     49 
     50 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
     51 const L10N = new LocalizationHelper(
     52  "devtools/client/locales/sourceeditor.properties"
     53 );
     54 
     55 loader.lazyRequireGetter(
     56  this,
     57  "wasm",
     58  "resource://devtools/client/shared/sourceeditor/wasm.js"
     59 );
     60 
     61 loader.lazyRequireGetter(
     62  this,
     63  "scopeUtils",
     64  "resource://devtools/client/shared/sourceeditor/scope-utils.js"
     65 );
     66 
     67 loader.lazyRequireGetter(
     68  this,
     69  "lezerUtils",
     70  "resource://devtools/client/shared/sourceeditor/lezer-utils.js"
     71 );
     72 
     73 const { OS } = Services.appinfo;
     74 
     75 // CM_BUNDLE and CM_IFRAME represent the HTML and JavaScript that is
     76 // injected into an iframe in order to initialize a CodeMirror instance.
     77 
     78 const CM_BUNDLE =
     79  "chrome://devtools/content/shared/sourceeditor/codemirror/codemirror.bundle.js";
     80 
     81 const CM_IFRAME =
     82  "chrome://devtools/content/shared/sourceeditor/codemirror/cmiframe.html";
     83 
     84 const CM_MAPPING = [
     85  "clearHistory",
     86  "defaultCharWidth",
     87  "extendSelection",
     88  "getCursor",
     89  "getLine",
     90  "getScrollInfo",
     91  "getSelection",
     92  "getViewport",
     93  "hasFocus",
     94  "lineCount",
     95  "openDialog",
     96  "redo",
     97  "refresh",
     98  "replaceSelection",
     99  "setSelection",
    100  "somethingSelected",
    101  "undo",
    102 ];
    103 
    104 const ONLY_SPACES_REGEXP = /^\s*$/;
    105 
    106 const editors = new WeakMap();
    107 
    108 /**
    109 * A very thin wrapper around CodeMirror. Provides a number
    110 * of helper methods to make our use of CodeMirror easier and
    111 * another method, appendTo, to actually create and append
    112 * the CodeMirror instance.
    113 *
    114 * Note that Editor doesn't expose CodeMirror instance to the
    115 * outside world.
    116 *
    117 * Constructor accepts one argument, config. It is very
    118 * similar to the CodeMirror configuration object so for most
    119 * properties go to CodeMirror's documentation (see below).
    120 *
    121 * Other than that, it accepts one additional and optional
    122 * property contextMenu. This property should be an element, or
    123 * an ID of an element that we can use as a context menu.
    124 *
    125 * This object is also an event emitter.
    126 *
    127 * CodeMirror docs: http://codemirror.net/doc/manual.html
    128 */
    129 class Editor extends EventEmitter {
    130  // Static methods on the Editor object itself.
    131 
    132  /**
    133   * Returns a string representation of a shortcut 'key' with
    134   * a OS specific modifier. Cmd- for Macs, Ctrl- for other
    135   * platforms. Useful with extraKeys configuration option.
    136   *
    137   * CodeMirror defines all keys with modifiers in the following
    138   * order: Shift - Ctrl/Cmd - Alt - Key
    139   */
    140  static accel(key, modifiers = {}) {
    141    return (
    142      (modifiers.shift ? "Shift-" : "") +
    143      (Services.appinfo.OS == "Darwin" ? "Cmd-" : "Ctrl-") +
    144      (modifiers.alt ? "Alt-" : "") +
    145      key
    146    );
    147  }
    148 
    149  /**
    150   * Returns a string representation of a shortcut for a
    151   * specified command 'cmd'. Append Cmd- for macs, Ctrl- for other
    152   * platforms unless noaccel is specified in the options. Useful when overwriting
    153   * or disabling default shortcuts.
    154   */
    155  static keyFor(cmd, opts = { noaccel: false }) {
    156    const key = L10N.getStr(cmd + ".commandkey");
    157    return opts.noaccel ? key : Editor.accel(key);
    158  }
    159 
    160  static modes = {
    161    cljs: { name: "text/x-clojure" },
    162    css: { name: "css" },
    163    fs: { name: "x-shader/x-fragment" },
    164    haxe: { name: "haxe" },
    165    http: { name: "http" },
    166    html: { name: "htmlmixed" },
    167    xml: { name: "xml" },
    168    javascript: { name: "javascript" },
    169    json: { name: "json" },
    170    text: { name: "text" },
    171    vs: { name: "x-shader/x-vertex" },
    172    wasm: { name: "wasm" },
    173  };
    174 
    175  markerTypes = {
    176    /* Line Markers */
    177    CONDITIONAL_BP_MARKER: "conditional-breakpoint-panel-marker",
    178    TRACE_MARKER: "trace-panel-marker",
    179    DEBUG_LINE_MARKER: "debug-line-marker",
    180    LINE_EXCEPTION_MARKER: "line-exception-marker",
    181    HIGHLIGHT_LINE_MARKER: "highlight-line-marker",
    182    MULTI_HIGHLIGHT_LINE_MARKER: "multi-highlight-line-marker",
    183    BLACKBOX_LINE_MARKER: "blackbox-line-marker",
    184    INLINE_PREVIEW_MARKER: "inline-preview-marker",
    185    /* Position Markers */
    186    COLUMN_BREAKPOINT_MARKER: "column-breakpoint-marker",
    187    DEBUG_POSITION_MARKER: "debug-position-marker",
    188    EXCEPTION_POSITION_MARKER: "exception-position-marker",
    189    ACTIVE_SELECTION_MARKER: "active-selection-marker",
    190    PAUSED_LOCATION_MARKER: "paused-location-marker",
    191    /* Gutter Markers */
    192    EMPTY_LINE_MARKER: "empty-line-marker",
    193    BLACKBOX_LINE_GUTTER_MARKER: "blackbox-line-gutter-marker",
    194    GUTTER_BREAKPOINT_MARKER: "gutter-breakpoint-marker",
    195  };
    196 
    197  container = null;
    198  version = null;
    199  config = null;
    200  Doc = null;
    201  searchState = {
    202    cursors: [],
    203    currentCursorIndex: -1,
    204    query: "",
    205  };
    206 
    207  #abortController;
    208 
    209  // The id for the current source in the editor (selected source). This is used to:
    210  // * cache the scroll snapshot for tracking scroll positions and the symbols,
    211  // * know when an actual source is displayed (and not only a loading/error message)
    212  #currentDocumentId = null;
    213 
    214  #currentDocument = null;
    215  #CodeMirror6;
    216  #compartments;
    217  #effects;
    218  #lastDirty;
    219  #loadedKeyMaps;
    220  #ownerDoc;
    221  #prefObserver;
    222  #win;
    223  #lineGutterMarkers = new Map();
    224  #lineContentMarkers = new Map();
    225  #posContentMarkers = new Map();
    226  #editorDOMEventHandlers = {};
    227  #gutterDOMEventHandlers = {};
    228  // A cache of all the scroll snapshots for the all the sources that
    229  // are currently open in the editor. The keys for the Map are the id's
    230  // for the source and the values are the scroll snapshots for the sources.
    231  #scrollSnapshots = new Map();
    232  #updateListener = null;
    233 
    234  // This stores the language support objects used to syntax highlight code,
    235  // These are keyed of the modes.
    236  #languageModes = new Map();
    237 
    238  #sources = new Map();
    239 
    240  constructor(config) {
    241    super();
    242 
    243    const tabSize = Services.prefs.getIntPref(TAB_SIZE);
    244    const useTabs = !Services.prefs.getBoolPref(EXPAND_TAB);
    245    const useAutoClose = Services.prefs.getBoolPref(AUTO_CLOSE);
    246 
    247    this.version = null;
    248    this.config = {
    249      cm6: false,
    250      value: "",
    251      mode: Editor.modes.text,
    252      indentUnit: tabSize,
    253      tabSize,
    254      contextMenu: null,
    255      matchBrackets: true,
    256      highlightSelectionMatches: {
    257        wordsOnly: true,
    258      },
    259      extraKeys: {},
    260      indentWithTabs: useTabs,
    261      inputStyle: "accessibleTextArea",
    262      // This is set to the biggest value for setTimeout (See https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout#Maximum_delay_value)
    263      // This is because codeMirror queries the underlying textArea for some things that
    264      // can't be retrieved with events in some browser (but we're fine in Firefox).
    265      pollInterval: Math.pow(2, 31) - 1,
    266      styleActiveLine: true,
    267      autoCloseBrackets: "()[]{}''\"\"``",
    268      autoCloseEnabled: useAutoClose,
    269      theme: "mozilla",
    270      themeSwitching: true,
    271      autocomplete: false,
    272      autocompleteOpts: {},
    273      // Expect a CssProperties object (see devtools/client/fronts/css-properties.js)
    274      cssProperties: null,
    275      // Set to `true` to prevent the search addon to be activated.
    276      disableSearchAddon: false,
    277      // When the search addon is activated (i.e disableSearchAddon == false),
    278      // `useSearchAddonPanel` determines if the default search panel for the search addon should be used.
    279      // Set to `false` when a custom search panel is used.
    280      // Note: This can probably be removed when Bug 1941575 is fixed, and custom search panel is used everywhere
    281      useSearchAddonPanel: true,
    282      maxHighlightLength: 1000,
    283      // Disable codeMirror setTimeout-based cursor blinking (will be replaced by a CSS animation)
    284      cursorBlinkRate: 0,
    285      // List of non-printable chars that will be displayed in the editor, showing their
    286      // unicode version. We only add a few characters to the default list:
    287      // - \u202d LEFT-TO-RIGHT OVERRIDE
    288      // - \u202e RIGHT-TO-LEFT OVERRIDE
    289      // - \u2066 LEFT-TO-RIGHT ISOLATE
    290      // - \u2067 RIGHT-TO-LEFT ISOLATE
    291      // - \u2069 POP DIRECTIONAL ISOLATE
    292      specialChars:
    293        // eslint-disable-next-line no-control-regex
    294        /[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b-\u200f\u2028\u2029\u202d\u202e\u2066\u2067\u2069\ufeff\ufff9-\ufffc]/,
    295      specialCharPlaceholder: char => {
    296        // Use the doc provided to the setup function if we don't have a reference to a codeMirror
    297        // editor yet (this can happen when an Editor is being created with existing content)
    298        const doc = this.#ownerDoc;
    299        const el = doc.createElement("span");
    300        el.classList.add("cm-non-printable-char");
    301        el.append(doc.createTextNode(`\\u${char.codePointAt(0).toString(16)}`));
    302        return el;
    303      },
    304      // In CodeMirror 5, adds a `CodeMirror-selectedtext` class on selected text that
    305      // can be used to set the selected text color, which isn't possible by default.
    306      // This is especially useful for High Contrast Mode where we do need to adjust the
    307      // selection text color
    308      styleSelectedText: true,
    309    };
    310 
    311    // Additional shortcuts.
    312    this.config.extraKeys[Editor.keyFor("jumpToLine")] = () =>
    313      this.jumpToLine();
    314    this.config.extraKeys[Editor.keyFor("moveLineUp", { noaccel: true })] =
    315      () => this.moveLineUp();
    316    this.config.extraKeys[Editor.keyFor("moveLineDown", { noaccel: true })] =
    317      () => this.moveLineDown();
    318    this.config.extraKeys[Editor.keyFor("toggleComment")] = "toggleComment";
    319 
    320    // Disable ctrl-[ and ctrl-] because toolbox uses those shortcuts.
    321    this.config.extraKeys[Editor.keyFor("indentLess")] = false;
    322    this.config.extraKeys[Editor.keyFor("indentMore")] = false;
    323 
    324    // Disable Alt-B and Alt-F to navigate groups (respectively previous and next) since:
    325    // - it's not standard in input fields
    326    // - it also inserts a character which feels weird
    327    this.config.extraKeys["Alt-B"] = false;
    328    this.config.extraKeys["Alt-F"] = false;
    329 
    330    // Disable Ctrl/Cmd + U as it's used for "View Source". It's okay to disable Ctrl+U as
    331    // the underlying command, `undoSelection`, isn't standard in input fields and isn't
    332    // widely known.
    333    this.config.extraKeys[Editor.accel("U")] = false;
    334 
    335    if (!config.disableSearchAddon) {
    336      // Override the default search shortcut so the built-in UI doesn't get hidden
    337      // when hitting Enter (so the user can cycle through results).
    338      this.config.extraKeys[Editor.accel("F")] = () =>
    339        editors.get(this).execCommand("findPersistent");
    340    }
    341 
    342    // Disable keys that trigger events with a null-string `which` property.
    343    // It looks like some of those (e.g. the Function key), can trigger a poll
    344    // which fails to see that there's a selection, which end up replacing the
    345    // selected text with an empty string.
    346    // TODO: We should investigate the root cause.
    347    this.config.extraKeys["'\u0000'"] = false;
    348 
    349    // Overwrite default config with user-provided, if needed.
    350    Object.keys(config).forEach(k => {
    351      if (k != "extraKeys") {
    352        this.config[k] = config[k];
    353        return;
    354      }
    355 
    356      if (!config.extraKeys) {
    357        return;
    358      }
    359 
    360      Object.keys(config.extraKeys).forEach(key => {
    361        this.config.extraKeys[key] = config.extraKeys[key];
    362      });
    363    });
    364 
    365    if (!this.config.gutters) {
    366      this.config.gutters = [];
    367    }
    368    if (
    369      this.config.lineNumbers &&
    370      !this.config.gutters.includes("CodeMirror-linenumbers")
    371    ) {
    372      this.config.gutters.push("CodeMirror-linenumbers");
    373    }
    374 
    375    // Remember the initial value of autoCloseBrackets.
    376    this.config.autoCloseBracketsSaved = this.config.autoCloseBrackets;
    377 
    378    // If the tab behaviour is not explicitly set to `false` from the config, set a tab behavior.
    379    // If something is selected, indent those lines. If nothing is selected and we're
    380    // indenting with tabs, insert one tab. Otherwise insert N
    381    // whitespaces where N == indentUnit option.
    382    if (this.config.extraKeys.Tab !== false) {
    383      this.config.extraKeys.Tab = cm => {
    384        if (config.extraKeys?.Tab) {
    385          // If a consumer registers its own extraKeys.Tab, we execute it before doing
    386          // anything else. If it returns false, that mean that all the key handling work is
    387          // done, so we can do an early return.
    388          const res = config.extraKeys.Tab(cm);
    389          if (res === false) {
    390            return;
    391          }
    392        }
    393 
    394        if (cm.somethingSelected()) {
    395          cm.indentSelection("add");
    396          return;
    397        }
    398 
    399        if (this.config.indentWithTabs) {
    400          cm.replaceSelection("\t", "end", "+input");
    401          return;
    402        }
    403 
    404        let num = cm.getOption("indentUnit");
    405        if (cm.getCursor().ch !== 0) {
    406          num -= cm.getCursor().ch % num;
    407        }
    408        cm.replaceSelection(" ".repeat(num), "end", "+input");
    409      };
    410 
    411      if (this.config.cssProperties) {
    412        // Ensure that autocompletion has cssProperties if it's passed in via the options.
    413        this.config.autocompleteOpts.cssProperties = this.config.cssProperties;
    414      }
    415    }
    416  }
    417 
    418  /**
    419   * Exposes the CodeMirror class. We want to be able to
    420   * invoke static commands such as runMode for syntax highlighting.
    421   */
    422  get CodeMirror() {
    423    const codeMirror = editors.get(this);
    424    return codeMirror?.constructor;
    425  }
    426 
    427  /**
    428   * Exposes the CodeMirror instance. We want to get away from trying to
    429   * abstract away the API entirely, and this makes it easier to integrate in
    430   * various environments and do complex things.
    431   */
    432  get codeMirror() {
    433    if (!editors.has(this)) {
    434      throw new Error(
    435        "CodeMirror instance does not exist. You must wait " +
    436          "for it to be appended to the DOM."
    437      );
    438    }
    439    return editors.get(this);
    440  }
    441 
    442  /**
    443   * Return whether there is a CodeMirror instance associated with this Editor.
    444   */
    445  get hasCodeMirror() {
    446    return editors.has(this);
    447  }
    448 
    449  /**
    450   * Appends the current Editor instance to the element specified by
    451   * 'el'. You can also provide your own iframe to host the editor as
    452   * an optional second parameter. This method actually creates and
    453   * loads CodeMirror and all its dependencies.
    454   *
    455   * This method is asynchronous and returns a promise.
    456   */
    457  appendTo(el, env) {
    458    return new Promise(resolve => {
    459      const cm = editors.get(this);
    460 
    461      if (!env) {
    462        env = el.ownerDocument.createElementNS(XHTML_NS, "iframe");
    463        env.className = "source-editor-frame";
    464      }
    465 
    466      if (cm) {
    467        throw new Error("You can append an editor only once.");
    468      }
    469 
    470      const onLoad = () => {
    471        // Prevent flickering by showing the iframe once loaded.
    472        // See https://github.com/w3c/csswg-drafts/issues/9624
    473        env.style.visibility = "";
    474        const win = env.contentWindow.wrappedJSObject;
    475        this.container = env;
    476 
    477        const editorEl = win.document.body;
    478        const editorDoc = el.ownerDocument;
    479        if (this.config.cm6) {
    480          this.#setupCm6(editorEl, editorDoc);
    481        } else {
    482          this.#setup(editorEl, editorDoc);
    483        }
    484        resolve();
    485      };
    486 
    487      env.style.visibility = "hidden";
    488      env.addEventListener("load", onLoad, {
    489        capture: true,
    490        once: true,
    491        signal: this.#abortController?.signal,
    492      });
    493      env.src = CM_IFRAME;
    494      el.appendChild(env);
    495 
    496      this.once("destroy", () => el.removeChild(env));
    497    });
    498  }
    499 
    500  appendToLocalElement(el) {
    501    const win = el.ownerDocument.defaultView;
    502    this.#abortController = new win.AbortController();
    503    if (this.config.cm6) {
    504      this.#setupCm6(el);
    505    } else {
    506      this.#setup(el);
    507    }
    508  }
    509 
    510  // This update listener allows listening to the changes
    511  // to the codemiror editor.
    512  setUpdateListener(listener = null) {
    513    this.#updateListener = listener;
    514  }
    515 
    516  /**
    517   * Do the actual appending and configuring of the CodeMirror instance. This is
    518   * used by both append functions above, and does all the hard work to
    519   * configure CodeMirror with all the right options/modes/etc.
    520   */
    521  #setup(el, doc) {
    522    this.#ownerDoc = doc || el.ownerDocument;
    523    const win = el.ownerDocument.defaultView;
    524 
    525    Services.scriptloader.loadSubScript(CM_BUNDLE, win);
    526    this.#win = win;
    527 
    528    if (this.config.cssProperties) {
    529      // Replace the propertyKeywords, colorKeywords and valueKeywords
    530      // properties of the CSS MIME type with the values provided by the CSS properties
    531      // database.
    532      const { propertyKeywords, colorKeywords, valueKeywords } = getCSSKeywords(
    533        this.config.cssProperties
    534      );
    535 
    536      const cssSpec = win.CodeMirror.resolveMode("text/css");
    537      cssSpec.propertyKeywords = propertyKeywords;
    538      cssSpec.colorKeywords = colorKeywords;
    539      cssSpec.valueKeywords = valueKeywords;
    540      win.CodeMirror.defineMIME("text/css", cssSpec);
    541 
    542      const scssSpec = win.CodeMirror.resolveMode("text/x-scss");
    543      scssSpec.propertyKeywords = propertyKeywords;
    544      scssSpec.colorKeywords = colorKeywords;
    545      scssSpec.valueKeywords = valueKeywords;
    546      win.CodeMirror.defineMIME("text/x-scss", scssSpec);
    547    }
    548 
    549    win.CodeMirror.commands.save = () => this.emit("saveRequested");
    550 
    551    // Create a CodeMirror instance add support for context menus,
    552    // overwrite the default controller (otherwise items in the top and
    553    // context menus won't work).
    554 
    555    const cm = win.CodeMirror(el, this.config);
    556    this.Doc = win.CodeMirror.Doc;
    557 
    558    // Disable APZ for source editors. It currently causes the line numbers to
    559    // "tear off" and swim around on top of the content. Bug 1160601 tracks
    560    // finding a solution that allows APZ to work with CodeMirror.
    561    cm.getScrollerElement().addEventListener(
    562      "wheel",
    563      ev => {
    564        // By handling the wheel events ourselves, we force the platform to
    565        // scroll synchronously, like it did before APZ. However, we lose smooth
    566        // scrolling for users with mouse wheels. This seems acceptible vs.
    567        // doing nothing and letting the gutter slide around.
    568        ev.preventDefault();
    569 
    570        let { deltaX, deltaY } = ev;
    571 
    572        if (ev.deltaMode == ev.DOM_DELTA_LINE) {
    573          deltaX *= cm.defaultCharWidth();
    574          deltaY *= cm.defaultTextHeight();
    575        } else if (ev.deltaMode == ev.DOM_DELTA_PAGE) {
    576          deltaX *= cm.getWrapperElement().clientWidth;
    577          deltaY *= cm.getWrapperElement().clientHeight;
    578        }
    579 
    580        cm.getScrollerElement().scrollBy(deltaX, deltaY);
    581      },
    582      { signal: this.#abortController?.signal }
    583    );
    584 
    585    cm.getWrapperElement().addEventListener(
    586      "contextmenu",
    587      ev => {
    588        if (!this.config.contextMenu) {
    589          return;
    590        }
    591 
    592        ev.stopPropagation();
    593        ev.preventDefault();
    594 
    595        let popup = this.config.contextMenu;
    596        if (typeof popup == "string") {
    597          popup = this.#ownerDoc.getElementById(this.config.contextMenu);
    598        }
    599 
    600        this.emit("popupOpen", ev, popup);
    601        popup.openPopupAtScreen(ev.screenX, ev.screenY, true);
    602      },
    603      { signal: this.#abortController?.signal }
    604    );
    605 
    606    const pipedEvents = [
    607      "beforeChange",
    608      "blur",
    609      "changes",
    610      "cursorActivity",
    611      "focus",
    612      "keyHandled",
    613      "scroll",
    614    ];
    615    for (const eventName of pipedEvents) {
    616      cm.on(eventName, (...args) => this.emit(eventName, ...args));
    617    }
    618 
    619    cm.on("change", () => {
    620      this.emit("change");
    621      if (!this.#lastDirty) {
    622        this.#lastDirty = true;
    623        this.emit("dirty-change");
    624      }
    625    });
    626 
    627    cm.on("gutterClick", (cmArg, line, gutter, ev) => {
    628      const lineOrOffset = !this.isWasm ? line : this.lineToWasmOffset(line);
    629      this.emit("gutterClick", lineOrOffset, ev.button);
    630    });
    631 
    632    win.CodeMirror.defineExtension("l10n", name => {
    633      return L10N.getStr(name);
    634    });
    635 
    636    if (!this.config.disableSearchAddon) {
    637      this.#initSearchShortcuts(win);
    638    } else {
    639      // Hotfix for Bug 1527898. We should remove those overrides as part of Bug 1527903.
    640      Object.assign(win.CodeMirror.commands, {
    641        find: null,
    642        findPersistent: null,
    643        findPersistentNext: null,
    644        findPersistentPrev: null,
    645        findNext: null,
    646        findPrev: null,
    647        clearSearch: null,
    648        replace: null,
    649        replaceAll: null,
    650      });
    651    }
    652 
    653    // Retrieve the cursor blink rate from user preference, or fall back to CodeMirror's
    654    // default value.
    655    let cursorBlinkingRate = win.CodeMirror.defaults.cursorBlinkRate;
    656    if (Services.prefs.prefHasUserValue(CARET_BLINK_TIME)) {
    657      cursorBlinkingRate = Services.prefs.getIntPref(
    658        CARET_BLINK_TIME,
    659        cursorBlinkingRate
    660      );
    661    }
    662    // This will be used in the animation-duration property we set on the cursor to
    663    // implement the blinking animation. If cursorBlinkingRate is 0 or less, the cursor
    664    // won't blink.
    665    cm.getWrapperElement().style.setProperty(
    666      "--caret-blink-time",
    667      `${Math.max(0, cursorBlinkingRate)}ms`
    668    );
    669 
    670    editors.set(this, cm);
    671 
    672    this.reloadPreferences = this.reloadPreferences.bind(this);
    673    this.setKeyMap = this.setKeyMap.bind(this, win);
    674 
    675    this.#prefObserver = new PrefObserver("devtools.editor.");
    676    this.#prefObserver.on(TAB_SIZE, this.reloadPreferences);
    677    this.#prefObserver.on(EXPAND_TAB, this.reloadPreferences);
    678    this.#prefObserver.on(AUTO_CLOSE, this.reloadPreferences);
    679    this.#prefObserver.on(AUTOCOMPLETE, this.reloadPreferences);
    680    this.#prefObserver.on(DETECT_INDENT, this.reloadPreferences);
    681    this.#prefObserver.on(ENABLE_CODE_FOLDING, this.reloadPreferences);
    682 
    683    this.reloadPreferences();
    684 
    685    // Init a map of the loaded keymap files. Should be of the form Map<String->Boolean>.
    686    this.#loadedKeyMaps = new Set();
    687    this.#prefObserver.on(KEYMAP_PREF, this.setKeyMap);
    688    this.setKeyMap();
    689 
    690    win.editor = this;
    691    const editorReadyEvent = new win.CustomEvent("editorReady");
    692    win.dispatchEvent(editorReadyEvent);
    693  }
    694 
    695  #setupLanguageModes() {
    696    if (!this.config.cm6) {
    697      return;
    698    }
    699    const {
    700      codemirrorLangJavascript,
    701      codemirrorLangJson,
    702      codemirrorLangHtml,
    703      codemirrorLangXml,
    704      codemirrorLangCss,
    705    } = this.#CodeMirror6;
    706 
    707    this.#languageModes.set(
    708      Editor.modes.javascript,
    709      codemirrorLangJavascript.javascript()
    710    );
    711    this.#languageModes.set(Editor.modes.json, codemirrorLangJson.json());
    712    this.#languageModes.set(Editor.modes.html, codemirrorLangHtml.html());
    713    this.#languageModes.set(Editor.modes.xml, codemirrorLangXml.xml());
    714    this.#languageModes.set(Editor.modes.css, codemirrorLangCss.css());
    715  }
    716 
    717  /**
    718   * Do the actual appending and configuring of the CodeMirror 6 instance.
    719   * This is used by appendTo and appendToLocalElement, and does all the hard work to
    720   * configure CodeMirror 6 with all the right options/modes/etc.
    721   * This should be kept in sync with #setup.
    722   *
    723   * @param {Element} el: Element into which the codeMirror editor should be appended.
    724   * @param {Document} document: Optional document, if not set, will default to el.ownerDocument
    725   */
    726  #setupCm6(el, doc) {
    727    this.#ownerDoc = doc || el.ownerDocument;
    728    const win = el.ownerDocument.defaultView;
    729    this.#win = win;
    730 
    731    this.#CodeMirror6 = this.#win.ChromeUtils.importESModule(
    732      "resource://devtools/client/shared/sourceeditor/codemirror6/codemirror6.bundle.mjs",
    733      { global: "current" }
    734    );
    735 
    736    const {
    737      codemirror,
    738      codemirrorView: {
    739        drawSelection,
    740        EditorView,
    741        keymap,
    742        lineNumbers,
    743        placeholder,
    744      },
    745      codemirrorState: { EditorState, Compartment, Prec },
    746      codemirrorSearch: { search, searchKeymap, highlightSelectionMatches },
    747      codemirrorLanguage: {
    748        syntaxTreeAvailable,
    749        indentUnit,
    750        codeFolding,
    751        syntaxHighlighting,
    752        bracketMatching,
    753      },
    754      lezerHighlight,
    755    } = this.#CodeMirror6;
    756 
    757    this.#compartments = {
    758      tabSizeCompartment: new Compartment(),
    759      indentCompartment: new Compartment(),
    760      lineWrapCompartment: new Compartment(),
    761      lineNumberCompartment: new Compartment(),
    762      lineNumberMarkersCompartment: new Compartment(),
    763      searchHighlightCompartment: new Compartment(),
    764      domEventHandlersCompartment: new Compartment(),
    765      foldGutterCompartment: new Compartment(),
    766      languageCompartment: new Compartment(),
    767    };
    768 
    769    const { lineContentMarkerEffect, lineContentMarkerExtension } =
    770      this.#createlineContentMarkersExtension();
    771 
    772    const { positionContentMarkerEffect, positionContentMarkerExtension } =
    773      this.#createPositionContentMarkersExtension();
    774 
    775    this.#effects = { lineContentMarkerEffect, positionContentMarkerEffect };
    776 
    777    const indentStr = (this.config.indentWithTabs ? "\t" : " ").repeat(
    778      this.config.indentUnit || 2
    779    );
    780 
    781    // Track the scroll snapshot for the current document at the end of the scroll
    782    this.#editorDOMEventHandlers.scroll = [
    783      debounce(this.#cacheScrollSnapshot, 250),
    784    ];
    785 
    786    this.#setupLanguageModes();
    787 
    788    const languageMode = [];
    789    if (this.config.mode && this.#languageModes.has(this.config.mode)) {
    790      languageMode.push(this.#languageModes.get(this.config.mode));
    791    }
    792 
    793    const extensions = [
    794      bracketMatching(),
    795      this.#compartments.indentCompartment.of(indentUnit.of(indentStr)),
    796      this.#compartments.tabSizeCompartment.of(
    797        EditorState.tabSize.of(this.config.tabSize)
    798      ),
    799      this.#compartments.lineWrapCompartment.of(
    800        this.config.lineWrapping ? EditorView.lineWrapping : []
    801      ),
    802      EditorState.readOnly.of(this.config.readOnly),
    803      this.#compartments.lineNumberCompartment.of(
    804        this.config.lineNumbers ? lineNumbers() : []
    805      ),
    806      codeFolding({
    807        placeholderText: "↔",
    808      }),
    809      this.#compartments.foldGutterCompartment.of([]),
    810      syntaxHighlighting(lezerHighlight.classHighlighter),
    811      EditorView.updateListener.of(v => {
    812        if (!cm.isDocumentLoadComplete) {
    813          // Check that the full syntax tree is available the current viewport
    814          if (syntaxTreeAvailable(v.state, v.view.viewState.viewport.to)) {
    815            cm.isDocumentLoadComplete = true;
    816          }
    817        }
    818        if (v.viewportChanged || v.docChanged) {
    819          if (v.docChanged) {
    820            cm.isDocumentLoadComplete = false;
    821          }
    822          // reset line gutter markers for the new visible ranges
    823          // when the viewport changes(e.g when the page is scrolled).
    824          if (this.#lineGutterMarkers.size > 0) {
    825            this.setLineGutterMarkers();
    826          }
    827        }
    828        // Any custom defined update listener should be called
    829        if (typeof this.#updateListener == "function") {
    830          this.#updateListener(v);
    831        }
    832      }),
    833      this.#compartments.domEventHandlersCompartment.of(
    834        EditorView.domEventHandlers(this.#createEventHandlers())
    835      ),
    836      this.#compartments.lineNumberMarkersCompartment.of([]),
    837      lineContentMarkerExtension,
    838      positionContentMarkerExtension,
    839      this.#compartments.searchHighlightCompartment.of(
    840        this.#searchHighlighterExtension([])
    841      ),
    842      this.#compartments.languageCompartment.of(languageMode),
    843      highlightSelectionMatches(),
    844      // keep last so other extension take precedence
    845      codemirror.minimalSetup,
    846    ];
    847 
    848    if (!this.config.disableSearchAddon && this.config.useSearchAddonPanel) {
    849      this.config.keyMap = this.config.keyMap
    850        ? [...this.config.keyMap, ...searchKeymap]
    851        : [...searchKeymap];
    852      extensions.push(search({ top: true }));
    853    }
    854 
    855    if (this.config.placeholder) {
    856      extensions.push(placeholder(this.config.placeholder));
    857    }
    858 
    859    if (this.config.keyMap) {
    860      extensions.push(Prec.highest(keymap.of(this.config.keyMap)));
    861    }
    862 
    863    if (Services.prefs.prefHasUserValue(CARET_BLINK_TIME)) {
    864      // We need to multiply the preference value by 2 to match Firefox cursor rate
    865      const cursorBlinkRate = Services.prefs.getIntPref(CARET_BLINK_TIME) * 2;
    866      extensions.push(
    867        drawSelection({
    868          cursorBlinkRate,
    869        })
    870      );
    871    }
    872 
    873    const cm = new EditorView({
    874      parent: el,
    875      extensions,
    876    });
    877 
    878    cm.isDocumentLoadComplete = false;
    879    editors.set(this, cm);
    880 
    881    // For now, we only need to pipe the blur event
    882    cm.contentDOM.addEventListener("blur", e => this.emit("blur", e), {
    883      signal: this.#abortController?.signal,
    884    });
    885  }
    886 
    887  /**
    888   * This creates the extension which handles marking of lines within the editor.
    889   *
    890   * @returns {object} The object contains an extension and effects which used to trigger updates to the extension
    891   *          {Object} - lineContentMarkerExtension - The line content marker extension
    892   *          {Object} - lineContentMarkerEffect - The effects to add and remove markers
    893   */
    894  #createlineContentMarkersExtension() {
    895    const {
    896      codemirrorView: { Decoration, WidgetType, EditorView },
    897      codemirrorState: { StateField, StateEffect },
    898    } = this.#CodeMirror6;
    899 
    900    const lineContentMarkers = this.#lineContentMarkers;
    901 
    902    class LineContentWidget extends WidgetType {
    903      constructor(line, value, markerId, createElementNode) {
    904        super();
    905        this.line = line;
    906        this.value = value;
    907        this.markerId = markerId;
    908        this.createElementNode = createElementNode;
    909      }
    910 
    911      toDOM() {
    912        return this.createElementNode(this.line, this.value);
    913      }
    914 
    915      eq(widget) {
    916        return (
    917          widget.line == this.line &&
    918          widget.markerId == this.markerId &&
    919          widget.value == this.value
    920        );
    921      }
    922    }
    923 
    924    /**
    925     * Uses the marker and current decoration list to create a new decoration list
    926     *
    927     * @param {object} marker - The marker to be used to create the new decoration
    928     * @param {Transaction} transaction - The transaction object
    929     * @param {Array} newMarkerDecorations - List of the new marker decorations being built
    930     */
    931    function _buildDecorationsForMarker(
    932      marker,
    933      transaction,
    934      newMarkerDecorations
    935    ) {
    936      const vStartLine = transaction.state.doc.lineAt(
    937        marker._view.viewport.from
    938      );
    939      const vEndLine = transaction.state.doc.lineAt(marker._view.viewport.to);
    940 
    941      let decorationLines;
    942      if (marker.shouldMarkAllLines) {
    943        decorationLines = [];
    944        for (let i = vStartLine.number; i <= vEndLine.number; i++) {
    945          decorationLines.push({ line: i });
    946        }
    947      } else {
    948        decorationLines = marker.lines;
    949      }
    950 
    951      for (const { line, value } of decorationLines) {
    952        // Make sure the position is within the viewport
    953        if (line < vStartLine.number || line > vEndLine.number) {
    954          continue;
    955        }
    956 
    957        const lo = transaction.state.doc.line(line);
    958        if (marker.lineClassName) {
    959          // Markers used:
    960          // 1) blackboxed-line-marker
    961          // 2) multi-highlight-line-marker
    962          // 3) highlight-line-marker
    963          // 4) line-exception-marker
    964          // 5) debug-line-marker
    965          const classDecoration = Decoration.line({
    966            class: marker.lineClassName,
    967          });
    968          classDecoration.markerType = marker.id;
    969          newMarkerDecorations.push(classDecoration.range(lo.from));
    970        } else if (marker.createLineElementNode) {
    971          // Markers used:
    972          // 1) conditional-breakpoint-panel-marker
    973          // 2) inline-preview-marker
    974          const nodeDecoration = Decoration.widget({
    975            widget: new LineContentWidget(
    976              line,
    977              value,
    978              marker.id,
    979              marker.createLineElementNode
    980            ),
    981            // Render the widget after the cursor
    982            side: 1,
    983            block: !!marker.renderAsBlock,
    984          });
    985          nodeDecoration.markerType = marker.id;
    986          newMarkerDecorations.push(nodeDecoration.range(lo.to));
    987        }
    988      }
    989    }
    990 
    991    /**
    992     * This updates the decorations for the marker specified
    993     *
    994     * @param {Array} markerDecorations - The current decorations displayed in the document
    995     * @param {Array} marker - The current marker whose decoration should be update
    996     * @param {Transaction} transaction
    997     * @returns
    998     */
    999    function updateDecorations(markerDecorations, marker, transaction) {
   1000      const newDecorations = [];
   1001      _buildDecorationsForMarker(marker, transaction, newDecorations);
   1002 
   1003      return markerDecorations.update({
   1004        // Filter out old decorations for the specified marker
   1005        filter: (from, to, decoration) => {
   1006          return decoration.markerType !== marker.id;
   1007        },
   1008        add: newDecorations,
   1009        sort: true,
   1010      });
   1011    }
   1012 
   1013    /**
   1014     * This updates all the decorations for all the markers. This
   1015     * used in scenarios when an update to view (e.g vertically scrolling into a new viewport)
   1016     * requires all the marker decoraions.
   1017     *
   1018     * @param {Array} markerDecorations - The current decorations displayed in the document
   1019     * @param {Array} allMarkers - All the cached markers
   1020     * @param {object} transaction
   1021     * @returns
   1022     */
   1023    function updateDecorationsForAllMarkers(
   1024      markerDecorations,
   1025      allMarkers,
   1026      transaction
   1027    ) {
   1028      const allNewDecorations = [];
   1029 
   1030      for (const marker of allMarkers) {
   1031        _buildDecorationsForMarker(marker, transaction, allNewDecorations);
   1032      }
   1033 
   1034      return markerDecorations.update({
   1035        // This filters out all the old decorations
   1036        filter: () => false,
   1037        add: allNewDecorations,
   1038        sort: true,
   1039      });
   1040    }
   1041 
   1042    function removeDecorations(markerDecorations, markerId) {
   1043      return markerDecorations.update({
   1044        filter: (from, to, decoration) => {
   1045          return decoration.markerType !== markerId;
   1046        },
   1047      });
   1048    }
   1049 
   1050    // The effects used to create the transaction when markers are
   1051    // either added and removed.
   1052    const addEffect = StateEffect.define();
   1053    const removeEffect = StateEffect.define();
   1054 
   1055    const lineContentMarkerExtension = StateField.define({
   1056      create() {
   1057        return Decoration.none;
   1058      },
   1059      update(markerDecorations, transaction) {
   1060        // Map the decorations through the transaction changes, this is important
   1061        // as it remaps the decorations from positions in the old document to
   1062        // positions in the new document.
   1063        markerDecorations = markerDecorations.map(transaction.changes);
   1064        for (const effect of transaction.effects) {
   1065          // When a new marker is added
   1066          if (effect.is(addEffect)) {
   1067            markerDecorations = updateDecorations(
   1068              markerDecorations,
   1069              effect.value,
   1070              transaction
   1071            );
   1072          } else if (effect.is(removeEffect)) {
   1073            // when a marker is removed
   1074            markerDecorations = removeDecorations(
   1075              markerDecorations,
   1076              effect.value
   1077            );
   1078          } else {
   1079            const cachedMarkers = lineContentMarkers.values();
   1080            // For updates that are not related to this marker decoration,
   1081            // we want to update the decorations when the editor is scrolled
   1082            // and a new viewport is loaded.
   1083            markerDecorations = updateDecorationsForAllMarkers(
   1084              markerDecorations,
   1085              cachedMarkers,
   1086              transaction
   1087            );
   1088          }
   1089        }
   1090        return markerDecorations;
   1091      },
   1092      provide: field => EditorView.decorations.from(field),
   1093    });
   1094 
   1095    return {
   1096      lineContentMarkerExtension,
   1097      lineContentMarkerEffect: { addEffect, removeEffect },
   1098    };
   1099  }
   1100 
   1101  #createEventHandlers() {
   1102    const eventHandlers = {};
   1103    for (const eventName in this.#editorDOMEventHandlers) {
   1104      const handlers = this.#editorDOMEventHandlers[eventName];
   1105      eventHandlers[eventName] = (event, editor) => {
   1106        if (!event.target) {
   1107          return;
   1108        }
   1109        for (const handler of handlers) {
   1110          // Wait a cycle so the codemirror updates to the current cursor position,
   1111          // information, TODO: Currently noticed this issue with CM6, not ideal but should
   1112          // investigate further Bug 1890895.
   1113          event.target.ownerGlobal.setTimeout(() => {
   1114            const view = editor.viewState;
   1115            const cursorPos = lezerUtils.positionToLocation(
   1116              view.state.doc,
   1117              view.state.selection.main.head
   1118            );
   1119            handler(event, view, cursorPos.line, cursorPos.column);
   1120          }, 0);
   1121        }
   1122      };
   1123    }
   1124    return eventHandlers;
   1125  }
   1126 
   1127  /**
   1128   * Adds the DOM event handlers for the editor.
   1129   *
   1130   * @param {object} domEventHandlers - A dictionary of handlers for the DOM events
   1131   *                                    the handlers are getting called with the following arguments
   1132   *                                     - {Object} `event`: The DOM event
   1133   *                                     - {Object} `view`: The codemirror view
   1134   *                                     - {Number} cursorLine`: The line where the cursor is currently position
   1135   *                                     - {Number} `cursorColumn`: The column where the cursor is currently position
   1136   *                                     - {Number} `eventLine`: The line where the event was fired.
   1137   *                                                             This might be different from the cursor line for mouse events.
   1138   *                                     - {Number} `eventColumn`: The column where the event was fired.
   1139   *                                                                This might be different from the cursor column for mouse events.
   1140   */
   1141  addEditorDOMEventListeners(domEventHandlers) {
   1142    const cm = editors.get(this);
   1143    const {
   1144      codemirrorView: { EditorView },
   1145    } = this.#CodeMirror6;
   1146 
   1147    // Update the cache of dom event handlers
   1148    for (const eventName in domEventHandlers) {
   1149      if (!this.#editorDOMEventHandlers[eventName]) {
   1150        this.#editorDOMEventHandlers[eventName] = [];
   1151      }
   1152      this.#editorDOMEventHandlers[eventName].push(domEventHandlers[eventName]);
   1153    }
   1154 
   1155    cm.dispatch({
   1156      effects: this.#compartments.domEventHandlersCompartment.reconfigure(
   1157        EditorView.domEventHandlers(this.#createEventHandlers())
   1158      ),
   1159    });
   1160  }
   1161 
   1162  #cacheScrollSnapshot = () => {
   1163    const cm = editors.get(this);
   1164    if (!this.#currentDocumentId) {
   1165      return;
   1166    }
   1167    this.#scrollSnapshots.set(this.#currentDocumentId, cm.scrollSnapshot());
   1168    this.emitForTests("cm-editor-scrolled");
   1169  };
   1170 
   1171  /**
   1172   * Remove specified DOM event handlers for the editor.
   1173   *
   1174   * @param {object} domEventHandlers - A dictionary of handlers for the DOM events
   1175   */
   1176  removeEditorDOMEventListeners(domEventHandlers) {
   1177    const cm = editors.get(this);
   1178    const {
   1179      codemirrorView: { EditorView },
   1180    } = this.#CodeMirror6;
   1181 
   1182    for (const eventName in domEventHandlers) {
   1183      const domEventHandler = domEventHandlers[eventName];
   1184      const cachedEventHandlers = this.#editorDOMEventHandlers[eventName];
   1185      if (!domEventHandler || !cachedEventHandlers) {
   1186        continue;
   1187      }
   1188      const index = cachedEventHandlers.findIndex(
   1189        handler => handler == domEventHandler
   1190      );
   1191      this.#editorDOMEventHandlers[eventName].splice(index, 1);
   1192    }
   1193 
   1194    cm.dispatch({
   1195      effects: this.#compartments.domEventHandlersCompartment.reconfigure(
   1196        EditorView.domEventHandlers(this.#createEventHandlers())
   1197      ),
   1198    });
   1199  }
   1200 
   1201  /**
   1202   * Clear the DOM event handlers for the editor.
   1203   */
   1204  #clearEditorDOMEventListeners() {
   1205    const cm = editors.get(this);
   1206    const {
   1207      codemirrorView: { EditorView },
   1208    } = this.#CodeMirror6;
   1209 
   1210    this.#editorDOMEventHandlers = {};
   1211    this.#gutterDOMEventHandlers = {};
   1212    cm.dispatch({
   1213      effects: this.#compartments.domEventHandlersCompartment.reconfigure(
   1214        EditorView.domEventHandlers({})
   1215      ),
   1216    });
   1217  }
   1218 
   1219  /**
   1220   * This adds a marker used to add classes to editor line based on a condition.
   1221   *
   1222   *   @property {object}             marker
   1223   *                                  The rule rendering a marker or class.
   1224   *   @property {object}             marker.id
   1225   *                                  The unique identifier for this marker
   1226   *   @property {string}             marker.lineClassName
   1227   *                                  The css class to apply to the line
   1228   *   @property {Array<object>}      marker.lines
   1229   *                                  The lines to add markers to. Each line object has a `line` and `value` property.
   1230   *   @property {boolean}           marker.renderAsBlock
   1231   *                                  The specifies that the widget should be rendered as a block element. defaults to `false`. This is optional.
   1232   *   @property {boolean}           marker.shouldMarkAllLines
   1233   *                                  Set to true to apply the marker to all the lines. In such case, `positions` is ignored. This is optional.
   1234   *   @property {Function}           marker.createLineElementNode
   1235   *                                  This should return the DOM element which is used for the marker. The line number is passed as a parameter.
   1236   *                                  This is optional.
   1237   */
   1238  setLineContentMarker(marker) {
   1239    const cm = editors.get(this);
   1240    // We store the marker an the view state, this is gives access to view data
   1241    // when defining updates to the StateField.
   1242    marker._view = cm;
   1243    this.#lineContentMarkers.set(marker.id, marker);
   1244    cm.dispatch({
   1245      effects: this.#effects.lineContentMarkerEffect.addEffect.of(marker),
   1246    });
   1247  }
   1248 
   1249  /**
   1250   * This removes the marker which has the specified className
   1251   *
   1252   * @param {string} markerId - The unique identifier for this marker
   1253   */
   1254  removeLineContentMarker(markerId) {
   1255    const cm = editors.get(this);
   1256    this.#lineContentMarkers.delete(markerId);
   1257    cm.dispatch({
   1258      effects: this.#effects.lineContentMarkerEffect.removeEffect.of(markerId),
   1259    });
   1260  }
   1261 
   1262  /**
   1263   * This creates the extension used to manage the rendering of markers
   1264   * at specific positions with the editor. e.g used for column breakpoints
   1265   *
   1266   * @returns {object} The object contains an extension and effects which used to trigger updates to the extension
   1267   *          {Object} - positionContentMarkerExtension - The position content marker extension
   1268   *          {Object} - positionContentMarkerEffect - The effects to add and remove markers
   1269   */
   1270  #createPositionContentMarkersExtension() {
   1271    const {
   1272      codemirrorView: { Decoration, EditorView, WidgetType },
   1273      codemirrorState: { StateField, StateEffect },
   1274      codemirrorLanguage: { syntaxTree },
   1275    } = this.#CodeMirror6;
   1276 
   1277    const cachedPositionContentMarkers = this.#posContentMarkers;
   1278 
   1279    class NodeWidget extends WidgetType {
   1280      constructor({
   1281        line,
   1282        column,
   1283        isFirstNonSpaceColumn,
   1284        positionData,
   1285        markerId,
   1286        createElementNode,
   1287        customEq,
   1288      }) {
   1289        super();
   1290        this.line = line;
   1291        this.column = column;
   1292        this.isFirstNonSpaceColumn = isFirstNonSpaceColumn;
   1293        this.positionData = positionData;
   1294        this.markerId = markerId;
   1295        this.customEq = customEq;
   1296        this.toDOM = () =>
   1297          createElementNode(line, column, isFirstNonSpaceColumn, positionData);
   1298      }
   1299 
   1300      eq(widget) {
   1301        let eq =
   1302          this.line == widget.line &&
   1303          this.column == widget.column &&
   1304          this.markerId == widget.markerId;
   1305        if (this.positionData && this.customEq) {
   1306          eq = eq && this.customEq(this.positionData, widget.positionData);
   1307        }
   1308        return eq;
   1309      }
   1310    }
   1311 
   1312    function getIndentation(lineText) {
   1313      if (!lineText) {
   1314        return 0;
   1315      }
   1316 
   1317      const lineMatch = lineText.match(/^\s*/);
   1318      if (!lineMatch) {
   1319        return 0;
   1320      }
   1321      return lineMatch[0].length;
   1322    }
   1323 
   1324    function _buildDecorationsForPositionMarkers(
   1325      marker,
   1326      transaction,
   1327      newMarkerDecorations
   1328    ) {
   1329      const viewport = marker._view.viewport;
   1330      const vStartLine = transaction.state.doc.lineAt(viewport.from);
   1331      const vEndLine = transaction.state.doc.lineAt(viewport.to);
   1332 
   1333      for (const position of marker.positions) {
   1334        // If codemirror positions are provided (e.g from search cursor)
   1335        // compare that directly.
   1336        if (position.from && position.to) {
   1337          if (position.from >= viewport.from && position.to <= viewport.to) {
   1338            if (marker.positionClassName) {
   1339              // Markers used:
   1340              // 1. active-selection-marker
   1341              const classDecoration = Decoration.mark({
   1342                class: marker.positionClassName,
   1343              });
   1344              classDecoration.markerType = marker.id;
   1345              newMarkerDecorations.push(
   1346                classDecoration.range(position.from, position.to)
   1347              );
   1348            }
   1349          }
   1350          continue;
   1351        }
   1352        // If line and column are provided
   1353        if (
   1354          position.line >= vStartLine.number &&
   1355          position.line <= vEndLine.number
   1356        ) {
   1357          const line = transaction.state.doc.line(position.line);
   1358          // Make sure to track any indentation at the beginning of the line
   1359          const column = Math.max(position.column, getIndentation(line.text));
   1360          const pos = line.from + column;
   1361 
   1362          if (marker.createPositionElementNode) {
   1363            // Markers used:
   1364            // 1. column-breakpoint-marker
   1365            const isFirstNonSpaceColumn = ONLY_SPACES_REGEXP.test(
   1366              line.text.substr(0, column)
   1367            );
   1368            const nodeDecoration = Decoration.widget({
   1369              widget: new NodeWidget({
   1370                line: position.line,
   1371                column: position.column,
   1372                isFirstNonSpaceColumn,
   1373                positionData: position.positionData,
   1374                markerId: marker.id,
   1375                createElementNode: marker.createPositionElementNode,
   1376                customEq: marker.customEq,
   1377              }),
   1378              // Make sure the widget is rendered after the cursor
   1379              // see https://codemirror.net/docs/ref/#view.Decoration^widget^spec.side for details.
   1380              side: 1,
   1381            });
   1382            nodeDecoration.markerType = marker.id;
   1383            newMarkerDecorations.push(nodeDecoration.range(pos, pos));
   1384          }
   1385          if (marker.positionClassName) {
   1386            // Markers used:
   1387            // 1. exception-position-marker
   1388            // 2. debug-position-marker
   1389            const tokenAtPos = syntaxTree(transaction.state).resolve(pos, 1);
   1390            // While trying to update the markers, during content changes, the syntax tree is not
   1391            // guaranteed to be complete, so there is the possibility of getting wrong `from` and `to` values for the token.
   1392            // To make sure we are handling a valid token, let's check that the `from` value (which is the start position of the retrieved token)
   1393            // matches the position we want.
   1394            if (tokenAtPos.from !== pos) {
   1395              continue;
   1396            }
   1397            const tokenString = line.text.slice(
   1398              position.column,
   1399              tokenAtPos.to - line.from
   1400            );
   1401            // Ignore any empty strings and opening braces
   1402            if (
   1403              tokenString === "" ||
   1404              tokenString === "{" ||
   1405              tokenString === "["
   1406            ) {
   1407              continue;
   1408            }
   1409            const classDecoration = Decoration.mark({
   1410              class: marker.positionClassName,
   1411            });
   1412            classDecoration.markerType = marker.id;
   1413            newMarkerDecorations.push(
   1414              classDecoration.range(pos, tokenAtPos.to)
   1415            );
   1416          }
   1417        }
   1418      }
   1419    }
   1420 
   1421    /**
   1422     * This updates the decorations for the marker specified
   1423     *
   1424     * @param {Array} markerDecorations - The current decorations displayed in the document
   1425     * @param {Array} marker - The current marker whose decoration should be update
   1426     * @param {Transaction} transaction
   1427     * @returns
   1428     */
   1429    function updateDecorations(markerDecorations, marker, transaction) {
   1430      const newDecorations = [];
   1431 
   1432      _buildDecorationsForPositionMarkers(marker, transaction, newDecorations);
   1433      return markerDecorations.update({
   1434        filter: (from, to, decoration) => {
   1435          return decoration.markerType !== marker.id;
   1436        },
   1437        add: newDecorations,
   1438        sort: true,
   1439      });
   1440    }
   1441 
   1442    /**
   1443     * This updates all the decorations for all the markers. This
   1444     * used in scenarios when an update to view (e.g vertically scrolling into a new viewport)
   1445     * requires all the marker decoraions.
   1446     *
   1447     * @param {Array} markerDecorations - The current decorations displayed in the document
   1448     * @param {Array} markers - All the cached markers
   1449     * @param {object} transaction
   1450     * @returns
   1451     */
   1452    function updateDecorationsForAllMarkers(
   1453      markerDecorations,
   1454      markers,
   1455      transaction
   1456    ) {
   1457      const allNewDecorations = [];
   1458 
   1459      // Sort the markers iterator thanks to `displayLast` boolean.
   1460      // This is typically used by the paused location marker to be shown after the column breakpoints.
   1461      markers = Array.from(markers).sort((a, b) => {
   1462        if (a.displayLast) {
   1463          return 1;
   1464        }
   1465        if (b.displayLast) {
   1466          return -1;
   1467        }
   1468        return 0;
   1469      });
   1470 
   1471      for (const marker of markers) {
   1472        _buildDecorationsForPositionMarkers(
   1473          marker,
   1474          transaction,
   1475          allNewDecorations
   1476        );
   1477      }
   1478      return markerDecorations.update({
   1479        filter: () => false,
   1480        add: allNewDecorations,
   1481        sort: true,
   1482      });
   1483    }
   1484 
   1485    function removeDecorations(markerDecorations, markerId) {
   1486      return markerDecorations.update({
   1487        filter: (from, to, decoration) => {
   1488          return decoration.markerType !== markerId;
   1489        },
   1490      });
   1491    }
   1492 
   1493    const addEffect = StateEffect.define();
   1494    const removeEffect = StateEffect.define();
   1495 
   1496    const positionContentMarkerExtension = StateField.define({
   1497      create() {
   1498        return Decoration.none;
   1499      },
   1500      update(markerDecorations, transaction) {
   1501        // Map the decorations through the transaction changes, this is important
   1502        // as it remaps the decorations from positions in the old document to
   1503        // positions in the new document.
   1504        markerDecorations = markerDecorations.map(transaction.changes);
   1505        for (const effect of transaction.effects) {
   1506          if (effect.is(addEffect)) {
   1507            // When a new marker is added
   1508            markerDecorations = updateDecorations(
   1509              markerDecorations,
   1510              effect.value,
   1511              transaction
   1512            );
   1513          } else if (effect.is(removeEffect)) {
   1514            // When a marker is removed
   1515            markerDecorations = removeDecorations(
   1516              markerDecorations,
   1517              effect.value
   1518            );
   1519          } else {
   1520            // For updates that are not related to this marker decoration,
   1521            // we want to update the decorations when the editor is scrolled
   1522            // and a new viewport is loaded.
   1523            markerDecorations = updateDecorationsForAllMarkers(
   1524              markerDecorations,
   1525              cachedPositionContentMarkers.values(),
   1526              transaction
   1527            );
   1528          }
   1529        }
   1530        return markerDecorations;
   1531      },
   1532      provide: field => EditorView.decorations.from(field),
   1533    });
   1534 
   1535    return {
   1536      positionContentMarkerExtension,
   1537      positionContentMarkerEffect: { addEffect, removeEffect },
   1538    };
   1539  }
   1540 
   1541  /**
   1542   * This adds a marker used to decorate token / content at a specific position .
   1543   *
   1544   * @param {object} marker
   1545   * @param {string} marker.id
   1546   * @param {Array<object>} marker.positions - This includes the line / column and any optional positionData which defines each position.
   1547   * @param {Function} marker.createPositionElementNode - This describes how to render the marker.
   1548   *                                                      The position data (i.e line, column and positionData) are passed as arguments.
   1549   * @param {Function} marker.customEq - A custom function to determine the equality of the marker. This allows the user define special conditions
   1550   *                                     for when position details have changed and an update of the marker should happen.
   1551   *                                     The positionData defined for the current and the previous instance of the marker are passed as arguments.
   1552   */
   1553  setPositionContentMarker(marker) {
   1554    const cm = editors.get(this);
   1555 
   1556    // We store the marker an the view state, this is gives access to viewport data
   1557    // when defining updates to the StateField.
   1558    marker._view = cm;
   1559    this.#posContentMarkers.set(marker.id, marker);
   1560    cm.dispatch({
   1561      effects: this.#effects.positionContentMarkerEffect.addEffect.of(marker),
   1562    });
   1563  }
   1564 
   1565  /**
   1566   * This removes the marker which has the specified id
   1567   *
   1568   * @param {string} markerId - The unique identifier for this marker
   1569   */
   1570  removePositionContentMarker(markerId) {
   1571    const cm = editors.get(this);
   1572    this.#posContentMarkers.delete(markerId);
   1573    cm.dispatch({
   1574      effects:
   1575        this.#effects.positionContentMarkerEffect.removeEffect.of(markerId),
   1576    });
   1577  }
   1578 
   1579  /**
   1580   * Set event listeners for the line gutter
   1581   *
   1582   * @param {object} domEventHandlers
   1583   *
   1584   * example usage:
   1585   *  const domEventHandlers = { click(event) { console.log(event);} }
   1586   */
   1587  setGutterEventListeners(domEventHandlers) {
   1588    const cm = editors.get(this);
   1589    const {
   1590      codemirrorView: { lineNumbers },
   1591      codemirrorLanguage: { foldGutter },
   1592    } = this.#CodeMirror6;
   1593 
   1594    for (const eventName in domEventHandlers) {
   1595      const handler = domEventHandlers[eventName];
   1596      this.#gutterDOMEventHandlers[eventName] = (view, line, event) => {
   1597        line = view.state.doc.lineAt(line.from);
   1598        handler(event, view, line.number);
   1599      };
   1600    }
   1601 
   1602    cm.dispatch({
   1603      effects: [
   1604        this.#compartments.lineNumberCompartment.reconfigure(
   1605          lineNumbers({ domEventHandlers: this.#gutterDOMEventHandlers })
   1606        ),
   1607        this.#compartments.foldGutterCompartment.reconfigure(
   1608          foldGutter({
   1609            class: "cm6-dt-foldgutter",
   1610            markerDOM: open => {
   1611              if (!this.#ownerDoc) {
   1612                return null;
   1613              }
   1614              const button = this.#ownerDoc.createElement("button");
   1615              button.classList.add("cm6-dt-foldgutter__toggle-button");
   1616              button.setAttribute("aria-expanded", open);
   1617              return button;
   1618            },
   1619            domEventHandlers: this.#gutterDOMEventHandlers,
   1620          })
   1621        ),
   1622      ],
   1623    });
   1624  }
   1625 
   1626  /**
   1627   * This supports adding/removing of line classes or markers on the
   1628   * line number gutter based on the defined conditions. This only supports codemirror 6.
   1629   *
   1630   *   @param {Array<Marker>} markers         - The list of marker objects which defines the rules
   1631   *                                            for rendering each marker.
   1632   *   @property {object}     marker - The rule rendering a marker or class. This is required.
   1633   *   @property {string}     marker.id - The unique identifier for this marker.
   1634   *   @property {string}     marker.lineClassName - The css class to add to the line. This is required.
   1635   *   @property {function}   marker.condition - The condition that decides if the marker/class gets added or removed.
   1636   *                                              This should return `false` for lines where the marker should not be added and the
   1637   *                                              result of the condition for any other line.
   1638   *   @property {Function=}  marker.createLineElementNode - This gets the line and the result of the condition as arguments and should return the DOM element which
   1639   *                                            is used for the marker. This is optional.
   1640   */
   1641  setLineGutterMarkers(markers) {
   1642    const cm = editors.get(this);
   1643 
   1644    if (markers) {
   1645      // Cache the markers for use later. See next comment
   1646      for (const marker of markers) {
   1647        if (!marker.id) {
   1648          throw new Error("Marker has no unique identifier");
   1649        }
   1650        this.#lineGutterMarkers.set(marker.id, marker);
   1651      }
   1652    }
   1653    // When no markers are passed, the cached markers are used to update the line gutters.
   1654    // This is useful for re-rendering the line gutters when the viewport changes
   1655    // (note: the visible ranges will be different) in this case, mainly when the editor is scrolled.
   1656    else if (!this.#lineGutterMarkers.size) {
   1657      return;
   1658    }
   1659    markers = Array.from(this.#lineGutterMarkers.values());
   1660 
   1661    const {
   1662      codemirrorView: { lineNumberMarkers, GutterMarker },
   1663      codemirrorState: { RangeSetBuilder },
   1664    } = this.#CodeMirror6;
   1665 
   1666    // This creates a new GutterMarker https://codemirror.net/docs/ref/#view.GutterMarker
   1667    // to represents how each line gutter is rendered in the view.
   1668    // This is set as the value for the Range https://codemirror.net/docs/ref/#state.Range
   1669    // which represents the line.
   1670    class LineGutterMarker extends GutterMarker {
   1671      constructor(className, lineNumber, createElementNode, conditionResult) {
   1672        super();
   1673        this.elementClass = className || null;
   1674        this.lineNumber = lineNumber;
   1675        this.createElementNode = createElementNode;
   1676        this.conditionResult = conditionResult;
   1677 
   1678        this.toDOM = createElementNode
   1679          ? () => createElementNode(lineNumber, conditionResult)
   1680          : null;
   1681      }
   1682 
   1683      eq(marker) {
   1684        return (
   1685          marker.lineNumber == this.lineNumber &&
   1686          marker.conditionResult == this.conditionResult
   1687        );
   1688      }
   1689    }
   1690 
   1691    // Loop through the visible ranges https://codemirror.net/docs/ref/#view.EditorView.visibleRanges
   1692    // (representing the lines in the current viewport) and generate a new rangeset for updating the line gutter
   1693    // based on the conditions defined in the markers(for each line) provided.
   1694    const builder = new RangeSetBuilder();
   1695    const { from, to } = cm.viewport;
   1696    let pos = from;
   1697    while (pos <= to) {
   1698      const line = cm.state.doc.lineAt(pos);
   1699      for (const {
   1700        lineClassName,
   1701        condition,
   1702        createLineElementNode,
   1703      } of markers) {
   1704        if (typeof condition !== "function") {
   1705          throw new Error("The `condition` is not a valid function");
   1706        }
   1707        const conditionResult = condition(line.number);
   1708        if (conditionResult !== false) {
   1709          builder.add(
   1710            line.from,
   1711            line.to,
   1712            new LineGutterMarker(
   1713              lineClassName,
   1714              line.number,
   1715              createLineElementNode,
   1716              conditionResult
   1717            )
   1718          );
   1719        }
   1720      }
   1721      pos = line.to + 1;
   1722    }
   1723 
   1724    // To update the state with the newly generated marker range set, a dispatch is called on the view
   1725    // with an transaction effect created by the lineNumberMarkersCompartment, which is used to update the
   1726    // lineNumberMarkers extension configuration.
   1727    cm.dispatch({
   1728      effects: this.#compartments.lineNumberMarkersCompartment.reconfigure(
   1729        lineNumberMarkers.of(builder.finish())
   1730      ),
   1731    });
   1732  }
   1733 
   1734  /**
   1735   * This creates the extension used to manage the rendering of markers for
   1736   * results for any search pattern
   1737   *
   1738   * @param {RegExp}      pattern - The search pattern
   1739   * @param {string}      className - The class used to decorate each result
   1740   * @returns {Array<ViewPlugin>} An extension which is an array containing the view
   1741   *                              which manages the rendering of the line content markers.
   1742   */
   1743  #searchHighlighterExtension({
   1744    /* This defaults to matching nothing */ pattern = /.^/g,
   1745    className = "",
   1746  }) {
   1747    const cm = editors.get(this);
   1748    if (!cm) {
   1749      return [];
   1750    }
   1751    const {
   1752      codemirrorView: { Decoration, ViewPlugin, EditorView, MatchDecorator },
   1753      codemirrorSearch: { RegExpCursor },
   1754    } = this.#CodeMirror6;
   1755 
   1756    this.searchState.query = pattern;
   1757    const searchCursor = new RegExpCursor(cm.state.doc, pattern, {
   1758      ignoreCase: pattern.ignoreCase,
   1759    });
   1760    this.searchState.cursors = Array.from(searchCursor);
   1761    this.searchState.currentCursorIndex = -1;
   1762 
   1763    const patternMatcher = new MatchDecorator({
   1764      regexp: pattern,
   1765      decorate: (add, from, to) => {
   1766        add(from, to, Decoration.mark({ class: className }));
   1767      },
   1768    });
   1769 
   1770    const searchHighlightView = ViewPlugin.fromClass(
   1771      class {
   1772        decorations;
   1773        constructor(view) {
   1774          this.decorations = patternMatcher.createDeco(view);
   1775        }
   1776        update(viewUpdate) {
   1777          this.decorations = patternMatcher.updateDeco(
   1778            viewUpdate,
   1779            this.decorations
   1780          );
   1781        }
   1782      },
   1783      {
   1784        decorations: instance => instance.decorations,
   1785        provide: plugin =>
   1786          EditorView.atomicRanges.of(view => {
   1787            return view.plugin(plugin)?.decorations || Decoration.none;
   1788          }),
   1789      }
   1790    );
   1791 
   1792    return [searchHighlightView];
   1793  }
   1794 
   1795  /**
   1796   * This should add the class to the results of a search pattern specified
   1797   *
   1798   * @param {RegExp} pattern - The search pattern
   1799   * @param {string} className - The class used to decorate each result
   1800   */
   1801  highlightSearchMatches(pattern, className) {
   1802    const cm = editors.get(this);
   1803    cm.dispatch({
   1804      effects: this.#compartments.searchHighlightCompartment.reconfigure(
   1805        this.#searchHighlighterExtension({ pattern, className })
   1806      ),
   1807    });
   1808  }
   1809 
   1810  /**
   1811   * This clear any decoration on all the search results
   1812   */
   1813  clearSearchMatches() {
   1814    this.highlightSearchMatches(undefined, "");
   1815  }
   1816 
   1817  /**
   1818   * Retrieves the cursor for the next selection to be highlighted
   1819   *
   1820   * @param {boolean} reverse - Determines the direction of the cursor movement
   1821   * @returns {RegExpSearchCursor}
   1822   */
   1823  getNextSearchCursor(reverse) {
   1824    if (reverse) {
   1825      if (this.searchState.currentCursorIndex == 0) {
   1826        this.searchState.currentCursorIndex =
   1827          this.searchState.cursors.length - 1;
   1828      } else {
   1829        this.searchState.currentCursorIndex--;
   1830      }
   1831    } else if (
   1832      this.searchState.currentCursorIndex ==
   1833      this.searchState.cursors.length - 1
   1834    ) {
   1835      this.searchState.currentCursorIndex = 0;
   1836    } else {
   1837      this.searchState.currentCursorIndex++;
   1838    }
   1839    return this.searchState.cursors[this.searchState.currentCursorIndex];
   1840  }
   1841 
   1842  /**
   1843   * Get the start and end locations of the current viewport
   1844   *
   1845   * @param {number} offsetHorizontalCharacters - Offset of characters offscreen
   1846   * @param {number} offsetVerticalLines - Offset of lines offscreen
   1847   * @returns {object}  - The location information for the current viewport
   1848   */
   1849  getLocationsInViewport(
   1850    offsetHorizontalCharacters = 0,
   1851    offsetVerticalLines = 0
   1852  ) {
   1853    if (this.isDestroyed()) {
   1854      return null;
   1855    }
   1856    const cm = editors.get(this);
   1857    let startLine, endLine, scrollLeft, charWidth, rightPosition;
   1858    if (this.config.cm6) {
   1859      // Report no viewport until we show an actual source (and not a loading/error message)
   1860      if (!this.#currentDocumentId) {
   1861        return null;
   1862      }
   1863      const { from, to } = cm.viewport;
   1864      startLine = cm.state.doc.lineAt(from).number - offsetVerticalLines;
   1865      endLine = cm.state.doc.lineAt(to).number + offsetVerticalLines;
   1866      scrollLeft = cm.scrollDOM.scrollLeft;
   1867      charWidth = cm.defaultCharacterWidth;
   1868      rightPosition = scrollLeft + cm.dom.getBoundingClientRect().width;
   1869    } else {
   1870      if (!cm) {
   1871        return null;
   1872      }
   1873 
   1874      const scrollArea = cm.getScrollInfo();
   1875      const rect = cm.getWrapperElement().getBoundingClientRect();
   1876      startLine = cm.lineAtHeight(rect.top, "window") - offsetVerticalLines;
   1877      endLine = cm.lineAtHeight(rect.bottom, "window") + offsetVerticalLines;
   1878      scrollLeft = cm.doc.scrollLeft;
   1879      charWidth = cm.defaultCharWidth();
   1880      rightPosition = scrollLeft + (scrollArea.clientWidth - 30);
   1881    }
   1882 
   1883    return {
   1884      start: {
   1885        line: startLine,
   1886        column:
   1887          scrollLeft > 0
   1888            ? Math.floor(scrollLeft / charWidth) - offsetHorizontalCharacters
   1889            : 0,
   1890      },
   1891      end: {
   1892        line: endLine,
   1893        column:
   1894          Math.floor(rightPosition / charWidth) + offsetHorizontalCharacters,
   1895      },
   1896    };
   1897  }
   1898 
   1899  /**
   1900   * Gets the position information for the current selection
   1901   *
   1902   * @returns {object} cursor      - The location information for the  current selection
   1903   *                   cursor.from - An object with the starting line / column of the selection
   1904   *                   cursor.to   - An object with the end line / column of the selection
   1905   */
   1906  getSelectionCursor() {
   1907    const cm = editors.get(this);
   1908    if (this.config.cm6) {
   1909      const selection = cm.state.selection.ranges[0];
   1910      const lineFrom = cm.state.doc.lineAt(selection.from);
   1911      const lineTo = cm.state.doc.lineAt(selection.to);
   1912      return {
   1913        from: {
   1914          line: lineFrom.number,
   1915          ch: selection.from - lineFrom.from,
   1916        },
   1917        to: {
   1918          line: lineTo.number,
   1919          ch: selection.to - lineTo.from,
   1920        },
   1921      };
   1922    }
   1923    return {
   1924      from: cm.getCursor("from"),
   1925      to: cm.getCursor("to"),
   1926    };
   1927  }
   1928 
   1929  /**
   1930   * Gets the text content for the current selection
   1931   *
   1932   * @returns {string}
   1933   */
   1934  getSelectedText() {
   1935    const cm = editors.get(this);
   1936    if (this.config.cm6) {
   1937      const selection = cm.state.selection.ranges[0];
   1938      return cm.state.doc.sliceString(selection.from, selection.to);
   1939    }
   1940    return cm.getSelection().trim();
   1941  }
   1942 
   1943  /**
   1944   * Given screen coordinates this should return the line and column
   1945   * related. This used currently to determine the line and columns
   1946   * for the tokens that are hovered over.
   1947   *
   1948   * @param {number} left - Horizontal position from the left
   1949   * @param {number} top - Vertical position from the top
   1950   * @returns {object} position - The line and column related to the screen coordinates.
   1951   */
   1952  getPositionAtScreenCoords(left, top) {
   1953    const cm = editors.get(this);
   1954    if (this.config.cm6) {
   1955      const position = cm.posAtCoords(
   1956        { x: left, y: top },
   1957        // "precise", i.e. if a specific position cannot be determined, an estimated one will be used
   1958        false
   1959      );
   1960      const line = cm.state.doc.lineAt(position);
   1961      return {
   1962        line: line.number,
   1963        column: position - line.from,
   1964      };
   1965    }
   1966    const { line, ch } = cm.coordsChar(
   1967      { left, top },
   1968      // Use the "window" context where the coordinates are relative to the top-left corner
   1969      // of the currently visible (scrolled) window.
   1970      // This enables codemirror also correctly handle wrappped lines in the editor.
   1971      "window"
   1972    );
   1973    return {
   1974      line: line + 1,
   1975      column: ch,
   1976    };
   1977  }
   1978 
   1979  /**
   1980   * Check that text is selected
   1981   *
   1982   * @returns {boolean}
   1983   */
   1984  isTextSelected() {
   1985    const cm = editors.get(this);
   1986    if (this.config.cm6) {
   1987      const selection = cm.state.selection.ranges[0];
   1988      return selection.from !== selection.to;
   1989    }
   1990    return cm.somethingSelected();
   1991  }
   1992 
   1993  /**
   1994   * Returns a boolean indicating whether the editor is ready to
   1995   * use. Use appendTo(el).then(() => {}) for most cases
   1996   */
   1997  isAppended() {
   1998    return editors.has(this);
   1999  }
   2000 
   2001  /**
   2002   * Returns the currently active highlighting mode.
   2003   * See Editor.modes for the list of all suppoert modes.
   2004   */
   2005  getMode() {
   2006    return this.getOption("mode");
   2007  }
   2008 
   2009  /**
   2010   * Loads a script into editor's containing window.
   2011   */
   2012  loadScript(url) {
   2013    if (!this.container) {
   2014      throw new Error("Can't load a script until the editor is loaded.");
   2015    }
   2016    const win = this.container.contentWindow.wrappedJSObject;
   2017    Services.scriptloader.loadSubScript(url, win);
   2018  }
   2019 
   2020  /**
   2021   * Creates a CodeMirror Document
   2022   *
   2023   * @param {string} text: Initial text of the document
   2024   * @param {object | string} mode: Mode of the document. See https://codemirror.net/5/doc/manual.html#option_mode
   2025   * @returns CodeMirror.Doc
   2026   */
   2027  createDocument(text = "", mode) {
   2028    return new this.Doc(text, mode);
   2029  }
   2030 
   2031  /**
   2032   * Replaces the current document with a new source document
   2033   */
   2034  replaceDocument(doc) {
   2035    const cm = editors.get(this);
   2036    cm.swapDoc(doc);
   2037  }
   2038 
   2039  /**
   2040   * Changes the currently used syntax highlighting mode.
   2041   *
   2042   * @param {object} mode - Any of the modes from Editor.modes
   2043   * @returns
   2044   */
   2045  setMode(mode) {
   2046    if (this.config.cm6) {
   2047      const cm = editors.get(this);
   2048      // Fallback to using js syntax highlighting if there is none found
   2049      const languageMode = this.#languageModes.has(mode)
   2050        ? this.#languageModes.get(mode)
   2051        : this.#languageModes.get(Editor.modes.javascript);
   2052 
   2053      return cm.dispatch({
   2054        effects: this.#compartments.languageCompartment.reconfigure([
   2055          languageMode,
   2056        ]),
   2057      });
   2058    }
   2059    this.setOption("mode", mode);
   2060 
   2061    // If autocomplete was set up and the mode is changing, then
   2062    // turn it off and back on again so the proper mode can be used.
   2063    if (this.config.autocomplete) {
   2064      this.setOption("autocomplete", false);
   2065      this.setOption("autocomplete", true);
   2066    }
   2067    return null;
   2068  }
   2069 
   2070  /**
   2071   * The source editor can expose several commands linked from system and context menus.
   2072   * Kept for backward compatibility with styleeditor.
   2073   */
   2074  insertCommandsController() {
   2075    const {
   2076      insertCommandsController,
   2077    } = require("resource://devtools/client/shared/sourceeditor/editor-commands-controller.js");
   2078    insertCommandsController(this);
   2079  }
   2080 
   2081  /**
   2082   * Returns text from the text area. If line argument is provided
   2083   * the method returns only that line.
   2084   */
   2085  getText(line) {
   2086    const cm = editors.get(this);
   2087 
   2088    if (line == null) {
   2089      return this.config.cm6 ? cm.state.doc.toString() : cm.getValue();
   2090    }
   2091 
   2092    const info = this.lineInfo(line);
   2093    return info ? info.text : "";
   2094  }
   2095 
   2096  getDoc() {
   2097    if (!this.config) {
   2098      return null;
   2099    }
   2100    const cm = editors.get(this);
   2101    if (this.config.cm6) {
   2102      if (!this.#currentDocument) {
   2103        // A key for caching the WASM content in the WeakMap
   2104        this.#currentDocument = { id: this.#currentDocumentId };
   2105      }
   2106      return this.#currentDocument;
   2107    }
   2108    return cm.getDoc();
   2109  }
   2110 
   2111  get isWasm() {
   2112    return wasm.isWasm(this.getDoc());
   2113  }
   2114 
   2115  getWasmLineNumberFormatter() {
   2116    return wasm.getWasmLineNumberFormatter(this.getDoc());
   2117  }
   2118 
   2119  wasmOffsetToLine(offset) {
   2120    return wasm.wasmOffsetToLine(this.getDoc(), offset);
   2121  }
   2122 
   2123  lineToWasmOffset(number) {
   2124    return wasm.lineToWasmOffset(this.getDoc(), number);
   2125  }
   2126 
   2127  toLineIfWasmOffset(maybeOffset) {
   2128    if (typeof maybeOffset !== "number" || !this.isWasm) {
   2129      return maybeOffset;
   2130    }
   2131    return this.wasmOffsetToLine(maybeOffset);
   2132  }
   2133 
   2134  renderWasmText(content) {
   2135    return wasm.renderWasmText(this.getDoc(), content);
   2136  }
   2137 
   2138  /**
   2139   * Gets details about the line
   2140   *
   2141   * @param {number} line
   2142   * @returns {object} line info object
   2143   */
   2144  lineInfo(line) {
   2145    const cm = editors.get(this);
   2146    if (this.config.cm6) {
   2147      const el = this.getElementAtLine(line);
   2148      return {
   2149        text: el.innerText,
   2150        // TODO: Expose those, or see usage for those and do things differently
   2151        line: null,
   2152        gutterMarkers: null,
   2153        textClass: null,
   2154        bgClass: null,
   2155        wrapClass: el.className,
   2156        widgets: null,
   2157      };
   2158    }
   2159 
   2160    return cm.lineInfo(line);
   2161  }
   2162 
   2163  /**
   2164   * Get the functions symbols for the current source loaded in the
   2165   * the editor.
   2166   *
   2167   * @param {number} maxResults - The maximum no of results to display
   2168   */
   2169  async getFunctionSymbols(maxResults) {
   2170    const cm = editors.get(this);
   2171    const { codemirrorLanguage } = this.#CodeMirror6;
   2172 
   2173    const functionSymbols = [];
   2174    let resultsCount = 0;
   2175    await lezerUtils.walkTree(cm, codemirrorLanguage, {
   2176      filterSet: lezerUtils.nodeTypeSets.functionsDeclAndExpr,
   2177      enterVisitor: node => {
   2178        if (resultsCount == maxResults) {
   2179          return;
   2180        }
   2181        const syntaxNode = node.node;
   2182        const name = lezerUtils.getFunctionName(cm.state.doc, syntaxNode);
   2183        // Ignore anonymous functions
   2184        if (name == null) {
   2185          return;
   2186        }
   2187 
   2188        functionSymbols.push({
   2189          name,
   2190          klass: lezerUtils.getFunctionClass(cm.state.doc, syntaxNode),
   2191          location: {
   2192            start: lezerUtils.positionToLocation(cm.state.doc, node.from),
   2193            end: lezerUtils.positionToLocation(cm.state.doc, node.to),
   2194          },
   2195          parameterNames: lezerUtils.getFunctionParameterNames(
   2196            cm.state.doc,
   2197            syntaxNode
   2198          ),
   2199          identifier: null,
   2200          index: node.index,
   2201        });
   2202        resultsCount++;
   2203      },
   2204      forceParseTo: cm.state.doc.length,
   2205    });
   2206 
   2207    return functionSymbols;
   2208  }
   2209 
   2210  /**
   2211   * Get the class symbols for the current source loaded in the the editor.
   2212   *
   2213   * @returns
   2214   */
   2215  async getClassSymbols() {
   2216    const cm = editors.get(this);
   2217    const { codemirrorLanguage } = this.#CodeMirror6;
   2218 
   2219    const classSymbols = [];
   2220    await lezerUtils.walkTree(cm, codemirrorLanguage, {
   2221      filterSet: lezerUtils.nodeTypeSets.classes,
   2222      enterVisitor: node => {
   2223        const classVarDefNode = node.node.firstChild.nextSibling;
   2224        classSymbols.push({
   2225          name: cm.state.doc.sliceString(
   2226            classVarDefNode.from,
   2227            classVarDefNode.to
   2228          ),
   2229          location: {
   2230            start: lezerUtils.positionToLocation(cm.state.doc, node.from),
   2231            end: lezerUtils.positionToLocation(cm.state.doc, node.to),
   2232          },
   2233        });
   2234      },
   2235      forceParseTo: cm.state.doc.length,
   2236    });
   2237 
   2238    return classSymbols;
   2239  }
   2240 
   2241  /**
   2242   * Finds the best function name for the location specified.
   2243   * This is used to map original function names to their corresponding
   2244   * generated functions.
   2245   *
   2246   * @param {object} location
   2247   * @returns
   2248   */
   2249  async getClosestFunctionName(location) {
   2250    const cm = editors.get(this);
   2251    const {
   2252      codemirrorLangJavascript: { javascriptLanguage },
   2253      codemirrorLanguage: { forceParsing, syntaxTree },
   2254    } = this.#CodeMirror6;
   2255 
   2256    let doc, tree;
   2257    // If the specified source is already loaded in the editor,
   2258    // codemirror has likely parsed most or all the source needed,
   2259    // just leverage that
   2260    const sourceId = location.source.id;
   2261    if (this.#currentDocumentId === sourceId) {
   2262      doc = cm.state.doc;
   2263      // Parse the rest of the if needed.
   2264      await forceParsing(cm, doc.length, 10000);
   2265 
   2266      tree = syntaxTree(cm.state);
   2267    } else {
   2268      // If the source is not currently loaded in the editor we will need
   2269      // to explicitly parse its source text.
   2270      // Note: The `loadSourceText` actions is called before this util `getClosestFunctionName`
   2271      // to make sure source content is available to use.
   2272      const sourceContent = this.#sources.get(location.source.id);
   2273      if (!sourceContent) {
   2274        console.error(
   2275          `Can't find source content for ${location.source.id}, no function name can be determined`
   2276        );
   2277        return "";
   2278      }
   2279 
   2280      // Create a codemirror document for the current source text.
   2281      doc = cm.state.toText(sourceContent);
   2282      tree = lezerUtils.getTree(javascriptLanguage, sourceId, sourceContent);
   2283    }
   2284 
   2285    const token = lezerUtils.getTreeNodeAtLocation(doc, tree, location);
   2286    if (!token) {
   2287      return null;
   2288    }
   2289 
   2290    const enclosingScope = lezerUtils.getEnclosingFunction(doc, token);
   2291    return enclosingScope ? enclosingScope.funcName : "";
   2292  }
   2293 
   2294  /**
   2295   * Traverse the syntaxTree and return expressions
   2296   * which best match the specified token location is on our
   2297   * list of accepted symbol types.
   2298   *
   2299   * @param {object} tokenLocation
   2300   * @returns {Array} Member expression matches
   2301   */
   2302  async findBestMatchExpressions(tokenLocation) {
   2303    const cm = editors.get(this);
   2304    const { codemirrorLanguage } = this.#CodeMirror6;
   2305 
   2306    const expressions = [];
   2307 
   2308    const line = cm.state.doc.line(tokenLocation.line);
   2309    const tokPos = line.from + tokenLocation.column;
   2310 
   2311    await lezerUtils.walkTree(cm, codemirrorLanguage, {
   2312      filterSet: lezerUtils.nodeTypeSets.expressions,
   2313      enterVisitor: node => {
   2314        if (node.from <= tokPos && node.to >= tokPos) {
   2315          expressions.push({
   2316            type: node.name,
   2317            // Computed member expressions not currently supported
   2318            computed: false,
   2319            expression: cm.state.doc.sliceString(node.from, node.to),
   2320            location: {
   2321              start: lezerUtils.positionToLocation(cm.state.doc, node.from),
   2322              end: lezerUtils.positionToLocation(cm.state.doc, node.to),
   2323            },
   2324            from: node.from,
   2325            to: node.to,
   2326          });
   2327        }
   2328      },
   2329      walkFrom: line.from,
   2330      walkTo: line.to,
   2331    });
   2332 
   2333    // There might be multiple expressions which are within the locations.
   2334    // We want to match expressions based on dots before the desired token.
   2335    //
   2336    // ========================== EXAMPLE 1 ================================
   2337    // Full Expression: `this.myProperty.x`
   2338    // Hovered Token: `myProperty`
   2339    // Found Expressions:
   2340    // { name: "MemberExpression", expression: "this.myProperty.x", from: 1715, to: 1732 }
   2341    // { name: "MemberExpression", expression: "this.myProperty" from: 1715, to: 1730 } *
   2342    // { name: "PropertyName", expression: "myProperty" from: 1720, to: 1730 }
   2343    //
   2344    // ========================== EXAMPLE 2 ==================================
   2345    // Full Expression: `a(b).catch`
   2346    // Hovered Token: `b`
   2347    // Found Expressions:
   2348    // { name: "MemberExpression", expression: "a(b).catch", from: 1921  to: 1931 }
   2349    // { name: "VariableName", expression: "b", from: 1923  to: 1924 } *
   2350    //
   2351    // We sort based on the `to` make sure we return the correct property
   2352    return expressions.sort((a, b) => {
   2353      if (a.to < b.to) {
   2354        return -1;
   2355      } else if (a.to > b.to) {
   2356        return 1;
   2357      }
   2358      return 0;
   2359    });
   2360  }
   2361 
   2362  /**
   2363   * Get all the lines which are inscope when paused a the specified location.
   2364   *
   2365   * @param {object} location
   2366   * @param {Array} in scope lines
   2367   */
   2368  async getInScopeLines(location) {
   2369    const cm = editors.get(this);
   2370    const { codemirrorLanguage } = this.#CodeMirror6;
   2371 
   2372    const functionLocations = [];
   2373 
   2374    await lezerUtils.walkTree(cm, codemirrorLanguage, {
   2375      filterSet: lezerUtils.nodeTypeSets.functions,
   2376      enterVisitor: node => {
   2377        functionLocations.push({
   2378          name: node.name,
   2379          start: lezerUtils.positionToLocation(cm.state.doc, node.from),
   2380          end: lezerUtils.positionToLocation(cm.state.doc, node.to),
   2381        });
   2382      },
   2383      forceParseTo: cm.viewport.to,
   2384    });
   2385 
   2386    // Sort based on the start locations so the scopes
   2387    // are in the same order as in the source.
   2388    const sortedLocations = scopeUtils.sortByStart(functionLocations);
   2389 
   2390    // Any function locations which are within the immediate function scope
   2391    // of the paused location.
   2392    const innerLocations = scopeUtils.getInnerLocations(
   2393      sortedLocations,
   2394      location
   2395    );
   2396 
   2397    // Any outer locations which do not contain the immediate function
   2398    // of the paused location
   2399    const outerLocations = sortedLocations.filter(loc => {
   2400      if (innerLocations.includes(loc)) {
   2401        return false;
   2402      }
   2403      return !scopeUtils.containsPosition(loc, location);
   2404    });
   2405 
   2406    const outOfScopeLines = scopeUtils.getOutOfScopeLines(
   2407      scopeUtils.removeOverlapLocations(outerLocations)
   2408    );
   2409 
   2410    // This operation can be very costly for large files so we sacrifice a bit of readability
   2411    // for performance sake.
   2412    // We initialize an array with a fixed size and we'll directly assign value for lines
   2413    // that are not out of scope. This is much faster than having an empty array and pushing
   2414    // into it.
   2415    const sourceNumLines = cm.state.doc.lines;
   2416    const sourceLines = new Array(sourceNumLines);
   2417    for (let i = 0; i < sourceNumLines; i++) {
   2418      const line = i + 1;
   2419      if (outOfScopeLines.size == 0 || !outOfScopeLines.has(line)) {
   2420        sourceLines[i] = line;
   2421      }
   2422    }
   2423 
   2424    // Finally we need to remove any undefined values, i.e. the ones that were matching
   2425    // out of scope lines.
   2426    return sourceLines.filter(i => i != undefined);
   2427  }
   2428 
   2429  /**
   2430   * Gets all the bindings and generates the related references for
   2431   * the specified platform scope and its ancestry
   2432   *
   2433   * @param {object} location - The currently paused location
   2434   * @param {object} scope - The innermost scope node for the tree. This is provided by the
   2435   *                         platform.
   2436   * @returns {object} Binding references
   2437   *                  Structure
   2438   *                  ==========
   2439   *                  {
   2440   *                    0: { // Levels
   2441   *                      a: { // Binding
   2442   *                        enumerable: true,
   2443   *                        configurable: false
   2444   *                        value: "foo"
   2445   *                        refs: [{ // References
   2446   *                          start: { line: 1, column: 4 }
   2447   *                          end: { line: 3, column: 5 }
   2448   *                          meta: {...} // For details see https://searchfox.org/mozilla-central/rev/ba7293cb2710f015fcd34f2b9919d00e27a9c2f6/devtools/client/shared/sourceeditor/lezer-utils.js#414-420
   2449   *                        }]
   2450   *                      },
   2451   *                      ...
   2452   *                    }
   2453   */
   2454  async getBindingReferences(location, scope) {
   2455    const cm = editors.get(this);
   2456    const {
   2457      codemirrorLanguage: { syntaxTree },
   2458    } = this.#CodeMirror6;
   2459 
   2460    const token = lezerUtils.getTreeNodeAtLocation(
   2461      cm.state.doc,
   2462      syntaxTree(cm.state),
   2463      location
   2464    );
   2465 
   2466    if (!token) {
   2467      return null;
   2468    }
   2469 
   2470    let scopeNode = null;
   2471    let level = 0;
   2472    const bindingReferences = {};
   2473 
   2474    // Walk up the scope tree and generate the bindings and references
   2475    while (scope && scope.bindings) {
   2476      const bindings = lezerUtils.getScopeBindings(scope.bindings);
   2477      const seen = new Set();
   2478      scopeNode = lezerUtils.getParentScopeOfType(
   2479        scopeNode || token,
   2480        scope.type
   2481      );
   2482      if (!scopeNode) {
   2483        break;
   2484      }
   2485      await lezerUtils.walkCursor(scopeNode.node.cursor(), {
   2486        filterSet: lezerUtils.nodeTypeSets.bindingReferences,
   2487        enterVisitor: node => {
   2488          let bindingName = cm.state.doc.sliceString(node.from, node.to);
   2489          if (!(bindingName in bindings) || seen.has(bindingName)) {
   2490            return;
   2491          }
   2492          const bindingData = bindings[bindingName];
   2493          const ref = {
   2494            start: lezerUtils.positionToLocation(cm.state.doc, node.from),
   2495            end: lezerUtils.positionToLocation(cm.state.doc, node.to),
   2496          };
   2497          const syntaxNode = node.node;
   2498          // Previews for member expressions are built of the meta property which is
   2499          // reference of the child property and so on. e.g a.b.c
   2500          if (syntaxNode.parent.name == lezerUtils.nodeTypes.MemberExpression) {
   2501            ref.meta = lezerUtils.getMetaBindings(
   2502              cm.state.doc,
   2503              syntaxNode.parent
   2504            );
   2505            // For member expressions use the name of the parent object as the binding name
   2506            // i.e for `obj.a.b` the binding name should be `obj`
   2507            bindingName = cm.state.doc.sliceString(
   2508              syntaxNode.parent.from,
   2509              syntaxNode.parent.to
   2510            );
   2511            const dotIndex = bindingName.indexOf(".");
   2512            if (dotIndex > -1) {
   2513              bindingName = bindingName.substring(0, dotIndex);
   2514            }
   2515          }
   2516 
   2517          if (!bindingReferences[level]) {
   2518            bindingReferences[level] = Object.create(null);
   2519          }
   2520          if (!bindingReferences[level][bindingName]) {
   2521            // Put the binding info and related references together for
   2522            // easy and efficient access.
   2523            bindingReferences[level][bindingName] = {
   2524              ...bindingData,
   2525              refs: [],
   2526            };
   2527          }
   2528          bindingReferences[level][bindingName].refs.push(ref);
   2529          seen.add(bindingName);
   2530        },
   2531      });
   2532      if (scope.type === "function") {
   2533        break;
   2534      }
   2535      level++;
   2536      scope = scope.parent;
   2537    }
   2538    return bindingReferences;
   2539  }
   2540 
   2541  /**
   2542   * Replaces whatever is in the text area with the contents of
   2543   * the 'value' argument.
   2544   *
   2545   * @param {string} value: The text to replace the editor content
   2546   * @param {object} options
   2547   * @param {string} options.documentId
   2548   *                 Optional unique id represeting the specific document which is source of the text.
   2549   *                 Will be null for loading and error messages.
   2550   * @param {boolean} options.saveTransactionToHistory
   2551   *                 This determines if the transaction for this specific text change should be added to the undo/redo history.
   2552   */
   2553  async setText(value, { documentId, saveTransactionToHistory = true } = {}) {
   2554    const cm = editors.get(this);
   2555    const isWasm = typeof value !== "string" && "binary" in value;
   2556 
   2557    if (documentId) {
   2558      this.#currentDocumentId = documentId;
   2559    } else {
   2560      // Reset this ID when showing loading and error messages,
   2561      // so that we keep track when an actual source is displayed
   2562      this.#currentDocumentId = null;
   2563    }
   2564 
   2565    if (isWasm) {
   2566      // wasm?
   2567      // binary does not survive as Uint8Array, converting from string
   2568      const binary = value.binary;
   2569      const data = new Uint8Array(binary.length);
   2570      for (let i = 0; i < data.length; i++) {
   2571        data[i] = binary.charCodeAt(i);
   2572      }
   2573 
   2574      const { lines, done } = wasm.getWasmText(this.getDoc(), data);
   2575      const MAX_LINES = 10000000;
   2576      if (lines.length > MAX_LINES) {
   2577        lines.splice(MAX_LINES, lines.length - MAX_LINES);
   2578        lines.push(";; .... text is truncated due to the size");
   2579      }
   2580      if (!done) {
   2581        lines.push(";; .... possible error during wast conversion");
   2582      }
   2583 
   2584      if (this.config.cm6) {
   2585        value = lines.join("\n");
   2586      } else {
   2587        // cm will try to split into lines anyway, saving memory
   2588        value = { split: () => lines };
   2589      }
   2590    }
   2591 
   2592    if (this.config.cm6) {
   2593      if (cm.state.doc.toString() == value) {
   2594        return;
   2595      }
   2596 
   2597      const {
   2598        codemirrorView: { EditorView, lineNumbers },
   2599        codemirrorState: { Transaction },
   2600      } = this.#CodeMirror6;
   2601 
   2602      await cm.dispatch({
   2603        changes: { from: 0, to: cm.state.doc.length, insert: value },
   2604        selection: { anchor: 0 },
   2605        annotations: [Transaction.addToHistory.of(saveTransactionToHistory)],
   2606      });
   2607 
   2608      const effects = [];
   2609      if (this.config?.lineNumbers) {
   2610        const lineNumbersConfig = {
   2611          domEventHandlers: this.#gutterDOMEventHandlers,
   2612        };
   2613        if (isWasm) {
   2614          lineNumbersConfig.formatNumber = this.getWasmLineNumberFormatter();
   2615        }
   2616        effects.push(
   2617          this.#compartments.lineNumberCompartment.reconfigure(
   2618            lineNumbers(lineNumbersConfig)
   2619          )
   2620        );
   2621      }
   2622      // Get the cached scroll snapshot for this source and restore
   2623      // the scroll position. Note: The scroll has to be done in a seperate dispatch
   2624      // (after the previous dispatch has set the document), this is because
   2625      // it is required that the document the scroll snapshot is applied to
   2626      // is the exact document it was saved on.
   2627      const scrollSnapshot = this.#scrollSnapshots.get(documentId);
   2628 
   2629      effects.push(
   2630        scrollSnapshot ? scrollSnapshot : EditorView.scrollIntoView(0)
   2631      );
   2632 
   2633      await cm.dispatch({ effects });
   2634 
   2635      if (this.currentDocumentId) {
   2636        // If there is no scroll snapshot explicitly cache the snapshot set as no scroll
   2637        // is triggered.
   2638        if (!scrollSnapshot) {
   2639          this.#cacheScrollSnapshot();
   2640        }
   2641      }
   2642    } else {
   2643      cm.setValue(value);
   2644    }
   2645 
   2646    this.resetIndentUnit();
   2647  }
   2648 
   2649  addSource(id, sourceText) {
   2650    this.#sources.set(id, sourceText);
   2651  }
   2652 
   2653  clearSources(ids) {
   2654    if (ids) {
   2655      for (const id of ids) {
   2656        this.#sources.delete(id);
   2657      }
   2658    } else {
   2659      this.#sources.clear();
   2660      lezerUtils.clear();
   2661    }
   2662  }
   2663 
   2664  /* Currently used only in tests */
   2665  sourcesCount() {
   2666    return this.#sources.size;
   2667  }
   2668 
   2669  /**
   2670   * Reloads the state of the editor based on all current preferences.
   2671   * This is called automatically when any of the relevant preferences
   2672   * change.
   2673   */
   2674  reloadPreferences() {
   2675    // Restore the saved autoCloseBrackets value if it is preffed on.
   2676    const useAutoClose = Services.prefs.getBoolPref(AUTO_CLOSE);
   2677    this.setOption(
   2678      "autoCloseBrackets",
   2679      useAutoClose ? this.config.autoCloseBracketsSaved : false
   2680    );
   2681 
   2682    this.updateCodeFoldingGutter();
   2683 
   2684    this.resetIndentUnit();
   2685    this.setupAutoCompletion();
   2686  }
   2687 
   2688  /**
   2689   * Set the current keyMap for CodeMirror, and load the support file if needed.
   2690   *
   2691   * @param {Window} win: The window on which the keymap files should be loaded.
   2692   */
   2693  setKeyMap(win) {
   2694    if (this.config.isReadOnly) {
   2695      return;
   2696    }
   2697 
   2698    const keyMap = Services.prefs.getCharPref(KEYMAP_PREF);
   2699 
   2700    // If alternative keymap is provided, use it.
   2701    if (VALID_KEYMAPS.has(keyMap)) {
   2702      if (!this.#loadedKeyMaps.has(keyMap)) {
   2703        Services.scriptloader.loadSubScript(VALID_KEYMAPS.get(keyMap), win);
   2704        this.#loadedKeyMaps.add(keyMap);
   2705      }
   2706      this.setOption("keyMap", keyMap);
   2707    } else {
   2708      this.setOption("keyMap", "default");
   2709    }
   2710  }
   2711 
   2712  /**
   2713   * Sets the editor's indentation based on the current prefs and
   2714   * re-detect indentation if we should.
   2715   */
   2716  resetIndentUnit() {
   2717    if (this.isDestroyed()) {
   2718      return;
   2719    }
   2720    const cm = editors.get(this);
   2721    const iterFn = (start, maxEnd, callback) => {
   2722      if (!this.config.cm6) {
   2723        if (this.isDestroyed()) {
   2724          return;
   2725        }
   2726        cm.eachLine(start, maxEnd, line => {
   2727          return callback(line.text);
   2728        });
   2729      } else {
   2730        const iterator = cm.state.doc.iterLines(
   2731          start + 1,
   2732          Math.min(cm.state.doc.lines, maxEnd) + 1
   2733        );
   2734        let callbackRes;
   2735        do {
   2736          iterator.next();
   2737          callbackRes = callback(iterator.value);
   2738        } while (iterator.done !== true && !callbackRes);
   2739      }
   2740    };
   2741 
   2742    const { indentUnit, indentWithTabs } = getIndentationFromIteration(iterFn);
   2743 
   2744    if (!this.config.cm6) {
   2745      cm.setOption("tabSize", indentUnit);
   2746      cm.setOption("indentUnit", indentUnit);
   2747      cm.setOption("indentWithTabs", indentWithTabs);
   2748    } else {
   2749      const {
   2750        codemirrorState: { EditorState },
   2751        codemirrorLanguage,
   2752      } = this.#CodeMirror6;
   2753 
   2754      cm.dispatch({
   2755        effects: this.#compartments.tabSizeCompartment.reconfigure(
   2756          EditorState.tabSize.of(indentUnit)
   2757        ),
   2758      });
   2759      cm.dispatch({
   2760        effects: this.#compartments.indentCompartment.reconfigure(
   2761          codemirrorLanguage.indentUnit.of(
   2762            (indentWithTabs ? "\t" : " ").repeat(indentUnit)
   2763          )
   2764        ),
   2765      });
   2766    }
   2767  }
   2768 
   2769  /**
   2770   * Replaces contents of a text area within the from/to {line, ch}
   2771   * range. If neither `from` nor `to` arguments are provided works
   2772   * exactly like setText. If only `from` object is provided, inserts
   2773   * text at that point, *overwriting* as many characters as needed.
   2774   */
   2775  replaceText(value, from, to) {
   2776    const cm = editors.get(this);
   2777 
   2778    if (!from) {
   2779      this.setText(value);
   2780      return;
   2781    }
   2782 
   2783    if (!to) {
   2784      const text = cm.getRange({ line: 0, ch: 0 }, from);
   2785      this.setText(text + value);
   2786      return;
   2787    }
   2788 
   2789    cm.replaceRange(value, from, to);
   2790  }
   2791 
   2792  /**
   2793   * Inserts text at the specified {line, ch} position, shifting existing
   2794   * contents as necessary.
   2795   */
   2796  insertText(value, at) {
   2797    const cm = editors.get(this);
   2798    cm.replaceRange(value, at, at);
   2799  }
   2800 
   2801  /**
   2802   * Deselects contents of the text area.
   2803   */
   2804  dropSelection() {
   2805    if (!this.somethingSelected()) {
   2806      return;
   2807    }
   2808 
   2809    this.setCursor(this.getCursor());
   2810  }
   2811 
   2812  /**
   2813   * Returns true if there is more than one selection in the editor.
   2814   */
   2815  hasMultipleSelections() {
   2816    const cm = editors.get(this);
   2817    return cm.listSelections().length > 1;
   2818  }
   2819 
   2820  /**
   2821   * Gets the first visible line number in the editor.
   2822   */
   2823  getFirstVisibleLine() {
   2824    const cm = editors.get(this);
   2825    return cm.lineAtHeight(0, "local");
   2826  }
   2827 
   2828  /**
   2829   * Scrolls the view such that the given line number is the first visible line.
   2830   */
   2831  setFirstVisibleLine(line) {
   2832    const cm = editors.get(this);
   2833    const { top } = cm.charCoords({ line, ch: 0 }, "local");
   2834    cm.scrollTo(0, top);
   2835  }
   2836 
   2837  /**
   2838   * Sets the cursor to the specified {line, ch} position with an additional
   2839   * option to align the line at the "top", "center" or "bottom" of the editor
   2840   * with "top" being default value.
   2841   */
   2842  setCursor({ line, ch }, align) {
   2843    const cm = editors.get(this);
   2844    this.alignLine(line, align);
   2845    cm.setCursor({ line, ch });
   2846    this.emit("cursorActivity");
   2847  }
   2848 
   2849  /**
   2850   * Aligns the provided line to either "top", "center" or "bottom" of the
   2851   * editor view with a maximum margin of MAX_VERTICAL_OFFSET lines from top or
   2852   * bottom.
   2853   */
   2854  alignLine(line, align) {
   2855    const cm = editors.get(this);
   2856    const from = cm.lineAtHeight(0, "page");
   2857    const to = cm.lineAtHeight(cm.getWrapperElement().clientHeight, "page");
   2858    const linesVisible = to - from;
   2859    const halfVisible = Math.round(linesVisible / 2);
   2860 
   2861    // If the target line is in view, skip the vertical alignment part.
   2862    if (line <= to && line >= from) {
   2863      return;
   2864    }
   2865 
   2866    // Setting the offset so that the line always falls in the upper half
   2867    // of visible lines (lower half for bottom aligned).
   2868    // MAX_VERTICAL_OFFSET is the maximum allowed value.
   2869    const offset = Math.min(halfVisible, MAX_VERTICAL_OFFSET);
   2870 
   2871    let topLine =
   2872      {
   2873        center: Math.max(line - halfVisible, 0),
   2874        bottom: Math.max(line - linesVisible + offset, 0),
   2875        top: Math.max(line - offset, 0),
   2876      }[align || "top"] || offset;
   2877 
   2878    // Bringing down the topLine to total lines in the editor if exceeding.
   2879    topLine = Math.min(topLine, this.lineCount());
   2880    this.setFirstVisibleLine(topLine);
   2881  }
   2882 
   2883  /**
   2884   * Returns whether a marker of a specified class exists in a line's gutter.
   2885   */
   2886  hasMarker(line, gutterName, markerClass) {
   2887    const marker = this.getMarker(line, gutterName);
   2888    if (!marker) {
   2889      return false;
   2890    }
   2891 
   2892    return marker.classList.contains(markerClass);
   2893  }
   2894 
   2895  /**
   2896   * Adds a marker with a specified class to a line's gutter. If another marker
   2897   * exists on that line, the new marker class is added to its class list.
   2898   */
   2899  addMarker(line, gutterName, markerClass) {
   2900    const cm = editors.get(this);
   2901    const info = this.lineInfo(line);
   2902    if (!info) {
   2903      return;
   2904    }
   2905 
   2906    const gutterMarkers = info.gutterMarkers;
   2907    let marker;
   2908    if (gutterMarkers) {
   2909      marker = gutterMarkers[gutterName];
   2910      if (marker) {
   2911        marker.classList.add(markerClass);
   2912        return;
   2913      }
   2914    }
   2915 
   2916    marker = cm.getWrapperElement().ownerDocument.createElement("div");
   2917    marker.className = markerClass;
   2918    cm.setGutterMarker(info.line, gutterName, marker);
   2919  }
   2920 
   2921  /**
   2922   * The reverse of addMarker. Removes a marker of a specified class from a
   2923   * line's gutter.
   2924   */
   2925  removeMarker(line, gutterName, markerClass) {
   2926    if (!this.hasMarker(line, gutterName, markerClass)) {
   2927      return;
   2928    }
   2929 
   2930    this.lineInfo(line).gutterMarkers[gutterName].classList.remove(markerClass);
   2931  }
   2932 
   2933  /**
   2934   * Adds a marker with a specified class and an HTML content to a line's
   2935   * gutter. If another marker exists on that line, it is overwritten by a new
   2936   * marker.
   2937   */
   2938  addContentMarker(line, gutterName, markerClass, content) {
   2939    const cm = editors.get(this);
   2940    const info = this.lineInfo(line);
   2941    if (!info) {
   2942      return;
   2943    }
   2944 
   2945    const marker = cm.getWrapperElement().ownerDocument.createElement("div");
   2946    marker.className = markerClass;
   2947    // eslint-disable-next-line no-unsanitized/property
   2948    marker.innerHTML = content;
   2949    cm.setGutterMarker(info.line, gutterName, marker);
   2950  }
   2951 
   2952  /**
   2953   * The reverse of addContentMarker. Removes any line's markers in the
   2954   * specified gutter.
   2955   */
   2956  removeContentMarker(line, gutterName) {
   2957    const cm = editors.get(this);
   2958    const info = this.lineInfo(line);
   2959    if (!info) {
   2960      return;
   2961    }
   2962 
   2963    cm.setGutterMarker(info.line, gutterName, null);
   2964  }
   2965 
   2966  getMarker(line, gutterName) {
   2967    const info = this.lineInfo(line);
   2968    if (!info) {
   2969      return null;
   2970    }
   2971 
   2972    const gutterMarkers = info.gutterMarkers;
   2973    if (!gutterMarkers) {
   2974      return null;
   2975    }
   2976 
   2977    return gutterMarkers[gutterName];
   2978  }
   2979 
   2980  /**
   2981   * Removes all gutter markers in the gutter with the given name.
   2982   */
   2983  removeAllMarkers(gutterName) {
   2984    const cm = editors.get(this);
   2985    cm.clearGutter(gutterName);
   2986  }
   2987 
   2988  /**
   2989   * Handles attaching a set of events listeners on a marker. They should
   2990   * be passed as an object literal with keys as event names and values as
   2991   * function listeners. The line number, marker node and optional data
   2992   * will be passed as arguments to the function listener.
   2993   *
   2994   * You don't need to worry about removing these event listeners.
   2995   * They're automatically orphaned when clearing markers.
   2996   */
   2997  setMarkerListeners(line, gutterName, markerClass, eventsArg, data) {
   2998    if (!this.hasMarker(line, gutterName, markerClass)) {
   2999      return;
   3000    }
   3001 
   3002    const cm = editors.get(this);
   3003    const marker = cm.lineInfo(line).gutterMarkers[gutterName];
   3004 
   3005    for (const name in eventsArg) {
   3006      const listener = eventsArg[name].bind(this, line, marker, data);
   3007      marker.addEventListener(name, listener, {
   3008        signal: this.#abortController?.signal,
   3009      });
   3010    }
   3011  }
   3012 
   3013  /**
   3014   * Returns whether a line is decorated using the specified class name.
   3015   */
   3016  hasLineClass(line, className) {
   3017    const info = this.lineInfo(line);
   3018 
   3019    if (!info || !info.wrapClass) {
   3020      return false;
   3021    }
   3022 
   3023    return info.wrapClass.split(" ").includes(className);
   3024  }
   3025 
   3026  /**
   3027   * Sets a CSS class name for the given line, including the text and gutter.
   3028   */
   3029  addLineClass(lineOrOffset, className) {
   3030    const cm = editors.get(this);
   3031    const line = this.toLineIfWasmOffset(lineOrOffset);
   3032    cm.addLineClass(line, "wrap", className);
   3033  }
   3034 
   3035  /**
   3036   * The reverse of addLineClass.
   3037   */
   3038  removeLineClass(lineOrOffset, className) {
   3039    const cm = editors.get(this);
   3040    const line = this.toLineIfWasmOffset(lineOrOffset);
   3041    cm.removeLineClass(line, "wrap", className);
   3042  }
   3043 
   3044  /**
   3045   * Mark a range of text inside the two {line, ch} bounds. Since the range may
   3046   * be modified, for example, when typing text, this method returns a function
   3047   * that can be used to remove the mark.
   3048   */
   3049  markText(from, to, className = "marked-text") {
   3050    const cm = editors.get(this);
   3051    const text = cm.getRange(from, to);
   3052    const span = cm.getWrapperElement().ownerDocument.createElement("span");
   3053    span.className = className;
   3054    span.textContent = text;
   3055 
   3056    const mark = cm.markText(from, to, { replacedWith: span });
   3057    return {
   3058      anchor: span,
   3059      clear: () => mark.clear(),
   3060    };
   3061  }
   3062 
   3063  /**
   3064   * Calculates and returns one or more {line, ch} objects for
   3065   * a zero-based index who's value is relative to the start of
   3066   * the editor's text.
   3067   *
   3068   * If only one argument is given, this method returns a single
   3069   * {line,ch} object. Otherwise it returns an array.
   3070   */
   3071  getPosition(...args) {
   3072    const cm = editors.get(this);
   3073    const res = args.map(ind => cm.posFromIndex(ind));
   3074    return args.length === 1 ? res[0] : res;
   3075  }
   3076 
   3077  /**
   3078   * The reverse of getPosition. Similarly to getPosition this
   3079   * method returns a single value if only one argument was given
   3080   * and an array otherwise.
   3081   */
   3082  getOffset(...args) {
   3083    const cm = editors.get(this);
   3084    const res = args.map(pos => cm.indexFromPos(pos));
   3085    return args.length > 1 ? res : res[0];
   3086  }
   3087 
   3088  /**
   3089   * Returns a {line, ch} object that corresponds to the
   3090   * left, top coordinates.
   3091   */
   3092  getPositionFromCoords({ left, top }) {
   3093    const cm = editors.get(this);
   3094    return cm.coordsChar({ left, top });
   3095  }
   3096 
   3097  /**
   3098   * Returns true if there's something to undo and false otherwise.
   3099   */
   3100  canUndo() {
   3101    const cm = editors.get(this);
   3102    return cm.historySize().undo > 0;
   3103  }
   3104 
   3105  /**
   3106   * Returns true if there's something to redo and false otherwise.
   3107   */
   3108  canRedo() {
   3109    const cm = editors.get(this);
   3110    return cm.historySize().redo > 0;
   3111  }
   3112 
   3113  /**
   3114   * Marks the contents as clean and returns the current
   3115   * version number.
   3116   */
   3117  setClean() {
   3118    const cm = editors.get(this);
   3119    this.version = cm.changeGeneration();
   3120    this.#lastDirty = false;
   3121    this.emit("dirty-change");
   3122    return this.version;
   3123  }
   3124 
   3125  /**
   3126   * Returns true if contents of the text area are
   3127   * clean i.e. no changes were made since the last version.
   3128   */
   3129  isClean() {
   3130    const cm = editors.get(this);
   3131    return cm.isClean(this.version);
   3132  }
   3133 
   3134  /**
   3135   * This method opens an in-editor dialog asking for a line to
   3136   * jump to. Once given, it changes cursor to that line.
   3137   */
   3138  jumpToLine() {
   3139    const doc = editors.get(this).getWrapperElement().ownerDocument;
   3140    const div = doc.createElement("div");
   3141    const inp = doc.createElement("input");
   3142    const txt = doc.createTextNode(L10N.getStr("gotoLineCmd.promptTitle"));
   3143 
   3144    inp.type = "text";
   3145    inp.style.width = "10em";
   3146    inp.style.marginInlineStart = "1em";
   3147 
   3148    div.appendChild(txt);
   3149    div.appendChild(inp);
   3150 
   3151    this.openDialog(div, line => {
   3152      // Handle LINE:COLUMN as well as LINE
   3153      const match = line.toString().match(RE_JUMP_TO_LINE);
   3154      if (match) {
   3155        const [, matchLine, column] = match;
   3156        this.setCursor({ line: matchLine - 1, ch: column ? column - 1 : 0 });
   3157      }
   3158    });
   3159  }
   3160 
   3161  /**
   3162   * Moves the content of the current line or the lines selected up a line.
   3163   */
   3164  moveLineUp() {
   3165    const cm = editors.get(this);
   3166    const start = cm.getCursor("start");
   3167    const end = cm.getCursor("end");
   3168 
   3169    if (start.line === 0) {
   3170      return;
   3171    }
   3172 
   3173    // Get the text in the lines selected or the current line of the cursor
   3174    // and append the text of the previous line.
   3175    let value;
   3176    if (start.line !== end.line) {
   3177      value =
   3178        cm.getRange(
   3179          { line: start.line, ch: 0 },
   3180          { line: end.line, ch: cm.getLine(end.line).length }
   3181        ) + "\n";
   3182    } else {
   3183      value = cm.getLine(start.line) + "\n";
   3184    }
   3185    value += cm.getLine(start.line - 1);
   3186 
   3187    // Replace the previous line and the currently selected lines with the new
   3188    // value and maintain the selection of the text.
   3189    cm.replaceRange(
   3190      value,
   3191      { line: start.line - 1, ch: 0 },
   3192      { line: end.line, ch: cm.getLine(end.line).length }
   3193    );
   3194    cm.setSelection(
   3195      { line: start.line - 1, ch: start.ch },
   3196      { line: end.line - 1, ch: end.ch }
   3197    );
   3198  }
   3199 
   3200  /**
   3201   * Moves the content of the current line or the lines selected down a line.
   3202   */
   3203  moveLineDown() {
   3204    const cm = editors.get(this);
   3205    const start = cm.getCursor("start");
   3206    const end = cm.getCursor("end");
   3207 
   3208    if (end.line + 1 === cm.lineCount()) {
   3209      return;
   3210    }
   3211 
   3212    // Get the text of next line and append the text in the lines selected
   3213    // or the current line of the cursor.
   3214    let value = cm.getLine(end.line + 1) + "\n";
   3215    if (start.line !== end.line) {
   3216      value += cm.getRange(
   3217        { line: start.line, ch: 0 },
   3218        { line: end.line, ch: cm.getLine(end.line).length }
   3219      );
   3220    } else {
   3221      value += cm.getLine(start.line);
   3222    }
   3223 
   3224    // Replace the currently selected lines and the next line with the new
   3225    // value and maintain the selection of the text.
   3226    cm.replaceRange(
   3227      value,
   3228      { line: start.line, ch: 0 },
   3229      { line: end.line + 1, ch: cm.getLine(end.line + 1).length }
   3230    );
   3231    cm.setSelection(
   3232      { line: start.line + 1, ch: start.ch },
   3233      { line: end.line + 1, ch: end.ch }
   3234    );
   3235  }
   3236 
   3237  /**
   3238   * Intercept CodeMirror's Find and replace key shortcut to select the search input
   3239   */
   3240  findOrReplace(node, isReplaceAll) {
   3241    const cm = editors.get(this);
   3242    const isInput = node.tagName === "INPUT";
   3243    const isSearchInput = isInput && node.type === "search";
   3244    // replace box is a different input instance than search, and it is
   3245    // located in a code mirror dialog
   3246    const isDialogInput =
   3247      isInput &&
   3248      node.parentNode &&
   3249      node.parentNode.classList.contains("CodeMirror-dialog");
   3250    if (!(isSearchInput || isDialogInput)) {
   3251      return;
   3252    }
   3253 
   3254    if (isSearchInput || isReplaceAll) {
   3255      // select the search input
   3256      // it's the precise reason why we reimplement these key shortcuts
   3257      node.select();
   3258    }
   3259 
   3260    // need to call it since we prevent the propagation of the event and
   3261    // cancel codemirror's key handling
   3262    cm.execCommand("findPersistent");
   3263  }
   3264 
   3265  /**
   3266   * Intercept CodeMirror's findNext and findPrev key shortcut to allow
   3267   * immediately search for next occurance after typing a word to search.
   3268   */
   3269  findNextOrPrev(node, isFindPrev) {
   3270    const cm = editors.get(this);
   3271    const isInput = node.tagName === "INPUT";
   3272    const isSearchInput = isInput && node.type === "search";
   3273    if (!isSearchInput) {
   3274      return;
   3275    }
   3276    const query = node.value;
   3277    // cm.state.search allows to automatically start searching for the next occurance
   3278    // it's the precise reason why we reimplement these key shortcuts
   3279    if (!cm.state.search || cm.state.search.query !== query) {
   3280      cm.state.search = {
   3281        posFrom: null,
   3282        posTo: null,
   3283        overlay: null,
   3284        query,
   3285      };
   3286    }
   3287 
   3288    // need to call it since we prevent the propagation of the event and
   3289    // cancel codemirror's key handling
   3290    if (isFindPrev) {
   3291      cm.execCommand("findPrev");
   3292    } else {
   3293      cm.execCommand("findNext");
   3294    }
   3295  }
   3296 
   3297  /**
   3298   * Returns current font size for the editor area, in pixels.
   3299   */
   3300  getFontSize() {
   3301    const cm = editors.get(this);
   3302    const el = cm.getWrapperElement();
   3303    const win = el.ownerDocument.defaultView;
   3304 
   3305    return parseInt(win.getComputedStyle(el).getPropertyValue("font-size"), 10);
   3306  }
   3307 
   3308  /**
   3309   * Sets font size for the editor area.
   3310   */
   3311  setFontSize(size) {
   3312    const cm = editors.get(this);
   3313    cm.getWrapperElement().style.fontSize = parseInt(size, 10) + "px";
   3314    cm.refresh();
   3315  }
   3316 
   3317  setLineWrapping(value) {
   3318    const cm = editors.get(this);
   3319    if (this.config.cm6) {
   3320      const {
   3321        codemirrorView: { EditorView },
   3322      } = this.#CodeMirror6;
   3323      cm.dispatch({
   3324        effects: this.#compartments.lineWrapCompartment.reconfigure(
   3325          value ? EditorView.lineWrapping : []
   3326        ),
   3327      });
   3328    } else {
   3329      cm.setOption("lineWrapping", value);
   3330    }
   3331    this.config.lineWrapping = value;
   3332  }
   3333 
   3334  /**
   3335   * Sets an option for the editor.  For most options it just defers to
   3336   * CodeMirror.setOption, but certain ones are maintained within the editor
   3337   * instance.
   3338   */
   3339  setOption(o, v) {
   3340    const cm = editors.get(this);
   3341 
   3342    // Save the state of a valid autoCloseBrackets string, so we can reset
   3343    // it if it gets preffed off and back on.
   3344    if (o === "autoCloseBrackets" && v) {
   3345      this.config.autoCloseBracketsSaved = v;
   3346    }
   3347 
   3348    if (o === "autocomplete") {
   3349      this.config.autocomplete = v;
   3350      this.setupAutoCompletion();
   3351    } else {
   3352      cm.setOption(o, v);
   3353      this.config[o] = v;
   3354    }
   3355 
   3356    if (o === "enableCodeFolding") {
   3357      // The new value maybe explicitly force foldGUtter on or off, ignoring
   3358      // the prefs service.
   3359      this.updateCodeFoldingGutter();
   3360    }
   3361  }
   3362 
   3363  /**
   3364   * Gets an option for the editor.  For most options it just defers to
   3365   * CodeMirror.getOption, but certain ones are maintained within the editor
   3366   * instance.
   3367   */
   3368  getOption(o) {
   3369    const cm = editors.get(this);
   3370    if (o === "autocomplete") {
   3371      return this.config.autocomplete;
   3372    }
   3373 
   3374    return cm.getOption(o);
   3375  }
   3376 
   3377  /**
   3378   * Sets up autocompletion for the editor. Lazily imports the required
   3379   * dependencies because they vary by editor mode.
   3380   *
   3381   * Autocompletion is special, because we don't want to automatically use
   3382   * it just because it is preffed on (it still needs to be requested by the
   3383   * editor), but we do want to always disable it if it is preffed off.
   3384   */
   3385  setupAutoCompletion() {
   3386    if (!this.config.autocomplete && !this.initializeAutoCompletion) {
   3387      // Do nothing since there is no autocomplete config and no autocompletion have
   3388      // been initialized.
   3389      return;
   3390    }
   3391    // The autocomplete module will overwrite this.initializeAutoCompletion
   3392    // with a mode specific autocompletion handler.
   3393    if (!this.initializeAutoCompletion) {
   3394      this.extend(
   3395        require("resource://devtools/client/shared/sourceeditor/autocomplete.js")
   3396      );
   3397    }
   3398 
   3399    if (this.config.autocomplete && Services.prefs.getBoolPref(AUTOCOMPLETE)) {
   3400      this.initializeAutoCompletion(this.config.autocompleteOpts);
   3401    } else {
   3402      this.destroyAutoCompletion();
   3403    }
   3404  }
   3405 
   3406  getAutoCompletionText() {
   3407    const cm = editors.get(this);
   3408    const mark = cm
   3409      .getAllMarks()
   3410      .find(m => m.className === AUTOCOMPLETE_MARK_CLASSNAME);
   3411    if (!mark) {
   3412      return "";
   3413    }
   3414 
   3415    return mark.attributes["data-completion"] || "";
   3416  }
   3417 
   3418  setAutoCompletionText(text) {
   3419    const cursor = this.getCursor();
   3420    const cm = editors.get(this);
   3421    const className = AUTOCOMPLETE_MARK_CLASSNAME;
   3422 
   3423    cm.operation(() => {
   3424      cm.getAllMarks().forEach(mark => {
   3425        if (mark.className === className) {
   3426          mark.clear();
   3427        }
   3428      });
   3429 
   3430      if (text) {
   3431        cm.markText({ ...cursor, ch: cursor.ch - 1 }, cursor, {
   3432          className,
   3433          attributes: {
   3434            "data-completion": text,
   3435          },
   3436        });
   3437      }
   3438    });
   3439  }
   3440 
   3441  /**
   3442   * Gets the element at the specified codemirror offset
   3443   *
   3444   * @param {number} offset
   3445   * @return {Element|null}
   3446   */
   3447  #getElementAtOffset(offset) {
   3448    const cm = editors.get(this);
   3449    const el = cm.domAtPos(offset).node;
   3450    if (!el) {
   3451      return null;
   3452    }
   3453    // Text nodes do not have offset* properties, so lets use its
   3454    // parent element;
   3455    if (el.nodeType == nodeConstants.TEXT_NODE) {
   3456      return el.parentElement;
   3457    }
   3458    return el;
   3459  }
   3460 
   3461  /**
   3462   * This checks if the specified position (line/column) is within the current viewport
   3463   * bounds. it helps determine if scrolling should happen.
   3464   *
   3465   * @param {number} line - The line in the source
   3466   * @param {number} column - The column in the source
   3467   * @returns {boolean}
   3468   */
   3469  isPositionVisible(line, column) {
   3470    const cm = editors.get(this);
   3471    let inXView, inYView;
   3472 
   3473    function withinBounds(x, min, max) {
   3474      return x >= min && x <= max;
   3475    }
   3476 
   3477    if (this.config.cm6) {
   3478      const pos = this.#positionToOffset(line, column);
   3479      if (pos == null) {
   3480        return false;
   3481      }
   3482      // `coordsAtPos` returns the absolute position of the line/column location
   3483      // so that we have to ensure comparing with same absolute position for
   3484      // CodeMirror DOM Element.
   3485      //
   3486      // Note that it may return the coordinates for a column breakpoint marker
   3487      // so it may still report as visible, if the marker is on the edge of the viewport
   3488      // and the displayed character at line/column is actually hidden after the scrollable area.
   3489      const coords = cm.coordsAtPos(pos);
   3490      if (!coords) {
   3491        return false;
   3492      }
   3493      const { x, y, width, height } = cm.dom.getBoundingClientRect();
   3494      const gutterWidth = cm.dom.querySelector(".cm-gutters").clientWidth;
   3495 
   3496      inXView = coords.left > x + gutterWidth && coords.right < x + width;
   3497      inYView = coords.top > y && coords.bottom < y + height;
   3498    } else {
   3499      const { top, left } = cm.charCoords({ line, ch: column }, "local");
   3500      const scrollArea = cm.getScrollInfo();
   3501      const charWidth = cm.defaultCharWidth();
   3502      const fontHeight = cm.defaultTextHeight();
   3503      const { scrollTop, scrollLeft } = cm.doc;
   3504 
   3505      inXView = withinBounds(
   3506        left,
   3507        scrollLeft,
   3508        // Note: 30 might relate to the margin on one of the scroll bar elements.
   3509        // See comment https://github.com/firefox-devtools/debugger/pull/5182#discussion_r163439209
   3510        scrollLeft + (scrollArea.clientWidth - 30) - charWidth
   3511      );
   3512      inYView = withinBounds(
   3513        top,
   3514        scrollTop,
   3515        scrollTop + scrollArea.clientHeight - fontHeight
   3516      );
   3517    }
   3518    return inXView && inYView;
   3519  }
   3520 
   3521  /**
   3522   * Converts  line/col to CM6 offset position
   3523   *
   3524   * @param {number} line - The line in the source
   3525   * @param {number} col - The column in the source
   3526   * @returns {number}
   3527   */
   3528  #positionToOffset(line, col = 0) {
   3529    const cm = editors.get(this);
   3530    try {
   3531      const offset = cm.state.doc.line(line);
   3532      return offset.from + col;
   3533    } catch (e) {
   3534      // Line likey does not exist in viewport yet
   3535      console.warn(e.message);
   3536    }
   3537    return null;
   3538  }
   3539 
   3540  /**
   3541   * This returns the line and column for the specified search cursor's position
   3542   *
   3543   * @param {RegExpSearchCursor} searchCursor
   3544   * @returns {object}
   3545   */
   3546  getPositionFromSearchCursor(searchCursor) {
   3547    const cm = editors.get(this);
   3548    const lineFrom = cm.state.doc.lineAt(searchCursor.from);
   3549    return {
   3550      line: lineFrom.number - 1,
   3551      ch: searchCursor.to - searchCursor.match[0].length - lineFrom.from,
   3552    };
   3553  }
   3554 
   3555  /**
   3556   * Scrolls the editor to the specified codemirror position
   3557   *
   3558   * @param {number} position
   3559   */
   3560  scrollToPosition(position) {
   3561    const cm = editors.get(this);
   3562    if (!this.config.cm6) {
   3563      throw new Error("This function is only compatible with CM6");
   3564    }
   3565    const {
   3566      codemirrorView: { EditorView },
   3567    } = this.#CodeMirror6;
   3568    return cm.dispatch({
   3569      effects: EditorView.scrollIntoView(position, {
   3570        x: "nearest",
   3571        y: "center",
   3572      }),
   3573    });
   3574  }
   3575 
   3576  /**
   3577   * Scrolls the editor to the specified line and column
   3578   *
   3579   * @param {number} line - The line in the source
   3580   * @param {number} column - The column in the source
   3581   * @param {string | null} yAlign - Optional value for position of the line after the line is scrolled.
   3582   *                               (Used by `scrollEditorIntoView` test helper)
   3583   */
   3584  async scrollTo(line, column, yAlign) {
   3585    if (this.isDestroyed()) {
   3586      return null;
   3587    }
   3588    const cm = editors.get(this);
   3589    if (this.config.cm6) {
   3590      const {
   3591        codemirrorView: { EditorView },
   3592      } = this.#CodeMirror6;
   3593 
   3594      if (!this.isPositionVisible(line, column)) {
   3595        const offset = this.#positionToOffset(line, column);
   3596        if (offset == null) {
   3597          return null;
   3598        }
   3599        return cm.dispatch({
   3600          effects: EditorView.scrollIntoView(offset, {
   3601            x: "center",
   3602            y: yAlign || "center",
   3603          }),
   3604        });
   3605      }
   3606    } else {
   3607      // For all cases where these are on the first line and column,
   3608      // avoid the possibly slow computation of cursor location on large bundles.
   3609      if (!line && !column) {
   3610        cm.scrollTo(0, 0);
   3611        return null;
   3612      }
   3613 
   3614      const { top, left } = cm.charCoords({ line, ch: column }, "local");
   3615 
   3616      if (!this.isPositionVisible(line, column)) {
   3617        const scroller = cm.getScrollerElement();
   3618        const centeredX = Math.max(left - scroller.offsetWidth / 2, 0);
   3619        const centeredY = Math.max(top - scroller.offsetHeight / 2, 0);
   3620 
   3621        return cm.scrollTo(centeredX, centeredY);
   3622      }
   3623    }
   3624    return null;
   3625  }
   3626 
   3627  // Used only in tests
   3628  setSelectionAt(start, end) {
   3629    const cm = editors.get(this);
   3630    if (this.config.cm6) {
   3631      const from = this.#positionToOffset(start.line, start.column);
   3632      const to = this.#positionToOffset(end.line, end.column);
   3633      if (from == null || to == null) {
   3634        return;
   3635      }
   3636      cm.dispatch({ selection: { anchor: from, head: to } });
   3637    } else {
   3638      cm.setSelection(
   3639        { line: start.line - 1, ch: start.column },
   3640        { line: end.line - 1, ch: end.column }
   3641      );
   3642    }
   3643  }
   3644 
   3645  /**
   3646   * Move CodeMirror cursor to a given location.
   3647   * This will also scroll the editor to the specified position.
   3648   * Used only for CM6
   3649   *
   3650   * @param {number} line
   3651   * @param {number} column
   3652   */
   3653  async setCursorAt(line, column) {
   3654    await this.scrollTo(line, column);
   3655    const cm = editors.get(this);
   3656    const { lines } = cm.state.doc;
   3657    if (line > lines) {
   3658      console.error(
   3659        `Trying to set the cursor on a non-existing line ${line} > ${lines}`
   3660      );
   3661      return null;
   3662    }
   3663    const lineInfo = cm.state.doc.line(line);
   3664    if (column >= lineInfo.length) {
   3665      console.error(
   3666        `Trying to set the cursor on a non-existing column ${column} >= ${lineInfo.length}`
   3667      );
   3668      return null;
   3669    }
   3670    const position = lineInfo.from + column;
   3671    return cm.dispatch({ selection: { anchor: position, head: position } });
   3672  }
   3673 
   3674  // Used only in tests
   3675  getEditorFileMode() {
   3676    const cm = editors.get(this);
   3677    if (this.config.cm6) {
   3678      return cm.contentDOM.dataset.language;
   3679    }
   3680    return cm.getOption("mode").name;
   3681  }
   3682 
   3683  // Used only in tests
   3684  getEditorContent() {
   3685    const cm = editors.get(this);
   3686    if (this.config.cm6) {
   3687      return cm.state.doc.toString();
   3688    }
   3689    return cm.getValue();
   3690  }
   3691 
   3692  isSearchStateReady() {
   3693    const cm = editors.get(this);
   3694    if (this.config.cm6) {
   3695      return !!this.searchState.cursors;
   3696    }
   3697    return !!cm.state.search;
   3698  }
   3699 
   3700  // Used only in tests
   3701  getCoords(line, column = 0) {
   3702    const cm = editors.get(this);
   3703    if (this.config.cm6) {
   3704      const offset = this.#positionToOffset(line, column);
   3705      if (offset == null) {
   3706        return null;
   3707      }
   3708      return cm.coordsAtPos(offset);
   3709    }
   3710    // CodeMirror is 0-based while line and column arguments are 1-based.
   3711    // Pass "column=-1" when there is no column argument passed.
   3712    return cm.charCoords({ line: ~~line, ch: ~~column });
   3713  }
   3714 
   3715  // Used only in tests
   3716  // Only used for CM6
   3717  getElementAtLine(line) {
   3718    const offset = this.#positionToOffset(line);
   3719    const el = this.#getElementAtOffset(offset);
   3720    return el.closest(".cm-line");
   3721  }
   3722 
   3723  // Used only in tests
   3724  getSearchQuery() {
   3725    const cm = editors.get(this);
   3726    if (this.config.cm6) {
   3727      return this.searchState.query.toString();
   3728    }
   3729    return cm.state.search.query;
   3730  }
   3731 
   3732  // Used only in tests
   3733  // Gets currently selected search term
   3734  getSearchSelection() {
   3735    const cm = editors.get(this);
   3736    if (this.config.cm6) {
   3737      const cursor =
   3738        this.searchState.cursors[this.searchState.currentCursorIndex];
   3739      if (!cursor) {
   3740        return { text: "", line: -1, column: -1 };
   3741      }
   3742 
   3743      const cursorPosition = lezerUtils.positionToLocation(
   3744        cm.state.doc,
   3745        cursor.to
   3746      );
   3747      // The lines in CM6 are 1 based
   3748      return {
   3749        text: cursor.match[0],
   3750        line: cursorPosition.line - 1,
   3751        column: cursorPosition.column,
   3752      };
   3753    }
   3754    const cursor = cm.getCursor();
   3755    return {
   3756      text: cm.getSelection(),
   3757      line: cursor.line,
   3758      column: cursor.ch,
   3759    };
   3760  }
   3761 
   3762  // Only used for CM6
   3763  getElementAtPos(line, column) {
   3764    const offset = this.#positionToOffset(line, column);
   3765    const el = this.#getElementAtOffset(offset);
   3766    return el;
   3767  }
   3768 
   3769  // Used only in tests
   3770  getLineCount() {
   3771    const cm = editors.get(this);
   3772    if (this.config.cm6) {
   3773      return cm.state.doc.lines;
   3774    }
   3775    return cm.lineCount();
   3776  }
   3777 
   3778  /**
   3779   * Extends an instance of the Editor object with additional
   3780   * functions. Each function will be called with context as
   3781   * the first argument. Context is a {ed, cm} object where
   3782   * 'ed' is an instance of the Editor object and 'cm' is an
   3783   * instance of the CodeMirror object. Example:
   3784   *
   3785   * function hello(ctx, name) {
   3786   *   let { cm, ed } = ctx;
   3787   *   cm;   // CodeMirror instance
   3788   *   ed;   // Editor instance
   3789   *   name; // 'Mozilla'
   3790   * }
   3791   *
   3792   * editor.extend({ hello: hello });
   3793   * editor.hello('Mozilla');
   3794   */
   3795  extend(funcs) {
   3796    Object.keys(funcs).forEach(name => {
   3797      const cm = editors.get(this);
   3798      const ctx = { ed: this, cm, Editor };
   3799 
   3800      if (name === "initialize") {
   3801        funcs[name](ctx);
   3802        return;
   3803      }
   3804 
   3805      this[name] = funcs[name].bind(null, ctx);
   3806    });
   3807  }
   3808 
   3809  isDestroyed() {
   3810    return !this.config || !editors.get(this);
   3811  }
   3812 
   3813  destroy() {
   3814    if (this.config.cm6 && this.#CodeMirror6) {
   3815      this.#clearEditorDOMEventListeners();
   3816    }
   3817    if (this.#abortController) {
   3818      this.#abortController.abort();
   3819      this.#abortController = null;
   3820    }
   3821    this.container = null;
   3822    this.config = null;
   3823    this.version = null;
   3824    this.#ownerDoc = null;
   3825    this.#updateListener = null;
   3826    this.#lineGutterMarkers.clear();
   3827    this.#lineContentMarkers.clear();
   3828    this.#scrollSnapshots.clear();
   3829    this.#languageModes.clear();
   3830    this.clearSources();
   3831 
   3832    if (this.#prefObserver) {
   3833      this.#prefObserver.off(KEYMAP_PREF, this.setKeyMap);
   3834      this.#prefObserver.off(TAB_SIZE, this.reloadPreferences);
   3835      this.#prefObserver.off(EXPAND_TAB, this.reloadPreferences);
   3836      this.#prefObserver.off(AUTO_CLOSE, this.reloadPreferences);
   3837      this.#prefObserver.off(AUTOCOMPLETE, this.reloadPreferences);
   3838      this.#prefObserver.off(DETECT_INDENT, this.reloadPreferences);
   3839      this.#prefObserver.off(ENABLE_CODE_FOLDING, this.reloadPreferences);
   3840      this.#prefObserver.destroy();
   3841    }
   3842 
   3843    // Remove the link between the document and code-mirror.
   3844    const cm = editors.get(this);
   3845    if (cm?.doc) {
   3846      cm.doc.cm = null;
   3847    }
   3848 
   3849    // Destroy the CM6 view
   3850    if (cm?.destroy) {
   3851      cm.destroy();
   3852    }
   3853    this.emit("destroy");
   3854  }
   3855 
   3856  updateCodeFoldingGutter() {
   3857    let shouldFoldGutter = this.config.enableCodeFolding;
   3858    const foldGutterIndex = this.config.gutters.indexOf(
   3859      "CodeMirror-foldgutter"
   3860    );
   3861    const cm = editors.get(this);
   3862 
   3863    if (shouldFoldGutter === undefined) {
   3864      shouldFoldGutter = Services.prefs.getBoolPref(ENABLE_CODE_FOLDING);
   3865    }
   3866 
   3867    if (shouldFoldGutter) {
   3868      // Add the gutter before enabling foldGutter
   3869      if (foldGutterIndex === -1) {
   3870        const gutters = this.config.gutters.slice();
   3871        gutters.push("CodeMirror-foldgutter");
   3872        this.setOption("gutters", gutters);
   3873      }
   3874 
   3875      this.setOption("foldGutter", true);
   3876    } else {
   3877      // No code should remain folded when folding is off.
   3878      if (cm) {
   3879        cm.execCommand("unfoldAll");
   3880      }
   3881 
   3882      // Remove the gutter so it doesn't take up space
   3883      if (foldGutterIndex !== -1) {
   3884        const gutters = this.config.gutters.slice();
   3885        gutters.splice(foldGutterIndex, 1);
   3886        this.setOption("gutters", gutters);
   3887      }
   3888 
   3889      this.setOption("foldGutter", false);
   3890    }
   3891  }
   3892 
   3893  /**
   3894   * Register all key shortcuts.
   3895   */
   3896  #initSearchShortcuts(win) {
   3897    const shortcuts = new KeyShortcuts({
   3898      window: win,
   3899    });
   3900    const keys = ["find.key", "findNext.key", "findPrev.key"];
   3901 
   3902    if (OS === "Darwin") {
   3903      keys.push("replaceAllMac.key");
   3904    } else {
   3905      keys.push("replaceAll.key");
   3906    }
   3907    // Process generic keys:
   3908    keys.forEach(name => {
   3909      const key = L10N.getStr(name);
   3910      shortcuts.on(key, event => this.#onSearchShortcut(name, event));
   3911    });
   3912  }
   3913  /**
   3914   * Key shortcut listener.
   3915   */
   3916  #onSearchShortcut = (name, event) => {
   3917    if (!this.#isInputOrTextarea(event.target)) {
   3918      return;
   3919    }
   3920    const node = event.originalTarget;
   3921 
   3922    switch (name) {
   3923      // replaceAll.key is Alt + find.key
   3924      case "replaceAllMac.key":
   3925        this.findOrReplace(node, true);
   3926        break;
   3927      // replaceAll.key is Shift + find.key
   3928      case "replaceAll.key":
   3929        this.findOrReplace(node, true);
   3930        break;
   3931      case "find.key":
   3932        this.findOrReplace(node, false);
   3933        break;
   3934      // findPrev.key is Shift + findNext.key
   3935      case "findPrev.key":
   3936        this.findNextOrPrev(node, true);
   3937        break;
   3938      case "findNext.key":
   3939        this.findNextOrPrev(node, false);
   3940        break;
   3941      default:
   3942        console.error("Unexpected editor key shortcut", name);
   3943        return;
   3944    }
   3945    // Prevent default for this action
   3946    event.stopPropagation();
   3947    event.preventDefault();
   3948  };
   3949 
   3950  /**
   3951   * Check if a node is an input or textarea
   3952   */
   3953  #isInputOrTextarea(element) {
   3954    const name = element.tagName.toLowerCase();
   3955    return name === "input" || name === "textarea";
   3956  }
   3957 
   3958  /**
   3959   * Parse passed code string and returns an HTML string with the same classes CodeMirror
   3960   * adds to handle syntax highlighting.
   3961   *
   3962   * @param {Document} doc: A document that will be used to create elements
   3963   * @param {string} code: The code to highlight
   3964   * @returns {string} The HTML string for the parsed code
   3965   */
   3966  highlightText(doc, code) {
   3967    if (!doc) {
   3968      return code;
   3969    }
   3970 
   3971    const outputNode = doc.createElement("div");
   3972    if (!this.config.cm6) {
   3973      this.CodeMirror.runMode(code, "application/javascript", outputNode);
   3974    } else {
   3975      const { codemirrorLangJavascript, lezerHighlight } = this.#CodeMirror6;
   3976      const { highlightCode, classHighlighter } = lezerHighlight;
   3977 
   3978      function emit(text, classes) {
   3979        const textNode = doc.createTextNode(text);
   3980        if (classes) {
   3981          const span = doc.createElement("span");
   3982          span.appendChild(textNode);
   3983          span.className = classes;
   3984          outputNode.appendChild(span);
   3985        } else {
   3986          outputNode.appendChild(textNode);
   3987        }
   3988      }
   3989      function emitBreak() {
   3990        outputNode.appendChild(doc.createTextNode("\n"));
   3991      }
   3992 
   3993      highlightCode(
   3994        code,
   3995        codemirrorLangJavascript.javascriptLanguage.parser.parse(code),
   3996        classHighlighter,
   3997        emit,
   3998        emitBreak
   3999      );
   4000    }
   4001    return outputNode.innerHTML;
   4002  }
   4003 
   4004  /**
   4005   * Focus the CodeMirror editor
   4006   */
   4007  focus() {
   4008    const cm = editors.get(this);
   4009    cm.focus();
   4010  }
   4011 
   4012  /**
   4013   * Select the whole document
   4014   */
   4015  selectAll() {
   4016    const cm = editors.get(this);
   4017    if (this.config.cm6) {
   4018      cm.dispatch({
   4019        selection: { anchor: 0, head: cm.state.doc.length },
   4020        userEvent: "select",
   4021      });
   4022    } else {
   4023      cm.execCommand("selectAll");
   4024    }
   4025  }
   4026 }
   4027 
   4028 // Since Editor is a thin layer over CodeMirror some methods
   4029 // are mapped directly—without any changes.
   4030 
   4031 CM_MAPPING.forEach(name => {
   4032  Editor.prototype[name] = function (...args) {
   4033    const cm = editors.get(this);
   4034    return cm[name].apply(cm, args);
   4035  };
   4036 });
   4037 
   4038 /**
   4039 * We compute the CSS property names, values, and color names to be used with
   4040 * CodeMirror to more closely reflect what is supported by the target platform.
   4041 * The database is used to replace the values used in CodeMirror while initiating
   4042 * an editor object. This is done here instead of the file codemirror/css.js so
   4043 * as to leave that file untouched and easily upgradable.
   4044 */
   4045 function getCSSKeywords(cssProperties) {
   4046  function keySet(array) {
   4047    const keys = {};
   4048    for (let i = 0; i < array.length; ++i) {
   4049      keys[array[i]] = true;
   4050    }
   4051    return keys;
   4052  }
   4053 
   4054  const propertyKeywords = cssProperties.getNames();
   4055  const colorKeywords = {};
   4056  const valueKeywords = {};
   4057 
   4058  propertyKeywords.forEach(property => {
   4059    if (property.includes("color")) {
   4060      cssProperties.getValues(property).forEach(value => {
   4061        colorKeywords[value] = true;
   4062      });
   4063    } else {
   4064      cssProperties.getValues(property).forEach(value => {
   4065        valueKeywords[value] = true;
   4066      });
   4067    }
   4068  });
   4069 
   4070  return {
   4071    propertyKeywords: keySet(propertyKeywords),
   4072    colorKeywords,
   4073    valueKeywords,
   4074  };
   4075 }
   4076 
   4077 module.exports = Editor;