summaryrefslogtreecommitdiffstats
path: root/devtools/client
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 05:43:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 05:43:14 +0000
commit8dd16259287f58f9273002717ec4d27e97127719 (patch)
tree3863e62a53829a84037444beab3abd4ed9dfc7d0 /devtools/client
parentReleasing progress-linux version 126.0.1-1~progress7.99u1. (diff)
downloadfirefox-8dd16259287f58f9273002717ec4d27e97127719.tar.xz
firefox-8dd16259287f58f9273002717ec4d27e97127719.zip
Merging upstream version 127.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client')
-rw-r--r--devtools/client/aboutdebugging/test/node/README.md2
-rw-r--r--devtools/client/accessibility/test/node/README.md2
-rw-r--r--devtools/client/bin/devtools-node-test-runner.js2
-rw-r--r--devtools/client/debugger/src/actions/context-menus/editor.js25
-rw-r--r--devtools/client/debugger/src/components/Editor/Breakpoints.css8
-rw-r--r--devtools/client/debugger/src/components/Editor/ConditionalPanel.css5
-rw-r--r--devtools/client/debugger/src/components/Editor/ConditionalPanel.js72
-rw-r--r--devtools/client/debugger/src/components/Editor/Editor.css4
-rw-r--r--devtools/client/debugger/src/components/Editor/Footer.js2
-rw-r--r--devtools/client/debugger/src/components/Editor/HighlightLine.js132
-rw-r--r--devtools/client/debugger/src/components/Editor/HighlightLines.js44
-rw-r--r--devtools/client/debugger/src/components/Editor/InlinePreviews.js88
-rw-r--r--devtools/client/debugger/src/components/Editor/index.js116
-rw-r--r--devtools/client/debugger/src/utils/editor/index.js44
-rw-r--r--devtools/client/debugger/src/utils/editor/tests/editor.spec.js12
-rw-r--r--devtools/client/debugger/src/utils/source.js9
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg-event-breakpoints.js10
-rw-r--r--devtools/client/debugger/test/mochitest/browser_dbg-javascript-tracer.js45
-rw-r--r--devtools/client/debugger/test/mochitest/examples/event-breakpoints.js7
-rw-r--r--devtools/client/framework/test/browser.toml3
-rw-r--r--devtools/client/framework/test/browser_toolbox_many_toggles.js50
-rw-r--r--devtools/client/framework/test/node/README.md2
-rw-r--r--devtools/client/fronts/inspector/rule-rewriter.js100
-rw-r--r--devtools/client/fronts/walker.js7
-rw-r--r--devtools/client/inspector/rules/test/browser_part2.toml2
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_keyframes-rule-nested.js89
-rw-r--r--devtools/client/inspector/rules/test/browser_rules_pseudo-element_01.js99
-rw-r--r--devtools/client/inspector/rules/test/doc_pseudoelement.html20
-rw-r--r--devtools/client/inspector/shared/utils.js6
-rw-r--r--devtools/client/inspector/test/browser_inspector_picker-shift-key.js35
-rw-r--r--devtools/client/inspector/test/head.js8
-rw-r--r--devtools/client/netmonitor/test/browser.toml4
-rw-r--r--devtools/client/netmonitor/test/browser_net_clear.js8
-rw-r--r--devtools/client/netmonitor/test/browser_net_column_slow-request-indicator.js2
-rw-r--r--devtools/client/netmonitor/test/browser_net_send-beacon.js23
-rw-r--r--devtools/client/netmonitor/test/browser_net_zstd.js81
-rw-r--r--devtools/client/netmonitor/test/html_send-beacon-late-iframe-request.html25
-rw-r--r--devtools/client/netmonitor/test/html_zstd-test-page.html41
-rw-r--r--devtools/client/netmonitor/test/sjs_content-type-test-server.sjs12
-rw-r--r--devtools/client/performance-new/shared/background.sys.mjs23
-rw-r--r--devtools/client/performance-new/shared/utils.js7
-rw-r--r--devtools/client/performance-new/test/browser/browser_interaction-between-interfaces.js17
-rw-r--r--devtools/client/shared/components/object-inspector/utils/node.js5
-rw-r--r--devtools/client/shared/css-angle.js11
-rw-r--r--devtools/client/shared/output-parser.js316
-rw-r--r--devtools/client/shared/screenshot.js14
-rw-r--r--devtools/client/shared/sourceeditor/css-autocompleter.js578
-rw-r--r--devtools/client/shared/sourceeditor/editor.js324
-rw-r--r--devtools/client/shared/test/browser_filter-editor-01.js5
-rw-r--r--devtools/client/shared/test/browser_outputparser.js16
-rw-r--r--devtools/client/shared/test/xpcshell/test_parseDeclarations.js23
-rw-r--r--devtools/client/shared/test/xpcshell/test_rewriteDeclarations.js4
-rw-r--r--devtools/client/shared/widgets/CubicBezierWidget.js18
-rw-r--r--devtools/client/shared/widgets/FilterWidget.js69
-rw-r--r--devtools/client/shared/widgets/LinearEasingFunctionWidget.js16
-rw-r--r--devtools/client/storage/test/browser.toml1
-rw-r--r--devtools/client/themes/images/tool-profiler.svg2
-rw-r--r--devtools/client/webconsole/components/Output/Message.js4
-rw-r--r--devtools/client/webconsole/test/browser/_browser_console.toml3
-rw-r--r--devtools/client/webconsole/test/browser/browser_console_eager_eval_resolve.js64
-rw-r--r--devtools/client/webconsole/test/browser/browser_jsterm_screenshot_command_file.js14
61 files changed, 1964 insertions, 816 deletions
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 <http://mozilla.org/MPL/2.0/>. */
+/**
+ * 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 <http://mozilla.org/MPL/2.0/>. */
+/**
+ * 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 <http://mozilla.org/MPL/2.0/>. */
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,<input type="text" value="pre existing iframe" onkeydown="console.log('keydown')" />`;
+ 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,<input type="text" value="new iframe" onkeypress="console.log('keypress')" />`;
+ 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(`
+ <style>
+ body {
+ animation-name: in-layer,
+ in-starting-style,
+ in-media,
+ in-container;
+ animation-duration: 1s, 2s, 3s, 4s;
+ border: 4px solid;
+ outline: 4px solid;
+ }
+
+ @layer {
+ @keyframes in-layer {
+ from { color: red; }
+ to { color: blue; }
+ }
+ }
+
+ @starting-style {
+ /* keyframes is considered as being outside of @starting-style */
+ @keyframes in-starting-style {
+ from { border-color: tomato; }
+ to { border-color: gold; }
+ }
+ }
+
+ @media screen {
+ @keyframes in-media {
+ from { outline-color: purple; }
+ to { outline-color: pink; }
+ }
+ }
+
+ @container (width > 0px) {
+ /* keyframes is considered as being outside of @container */
+ @keyframes in-container {
+ from { background-color: green; }
+ to { background-color: lime; }
+ }
+ }
+ </style>
+ <h1>Nested <code>@keyframes</code></h1>
+`)}`;
+
+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;
+}
</style>
</head>
<body>
@@ -167,7 +175,10 @@ input::slider-track {
<li id="list" class="box">List element</li>
</ol>
- <dialog>In dialog</dialog>
+ <dialog>
+ In dialog
+ <div id="in-dialog" popover>hello</div>
+ </dialog>
<label>Range <input type="range" class="slider"></label>
<label>Not range <input type="text" class="slider"></label>
@@ -177,10 +188,15 @@ input::slider-track {
You can use them to examine, edit, and debug HTML, CSS, and JavaScript.
</section>
+ <section class="url-fragment-text-directives">May the fox be with you</section>
+
+ <canvas></canvas>
+
<script>
"use strict";
// This is the only way to have the ::backdrop style to be applied
document.querySelector("dialog").showModal()
+ document.querySelector("#in-dialog").showPopover()
// Register highlights for ::highlight pseudo elements
const highlightsContainer = document.querySelector(".highlights-container");
diff --git a/devtools/client/inspector/shared/utils.js b/devtools/client/inspector/shared/utils.js
index 542f9897b1..655f143978 100644
--- a/devtools/client/inspector/shared/utils.js
+++ b/devtools/client/inspector/shared/utils.js
@@ -12,7 +12,7 @@ loader.lazyRequireGetter(
);
loader.lazyRequireGetter(
this,
- "getCSSLexer",
+ "InspectorCSSParserWrapper",
"resource://devtools/shared/css/lexer.js",
true
);
@@ -51,11 +51,11 @@ function advanceValidate(keyCode, value, insertionPoint) {
// value. Otherwise it's been inserted in some spot where it has a
// valid meaning, like a comment or string.
value = value.slice(0, insertionPoint) + ";" + value.slice(insertionPoint);
- const lexer = getCSSLexer(value);
+ const lexer = new InspectorCSSParserWrapper(value);
while (true) {
const token = lexer.nextToken();
if (token.endOffset > insertionPoint) {
- if (token.tokenType === "symbol" && token.text === ";") {
+ if (token.tokenType === "Semicolon") {
// The ";" is a terminator.
return true;
}
diff --git a/devtools/client/inspector/test/browser_inspector_picker-shift-key.js b/devtools/client/inspector/test/browser_inspector_picker-shift-key.js
index 2eb1c04709..db1d5510e7 100644
--- a/devtools/client/inspector/test/browser_inspector_picker-shift-key.js
+++ b/devtools/client/inspector/test/browser_inspector_picker-shift-key.js
@@ -13,12 +13,18 @@ const TEST_URI = `data:text/html;charset=utf-8,
<div id="nopointer" style="pointer-events: none">Element with pointer-events: none</div>
<div id="transluscent" style="pointer-events: none;opacity: 0.1">Element with opacity of 0.1</div>
<div id="invisible" style="pointer-events: none;opacity: 0">Element with opacity of 0</div>
+ <div>
+ <header>Hello</header>
+ <div style="z-index:-1;position:relative;">
+ <span id="negative-z-index-child">ZZ</span>
+ </div>
+ </div>
</main>`;
const IS_OSX = Services.appinfo.OS === "Darwin";
add_task(async function () {
- const { inspector, toolbox } = await openInspectorForURL(TEST_URI);
-
+ const { inspector, toolbox, highlighterTestFront } =
+ await openInspectorForURL(TEST_URI);
const body = await getNodeFront("body", inspector);
is(
inspector.selection.nodeFront,
@@ -72,6 +78,31 @@ add_task(async function () {
shiftKey: true,
});
await checkElementSelected("main", inspector);
+
+ info("Shift-clicking element with negative z-index parent works");
+ await hoverElement(
+ inspector,
+ "#negative-z-index-child",
+ undefined,
+ undefined,
+ {
+ shiftKey: true,
+ }
+ );
+ is(
+ await highlighterTestFront.getHighlighterNodeTextContent(
+ "box-model-infobar-id"
+ ),
+ "#negative-z-index-child",
+ "The highlighter is shown on #negative-z-index-child"
+ );
+
+ await clickElement({
+ inspector,
+ selector: "#negative-z-index-child",
+ shiftKey: true,
+ });
+ await checkElementSelected("#negative-z-index-child", inspector);
});
async function clickElement({ selector, inspector, shiftKey }) {
diff --git a/devtools/client/inspector/test/head.js b/devtools/client/inspector/test/head.js
index cb1b00030c..73c8902c6b 100644
--- a/devtools/client/inspector/test/head.js
+++ b/devtools/client/inspector/test/head.js
@@ -131,10 +131,12 @@ function pickElement(inspector, selector, x, y) {
* X-offset from the top-left corner of the element matching the provided selector
* @param {Number} y
* Y-offset from the top-left corner of the element matching the provided selector
+ * @param {Object} eventOptions
+ * Options that will be passed to synthesizeMouse
* @return {Promise} promise that resolves when both the "picker-node-hovered" and
* "highlighter-shown" events are emitted.
*/
-async function hoverElement(inspector, selector, x, y) {
+async function hoverElement(inspector, selector, x, y, eventOptions = {}) {
const { waitForHighlighterTypeShown } = getHighlighterTestHelpers(inspector);
info(`Waiting for element "${selector}" to be hovered`);
const onHovered = inspector.toolbox.nodePicker.once("picker-node-hovered");
@@ -159,7 +161,7 @@ async function hoverElement(inspector, selector, x, y) {
if (isNaN(x) || isNaN(y)) {
BrowserTestUtils.synthesizeMouseAtCenter(
selector,
- { type: "mousemove" },
+ { ...eventOptions, type: "mousemove" },
browsingContext
);
} else {
@@ -167,7 +169,7 @@ async function hoverElement(inspector, selector, x, y) {
selector,
x,
y,
- { type: "mousemove" },
+ { ...eventOptions, type: "mousemove" },
browsingContext
);
}
diff --git a/devtools/client/netmonitor/test/browser.toml b/devtools/client/netmonitor/test/browser.toml
index 186810de06..52f465ab57 100644
--- a/devtools/client/netmonitor/test/browser.toml
+++ b/devtools/client/netmonitor/test/browser.toml
@@ -12,6 +12,7 @@ support-files = [
"html_cause-test-page.html",
"html_content-type-without-cache-test-page.html",
"html_brotli-test-page.html",
+ "html_zstd-test-page.html",
"html_image-tooltip-test-page.html",
"html_cache-test-page.html",
"html_cors-test-page.html",
@@ -49,6 +50,7 @@ support-files = [
"html_single-get-page.html",
"html_slow-requests-test-page.html",
"html_send-beacon.html",
+ "html_send-beacon-late-iframe-request.html",
"html_sorting-test-page.html",
"html_statistics-edge-case-page.html",
"html_statistics-test-page.html",
@@ -557,3 +559,5 @@ fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and
["browser_net_ws-sse-persist-columns.js"]
["browser_net_ws-stomp-payload.js"]
+
+["browser_net_zstd.js"]
diff --git a/devtools/client/netmonitor/test/browser_net_clear.js b/devtools/client/netmonitor/test/browser_net_clear.js
index 5a97de3e02..e831d07e36 100644
--- a/devtools/client/netmonitor/test/browser_net_clear.js
+++ b/devtools/client/netmonitor/test/browser_net_clear.js
@@ -15,7 +15,7 @@ Services.scriptloader.loadSubScript(
add_task(async function () {
Services.prefs.setBoolPref("devtools.webconsole.filter.net", true);
- const { monitor, toolbox } = await initNetMonitor(SIMPLE_URL, {
+ const { monitor, toolbox } = await initNetMonitor(HTTPS_SIMPLE_URL, {
requestCount: 1,
});
info("Starting test... ");
@@ -43,7 +43,7 @@ add_task(async function () {
await onWebConsole;
info("Wait for request");
- await waitFor(() => findMessageByType(hud, SIMPLE_URL, ".network"));
+ await waitFor(() => findMessageByType(hud, HTTPS_SIMPLE_URL, ".network"));
info("Switch back the the netmonitor");
await monitor.toolbox.selectTool("netmonitor");
@@ -63,7 +63,9 @@ add_task(async function () {
info("Wait for network request to show and that its disabled");
- await waitFor(() => findMessageByType(hud, SIMPLE_URL, ".network.disabled"));
+ await waitFor(() =>
+ findMessageByType(hud, HTTPS_SIMPLE_URL, ".network.disabled")
+ );
// Switch back to the netmonitor.
await monitor.toolbox.selectTool("netmonitor");
diff --git a/devtools/client/netmonitor/test/browser_net_column_slow-request-indicator.js b/devtools/client/netmonitor/test/browser_net_column_slow-request-indicator.js
index 368873c3c6..34dd64ac38 100644
--- a/devtools/client/netmonitor/test/browser_net_column_slow-request-indicator.js
+++ b/devtools/client/netmonitor/test/browser_net_column_slow-request-indicator.js
@@ -12,6 +12,8 @@ add_task(async function () {
// definately above the slow threshold set here.
const SLOW_THRESHOLD = 450;
+ await pushPref("dom.security.https_first", false);
+
Services.prefs.setIntPref("devtools.netmonitor.audits.slow", SLOW_THRESHOLD);
const { monitor } = await initNetMonitor(SLOW_REQUESTS_URL, {
diff --git a/devtools/client/netmonitor/test/browser_net_send-beacon.js b/devtools/client/netmonitor/test/browser_net_send-beacon.js
index d47299ca7f..9d1f078bf5 100644
--- a/devtools/client/netmonitor/test/browser_net_send-beacon.js
+++ b/devtools/client/netmonitor/test/browser_net_send-beacon.js
@@ -7,6 +7,8 @@
* Tests if beacons are handled correctly.
*/
+const IFRAME_URL = EXAMPLE_URL + "html_send-beacon-late-iframe-request.html";
+
add_task(async function () {
const { tab, monitor } = await initNetMonitor(SEND_BEACON_URL, {
requestCount: 1,
@@ -40,5 +42,26 @@ add_task(async function () {
is(request.status, "404", "The status is correct.");
is(request.blockedReason, 0, "The request is not blocked");
+ const onNetworkEvents = waitForNetworkEvents(monitor, 2);
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [IFRAME_URL],
+ async function (url) {
+ const iframe = content.document.createElement("iframe");
+ iframe.src = url;
+ content.document.body.appendChild(iframe);
+ await new Promise(resolve => (iframe.onload = resolve));
+ iframe.remove();
+ }
+ );
+ await onNetworkEvents;
+
+ // Request at index 1 will be the HTML page of the iframe
+ const lateRequest = getSortedRequests(store.getState())[2];
+ is(lateRequest.method, "POST", "The method is correct.");
+ ok(lateRequest.url.endsWith("beacon_late_request"), "The URL is correct.");
+ is(lateRequest.status, "404", "The status is correct.");
+ is(lateRequest.blockedReason, 0, "The request is not blocked");
+
return teardown(monitor);
});
diff --git a/devtools/client/netmonitor/test/browser_net_zstd.js b/devtools/client/netmonitor/test/browser_net_zstd.js
new file mode 100644
index 0000000000..07b6f3a89f
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_zstd.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const ZSTD_URL = HTTPS_EXAMPLE_URL + "html_zstd-test-page.html";
+const ZSTD_REQUESTS = 1;
+
+/**
+ * Test zstd encoded response is handled correctly on HTTPS.
+ */
+
+add_task(async function () {
+ const {
+ L10N,
+ } = require("resource://devtools/client/netmonitor/src/utils/l10n.js");
+
+ const { tab, monitor } = await initNetMonitor(ZSTD_URL, {
+ requestCount: 1,
+ });
+ info("Starting test... ");
+
+ const { document, store, windowRequire } = monitor.panelWin;
+ const Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
+ const { getDisplayedRequests, getSortedRequests } = windowRequire(
+ "devtools/client/netmonitor/src/selectors/index"
+ );
+
+ store.dispatch(Actions.batchEnable(false));
+
+ // Execute requests.
+ await performRequests(monitor, tab, ZSTD_REQUESTS);
+
+ const requestItem = document.querySelector(".request-list-item");
+ // Status code title is generated on hover
+ const requestsListStatus = requestItem.querySelector(".status-code");
+ EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus);
+ await waitUntil(() => requestsListStatus.title);
+ await waitForDOMIfNeeded(requestItem, ".requests-list-timings-total");
+
+ verifyRequestItemTarget(
+ document,
+ getDisplayedRequests(store.getState()),
+ getSortedRequests(store.getState())[0],
+ "GET",
+ HTTPS_CONTENT_TYPE_SJS + "?fmt=zstd",
+ {
+ status: 200,
+ statusText: "OK",
+ type: "json",
+ fullMimeType: "text/json",
+ transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 261),
+ size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 64),
+ time: true,
+ }
+ );
+
+ const wait = waitForDOM(document, ".CodeMirror-code");
+ const onResponseContent = monitor.panelWin.api.once(
+ TEST_EVENTS.RECEIVED_RESPONSE_CONTENT
+ );
+ store.dispatch(Actions.toggleNetworkDetails());
+ clickOnSidebarTab(document, "response");
+ await wait;
+ await onResponseContent;
+ await testResponse("zstd");
+ await teardown(monitor);
+
+ function testResponse(type) {
+ switch (type) {
+ case "zstd": {
+ is(
+ getCodeMirrorValue(monitor),
+ "X".repeat(64),
+ "The text shown in the source editor is incorrect for the zstd request."
+ );
+ break;
+ }
+ }
+ }
+});
diff --git a/devtools/client/netmonitor/test/html_send-beacon-late-iframe-request.html b/devtools/client/netmonitor/test/html_send-beacon-late-iframe-request.html
new file mode 100644
index 0000000000..3d567bd5d9
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_send-beacon-late-iframe-request.html
@@ -0,0 +1,25 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Do a late beacon request on iframe removal</p>
+
+ <script type="text/javascript">
+ "use strict";
+
+ window.onunload = () => {
+ navigator.sendBeacon("beacon_late_request");
+ };
+ </script>
+ </body>
+</html>
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 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <!doctype html>
+
+ <html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Zstd test</p>
+
+ <script type="text/javascript">
+ /* exported performRequests */
+ "use strict";
+
+ function get(address) {
+ return new Promise(resolve => {
+ const xhr = new XMLHttpRequest();
+ xhr.open("GET", address, true);
+
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ resolve();
+ }
+ };
+ xhr.send(null);
+ });
+ }
+
+ async function performRequests() {
+ await get("sjs_content-type-test-server.sjs?fmt=zstd");
+ }
+ </script>
+ </body>
+
+ </html>
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
@@ -415,6 +415,13 @@ const featureDescriptions = [
recommended: true,
},
{
+ name: "Memory Tracking",
+ value: "memory",
+ title:
+ "Track the memory allocations and deallocations per process over time.",
+ recommended: true,
+ },
+ {
name: "Java",
value: "java",
title: "Profile Java code",
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 <entries>
- // 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 =
+ /^(?<leader>url\([ \t\r\n\f]*(["']?))(?<body>.*?)(?<trailer>\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<String>} 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<ViewPlugin>} 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
@@ -907,6 +971,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
*/
@@ -1860,6 +1979,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
* the first argument. Context is a {ed, cm} object where
@@ -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) {
")</span>" +
"</span>",
},
+ {
+ text: "rgb(var(--not-seen), 0, 0)",
+ variables: {},
+ expected:
+ // prettier-ignore
+ `rgb(` +
+ `<span>` +
+ `var(` +
+ `<span class="unmatched-class" data-variable="--not-seen is not set">` +
+ `--not-seen` +
+ `</span>` +
+ `)` +
+ `</span>` +
+ `, 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 @@
<!-- 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/. -->
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="context-fill" fill-opacity="context-fill-opacity">
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="context-fill light-dark(black, white)" fill-opacity="context-fill-opacity">
<path d="M8 0A8 8 0 0 0 .78 11.43a1 1 0 1 0 1.8-.86 5.94 5.94 0 0 1 0-5.17 6 6 0 0 1 10.83 5.17 1 1 0 1 0 1.81.86A7.99 7.99 0 0 0 8 0M10 12a2 2 0 1 1-4 0 2 2 0 0 1 4 0M10.96 6.7a.5.5 0 1 0-.92-.4l-1.7 3.73a2 2 0 0 1 .92.41l1.7-3.73z"/>
</svg>
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,<!DOCTYPE html>";
+
+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 (?<filePath>.*Screen Shot (?<renderedDate>\d{4}-\d{2}-\d{2}) at \d{2}.\d{2}.\d{2}\.png)/.exec(