Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quellverzeichnis  editor.js   Sprache: JAVA

 
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */


"use strict";

const {
  EXPAND_TAB,
  TAB_SIZE,
  DETECT_INDENT,
  getIndentationFromIteration,
} = require("resource://devtools/shared/indentation.js");

const { debounce } = require("resource://devtools/shared/debounce.js");
const nodeConstants = require("resource://devtools/shared/dom-node-constants.js");

const ENABLE_CODE_FOLDING = "devtools.editor.enableCodeFolding";
const KEYMAP_PREF = "devtools.editor.keymap";
const AUTO_CLOSE = "devtools.editor.autoclosebrackets";
const AUTOCOMPLETE = "devtools.editor.autocomplete";
const CARET_BLINK_TIME = "ui.caretBlinkTime";
const XHTML_NS = "http://www.w3.org/1999/xhtml";

const VALID_KEYMAPS = new Map([
  [
    "emacs",
    "chrome://devtools/content/shared/sourceeditor/codemirror/keymap/emacs.js",
  ],
  [
    "vim",
    "chrome://devtools/content/shared/sourceeditor/codemirror/keymap/vim.js",
  ],
  [
    "sublime",
    "chrome://devtools/content/shared/sourceeditor/codemirror/keymap/sublime.js",
  ],
]);

// Maximum allowed margin (in number of lines) from top or bottom of the editor
// while shifting to a line which was initially out of view.
const MAX_VERTICAL_OFFSET = 3;

const RE_JUMP_TO_LINE = /^(\d+):?(\d+)?/;
const AUTOCOMPLETE_MARK_CLASSNAME = "cm-auto-complete-shadow-text";

const EventEmitter = require("resource://devtools/shared/event-emitter.js");
const { PrefObserver } = require("resource://devtools/client/shared/prefs.js");
const KeyShortcuts = require("resource://devtools/client/shared/key-shortcuts.js");

const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
const L10N = new LocalizationHelper(
  "devtools/client/locales/sourceeditor.properties"
);

loader.lazyRequireGetter(
  this,
  "wasm",
  "resource://devtools/client/shared/sourceeditor/wasm.js"
);

const { OS } = Services.appinfo;

// CM_BUNDLE and CM_IFRAME represent the HTML and JavaScript that is
// injected into an iframe in order to initialize a CodeMirror instance.

const CM_BUNDLE =
  "chrome://devtools/content/shared/sourceeditor/codemirror/codemirror.bundle.js";

const CM_IFRAME =
  "chrome://devtools/content/shared/sourceeditor/codemirror/cmiframe.html";

const CM_MAPPING = [
  "clearHistory",
  "defaultCharWidth",
  "extendSelection",
  "focus",
  "getCursor",
  "getLine",
  "getScrollInfo",
  "getSelection",
  "getViewport",
  "hasFocus",
  "lineCount",
  "openDialog",
  "redo",
  "refresh",
  "replaceSelection",
  "setSelection",
  "somethingSelected",
  "undo",
];

const editors = new WeakMap();

/**
 * A very thin wrapper around CodeMirror. Provides a number
 * of helper methods to make our use of CodeMirror easier and
 * another method, appendTo, to actually create and append
 * the CodeMirror instance.
 *
 * Note that Editor doesn't expose CodeMirror instance to the
 * outside world.
 *
 * Constructor accepts one argument, config. It is very
 * similar to the CodeMirror configuration object so for most
 * properties go to CodeMirror's documentation (see below).
 *
 * Other than that, it accepts one additional and optional
 * property contextMenu. This property should be an element, or
 * an ID of an element that we can use as a context menu.
 *
 * This object is also an event emitter.
 *
 * CodeMirror docs: http://codemirror.net/doc/manual.html
 */

class Editor extends EventEmitter {
  // Static methods on the Editor object itself.

  /**
   * Returns a string representation of a shortcut 'key' with
   * a OS specific modifier. Cmd- for Macs, Ctrl- for other
   * platforms. Useful with extraKeys configuration option.
   *
   * CodeMirror defines all keys with modifiers in the following
   * order: Shift - Ctrl/Cmd - Alt - Key
   */

  static accel(key, modifiers = {}) {
    return (
      (modifiers.shift ? "Shift-" : "") +
      (Services.appinfo.OS == "Darwin" ? "Cmd-" : "Ctrl-") +
      (modifiers.alt ? "Alt-" : "") +
      key
    );
  }

  /**
   * Returns a string representation of a shortcut for a
   * specified command 'cmd'. Append Cmd- for macs, Ctrl- for other
   * platforms unless noaccel is specified in the options. Useful when overwriting
   * or disabling default shortcuts.
   */

  static keyFor(cmd, opts = { noaccel: false }) {
    const key = L10N.getStr(cmd + ".commandkey");
    return opts.noaccel ? key : Editor.accel(key);
  }

  static modes = {
    cljs: { name: "text/x-clojure" },
    css: { name: "css" },
    fs: { name: "x-shader/x-fragment" },
    haxe: { name: "haxe" },
    http: { name: "http" },
    html: { name: "htmlmixed" },
    js: { name: "javascript" },
    text: { name: "text" },
    vs: { name: "x-shader/x-vertex" },
    wasm: { name: "wasm" },
  };

  container = null;
  version = null;
  config = null;
  Doc = null;
  searchState = {
    cursors: [],
    currentCursorIndex: -1,
    query: "",
  };

  #abortController;
  // The id for the current source in the editor (selected source). This
  // is used to cache the scroll snapshot for tracking scroll positions and the
  // symbols.
  #currentDocumentId = null;
  #currentDocument = null;
  #CodeMirror6;
  #compartments;
  #effects;
  #lastDirty;
  #loadedKeyMaps;
  #ownerDoc;
  #prefObserver;
  #win;
  #lineGutterMarkers = new Map();
  #lineContentMarkers = new Map();
  #posContentMarkers = new Map();
  #editorDOMEventHandlers = {};
  #gutterDOMEventHandlers = {};
  // A cache of all the scroll snapshots for the all the sources that
  // are currently open in the editor. The keys for the Map are the id's
  // for the source and the values are the scroll snapshots for the sources.
  #scrollSnapshots = new Map();
  #updateListener = null;

  constructor(config) {
    super();

    const tabSize = Services.prefs.getIntPref(TAB_SIZE);
    const useTabs = !Services.prefs.getBoolPref(EXPAND_TAB);
    const useAutoClose = Services.prefs.getBoolPref(AUTO_CLOSE);

    this.version = null;
    this.config = {
      cm6: false,
      value: "",
      mode: Editor.modes.text,
      indentUnit: tabSize,
      tabSize,
      contextMenu: null,
      matchBrackets: true,
      highlightSelectionMatches: {
        wordsOnly: true,
      },
      extraKeys: {},
      indentWithTabs: useTabs,
      inputStyle: "accessibleTextArea",
      // This is set to the biggest value for setTimeout (See https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout#Maximum_delay_value)
      // This is because codeMirror queries the underlying textArea for some things that
      // can't be retrieved with events in some browser (but we're fine in Firefox).
      pollInterval: Math.pow(2, 31) - 1,
      styleActiveLine: true,
      autoCloseBrackets: "()[]{}''\"\"``",
      autoCloseEnabled: useAutoClose,
      theme: "mozilla",
      themeSwitching: true,
      autocomplete: false,
      autocompleteOpts: {},
      // Expect a CssProperties object (see devtools/client/fronts/css-properties.js)
      cssProperties: null,
      // Set to true to prevent the search addon to be activated.
      disableSearchAddon: false,
      maxHighlightLength: 1000,
      // Disable codeMirror setTimeout-based cursor blinking (will be replaced by a CSS animation)
      cursorBlinkRate: 0,
      // List of non-printable chars that will be displayed in the editor, showing their
      // unicode version. We only add a few characters to the default list:
      // - \u202d LEFT-TO-RIGHT OVERRIDE
      // - \u202e RIGHT-TO-LEFT OVERRIDE
      // - \u2066 LEFT-TO-RIGHT ISOLATE
      // - \u2067 RIGHT-TO-LEFT ISOLATE
      // - \u2069 POP DIRECTIONAL ISOLATE
      specialChars:
        // eslint-disable-next-line no-control-regex
        /[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b-\u200f\u2028\u2029\u202d\u202e\u2066\u2067\u2069\ufeff\ufff9-\ufffc]/,
      specialCharPlaceholder: char => {
        // Use the doc provided to the setup function if we don't have a reference to a codeMirror
        // editor yet (this can happen when an Editor is being created with existing content)
        const doc = this.#ownerDoc;
        const el = doc.createElement("span");
        el.classList.add("cm-non-printable-char");
        el.append(doc.createTextNode(`\\u${char.codePointAt(0).toString(16)}`));
        return el;
      },
      // In CodeMirror 5, adds a `CodeMirror-selectedtext` class on selected text that
      // can be used to set the selected text color, which isn't possible by default.
      // This is especially useful for High Contrast Mode where we do need to adjust the
      // selection text color
      styleSelectedText: true,
    };

    // Additional shortcuts.
    this.config.extraKeys[Editor.keyFor("jumpToLine")] = () =>
      this.jumpToLine();
    this.config.extraKeys[Editor.keyFor("moveLineUp", { noaccel: true })] =
      () => this.moveLineUp();
    this.config.extraKeys[Editor.keyFor("moveLineDown", { noaccel: true })] =
      () => this.moveLineDown();
    this.config.extraKeys[Editor.keyFor("toggleComment")] = "toggleComment";

    // Disable ctrl-[ and ctrl-] because toolbox uses those shortcuts.
    this.config.extraKeys[Editor.keyFor("indentLess")] = false;
    this.config.extraKeys[Editor.keyFor("indentMore")] = false;

    // Disable Alt-B and Alt-F to navigate groups (respectively previous and next) since:
    // - it's not standard in input fields
    // - it also inserts a character which feels weird
    this.config.extraKeys["Alt-B"] = false;
    this.config.extraKeys["Alt-F"] = false;

    // Disable Ctrl/Cmd + U as it's used for "View Source". It's okay to disable Ctrl+U as
    // the underlying command, `undoSelection`, isn't standard in input fields and isn't
    // widely known.
    this.config.extraKeys[Editor.accel("U")] = false;

    // Disable keys that trigger events with a null-string `which` property.
    // It looks like some of those (e.g. the Function key), can trigger a poll
    // which fails to see that there's a selection, which end up replacing the
    // selected text with an empty string.
    // TODO: We should investigate the root cause.
    this.config.extraKeys["'\u0000'"] = false;

    // Overwrite default config with user-provided, if needed.
    Object.keys(config).forEach(k => {
      if (k != "extraKeys") {
        this.config[k] = config[k];
        return;
      }

      if (!config.extraKeys) {
        return;
      }

      Object.keys(config.extraKeys).forEach(key => {
        this.config.extraKeys[key] = config.extraKeys[key];
      });
    });

    if (!this.config.gutters) {
      this.config.gutters = [];
    }
    if (
      this.config.lineNumbers &&
      !this.config.gutters.includes("CodeMirror-linenumbers")
    ) {
      this.config.gutters.push("CodeMirror-linenumbers");
    }

    // Remember the initial value of autoCloseBrackets.
    this.config.autoCloseBracketsSaved = this.config.autoCloseBrackets;

    // If the tab behaviour is not explicitly set to `false` from the config, set a tab behavior.
    // If something is selected, indent those lines. If nothing is selected and we're
    // indenting with tabs, insert one tab. Otherwise insert N
    // whitespaces where N == indentUnit option.
    if (this.config.extraKeys.Tab !== false) {
      this.config.extraKeys.Tab = cm => {
        if (config.extraKeys?.Tab) {
          // If a consumer registers its own extraKeys.Tab, we execute it before doing
          // anything else. If it returns false, that mean that all the key handling work is
          // done, so we can do an early return.
          const res = config.extraKeys.Tab(cm);
          if (res === false) {
            return;
          }
        }

        if (cm.somethingSelected()) {
          cm.indentSelection("add");
          return;
        }

        if (this.config.indentWithTabs) {
          cm.replaceSelection("\t""end""+input");
          return;
        }

        let num = cm.getOption("indentUnit");
        if (cm.getCursor().ch !== 0) {
          num -= cm.getCursor().ch % num;
        }
        cm.replaceSelection(" ".repeat(num), "end""+input");
      };

      if (this.config.cssProperties) {
        // Ensure that autocompletion has cssProperties if it's passed in via the options.
        this.config.autocompleteOpts.cssProperties = this.config.cssProperties;
      }
    }
  }

  /**
   * Exposes the CodeMirror class. We want to be able to
   * invoke static commands such as runMode for syntax highlighting.
   */

  get CodeMirror() {
    const codeMirror = editors.get(this);
    return codeMirror?.constructor;
  }

  /**
   * Exposes the CodeMirror instance. We want to get away from trying to
   * abstract away the API entirely, and this makes it easier to integrate in
   * various environments and do complex things.
   */

  get codeMirror() {
    if (!editors.has(this)) {
      throw new Error(
        "CodeMirror instance does not exist. You must wait " +
          "for it to be appended to the DOM."
      );
    }
    return editors.get(this);
  }

  /**
   * Return whether there is a CodeMirror instance associated with this Editor.
   */

  get hasCodeMirror() {
    return editors.has(this);
  }

  /**
   * Appends the current Editor instance to the element specified by
   * 'el'. You can also provide your own iframe to host the editor as
   * an optional second parameter. This method actually creates and
   * loads CodeMirror and all its dependencies.
   *
   * This method is asynchronous and returns a promise.
   */

  appendTo(el, env) {
    return new Promise(resolve => {
      const cm = editors.get(this);

      if (!env) {
        env = el.ownerDocument.createElementNS(XHTML_NS, "iframe");
        env.className = "source-editor-frame";
      }

      if (cm) {
        throw new Error("You can append an editor only once.");
      }

      const onLoad = () => {
        // Prevent flickering by showing the iframe once loaded.
        // See https://github.com/w3c/csswg-drafts/issues/9624
        env.style.visibility = "";
        const win = env.contentWindow.wrappedJSObject;
        this.container = env;

        const editorEl = win.document.body;
        const editorDoc = el.ownerDocument;
        if (this.config.cm6) {
          this.#setupCm6(editorEl, editorDoc);
        } else {
          this.#setup(editorEl, editorDoc);
        }
        resolve();
      };

      env.style.visibility = "hidden";
      env.addEventListener("load", onLoad, {
        capture: true,
        once: true,
        signal: this.#abortController?.signal,
      });
      env.src = CM_IFRAME;
      el.appendChild(env);

      this.once("destroy", () => el.removeChild(env));
    });
  }

  appendToLocalElement(el) {
    const win = el.ownerDocument.defaultView;
    this.#abortController = new win.AbortController();
    if (this.config.cm6) {
      this.#setupCm6(el);
    } else {
      this.#setup(el);
    }
  }

