forked from mirrors/gecko-dev
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:
parent
f1aef7d2f6
commit
ecf63f1f50
6 changed files with 312 additions and 61 deletions
|
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue