summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/sourceeditor
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/client/shared/sourceeditor/editor.js251
1 files changed, 247 insertions, 4 deletions
diff --git a/devtools/client/shared/sourceeditor/editor.js b/devtools/client/shared/sourceeditor/editor.js
index 056914b931..3487acffa4 100644
--- a/devtools/client/shared/sourceeditor/editor.js
+++ b/devtools/client/shared/sourceeditor/editor.js
@@ -166,6 +166,10 @@ class Editor extends EventEmitter {
#ownerDoc;
#prefObserver;
#win;
+ #lineGutterMarkers = new Map();
+ #lineContentMarkers = new Map();
+
+ #updateListener = null;
constructor(config) {
super();
@@ -412,6 +416,12 @@ class Editor extends EventEmitter {
}
}
+ // 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
@@ -614,11 +624,17 @@ class Editor extends EventEmitter {
const tabSizeCompartment = new Compartment();
const indentCompartment = new Compartment();
const lineWrapCompartment = new Compartment();
+ const lineNumberCompartment = new Compartment();
+ const lineNumberMarkersCompartment = new Compartment();
+ const lineContentMarkerCompartment = new Compartment();
this.#compartments = {
tabSizeCompartment,
indentCompartment,
lineWrapCompartment,
+ lineNumberCompartment,
+ lineNumberMarkersCompartment,
+ lineContentMarkerCompartment,
};
const indentStr = (this.config.indentWithTabs ? "\t" : " ").repeat(
@@ -632,6 +648,7 @@ class Editor extends EventEmitter {
this.config.lineWrapping ? EditorView.lineWrapping : []
),
EditorState.readOnly.of(this.config.readOnly),
+ lineNumberCompartment.of(this.config.lineNumbers ? lineNumbers() : []),
codemirrorLanguage.codeFolding({
placeholderText: "↔",
}),
@@ -645,6 +662,21 @@ class Editor extends EventEmitter {
},
}),
codemirrorLanguage.syntaxHighlighting(lezerHighlight.classHighlighter),
+ EditorView.updateListener.of(v => {
+ if (v.viewportChanged || v.docChanged) {
+ // 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);
+ }
+ }),
+ lineNumberMarkersCompartment.of([]),
+ lineContentMarkerCompartment.of(this.#lineContentMarkersExtension([])),
// keep last so other extension take precedence
codemirror.minimalSetup,
];
@@ -653,10 +685,6 @@ class Editor extends EventEmitter {
extensions.push(codemirrorLangJavascript.javascript());
}
- if (this.config.lineNumbers) {
- extensions.push(lineNumbers());
- }
-
const cm = new EditorView({
parent: el,
extensions,
@@ -666,6 +694,219 @@ class Editor extends EventEmitter {
}
/**
+ * This creates the extension used to manage the rendering of markers
+ * for in editor line content.
+ * @param {Array} markers - The current list of markers
+ * @returns {Array<ViewPlugin>} showLineContentDecorations - An extension which is an array containing the view
+ * which manages the rendering of the line content markers.
+ */
+ #lineContentMarkersExtension(markers) {
+ const {
+ codemirrorView: { Decoration, ViewPlugin },
+ codemirrorState: { RangeSetBuilder },
+ } = this.#CodeMirror6;
+
+ // Build and return the decoration set
+ function buildDecorations(view) {
+ const builder = new RangeSetBuilder();
+ for (const { from, to } of view.visibleRanges) {
+ for (let pos = from; pos <= to; ) {
+ const line = view.state.doc.lineAt(pos);
+ for (const { lineClassName, condition } of markers) {
+ if (condition(line.number)) {
+ builder.add(
+ line.from,
+ line.from,
+ Decoration.line({ class: lineClassName })
+ );
+ }
+ }
+ pos = line.to + 1;
+ }
+ }
+ return builder.finish();
+ }
+
+ // The view which handles rendering and updating the
+ // markers decorations
+ const showLineContentDecorations = ViewPlugin.fromClass(
+ class {
+ decorations;
+ constructor(view) {
+ this.decorations = buildDecorations(view);
+ }
+ update(update) {
+ if (update.docChanged || update.viewportChanged) {
+ this.decorations = buildDecorations(update.view);
+ }
+ }
+ },
+ { decorations: v => v.decorations }
+ );
+
+ return [showLineContentDecorations];
+ }
+
+ /**
+ * 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 add to the line
+ * @property {function} marker.condition - The condition that decides if the marker/class gets added or removed.
+ * The line is passed as an argument.
+ */
+ setLineContentMarker(marker) {
+ const cm = editors.get(this);
+ this.#lineContentMarkers.set(marker.id, marker);
+
+ cm.dispatch({
+ effects: this.#compartments.lineContentMarkerCompartment.reconfigure(
+ this.#lineContentMarkersExtension(
+ Array.from(this.#lineContentMarkers.values())
+ )
+ ),
+ });
+ }
+
+ /**
+ * 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.#compartments.lineContentMarkerCompartment.reconfigure(
+ this.#lineContentMarkersExtension(
+ Array.from(this.#lineContentMarkers.values())
+ )
+ ),
+ });
+ }
+
+ /**
+ * 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 },
+ } = this.#CodeMirror6;
+
+ for (const eventName in domEventHandlers) {
+ const handler = domEventHandlers[eventName];
+ domEventHandlers[eventName] = (view, line, event) => {
+ line = view.state.doc.lineAt(line.from);
+ handler(event, view, line.number);
+ };
+ }
+
+ cm.dispatch({
+ effects: this.#compartments.lineWrapCompartment.reconfigure(
+ lineNumbers({ domEventHandlers })
+ ),
+ });
+ }
+
+ /**
+ * 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.
+ * @property {function=} marker.createLineElementNode - This gets the line as an argument 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) {
+ super();
+ this.elementClass = className || null;
+ this.toDOM = createElementNode
+ ? () => createElementNode(lineNumber)
+ : null;
+ }
+ }
+
+ // 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();
+ for (const { from, to } of cm.visibleRanges) {
+ for (let pos = from; 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");
+ }
+ if (condition(line.number)) {
+ builder.add(
+ line.from,
+ line.to,
+ new LineGutterMarker(
+ lineClassName,
+ line.number,
+ createLineElementNode
+ )
+ );
+ }
+ }
+ 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())
+ ),
+ });
+ }
+
+ /**
* Returns a boolean indicating whether the editor is ready to
* use. Use appendTo(el).then(() => {}) for most cases
*/
@@ -1658,6 +1899,8 @@ class Editor extends EventEmitter {
this.config = null;
this.version = null;
this.#ownerDoc = null;
+ this.#updateListener = null;
+ this.#lineGutterMarkers.clear();
if (this.#prefObserver) {
this.#prefObserver.off(KEYMAP_PREF, this.setKeyMap);