diff --git a/devtools/client/debugger/src/actions/ui.js b/devtools/client/debugger/src/actions/ui.js index 8d4d62307a01..c4aa3553c248 100644 --- a/devtools/client/debugger/src/actions/ui.js +++ b/devtools/client/debugger/src/actions/ui.js @@ -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(), }; } diff --git a/devtools/client/debugger/src/components/Editor/ColumnBreakpoints.js b/devtools/client/debugger/src/components/Editor/ColumnBreakpoints.js index 33ccfad32527..cab02dbc547e 100644 --- a/devtools/client/debugger/src/components/Editor/ColumnBreakpoints.js +++ b/devtools/client/debugger/src/components/Editor/ColumnBreakpoints.js @@ -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; } diff --git a/devtools/client/debugger/src/components/Editor/index.js b/devtools/client/debugger/src/components/Editor/index.js index 38040e33140a..1ef334699868 100644 --- a/devtools/client/debugger/src/components/Editor/index.js +++ b/devtools/client/debugger/src/components/Editor/index.js @@ -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, + }) ); } diff --git a/devtools/client/debugger/src/selectors/visibleColumnBreakpoints.js b/devtools/client/debugger/src/selectors/visibleColumnBreakpoints.js index ce463bea1481..07b78c0a4009 100644 --- a/devtools/client/debugger/src/selectors/visibleColumnBreakpoints.js +++ b/devtools/client/debugger/src/selectors/visibleColumnBreakpoints.js @@ -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 && diff --git a/devtools/client/debugger/src/utils/editor/index.js b/devtools/client/debugger/src/utils/editor/index.js index 3146581fdd7f..5d834771e2ff 100644 --- a/devtools/client/debugger/src/utils/editor/index.js +++ b/devtools/client/debugger/src/utils/editor/index.js @@ -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 }, diff --git a/devtools/client/shared/sourceeditor/editor.js b/devtools/client/shared/sourceeditor/editor.js index 90e9f6e37313..e47c0f51ffe6 100644 --- a/devtools/client/shared/sourceeditor/editor.js +++ b/devtools/client/shared/sourceeditor/editor.js @@ -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} showLineContentDecorations - An extension which is an array containing the view - * which manages the rendering of the line content markers. + * @returns {Array} 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} 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