  // This update listener allows listening to the changes
  // to the codemiror editor.
  setUpdateListener(listener = null) {
    this.#updateListener = listener;
  }

  /**
   * Do the actual appending and configuring of the CodeMirror instance. This is
   * used by both append functions above, and does all the hard work to
   * configure CodeMirror with all the right options/modes/etc.
   */

  #setup(el, doc) {
    this.#ownerDoc = doc || el.ownerDocument;
    const win = el.ownerDocument.defaultView;

    Services.scriptloader.loadSubScript(CM_BUNDLE, win);
    this.#win = win;

    if (this.config.cssProperties) {
      // Replace the propertyKeywords, colorKeywords and valueKeywords
      // properties of the CSS MIME type with the values provided by the CSS properties
      // database.
      const { propertyKeywords, colorKeywords, valueKeywords } = getCSSKeywords(
        this.config.cssProperties
      );

      const cssSpec = win.CodeMirror.resolveMode("text/css");
      cssSpec.propertyKeywords = propertyKeywords;
      cssSpec.colorKeywords = colorKeywords;
      cssSpec.valueKeywords = valueKeywords;
      win.CodeMirror.defineMIME("text/css", cssSpec);

      const scssSpec = win.CodeMirror.resolveMode("text/x-scss");
      scssSpec.propertyKeywords = propertyKeywords;
      scssSpec.colorKeywords = colorKeywords;
      scssSpec.valueKeywords = valueKeywords;
      win.CodeMirror.defineMIME("text/x-scss", scssSpec);
    }

    win.CodeMirror.commands.save = () => this.emit("saveRequested");

    // Create a CodeMirror instance add support for context menus,
    // overwrite the default controller (otherwise items in the top and
    // context menus won't work).

    const cm = win.CodeMirror(el, this.config);
    this.Doc = win.CodeMirror.Doc;

    // Disable APZ for source editors. It currently causes the line numbers to
    // "tear off" and swim around on top of the content. Bug 1160601 tracks
    // finding a solution that allows APZ to work with CodeMirror.
    cm.getScrollerElement().addEventListener(
      "wheel",
      ev => {
        // By handling the wheel events ourselves, we force the platform to
        // scroll synchronously, like it did before APZ. However, we lose smooth
        // scrolling for users with mouse wheels. This seems acceptible vs.
        // doing nothing and letting the gutter slide around.
        ev.preventDefault();

        let { deltaX, deltaY } = ev;

        if (ev.deltaMode == ev.DOM_DELTA_LINE) {
          deltaX *= cm.defaultCharWidth();
          deltaY *= cm.defaultTextHeight();
        } else if (ev.deltaMode == ev.DOM_DELTA_PAGE) {
          deltaX *= cm.getWrapperElement().clientWidth;
          deltaY *= cm.getWrapperElement().clientHeight;
        }

        cm.getScrollerElement().scrollBy(deltaX, deltaY);
      },
      { signal: this.#abortController?.signal }
    );

    cm.getWrapperElement().addEventListener(
      "contextmenu",
      ev => {
        if (!this.config.contextMenu) {
          return;
        }

        ev.stopPropagation();
        ev.preventDefault();

        let popup = this.config.contextMenu;
        if (typeof popup == "string") {
          popup = this.#ownerDoc.getElementById(this.config.contextMenu);
        }

        this.emit("popupOpen", ev, popup);
        popup.openPopupAtScreen(ev.screenX, ev.screenY, true);
      },
      { signal: this.#abortController?.signal }
    );

    const pipedEvents = [
      "beforeChange",
      "blur",
      "changes",
      "cursorActivity",
      "focus",
      "keyHandled",
      "scroll",
    ];
    for (const eventName of pipedEvents) {
      cm.on(eventName, (...args) => this.emit(eventName, ...args));
    }

    cm.on("change", () => {
      this.emit("change");
      if (!this.#lastDirty) {
        this.#lastDirty = true;
        this.emit("dirty-change");
      }
    });

    cm.on("gutterClick", (cmArg, line, gutter, ev) => {
      const lineOrOffset = !this.isWasm ? line : this.lineToWasmOffset(line);
      this.emit("gutterClick", lineOrOffset, ev.button);
    });

    win.CodeMirror.defineExtension("l10n", name => {
      return L10N.getStr(name);
    });

    if (!this.config.disableSearchAddon) {
      this.#initSearchShortcuts(win);
    } else {
      // Hotfix for Bug 1527898. We should remove those overrides as part of Bug 1527903.
      Object.assign(win.CodeMirror.commands, {
        find: null,
        findPersistent: null,
        findPersistentNext: null,
        findPersistentPrev: null,
        findNext: null,
        findPrev: null,
        clearSearch: null,
        replace: null,
        replaceAll: null,
      });
    }

    // Retrieve the cursor blink rate from user preference, or fall back to CodeMirror's
    // default value.
    let cursorBlinkingRate = win.CodeMirror.defaults.cursorBlinkRate;
    if (Services.prefs.prefHasUserValue(CARET_BLINK_TIME)) {
      cursorBlinkingRate = Services.prefs.getIntPref(
        CARET_BLINK_TIME,
        cursorBlinkingRate
      );
    }
    // This will be used in the animation-duration property we set on the cursor to
    // implement the blinking animation. If cursorBlinkingRate is 0 or less, the cursor
    // won't blink.
    cm.getWrapperElement().style.setProperty(
      "--caret-blink-time",
      `${Math.max(0, cursorBlinkingRate)}ms`
    );

    editors.set(this, cm);

    this.reloadPreferences = this.reloadPreferences.bind(this);
    this.setKeyMap = this.setKeyMap.bind(this, win);

    this.#prefObserver = new PrefObserver("devtools.editor.");
    this.#prefObserver.on(TAB_SIZE, this.reloadPreferences);
    this.#prefObserver.on(EXPAND_TAB, this.reloadPreferences);
    this.#prefObserver.on(AUTO_CLOSE, this.reloadPreferences);
    this.#prefObserver.on(AUTOCOMPLETE, this.reloadPreferences);
    this.#prefObserver.on(DETECT_INDENT, this.reloadPreferences);
    this.#prefObserver.on(ENABLE_CODE_FOLDING, this.reloadPreferences);

    this.reloadPreferences();

    // Init a map of the loaded keymap files. Should be of the form Map<String->Boolean>.
    this.#loadedKeyMaps = new Set();
    this.#prefObserver.on(KEYMAP_PREF, this.setKeyMap);
    this.setKeyMap();

    win.editor = this;
    const editorReadyEvent = new win.CustomEvent("editorReady");
    win.dispatchEvent(editorReadyEvent);
  }

  /**
   * Do the actual appending and configuring of the CodeMirror 6 instance.
   * This is used by appendTo and appendToLocalElement, and does all the hard work to
   * configure CodeMirror 6 with all the right options/modes/etc.
   * This should be kept in sync with #setup.
   *
   * @param {Element} el: Element into which the codeMirror editor should be appended.
   * @param {Document} document: Optional document, if not set, will default to el.ownerDocument
   */

  #setupCm6(el, doc) {
    this.#ownerDoc = doc || el.ownerDocument;
    const win = el.ownerDocument.defaultView;
    this.#win = win;

    this.#CodeMirror6 = this.#win.ChromeUtils.importESModule(
      "resource://devtools/client/shared/sourceeditor/codemirror6/codemirror6.bundle.mjs",
      { global: "current" }
    );

