From a90a5cba08fdf6c0ceb95101c275108a152a3aed Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 12 Jun 2024 07:35:37 +0200 Subject: Merging upstream version 127.0. Signed-off-by: Daniel Baumann --- devtools/client/aboutdebugging/test/node/README.md | 2 +- devtools/client/accessibility/test/node/README.md | 2 +- devtools/client/bin/devtools-node-test-runner.js | 2 +- .../debugger/src/actions/context-menus/editor.js | 25 +- .../debugger/src/components/Editor/Breakpoints.css | 8 +- .../src/components/Editor/ConditionalPanel.css | 5 + .../src/components/Editor/ConditionalPanel.js | 72 ++- .../debugger/src/components/Editor/Editor.css | 4 +- .../debugger/src/components/Editor/Footer.js | 2 - .../src/components/Editor/HighlightLine.js | 132 +++-- .../src/components/Editor/HighlightLines.js | 44 +- .../src/components/Editor/InlinePreviews.js | 88 +++- .../client/debugger/src/components/Editor/index.js | 116 +++-- devtools/client/debugger/src/utils/editor/index.js | 44 -- .../debugger/src/utils/editor/tests/editor.spec.js | 12 - devtools/client/debugger/src/utils/source.js | 9 + .../mochitest/browser_dbg-event-breakpoints.js | 10 + .../mochitest/browser_dbg-javascript-tracer.js | 45 ++ .../test/mochitest/examples/event-breakpoints.js | 7 +- devtools/client/framework/test/browser.toml | 3 + .../framework/test/browser_toolbox_many_toggles.js | 50 ++ devtools/client/framework/test/node/README.md | 2 +- devtools/client/fronts/inspector/rule-rewriter.js | 100 ++-- devtools/client/fronts/walker.js | 7 - .../client/inspector/rules/test/browser_part2.toml | 2 + .../test/browser_rules_keyframes-rule-nested.js | 89 ++++ .../rules/test/browser_rules_pseudo-element_01.js | 99 +++- .../inspector/rules/test/doc_pseudoelement.html | 20 +- devtools/client/inspector/shared/utils.js | 6 +- .../test/browser_inspector_picker-shift-key.js | 35 +- devtools/client/inspector/test/head.js | 8 +- devtools/client/netmonitor/test/browser.toml | 4 + .../client/netmonitor/test/browser_net_clear.js | 8 +- .../browser_net_column_slow-request-indicator.js | 2 + .../netmonitor/test/browser_net_send-beacon.js | 23 + .../client/netmonitor/test/browser_net_zstd.js | 81 +++ .../test/html_send-beacon-late-iframe-request.html | 25 + .../netmonitor/test/html_zstd-test-page.html | 41 ++ .../test/sjs_content-type-test-server.sjs | 12 + .../performance-new/shared/background.sys.mjs | 23 +- devtools/client/performance-new/shared/utils.js | 7 + .../browser_interaction-between-interfaces.js | 17 +- .../components/object-inspector/utils/node.js | 5 +- devtools/client/shared/css-angle.js | 11 +- devtools/client/shared/output-parser.js | 316 ++++++----- devtools/client/shared/screenshot.js | 14 +- .../shared/sourceeditor/css-autocompleter.js | 578 +++++++++++---------- devtools/client/shared/sourceeditor/editor.js | 324 ++++++++++-- .../client/shared/test/browser_filter-editor-01.js | 5 +- .../client/shared/test/browser_outputparser.js | 16 + .../shared/test/xpcshell/test_parseDeclarations.js | 23 +- .../test/xpcshell/test_rewriteDeclarations.js | 4 +- .../client/shared/widgets/CubicBezierWidget.js | 18 +- devtools/client/shared/widgets/FilterWidget.js | 69 ++- .../shared/widgets/LinearEasingFunctionWidget.js | 16 +- devtools/client/storage/test/browser.toml | 1 + devtools/client/themes/images/tool-profiler.svg | 2 +- .../client/webconsole/components/Output/Message.js | 4 + .../webconsole/test/browser/_browser_console.toml | 3 + .../browser/browser_console_eager_eval_resolve.js | 64 +++ .../browser_jsterm_screenshot_command_file.js | 14 +- 61 files changed, 1964 insertions(+), 816 deletions(-) create mode 100644 devtools/client/framework/test/browser_toolbox_many_toggles.js create mode 100644 devtools/client/inspector/rules/test/browser_rules_keyframes-rule-nested.js create mode 100644 devtools/client/netmonitor/test/browser_net_zstd.js create mode 100644 devtools/client/netmonitor/test/html_send-beacon-late-iframe-request.html create mode 100644 devtools/client/netmonitor/test/html_zstd-test-page.html create mode 100644 devtools/client/webconsole/test/browser/browser_console_eager_eval_resolve.js (limited to 'devtools/client') diff --git a/devtools/client/aboutdebugging/test/node/README.md b/devtools/client/aboutdebugging/test/node/README.md index 58cbe55691..f6a37e2607 100644 --- a/devtools/client/aboutdebugging/test/node/README.md +++ b/devtools/client/aboutdebugging/test/node/README.md @@ -19,4 +19,4 @@ The tests run on try on linux64 platforms. The complete name of try job is `devt Adding the tests to a try push depends on the try selector you are using. - try fuzzy: look for the job named `source-test-node-devtools-tests` -The configuration file for try can be found at `taskcluster/ci/source-test/node.yml` +The configuration file for try can be found at `taskcluster/kinds/source-test/node.yml` diff --git a/devtools/client/accessibility/test/node/README.md b/devtools/client/accessibility/test/node/README.md index 4b40aacffe..574f886e94 100644 --- a/devtools/client/accessibility/test/node/README.md +++ b/devtools/client/accessibility/test/node/README.md @@ -19,4 +19,4 @@ The tests run on try on linux64 platforms. The complete name of try job is `devt Adding the tests to a try push depends on the try selector you are using. - try fuzzy: look for the job named `source-test-node-devtools-tests` -The configuration file for try can be found at `taskcluster/ci/source-test/node.yml` +The configuration file for try can be found at `taskcluster/kinds/source-test/node.yml` diff --git a/devtools/client/bin/devtools-node-test-runner.js b/devtools/client/bin/devtools-node-test-runner.js index 087664b957..3c08b6577f 100644 --- a/devtools/client/bin/devtools-node-test-runner.js +++ b/devtools/client/bin/devtools-node-test-runner.js @@ -10,7 +10,7 @@ * This is a test runner dedicated to run DevTools node tests continuous integration * platforms. It will parse the logs to output errors compliant with treeherder tooling. * - * See taskcluster/ci/source-test/node.yml for the definition of the task running those + * See taskcluster/kinds/source-test/node.yml for the definition of the task running those * tests on try. */ diff --git a/devtools/client/debugger/src/actions/context-menus/editor.js b/devtools/client/debugger/src/actions/context-menus/editor.js index 4785c963cb..2b82628f1a 100644 --- a/devtools/client/debugger/src/actions/context-menus/editor.js +++ b/devtools/client/debugger/src/actions/context-menus/editor.js @@ -39,7 +39,7 @@ import { toggleBlackBox } from "../../actions/sources/blackbox"; import { addExpression } from "../../actions/expressions"; import { evaluateInConsole } from "../../actions/toolbox"; -export function showEditorContextMenu(event, editor, location) { +export function showEditorContextMenu(event, editor, lineObject, location) { return async ({ dispatch, getState }) => { const { source } = location; const state = getState(); @@ -63,9 +63,9 @@ export function showEditorContextMenu(event, editor, location) { location, isPaused, editorWrappingEnabled, - selectionText: editor.codeMirror.getSelection().trim(), - isTextSelected: editor.codeMirror.somethingSelected(), - editor, + selectionText: editor.getSelectedText(), + isTextSelected: editor.isTextSelected(), + lineObject, isSourceOnIgnoreList, dispatch, }) @@ -339,7 +339,7 @@ function editorMenuItems({ isTextSelected, isPaused, editorWrappingEnabled, - editor, + lineObject, isSourceOnIgnoreList, dispatch, }) { @@ -368,14 +368,8 @@ function editorMenuItems({ blackBoxMenuItem(source, blackboxedRanges, isSourceOnIgnoreList, dispatch) ); - const startLine = toSourceLine( - source.id, - editor.codeMirror.getCursor("from").line - ); - const endLine = toSourceLine( - source.id, - editor.codeMirror.getCursor("to").line - ); + const startLine = toSourceLine(source.id, lineObject.from.line); + const endLine = toSourceLine(source.id, lineObject.to.line); // Find any blackbox ranges that exist for the selected lines const blackboxRange = findBlackBoxRange(source, blackboxedRanges, { @@ -400,10 +394,7 @@ function editorMenuItems({ items.push( blackBoxSourceLinesMenuItem( source, - { - from: editor.codeMirror.getCursor("from"), - to: editor.codeMirror.getCursor("to"), - }, + lineObject, blackboxedRanges, isSourceOnIgnoreList, null, diff --git a/devtools/client/debugger/src/components/Editor/Breakpoints.css b/devtools/client/debugger/src/components/Editor/Breakpoints.css index 92121e0f46..4983688031 100644 --- a/devtools/client/debugger/src/components/Editor/Breakpoints.css +++ b/devtools/client/debugger/src/components/Editor/Breakpoints.css @@ -95,12 +95,16 @@ right: -16px; } -.new-breakpoint.has-condition .CodeMirror-gutter-wrapper svg { +.new-breakpoint.has-condition .CodeMirror-gutter-wrapper svg, +/* Codemirror 6*/ +.cm6-gutter-breakpoint .breakpoint-marker.has-condition svg { fill: var(--breakpoint-condition-fill); stroke: var(--breakpoint-condition-stroke); } -.new-breakpoint.has-log .CodeMirror-gutter-wrapper svg { +.new-breakpoint.has-log .CodeMirror-gutter-wrapper svg, +/* Codemirror 6*/ +.cm6-gutter-breakpoint .breakpoint-marker.has-log svg { fill: var(--logpoint-fill); stroke: var(--logpoint-stroke); } diff --git a/devtools/client/debugger/src/components/Editor/ConditionalPanel.css b/devtools/client/debugger/src/components/Editor/ConditionalPanel.css index 4ce8dbcd8c..1aeac91604 100644 --- a/devtools/client/debugger/src/components/Editor/ConditionalPanel.css +++ b/devtools/client/debugger/src/components/Editor/ConditionalPanel.css @@ -37,3 +37,8 @@ /* Match the color of the placeholder text to existing inputs in the Debugger */ color: var(--theme-text-color-alt); } + +/* Removing the line padding for Codemirror 6 */ +.cm-line:has(div.conditional-breakpoint-panel) { + padding: 0; +} diff --git a/devtools/client/debugger/src/components/Editor/ConditionalPanel.js b/devtools/client/debugger/src/components/Editor/ConditionalPanel.js index 97876f2f00..4e6f0b58ea 100644 --- a/devtools/client/debugger/src/components/Editor/ConditionalPanel.js +++ b/devtools/client/debugger/src/components/Editor/ConditionalPanel.js @@ -11,7 +11,8 @@ import ReactDOM from "devtools/client/shared/vendor/react-dom"; import PropTypes from "devtools/client/shared/vendor/react-prop-types"; import { connect } from "devtools/client/shared/vendor/react-redux"; import { toEditorLine } from "../../utils/editor/index"; -import { prefs } from "../../utils/prefs"; +import { createEditor } from "../../utils/editor/create-editor"; +import { prefs, features } from "../../utils/prefs"; import actions from "../../actions/index"; import { @@ -21,6 +22,7 @@ import { } from "../../selectors/index"; const classnames = require("resource://devtools/client/shared/classnames.js"); +const CONDITIONAL_BP_MARKER = "conditional-breakpoint-panel-marker"; function addNewLine(doc) { const cursor = doc.getCursor(); @@ -49,6 +51,7 @@ export class ConditionalPanel extends PureComponent { log: PropTypes.bool.isRequired, openConditionalPanel: PropTypes.func.isRequired, setBreakpointOptions: PropTypes.func.isRequired, + selectedSource: PropTypes.object.isRequired, }; } @@ -111,17 +114,53 @@ export class ConditionalPanel extends PureComponent { } }; + showConditionalPanel(prevProps) { + const { location, editor, breakpoint, selectedSource } = this.props; + // When breakpoint is removed + if (prevProps?.breakpoint && !breakpoint) { + editor.removeLineContentMarker(CONDITIONAL_BP_MARKER); + return; + } + if (selectedSource.id !== location.source.id) { + editor.removeLineContentMarker(CONDITIONAL_BP_MARKER); + return; + } + const editorLine = toEditorLine(location.source.id, location.line || 0); + editor.setLineContentMarker({ + id: CONDITIONAL_BP_MARKER, + condition: line => line == editorLine, + createLineElementNode: () => { + // Create a Codemirror 5 editor for the breakpoint panel + // TODO: Switch to use Codemirror 6 version Bug 1890205 + const breakpointPanelEditor = createEditor(); + breakpointPanelEditor.appendToLocalElement( + document.createElement("div") + ); + return this.renderConditionalPanel(this.props, breakpointPanelEditor); + }, + }); + } + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 UNSAFE_componentWillMount() { - return this.renderToWidget(this.props); + if (features.codemirrorNext) { + this.showConditionalPanel(); + } else { + this.renderToWidget(this.props); + } } // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 UNSAFE_componentWillUpdate() { - return this.clearConditionalPanel(); + if (!features.codemirrorNext) { + this.clearConditionalPanel(); + } } - componentDidUpdate() { + componentDidUpdate(prevProps) { + if (features.codemirrorNext) { + this.showConditionalPanel(prevProps); + } this.keepFocusOnInput(); } @@ -129,7 +168,12 @@ export class ConditionalPanel extends PureComponent { // This is called if CodeMirror is re-initializing itself before the // user closes the conditional panel. Clear the widget, and re-render it // as soon as this component gets remounted - return this.clearConditionalPanel(); + const { editor } = this.props; + if (features.codemirrorNext) { + editor.removeLineContentMarker(CONDITIONAL_BP_MARKER); + } else { + this.clearConditionalPanel(); + } } renderToWidget(props) { @@ -141,7 +185,7 @@ export class ConditionalPanel extends PureComponent { const editorLine = toEditorLine(location.source.id, location.line || 0); this.cbPanel = editor.codeMirror.addLineWidget( editorLine, - this.renderConditionalPanel(props), + this.renderConditionalPanel(props, editor), { coverGutter: true, noHScroll: true, @@ -168,8 +212,8 @@ export class ConditionalPanel extends PureComponent { } } - createEditor = input => { - const { log, editor, closeConditionalPanel } = this.props; + createEditor = (input, editor) => { + const { log, closeConditionalPanel } = this.props; const codeMirror = editor.CodeMirror.fromTextArea(input, { mode: "javascript", theme: "mozilla", @@ -189,8 +233,12 @@ export class ConditionalPanel extends PureComponent { codeMirror.on("blur", (cm, e) => { if ( - e?.relatedTarget && - e.relatedTarget.closest(".conditional-breakpoint-panel") + // if there is no event + // or if the focus is the conditional panel + // do not close the conditional panel + !e || + (e?.relatedTarget && + e.relatedTarget.closest(".conditional-breakpoint-panel")) ) { return; } @@ -217,7 +265,7 @@ export class ConditionalPanel extends PureComponent { return log ? options.logValue : options.condition; } - renderConditionalPanel(props) { + renderConditionalPanel(props, editor) { const { log } = props; const defaultValue = this.getDefaultValue(); @@ -239,7 +287,7 @@ export class ConditionalPanel extends PureComponent { ), textarea({ defaultValue, - ref: input => this.createEditor(input), + ref: input => this.createEditor(input, editor), }) ), panel diff --git a/devtools/client/debugger/src/components/Editor/Editor.css b/devtools/client/debugger/src/components/Editor/Editor.css index f28833747d..d2f55ed6db 100644 --- a/devtools/client/debugger/src/components/Editor/Editor.css +++ b/devtools/client/debugger/src/components/Editor/Editor.css @@ -146,7 +146,9 @@ html[dir="rtl"] .editor-mount { .new-debug-line-error .CodeMirror-activeline-background { display: none; } -.highlight-line .CodeMirror-line { +.highlight-line .CodeMirror-line, +/* For CM6 */ +.cm-editor .cm-line.highlight-line { animation-name: fade-highlight-out; animation-duration: var(--highlight-line-duration); animation-timing-function: ease-out; diff --git a/devtools/client/debugger/src/components/Editor/Footer.js b/devtools/client/debugger/src/components/Editor/Footer.js index 69c7b52b68..278bc1dad7 100644 --- a/devtools/client/debugger/src/components/Editor/Footer.js +++ b/devtools/client/debugger/src/components/Editor/Footer.js @@ -459,8 +459,6 @@ const mapStateToProps = state => { : null, isSourceActorWithSourceMap: isSourceActorWithSourceMapProp, - sourceMapURL: selectedLocation?.sourceActor.sourceMapURL, - areSourceMapsEnabled: areSourceMapsEnabledProp, shouldSelectOriginalLocation: getShouldSelectOriginalLocation(state), }; diff --git a/devtools/client/debugger/src/components/Editor/HighlightLine.js b/devtools/client/debugger/src/components/Editor/HighlightLine.js index 8639128905..0df0fa482e 100644 --- a/devtools/client/debugger/src/components/Editor/HighlightLine.js +++ b/devtools/client/debugger/src/components/Editor/HighlightLine.js @@ -2,10 +2,20 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at . */ +/** + * Uses of this panel are:- + * - Highlight line when source is opened using view source links from other panels + * - Highlight line with function or class from an Outline search result selection + * - Highlight line from a Quick open panel search result selection + * - Highlight the last selected line when a source is selected + * - Highlight the breakpoint line when the breakpoint is selected + */ + import { Component } from "devtools/client/shared/vendor/react"; import PropTypes from "devtools/client/shared/vendor/react-prop-types"; import { toEditorLine, + fromEditorLine, endOperation, startOperation, } from "../../utils/editor/index"; @@ -20,6 +30,7 @@ import { getCurrentThread, getShouldHighlightSelectedLocation, } from "../../selectors/index"; +import { features } from "../../utils/prefs"; function isDebugLine(selectedFrame, selectedLocation) { if (!selectedFrame) { @@ -32,14 +43,6 @@ function isDebugLine(selectedFrame, selectedLocation) { ); } -function isDocumentReady(selectedLocation, selectedSourceTextContent) { - return ( - selectedLocation && - selectedSourceTextContent && - hasDocument(selectedLocation.source.id) - ); -} - export class HighlightLine extends Component { isStepping = false; previousEditorLine = null; @@ -56,30 +59,34 @@ export class HighlightLine extends Component { selectedFrame: PropTypes.object, selectedLocation: PropTypes.object.isRequired, selectedSourceTextContent: PropTypes.object.isRequired, + shouldHighlightSelectedLocation: PropTypes.func.isRequired, + editor: PropTypes.object, }; } shouldComponentUpdate(nextProps) { - const { selectedLocation, selectedSourceTextContent } = nextProps; - return this.shouldSetHighlightLine( - selectedLocation, - selectedSourceTextContent - ); + return this.shouldSetHighlightLine(nextProps); } componentDidUpdate(prevProps) { - this.completeHighlightLine(prevProps); + this.highlightLine(prevProps); } componentDidMount() { - this.completeHighlightLine(null); + this.highlightLine(null); } - shouldSetHighlightLine(selectedLocation, selectedSourceTextContent) { - const { line } = selectedLocation; - const editorLine = toEditorLine(selectedLocation.source.id, line); + shouldSetHighlightLine({ selectedLocation, selectedSourceTextContent }) { + const editorLine = toEditorLine( + selectedLocation.source.id, + selectedLocation.line + ); - if (!isDocumentReady(selectedLocation, selectedSourceTextContent)) { + if ( + !selectedLocation || + !selectedSourceTextContent || + (!features.codemirrorNext && !hasDocument(selectedLocation.source.id)) + ) { return false; } @@ -90,58 +97,61 @@ export class HighlightLine extends Component { return true; } - completeHighlightLine(prevProps) { - const { - pauseCommand, - selectedLocation, - selectedFrame, - selectedSourceTextContent, - shouldHighlightSelectedLocation, - } = this.props; + highlightLine(prevProps) { + const { pauseCommand, shouldHighlightSelectedLocation } = this.props; if (pauseCommand) { this.isStepping = true; } - startOperation(); + if (!features.codemirrorNext) { + startOperation(); + } if (prevProps) { - this.clearHighlightLine( - prevProps.selectedLocation, - prevProps.selectedSourceTextContent - ); + this.clearHighlightLine(prevProps); } if (shouldHighlightSelectedLocation) { - this.setHighlightLine( - selectedLocation, - selectedFrame, - selectedSourceTextContent - ); + this.setHighlightLine(); + } + if (!features.codemirrorNext) { + endOperation(); } - endOperation(); } - setHighlightLine(selectedLocation, selectedFrame, selectedSourceTextContent) { - const { line } = selectedLocation; - if ( - !this.shouldSetHighlightLine(selectedLocation, selectedSourceTextContent) - ) { + setHighlightLine() { + const { selectedLocation, selectedFrame, editor } = this.props; + if (!this.shouldSetHighlightLine(this.props)) { return; } this.isStepping = false; const sourceId = selectedLocation.source.id; - const editorLine = toEditorLine(sourceId, line); + const editorLine = toEditorLine(sourceId, selectedLocation.line); this.previousEditorLine = editorLine; - if (!line || isDebugLine(selectedFrame, selectedLocation)) { + if ( + !selectedLocation.line || + isDebugLine(selectedFrame, selectedLocation) + ) { return; } - const doc = getDocument(sourceId); - doc.addLineClass(editorLine, "wrap", "highlight-line"); - this.resetHighlightLine(doc, editorLine); + if (features.codemirrorNext) { + editor.setLineContentMarker({ + id: "highlight-line-marker", + lineClassName: "highlight-line", + condition(line) { + const lineNumber = fromEditorLine(sourceId, line); + return selectedLocation.line == lineNumber; + }, + }); + } else { + const doc = getDocument(sourceId); + doc.addLineClass(editorLine, "wrap", "highlight-line"); + } + this.clearHighlightLineAfterDuration(); } - resetHighlightLine(doc, editorLine) { + clearHighlightLineAfterDuration() { const editorWrapper = document.querySelector(".editor-wrapper"); if (editorWrapper === null) { @@ -155,20 +165,28 @@ export class HighlightLine extends Component { 10 ); - setTimeout( - () => doc && doc.removeLineClass(editorLine, "wrap", "highlight-line"), - duration - ); + setTimeout(() => this.clearHighlightLine(this.props), duration); } - clearHighlightLine(selectedLocation, selectedSourceTextContent) { - if (!isDocumentReady(selectedLocation, selectedSourceTextContent)) { + clearHighlightLine({ selectedLocation, selectedSourceTextContent }) { + if (!selectedLocation || !selectedSourceTextContent) { + return; + } + + if (features.codemirrorNext) { + const { editor } = this.props; + if (editor) { + editor.removeLineContentMarker("highlight-line-marker"); + } + return; + } + + if (!hasDocument(selectedLocation.source.id)) { return; } - const { line } = selectedLocation; const sourceId = selectedLocation.source.id; - const editorLine = toEditorLine(sourceId, line); + const editorLine = toEditorLine(sourceId, selectedLocation.line); const doc = getDocument(sourceId); doc.removeLineClass(editorLine, "wrap", "highlight-line"); } diff --git a/devtools/client/debugger/src/components/Editor/HighlightLines.js b/devtools/client/debugger/src/components/Editor/HighlightLines.js index e34a86aba9..e62125e5f8 100644 --- a/devtools/client/debugger/src/components/Editor/HighlightLines.js +++ b/devtools/client/debugger/src/components/Editor/HighlightLines.js @@ -2,8 +2,15 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at . */ +/** + * Uses of this panel are:- + * - Highlighting lines of a function selected to be copied using the "Copy function" context menu in the Outline panel + */ + import { Component } from "devtools/client/shared/vendor/react"; import PropTypes from "devtools/client/shared/vendor/react-prop-types"; +import { fromEditorLine } from "../../utils/editor/index"; +import { features } from "../../utils/prefs"; class HighlightLines extends Component { static get propTypes() { @@ -33,9 +40,19 @@ class HighlightLines extends Component { clearHighlightRange() { const { range, editor } = this.props; - const { codeMirror } = editor; + if (!range) { + return; + } - if (!range || !codeMirror) { + if (features.codemirrorNext) { + if (editor) { + editor.removeLineContentMarker("multi-highlight-line-marker"); + } + return; + } + + const { codeMirror } = editor; + if (!codeMirror) { return; } @@ -50,14 +67,31 @@ class HighlightLines extends Component { highlightLineRange = () => { const { range, editor } = this.props; - const { codeMirror } = editor; + if (!range) { + return; + } - if (!range || !codeMirror) { + if (features.codemirrorNext) { + // TODO: Fix scrolling into view if its out Bug 1894725 + if (editor) { + editor.setLineContentMarker({ + id: "multi-highlight-line-marker", + lineClassName: "highlight-lines", + condition(line) { + const lineNumber = fromEditorLine(null, line); + return lineNumber >= range.start && lineNumber <= range.end; + }, + }); + } return; } - const { start, end } = range; + const { codeMirror } = editor; + if (!codeMirror) { + return; + } + const { start, end } = range; codeMirror.operation(() => { editor.alignLine(start); for (let line = start - 1; line < end; line++) { diff --git a/devtools/client/debugger/src/components/Editor/InlinePreviews.js b/devtools/client/debugger/src/components/Editor/InlinePreviews.js index ba8b08669a..56c5f5021f 100644 --- a/devtools/client/debugger/src/components/Editor/InlinePreviews.js +++ b/devtools/client/debugger/src/components/Editor/InlinePreviews.js @@ -3,9 +3,13 @@ * file, You can obtain one at . */ import React, { Component } from "devtools/client/shared/vendor/react"; +import ReactDOM from "devtools/client/shared/vendor/react-dom"; + +import actions from "../../actions/index"; import { div } from "devtools/client/shared/vendor/react-dom-factories"; import PropTypes from "devtools/client/shared/vendor/react-prop-types"; import InlinePreviewRow from "./InlinePreviewRow"; +import InlinePreview from "./InlinePreview"; import { connect } from "devtools/client/shared/vendor/react-redux"; import { getSelectedFrame, @@ -13,6 +17,8 @@ import { getInlinePreviews, } from "../../selectors/index"; +import { features } from "../../utils/prefs"; + function hasPreviews(previews) { return !!previews && !!Object.keys(previews).length; } @@ -31,9 +37,85 @@ class InlinePreviews extends Component { return hasPreviews(previews); } + componentDidMount() { + this.renderInlinePreviewMarker(); + } + + componentDidUpdate() { + this.renderInlinePreviewMarker(); + } + + renderInlinePreviewMarker() { + const { + editor, + selectedFrame, + selectedSource, + previews, + openElementInInspector, + highlightDomElement, + unHighlightDomElement, + } = this.props; + + if (!features.codemirrorNext) { + return; + } + + if ( + !editor || + !selectedFrame || + selectedFrame.location.source.id !== selectedSource.id || + !hasPreviews(previews) + ) { + editor.removeLineContentMarker("inline-preview-marker"); + return; + } + editor.setLineContentMarker({ + id: "inline-preview-marker", + condition: line => { + // CM6 line is 1-based unlike CM5 which is 0-based. + return !!previews[line - 1]; + }, + createLineElementNode: line => { + const widgetNode = document.createElement("div"); + widgetNode.className = "inline-preview"; + + ReactDOM.render( + React.createElement( + React.Fragment, + null, + previews[line - 1].map(preview => + React.createElement(InlinePreview, { + line, + key: `${line}-${preview.name}`, + variable: preview.name, + value: preview.value, + openElementInInspector, + highlightDomElement, + unHighlightDomElement, + }) + ) + ), + widgetNode + ); + return widgetNode; + }, + }); + } + + componentWillUnmount() { + if (!features.codemirrorNext) { + return; + } + this.props.editor.removeLineContentMarker("inline-preview-marker"); + } + render() { const { editor, selectedFrame, selectedSource, previews } = this.props; + if (features.codemirrorNext) { + return null; + } + // Render only if currently open file is the one where debugger is paused if ( !selectedFrame || @@ -77,4 +159,8 @@ const mapStateToProps = state => { }; }; -export default connect(mapStateToProps)(InlinePreviews); +export default connect(mapStateToProps, { + openElementInInspector: actions.openElementInInspectorCommand, + highlightDomElement: actions.highlightDomElement, + unHighlightDomElement: actions.unHighlightDomElement, +})(InlinePreviews); diff --git a/devtools/client/debugger/src/components/Editor/index.js b/devtools/client/debugger/src/components/Editor/index.js index e21e05c11a..38040e3314 100644 --- a/devtools/client/debugger/src/components/Editor/index.js +++ b/devtools/client/debugger/src/components/Editor/index.js @@ -61,7 +61,6 @@ import { lineAtHeight, toSourceLine, getDocument, - scrollToPosition, toEditorPosition, getSourceLocationFromMouseEvent, hasDocument, @@ -149,15 +148,15 @@ class Editor extends PureComponent { this.props.selectedSourceTextContent?.value || nextProps.symbols !== this.props.symbols; + const shouldScroll = + nextProps.selectedLocation && + this.shouldScrollToLocation(nextProps, editor); + if (!features.codemirrorNext) { const shouldUpdateSize = nextProps.startPanelSize !== this.props.startPanelSize || nextProps.endPanelSize !== this.props.endPanelSize; - const shouldScroll = - nextProps.selectedLocation && - this.shouldScrollToLocation(nextProps, editor); - if (shouldUpdateText || shouldUpdateSize || shouldScroll) { startOperation(); if (shouldUpdateText) { @@ -183,6 +182,10 @@ class Editor extends PureComponent { if (shouldUpdateText) { this.setText(nextProps, editor); } + + if (shouldScroll) { + this.scrollToLocation(nextProps, editor); + } } } @@ -238,7 +241,12 @@ class Editor extends PureComponent { editor.setUpdateListener(this.onEditorUpdated); editor.setGutterEventListeners({ click: (event, cm, line) => this.onGutterClick(cm, line, null, event), - contextmenu: (event, cm, line) => this.openMenu(event, line, true), + contextmenu: (event, cm, line) => this.openMenu(event, line), + }); + editor.setContentEventListeners({ + click: (event, cm, line, column) => this.onClick(event, line, column), + contextmenu: (event, cm, line, column) => + this.openMenu(event, line, column), }); } this.setState({ editor }); @@ -278,7 +286,7 @@ class Editor extends PureComponent { } }; - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps, prevState) { const { selectedSource, blackboxedRanges, @@ -295,7 +303,9 @@ class Editor extends PureComponent { if (features.codemirrorNext) { const shouldUpdateBreakableLines = prevProps.breakableLines.size !== this.props.breakableLines.size || - prevProps.selectedSource?.id !== selectedSource.id; + prevProps.selectedSource?.id !== selectedSource.id || + // Make sure we update after the editor has loaded + (!prevState.editor && !!editor); const isSourceWasm = isWasm(selectedSource.id); @@ -459,7 +469,7 @@ class Editor extends PureComponent { }; // Note: The line is optional, if not passed (as is likely for codemirror 6) // it fallsback to lineAtHeight. - openMenu(event, line) { + openMenu(event, line, ch) { event.stopPropagation(); event.preventDefault(); @@ -508,7 +518,7 @@ class Editor extends PureComponent { line ).trim(); - const lineObject = { from: { line }, to: { line } }; + const lineObject = { from: { line, ch }, to: { line, ch } }; this.props.showEditorGutterContextMenu( event, @@ -523,13 +533,23 @@ class Editor extends PureComponent { return; } - const location = getSourceLocationFromMouseEvent( - editor, - selectedSource, - event - ); + let location; + if (features.codemirrorNext) { + location = createLocation({ + source: selectedSource, + line: fromEditorLine( + selectedSource.id, + line, + isWasm(selectedSource.id) + ), + column: isWasm(selectedSource.id) ? 0 : ch + 1, + }); + } else { + location = getSourceLocationFromMouseEvent(editor, selectedSource, event); + } - this.props.showEditorContextMenu(event, editor, location); + const lineObject = editor.getSelectionCursor(); + this.props.showEditorContextMenu(event, editor, lineObject, location); } /** @@ -617,16 +637,29 @@ class Editor extends PureComponent { ); }; - onClick(e) { + onClick(e, line, ch) { const { selectedSource, updateCursorPosition, jumpToMappedLocation } = this.props; if (selectedSource) { - const sourceLocation = getSourceLocationFromMouseEvent( - this.state.editor, - selectedSource, - e - ); + let sourceLocation; + if (features.codemirrorNext) { + sourceLocation = createLocation({ + source: selectedSource, + line: fromEditorLine( + selectedSource.id, + line, + isWasm(selectedSource.id) + ), + column: isWasm(selectedSource.id) ? 0 : ch + 1, + }); + } else { + sourceLocation = getSourceLocationFromMouseEvent( + this.state.editor, + selectedSource, + e + ); + } if (e.metaKey && e.altKey) { jumpToMappedLocation(sourceLocation); @@ -664,8 +697,7 @@ class Editor extends PureComponent { const lineText = doc.getLine(line); column = Math.max(column, getIndentation(lineText)); } - - scrollToPosition(editor.codeMirror, line, column); + editor.scrollTo(line, column); } setText(props, editor) { @@ -774,6 +806,7 @@ class Editor extends PureComponent { }; } + // eslint-disable-next-line complexity renderItems() { const { selectedSource, @@ -788,19 +821,44 @@ class Editor extends PureComponent { } = this.props; const { editor } = this.state; + if (!selectedSource || !editor) { + return null; + } + if (features.codemirrorNext) { return React.createElement( React.Fragment, null, - React.createElement(Breakpoints, { - editor, - }), + React.createElement(Breakpoints, { editor }), React.createElement(DebugLine, { editor, selectedSource }), - React.createElement(Exceptions, { editor }) + React.createElement(HighlightLine, { editor }), + React.createElement(Exceptions, { editor }), + conditionalPanelLocation + ? React.createElement(ConditionalPanel, { + editor, + selectedSource, + }) + : null, + isPaused && + inlinePreviewEnabled && + (!selectedSource.isOriginal || + selectedSource.isPrettyPrinted || + mapScopesEnabled) + ? React.createElement(InlinePreviews, { + editor, + selectedSource, + }) + : null, + highlightedLineRange + ? React.createElement(HighlightLines, { + editor, + range: highlightedLineRange, + }) + : null ); } - if (!selectedSource || !editor || !getDocument(selectedSource.id)) { + if (!getDocument(selectedSource.id)) { return null; } return div( diff --git a/devtools/client/debugger/src/utils/editor/index.js b/devtools/client/debugger/src/utils/editor/index.js index e729388acf..3146581fdd 100644 --- a/devtools/client/debugger/src/utils/editor/index.js +++ b/devtools/client/debugger/src/utils/editor/index.js @@ -106,50 +106,6 @@ export function toSourceLine(sourceId, line) { return line + 1; } -export function scrollToPosition(codeMirror, line, column) { - // For all cases where these are on the first line and column, - // avoid the possibly slow computation of cursor location on large bundles. - if (!line && !column) { - codeMirror.scrollTo(0, 0); - return; - } - - const { top, left } = codeMirror.charCoords({ line, ch: column }, "local"); - - if (!isVisible(codeMirror, top, left)) { - const scroller = codeMirror.getScrollerElement(); - const centeredX = Math.max(left - scroller.offsetWidth / 2, 0); - const centeredY = Math.max(top - scroller.offsetHeight / 2, 0); - - codeMirror.scrollTo(centeredX, centeredY); - } -} - -function isVisible(codeMirror, top, left) { - function withinBounds(x, min, max) { - return x >= min && x <= max; - } - - const scrollArea = codeMirror.getScrollInfo(); - const charWidth = codeMirror.defaultCharWidth(); - const fontHeight = codeMirror.defaultTextHeight(); - const { scrollTop, scrollLeft } = codeMirror.doc; - - const inXView = withinBounds( - left, - scrollLeft, - scrollLeft + (scrollArea.clientWidth - 30) - charWidth - ); - - const inYView = withinBounds( - top, - scrollTop, - scrollTop + scrollArea.clientHeight - fontHeight - ); - - return inXView && inYView; -} - export function getLocationsInViewport( { codeMirror }, // Offset represents an allowance of characters or lines offscreen to improve diff --git a/devtools/client/debugger/src/utils/editor/tests/editor.spec.js b/devtools/client/debugger/src/utils/editor/tests/editor.spec.js index b3fcad17ff..3917adaba1 100644 --- a/devtools/client/debugger/src/utils/editor/tests/editor.spec.js +++ b/devtools/client/debugger/src/utils/editor/tests/editor.spec.js @@ -6,7 +6,6 @@ import { toEditorLine, toEditorPosition, toSourceLine, - scrollToPosition, markText, lineAtHeight, getSourceLocationFromMouseEvent, @@ -82,17 +81,6 @@ const codeMirror = { const editor = { codeMirror }; -describe("scrollToPosition", () => { - it("calls codemirror APIs charCoords, getScrollerElement, scrollTo", () => { - scrollToPosition(codeMirror, 60, 123); - expect(codeMirror.charCoords).toHaveBeenCalledWith( - { line: 60, ch: 123 }, - "local" - ); - expect(codeMirror.scrollTo).toHaveBeenCalledWith(0, 50); - }); -}); - describe("markText", () => { it("calls codemirror API markText & returns marker", () => { const loc = { diff --git a/devtools/client/debugger/src/utils/source.js b/devtools/client/debugger/src/utils/source.js index 91a02778e2..1a2b453ec2 100644 --- a/devtools/client/debugger/src/utils/source.js +++ b/devtools/client/debugger/src/utils/source.js @@ -209,6 +209,15 @@ function resolveFileURL( } export function getFormattedSourceId(id) { + if (typeof id != "string") { + console.error( + "Expected source id to be a string, got", + typeof id, + " | id:", + id + ); + return ""; + } return id.substring(id.lastIndexOf("/") + 1); } diff --git a/devtools/client/debugger/test/mochitest/browser_dbg-event-breakpoints.js b/devtools/client/debugger/test/mochitest/browser_dbg-event-breakpoints.js index 1065674186..49b3d3ffce 100644 --- a/devtools/client/debugger/test/mochitest/browser_dbg-event-breakpoints.js +++ b/devtools/client/debugger/test/mochitest/browser_dbg-event-breakpoints.js @@ -7,6 +7,7 @@ add_task(async function () { await pushPref("dom.element.invokers.enabled", true); await pushPref("dom.element.popover.enabled", true); + await pushPref("dom.events.textevent.enabled", true); const dbg = await initDebugger( "doc-event-breakpoints.html", @@ -151,6 +152,15 @@ add_task(async function () { assertPausedAtSourceAndLine(dbg, eventBreakpointsSource.id, 63); await resume(dbg); + info("Test textInput"); + await toggleEventBreakpoint(dbg, "Keyboard", "event.keyboard.textInput"); + invokeOnElement("#focus-text", "focus"); + EventUtils.sendChar("N"); + await waitForPaused(dbg); + assertPausedAtSourceAndLine(dbg, eventBreakpointsSource.id, 98); + await resume(dbg); + await toggleEventBreakpoint(dbg, "Keyboard", "event.keyboard.textInput"); + info(`Check that breakpoint can be set on "scrollend"`); await toggleEventBreakpoint(dbg, "Control", "event.control.scrollend"); diff --git a/devtools/client/debugger/test/mochitest/browser_dbg-javascript-tracer.js b/devtools/client/debugger/test/mochitest/browser_dbg-javascript-tracer.js index bfa2447474..29335af768 100644 --- a/devtools/client/debugger/test/mochitest/browser_dbg-javascript-tracer.js +++ b/devtools/client/debugger/test/mochitest/browser_dbg-javascript-tracer.js @@ -12,6 +12,19 @@ add_task(async function () { const dbg = await initDebugger("doc-scripts.html"); + // Add an iframe before starting the tracer to later check for key event on it + const preExistingIframeBrowsingContext = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + async function () { + const iframe = content.document.createElement("iframe"); + iframe.src = `data:text/html,`; + content.document.body.appendChild(iframe); + await new Promise(resolve => (iframe.onload = resolve)); + return iframe.contentWindow.browsingContext; + } + ); + info("Enable the tracing"); await clickElement(dbg, "trace"); @@ -55,6 +68,38 @@ add_task(async function () { await hasConsoleMessage(dbg, "DOM | click"); await hasConsoleMessage(dbg, "λ simple"); + const iframeBrowsingContext = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + async function () { + const iframe = content.document.createElement("iframe"); + iframe.src = `data:text/html,`; + content.document.body.appendChild(iframe); + await new Promise(resolve => (iframe.onload = resolve)); + iframe.contentWindow.document.querySelector("input").focus(); + return iframe.contentWindow.browsingContext; + } + ); + + await BrowserTestUtils.synthesizeKey("x", {}, iframeBrowsingContext); + await hasConsoleMessage(dbg, "DOM | keypress"); + await hasConsoleMessage(dbg, "λ onkeypress"); + + await SpecialPowers.spawn( + preExistingIframeBrowsingContext, + [], + async function () { + content.document.querySelector("input").focus(); + } + ); + await BrowserTestUtils.synthesizeKey( + "x", + {}, + preExistingIframeBrowsingContext + ); + await hasConsoleMessage(dbg, "DOM | keydown"); + await hasConsoleMessage(dbg, "λ onkeydown"); + // Test Blackboxing info("Clear the console from previous traces"); const { hud } = await dbg.toolbox.getPanel("webconsole"); diff --git a/devtools/client/debugger/test/mochitest/examples/event-breakpoints.js b/devtools/client/debugger/test/mochitest/examples/event-breakpoints.js index f9aa16b858..3d8163a8c4 100644 --- a/devtools/client/debugger/test/mochitest/examples/event-breakpoints.js +++ b/devtools/client/debugger/test/mochitest/examples/event-breakpoints.js @@ -91,4 +91,9 @@ function onBeforeToggle(event) { popover.addEventListener("toggle", onToggle); function onToggle(event) { console.log("toggle", event); -} \ No newline at end of file +} + +document.getElementById("focus-text").addEventListener("textInput", onTextInput); +function onTextInput() { + console.log("textInput"); +} diff --git a/devtools/client/framework/test/browser.toml b/devtools/client/framework/test/browser.toml index e7d979ebb3..c0faa66301 100644 --- a/devtools/client/framework/test/browser.toml +++ b/devtools/client/framework/test/browser.toml @@ -183,6 +183,9 @@ skip-if = [ "http2", ] +["browser_toolbox_many_toggles.js"] +skip-if = ["os == 'win'"] # Content process killing throws on Window + ["browser_toolbox_meatball.js"] ["browser_toolbox_options.js"] diff --git a/devtools/client/framework/test/browser_toolbox_many_toggles.js b/devtools/client/framework/test/browser_toolbox_many_toggles.js new file mode 100644 index 0000000000..1841ce1874 --- /dev/null +++ b/devtools/client/framework/test/browser_toolbox_many_toggles.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Bug 1898490: DevTools may prevents opening when some random content process is being destroyed +// in middle of DevTools initialization. +// So try opening DevTools a couple of times while destroying content processes in the background. + +const URL = + "data:text/html;charset=utf8,test many toggles with other content process destructions"; + +add_task(async function () { + const tab = await addTab(URL); + + const ProcessTools = Cc["@mozilla.org/processtools-service;1"].getService( + Ci.nsIProcessToolsService + ); + + const openedTabs = []; + const interval = setInterval(() => { + // Close the process specific to about:home, which is using a privilegedabout process type + const pid = ChromeUtils.getAllDOMProcesses().filter( + r => r.remoteType == "privilegedabout" + )[0]?.osPid; + if (!pid) { + return; + } + ProcessTools.kill(pid); + // The privilegedabout process wouldn't be automatically re-created, so open a new tab to force creating a new process. + openedTabs.push(BrowserTestUtils.addTab(gBrowser, "about:home")); + }); + + info( + "Open/close DevTools many times in a row while some processes get destroyed" + ); + for (let i = 0; i < 5; i++) { + const toolbox = await gDevTools.showToolboxForTab(tab, { + toolId: "webconsole", + }); + await toolbox.destroy(); + } + + clearInterval(interval); + + info("Close all tabs that were used to spawn a new content process"); + for (const tab of openedTabs) { + await removeTab(tab); + } +}); diff --git a/devtools/client/framework/test/node/README.md b/devtools/client/framework/test/node/README.md index 9fb86edfc5..6530aaf873 100644 --- a/devtools/client/framework/test/node/README.md +++ b/devtools/client/framework/test/node/README.md @@ -19,4 +19,4 @@ The tests run on try on linux64 platforms. The complete name of the try job is ` Adding the tests to a try push depends on the try selector you are using. - try fuzzy: look for the job named `source-test-node-devtools-tests` -The configuration file for try can be found at `taskcluster/ci/source-test/node.yml` +The configuration file for try can be found at `taskcluster/kinds/source-test/node.yml` diff --git a/devtools/client/fronts/inspector/rule-rewriter.js b/devtools/client/fronts/inspector/rule-rewriter.js index 30d1cf88d2..9fd84ccd8c 100644 --- a/devtools/client/fronts/inspector/rule-rewriter.js +++ b/devtools/client/fronts/inspector/rule-rewriter.js @@ -12,7 +12,9 @@ "use strict"; -const { getCSSLexer } = require("resource://devtools/shared/css/lexer.js"); +const { + InspectorCSSParserWrapper, +} = require("resource://devtools/shared/css/lexer.js"); const { COMMENT_PARSING_HEURISTIC_BYPASS_CHAR, escapeCSSComment, @@ -194,7 +196,7 @@ RuleRewriter.prototype = { // into "url(;)" by this code -- due to the way "url(...)" is // parsed as a single token. text = text.replace(/;$/, ""); - const lexer = getCSSLexer(text); + const lexer = new InspectorCSSParserWrapper(text, { trackEOFChars: true }); let result = ""; let previousOffset = 0; @@ -210,7 +212,7 @@ RuleRewriter.prototype = { // We set the location of the paren in a funny way, to handle // the case where we've seen a function token, where the paren // appears at the end. - parenStack.push({ closer, offset: result.length - 1 }); + parenStack.push({ closer, offset: result.length - 1, token }); previousOffset = token.endOffset; }; @@ -223,6 +225,18 @@ RuleRewriter.prototype = { return true; } + // We need to handle non-closed url function differently, as performEOFFixup will + // only automatically close missing parenthesis `url`. + // In such case, don't do anything here. + if ( + paren.closer === ")" && + closer == null && + paren.token.tokenType === "Function" && + paren.token.value === "url" + ) { + return true; + } + // Found a non-matching closing paren, so quote it. Note that // these are processed in reverse order. result = @@ -234,50 +248,43 @@ RuleRewriter.prototype = { return false; }; - while (true) { - const token = lexer.nextToken(); - if (!token) { - break; - } - - if (token.tokenType === "symbol") { - switch (token.text) { - case ";": - // We simply drop the ";" here. This lets us cope with - // declarations that don't have a ";" and also other - // termination. The caller handles adding the ";" again. + let token; + while ((token = lexer.nextToken())) { + switch (token.tokenType) { + case "Semicolon": + // We simply drop the ";" here. This lets us cope with + // declarations that don't have a ";" and also other + // termination. The caller handles adding the ";" again. + result += text.substring(previousOffset, token.startOffset); + previousOffset = token.endOffset; + break; + + case "CurlyBracketBlock": + pushParen(token, "}"); + break; + + case "ParenthesisBlock": + case "Function": + pushParen(token, ")"); + break; + + case "SquareBracketBlock": + pushParen(token, "]"); + break; + + case "CloseCurlyBracket": + case "CloseParenthesis": + case "CloseSquareBracket": + // Did we find an unmatched close bracket? + if (!popSomeParens(token.text)) { + // Copy out text from |previousOffset|. result += text.substring(previousOffset, token.startOffset); + // Quote the offending symbol. + result += "\\" + token.text; previousOffset = token.endOffset; - break; - - case "{": - pushParen(token, "}"); - break; - - case "(": - pushParen(token, ")"); - break; - - case "[": - pushParen(token, "]"); - break; - - case "}": - case ")": - case "]": - // Did we find an unmatched close bracket? - if (!popSomeParens(token.text)) { - // Copy out text from |previousOffset|. - result += text.substring(previousOffset, token.startOffset); - // Quote the offending symbol. - result += "\\" + token.text; - previousOffset = token.endOffset; - anySanitized = true; - } - break; - } - } else if (token.tokenType === "function") { - pushParen(token, ")"); + anySanitized = true; + } + break; } } @@ -286,7 +293,8 @@ RuleRewriter.prototype = { // Copy out any remaining text, then any needed terminators. result += text.substring(previousOffset, text.length); - const eofFixup = lexer.performEOFFixup("", true); + + const eofFixup = lexer.performEOFFixup(""); if (eofFixup) { anySanitized = true; result += eofFixup; diff --git a/devtools/client/fronts/walker.js b/devtools/client/fronts/walker.js index 5cffef612d..6d364f07a1 100644 --- a/devtools/client/fronts/walker.js +++ b/devtools/client/fronts/walker.js @@ -149,13 +149,6 @@ class WalkerFront extends FrontClassWithSpec(walkerSpec) { } async getIdrefNode(queryNode, id) { - // @backward-compat { version 125 } getIdrefNode was added in 125, so the whole if - // block below can be removed once 125 hits release. - if (!this.traits.hasGetIdrefNode) { - const doc = await this.document(queryNode); - return this.querySelector(doc, "#" + id); - } - const response = await super.getIdrefNode(queryNode, id); return response.node; } diff --git a/devtools/client/inspector/rules/test/browser_part2.toml b/devtools/client/inspector/rules/test/browser_part2.toml index 1cffe88e6e..19d7e2997b 100644 --- a/devtools/client/inspector/rules/test/browser_part2.toml +++ b/devtools/client/inspector/rules/test/browser_part2.toml @@ -181,6 +181,8 @@ skip-if = [ ["browser_rules_keyframeLineNumbers.js"] +["browser_rules_keyframes-rule-nested.js"] + ["browser_rules_keyframes-rule-shadowdom.js"] ["browser_rules_keyframes-rule_01.js"] diff --git a/devtools/client/inspector/rules/test/browser_rules_keyframes-rule-nested.js b/devtools/client/inspector/rules/test/browser_rules_keyframes-rule-nested.js new file mode 100644 index 0000000000..97b4b21a65 --- /dev/null +++ b/devtools/client/inspector/rules/test/browser_rules_keyframes-rule-nested.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that nested @keyframes rules are displayed correctly in the rule view + +const TEST_URI = `data:text/html,${encodeURIComponent(` + +

Nested @keyframes

+`)}`; + +add_task(async function () { + await pushPref("layout.css.starting-style-at-rules.enabled", true); + await addTab(TEST_URI); + const { inspector, view } = await openRuleView(); + + await selectNode("body", inspector); + const headers = Array.from(view.element.querySelectorAll(".ruleview-header")); + Assert.deepEqual( + headers.map(el => el.textContent), + [ + "Keyframes in-layer", + "Keyframes in-starting-style", + "Keyframes in-media", + "Keyframes in-container", + ], + "Got expected keyframes sections" + ); + + info("Check that keyframes' keyframe ancestor rules are not displayed"); + for (const headerEl of headers) { + const keyframeContainerId = headerEl + .querySelector("button") + .getAttribute("aria-controls"); + const keyframeContainer = view.element.querySelector( + `#${keyframeContainerId}` + ); + ok( + !!keyframeContainer, + `Got keyframe container for "${headerEl.textContent}"` + ); + is( + keyframeContainer.querySelector(".ruleview-rule-ancestor"), + null, + `ancestor data are not displayed for "${headerEl.textContent}" keyframe rules` + ); + } +}); diff --git a/devtools/client/inspector/rules/test/browser_rules_pseudo-element_01.js b/devtools/client/inspector/rules/test/browser_rules_pseudo-element_01.js index fcd0302624..d1919c6f36 100644 --- a/devtools/client/inspector/rules/test/browser_rules_pseudo-element_01.js +++ b/devtools/client/inspector/rules/test/browser_rules_pseudo-element_01.js @@ -5,13 +5,16 @@ // Test that pseudoelements are displayed correctly in the rule view -const TEST_URI = URL_ROOT + "doc_pseudoelement.html"; +const TEST_URI = URL_ROOT + "doc_pseudoelement.html?#:~:text=fox"; const PSEUDO_PREF = "devtools.inspector.show_pseudo_elements"; add_task(async function () { await pushPref(PSEUDO_PREF, true); await pushPref("dom.customHighlightAPI.enabled", true); + await pushPref("dom.text_fragments.enabled", true); await pushPref("layout.css.modern-range-pseudos.enabled", true); + await pushPref("full-screen-api.transition-duration.enter", "0 0"); + await pushPref("full-screen-api.transition-duration.leave", "0 0"); await addTab(TEST_URI); const { inspector, view } = await openRuleView(); @@ -23,9 +26,11 @@ add_task(async function () { await testParagraph(inspector, view); await testBody(inspector, view); await testList(inspector, view); - await testDialogBackdrop(inspector, view); await testCustomHighlight(inspector, view); await testSlider(inspector, view); + await testUrlFragmentTextDirective(inspector, view); + // keep this one last as it makes the browser go fullscreen and seem to impact other tests + await testBackdrop(inspector, view); }); async function testTopLeft(inspector, view) { @@ -288,13 +293,80 @@ async function testList(inspector, view) { assertGutters(view); } -async function testDialogBackdrop(inspector, view) { +async function testBackdrop(inspector, view) { + info("Test ::backdrop for dialog element"); await assertPseudoElementRulesNumbers("dialog", inspector, view, { elementRulesNb: 3, backdropRules: 1, }); + info("Test ::backdrop for popover element"); + await assertPseudoElementRulesNumbers( + "#in-dialog[popover]", + inspector, + view, + { + elementRulesNb: 3, + backdropRules: 1, + } + ); + assertGutters(view); + + info("Test ::backdrop rules are displayed when elements is fullscreen"); + + // Wait for the document being activated, so that + // fullscreen request won't be denied. + const onTabFocused = SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + return ContentTaskUtils.waitForCondition( + () => content.browsingContext.isActive && content.document.hasFocus(), + "document is active" + ); + }); + gBrowser.selectedBrowser.focus(); + await onTabFocused; + + info("Request fullscreen"); + // Entering fullscreen is triggering an update, wait for it so it doesn't impact + // the rest of the test + let onInspectorUpdated = view.once("ruleview-refreshed"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => { + const canvas = content.document.querySelector("canvas"); + canvas.requestFullscreen(); + + await ContentTaskUtils.waitForCondition( + () => content.document.fullscreenElement === canvas, + "canvas is fullscreen" + ); + }); + await onInspectorUpdated; + + await assertPseudoElementRulesNumbers("canvas", inspector, view, { + elementRulesNb: 3, + backdropRules: 1, + }); + + assertGutters(view); + + // Exiting fullscreen is triggering an update, wait for it so it doesn't impact + // the rest of the test + onInspectorUpdated = view.once("ruleview-refreshed"); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => { + content.document.exitFullscreen(); + await ContentTaskUtils.waitForCondition( + () => content.document.fullscreenElement === null, + "canvas is no longer fullscreen" + ); + }); + await onInspectorUpdated; + + info( + "Test ::backdrop rules are not displayed when elements are not fullscreen" + ); + await assertPseudoElementRulesNumbers("canvas", inspector, view, { + elementRulesNb: 3, + backdropRules: 0, + }); } async function testCustomHighlight(inspector, view) { @@ -376,6 +448,19 @@ async function testSlider(inspector, view) { ); } +async function testUrlFragmentTextDirective(inspector, view) { + await assertPseudoElementRulesNumbers( + ".url-fragment-text-directives", + inspector, + view, + { + elementRulesNb: 3, + targetTextRulesNb: 1, + } + ); + assertGutters(view); +} + function convertTextPropsToString(textProps) { return textProps .map( @@ -436,6 +521,9 @@ async function assertPseudoElementRulesNumbers( sliderTrackRules: elementStyle.rules.filter( rule => rule.pseudoElement === "::slider-track" ), + targetTextRules: elementStyle.rules.filter( + rule => rule.pseudoElement === "::target-text" + ), }; is( @@ -493,6 +581,11 @@ async function assertPseudoElementRulesNumbers( ruleNbs.sliderTrackRulesNb || 0, selector + " has the correct number of ::slider-track rules" ); + is( + rules.targetTextRules.length, + ruleNbs.targetTextRulesNb || 0, + selector + " has the correct number of ::target-text rules" + ); // If we do have pseudo element rules displayed, ensure we don't mark their selectors // as matched or unmatched diff --git a/devtools/client/inspector/rules/test/doc_pseudoelement.html b/devtools/client/inspector/rules/test/doc_pseudoelement.html index 8e077e220b..88611d6633 100644 --- a/devtools/client/inspector/rules/test/doc_pseudoelement.html +++ b/devtools/client/inspector/rules/test/doc_pseudoelement.html @@ -110,7 +110,11 @@ p:first-letter { color: purple; } -dialog::backdrop { +:is( + dialog, + [popover], + :fullscreen, +)::backdrop { background-color: transparent; } @@ -142,6 +146,10 @@ input::slider-thumb { input::slider-track { background: seagreen; } + +.url-fragment-text-directives::target-text { + background-color: salmon; +} @@ -167,7 +175,10 @@ input::slider-track {
  • List element
  • - In dialog + + In dialog +
    hello
    +
    @@ -177,10 +188,15 @@ input::slider-track { You can use them to examine, edit, and debug HTML, CSS, and JavaScript. +
    May the fox be with you
    + + + + + diff --git a/devtools/client/netmonitor/test/html_zstd-test-page.html b/devtools/client/netmonitor/test/html_zstd-test-page.html new file mode 100644 index 0000000000..52fdabcecb --- /dev/null +++ b/devtools/client/netmonitor/test/html_zstd-test-page.html @@ -0,0 +1,41 @@ + + + + + + + + + + Network Monitor test page + + + +

    Zstd test

    + + + + + diff --git a/devtools/client/netmonitor/test/sjs_content-type-test-server.sjs b/devtools/client/netmonitor/test/sjs_content-type-test-server.sjs index d9ac186396..adc882252d 100644 --- a/devtools/client/netmonitor/test/sjs_content-type-test-server.sjs +++ b/devtools/client/netmonitor/test/sjs_content-type-test-server.sjs @@ -372,6 +372,18 @@ function handleRequest(request, response) { response.finish(); break; } + case "zstd": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "text/json", false); + response.setHeader("Content-Encoding", "zstd", false); + setCacheHeaders(); + // Use static data since we cannot encode zstandard. + const data = "(\xb5/\xfd @E\x00\x00\x10XX\x01\x00\x93\x00\x16"; + response.setHeader("Content-Length", "" + data.length, false); + response.write(data); + response.finish(); + break; + } case "hls-m3u8": { response.setStatusLine(request.httpVersion, status, "OK"); response.setHeader("Content-Type", "application/x-mpegurl", false); diff --git a/devtools/client/performance-new/shared/background.sys.mjs b/devtools/client/performance-new/shared/background.sys.mjs index 4b19f68b71..a110b3e4cb 100644 --- a/devtools/client/performance-new/shared/background.sys.mjs +++ b/devtools/client/performance-new/shared/background.sys.mjs @@ -124,7 +124,7 @@ export const presets = { "web-developer": { entries: 128 * 1024 * 1024, interval: 1, - features: ["screenshots", "js", "cpu"], + features: ["screenshots", "js", "cpu", "memory"], threads: ["GeckoMain", "Compositor", "Renderer", "DOM Worker"], duration: 0, profilerViewMode: "active-tab", @@ -142,7 +142,15 @@ export const presets = { "firefox-platform": { entries: 128 * 1024 * 1024, interval: 1, - features: ["screenshots", "js", "stackwalk", "cpu", "java", "processcpu"], + features: [ + "screenshots", + "js", + "stackwalk", + "cpu", + "java", + "processcpu", + "memory", + ], threads: [ "GeckoMain", "Compositor", @@ -165,7 +173,7 @@ export const presets = { graphics: { entries: 128 * 1024 * 1024, interval: 1, - features: ["stackwalk", "js", "cpu", "java", "processcpu"], + features: ["stackwalk", "js", "cpu", "java", "processcpu", "memory"], threads: [ "GeckoMain", "Compositor", @@ -199,6 +207,7 @@ export const presets = { "audiocallbacktracing", "ipcmessages", "processcpu", + "memory", ], threads: [ "cubeb", @@ -248,6 +257,7 @@ export const presets = { "java", "processcpu", "bandwidth", + "memory", ], threads: [ "Compositor", @@ -286,6 +296,7 @@ export const presets = { "markersallthreads", "power", "bandwidth", + "memory", ], threads: ["GeckoMain", "Renderer"], duration: 0, @@ -785,12 +796,13 @@ async function getResponseForMessage(request, browser) { return profileCaptureResult.profile; case "ERROR": throw profileCaptureResult.error; - default: + default: { const { UnhandledCaseError } = lazy.Utils(); throw new UnhandledCaseError( profileCaptureResult, "profileCaptureResult" ); + } } } case "GET_SYMBOL_TABLE": { @@ -831,13 +843,14 @@ async function getResponseForMessage(request, browser) { } return []; } - default: + default: { console.error( "An unknown message type was received by the profiler's WebChannel handler.", request ); const { UnhandledCaseError } = lazy.Utils(); throw new UnhandledCaseError(request, "WebChannel request"); + } } } diff --git a/devtools/client/performance-new/shared/utils.js b/devtools/client/performance-new/shared/utils.js index ae00b673de..ac98962307 100644 --- a/devtools/client/performance-new/shared/utils.js +++ b/devtools/client/performance-new/shared/utils.js @@ -414,6 +414,13 @@ const featureDescriptions = [ "Record how much CPU has been used between samples by each profiled thread.", recommended: true, }, + { + name: "Memory Tracking", + value: "memory", + title: + "Track the memory allocations and deallocations per process over time.", + recommended: true, + }, { name: "Java", value: "java", diff --git a/devtools/client/performance-new/test/browser/browser_interaction-between-interfaces.js b/devtools/client/performance-new/test/browser/browser_interaction-between-interfaces.js index 1268cf818d..6171ed546c 100644 --- a/devtools/client/performance-new/test/browser/browser_interaction-between-interfaces.js +++ b/devtools/client/performance-new/test/browser/browser_interaction-between-interfaces.js @@ -294,18 +294,11 @@ add_task(async function test_change_in_about_profiling() { "The new value should have the same count of threads as the old value, please double check the test code." ); setThreadInputValue(newThreadValue); - checkDevtoolsCustomPresetContent( - devtoolsDocument, - ` - Interval: 2 ms - Threads: GeckoMain, Dummy - JavaScript - Native Stacks - CPU Utilization - Audio Callback Tracing - IPC Messages - Process CPU Utilization - ` + ok( + getDevtoolsCustomPresetContent(devtoolsDocument).includes( + "Threads: GeckoMain, Dummy\n" + ), + "Threads list should match the changed value" ); } ); diff --git a/devtools/client/shared/components/object-inspector/utils/node.js b/devtools/client/shared/components/object-inspector/utils/node.js index 0d049e1c4b..63d358e580 100644 --- a/devtools/client/shared/components/object-inspector/utils/node.js +++ b/devtools/client/shared/components/object-inspector/utils/node.js @@ -283,10 +283,7 @@ function nodeHasEntries(item) { className === "MIDIInputMap" || className === "MIDIOutputMap" || className === "HighlightRegistry" || - // @backward-compat { version 125 } Support for enumerate CustomStateSet items was - // added in 125. When connecting to older server, we don't want to show the - // node for them. The extra check can be removed once 125 hits release. - (className === "CustomStateSet" && Array.isArray(value.preview?.items)) + className === "CustomStateSet" ); } diff --git a/devtools/client/shared/css-angle.js b/devtools/client/shared/css-angle.js index 903b7813ad..d89cba5b7f 100644 --- a/devtools/client/shared/css-angle.js +++ b/devtools/client/shared/css-angle.js @@ -6,7 +6,9 @@ const SPECIALVALUES = new Set(["initial", "inherit", "unset"]); -const { getCSSLexer } = require("resource://devtools/shared/css/lexer.js"); +const { + InspectorCSSParserWrapper, +} = require("resource://devtools/shared/css/lexer.js"); loader.lazyRequireGetter( this, @@ -68,13 +70,14 @@ CssAngle.prototype = { }, get valid() { - const token = getCSSLexer(this.authored).nextToken(); + const token = new InspectorCSSParserWrapper(this.authored).nextToken(); if (!token) { return false; } + return ( - token.tokenType === "dimension" && - token.text.toLowerCase() in this.ANGLEUNIT + token.tokenType === "Dimension" && + token.unit.toLowerCase() in this.ANGLEUNIT ); }, diff --git a/devtools/client/shared/output-parser.js b/devtools/client/shared/output-parser.js index bd514096b3..5202ae3a8e 100644 --- a/devtools/client/shared/output-parser.js +++ b/devtools/client/shared/output-parser.js @@ -8,7 +8,9 @@ const { angleUtils, } = require("resource://devtools/client/shared/css-angle.js"); const { colorUtils } = require("resource://devtools/shared/css/color.js"); -const { getCSSLexer } = require("resource://devtools/shared/css/lexer.js"); +const { + InspectorCSSParserWrapper, +} = require("resource://devtools/shared/css/lexer.js"); const { appendText, } = require("resource://devtools/client/inspector/shared/utils.js"); @@ -67,6 +69,13 @@ const BACKDROP_FILTER_ENABLED = Services.prefs.getBoolPref( ); const HTML_NS = "http://www.w3.org/1999/xhtml"; +// This regexp matches a URL token. It puts the "url(", any +// leading whitespace, and any opening quote into |leader|; the +// URL text itself into |body|, and any trailing quote, trailing +// whitespace, and the ")" into |trailer|. +const URL_REGEX = + /^(?url\([ \t\r\n\f]*(["']?))(?.*?)(?\2[ \t\r\n\f]*\))$/i; + // Very long text properties should be truncated using CSS to avoid creating // extremely tall propertyvalue containers. 5000 characters is an arbitrary // limit. Assuming an average ruleview can hold 50 characters per line, this @@ -175,7 +184,7 @@ class OutputParser { * @param {Boolean} stopAtComma * If true, stop at a comma. * @return {Object} - * An object of the form {tokens, functionData, sawComma, sawVariable}. + * An object of the form {tokens, functionData, sawComma, sawVariable, depth}. * |tokens| is a list of the non-comment, non-whitespace tokens * that were seen. The stopping token (paren or comma) will not * be included. @@ -184,6 +193,7 @@ class OutputParser { * not be included. * |sawComma| is true if the stop was due to a comma, or false otherwise. * |sawVariable| is true if a variable was seen while parsing the text. + * |depth| is the number of unclosed parenthesis remaining when we return. */ #parseMatchingParens(text, tokenStream, options, stopAtComma) { let depth = 1; @@ -196,24 +206,22 @@ class OutputParser { if (!token) { break; } - if (token.tokenType === "comment") { + if (token.tokenType === "Comment") { continue; } - if (token.tokenType === "symbol") { - if (stopAtComma && depth === 1 && token.text === ",") { - return { tokens, functionData, sawComma: true, sawVariable }; - } else if (token.text === "(") { - ++depth; - } else if (token.text === ")") { - --depth; - if (depth === 0) { - break; - } + if (stopAtComma && depth === 1 && token.tokenType === "Comma") { + return { tokens, functionData, sawComma: true, sawVariable, depth }; + } else if (token.tokenType === "ParenthesisBlock") { + ++depth; + } else if (token.tokenType === "CloseParenthesis") { + --depth; + if (depth === 0) { + break; } } else if ( - token.tokenType === "function" && - token.text === "var" && + token.tokenType === "Function" && + token.value === "var" && options.getVariableValue ) { sawVariable = true; @@ -224,24 +232,24 @@ class OutputParser { options ); functionData.push({ node, value, fallbackValue }); - } else if (token.tokenType === "function") { + } else if (token.tokenType === "Function") { ++depth; } if ( - token.tokenType !== "function" || - token.text !== "var" || + token.tokenType !== "Function" || + token.value !== "var" || !options.getVariableValue ) { functionData.push(text.substring(token.startOffset, token.endOffset)); } - if (token.tokenType !== "whitespace") { + if (token.tokenType !== "WhiteSpace") { tokens.push(token); } } - return { tokens, functionData, sawComma: false, sawVariable }; + return { tokens, functionData, sawComma: false, sawVariable, depth }; } /** @@ -389,19 +397,23 @@ class OutputParser { } const lowerCaseTokenText = token.text?.toLowerCase(); - if (token.tokenType === "comment") { + if (token.tokenType === "Comment") { // This doesn't change spaceNeeded, because we didn't emit // anything to the output. continue; } switch (token.tokenType) { - case "function": { - const isColorTakingFunction = - COLOR_TAKING_FUNCTIONS.includes(lowerCaseTokenText); + case "Function": { + const functionName = token.value; + const lowerCaseFunctionName = functionName.toLowerCase(); + + const isColorTakingFunction = COLOR_TAKING_FUNCTIONS.includes( + lowerCaseFunctionName + ); if ( isColorTakingFunction || - ANGLE_TAKING_FUNCTIONS.includes(lowerCaseTokenText) + ANGLE_TAKING_FUNCTIONS.includes(lowerCaseFunctionName) ) { // The function can accept a color or an angle argument, and we know // it isn't special in some other way. So, we let it @@ -414,10 +426,13 @@ class OutputParser { outerMostFunctionTakesColor = isColorTakingFunction; } if (isColorTakingFunction) { - colorFunctions.push({ parenDepth, functionName: token.text }); + colorFunctions.push({ parenDepth, functionName }); } ++parenDepth; - } else if (lowerCaseTokenText === "var" && options.getVariableValue) { + } else if ( + lowerCaseFunctionName === "var" && + options.getVariableValue + ) { const { node: variableNode, value } = this.#parseVariable( token, text, @@ -434,20 +449,17 @@ class OutputParser { this.#parsed.push(variableNode); } } else { - const { functionData, sawVariable } = this.#parseMatchingParens( - text, - tokenStream, - options - ); - - const functionName = text.substring( - token.startOffset, - token.endOffset - ); + const { + functionData, + sawVariable, + tokens: functionArgTokens, + depth, + } = this.#parseMatchingParens(text, tokenStream, options); if (sawVariable) { const computedFunctionText = functionName + + "(" + functionData .map(data => { if (typeof data === "string") { @@ -466,6 +478,7 @@ class OutputParser { colorFunction: colorFunctions.at(-1)?.functionName, valueParts: [ functionName, + "(", ...functionData.map(data => data.node || data), ")", ], @@ -473,7 +486,7 @@ class OutputParser { } else { // If function contains variable, we need to add both strings // and nodes. - this.#appendTextNode(functionName); + this.#appendTextNode(functionName + "("); for (const data of functionData) { if (typeof data === "string") { this.#appendTextNode(data); @@ -486,16 +499,40 @@ class OutputParser { } else { // If no variable in function, join the text together and add // to DOM accordingly. - const functionText = functionName + functionData.join("") + ")"; + const functionText = + functionName + + "(" + + functionData.join("") + + // only append closing parenthesis if the authored text actually had it + // In such case, we should probably indicate that there's a "syntax error" + // See Bug 1891461. + (depth == 0 ? ")" : ""); + + if (lowerCaseFunctionName === "url" && options.urlClass) { + // url() with quoted strings are not mapped as UnquotedUrl, + // instead, we get a "Function" token with "url" function name, + // and later, a "QuotedString" token, which contains the actual URL. + let url; + for (const argToken of functionArgTokens) { + if (argToken.tokenType === "QuotedString") { + url = argToken.value; + break; + } + } - if ( + if (url !== undefined) { + this.#appendURL(functionText, url, options); + } else { + this.#appendTextNode(functionText); + } + } else if ( options.expectCubicBezier && - lowerCaseTokenText === "cubic-bezier" + lowerCaseFunctionName === "cubic-bezier" ) { this.#appendCubicBezier(functionText, options); } else if ( options.expectLinearEasing && - lowerCaseTokenText === "linear" + lowerCaseFunctionName === "linear" ) { this.#appendLinear(functionText, options); } else if ( @@ -508,7 +545,7 @@ class OutputParser { }); } else if ( options.expectShape && - BASIC_SHAPE_FUNCTIONS.includes(lowerCaseTokenText) + BASIC_SHAPE_FUNCTIONS.includes(lowerCaseFunctionName) ) { this.#appendShape(functionText, options); } else { @@ -519,7 +556,7 @@ class OutputParser { break; } - case "ident": + case "Ident": if ( options.expectCubicBezier && BEZIER_KEYWORDS.includes(lowerCaseTokenText) @@ -553,8 +590,8 @@ class OutputParser { } break; - case "id": - case "hash": { + case "IDHash": + case "Hash": { const original = text.substring(token.startOffset, token.endOffset); if (colorOK() && InspectorUtils.isValidCSSColor(original)) { if (spaceNeeded) { @@ -571,7 +608,7 @@ class OutputParser { } break; } - case "dimension": + case "Dimension": const value = text.substring(token.startOffset, token.endOffset); if (angleOK(value)) { this.#appendAngle(value, options); @@ -579,16 +616,16 @@ class OutputParser { this.#appendTextNode(value); } break; - case "url": - case "bad_url": + case "UnquotedUrl": + case "BadUrl": this.#appendURL( text.substring(token.startOffset, token.endOffset), - token.text, + token.value, options ); break; - case "string": + case "QuotedString": if (options.expectFont) { fontFamilyNameParts.push( text.substring(token.startOffset, token.endOffset) @@ -600,7 +637,7 @@ class OutputParser { } break; - case "whitespace": + case "WhiteSpace": if (options.expectFont) { fontFamilyNameParts.push(" "); } else { @@ -610,32 +647,44 @@ class OutputParser { } break; - case "symbol": - if (token.text === "(") { - ++parenDepth; - } else if (token.text === ")") { - --parenDepth; + case "ParenthesisBlock": + ++parenDepth; + this.#appendTextNode( + text.substring(token.startOffset, token.endOffset) + ); + break; - if (colorFunctions.at(-1)?.parenDepth == parenDepth) { - colorFunctions.pop(); - } + case "CloseParenthesis": + --parenDepth; - if (stopAtCloseParen && parenDepth === 0) { - done = true; - break; - } + if (colorFunctions.at(-1)?.parenDepth == parenDepth) { + colorFunctions.pop(); + } - if (parenDepth === 0) { - outerMostFunctionTakesColor = false; - } - } else if ( - (token.text === "," || token.text === "!") && + if (stopAtCloseParen && parenDepth === 0) { + done = true; + break; + } + + if (parenDepth === 0) { + outerMostFunctionTakesColor = false; + } + this.#appendTextNode( + text.substring(token.startOffset, token.endOffset) + ); + break; + + case "Comma": + case "Delim": + if ( + (token.tokenType === "Comma" || token.text === "!") && options.expectFont && fontFamilyNameParts.length !== 0 ) { this.#appendFontFamily(fontFamilyNameParts.join(""), options); fontFamilyNameParts = []; } + // falls through default: this.#appendTextNode( @@ -647,15 +696,15 @@ class OutputParser { // If this token might possibly introduce token pasting when // color-cycling, require a space. spaceNeeded = - token.tokenType === "ident" || - token.tokenType === "at" || - token.tokenType === "id" || - token.tokenType === "hash" || - token.tokenType === "number" || - token.tokenType === "dimension" || - token.tokenType === "percentage" || - token.tokenType === "dimension"; - previousWasBang = token.tokenType === "symbol" && token.text === "!"; + token.tokenType === "Ident" || + token.tokenType === "AtKeyword" || + token.tokenType === "IDHash" || + token.tokenType === "Hash" || + token.tokenType === "Number" || + token.tokenType === "Dimension" || + token.tokenType === "Percentage" || + token.tokenType === "Dimension"; + previousWasBang = token.tokenType === "Delim" && token.text === "!"; } if (options.expectFont && fontFamilyNameParts.length !== 0) { @@ -686,7 +735,7 @@ class OutputParser { text = text.trim(); this.#parsed.length = 0; - const tokenStream = getCSSLexer(text); + const tokenStream = new InspectorCSSParserWrapper(text); return this.#doParse(text, options, tokenStream, false); } @@ -884,7 +933,7 @@ class OutputParser { */ // eslint-disable-next-line complexity #addPolygonPointNodes(coords, container) { - const tokenStream = getCSSLexer(coords); + const tokenStream = new InspectorCSSParserWrapper(coords); let token = tokenStream.nextToken(); let coord = ""; let i = 0; @@ -897,7 +946,7 @@ class OutputParser { }); while (token) { - if (token.tokenType === "symbol" && token.text === ",") { + if (token.tokenType === "Comma") { // Comma separating coordinate pairs; add coordNode to container and reset vars if (!isXCoord) { // Y coord not added to coordNode yet @@ -933,19 +982,19 @@ class OutputParser { class: "ruleview-shape-point", "data-point": `${i}`, }); - } else if (token.tokenType === "symbol" && token.text === "(") { + } else if (token.tokenType === "ParenthesisBlock") { depth++; coord += coords.substring(token.startOffset, token.endOffset); - } else if (token.tokenType === "symbol" && token.text === ")") { + } else if (token.tokenType === "CloseParenthesis") { depth--; coord += coords.substring(token.startOffset, token.endOffset); - } else if (token.tokenType === "whitespace" && coord === "") { + } else if (token.tokenType === "WhiteSpace" && coord === "") { // Whitespace at beginning of coord; add to container appendText( container, coords.substring(token.startOffset, token.endOffset) ); - } else if (token.tokenType === "whitespace" && depth === 0) { + } else if (token.tokenType === "WhiteSpace" && depth === 0) { // Whitespace signifying end of coord const node = this.#createNode( "span", @@ -964,10 +1013,10 @@ class OutputParser { coord = ""; isXCoord = !isXCoord; } else if ( - token.tokenType === "number" || - token.tokenType === "dimension" || - token.tokenType === "percentage" || - token.tokenType === "function" + token.tokenType === "Number" || + token.tokenType === "Dimension" || + token.tokenType === "Percentage" || + token.tokenType === "Function" ) { if (isXCoord && coord && depth === 0) { // Whitespace is not necessary between x/y coords. @@ -986,11 +1035,11 @@ class OutputParser { } coord += coords.substring(token.startOffset, token.endOffset); - if (token.tokenType === "function") { + if (token.tokenType === "Function") { depth++; } } else if ( - token.tokenType === "ident" && + token.tokenType === "Ident" && (token.text === "nonzero" || token.text === "evenodd") ) { // A fill-rule (nonzero or evenodd). @@ -1034,7 +1083,7 @@ class OutputParser { */ // eslint-disable-next-line complexity #addCirclePointNodes(coords, container) { - const tokenStream = getCSSLexer(coords); + const tokenStream = new InspectorCSSParserWrapper(coords); let token = tokenStream.nextToken(); let depth = 0; let coord = ""; @@ -1044,20 +1093,20 @@ class OutputParser { "data-point": "center", }); while (token) { - if (token.tokenType === "symbol" && token.text === "(") { + if (token.tokenType === "ParenthesisBlock") { depth++; coord += coords.substring(token.startOffset, token.endOffset); - } else if (token.tokenType === "symbol" && token.text === ")") { + } else if (token.tokenType === "CloseParenthesis") { depth--; coord += coords.substring(token.startOffset, token.endOffset); - } else if (token.tokenType === "whitespace" && coord === "") { + } else if (token.tokenType === "WhiteSpace" && coord === "") { // Whitespace at beginning of coord; add to container appendText( container, coords.substring(token.startOffset, token.endOffset) ); } else if ( - token.tokenType === "whitespace" && + token.tokenType === "WhiteSpace" && point === "radius" && depth === 0 ) { @@ -1078,7 +1127,7 @@ class OutputParser { point = "cx"; coord = ""; depth = 0; - } else if (token.tokenType === "whitespace" && depth === 0) { + } else if (token.tokenType === "WhiteSpace" && depth === 0) { // Whitespace signifying end of cx/cy const node = this.#createNode( "span", @@ -1097,7 +1146,7 @@ class OutputParser { point = point === "cx" ? "cy" : "cx"; coord = ""; depth = 0; - } else if (token.tokenType === "ident" && token.text === "at") { + } else if (token.tokenType === "Ident" && token.text === "at") { // "at"; Add radius to container if not already done so if (point === "radius" && coord) { const node = this.#createNode( @@ -1118,10 +1167,10 @@ class OutputParser { coord = ""; depth = 0; } else if ( - token.tokenType === "number" || - token.tokenType === "dimension" || - token.tokenType === "percentage" || - token.tokenType === "function" + token.tokenType === "Number" || + token.tokenType === "Dimension" || + token.tokenType === "Percentage" || + token.tokenType === "Function" ) { if (point === "cx" && coord && depth === 0) { // Center coords don't require whitespace between x/y. So if current point is @@ -1142,7 +1191,7 @@ class OutputParser { } coord += coords.substring(token.startOffset, token.endOffset); - if (token.tokenType === "function") { + if (token.tokenType === "Function") { depth++; } } else { @@ -1195,7 +1244,7 @@ class OutputParser { */ // eslint-disable-next-line complexity #addEllipsePointNodes(coords, container) { - const tokenStream = getCSSLexer(coords); + const tokenStream = new InspectorCSSParserWrapper(coords); let token = tokenStream.nextToken(); let depth = 0; let coord = ""; @@ -1205,19 +1254,19 @@ class OutputParser { "data-point": "center", }); while (token) { - if (token.tokenType === "symbol" && token.text === "(") { + if (token.tokenType === "ParenthesisBlock") { depth++; coord += coords.substring(token.startOffset, token.endOffset); - } else if (token.tokenType === "symbol" && token.text === ")") { + } else if (token.tokenType === "CloseParenthesis") { depth--; coord += coords.substring(token.startOffset, token.endOffset); - } else if (token.tokenType === "whitespace" && coord === "") { + } else if (token.tokenType === "WhiteSpace" && coord === "") { // Whitespace at beginning of coord; add to container appendText( container, coords.substring(token.startOffset, token.endOffset) ); - } else if (token.tokenType === "whitespace" && depth === 0) { + } else if (token.tokenType === "WhiteSpace" && depth === 0) { if (point === "rx" || point === "ry") { // Whitespace signifying end of rx/ry const node = this.#createNode( @@ -1256,7 +1305,7 @@ class OutputParser { coord = ""; depth = 0; } - } else if (token.tokenType === "ident" && token.text === "at") { + } else if (token.tokenType === "Ident" && token.text === "at") { // "at"; Add radius to container if not already done so if (point === "ry" && coord) { const node = this.#createNode( @@ -1277,10 +1326,10 @@ class OutputParser { coord = ""; depth = 0; } else if ( - token.tokenType === "number" || - token.tokenType === "dimension" || - token.tokenType === "percentage" || - token.tokenType === "function" + token.tokenType === "Number" || + token.tokenType === "Dimension" || + token.tokenType === "Percentage" || + token.tokenType === "Function" ) { if (point === "rx" && coord && depth === 0) { // Radius coords don't require whitespace between x/y. @@ -1313,7 +1362,7 @@ class OutputParser { } coord += coords.substring(token.startOffset, token.endOffset); - if (token.tokenType === "function") { + if (token.tokenType === "Function") { depth++; } } else { @@ -1366,7 +1415,7 @@ class OutputParser { // eslint-disable-next-line complexity #addInsetPointNodes(coords, container) { const insetPoints = ["top", "right", "bottom", "left"]; - const tokenStream = getCSSLexer(coords); + const tokenStream = new InspectorCSSParserWrapper(coords); let token = tokenStream.nextToken(); let depth = 0; let coord = ""; @@ -1383,16 +1432,16 @@ class OutputParser { if (round) { // Everything that comes after "round" should just be plain text otherText[i].push(coords.substring(token.startOffset, token.endOffset)); - } else if (token.tokenType === "symbol" && token.text === "(") { + } else if (token.tokenType === "ParenthesisBlock") { depth++; coord += coords.substring(token.startOffset, token.endOffset); - } else if (token.tokenType === "symbol" && token.text === ")") { + } else if (token.tokenType === "CloseParenthesis") { depth--; coord += coords.substring(token.startOffset, token.endOffset); - } else if (token.tokenType === "whitespace" && coord === "") { + } else if (token.tokenType === "WhiteSpace" && coord === "") { // Whitespace at beginning of coord; add to container otherText[i].push(coords.substring(token.startOffset, token.endOffset)); - } else if (token.tokenType === "whitespace" && depth === 0) { + } else if (token.tokenType === "WhiteSpace" && depth === 0) { // Whitespace signifying end of coord; create node and push to nodes const node = this.#createNode( "span", @@ -1407,10 +1456,10 @@ class OutputParser { otherText[i] = [coords.substring(token.startOffset, token.endOffset)]; depth = 0; } else if ( - token.tokenType === "number" || - token.tokenType === "dimension" || - token.tokenType === "percentage" || - token.tokenType === "function" + token.tokenType === "Number" || + token.tokenType === "Dimension" || + token.tokenType === "Percentage" || + token.tokenType === "Function" ) { if (coord && depth === 0) { // Inset coords don't require whitespace between each coord. @@ -1428,10 +1477,10 @@ class OutputParser { } coord += coords.substring(token.startOffset, token.endOffset); - if (token.tokenType === "function") { + if (token.tokenType === "Function") { depth++; } - } else if (token.tokenType === "ident" && token.text === "round") { + } else if (token.tokenType === "Ident" && token.text === "round") { if (coord && depth === 0) { // Whitespace is not necessary before "round"; create a new node for the coord const node = this.#createNode( @@ -1730,13 +1779,15 @@ class OutputParser { */ #sanitizeURL(url) { // Re-lex the URL and add any needed termination characters. - const urlTokenizer = getCSSLexer(url); + const urlTokenizer = new InspectorCSSParserWrapper(url, { + trackEOFChars: true, + }); // Just read until EOF; there will only be a single token. while (urlTokenizer.nextToken()) { // Nothing. } - return urlTokenizer.performEOFFixup(url, true); + return urlTokenizer.performEOFFixup(url); } /** @@ -1756,14 +1807,7 @@ class OutputParser { // leave the termination characters. This isn't strictly // "as-authored", but it makes a bit more sense. match = this.#sanitizeURL(match); - // This regexp matches a URL token. It puts the "url(", any - // leading whitespace, and any opening quote into |leader|; the - // URL text itself into |body|, and any trailing quote, trailing - // whitespace, and the ")" into |trailer|. We considered adding - // functionality for this to CSSLexer, in some way, but this - // seemed simpler on the whole. - const urlParts = - /^(url\([ \t\r\n\f]*(["']?))(.*?)(\2[ \t\r\n\f]*\))$/i.exec(match); + const urlParts = URL_REGEX.exec(match); // Bail out if that didn't match anything. if (!urlParts) { @@ -1771,7 +1815,7 @@ class OutputParser { return; } - const [, leader, , body, trailer] = urlParts; + const { leader, body, trailer } = urlParts.groups; this.#appendTextNode(leader); diff --git a/devtools/client/shared/screenshot.js b/devtools/client/shared/screenshot.js index ca746f66e7..4031708293 100644 --- a/devtools/client/shared/screenshot.js +++ b/devtools/client/shared/screenshot.js @@ -327,9 +327,9 @@ function saveToClipboard(base64URI) { let _outputDirectory = null; /** - * Returns the default directory for screenshots. - * If a specific directory for screenshots is not defined, - * it falls back to the system downloads directory. + * Returns the default directory for DevTools screenshots. + * For consistency with the Firefox Screenshots feature, this will default to + * the preferred downloads directory. * * @return {Promise} Resolves the path as a string */ @@ -338,13 +338,7 @@ async function getOutputDirectory() { return _outputDirectory; } - try { - // This will throw if there is not a screenshot directory set for the platform - _outputDirectory = Services.dirsvc.get("Scrnshts", Ci.nsIFile).path; - } catch (e) { - _outputDirectory = await lazy.Downloads.getPreferredDownloadsDirectory(); - } - + _outputDirectory = await lazy.Downloads.getPreferredDownloadsDirectory(); return _outputDirectory; } diff --git a/devtools/client/shared/sourceeditor/css-autocompleter.js b/devtools/client/shared/sourceeditor/css-autocompleter.js index 7db3cbbc0f..4266bd02b8 100644 --- a/devtools/client/shared/sourceeditor/css-autocompleter.js +++ b/devtools/client/shared/sourceeditor/css-autocompleter.js @@ -28,15 +28,14 @@ const { * The file 'css-parsing-utils' helps to convert the CSS into meaningful tokens, * each having a certain type associated with it. These tokens help us to figure * out the currently edited word and to write a CSS state machine to figure out - * what the user is currently editing. By that, I mean, whether he is editing a - * selector or a property or a value, or even fine grained information like an - * id in the selector. + * what the user is currently editing (e.g. a selector or a property or a value, + * or even fine grained information like an id in the selector). * * The `resolveState` method iterated over the tokens spitted out by the * tokenizer, using switch cases, follows a state machine logic and finally * figures out these informations: * - The state of the CSS at the cursor (one out of CSS_STATES) - * - The current token that is being edited `cmpleting` + * - The current token that is being edited `completing` * - If the state is "selector", the selector state (one of SELECTOR_STATES) * - If the state is "selector", the current selector till the cursor * - If the state is "value", the corresponding property name @@ -214,30 +213,26 @@ CSSCompleter.prototype = { // From CSS_STATES.property, we can either go to CSS_STATES.value // state when we hit the first ':' or CSS_STATES.selector if "}" is // reached. - if (token.tokenType === "symbol") { - switch (token.text) { - case ":": - scopeStack.push(":"); - if (tokens[cursor - 2].tokenType != "whitespace") { - propertyName = tokens[cursor - 2].text; - } else { - propertyName = tokens[cursor - 3].text; - } - _state = CSS_STATES.value; - break; + if (token.tokenType === "Colon") { + scopeStack.push(":"); + if (tokens[cursor - 2].tokenType != "WhiteSpace") { + propertyName = tokens[cursor - 2].text; + } else { + propertyName = tokens[cursor - 3].text; + } + _state = CSS_STATES.value; + } - case "}": - if (/[{f]/.test(peek(scopeStack))) { - const popped = scopeStack.pop(); - if (popped == "f") { - _state = CSS_STATES.frame; - } else { - selector = ""; - selectors = []; - _state = CSS_STATES.null; - } - } - break; + if (token.tokenType === "CloseCurlyBracket") { + if (/[{f]/.test(peek(scopeStack))) { + const popped = scopeStack.pop(); + if (popped == "f") { + _state = CSS_STATES.frame; + } else { + selector = ""; + selectors = []; + _state = CSS_STATES.null; + } } } break; @@ -245,31 +240,27 @@ CSSCompleter.prototype = { case CSS_STATES.value: // From CSS_STATES.value, we can go to one of CSS_STATES.property, // CSS_STATES.frame, CSS_STATES.selector and CSS_STATES.null - if (token.tokenType === "symbol") { - switch (token.text) { - case ";": - if (/[:]/.test(peek(scopeStack))) { - scopeStack.pop(); - _state = CSS_STATES.property; - } - break; + if (token.tokenType === "Semicolon") { + if (/[:]/.test(peek(scopeStack))) { + scopeStack.pop(); + _state = CSS_STATES.property; + } + } - case "}": - if (peek(scopeStack) == ":") { - scopeStack.pop(); - } + if (token.tokenType === "CloseCurlyBracket") { + if (peek(scopeStack) == ":") { + scopeStack.pop(); + } - if (/[{f]/.test(peek(scopeStack))) { - const popped = scopeStack.pop(); - if (popped == "f") { - _state = CSS_STATES.frame; - } else { - selector = ""; - selectors = []; - _state = CSS_STATES.null; - } - } - break; + if (/[{f]/.test(peek(scopeStack))) { + const popped = scopeStack.pop(); + if (popped == "f") { + _state = CSS_STATES.frame; + } else { + selector = ""; + selectors = []; + _state = CSS_STATES.null; + } } } break; @@ -277,7 +268,7 @@ CSSCompleter.prototype = { case CSS_STATES.selector: // From CSS_STATES.selector, we can only go to CSS_STATES.property // when we hit "{" - if (token.tokenType === "symbol" && token.text == "{") { + if (token.tokenType === "CurlyBracketBlock") { scopeStack.push("{"); _state = CSS_STATES.property; selectors.push(selector); @@ -290,74 +281,87 @@ CSSCompleter.prototype = { case SELECTOR_STATES.class: case SELECTOR_STATES.tag: switch (token.tokenType) { - case "hash": - case "id": + case "Hash": + case "IDHash": selectorState = SELECTOR_STATES.id; - selector += "#" + token.text; + selector += token.text; break; - case "symbol": + case "Delim": if (token.text == ".") { selectorState = SELECTOR_STATES.class; selector += "."; if ( cursor <= tokIndex && - tokens[cursor].tokenType == "ident" + tokens[cursor].tokenType == "Ident" ) { token = tokens[cursor++]; selector += token.text; } } else if (token.text == "#") { + // Lonely # char, that doesn't produce a Hash nor IDHash selectorState = SELECTOR_STATES.id; selector += "#"; - } else if (/[>~+]/.test(token.text)) { + } else if ( + token.text == "+" || + token.text == "~" || + token.text == ">" + ) { selectorState = SELECTOR_STATES.null; selector += token.text; - } else if (token.text == ",") { - selectorState = SELECTOR_STATES.null; - selectors.push(selector); - selector = ""; - } else if (token.text == ":") { - selectorState = SELECTOR_STATES.pseudo; - selector += ":"; - if (cursor > tokIndex) { - break; - } + } + break; - token = tokens[cursor++]; - switch (token.tokenType) { - case "function": - if (token.text == "not") { - selectorBeforeNot = selector; - selector = ""; - scopeStack.push("("); - } else { - selector += token.text + "("; - } - selectorState = SELECTOR_STATES.null; - break; - - case "ident": + case "Comma": + selectorState = SELECTOR_STATES.null; + selectors.push(selector); + selector = ""; + break; + + case "Colon": + selectorState = SELECTOR_STATES.pseudo; + selector += ":"; + if (cursor > tokIndex) { + break; + } + + token = tokens[cursor++]; + switch (token.tokenType) { + case "Function": + if (token.value == "not") { + selectorBeforeNot = selector; + selector = ""; + scopeStack.push("("); + } else { selector += token.text; - break; - } - } else if (token.text == "[") { - selectorState = SELECTOR_STATES.attribute; - scopeStack.push("["); - selector += "["; - } else if (token.text == ")") { - if (peek(scopeStack) == "(") { - scopeStack.pop(); - selector = selectorBeforeNot + "not(" + selector + ")"; - selectorBeforeNot = null; - } else { - selector += ")"; - } - selectorState = SELECTOR_STATES.null; + } + selectorState = SELECTOR_STATES.null; + break; + + case "Ident": + selector += token.text; + break; + } + break; + + case "SquareBracketBlock": + selectorState = SELECTOR_STATES.attribute; + scopeStack.push("["); + selector += "["; + break; + + case "CloseParenthesis": + if (peek(scopeStack) == "(") { + scopeStack.pop(); + selector = selectorBeforeNot + "not(" + selector + ")"; + selectorBeforeNot = null; + } else { + selector += ")"; } + selectorState = SELECTOR_STATES.null; break; - case "whitespace": + case "WhiteSpace": selectorState = SELECTOR_STATES.null; selector && (selector += " "); break; @@ -369,81 +373,94 @@ CSSCompleter.prototype = { // SELECTOR_STATES.id, SELECTOR_STATES.class or // SELECTOR_STATES.tag switch (token.tokenType) { - case "hash": - case "id": + case "Hash": + case "IDHash": selectorState = SELECTOR_STATES.id; - selector += "#" + token.text; + selector += token.text; break; - case "ident": + case "Ident": selectorState = SELECTOR_STATES.tag; selector += token.text; break; - case "symbol": + case "Delim": if (token.text == ".") { selectorState = SELECTOR_STATES.class; selector += "."; if ( cursor <= tokIndex && - tokens[cursor].tokenType == "ident" + tokens[cursor].tokenType == "Ident" ) { token = tokens[cursor++]; selector += token.text; } } else if (token.text == "#") { + // Lonely # char, that doesn't produce a Hash nor IDHash selectorState = SELECTOR_STATES.id; selector += "#"; } else if (token.text == "*") { selectorState = SELECTOR_STATES.tag; selector += "*"; - } else if (/[>~+]/.test(token.text)) { + } else if ( + token.text == "+" || + token.text == "~" || + token.text == ">" + ) { selector += token.text; - } else if (token.text == ",") { - selectorState = SELECTOR_STATES.null; - selectors.push(selector); - selector = ""; - } else if (token.text == ":") { - selectorState = SELECTOR_STATES.pseudo; - selector += ":"; - if (cursor > tokIndex) { - break; - } + } + break; - token = tokens[cursor++]; - switch (token.tokenType) { - case "function": - if (token.text == "not") { - selectorBeforeNot = selector; - selector = ""; - scopeStack.push("("); - } else { - selector += token.text + "("; - } - selectorState = SELECTOR_STATES.null; - break; - - case "ident": + case "Comma": + selectorState = SELECTOR_STATES.null; + selectors.push(selector); + selector = ""; + break; + + case "Colon": + selectorState = SELECTOR_STATES.pseudo; + selector += ":"; + if (cursor > tokIndex) { + break; + } + + token = tokens[cursor++]; + switch (token.tokenType) { + case "Function": + if (token.value == "not") { + selectorBeforeNot = selector; + selector = ""; + scopeStack.push("("); + } else { selector += token.text; - break; - } - } else if (token.text == "[") { - selectorState = SELECTOR_STATES.attribute; - scopeStack.push("["); - selector += "["; - } else if (token.text == ")") { - if (peek(scopeStack) == "(") { - scopeStack.pop(); - selector = selectorBeforeNot + "not(" + selector + ")"; - selectorBeforeNot = null; - } else { - selector += ")"; - } - selectorState = SELECTOR_STATES.null; + } + selectorState = SELECTOR_STATES.null; + break; + + case "Ident": + selector += token.text; + break; } break; - case "whitespace": + case "SquareBracketBlock": + selectorState = SELECTOR_STATES.attribute; + scopeStack.push("["); + selector += "["; + break; + + case "CloseParenthesis": + if (peek(scopeStack) == "(") { + scopeStack.pop(); + selector = selectorBeforeNot + "not(" + selector + ")"; + selectorBeforeNot = null; + } else { + selector += ")"; + } + selectorState = SELECTOR_STATES.null; + break; + + case "WhiteSpace": selector && (selector += " "); break; } @@ -451,46 +468,55 @@ CSSCompleter.prototype = { case SELECTOR_STATES.pseudo: switch (token.tokenType) { - case "symbol": - if (/[>~+]/.test(token.text)) { + case "Delim": + if ( + token.text == "+" || + token.text == "~" || + token.text == ">" + ) { selectorState = SELECTOR_STATES.null; selector += token.text; - } else if (token.text == ",") { - selectorState = SELECTOR_STATES.null; - selectors.push(selector); - selector = ""; - } else if (token.text == ":") { - selectorState = SELECTOR_STATES.pseudo; - selector += ":"; - if (cursor > tokIndex) { - break; - } + } + break; + + case "Comma": + selectorState = SELECTOR_STATES.null; + selectors.push(selector); + selector = ""; + break; + + case "Colon": + selectorState = SELECTOR_STATES.pseudo; + selector += ":"; + if (cursor > tokIndex) { + break; + } - token = tokens[cursor++]; - switch (token.tokenType) { - case "function": - if (token.text == "not") { - selectorBeforeNot = selector; - selector = ""; - scopeStack.push("("); - } else { - selector += token.text + "("; - } - selectorState = SELECTOR_STATES.null; - break; - - case "ident": + token = tokens[cursor++]; + switch (token.tokenType) { + case "Function": + if (token.value == "not") { + selectorBeforeNot = selector; + selector = ""; + scopeStack.push("("); + } else { selector += token.text; - break; - } - } else if (token.text == "[") { - selectorState = SELECTOR_STATES.attribute; - scopeStack.push("["); - selector += "["; + } + selectorState = SELECTOR_STATES.null; + break; + + case "Ident": + selector += token.text; + break; } break; + case "SquareBracketBlock": + selectorState = SELECTOR_STATES.attribute; + scopeStack.push("["); + selector += "["; + break; - case "whitespace": + case "WhiteSpace": selectorState = SELECTOR_STATES.null; selector && (selector += " "); break; @@ -499,29 +525,40 @@ CSSCompleter.prototype = { case SELECTOR_STATES.attribute: switch (token.tokenType) { - case "symbol": - if (/[~|^$*]/.test(token.text)) { - selector += token.text; - token = tokens[cursor++]; - } else if (token.text == "=") { + case "IncludeMatch": + case "DashMatch": + case "PrefixMatch": + case "IncludeSuffixMatchMatch": + case "SubstringMatch": + selector += token.text; + token = tokens[cursor++]; + break; + + case "Delim": + if (token.text == "=") { selectorState = SELECTOR_STATES.value; selector += token.text; - } else if (token.text == "]") { - if (peek(scopeStack) == "[") { - scopeStack.pop(); - } + } + break; - selectorState = SELECTOR_STATES.null; - selector += "]"; + case "CloseSquareBracket": + if (peek(scopeStack) == "[") { + scopeStack.pop(); } + + selectorState = SELECTOR_STATES.null; + selector += "]"; break; - case "ident": - case "string": + case "Ident": selector += token.text; break; - case "whitespace": + case "QuotedString": + selector += token.value; + break; + + case "WhiteSpace": selector && (selector += " "); break; } @@ -529,23 +566,24 @@ CSSCompleter.prototype = { case SELECTOR_STATES.value: switch (token.tokenType) { - case "string": - case "ident": + case "Ident": selector += token.text; break; - case "symbol": - if (token.text == "]") { - if (peek(scopeStack) == "[") { - scopeStack.pop(); - } + case "QuotedString": + selector += token.value; + break; - selectorState = SELECTOR_STATES.null; - selector += "]"; + case "CloseSquareBracket": + if (peek(scopeStack) == "[") { + scopeStack.pop(); } + + selectorState = SELECTOR_STATES.null; + selector += "]"; break; - case "whitespace": + case "WhiteSpace": selector && (selector += " "); break; } @@ -557,29 +595,30 @@ CSSCompleter.prototype = { // From CSS_STATES.null state, we can go to either CSS_STATES.media or // CSS_STATES.selector. switch (token.tokenType) { - case "hash": - case "id": + case "Hash": + case "IDHash": selectorState = SELECTOR_STATES.id; - selector = "#" + token.text; + selector = token.text; _state = CSS_STATES.selector; break; - case "ident": + case "Ident": selectorState = SELECTOR_STATES.tag; selector = token.text; _state = CSS_STATES.selector; break; - case "symbol": + case "Delim": if (token.text == ".") { selectorState = SELECTOR_STATES.class; selector = "."; _state = CSS_STATES.selector; - if (cursor <= tokIndex && tokens[cursor].tokenType == "ident") { + if (cursor <= tokIndex && tokens[cursor].tokenType == "Ident") { token = tokens[cursor++]; selector += token.text; } } else if (token.text == "#") { + // Lonely # char, that doesn't produce a Hash nor IDHash selectorState = SELECTOR_STATES.id; selector = "#"; _state = CSS_STATES.selector; @@ -587,45 +626,52 @@ CSSCompleter.prototype = { selectorState = SELECTOR_STATES.tag; selector = "*"; _state = CSS_STATES.selector; - } else if (token.text == ":") { - _state = CSS_STATES.selector; - selectorState = SELECTOR_STATES.pseudo; - selector += ":"; - if (cursor > tokIndex) { - break; - } + } + break; - token = tokens[cursor++]; - switch (token.tokenType) { - case "function": - if (token.text == "not") { - selectorBeforeNot = selector; - selector = ""; - scopeStack.push("("); - } else { - selector += token.text + "("; - } - selectorState = SELECTOR_STATES.null; - break; + case "Colon": + _state = CSS_STATES.selector; + selectorState = SELECTOR_STATES.pseudo; + selector += ":"; + if (cursor > tokIndex) { + break; + } - case "ident": + token = tokens[cursor++]; + switch (token.tokenType) { + case "Function": + if (token.value == "not") { + selectorBeforeNot = selector; + selector = ""; + scopeStack.push("("); + } else { selector += token.text; - break; - } - } else if (token.text == "[") { - _state = CSS_STATES.selector; - selectorState = SELECTOR_STATES.attribute; - scopeStack.push("["); - selector += "["; - } else if (token.text == "}") { - if (peek(scopeStack) == "@m") { - scopeStack.pop(); - } + } + selectorState = SELECTOR_STATES.null; + break; + + case "Ident": + selector += token.text; + break; + } + break; + + case "CloseSquareBracket": + _state = CSS_STATES.selector; + selectorState = SELECTOR_STATES.attribute; + scopeStack.push("["); + selector += "["; + break; + + case "CurlyBracketBlock": + if (peek(scopeStack) == "@m") { + scopeStack.pop(); } break; - case "at": - _state = token.text.startsWith("m") + case "AtKeyword": + // XXX: We should probably handle other at-rules (@container, @property, …) + _state = token.value.startsWith("m") ? CSS_STATES.media : CSS_STATES.keyframes; break; @@ -635,7 +681,7 @@ CSSCompleter.prototype = { case CSS_STATES.media: // From CSS_STATES.media, we can only go to CSS_STATES.null state when // we hit the first '{' - if (token.tokenType == "symbol" && token.text == "{") { + if (token.tokenType == "CurlyBracketBlock") { scopeStack.push("@m"); _state = CSS_STATES.null; } @@ -644,7 +690,7 @@ CSSCompleter.prototype = { case CSS_STATES.keyframes: // From CSS_STATES.keyframes, we can only go to CSS_STATES.frame state // when we hit the first '{' - if (token.tokenType == "symbol" && token.text == "{") { + if (token.tokenType == "CurlyBracketBlock") { scopeStack.push("@k"); _state = CSS_STATES.frame; } @@ -654,17 +700,15 @@ CSSCompleter.prototype = { // From CSS_STATES.frame, we can either go to CSS_STATES.property // state when we hit the first '{' or to CSS_STATES.selector when we // hit '}' - if (token.tokenType == "symbol") { - if (token.text == "{") { - scopeStack.push("f"); - _state = CSS_STATES.property; - } else if (token.text == "}") { - if (peek(scopeStack) == "@k") { - scopeStack.pop(); - } - - _state = CSS_STATES.null; + if (token.tokenType == "CurlyBracketBlock") { + scopeStack.push("f"); + _state = CSS_STATES.property; + } else if (token.tokenType == "CloseCurlyBracket") { + if (peek(scopeStack) == "@k") { + scopeStack.pop(); } + + _state = CSS_STATES.null; } break; } @@ -688,6 +732,8 @@ CSSCompleter.prototype = { this.nullStates.push([tokenLine, tokenCh, [...scopeStack]]); } } + // ^ while loop end + this.state = _state; this.propertyName = _state == CSS_STATES.value ? propertyName : null; this.selectorState = _state == CSS_STATES.selector ? selectorState : null; @@ -701,10 +747,18 @@ CSSCompleter.prototype = { } this.selectors = selectors; - if (token && token.tokenType != "whitespace") { + if (token && token.tokenType != "WhiteSpace") { let text; - if (token.tokenType == "dimension" || !token.text) { + if (token.tokenType == "Dimension" || !token.text) { text = source.substring(token.startOffset, token.endOffset); + } else if ( + token.tokenType === "IDHash" || + token.tokenType === "Hash" || + token.tokenType === "AtKeyword" || + token.tokenType === "Function" || + token.tokenType === "QuotedString" + ) { + text = token.value; } else { text = token.text; } @@ -1047,10 +1101,10 @@ CSSCompleter.prototype = { } let prevToken = undefined; - const tokens = cssTokenizer(lineText); + const tokensIterator = cssTokenizer(lineText); let found = false; const ech = line == caret.line ? caret.ch : 0; - for (let token of tokens) { + for (let token of tokensIterator) { // If the line is completely spaces, handle it differently if (lineText.trim() == "") { limitedSource += lineText; @@ -1061,8 +1115,8 @@ CSSCompleter.prototype = { ); } - // Whitespace cannot change state. - if (token.tokenType == "whitespace") { + // WhiteSpace cannot change state. + if (token.tokenType == "WhiteSpace") { prevToken = token; continue; } @@ -1072,7 +1126,7 @@ CSSCompleter.prototype = { ch: token.endOffset + ech, }); if (check(forwState)) { - if (prevToken && prevToken.tokenType == "whitespace") { + if (prevToken && prevToken.tokenType == "WhiteSpace") { token = prevToken; } location = { @@ -1123,8 +1177,8 @@ CSSCompleter.prototype = { limitedSource = limitedSource.slice(0, -1 * length); } - // Whitespace cannot change state. - if (token.tokenType == "whitespace") { + // WhiteSpace cannot change state. + if (token.tokenType == "WhiteSpace") { continue; } @@ -1133,7 +1187,7 @@ CSSCompleter.prototype = { ch: token.startOffset, }); if (check(backState)) { - if (tokens[i + 1] && tokens[i + 1].tokenType == "whitespace") { + if (tokens[i + 1] && tokens[i + 1].tokenType == "WhiteSpace") { token = tokens[i + 1]; } location = { diff --git a/devtools/client/shared/sourceeditor/editor.js b/devtools/client/shared/sourceeditor/editor.js index 3487acffa4..90e9f6e373 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(); + #lineContentEventHandlers = {}; #updateListener = null; @@ -676,7 +677,9 @@ class Editor extends EventEmitter { } }), lineNumberMarkersCompartment.of([]), - lineContentMarkerCompartment.of(this.#lineContentMarkersExtension([])), + lineContentMarkerCompartment.of( + this.#lineContentMarkersExtension({ markers: [] }) + ), // keep last so other extension take precedence codemirror.minimalSetup, ]; @@ -696,29 +699,51 @@ 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} markers - 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. */ - #lineContentMarkersExtension(markers) { + #lineContentMarkersExtension({ markers, domEventHandlers }) { const { - codemirrorView: { Decoration, ViewPlugin }, - codemirrorState: { RangeSetBuilder }, + codemirrorView: { Decoration, ViewPlugin, WidgetType }, + codemirrorState: { RangeSetBuilder, RangeSet }, } = this.#CodeMirror6; + class LineContentWidget extends WidgetType { + constructor(line, createElementNode) { + super(); + this.toDOM = () => createElementNode(line); + } + } + // Build and return the decoration set function buildDecorations(view) { + if (!markers) { + 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 { lineClassName, condition } of markers) { - if (condition(line.number)) { - builder.add( - line.from, - line.from, - Decoration.line({ class: lineClassName }) - ); + for (const marker of markers) { + if (marker.condition(line.number)) { + if (marker.lineClassName) { + const classDecoration = Decoration.line({ + class: marker.lineClassName, + }); + builder.add(line.from, line.from, classDecoration); + } + if (marker.createLineElementNode) { + const nodeDecoration = Decoration.widget({ + widget: new LineContentWidget( + line.number, + marker.createLineElementNode + ), + }); + builder.add(line.to, line.to, nodeDecoration); + } } } pos = line.to + 1; @@ -727,9 +752,9 @@ class Editor extends EventEmitter { return builder.finish(); } - // The view which handles rendering and updating the + // The view which handles events, rendering and updating the // markers decorations - const showLineContentDecorations = ViewPlugin.fromClass( + const lineContentMarkersView = ViewPlugin.fromClass( class { decorations; constructor(view) { @@ -741,10 +766,46 @@ class Editor extends EventEmitter { } } }, - { decorations: v => v.decorations } + { + decorations: v => v.decorations, + eventHandlers: domEventHandlers || this.#lineContentEventHandlers, + } ); - return [showLineContentDecorations]; + return [lineContentMarkersView]; + } + + /** + * + * @param {Object} domEventHandlers - A dictionary of handlers for the DOM events + */ + setContentEventListeners(domEventHandlers) { + const cm = editors.get(this); + + for (const eventName in domEventHandlers) { + const handler = domEventHandlers[eventName]; + domEventHandlers[eventName] = (event, editor) => { + // 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 head = view.state.selection.main.head; + const cursor = view.state.doc.lineAt(head); + const column = head - cursor.from; + handler(event, view, cursor.number, column); + }, 0); + }; + } + + // Cache the handlers related to the editor content + this.#lineContentEventHandlers = domEventHandlers; + + cm.dispatch({ + effects: this.#compartments.lineContentMarkerCompartment.reconfigure( + this.#lineContentMarkersExtension({ domEventHandlers }) + ), + }); } /** @@ -754,6 +815,9 @@ class Editor extends EventEmitter { * @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. + * @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. */ setLineContentMarker(marker) { const cm = editors.get(this); @@ -761,9 +825,9 @@ class Editor extends EventEmitter { cm.dispatch({ effects: this.#compartments.lineContentMarkerCompartment.reconfigure( - this.#lineContentMarkersExtension( - Array.from(this.#lineContentMarkers.values()) - ) + this.#lineContentMarkersExtension({ + markers: Array.from(this.#lineContentMarkers.values()), + }) ), }); } @@ -778,9 +842,9 @@ class Editor extends EventEmitter { cm.dispatch({ effects: this.#compartments.lineContentMarkerCompartment.reconfigure( - this.#lineContentMarkersExtension( - Array.from(this.#lineContentMarkers.values()) - ) + this.#lineContentMarkersExtension({ + markers: Array.from(this.#lineContentMarkers.values()), + }) ), }); } @@ -869,31 +933,31 @@ class Editor extends EventEmitter { // (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 - ) - ); - } + 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"); + } + if (condition(line.number)) { + builder.add( + line.from, + line.to, + new LineGutterMarker( + lineClassName, + line.number, + createLineElementNode + ) + ); } - pos = line.to + 1; } + pos = line.to + 1; } // To update the state with the newly generated marker range set, a dispatch is called on the view @@ -906,6 +970,61 @@ class Editor extends EventEmitter { }); } + /** + * 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(); + } + + /** + * Check that text is selected + * @returns {Boolean} + */ + isTextSelected() { + const cm = editors.get(this); + if (this.config.cm6) { + const selection = cm.state.selection.ranges[0]; + return selection.from !== selection.to; + } + return cm.somethingSelected(); + } + /** * Returns a boolean indicating whether the editor is ready to * use. Use appendTo(el).then(() => {}) for most cases @@ -1859,6 +1978,119 @@ class Editor extends EventEmitter { }); } + /** + * This checks if the specified position (top/left) is within the current viewpport + * bounds. it helps determine is scrolling should happen. + * @param {Object} cm - The codemirror instance + * @param {Number} line - The line in the source + * @param {Number} column - The column in the source + * @returns {Boolean} + */ + #isVisible(cm, line, column) { + let inXView, inYView; + + function withinBounds(x, min, max) { + return x >= min && x <= max; + } + + if (this.config.cm6) { + const pos = this.#posToOffset(cm.state.doc, line, column); + const coords = pos && cm.coordsAtPos(pos); + if (!coords) { + return false; + } + const { scrollTop, scrollLeft, clientHeight, clientWidth } = cm.scrollDOM; + + inXView = withinBounds(coords.left, scrollLeft, scrollLeft + clientWidth); + inYView = withinBounds(coords.top, scrollTop, scrollTop + clientHeight); + } else { + const { top, left } = cm.charCoords({ line, ch: column }, "local"); + const scrollArea = cm.getScrollInfo(); + const charWidth = cm.defaultCharWidth(); + const fontHeight = cm.defaultTextHeight(); + const { scrollTop, scrollLeft } = cm.doc; + + inXView = withinBounds( + left, + scrollLeft, + // Note: 30 might relate to the margin on one of the scroll bar elements. + // See comment https://github.com/firefox-devtools/debugger/pull/5182#discussion_r163439209 + scrollLeft + (scrollArea.clientWidth - 30) - charWidth + ); + inYView = withinBounds( + top, + scrollTop, + scrollTop + scrollArea.clientHeight - fontHeight + ); + } + return inXView && inYView; + } + + /** + * Converts line/col to CM6 offset position + * @param {Object} doc - the codemirror document + * @param {Number} line - The line in the source + * @param {Number} col - The column in the source + * @returns {Number} + */ + #posToOffset(doc, line, col) { + if (!this.config.cm6) { + throw new Error("This function is only compatible with CM6"); + } + try { + const offset = doc.line(line); + return offset.from + col; + } catch (e) { + // Line likey does not exist in viewport yet + console.warn(e.message); + } + return null; + } + + /** + * Scrolls the editor to the specified line and column + * @param {Number} line - The line in the source + * @param {Number} column - The column in the source + */ + scrollTo(line, column) { + const cm = editors.get(this); + if (this.config.cm6) { + const { + codemirrorView: { EditorView }, + } = this.#CodeMirror6; + + if (!this.#isVisible(cm, line, column)) { + const offset = this.#posToOffset(cm.state.doc, line, column); + if (!offset) { + return; + } + cm.dispatch({ + effects: EditorView.scrollIntoView(offset, { + x: "nearest", + y: "center", + }), + }); + } + } else { + // For all cases where these are on the first line and column, + // avoid the possibly slow computation of cursor location on large bundles. + if (!line && !column) { + cm.scrollTo(0, 0); + return; + } + + const { top, left } = cm.charCoords({ line, ch: column }, "local"); + + if (!this.#isVisible(cm, line, column)) { + const scroller = cm.getScrollerElement(); + const centeredX = Math.max(left - scroller.offsetWidth / 2, 0); + const centeredY = Math.max(top - scroller.offsetHeight / 2, 0); + + cm.scrollTo(centeredX, centeredY); + } + } + } + /** * Extends an instance of the Editor object with additional * functions. Each function will be called with context as @@ -1901,6 +2133,8 @@ class Editor extends EventEmitter { this.#ownerDoc = null; this.#updateListener = null; this.#lineGutterMarkers.clear(); + this.#lineContentMarkers.clear(); + this.#lineContentEventHandlers = {}; if (this.#prefObserver) { this.#prefObserver.off(KEYMAP_PREF, this.setKeyMap); diff --git a/devtools/client/shared/test/browser_filter-editor-01.js b/devtools/client/shared/test/browser_filter-editor-01.js index 557a02857c..106f89dbc4 100644 --- a/devtools/client/shared/test/browser_filter-editor-01.js +++ b/devtools/client/shared/test/browser_filter-editor-01.js @@ -10,15 +10,14 @@ const { } = require("resource://devtools/client/shared/widgets/FilterWidget.js"); const TEST_URI = CHROME_URL_ROOT + "doc_filter-editor-01.html"; -const { getCSSLexer } = require("resource://devtools/shared/css/lexer.js"); // Verify that the given string consists of a valid CSS URL token. // Return true on success, false on error. function verifyURL(string) { - const lexer = getCSSLexer(string); + const lexer = new InspectorCSSParser(string); const token = lexer.nextToken(); - if (!token || token.tokenType !== "url") { + if (!token || token.tokenType !== "UnquotedUrl") { return false; } diff --git a/devtools/client/shared/test/browser_outputparser.js b/devtools/client/shared/test/browser_outputparser.js index 7e0c1b7e00..a2fcce8f78 100644 --- a/devtools/client/shared/test/browser_outputparser.js +++ b/devtools/client/shared/test/browser_outputparser.js @@ -700,6 +700,22 @@ function testParseVariable(doc, parser) { ")" + "", }, + { + text: "rgb(var(--not-seen), 0, 0)", + variables: {}, + expected: + // prettier-ignore + `rgb(` + + `` + + `var(` + + `` + + `--not-seen` + + `` + + `)` + + `` + + `, 0, 0` + + `)`, + }, ]; for (const test of TESTS) { diff --git a/devtools/client/shared/test/xpcshell/test_parseDeclarations.js b/devtools/client/shared/test/xpcshell/test_parseDeclarations.js index 593087d46b..373d5acc56 100644 --- a/devtools/client/shared/test/xpcshell/test_parseDeclarations.js +++ b/devtools/client/shared/test/xpcshell/test_parseDeclarations.js @@ -750,7 +750,7 @@ const TEST_DATA = [ expected: [ { name: "color", - value: "blue \\9 no_need", + value: "blue \\9 no\\_need", priority: "", offsets: [0, 23], declarationText: "color: blue \\9 no\\_need", @@ -1609,18 +1609,27 @@ function assertOutput(input, actual, expected) { "Check that the output item has the expected name, " + "value and priority" ); - Assert.equal(expected[i].name, actual[i].name); - Assert.equal(expected[i].value, actual[i].value); - Assert.equal(expected[i].priority, actual[i].priority); - deepEqual(expected[i].offsets, actual[i].offsets); + Assert.equal(actual[i].name, expected[i].name, "Expected name"); + Assert.equal(actual[i].value, expected[i].value, "Expected value"); + Assert.equal( + actual[i].priority, + expected[i].priority, + "Expected priority" + ); + deepEqual(actual[i].offsets, expected[i].offsets, "Expected offsets"); if ("commentOffsets" in expected[i]) { - deepEqual(expected[i].commentOffsets, actual[i].commentOffsets); + deepEqual( + actual[i].commentOffsets, + expected[i].commentOffsets, + "Expected commentOffsets" + ); } if (expected[i].declarationText) { Assert.equal( input.substring(expected[i].offsets[0], expected[i].offsets[1]), - expected[i].declarationText + expected[i].declarationText, + "Expected declarationText" ); } } diff --git a/devtools/client/shared/test/xpcshell/test_rewriteDeclarations.js b/devtools/client/shared/test/xpcshell/test_rewriteDeclarations.js index 228d2dc79d..9842db7c02 100644 --- a/devtools/client/shared/test/xpcshell/test_rewriteDeclarations.js +++ b/devtools/client/shared/test/xpcshell/test_rewriteDeclarations.js @@ -483,9 +483,7 @@ const TEST_DATA = [ enabled: true, }, expected: "something: \\\\;color: red;", - // The lexer rewrites the token before we see it. However this is - // so obscure as to be inconsequential. - changed: { 0: "\uFFFD\\" }, + changed: { 0: "\\\\" }, }, // Termination insertion corner case. diff --git a/devtools/client/shared/widgets/CubicBezierWidget.js b/devtools/client/shared/widgets/CubicBezierWidget.js index 39407d4711..df41949df5 100644 --- a/devtools/client/shared/widgets/CubicBezierWidget.js +++ b/devtools/client/shared/widgets/CubicBezierWidget.js @@ -31,7 +31,9 @@ const { PRESETS, DEFAULT_PRESET_CATEGORY, } = require("resource://devtools/client/shared/widgets/CubicBezierPresets.js"); -const { getCSSLexer } = require("resource://devtools/shared/css/lexer.js"); +const { + InspectorCSSParserWrapper, +} = require("resource://devtools/shared/css/lexer.js"); const XHTML_NS = "http://www.w3.org/1999/xhtml"; /** @@ -918,13 +920,13 @@ function parseTimingFunction(value) { return PREDEFINED[value]; } - const tokenStream = getCSSLexer(value); + const tokenStream = new InspectorCSSParserWrapper(value); const getNextToken = () => { while (true) { const token = tokenStream.nextToken(); if ( !token || - (token.tokenType !== "whitespace" && token.tokenType !== "comment") + (token.tokenType !== "WhiteSpace" && token.tokenType !== "Comment") ) { return token; } @@ -932,24 +934,20 @@ function parseTimingFunction(value) { }; let token = getNextToken(); - if (token.tokenType !== "function" || token.text !== "cubic-bezier") { + if (token.tokenType !== "Function" || token.value !== "cubic-bezier") { return undefined; } const result = []; for (let i = 0; i < 4; ++i) { token = getNextToken(); - if (!token || token.tokenType !== "number") { + if (!token || token.tokenType !== "Number") { return undefined; } result.push(token.number); token = getNextToken(); - if ( - !token || - token.tokenType !== "symbol" || - token.text !== (i == 3 ? ")" : ",") - ) { + if (!token || token.tokenType !== (i == 3 ? "CloseParenthesis" : "Comma")) { return undefined; } } diff --git a/devtools/client/shared/widgets/FilterWidget.js b/devtools/client/shared/widgets/FilterWidget.js index bb23bdfeca..487cc9ad0b 100644 --- a/devtools/client/shared/widgets/FilterWidget.js +++ b/devtools/client/shared/widgets/FilterWidget.js @@ -814,7 +814,7 @@ CSSFilterEditorWidget.prototype = { return; } - for (let { name, value, quote } of tokenizeFilterValue(cssValue)) { + for (let { name, value } of tokenizeFilterValue(cssValue)) { // If the specified value is invalid, replace it with the // default. if (name !== "url") { @@ -823,7 +823,7 @@ CSSFilterEditorWidget.prototype = { } } - this.add(name, value, quote, true); + this.add(name, value, true); } this.emit("updated", this.getCssValue()); @@ -838,9 +838,6 @@ CSSFilterEditorWidget.prototype = { * @param {String} value * value of the filter (e.g. 30px, 20%) * If this is |null|, then a default value may be supplied. - * @param {String} quote - * For a url filter, the quoting style. This can be a - * single quote, a double quote, or empty. * @return {Number} * The index of the new filter in the current list of filters * @param {Boolean} @@ -848,7 +845,7 @@ CSSFilterEditorWidget.prototype = { * you're calling add in a loop and wait to emit a single event after * the loop yourself, set this parameter to true. */ - add(name, value, quote, noEvent) { + add(name, value, noEvent) { const def = this._definition(name); if (!def) { return false; @@ -868,11 +865,6 @@ CSSFilterEditorWidget.prototype = { } else { value = def.range[0] + unitLabel; } - - if (name === "url") { - // Default quote. - quote = '"'; - } } let unit = def.type === "string" ? "" : (/[a-zA-Z%]+/.exec(value) || [])[0]; @@ -894,7 +886,7 @@ CSSFilterEditorWidget.prototype = { } } - const index = this.filters.push({ value, unit, name, quote }) - 1; + const index = this.filters.push({ value, unit, name }) - 1; if (!noEvent) { this.emit("updated", this.getCssValue()); } @@ -916,22 +908,12 @@ CSSFilterEditorWidget.prototype = { return null; } - // Just return the value+unit for non-url functions. - if (filter.name !== "url") { - return filter.value + filter.unit; + // Just return the value url functions. + if (filter.name === "url") { + return filter.value; } - // url values need to be quoted and escaped. - if (filter.quote === "'") { - return "'" + filter.value.replace(/\'/g, "\\'") + "'"; - } else if (filter.quote === '"') { - return '"' + filter.value.replace(/\"/g, '\\"') + '"'; - } - - // Unquoted. This approach might change the original input -- for - // example the original might be over-quoted. But, this is - // correct and probably good enough. - return filter.value.replace(/[\\ \t()"']/g, "\\$&"); + return filter.value + filter.unit; }, removeAt(index) { @@ -1051,27 +1033,34 @@ function tokenizeFilterValue(css) { for (const token of cssTokenizer(css)) { switch (state) { case "initial": - if (token.tokenType === "function") { - name = token.text; + if (token.tokenType === "Function") { + name = token.value; contents = ""; state = "function"; depth = 1; - } else if (token.tokenType === "url" || token.tokenType === "bad_url") { - // Extract the quoting style from the url. - const originalText = css.substring( - token.startOffset, - token.endOffset - ); - const [, quote] = /^url\([ \t\r\n\f]*(["']?)/i.exec(originalText); - - filters.push({ name: "url", value: token.text.trim(), quote }); + } else if ( + token.tokenType === "UnquotedUrl" || + token.tokenType === "BadUrl" + ) { + const url = token.text + .substring( + // token text starts with `url(` + 4, + // unquoted url also include the closing parenthesis + token.tokenType == "UnquotedUrl" + ? token.text.length - 1 + : undefined + ) + .trim(); + + filters.push({ name: "url", value: url }); // Leave state as "initial" because the URL token includes // the trailing close paren. } break; case "function": - if (token.tokenType === "symbol" && token.text === ")") { + if (token.tokenType === "CloseParenthesis") { --depth; if (depth === 0) { filters.push({ name, value: contents.trim() }); @@ -1081,8 +1070,8 @@ function tokenizeFilterValue(css) { } contents += css.substring(token.startOffset, token.endOffset); if ( - token.tokenType === "function" || - (token.tokenType === "symbol" && token.text === "(") + token.tokenType === "Function" || + token.tokenType === "Parenthesis" ) { ++depth; } diff --git a/devtools/client/shared/widgets/LinearEasingFunctionWidget.js b/devtools/client/shared/widgets/LinearEasingFunctionWidget.js index e6d2e604df..5ea3b33d15 100644 --- a/devtools/client/shared/widgets/LinearEasingFunctionWidget.js +++ b/devtools/client/shared/widgets/LinearEasingFunctionWidget.js @@ -9,7 +9,7 @@ */ const EventEmitter = require("devtools/shared/event-emitter"); -const { getCSSLexer } = require("devtools/shared/css/lexer"); +const { InspectorCSSParserWrapper } = require("devtools/shared/css/lexer"); const { throttle } = require("devtools/shared/throttle"); const XHTML_NS = "http://www.w3.org/1999/xhtml"; const SVG_NS = "http://www.w3.org/2000/svg"; @@ -578,13 +578,13 @@ class TimingFunctionPreviewWidget { */ function parseTimingFunction(value) { value = value.trim(); - const tokenStream = getCSSLexer(value); + const tokenStream = new InspectorCSSParserWrapper(value); const getNextToken = () => { while (true) { const token = tokenStream.nextToken(); if ( !token || - (token.tokenType !== "whitespace" && token.tokenType !== "comment") + (token.tokenType !== "WhiteSpace" && token.tokenType !== "Comment") ) { return token; } @@ -592,7 +592,7 @@ function parseTimingFunction(value) { }; let token = getNextToken(); - if (!token || token.tokenType !== "function" || token.text !== "linear") { + if (!token || token.tokenType !== "Function" || token.value !== "linear") { return undefined; } @@ -601,11 +601,11 @@ function parseTimingFunction(value) { let largestInput = -Infinity; while ((token = getNextToken())) { - if (token.text === ")") { + if (token.tokenType === "CloseParenthesis") { break; } - if (token.tokenType === "number") { + if (token.tokenType === "Number") { // [parsing step 4.1] const point = { input: null, output: token.number }; // [parsing step 4.2] @@ -614,7 +614,7 @@ function parseTimingFunction(value) { // get nextToken to see if there's a linear stop length token = getNextToken(); // [parsing step 4.3] - if (token && token.tokenType === "percentage") { + if (token && token.tokenType === "Percentage") { // [parsing step 4.3.1] point.input = Math.max(token.number, largestInput); // [parsing step 4.3.2] @@ -624,7 +624,7 @@ function parseTimingFunction(value) { token = getNextToken(); // [parsing step 4.3.3] - if (token && token.tokenType === "percentage") { + if (token && token.tokenType === "Percentage") { // [parsing step 4.3.3.1] const extraPoint = { input: null, output: point.output }; // [parsing step 4.3.3.2] diff --git a/devtools/client/storage/test/browser.toml b/devtools/client/storage/test/browser.toml index 28b4b04258..6afcd80bf2 100644 --- a/devtools/client/storage/test/browser.toml +++ b/devtools/client/storage/test/browser.toml @@ -39,6 +39,7 @@ support-files = [ "storage-unsecured-iframe-usercontextid.html", "storage-updates.html", "head.js", + "!/devtools/client/shared/test/highlighter-test-actor.js", "!/devtools/client/shared/test/shared-head.js", "!/devtools/client/shared/test/telemetry-test-helpers.js", ] diff --git a/devtools/client/themes/images/tool-profiler.svg b/devtools/client/themes/images/tool-profiler.svg index 44e435001a..1c605751ca 100644 --- a/devtools/client/themes/images/tool-profiler.svg +++ b/devtools/client/themes/images/tool-profiler.svg @@ -1,6 +1,6 @@ - + diff --git a/devtools/client/webconsole/components/Output/Message.js b/devtools/client/webconsole/components/Output/Message.js index ee65c8947a..8fed9fd084 100644 --- a/devtools/client/webconsole/components/Output/Message.js +++ b/devtools/client/webconsole/components/Output/Message.js @@ -255,6 +255,10 @@ class Message extends Component { JSON.stringify( this.props.message, function (key, value) { + if (key === "targetFront") { + return null; + } + // The message can hold one or multiple fronts that we need to serialize if (value?.getGrip) { return value.getGrip(); diff --git a/devtools/client/webconsole/test/browser/_browser_console.toml b/devtools/client/webconsole/test/browser/_browser_console.toml index d3d3d42146..e5530874ab 100644 --- a/devtools/client/webconsole/test/browser/_browser_console.toml +++ b/devtools/client/webconsole/test/browser/_browser_console.toml @@ -61,6 +61,9 @@ skip-if = [ ["browser_console_eager_eval.js"] +["browser_console_eager_eval_resolve.js"] +skip-if = ["verify"] + ["browser_console_enable_network_monitoring.js"] skip-if = [ "verify", diff --git a/devtools/client/webconsole/test/browser/browser_console_eager_eval_resolve.js b/devtools/client/webconsole/test/browser/browser_console_eager_eval_resolve.js new file mode 100644 index 0000000000..cc2daa33ea --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_console_eager_eval_resolve.js @@ -0,0 +1,64 @@ +/* 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"; + +// Check evaluating eager-evaluation values. +const TEST_URI = "data:text/html;charset=utf8,"; + +add_task(async function () { + await addTab(TEST_URI); + + await pushPref("devtools.chrome.enabled", true); + + info("Open the Browser Console"); + const hud = await BrowserConsoleManager.toggleBrowserConsole(); + + await executeResolveHookWithSideEffect(hud); +}); + +async function executeResolveHookWithSideEffect(hud) { + // Services.droppedLinkHandler is implemented with resolve hook, which imports + // ContentAreaDropListener.sys.mjs. + // + // In order to test the resolve hook behavior, ensure the module is not yet + // loaded, which ensures the property is not yet resolved. + // + // NOTE: This test is not compatible with verify mode, given it depends on the + // initial state of the Services object and the module. + is( + Cu.isESModuleLoaded( + "resource://gre/modules/ContentAreaDropListener.sys.mjs" + ), + false + ); + + setInputValue(hud, `Services.droppedLinkHandler`); + + await wait(500); + // Eager evaluation should fail, due to the side effect in the resolve hook. + await waitForEagerEvaluationResult(hud, ""); + + setInputValue(hud, ""); + await wait(500); + + // The property should be resolved when evaluating after the eager evaluation. + await executeAndWaitForResultMessage( + hud, + `Services.droppedLinkHandler;`, + "XPCWrappedNative_NoHelper" + ); + + is( + Cu.isESModuleLoaded( + "resource://gre/modules/ContentAreaDropListener.sys.mjs" + ), + true + ); + + // Eager evaluation should work after the property is resolved. + setInputValue(hud, `Services.droppedLinkHandler`); + await wait(500); + await waitForEagerEvaluationResult(hud, /XPCWrappedNative_NoHelper/); +} diff --git a/devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_file.js b/devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_file.js index f872745bbd..09e17594ab 100644 --- a/devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_file.js +++ b/devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_file.js @@ -97,16 +97,10 @@ add_task(async function () { const dayString = date.getDate().toString().padStart(2, "0"); const expectedDateString = `${date.getFullYear()}-${monthString}-${dayString}`; - let screenshotDir; - try { - // This will throw if there is not a screenshot directory set for the platform - screenshotDir = Services.dirsvc.get("Scrnshts", Ci.nsIFile).path; - } catch (e) { - const { Downloads } = ChromeUtils.importESModule( - "resource://gre/modules/Downloads.sys.mjs" - ); - screenshotDir = await Downloads.getPreferredDownloadsDirectory(); - } + const { Downloads } = ChromeUtils.importESModule( + "resource://gre/modules/Downloads.sys.mjs" + ); + const screenshotDir = await Downloads.getPreferredDownloadsDirectory(); const { renderedDate, filePath } = /Saved to (?.*Screen Shot (?\d{4}-\d{2}-\d{2}) at \d{2}.\d{2}.\d{2}\.png)/.exec( -- cgit v1.2.3