Bug 1889281 - [devtools] Display column breakpoints for codemirror 6 r=devtools-reviewers,nchevobbe

This patch creates a new extension and api enable marking content nodes

Differential Revision: https://phabricator.services.mozilla.com/D207408
This commit is contained in:
Hubert Boma Manilla 2024-05-19 22:28:23 +00:00
parent f1aef7d2f6
commit ecf63f1f50
6 changed files with 312 additions and 61 deletions

View file

@ -13,11 +13,7 @@ import {
getBreakpointsForSource,
} from "../selectors/index";
import { selectSource } from "../actions/sources/select";
import {
getEditor,
getLocationsInViewport,
updateEditorLineWrapping,
} from "../utils/editor/index";
import { getEditor, updateEditorLineWrapping } from "../utils/editor/index";
import { blackboxSourceActorsForSource } from "./sources/blackbox";
import { toggleBreakpoints } from "./breakpoints/index";
import { copyToTheClipboard } from "../utils/clipboard";
@ -198,9 +194,10 @@ export function closeConditionalPanel() {
}
export function updateViewport() {
const editor = getEditor();
return {
type: "SET_VIEWPORT",
viewport: getLocationsInViewport(getEditor()),
viewport: editor.getLocationsInViewport(),
};
}

View file

@ -6,6 +6,9 @@ import React, { Component } from "devtools/client/shared/vendor/react";
import { div } from "devtools/client/shared/vendor/react-dom-factories";
import PropTypes from "devtools/client/shared/vendor/react-prop-types";
import { features } from "../../utils/prefs";
const classnames = require("resource://devtools/client/shared/classnames.js");
import ColumnBreakpoint from "./ColumnBreakpoint";
import {
@ -16,8 +19,25 @@ import {
import actions from "../../actions/index";
import { connect } from "devtools/client/shared/vendor/react-redux";
import { makeBreakpointId } from "../../utils/breakpoint/index";
import { fromEditorLine } from "../../utils/editor/index";
// eslint-disable-next-line max-len
const breakpointButton = document.createElement("button");
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("viewBox", "0 0 11 13");
svg.setAttribute("width", 11);
svg.setAttribute("height", 13);
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttributeNS(
null,
"d",
"M5.07.5H1.5c-.54 0-1 .46-1 1v10c0 .54.46 1 1 1h3.57c.58 0 1.15-.26 1.53-.7l3.7-5.3-3.7-5.3C6.22.76 5.65.5 5.07.5z"
);
svg.appendChild(path);
breakpointButton.appendChild(svg);
const COLUMN_BREAKPOINT_MARKER = "column-breakpoint-marker";
class ColumnBreakpoints extends Component {
static get propTypes() {
@ -33,6 +53,87 @@ class ColumnBreakpoints extends Component {
};
}
componentDidUpdate() {
const { selectedSource, columnBreakpoints, editor } = this.props;
// Only for codemirror 6
if (!features.codemirrorNext) {
return;
}
if (!selectedSource || !editor) {
return;
}
if (!columnBreakpoints.length) {
editor.removePositionContentMarker(COLUMN_BREAKPOINT_MARKER);
return;
}
editor.setPositionContentMarker({
id: COLUMN_BREAKPOINT_MARKER,
positions: columnBreakpoints.map(bp => bp.location),
createPositionElementNode: (line, column) => {
const lineNumber = fromEditorLine(selectedSource.id, line);
const columnBreakpoint = columnBreakpoints.find(
bp => bp.location.line === lineNumber && bp.location.column === column
);
const breakpointNode = breakpointButton.cloneNode(true);
breakpointNode.className = classnames("column-breakpoint", {
"has-condition": columnBreakpoint.breakpoint?.options.condition,
"has-log": columnBreakpoint.breakpoint?.options.logValue,
active:
columnBreakpoint.breakpoint &&
!columnBreakpoint.breakpoint.disabled,
disabled: columnBreakpoint.breakpoint?.disabled,
});
breakpointNode.addEventListener("click", event =>
this.onClick(event, columnBreakpoint)
);
breakpointNode.addEventListener("contextmenu", event =>
this.onContextMenu(event, columnBreakpoint)
);
return breakpointNode;
},
});
}
onClick = (event, columnBreakpoint) => {
event.stopPropagation();
event.preventDefault();
const { toggleDisabledBreakpoint, removeBreakpoint, addBreakpoint } =
this.props;
// disable column breakpoint on shift-click.
if (event.shiftKey) {
toggleDisabledBreakpoint(columnBreakpoint.breakpoint);
return;
}
if (columnBreakpoint.breakpoint) {
removeBreakpoint(columnBreakpoint.breakpoint);
} else {
addBreakpoint(columnBreakpoint.location);
}
};
onContextMenu = (event, columnBreakpoint) => {
event.stopPropagation();
event.preventDefault();
if (columnBreakpoint.breakpoint) {
this.props.showEditorEditBreakpointContextMenu(
event,
columnBreakpoint.breakpoint
);
} else {
this.props.showEditorCreateBreakpointContextMenu(
event,
columnBreakpoint.location
);
}
};
render() {
const {
editor,
@ -45,6 +146,10 @@ class ColumnBreakpoints extends Component {
addBreakpoint,
} = this.props;
if (features.codemirrorNext) {
return null;
}
if (!selectedSource || columnBreakpoints.length === 0) {
return null;
}

View file

@ -189,11 +189,12 @@ class Editor extends PureComponent {
}
}
onEditorUpdated(v) {
onEditorUpdated = v => {
if (v.docChanged || v.geometryChanged) {
resizeToggleButton(v.view.dom.querySelector(".cm-gutters").clientWidth);
this.props.updateViewport();
}
}
};
setupEditor() {
const editor = getEditor(features.codemirrorNext);
@ -854,7 +855,10 @@ class Editor extends PureComponent {
editor,
range: highlightedLineRange,
})
: null
: null,
React.createElement(ColumnBreakpoints, {
editor,
})
);
}

View file

@ -14,8 +14,19 @@ import { getVisibleBreakpoints } from "./visibleBreakpoints";
import { getSelectedLocation } from "../utils/selected-location";
import { sortSelectedLocations } from "../utils/location";
import { getLineText } from "../utils/source";
import { features } from "../utils/prefs";
function contains(location, range) {
if (features.codemirrorNext) {
// If the location is within the viewport lines or if the location is on the first or last line
// and the columns are within the start or end line content.
return (
(location.line > range.start.line && location.line < range.end.line) ||
(location.line == range.start.line &&
location.column >= range.start.column) ||
(location.line == range.end.line && location.column <= range.end.column)
);
}
return (
location.line >= range.start.line &&
location.line <= range.end.line &&

View file

@ -106,48 +106,6 @@ export function toSourceLine(sourceId, line) {
return line + 1;
}
export function getLocationsInViewport(
{ codeMirror },
// Offset represents an allowance of characters or lines offscreen to improve
// perceived performance of column breakpoint rendering
offsetHorizontalCharacters = 100,
offsetVerticalLines = 20
) {
// Get scroll position
if (!codeMirror) {
return {
start: { line: 0, column: 0 },
end: { line: 0, column: 0 },
};
}
const charWidth = codeMirror.defaultCharWidth();
const scrollArea = codeMirror.getScrollInfo();
const { scrollLeft } = codeMirror.doc;
const rect = codeMirror.getWrapperElement().getBoundingClientRect();
const topVisibleLine =
codeMirror.lineAtHeight(rect.top, "window") - offsetVerticalLines;
const bottomVisibleLine =
codeMirror.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,
},
};
}
export function markText({ codeMirror }, className, { start, end }) {
return codeMirror.markText(
{ ch: start.column, line: start.line },

View file

@ -168,6 +168,7 @@ class Editor extends EventEmitter {
#win;
#lineGutterMarkers = new Map();
#lineContentMarkers = new Map();
#posContentMarkers = new Map();
#lineContentEventHandlers = {};
#updateListener = null;
@ -628,6 +629,7 @@ class Editor extends EventEmitter {
const lineNumberCompartment = new Compartment();
const lineNumberMarkersCompartment = new Compartment();
const lineContentMarkerCompartment = new Compartment();
const positionContentMarkersCompartment = new Compartment();
this.#compartments = {
tabSizeCompartment,
@ -636,6 +638,7 @@ class Editor extends EventEmitter {
lineNumberCompartment,
lineNumberMarkersCompartment,
lineContentMarkerCompartment,
positionContentMarkersCompartment,
};
const indentStr = (this.config.indentWithTabs ? "\t" : " ").repeat(
@ -678,7 +681,10 @@ class Editor extends EventEmitter {
}),
lineNumberMarkersCompartment.of([]),
lineContentMarkerCompartment.of(
this.#lineContentMarkersExtension({ markers: [] })
this.#lineContentMarkersExtension({ lineMarkers: [] })
),
positionContentMarkersCompartment.of(
this.#positionContentMarkersExtension([])
),
// keep last so other extension take precedence
codemirror.minimalSetup,
@ -699,13 +705,13 @@ 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
* @param {Array} lineMarkers - The current list of markers
* @param {Object} domEventHandlers - A dictionary of handlers for the DOM events
* See https://codemirror.net/docs/ref/#view.PluginSpec.eventHandlers
* @returns {Array<ViewPlugin>} showLineContentDecorations - An extension which is an array containing the view
* which manages the rendering of the line content markers.
* @returns {Array<ViewPlugin>} An extension which is an array containing the view
* which manages the rendering of the line content markers.
*/
#lineContentMarkersExtension({ markers, domEventHandlers }) {
#lineContentMarkersExtension({ lineMarkers, domEventHandlers }) {
const {
codemirrorView: { Decoration, ViewPlugin, WidgetType },
codemirrorState: { RangeSetBuilder, RangeSet },
@ -720,14 +726,14 @@ class Editor extends EventEmitter {
// Build and return the decoration set
function buildDecorations(view) {
if (!markers) {
if (!lineMarkers) {
return RangeSet.empty;
}
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 marker of markers) {
for (const marker of lineMarkers) {
if (marker.condition(line.number)) {
if (marker.lineClassName) {
const classDecoration = Decoration.line({
@ -826,7 +832,7 @@ class Editor extends EventEmitter {
cm.dispatch({
effects: this.#compartments.lineContentMarkerCompartment.reconfigure(
this.#lineContentMarkersExtension({
markers: Array.from(this.#lineContentMarkers.values()),
lineMarkers: Array.from(this.#lineContentMarkers.values()),
})
),
});
@ -843,12 +849,126 @@ class Editor extends EventEmitter {
cm.dispatch({
effects: this.#compartments.lineContentMarkerCompartment.reconfigure(
this.#lineContentMarkersExtension({
markers: Array.from(this.#lineContentMarkers.values()),
lineMarkers: Array.from(this.#lineContentMarkers.values()),
})
),
});
}
/**
* This creates the extension used to manage the rendering of markers
* at specific positions with the editor. e.g used for column breakpoints
* @param {Array} markers - The current list of markers
* @returns {Array<ViewPlugin>} An extension which is an array containing the view
* which manages the rendering of the position content markers.
*/
#positionContentMarkersExtension(markers) {
const {
codemirrorView: { Decoration, ViewPlugin, WidgetType },
codemirrorState: { RangeSet },
} = this.#CodeMirror6;
class NodeWidget extends WidgetType {
constructor(line, column, createElementNode) {
super();
this.toDOM = () => createElementNode(line, column);
}
}
// Build and return the decoration set
function buildDecorations(view) {
const ranges = [];
const { from, to } = view.viewport;
const vStartLine = view.state.doc.lineAt(from);
const vEndLine = view.state.doc.lineAt(to);
for (const marker of markers) {
for (const position of marker.positions) {
if (
position.line >= vStartLine.number &&
position.line <= vEndLine.number
) {
const line = view.state.doc.line(position.line);
const pos = line.from + position.column;
if (marker.createPositionElementNode) {
const nodeDecoration = Decoration.widget({
widget: new NodeWidget(
position.line,
position.column,
marker.createPositionElementNode
),
// Make sure the widget is rendered after the cursor
// see https://codemirror.net/docs/ref/#view.Decoration^widget^spec.side for details.
side: 1,
});
ranges.push({ from: pos, to: pos, value: nodeDecoration });
}
}
}
}
// Make sure to sort the rangeset as its required to render in order
return RangeSet.of(ranges, /*sort*/ true);
}
// The view which handles rendering and updating the
// markers decorations
const positionContentMarkersView = 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 [positionContentMarkersView];
}
/**
* 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);
this.#posContentMarkers.set(marker.id, marker);
cm.dispatch({
effects: this.#compartments.positionContentMarkersCompartment.reconfigure(
this.#positionContentMarkersExtension(
Array.from(this.#posContentMarkers.values())
)
),
});
}
/**
* 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.#compartments.positionContentMarkersCompartment.reconfigure(
this.#positionContentMarkersExtension(
Array.from(this.#posContentMarkers.values())
)
),
});
}
/**
* Set event listeners for the line gutter
* @param {Object} domEventHandlers
@ -970,6 +1090,62 @@ class Editor extends EventEmitter {
});
}
/**
* Get the start and end locations of the current viewport
* @returns {Object} - The location information for the current viewport
*/
getLocationsInViewport() {
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