    const {
      codemirror,
      codemirrorView: {
        drawSelection,
        EditorView,
        keymap,
        lineNumbers,
        placeholder,
      },
      codemirrorState: { EditorState, Compartment, Prec },
      codemirrorSearch: { highlightSelectionMatches },
      codemirrorLanguage: {
        syntaxTreeAvailable,
        indentUnit,
        codeFolding,
        syntaxHighlighting,
        bracketMatching,
      },
      codemirrorLangJavascript,
      lezerHighlight,
    } = this.#CodeMirror6;

    const tabSizeCompartment = new Compartment();
    const indentCompartment = new Compartment();
    const lineWrapCompartment = new Compartment();
    const lineNumberCompartment = new Compartment();
    const lineNumberMarkersCompartment = new Compartment();
    const searchHighlightCompartment = new Compartment();
    const domEventHandlersCompartment = new Compartment();
    const foldGutterCompartment = new Compartment();

    this.#compartments = {
      tabSizeCompartment,
      indentCompartment,
      lineWrapCompartment,
      lineNumberCompartment,
      lineNumberMarkersCompartment,
      searchHighlightCompartment,
      domEventHandlersCompartment,
      foldGutterCompartment,
    };

    const { lineContentMarkerEffect, lineContentMarkerExtension } =
      this.#createlineContentMarkersExtension();

    const { positionContentMarkerEffect, positionContentMarkerExtension } =
      this.#createPositionContentMarkersExtension();

    this.#effects = { lineContentMarkerEffect, positionContentMarkerEffect };

    const indentStr = (this.config.indentWithTabs ? "\t" : " ").repeat(
      this.config.indentUnit || 2
    );

    // Track the scroll snapshot for the current document at the end of the scroll
    this.#editorDOMEventHandlers.scroll = [
      debounce(this.#cacheScrollSnapshot, 250),
    ];

    const extensions = [
      bracketMatching(),
      indentCompartment.of(indentUnit.of(indentStr)),
      tabSizeCompartment.of(EditorState.tabSize.of(this.config.tabSize)),
      lineWrapCompartment.of(
        this.config.lineWrapping ? EditorView.lineWrapping : []
      ),
      EditorState.readOnly.of(this.config.readOnly),
      lineNumberCompartment.of(this.config.lineNumbers ? lineNumbers() : []),
      codeFolding({
        placeholderText: "↔",
      }),
      foldGutterCompartment.of([]),
      syntaxHighlighting(lezerHighlight.classHighlighter),
      EditorView.updateListener.of(v => {
        if (!cm.isDocumentLoadComplete) {
          // Check that the full syntax tree is available the current viewport
          if (syntaxTreeAvailable(v.state, v.view.viewState.viewport.to)) {
            cm.isDocumentLoadComplete = true;
          }
        }
        if (v.viewportChanged || v.docChanged) {
          if (v.docChanged) {
            cm.isDocumentLoadComplete = false;
          }
          // reset line gutter markers for the new visible ranges
          // when the viewport changes(e.g when the page is scrolled).
          if (this.#lineGutterMarkers.size > 0) {
            this.setLineGutterMarkers();
          }
        }
        // Any custom defined update listener should be called
        if (typeof this.#updateListener == "function") {
          this.#updateListener(v);
        }
      }),
      domEventHandlersCompartment.of(
        EditorView.domEventHandlers(this.#createEventHandlers())
      ),
      lineNumberMarkersCompartment.of([]),
      lineContentMarkerExtension,
      positionContentMarkerExtension,
      searchHighlightCompartment.of(this.#searchHighlighterExtension([])),
      highlightSelectionMatches(),
      // keep last so other extension take precedence
      codemirror.minimalSetup,
    ];

    if (this.config.mode === Editor.modes.js) {
      extensions.push(codemirrorLangJavascript.javascript());
    }

    if (this.config.placeholder) {
      extensions.push(placeholder(this.config.placeholder));
    }

    if (this.config.keyMap) {
      extensions.push(Prec.highest(keymap.of(this.config.keyMap)));
    }

    if (Services.prefs.prefHasUserValue(CARET_BLINK_TIME)) {
      // We need to multiply the preference value by 2 to match Firefox cursor rate
      const cursorBlinkRate = Services.prefs.getIntPref(CARET_BLINK_TIME) * 2;
      extensions.push(
        drawSelection({
          cursorBlinkRate,
        })
      );
    }

    const cm = new EditorView({
      parent: el,
      extensions,
    });

    cm.isDocumentLoadComplete = false;
    editors.set(this, cm);

    // For now, we only need to pipe the blur event
    cm.contentDOM.addEventListener("blur", e => this.emit("blur", e), {
      signal: this.#abortController?.signal,
    });
  }

  /**
   * This creates the extension which handles marking of lines within the editor.
   *
   * @returns {Object} The object contains an extension and effects which used to trigger updates to the extension
   *          {Object} - lineContentMarkerExtension - The line content marker extension
   *          {Object} - lineContentMarkerEffect - The effects to add and remove markers
   *
   */

  #createlineContentMarkersExtension() {
    const {
      codemirrorView: { Decoration, WidgetType, EditorView },
      codemirrorState: { StateField, StateEffect },
    } = this.#CodeMirror6;

    const lineContentMarkers = this.#lineContentMarkers;

    class LineContentWidget extends WidgetType {
      constructor(line, value, markerId, createElementNode) {
        super();
        this.line = line;
        this.value = value;
        this.markerId = markerId;
        this.createElementNode = createElementNode;
      }

      toDOM() {
        return this.createElementNode(this.line, this.value);
      }

      eq(widget) {
        return (
          widget.line == this.line &&
          widget.markerId == this.markerId &&
          widget.value == this.value
        );
      }
    }

    /**
     * Uses the marker and current decoration list to create a new decoration list
     *
     * @param {Object} marker - The marker to be used to create the new decoration
     * @param {Transaction} transaction - The transaction object
     * @param {Array} newMarkerDecorations - List of the new marker decorations being built
     */

    function _buildDecorationsForMarker(
      marker,
      transaction,
      newMarkerDecorations
    ) {
      const vStartLine = transaction.state.doc.lineAt(
        marker._view.viewport.from
      );
      const vEndLine = transaction.state.doc.lineAt(marker._view.viewport.to);

      let decorationLines;
      if (marker.shouldMarkAllLines) {
        decorationLines = [];
        for (let i = vStartLine.number; i <= vEndLine.number; i++) {
          decorationLines.push({ line: i });
        }
      } else {
        decorationLines = marker.lines;
      }

      for (const { line, value } of decorationLines) {
        // Make sure the position is within the viewport
        if (line < vStartLine.number || line > vEndLine.number) {
          continue;
        }

        const lo = transaction.state.doc.line(line);
        if (marker.lineClassName) {
          // Markers used:
          // 1) blackboxed-line-marker
          // 2) multi-highlight-line-marker
          // 3) highlight-line-marker
          // 4) line-exception-marker
          // 5) debug-line-marker
          const classDecoration = Decoration.line({
            class: marker.lineClassName,
          });
          classDecoration.markerType = marker.id;
          newMarkerDecorations.push(classDecoration.range(lo.from));
        } else if (marker.createLineElementNode) {
          // Markers used:
          // 1) conditional-breakpoint-panel-marker
          // 2) inline-preview-marker
          const nodeDecoration = Decoration.widget({
            widget: new LineContentWidget(
              line,
              value,
              marker.id,
              marker.createLineElementNode
            ),
            // Render the widget after the cursor
            side: 1,
            block: !!marker.renderAsBlock,
          });
          nodeDecoration.markerType = marker.id;
          newMarkerDecorations.push(nodeDecoration.range(lo.to));
        }
      }
    }

    /**
     * This updates the decorations for the marker specified
     *
     * @param {Array} markerDecorations - The current decorations displayed in the document
     * @param {Array} marker - The current marker whose decoration should be update
     * @param {Transaction} transaction
     * @returns
     */

    function updateDecorations(markerDecorations, marker, transaction) {
      const newDecorations = [];
      _buildDecorationsForMarker(marker, transaction, newDecorations);

      return markerDecorations.update({
        // Filter out old decorations for the specified marker
        filter: (from, to, decoration) => {
          return decoration.markerType !== marker.id;
        },
        add: newDecorations,
        sort: true,
      });
    }

    /**
     * This updates all the decorations for all the markers. This
     * used in scenarios when an update to view (e.g vertically scrolling into a new viewport)
     * requires all the marker decoraions.
     *
     * @param {Array} markerDecorations - The current decorations displayed in the document
     * @param {Array} allMarkers - All the cached markers
     * @param {Object} transaction
     * @returns
     */

    function updateDecorationsForAllMarkers(
      markerDecorations,
      allMarkers,
      transaction
    ) {
      const allNewDecorations = [];

      for (const marker of allMarkers) {
        _buildDecorationsForMarker(marker, transaction, allNewDecorations);
      }

      return markerDecorations.update({
        // This filters out all the old decorations
        filter: () => false,
        add: allNewDecorations,
        sort: true,
      });
    }

    function removeDecorations(markerDecorations, markerId) {
      return markerDecorations.update({
        filter: (from, to, decoration) => {
          return decoration.markerType !== markerId;
        },
      });
    }

    // The effects used to create the transaction when markers are
    // either added and removed.
    const addEffect = StateEffect.define();
    const removeEffect = StateEffect.define();

    const lineContentMarkerExtension = StateField.define({
      create() {
        return Decoration.none;
      },
      update(markerDecorations, transaction) {
        // Map the decorations through the transaction changes, this is important
        // as it remaps the decorations from positions in the old document to
        // positions in the new document.
        markerDecorations = markerDecorations.map(transaction.changes);
        for (const effect of transaction.effects) {
          // When a new marker is added
          if (effect.is(addEffect)) {
            markerDecorations = updateDecorations(
              markerDecorations,
              effect.value,
              transaction
            );
          } else if (effect.is(removeEffect)) {
            // when a marker is removed
            markerDecorations = removeDecorations(
              markerDecorations,
              effect.value
            );
          } else {
            const cachedMarkers = lineContentMarkers.values();
            // For updates that are not related to this marker decoration,
            // we want to update the decorations when the editor is scrolled
            // and a new viewport is loaded.
            markerDecorations = updateDecorationsForAllMarkers(
              markerDecorations,
              cachedMarkers,
              transaction
            );
          }
        }
        return markerDecorations;
      },
      provide: field => EditorView.decorations.from(field),
    });

    return {
      lineContentMarkerExtension,
      lineContentMarkerEffect: { addEffect, removeEffect },
    };
  }

  #createEventHandlers() {
    const eventHandlers = {};
    for (const eventName in this.#editorDOMEventHandlers) {
      const handlers = this.#editorDOMEventHandlers[eventName];
      eventHandlers[eventName] = (event, editor) => {
        if (!event.target) {
          return;
        }
        for (const handler of handlers) {
          // Wait a cycle so the codemirror updates to the current cursor position,
          // information, TODO: Currently noticed this issue with CM6, not ideal but should
          // investigate further Bug 1890895.
          event.target.ownerGlobal.setTimeout(() => {
            const view = editor.viewState;
            const cursorPos = this.#posToLineColumn(
              view.state.selection.main.head
            );
            handler(event, view, cursorPos.line, cursorPos.column);
          }, 0);
        }
      };
    }
    return eventHandlers;
  }

  /**
   * Adds the DOM event handlers for the editor.
   * @param {Object} domEventHandlers - A dictionary of handlers for the DOM events
   *                                    the handlers are getting called with the following arguments
   *                                     - {Object} `event`: The DOM event
   *                                     - {Object} `view`: The codemirror view
   *                                     - {Number} cursorLine`: The line where the cursor is currently position
   *                                     - {Number} `cursorColumn`: The column where the cursor is currently position
   *                                     - {Number} `eventLine`: The line where the event was fired.
   *                                                             This might be different from the cursor line for mouse events.
   *                                     - {Number} `eventColumn`: The column where the event was fired.
   *                                                                This might be different from the cursor column for mouse events.
   */

  addEditorDOMEventListeners(domEventHandlers) {
    const cm = editors.get(this);
    const {
      codemirrorView: { EditorView },
    } = this.#CodeMirror6;

    // Update the cache of dom event handlers
    for (const eventName in domEventHandlers) {
      if (!this.#editorDOMEventHandlers[eventName]) {
        this.#editorDOMEventHandlers[eventName] = [];
      }
      this.#editorDOMEventHandlers[eventName].push(domEventHandlers[eventName]);
    }

    cm.dispatch({
      effects: this.#compartments.domEventHandlersCompartment.reconfigure(
        EditorView.domEventHandlers(this.#createEventHandlers())
      ),
    });
  }

  #cacheScrollSnapshot = () => {
    const cm = editors.get(this);
    if (!this.#currentDocumentId) {
      return;
    }
    this.#scrollSnapshots.set(this.#currentDocumentId, cm.scrollSnapshot());
    this.emitForTests("cm-editor-scrolled");
  };

  /**
   * Remove specified DOM event handlers for the editor.
   * @param {Object} domEventHandlers - A dictionary of handlers for the DOM events
   */

  removeEditorDOMEventListeners(domEventHandlers) {
    const cm = editors.get(this);
    const {
      codemirrorView: { EditorView },
    } = this.#CodeMirror6;

    for (const eventName in domEventHandlers) {
      const domEventHandler = domEventHandlers[eventName];
      const cachedEventHandlers = this.#editorDOMEventHandlers[eventName];
      if (!domEventHandler || !cachedEventHandlers) {
        continue;
      }
      const index = cachedEventHandlers.findIndex(
        handler => handler == domEventHandler
      );
      this.#editorDOMEventHandlers[eventName].splice(index, 1);
    }

    cm.dispatch({
      effects: this.#compartments.domEventHandlersCompartment.reconfigure(
        EditorView.domEventHandlers(this.#createEventHandlers())
      ),
    });
  }

  /**
   * Clear the DOM event handlers for the editor.
   */

  #clearEditorDOMEventListeners() {
    const cm = editors.get(this);
    const {
      codemirrorView: { EditorView },
    } = this.#CodeMirror6;

    this.#editorDOMEventHandlers = {};
    this.#gutterDOMEventHandlers = {};
    cm.dispatch({
      effects: this.#compartments.domEventHandlersCompartment.reconfigure(
        EditorView.domEventHandlers({})
      ),
    });
  }

  /**
   * This adds a marker used to add classes to editor line based on a condition.
   *   @property {object}             marker
   *                                  The rule rendering a marker or class.
   *   @property {object}             marker.id
   *                                  The unique identifier for this marker
   *   @property {string}             marker.lineClassName
   *                                  The css class to apply to the line
   *   @property {Array<Object>}      marker.lines
   *                                  The lines to add markers to. Each line object has a `line` and `value` property.
   *   @property {Boolean}           marker.renderAsBlock
   *                                  The specifies that the widget should be rendered as a block element. defaults to `false`. This is optional.
   *   @property {Boolean}           marker.shouldMarkAllLines
   *                                  Set to true to apply the marker to all the lines. In such case, `positions` is ignored. This is optional.
   *   @property {Function}           marker.createLineElementNode
   *                                  This should return the DOM element which is used for the marker. The line number is passed as a parameter.
   *                                  This is optional.
   *   @property {Function}           marker.getMarkerEqualityValue
   *                                  Custom equality function. The line and column will be passed as arguments when this is called.
   *                                  This should return a value used for an equality check. This is optional.
   */

  setLineContentMarker(marker) {
    const cm = editors.get(this);
    // We store the marker an the view state, this is gives access to view data
    // when defining updates to the StateField.
    marker._view = cm;
    this.#lineContentMarkers.set(marker.id, marker);
    cm.dispatch({
      effects: this.#effects.lineContentMarkerEffect.addEffect.of(marker),
    });
  }

  /**
   * This removes the marker which has the specified className
   * @param {string} markerId - The unique identifier for this marker
   */

  removeLineContentMarker(markerId) {
    const cm = editors.get(this);
    this.#lineContentMarkers.delete(markerId);
    cm.dispatch({
      effects: this.#effects.lineContentMarkerEffect.removeEffect.of(markerId),
    });
  }

  /**
   * This creates the extension used to manage the rendering of markers
   * at specific positions with the editor. e.g used for column breakpoints
   *
   * @returns {Object} The object contains an extension and effects which used to trigger updates to the extension
   *          {Object} - positionContentMarkerExtension - The position content marker extension
   *          {Object} - positionContentMarkerEffect - The effects to add and remove markers
   */
  #createPositionContentMarkersExtension() {
    const {
      codemirrorView: { Decoration, EditorView, WidgetType },
      codemirrorState: { StateField, StateEffect },
      codemirrorLanguage: { syntaxTree },
    } = this.#CodeMirror6;

    const cachedPositionContentMarkers = this.#posContentMarkers;

    class NodeWidget extends WidgetType {
      constructor({
        line,
        column,
        markerId,
        createElementNode,
        getMarkerEqualityValue,
      }) {
        super();
        this.line = line;
        this.column = column;
        this.markerId = markerId;
        this.equalityValue = getMarkerEqualityValue
          ? getMarkerEqualityValue(line, column)
          : {};
        this.toDOM = () => createElementNode(line, column);
      }

      eq(widget) {
        return (
          this.line == widget.line &&
          this.column == widget.column &&
          this.markerId == widget.markerId &&
          this.#isCustomValueEqual(widget)
        );
      }

      #isCustomValueEqual(widget) {
        return Object.keys(this.equalityValue).every(
          key =>
            widget.equalityValue.hasOwnProperty(key) &&
            widget.equalityValue[key] === this.equalityValue[key]
        );
      }
    }

    function getIndentation(lineText) {
      if (!lineText) {
        return 0;
      }

      const lineMatch = lineText.match(/^\s*/);
      if (!lineMatch) {
        return 0;
      }
      return lineMatch[0].length;
    }

    function _buildDecorationsForPositionMarkers(
      marker,
      transaction,
      newMarkerDecorations
    ) {
      const viewport = marker._view.viewport;
      const vStartLine = transaction.state.doc.lineAt(viewport.from);
      const vEndLine = transaction.state.doc.lineAt(viewport.to);

      for (const position of marker.positions) {
        // If codemirror positions are provided (e.g from search cursor)
        // compare that directly.
        if (position.from && position.to) {
          if (position.from >= viewport.from && position.to <= viewport.to) {
            if (marker.positionClassName) {
              // Markers used:
              // 1. active-selection-marker
              const classDecoration = Decoration.mark({
                class: marker.positionClassName,
              });
              classDecoration.markerType = marker.id;
              newMarkerDecorations.push(
                classDecoration.range(position.from, position.to)
              );
            }
          }
          continue;
        }
        // If line and column are provided
        if (
          position.line >= vStartLine.number &&
          position.line <= vEndLine.number
        ) {
          const line = transaction.state.doc.line(position.line);
          // Make sure to track any indentation at the beginning of the line
          const column = Math.max(position.column, getIndentation(line.text));
          const pos = line.from + column;

          if (marker.createPositionElementNode) {
            // Markers used:
            // 1. column-breakpoint-marker
            const nodeDecoration = Decoration.widget({
              widget: new NodeWidget({
                line: position.line,
                column: position.column,
                markerId: marker.id,
                createElementNode: marker.createPositionElementNode,
                getMarkerEqualityValue: marker.getMarkerEqualityValue,
              }),
              // Make sure the widget is rendered after the cursor
              // see https://codemirror.net/docs/ref/#view.Decoration^widget^spec.side for details.
              side: 1,
            });
            nodeDecoration.markerType = marker.id;
            newMarkerDecorations.push(nodeDecoration.range(pos, pos));
          }
          if (marker.positionClassName) {
            // Markers used:
            // 1. exception-position-marker
            // 2. debug-position-marker
            const tokenAtPos = syntaxTree(transaction.state).resolve(pos, 1);
            // While trying to update the markers, during content changes, the syntax tree is not
            // guaranteed to be complete, so there is the possibility of getting wrong `from` and `to` values for the token.
            // 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)
            // matches the position we want.
            if (tokenAtPos.from !== pos) {
              continue;
            }
            const tokenString = line.text.slice(
              position.column,
              tokenAtPos.to - line.from
            );
            // Ignore any empty strings and opening braces
            if (
              tokenString === "" ||
              tokenString === "{" ||
              tokenString === "["
            ) {
              continue;
            }
            const classDecoration = Decoration.mark({
              class: marker.positionClassName,
            });
            classDecoration.markerType = marker.id;
            newMarkerDecorations.push(
              classDecoration.range(pos, tokenAtPos.to)
            );
          }
        }
      }
    }

    /**
     * This updates the decorations for the marker specified
     *
     * @param {Array} markerDecorations - The current decorations displayed in the document
     * @param {Array} marker - The current marker whose decoration should be update
     * @param {Transaction} transaction
     * @returns
     */
    function updateDecorations(markerDecorations, marker, transaction) {
      const newDecorations = [];

      _buildDecorationsForPositionMarkers(marker, transaction, newDecorations);
      return markerDecorations.update({
        filter: (from, to, decoration) => {
          return decoration.markerType !== marker.id;
        },
        add: newDecorations,
        sort: true,
      });
    }

    /**
     * This updates all the decorations for all the markers. This
     * used in scenarios when an update to view (e.g vertically scrolling into a new viewport)
     * requires all the marker decoraions.
     *
     * @param {Array} markerDecorations - The current decorations displayed in the document
     * @param {Array} markers - All the cached markers
     * @param {Object} transaction
     * @returns
     */
    function updateDecorationsForAllMarkers(
      markerDecorations,
      markers,
      transaction
    ) {
      const allNewDecorations = [];

      for (const marker of markers) {
        _buildDecorationsForPositionMarkers(
          marker,
          transaction,
          allNewDecorations
        );
      }
      return markerDecorations.update({
        filter: () => false,
        add: allNewDecorations,
        sort: true,
      });
    }

    function removeDecorations(markerDecorations, markerId) {
      return markerDecorations.update({
        filter: (from, to, decoration) => {
          return decoration.markerType !== markerId;
        },
      });
    }

    const addEffect = StateEffect.define();
    const removeEffect = StateEffect.define();

    const positionContentMarkerExtension = StateField.define({
      create() {
        return Decoration.none;
      },
      update(markerDecorations, transaction) {
        // Map the decorations through the transaction changes, this is important
        // as it remaps the decorations from positions in the old document to
        // positions in the new document.
        markerDecorations = markerDecorations.map(transaction.changes);
        for (const effect of transaction.effects) {
          if (effect.is(addEffect)) {
            // When a new marker is added
            markerDecorations = updateDecorations(
              markerDecorations,
              effect.value,
              transaction
            );
          } else if (effect.is(removeEffect)) {
            // When a marker is removed
            markerDecorations = removeDecorations(
              markerDecorations,
              effect.value
            );
          } else {
            // For updates that are not related to this marker decoration,
            // we want to update the decorations when the editor is scrolled
            // and a new viewport is loaded.
            markerDecorations = updateDecorationsForAllMarkers(
              markerDecorations,
              cachedPositionContentMarkers.values(),
              transaction
            );
          }
        }
        return markerDecorations;
      },
      provide: field => EditorView.decorations.from(field),
    });

    return {
      positionContentMarkerExtension,
      positionContentMarkerEffect: { addEffect, removeEffect },
    };
  }

  /**
   * This adds a marker used to decorate token / content at
   * a specific position (defined by a line and column).
   * @param {Object} marker
   * @param {String} marker.id
   * @param {Array} marker.positions
   * @param {Function} marker.createPositionElementNode
   */
  setPositionContentMarker(marker) {
    const cm = editors.get(this);

    // We store the marker an the view state, this is gives access to viewport data
    // when defining updates to the StateField.
    marker._view = cm;
    this.#posContentMarkers.set(marker.id, marker);
    cm.dispatch({
      effects: this.#effects.positionContentMarkerEffect.addEffect.of(marker),
    });
  }

  /**
   * This removes the marker which has the specified id
   * @param {string} markerId - The unique identifier for this marker
   */
  removePositionContentMarker(markerId) {
    const cm = editors.get(this);
    this.#posContentMarkers.delete(markerId);
    cm.dispatch({
      effects:
        this.#effects.positionContentMarkerEffect.removeEffect.of(markerId),
    });
  }

  /**
   * Set event listeners for the line gutter
   * @param {Object} domEventHandlers
   *
   * example usage:
   *  const domEventHandlers = { click(event) { console.log(event);} }
   */
  setGutterEventListeners(domEventHandlers) {
    const cm = editors.get(this);
    const {
      codemirrorView: { lineNumbers },
      codemirrorLanguage: { foldGutter },
    } = this.#CodeMirror6;

    for (const eventName in domEventHandlers) {
      const handler = domEventHandlers[eventName];
      this.#gutterDOMEventHandlers[eventName] = (view, line, event) => {
        line = view.state.doc.lineAt(line.from);
        handler(event, view, line.number);
      };
    }

    cm.dispatch({
      effects: [
        this.#compartments.lineNumberCompartment.reconfigure(
          lineNumbers({ domEventHandlers: this.#gutterDOMEventHandlers })
        ),
        this.#compartments.foldGutterCompartment.reconfigure(
          foldGutter({
            class: "cm6-dt-foldgutter",
            markerDOM: open => {
              if (!this.#ownerDoc) {
                return null;
              }
              const button = this.#ownerDoc.createElement("button");
              button.classList.add("cm6-dt-foldgutter__toggle-button");
              button.setAttribute("aria-expanded", open);
              return button;
            },
            domEventHandlers: this.#gutterDOMEventHandlers,
          })
        ),
      ],
    });
  }

  /**
   * This supports adding/removing of line classes or markers on the
   * line number gutter based on the defined conditions. This only supports codemirror 6.
   *
   *   @param {Array<Marker>} markers         - The list of marker objects which defines the rules
   *                                            for rendering each marker.
   *   @property {object}     marker - The rule rendering a marker or class. This is required.
   *   @property {string}     marker.id - The unique identifier for this marker.
   *   @property {string}     marker.lineClassName - The css class to add to the line. This is required.
   *   @property {function}   marker.condition - The condition that decides if the marker/class gets added or removed.
   *                                              This should return `false` for lines where the marker should not be added and the
   *                                              result of the condition for any other line.
   *   @property {function=}  marker.createLineElementNode - This gets the line and the result of the condition as arguments and should return the DOM element which
   *                                            is used for the marker. This is optional.
   */
  setLineGutterMarkers(markers) {
    const cm = editors.get(this);

    if (markers) {
      // Cache the markers for use later. See next comment
      for (const marker of markers) {
        if (!marker.id) {
          throw new Error("Marker has no unique identifier");
        }
        this.#lineGutterMarkers.set(marker.id, marker);
      }
    }
    // When no markers are passed, the cached markers are used to update the line gutters.
    // This is useful for re-rendering the line gutters when the viewport changes
    // (note: the visible ranges will be different) in this case, mainly when the editor is scrolled.
    else if (!this.#lineGutterMarkers.size) {
      return;
    }
    markers = Array.from(this.#lineGutterMarkers.values());

    const {
      codemirrorView: { lineNumberMarkers, GutterMarker },
      codemirrorState: { RangeSetBuilder },
    } = this.#CodeMirror6;

    // This creates a new GutterMarker https://codemirror.net/docs/ref/#view.GutterMarker
    // to represents how each line gutter is rendered in the view.
    // This is set as the value for the Range https://codemirror.net/docs/ref/#state.Range
    // which represents the line.
    class LineGutterMarker extends GutterMarker {
      constructor(className, lineNumber, createElementNode, conditionResult) {
        super();
        this.elementClass = className || null;
        this.lineNumber = lineNumber;
        this.createElementNode = createElementNode;
        this.conditionResult = conditionResult;

        this.toDOM = createElementNode
          ? () => createElementNode(lineNumber, conditionResult)
          : null;
      }

      eq(marker) {
        return (
          marker.lineNumber == this.lineNumber &&
          marker.conditionResult == this.conditionResult
        );
      }
    }

    // Loop through the visible ranges https://codemirror.net/docs/ref/#view.EditorView.visibleRanges
    // (representing the lines in the current viewport) and generate a new rangeset for updating the line gutter
    // based on the conditions defined in the markers(for each line) provided.
    const builder = new RangeSetBuilder();
    const { from, to } = cm.viewport;
    let pos = from;
    while (pos <= to) {
      const line = cm.state.doc.lineAt(pos);
      for (const {
        lineClassName,
        condition,
        createLineElementNode,
      } of markers) {
        if (typeof condition !== "function") {
          throw new Error("The `condition` is not a valid function");
        }
        const conditionResult = condition(line.number);
        if (conditionResult !== false) {
          builder.add(
            line.from,
            line.to,
            new LineGutterMarker(
              lineClassName,
              line.number,
              createLineElementNode,
              conditionResult
            )
          );
        }
      }
      pos = line.to + 1;
    }

    // To update the state with the newly generated marker range set, a dispatch is called on the view
    // with an transaction effect created by the lineNumberMarkersCompartment, which is used to update the
    // lineNumberMarkers extension configuration.
    cm.dispatch({
      effects: this.#compartments.lineNumberMarkersCompartment.reconfigure(
        lineNumberMarkers.of(builder.finish())
      ),
    });
  }

  /**
   * This creates the extension used to manage the rendering of markers for
   * results for any search pattern
   * @param {RegExp}      pattern - The search pattern
   * @param {String}      className - The class used to decorate each result
   * @returns {Array<ViewPlugin>} An extension which is an array containing the view
   *                              which manages the rendering of the line content markers.
   */
  #searchHighlighterExtension({
    /* This defaults to matching nothing */ pattern = /.^/g,
    className = "",
  }) {
    const cm = editors.get(this);
    if (!cm) {
      return [];
    }
    const {
      codemirrorView: { Decoration, ViewPlugin, EditorView, MatchDecorator },
      codemirrorSearch: { RegExpCursor },
    } = this.#CodeMirror6;

    this.searchState.query = pattern;
    const searchCursor = new RegExpCursor(cm.state.doc, pattern, {
      ignoreCase: pattern.ignoreCase,
    });
    this.searchState.cursors = Array.from(searchCursor);
    this.searchState.currentCursorIndex = -1;

    const patternMatcher = new MatchDecorator({
      regexp: pattern,
      decorate: (add, from, to) => {
        add(from, to, Decoration.mark({ class: className }));
      },
    });

    const searchHighlightView = ViewPlugin.fromClass(
      class {
        decorations;
        constructor(view) {
          this.decorations = patternMatcher.createDeco(view);
        }
        update(viewUpdate) {
          this.decorations = patternMatcher.updateDeco(
            viewUpdate,
            this.decorations
          );
        }
      },
      {
        decorations: instance => instance.decorations,
        provide: plugin =>
          EditorView.atomicRanges.of(view => {
            return view.plugin(plugin)?.decorations || Decoration.none;
          }),
      }
    );

    return [searchHighlightView];
  }

  /**
   * This should add the class to the results of a search pattern specified
   *
   * @param {RegExp} pattern - The search pattern
   * @param {String} className - The class used to decorate each result
   */
  highlightSearchMatches(pattern, className) {
    const cm = editors.get(this);
    cm.dispatch({
      effects: this.#compartments.searchHighlightCompartment.reconfigure(
        this.#searchHighlighterExtension({ pattern, className })
      ),
    });
  }

  /**
   * This clear any decoration on all the search results
   */
  clearSearchMatches() {
    this.highlightSearchMatches(undefined, "");
  }

  /**
   * Retrieves the cursor for the next selection to be highlighted
   *
   * @param {Boolean} reverse - Determines the direction of the cursor movement
   * @returns {RegExpSearchCursor}
   */
  getNextSearchCursor(reverse) {
    if (reverse) {
      if (this.searchState.currentCursorIndex == 0) {
        this.searchState.currentCursorIndex =
          this.searchState.cursors.length - 1;
      } else {
        this.searchState.currentCursorIndex--;
      }
    } else if (
      this.searchState.currentCursorIndex ==
      this.searchState.cursors.length - 1
    ) {
      this.searchState.currentCursorIndex = 0;
    } else {
      this.searchState.currentCursorIndex++;
    }
    return this.searchState.cursors[this.searchState.currentCursorIndex];
  }

  /**
   * Get the start and end locations of the current viewport
   * @returns {Object}  - The location information for the current viewport
   */
  getLocationsInViewport() {
    if (this.isDestroyed()) {
      return null;
    }
    const cm = editors.get(this);
    if (this.config.cm6) {
      const { from, to } = cm.viewport;
      const lineFrom = cm.state.doc.lineAt(from);
      const lineTo = cm.state.doc.lineAt(to);
      // This returns boundary of the full viewport regardless of the horizontal
      // scroll position.
      return {
        start: { line: lineFrom.number, column: 0 },
        end: { line: lineTo.number, column: lineTo.to - lineTo.from },
      };
    }
    // Offset represents an allowance of characters or lines offscreen to improve
    // perceived performance of column breakpoint rendering
    const offsetHorizontalCharacters = 100;
    const offsetVerticalLines = 20;
    // Get scroll position
    if (!cm) {
      return {
        start: { line: 0, column: 0 },
        end: { line: 0, column: 0 },
      };
    }
    const charWidth = cm.defaultCharWidth();
    const scrollArea = cm.getScrollInfo();
    const { scrollLeft } = cm.doc;
    const rect = cm.getWrapperElement().getBoundingClientRect();
    const topVisibleLine =
      cm.lineAtHeight(rect.top, "window") - offsetVerticalLines;
    const bottomVisibleLine =
      cm.lineAtHeight(rect.bottom, "window") + offsetVerticalLines;

    const leftColumn = Math.floor(
      scrollLeft > 0 ? scrollLeft / charWidth - offsetHorizontalCharacters : 0
    );
    const rightPosition = scrollLeft + (scrollArea.clientWidth - 30);
    const rightCharacter =
      Math.floor(rightPosition / charWidth) + offsetHorizontalCharacters;

    return {
      start: {
        line: topVisibleLine || 0,
        column: leftColumn || 0,
      },
      end: {
        line: bottomVisibleLine || 0,
        column: rightCharacter,
      },
    };
  }

  /**
   * Gets the position information for the current selection
   * @returns {Object} cursor      - The location information for the  current selection
   *                   cursor.from - An object with the starting line / column of the selection
   *                   cursor.to   - An object with the end line / column of the selection
   */
  getSelectionCursor() {
    const cm = editors.get(this);
    if (this.config.cm6) {
      const selection = cm.state.selection.ranges[0];
      const lineFrom = cm.state.doc.lineAt(selection.from);
      const lineTo = cm.state.doc.lineAt(selection.to);
      return {
        from: {
          line: lineFrom.number,
          ch: selection.from - lineFrom.from,
        },
        to: {
          line: lineTo.number,
          ch: selection.to - lineTo.from,
        },
      };
    }
    return {
      from: cm.getCursor("from"),
      to: cm.getCursor("to"),
    };
  }

  /**
   * Gets the text content for the current selection
   * @returns {String}
   */
  getSelectedText() {
    const cm = editors.get(this);
    if (this.config.cm6) {
      const selection = cm.state.selection.ranges[0];
      return cm.state.doc.sliceString(selection.from, selection.to);
    }
    return cm.getSelection().trim();
  }

  /**
   * Returns the token at a specific position in the source
   *
   * @param   {Object} cm
   * @param   {Object} position
   * @param   {Number} position.line - line in the source
   * @param   {Number} position.column  - column on a line in the source
   * @returns {Object|null} token - start position of the token, end position of the token and
   *                                      the  type of the token. Returns null if no token are
   *                                      found at the passed coords
   */
  #tokenAtCoords(cm, { line, column }) {
    if (this.config.cm6) {
      const {
        codemirrorLanguage: { syntaxTree },
      } = this.#CodeMirror6;
      const lineObject = cm.state.doc.line(line);
      const pos = lineObject.from + column;
      const token = syntaxTree(cm.state).resolve(pos, 1);
      if (!token) {
        return null;
      }
      return {
        startColumn: column,
        endColumn: token.to - token.from,
        type: token.type?.name,
      };
    }
    if (line < 0 || line >= cm.lineCount()) {
      return null;
    }

    const token = cm.getTokenAt({ line: line - 1, ch: column });
    if (!token) {
      return null;
    }

    return { startColumn: token.start, endColumn: token.end, type: token.type };
  }

  /**
   * Returns the expression at the specified position.
   *
   * The strategy of querying codeMirror tokens was borrowed
   * from Chrome's inital implementation in JavaScriptSourceFrame.js#L414
   *
   * @param {Object} coord
   * @param {Number} coord.line - line in the source
   * @param {Number} coord.column  - column on a line in the source
   * @return {Object|null} An object with the following properties:
   *                       - {String} `expression`: The expression at specified coordinate
   *                       - {Object} `location`: start and end lines/columns of the expression
   *                       Returns null if no suitable expression could be found at passed coordinates
   */
  getExpressionFromCoords(coord) {
--> --------------------

--> maximum size reached

--> --------------------

96%


¤ Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.0.112Bemerkung:  (vorverarbeitet)  ¤

*Bot Zugriff






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung ist noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge