From c4524619ee9f0ca50bda5c73505f08ce19085119 Mon Sep 17 00:00:00 2001 From: David Walsh Date: Wed, 20 Mar 2019 19:59:20 -0500 Subject: [PATCH] Sync 130 from MC --- assets/panel/debugger.properties | 37 ++-- assets/panel/panel.js | 11 +- packages/devtools-components/package.json | 2 +- packages/devtools-components/src/tree.css | 6 +- packages/devtools-reps/package.json | 2 +- .../src/launchpad/components/Console.css | 6 +- .../components/ObjectInspector.css | 2 +- packages/devtools-reps/src/reps/reps.css | 2 +- packages/devtools-source-map/src/index.js | 6 + .../devtools-source-map/src/source-map.js | 17 +- packages/devtools-source-map/src/worker.js | 2 + packages/devtools-splitter/src/SplitBox.css | 2 - postcss.config.js | 1 + src/actions/ast.js | 11 +- src/actions/breakpoints/addBreakpoint.js | 4 +- .../breakpoints/breakpointPositions.js | 25 ++- src/actions/breakpoints/index.js | 4 +- src/actions/breakpoints/syncBreakpoint.js | 25 ++- src/actions/expressions.js | 22 ++- src/actions/pause/breakOnNext.js | 2 +- src/actions/pause/commands.js | 74 ++++--- src/actions/pause/continueToHere.js | 4 +- src/actions/pause/fetchScopes.js | 13 +- src/actions/pause/mapFrames.js | 18 +- src/actions/pause/mapScopes.js | 18 +- src/actions/pause/pauseOnExceptions.js | 8 +- src/actions/pause/paused.js | 10 +- src/actions/pause/resumed.js | 7 +- src/actions/pause/selectFrame.js | 5 +- src/actions/pause/setPopupObjectProperties.js | 6 +- src/actions/pause/tests/pause.spec.js | 41 ++-- src/actions/preview.js | 8 +- src/actions/sources/loadSourceText.js | 138 ++++++++----- src/actions/sources/newSources.js | 15 +- src/actions/sources/prettyPrint.js | 56 +++--- src/actions/sources/tests/loadSource.spec.js | 20 +- src/actions/sources/tests/newSources.spec.js | 45 ++--- .../sources/tests/querystrings.spec.js | 38 ++++ src/actions/sources/tests/select.spec.js | 2 + src/actions/tests/ast.spec.js | 10 +- src/actions/tests/pending-breakpoints.spec.js | 6 +- src/actions/tests/project-text-search.spec.js | 3 +- src/actions/types/BreakpointAction.js | 3 +- src/actions/types/PauseAction.js | 1 - src/actions/types/SourceAction.js | 3 +- src/actions/types/index.js | 22 +-- src/client/firefox/commands.js | 12 +- src/client/firefox/types.js | 8 +- src/client/index.js | 15 +- src/components/App.js | 2 - src/components/Editor/Breakpoints.css | 154 +++++++++++++++ src/components/Editor/ColumnBreakpoint.js | 3 +- src/components/Editor/ColumnBreakpoints.js | 1 - src/components/Editor/DebugLine.js | 5 +- src/components/Editor/Editor.css | 91 +-------- src/components/Editor/EditorMenu.js | 8 +- src/components/Editor/Footer.css | 3 +- src/components/Editor/Footer.js | 68 +++---- src/components/Editor/HighlightLine.js | 5 +- src/components/Editor/Preview/Popup.js | 10 +- src/components/Editor/Preview/index.js | 9 +- src/components/Editor/SearchBar.css | 2 +- src/components/Editor/Tabs.js | 5 +- src/components/Editor/index.js | 71 ++++--- src/components/Editor/menus/breakpoints.js | 8 +- .../tests/__snapshots__/Editor.spec.js.snap | 2 +- .../tests/__snapshots__/Footer.spec.js.snap | 12 +- src/components/PrimaryPanes/Sources.css | 3 +- src/components/PrimaryPanes/SourcesTree.js | 10 +- .../PrimaryPanes/tests/SourcesTree.spec.js | 8 - .../__snapshots__/SourcesTree.spec.js.snap | 26 --- src/components/QuickOpenModal.js | 2 +- .../SecondaryPanes/Breakpoints/Breakpoint.js | 5 +- .../Breakpoints/Breakpoints.css | 99 ++++------ .../Breakpoints/BreakpointsContextMenu.js | 10 +- .../SecondaryPanes/Breakpoints/index.js | 62 +++--- .../__snapshots__/Breakpoint.spec.js.snap | 10 +- src/components/SecondaryPanes/CommandBar.js | 9 +- .../SecondaryPanes/EventListeners.css | 64 +++--- src/components/SecondaryPanes/Expressions.css | 20 +- src/components/SecondaryPanes/Frames/index.js | 7 +- src/components/SecondaryPanes/Scopes.css | 33 +--- src/components/SecondaryPanes/Scopes.js | 69 ++----- .../SecondaryPanes/SecondaryPanes.css | 5 - src/components/SecondaryPanes/Worker.js | 4 +- .../SecondaryPanes/XHRBreakpoints.css | 39 ++-- src/components/SecondaryPanes/index.js | 82 ++++++-- .../tests/XHRBreakpoints.spec.js | 6 +- src/components/ShortcutsModal.js | 8 +- src/components/WelcomeBox.css | 14 +- src/components/WelcomeBox.js | 18 ++ src/components/shared/Accordion.css | 2 +- src/components/shared/SearchInput.css | 17 +- src/components/test/QuickOpenModal.spec.js | 2 +- .../__snapshots__/ShortcutsModal.spec.js.snap | 15 +- .../__snapshots__/WelcomeBox.spec.js.snap | 6 + src/reducers/ast.js | 17 -- src/reducers/breakpoints.js | 39 +++- src/reducers/expressions.js | 20 -- src/reducers/pause.js | 186 +++++++++--------- src/reducers/pending-breakpoints.js | 9 +- src/reducers/sources.js | 84 ++++---- src/selectors/breakpointSources.js | 8 +- src/selectors/breakpoints.js | 6 + src/selectors/getCallStackFrames.js | 4 +- src/selectors/inComponent.js | 5 +- src/selectors/index.js | 6 +- src/selectors/isSelectedFrameVisible.js | 6 +- src/selectors/pause.js | 57 ++++++ src/selectors/visibleColumnBreakpoints.js | 25 +-- src/utils/breakpoint/breakpointPositions.js | 13 +- src/utils/breakpoint/index.js | 2 +- src/utils/dbg.js | 26 ++- src/utils/editor/index.js | 18 +- src/utils/editor/tests/editor.spec.js | 24 +++ src/utils/empty-lines.js | 30 +++ src/utils/prefs.js | 20 +- src/utils/source-maps.js | 15 +- src/utils/source.js | 5 - src/utils/test-head.js | 32 ++- src/utils/tests/empty-lines.spec.js | 33 ++++ src/workers/parser/getSymbols.js | 19 +- test/mochitest/browser.ini | 6 +- .../browser_dbg-breakpoints-actions.js | 33 ++-- .../mochitest/browser_dbg-breakpoints-cond.js | 2 +- test/mochitest/browser_dbg-breakpoints.js | 20 +- test/mochitest/browser_dbg-call-stack.js | 8 +- test/mochitest/browser_dbg-console-eval.js | 16 +- test/mochitest/browser_dbg-console.js | 4 + test/mochitest/browser_dbg-debug-line.js | 2 +- test/mochitest/browser_dbg-editor-gutter.js | 16 -- .../browser_dbg-expressions-focus.js | 12 +- test/mochitest/browser_dbg-log-points.js | 29 +++ test/mochitest/browser_dbg-navigation.js | 4 +- test/mochitest/browser_dbg-pause-on-next.js | 5 +- test/mochitest/browser_dbg-pause-points.js | 6 +- .../browser_dbg-pretty-print-breakpoints.js | 21 ++ test/mochitest/browser_dbg-react-app.js | 7 +- .../browser_dbg-scroll-run-to-completion.js | 4 +- .../browser_dbg-tabs-without-urls.js | 7 + .../browser_dbg-windowless-workers.js | 12 ++ test/mochitest/examples/doc-pretty.html | 14 ++ test/mochitest/examples/pretty.js | 7 + test/mochitest/helpers.js | 99 ++++++---- 144 files changed, 1717 insertions(+), 1238 deletions(-) create mode 100644 src/actions/sources/tests/querystrings.spec.js create mode 100644 src/components/Editor/Breakpoints.css create mode 100644 src/selectors/pause.js create mode 100644 src/utils/empty-lines.js create mode 100644 src/utils/tests/empty-lines.spec.js create mode 100644 test/mochitest/browser_dbg-log-points.js create mode 100644 test/mochitest/browser_dbg-pretty-print-breakpoints.js create mode 100644 test/mochitest/examples/doc-pretty.html create mode 100644 test/mochitest/examples/pretty.js diff --git a/assets/panel/debugger.properties b/assets/panel/debugger.properties index 01559d8716..8b795d2293 100644 --- a/assets/panel/debugger.properties +++ b/assets/panel/debugger.properties @@ -243,17 +243,11 @@ functionSearch.key=CmdOrCtrl+Shift+O # key identifiers, not messages displayed to the user. toggleBreakpoint.key=CmdOrCtrl+B -# LOCALIZATION NOTE (toggleCondPanel.breakpoint.key): A key shortcut to toggle -# the conditional panel for breakpoints. +# LOCALIZATION NOTE (toggleCondPanel.key): A key shortcut to toggle +# the conditional breakpoint panel. # Do not localize "CmdOrCtrl+Shift+B", or change the format of the string. These are # key identifiers, not messages displayed to the user. -toggleCondPanel.breakpoint.key=CmdOrCtrl+Shift+B - -# LOCALIZATION NOTE (toggleCondPanel.logPoint.key): A key shortcut to toggle -# the conditional panel for log points. -# Do not localize "CmdOrCtrl+Shift+Y", or change the format of the string. These are -# key identifiers, not messages displayed to the user. -toggleCondPanel.logPoint.key=CmdOrCtrl+Shift+Y +toggleCondPanel.key=CmdOrCtrl+Shift+B # LOCALIZATION NOTE (stepOut.key): A key shortcut to # step out. @@ -697,13 +691,16 @@ scopes.notAvailable=Scopes unavailable # for when the debugger is not paused. scopes.notPaused=Not paused -# LOCALIZATION NOTE (scopes.toggleToGenerated): Link displayed in the right -# sidebar scope pane to update the view to show generated scope data. -scopes.toggleToGenerated=Show generated scope +# LOCALIZATION NOTE (scopes.mapping.label): Scopes right sidebar pane +# tooltip for checkbox and label +scopes.mapping.label=Map original variable names -# LOCALIZATION NOTE (scopes.toggleToOriginal): Link displayed in the right -# sidebar scope pane to update the view to show original scope data. -scopes.toggleToOriginal=Show original scope +# LOCALIZATION NOTE (scopes.helpTooltip.label): Scopes right sidebar pane +# icon tooltip for link to MDN +scopes.helpTooltip.label=Learn more about map scopes + +# LOCALIZATION NOTE (scopes.map.label): Checkbox label to map scopes +scopes.map.label=Map # LOCALIZATION NOTE (scopes.block): Refers to a block of code in # the scopes pane when the debugger is paused. @@ -1031,13 +1028,9 @@ anonymousFunction= shortcuts.toggleBreakpoint=Toggle Breakpoint shortcuts.toggleBreakpoint.accesskey=B -# LOCALIZATION NOTE (shortcuts.toggleCondPanel.breakpoint): text describing -# keyboard shortcut action for toggling conditional panel for breakpoints -shortcuts.toggleCondPanel.breakpoint=Edit Conditional Breakpoint - -# LOCALIZATION NOTE (shortcuts.toggleCondPanel.logPoint): text describing -# keyboard shortcut action for toggling conditional panel for log points -shortcuts.toggleCondPanel.logPoint=Edit Log Point +# LOCALIZATION NOTE (shortcuts.toggleCondPanel): text describing +# keyboard shortcut action for toggling conditional panel keyboard +shortcuts.toggleCondPanel=Toggle Conditional Panel # LOCALIZATION NOTE (shortcuts.pauseOrResume): text describing # keyboard shortcut action for pause of resume diff --git a/assets/panel/panel.js b/assets/panel/panel.js index 1367dbd2b0..34941c6bc2 100644 --- a/assets/panel/panel.js +++ b/assets/panel/panel.js @@ -100,7 +100,8 @@ DebuggerPanel.prototype = { }, getFrames: function() { - const frames = this._selectors.getFrames(this._getState()); + const thread = this._selectors.getCurrentThread(this._getState()); + const frames = this._selectors.getFrames(this._getState(), thread); // Frames is null when the debugger is not paused. if (!frames) { @@ -110,7 +111,10 @@ DebuggerPanel.prototype = { }; } - const selectedFrame = this._selectors.getSelectedFrame(this._getState()); + const selectedFrame = this._selectors.getSelectedFrame( + this._getState(), + thread + ); const selected = frames.findIndex(frame => frame.id == selectedFrame.id); frames.forEach(frame => { @@ -125,7 +129,8 @@ DebuggerPanel.prototype = { }, isPaused() { - return this._selectors.isPaused(this._getState()); + const thread = this._selectors.getCurrentThread(this._getState()); + return this._selectors.getIsPaused(this._getState(), thread); }, selectSourceURL(url, line) { diff --git a/packages/devtools-components/package.json b/packages/devtools-components/package.json index 0fef689a18..dc5b0fbe0b 100644 --- a/packages/devtools-components/package.json +++ b/packages/devtools-components/package.json @@ -26,7 +26,7 @@ "enzyme": "^3.3.0", "enzyme-adapter-react-16": "^1.1.1", "eslint": "^5.0.0", - "eslint-plugin-mozilla": "1.1.2", + "eslint-plugin-mozilla": "1.1.1", "fs-extra": "^7.0.0", "lodash": "^4.17.2" } diff --git a/packages/devtools-components/src/tree.css b/packages/devtools-components/src/tree.css index 66becfd418..fa343d0ea7 100644 --- a/packages/devtools-components/src/tree.css +++ b/packages/devtools-components/src/tree.css @@ -2,7 +2,7 @@ * 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/. */ -/* We can remove the outline since we do add our own focus style on nodes */ + /* We can remove the outline since we do add our own focus style on nodes */ .tree:focus { outline: none; } @@ -35,7 +35,7 @@ display: inline-block; width: 12px; margin-inline-start: 5px; - border-inline-start: 1px solid #a2d1ff; + border-inline-start: 1px solid #A2D1FF; flex-shrink: 0; } @@ -47,7 +47,7 @@ /* For non expandable root nodes, we don't have .tree-indent elements, so we declare the margin on the start of the node */ .tree-node[data-expandable="false"][aria-level="1"] { - padding-inline-start: 15px; + padding-inline-start: 15px } .tree .tree-node[data-expandable="true"] { diff --git a/packages/devtools-reps/package.json b/packages/devtools-reps/package.json index 979faf5dc9..19e768e089 100644 --- a/packages/devtools-reps/package.json +++ b/packages/devtools-reps/package.json @@ -46,7 +46,7 @@ "enzyme-adapter-react-16": "^1.1.1", "enzyme-to-json": "^3.3.1", "eslint": "^5.0.0", - "eslint-plugin-mozilla": "1.1.2", + "eslint-plugin-mozilla": "1.1.1", "fs-extra": "^7.0.0", "immutable": "^3.8.2", "postcss-url-mapper": "^1.2.0", diff --git a/packages/devtools-reps/src/launchpad/components/Console.css b/packages/devtools-reps/src/launchpad/components/Console.css index 7748afecd7..c163071a76 100644 --- a/packages/devtools-reps/src/launchpad/components/Console.css +++ b/packages/devtools-reps/src/launchpad/components/Console.css @@ -27,7 +27,7 @@ main { padding: 0.5rem; } -.rep-input::before { +.rep-input:before { content: "➜ "; } @@ -48,7 +48,7 @@ main { } .rep-element[data-mode]::before { - content: attr(data-mode) ":"; + content: attr(data-mode)":"; background-color: var(--theme-toolbar-background); font-family: monospace; display: inline-block; @@ -73,7 +73,7 @@ main { .packet header .copy-label { margin: 0 0.5em; padding-inline-start: 0.5em; - border-inline-start: 1px solid rgba(0, 0, 0, 0.2); + border-inline-start: 1px solid rgba(0,0,0, 0.2); } .packet header.packet-expanded::before { diff --git a/packages/devtools-reps/src/object-inspector/components/ObjectInspector.css b/packages/devtools-reps/src/object-inspector/components/ObjectInspector.css index cd312a0203..fc1fc8d71b 100644 --- a/packages/devtools-reps/src/object-inspector/components/ObjectInspector.css +++ b/packages/devtools-reps/src/object-inspector/components/ObjectInspector.css @@ -27,7 +27,7 @@ color: var(--theme-body-color); } -.tree.object-inspector .block .object-label::before { +.tree.object-inspector .block .object-label:before { content: "☲ "; font-size: 1.1em; } diff --git a/packages/devtools-reps/src/reps/reps.css b/packages/devtools-reps/src/reps/reps.css index 5d9c9a8d82..3d4d4bd7bb 100644 --- a/packages/devtools-reps/src/reps/reps.css +++ b/packages/devtools-reps/src/reps/reps.css @@ -288,7 +288,7 @@ button.invoke-getter { background-color: var(--theme-icon-color); height: 10px; vertical-align: middle; - border: none; + border:none; } .invoke-getter:hover { diff --git a/packages/devtools-source-map/src/index.js b/packages/devtools-source-map/src/index.js index eaa752fe5e..b87d2c12ba 100644 --- a/packages/devtools-source-map/src/index.js +++ b/packages/devtools-source-map/src/index.js @@ -77,6 +77,12 @@ export const getOriginalLocation = async ( options: locationOptions = {} ): Promise => _getOriginalLocation(location, options); +export const getOriginalLocations = async ( + locations: SourceLocation[], + options: locationOptions = {} +): Promise => + dispatcher.invoke("getOriginalLocations", locations, options); + export const getGeneratedRangesForOriginal = async ( sourceId: SourceId, url: string, diff --git a/packages/devtools-source-map/src/source-map.js b/packages/devtools-source-map/src/source-map.js index 2a5a4dca20..7cf098e59b 100644 --- a/packages/devtools-source-map/src/source-map.js +++ b/packages/devtools-source-map/src/source-map.js @@ -46,6 +46,10 @@ type Range = { } }; +export type locationOptions = { + search?: "LEAST_UPPER_BOUND" | "GREATEST_LOWER_BOUND" +}; + async function getOriginalURLs( generatedSource: Source ): Promise { @@ -257,9 +261,15 @@ async function getAllGeneratedLocations( })); } -export type locationOptions = { - search?: "LEAST_UPPER_BOUND" | "GREATEST_LOWER_BOUND" -}; +function getOriginalLocations( + locations: SourceLocation[], + options: locationOptions = {} +) { + return Promise.all( + locations.map(location => getOriginalLocation(location, options)) + ); +} + async function getOriginalLocation( location: SourceLocation, { search }: locationOptions = {} @@ -546,6 +556,7 @@ module.exports = { getGeneratedLocation, getAllGeneratedLocations, getOriginalLocation, + getOriginalLocations, getOriginalSourceText, getGeneratedRangesForOriginal, getFileGeneratedRange, diff --git a/packages/devtools-source-map/src/worker.js b/packages/devtools-source-map/src/worker.js index 1bd5f4b7cd..7308f8ae3e 100644 --- a/packages/devtools-source-map/src/worker.js +++ b/packages/devtools-source-map/src/worker.js @@ -11,6 +11,7 @@ const { getGeneratedLocation, getAllGeneratedLocations, getOriginalLocation, + getOriginalLocations, getOriginalSourceText, getGeneratedRangesForOriginal, getFileGeneratedRange, @@ -37,6 +38,7 @@ self.onmessage = workerHandler({ getGeneratedLocation, getAllGeneratedLocations, getOriginalLocation, + getOriginalLocations, getOriginalSourceText, getOriginalStackFrames, getGeneratedRangesForOriginal, diff --git a/packages/devtools-splitter/src/SplitBox.css b/packages/devtools-splitter/src/SplitBox.css index 42c42d257b..ecfbbe1b26 100644 --- a/packages/devtools-splitter/src/SplitBox.css +++ b/packages/devtools-splitter/src/SplitBox.css @@ -48,7 +48,6 @@ } .split-box.vert > .splitter { - /* prettier-ignore */ min-width: calc(var(--devtools-splitter-inline-start-width) + var(--devtools-splitter-inline-end-width) + 1px); @@ -62,7 +61,6 @@ } .split-box.horz > .splitter { - /* prettier-ignore */ min-height: calc(var(--devtools-splitter-top-width) + var(--devtools-splitter-bottom-width) + 1px); border-top-width: var(--devtools-splitter-top-width); diff --git a/postcss.config.js b/postcss.config.js index 95b4bab2e5..581fa131ce 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -34,6 +34,7 @@ module.exports = ({ file, options, env }) => { return { plugins: [ + require("postcss-bidirection"), require("autoprefixer")({ browsers: ["last 2 Firefox versions", "last 2 Chrome versions"], flexbox: false, diff --git a/src/actions/ast.js b/src/actions/ast.js index a3a7e8c17e..01d42754fa 100644 --- a/src/actions/ast.js +++ b/src/actions/ast.js @@ -7,9 +7,9 @@ import { getSource, getSourceFromId, + getSourceThreads, getSymbols, - getSelectedLocation, - isPaused + getSelectedLocation } from "../selectors"; import { mapFrames } from "./pause"; @@ -64,9 +64,8 @@ export function setSymbols(sourceId: SourceId) { [PROMISE]: parser.getSymbols(sourceId) }); - if (isPaused(getState())) { - await dispatch(mapFrames()); - } + const threads = getSourceThreads(getState(), source); + await Promise.all(threads.map(thread => dispatch(mapFrames(thread)))); await dispatch(setSourceMetaData(sourceId)); }; @@ -86,7 +85,7 @@ export function setOutOfScopeLocations() { } let locations = null; - if (location.line && source && !source.isWasm && isPaused(getState())) { + if (location.line && source && !source.isWasm) { locations = await parser.findOutOfScopeLocations( source.id, ((location: any): parser.AstPosition) diff --git a/src/actions/breakpoints/addBreakpoint.js b/src/actions/breakpoints/addBreakpoint.js index cf6a92cd71..41f6ecb63a 100644 --- a/src/actions/breakpoints/addBreakpoint.js +++ b/src/actions/breakpoints/addBreakpoint.js @@ -16,7 +16,7 @@ import { import { PROMISE } from "../utils/middleware/promise"; import { getSymbols, - getFirstVisibleBreakpointPosition, + getFirstBreakpointPosition, getBreakpointPositionsForSource, getSourceFromId } from "../../selectors"; @@ -105,7 +105,7 @@ export function addBreakpoint( const { sourceId, column } = location; if (column === undefined) { - position = getFirstVisibleBreakpointPosition(getState(), location); + position = getFirstBreakpointPosition(getState(), location); } else { const positions = getBreakpointPositionsForSource(getState(), sourceId); position = findPosition(positions, location); diff --git a/src/actions/breakpoints/breakpointPositions.js b/src/actions/breakpoints/breakpointPositions.js index 9e1f9aa9fd..760fba6dd3 100644 --- a/src/actions/breakpoints/breakpointPositions.js +++ b/src/actions/breakpoints/breakpointPositions.js @@ -5,7 +5,7 @@ // @flow import { isOriginalId, originalToGeneratedId } from "devtools-source-map"; -import { uniqBy } from "lodash"; +import { uniqBy, zip } from "lodash"; import { getSource, @@ -16,7 +16,6 @@ import { import type { MappedLocation, SourceLocation } from "../../types"; import type { ThunkArgs } from "../../actions/types"; -import { getOriginalLocation } from "../../utils/source-maps"; import { makeBreakpointId } from "../../utils/breakpoint"; import typeof SourceMaps from "../../../packages/devtools-source-map/src"; @@ -26,12 +25,12 @@ async function mapLocations( generatedLocations: SourceLocation[], { sourceMaps }: { sourceMaps: SourceMaps } ) { - return Promise.all( - (generatedLocations: any).map(async (generatedLocation: SourceLocation) => { - const location = await getOriginalLocation(generatedLocation, sourceMaps); + const originalLocations = await sourceMaps.getOriginalLocations( + generatedLocations + ); - return { location, generatedLocation }; - }) + return zip(originalLocations, generatedLocations).map( + ([location, generatedLocation]) => ({ location, generatedLocation }) ); } @@ -98,7 +97,17 @@ async function _setBreakpointPositions(sourceId, thunkArgs) { let positions = convertToList(results, generatedSource); positions = await mapLocations(positions, thunkArgs); positions = filterByUniqLocation(positions); - dispatch({ type: "ADD_BREAKPOINT_POSITIONS", sourceId, positions }); + + const source = getSource(getState(), sourceId); + // NOTE: it's possible that the source was removed during a navigate + if (!source) { + return; + } + dispatch({ + type: "ADD_BREAKPOINT_POSITIONS", + source: source, + positions + }); } export function setBreakpointPositions(sourceId: string) { diff --git a/src/actions/breakpoints/index.js b/src/actions/breakpoints/index.js index 444203c789..00ba53032c 100644 --- a/src/actions/breakpoints/index.js +++ b/src/actions/breakpoints/index.js @@ -17,7 +17,8 @@ import { getSelectedSource, getBreakpointAtLocation, getConditionalPanelLocation, - getBreakpointsForSource + getBreakpointsForSource, + isEmptyLineInSource } from "../../selectors"; import { assertBreakpoint, @@ -32,7 +33,6 @@ import { import remapLocations from "./remapLocations"; import { syncBreakpoint } from "./syncBreakpoint"; import { closeConditionalPanel } from "../ui"; -import { isEmptyLineInSource } from "../../reducers/ast"; // this will need to be changed so that addCLientBreakpoint is removed diff --git a/src/actions/breakpoints/syncBreakpoint.js b/src/actions/breakpoints/syncBreakpoint.js index f9cc75baf9..50f3158928 100644 --- a/src/actions/breakpoints/syncBreakpoint.js +++ b/src/actions/breakpoints/syncBreakpoint.js @@ -18,7 +18,8 @@ import { getTextAtPosition } from "../../utils/source"; import { comparePosition } from "../../utils/location"; import { originalToGeneratedId, isOriginalId } from "devtools-source-map"; -import { getSource } from "../../selectors"; +import { getSource, getBreakpointsList } from "../../selectors"; +import { removeBreakpoint } from "."; import type { ThunkArgs, Action } from "../types"; @@ -87,6 +88,19 @@ function createSyncData( return { breakpoint, previousLocation }; } +// Look for an existing breakpoint at the specified generated location. +function findExistingBreakpoint(state, generatedLocation) { + const breakpoints = getBreakpointsList(state); + + return breakpoints.find(bp => { + return ( + bp.generatedLocation.sourceUrl == generatedLocation.sourceUrl && + bp.generatedLocation.line == generatedLocation.line && + bp.generatedLocation.column == generatedLocation.column + ); + }); +} + // we have three forms of syncing: disabled syncing, existing server syncing // and adding a new breakpoint export async function syncBreakpointPromise( @@ -94,7 +108,7 @@ export async function syncBreakpointPromise( sourceId: SourceId, pendingBreakpoint: PendingBreakpoint ): Promise { - const { getState, client } = thunkArgs; + const { getState, client, dispatch } = thunkArgs; assertPendingBreakpoint(pendingBreakpoint); const source = getSource(getState(), sourceId); @@ -152,8 +166,11 @@ export async function syncBreakpointPromise( ); } - // clear server breakpoints if they exist and we have moved - await client.removeBreakpoint(generatedLocation); + // Clear any breakpoint for the generated location. + const bp = findExistingBreakpoint(getState(), generatedLocation); + if (bp) { + await dispatch(removeBreakpoint(bp)); + } if (!newGeneratedLocation) { return { previousLocation, breakpoint: null }; diff --git a/src/actions/expressions.js b/src/actions/expressions.js index e1e4b094b0..396545d158 100644 --- a/src/actions/expressions.js +++ b/src/actions/expressions.js @@ -5,7 +5,6 @@ // @flow import { - getCurrentThread, getExpression, getExpressions, getSelectedFrame, @@ -14,7 +13,8 @@ import { getSelectedSource, getSelectedScopeMappings, getSelectedFrameBindings, - isPaused + getCurrentThread, + getIsPaused } from "../selectors"; import { PROMISE } from "./utils/middleware/promise"; import { wrapExpression } from "../utils/expressions"; @@ -60,7 +60,8 @@ export function autocomplete(input: string, cursor: number) { if (!input) { return; } - const frameId = getSelectedFrameId(getState()); + const thread = getCurrentThread(getState()); + const frameId = getSelectedFrameId(getState(), thread); const result = await client.autocomplete(input, cursor, frameId); await dispatch({ type: "AUTOCOMPLETE", input, result }); }; @@ -118,8 +119,8 @@ export function evaluateExpressions() { return async function({ dispatch, getState, client }: ThunkArgs) { const expressions = getExpressions(getState()).toJS(); const inputs = expressions.map(({ input }) => input); - const frameId = getSelectedFrameId(getState()); const thread = getCurrentThread(getState()); + const frameId = getSelectedFrameId(getState(), thread); const results = await client.evaluateExpressions(inputs, { frameId, thread @@ -136,7 +137,8 @@ function evaluateExpression(expression: Expression) { } let input = expression.input; - const frame = getSelectedFrame(getState()); + const thread = getCurrentThread(getState()); + const frame = getSelectedFrame(getState(), thread); if (frame) { const { location } = frame; @@ -152,8 +154,7 @@ function evaluateExpression(expression: Expression) { } } - const frameId = getSelectedFrameId(getState()); - const thread = getCurrentThread(getState()); + const frameId = getSelectedFrameId(getState(), thread); return dispatch({ type: "EVALUATE_EXPRESSION", @@ -174,8 +175,9 @@ function evaluateExpression(expression: Expression) { export function getMappedExpression(expression: string) { return async function({ dispatch, getState, client, sourceMaps }: ThunkArgs) { const state = getState(); - const mappings = getSelectedScopeMappings(state); - const bindings = getSelectedFrameBindings(state); + const thread = getCurrentThread(getState()); + const mappings = getSelectedScopeMappings(state, thread); + const bindings = getSelectedFrameBindings(state, thread); // We bail early if we do not need to map the expression. This is important // because mapping an expression can be slow if the parser worker is @@ -192,7 +194,7 @@ export function getMappedExpression(expression: string) { expression, mappings, bindings || [], - features.mapExpressionBindings && isPaused(state), + features.mapExpressionBindings && getIsPaused(state, thread), features.mapAwaitExpression ); }; diff --git a/src/actions/pause/breakOnNext.js b/src/actions/pause/breakOnNext.js index 70b4a7c328..8f5bcbefff 100644 --- a/src/actions/pause/breakOnNext.js +++ b/src/actions/pause/breakOnNext.js @@ -4,8 +4,8 @@ // @flow -import type { ThunkArgs } from "../types"; import { getCurrentThread } from "../../selectors"; +import type { ThunkArgs } from "../types"; /** * Debugger breakOnNext command. diff --git a/src/actions/pause/commands.js b/src/actions/pause/commands.js index 38bd3ff5ed..8971eb5549 100644 --- a/src/actions/pause/commands.js +++ b/src/actions/pause/commands.js @@ -6,23 +6,34 @@ // @flow import { - isPaused, + getIsPaused, getCurrentThread, getSource, - getTopFrame + getTopFrame, + getSelectedFrame } from "../../selectors"; import { PROMISE } from "../utils/middleware/promise"; import { getNextStep } from "../../workers/parser"; import { addHiddenBreakpoint } from "../breakpoints"; +import { evaluateExpressions } from "../expressions"; +import { selectLocation } from "../sources"; import { features } from "../../utils/prefs"; import { recordEvent } from "../../utils/telemetry"; -import type { Source } from "../../types"; +import type { Source, ThreadId } from "../../types"; import type { ThunkArgs } from "../types"; import type { Command } from "../../reducers/types"; -export function selectThread(thread: string) { - return { type: "SELECT_THREAD", thread }; +export function selectThread(thread: ThreadId) { + return async ({ dispatch, getState, client }: ThunkArgs) => { + await dispatch({ type: "SELECT_THREAD", thread }); + dispatch(evaluateExpressions()); + + const frame = getSelectedFrame(getState(), thread); + if (frame) { + dispatch(selectLocation(frame.location)); + } + }; } /** @@ -32,10 +43,9 @@ export function selectThread(thread: string) { * @memberof actions/pause * @static */ -export function command(type: Command) { +export function command(thread: ThreadId, type: Command) { return async ({ dispatch, getState, client }: ThunkArgs) => { if (type) { - const thread = getCurrentThread(getState()); return dispatch({ type: "COMMAND", command: type, @@ -54,8 +64,9 @@ export function command(type: Command) { */ export function stepIn() { return ({ dispatch, getState }: ThunkArgs) => { - if (isPaused(getState())) { - return dispatch(command("stepIn")); + const thread = getCurrentThread(getState()); + if (getIsPaused(getState(), thread)) { + return dispatch(command(thread, "stepIn")); } }; } @@ -68,8 +79,9 @@ export function stepIn() { */ export function stepOver() { return ({ dispatch, getState }: ThunkArgs) => { - if (isPaused(getState())) { - return dispatch(astCommand("stepOver")); + const thread = getCurrentThread(getState()); + if (getIsPaused(getState(), thread)) { + return dispatch(astCommand(thread, "stepOver")); } }; } @@ -82,8 +94,9 @@ export function stepOver() { */ export function stepOut() { return ({ dispatch, getState }: ThunkArgs) => { - if (isPaused(getState())) { - return dispatch(command("stepOut")); + const thread = getCurrentThread(getState()); + if (getIsPaused(getState(), thread)) { + return dispatch(command(thread, "stepOut")); } }; } @@ -96,9 +109,10 @@ export function stepOut() { */ export function resume() { return ({ dispatch, getState }: ThunkArgs) => { - if (isPaused(getState())) { + const thread = getCurrentThread(getState()); + if (getIsPaused(getState(), thread)) { recordEvent("continue"); - return dispatch(command("resume")); + return dispatch(command(thread, "resume")); } }; } @@ -111,8 +125,9 @@ export function resume() { */ export function rewind() { return ({ dispatch, getState }: ThunkArgs) => { - if (isPaused(getState())) { - return dispatch(command("rewind")); + const thread = getCurrentThread(getState()); + if (getIsPaused(getState(), thread)) { + return dispatch(command(thread, "rewind")); } }; } @@ -125,8 +140,9 @@ export function rewind() { */ export function reverseStepIn() { return ({ dispatch, getState }: ThunkArgs) => { - if (isPaused(getState())) { - return dispatch(command("reverseStepIn")); + const thread = getCurrentThread(getState()); + if (getIsPaused(getState(), thread)) { + return dispatch(command(thread, "reverseStepIn")); } }; } @@ -139,8 +155,9 @@ export function reverseStepIn() { */ export function reverseStepOver() { return ({ dispatch, getState }: ThunkArgs) => { - if (isPaused(getState())) { - return dispatch(astCommand("reverseStepOver")); + const thread = getCurrentThread(getState()); + if (getIsPaused(getState(), thread)) { + return dispatch(astCommand(thread, "reverseStepOver")); } }; } @@ -153,8 +170,9 @@ export function reverseStepOver() { */ export function reverseStepOut() { return ({ dispatch, getState }: ThunkArgs) => { - if (isPaused(getState())) { - return dispatch(command("reverseStepOut")); + const thread = getCurrentThread(getState()); + if (getIsPaused(getState(), thread)) { + return dispatch(command(thread, "reverseStepOut")); } }; } @@ -187,26 +205,26 @@ function hasAwait(source: Source, pauseLocation) { * @param stepType * @returns {function(ThunkArgs)} */ -export function astCommand(stepType: Command) { +export function astCommand(thread: ThreadId, stepType: Command) { return async ({ dispatch, getState, sourceMaps }: ThunkArgs) => { if (!features.asyncStepping) { - return dispatch(command(stepType)); + return dispatch(command(thread, stepType)); } if (stepType == "stepOver") { // This type definition is ambiguous: - const frame: any = getTopFrame(getState()); + const frame: any = getTopFrame(getState(), thread); const source = getSource(getState(), frame.location.sourceId); if (source && hasAwait(source, frame.location)) { const nextLocation = await getNextStep(source.id, frame.location); if (nextLocation) { await dispatch(addHiddenBreakpoint(nextLocation)); - return dispatch(command("resume")); + return dispatch(command(thread, "resume")); } } } - return dispatch(command(stepType)); + return dispatch(command(thread, stepType)); }; } diff --git a/src/actions/pause/continueToHere.js b/src/actions/pause/continueToHere.js index 7e8813aba1..0122726d8a 100644 --- a/src/actions/pause/continueToHere.js +++ b/src/actions/pause/continueToHere.js @@ -5,6 +5,7 @@ // @flow import { + getCurrentThread, getSelectedSource, getSelectedFrame, getCanRewind @@ -16,8 +17,9 @@ import type { ThunkArgs } from "../types"; export function continueToHere(line: number, column?: number) { return async function({ dispatch, getState }: ThunkArgs) { + const thread = getCurrentThread(getState()); const selectedSource = getSelectedSource(getState()); - const selectedFrame = getSelectedFrame(getState()); + const selectedFrame = getSelectedFrame(getState(), thread); if (!selectedFrame || !selectedSource) { return; diff --git a/src/actions/pause/fetchScopes.js b/src/actions/pause/fetchScopes.js index d5820914ae..0661c9eca4 100644 --- a/src/actions/pause/fetchScopes.js +++ b/src/actions/pause/fetchScopes.js @@ -4,25 +4,22 @@ // @flow -import { - getCurrentThread, - getSelectedFrame, - getGeneratedFrameScope -} from "../../selectors"; +import { getSelectedFrame, getGeneratedFrameScope } from "../../selectors"; import { mapScopes } from "./mapScopes"; import { PROMISE } from "../utils/middleware/promise"; +import type { ThreadId } from "../../types"; import type { ThunkArgs } from "../types"; -export function fetchScopes() { +export function fetchScopes(thread: ThreadId) { return async function({ dispatch, getState, client, sourceMaps }: ThunkArgs) { - const frame = getSelectedFrame(getState()); + const frame = getSelectedFrame(getState(), thread); if (!frame || getGeneratedFrameScope(getState(), frame.id)) { return; } const scopes = dispatch({ type: "ADD_SCOPES", - thread: getCurrentThread(getState()), + thread, frame, [PROMISE]: client.getFrameScopes(frame) }); diff --git a/src/actions/pause/mapFrames.js b/src/actions/pause/mapFrames.js index 3ef975693c..26c437a3e8 100644 --- a/src/actions/pause/mapFrames.js +++ b/src/actions/pause/mapFrames.js @@ -5,7 +5,6 @@ // @flow import { - getCurrentThread, getFrames, getSymbols, getSource, @@ -15,7 +14,7 @@ import { import assert from "../../utils/assert"; import { findClosestFunction } from "../../utils/ast"; -import type { Frame } from "../../types"; +import type { Frame, ThreadId } from "../../types"; import type { State } from "../../reducers/types"; import type { ThunkArgs } from "../types"; @@ -26,8 +25,8 @@ function isFrameBlackboxed(state, frame) { return source && source.isBlackBoxed; } -function getSelectedFrameId(state, frames) { - let selectedFrame = getSelectedFrame(state); +function getSelectedFrameId(state, thread, frames) { + let selectedFrame = getSelectedFrame(state, thread); if (selectedFrame && !isFrameBlackboxed(state, selectedFrame)) { return selectedFrame.id; } @@ -162,9 +161,9 @@ async function expandFrames( * @memberof actions/pause * @static */ -export function mapFrames() { +export function mapFrames(thread: ThreadId) { return async function({ dispatch, getState, sourceMaps }: ThunkArgs) { - const frames = getFrames(getState()); + const frames = getFrames(getState(), thread); if (!frames) { return; } @@ -173,8 +172,11 @@ export function mapFrames() { mappedFrames = await expandFrames(mappedFrames, sourceMaps, getState); mappedFrames = mapDisplayNames(mappedFrames, getState); - const thread = getCurrentThread(getState()); - const selectedFrameId = getSelectedFrameId(getState(), mappedFrames); + const selectedFrameId = getSelectedFrameId( + getState(), + thread, + mappedFrames + ); dispatch({ type: "MAP_FRAMES", thread, diff --git a/src/actions/pause/mapScopes.js b/src/actions/pause/mapScopes.js index c9f1cdeb79..31428992fc 100644 --- a/src/actions/pause/mapScopes.js +++ b/src/actions/pause/mapScopes.js @@ -5,19 +5,19 @@ // @flow import { - getCurrentThread, getSource, getMapScopes, getSelectedFrame, getSelectedGeneratedScope, - getSelectedOriginalScope + getSelectedOriginalScope, + getCurrentThread } from "../../selectors"; import { loadSourceText } from "../sources/loadSourceText"; import { PROMISE } from "../utils/middleware/promise"; import { features } from "../../utils/prefs"; import { log } from "../../utils/log"; -import { isGenerated } from "../../utils/source"; +import { isGenerated, isOriginal } from "../../utils/source"; import type { Frame, Scope } from "../../types"; import type { ThunkArgs } from "../types"; @@ -32,12 +32,13 @@ export function toggleMapScopes() { dispatch({ type: "TOGGLE_MAP_SCOPES", mapScopes: true }); - if (getSelectedOriginalScope(getState())) { + const thread = getCurrentThread(getState()); + if (getSelectedOriginalScope(getState(), thread)) { return; } - const scopes = getSelectedGeneratedScope(getState()); - const frame = getSelectedFrame(getState()); + const scopes = getSelectedGeneratedScope(getState(), thread); + const frame = getSelectedFrame(getState(), thread); if (!scopes || !frame) { return; } @@ -57,7 +58,7 @@ export function mapScopes(scopes: Promise, frame: Frame) { await dispatch({ type: "MAP_SCOPES", - thread: getCurrentThread(getState()), + thread: frame.thread, frame, [PROMISE]: (async function() { if ( @@ -72,6 +73,9 @@ export function mapScopes(scopes: Promise, frame: Frame) { } await dispatch(loadSourceText(source)); + if (isOriginal(source)) { + await dispatch(loadSourceText(generatedSource)); + } try { return await buildMappedScopes( diff --git a/src/actions/pause/pauseOnExceptions.js b/src/actions/pause/pauseOnExceptions.js index 63728a13e3..d217d5461d 100644 --- a/src/actions/pause/pauseOnExceptions.js +++ b/src/actions/pause/pauseOnExceptions.js @@ -7,7 +7,6 @@ import { PROMISE } from "../utils/middleware/promise"; import { recordEvent } from "../../utils/telemetry"; import type { ThunkArgs } from "../types"; -import { getCurrentThread } from "../../selectors"; /** * @@ -19,22 +18,17 @@ export function pauseOnExceptions( shouldPauseOnCaughtExceptions: boolean ) { return ({ dispatch, getState, client }: ThunkArgs) => { - /* eslint-disable camelcase */ recordEvent("pause_on_exceptions", { exceptions: shouldPauseOnExceptions, // There's no "n" in the key below (#1463117) - caught_exceptio: shouldPauseOnCaughtExceptions + ["caught_exceptio"]: shouldPauseOnCaughtExceptions }); - /* eslint-enable camelcase */ - const thread = getCurrentThread(getState()); return dispatch({ type: "PAUSE_ON_EXCEPTIONS", - thread, shouldPauseOnExceptions, shouldPauseOnCaughtExceptions, [PROMISE]: client.pauseOnExceptions( - thread, shouldPauseOnExceptions, shouldPauseOnCaughtExceptions ) diff --git a/src/actions/pause/paused.js b/src/actions/pause/paused.js index 575175f167..1b330edea3 100644 --- a/src/actions/pause/paused.js +++ b/src/actions/pause/paused.js @@ -47,23 +47,23 @@ export function paused(pauseInfo: Pause) { dispatch(removeBreakpoint(hiddenBreakpoint)); } - await dispatch(mapFrames()); + await dispatch(mapFrames(thread)); - const selectedFrame = getSelectedFrame(getState()); + const selectedFrame = getSelectedFrame(getState(), thread); if (selectedFrame) { await dispatch(selectLocation(selectedFrame.location)); } - if (!wasStepping(getState())) { + if (!wasStepping(getState(), thread)) { dispatch(togglePaneCollapse("end", false)); } - await dispatch(fetchScopes()); + await dispatch(fetchScopes(thread)); // Run after fetching scoping data so that it may make use of the sourcemap // expression mappings for local variables. const atException = why.type == "exception"; - if (!atException || !isEvaluatingExpression(getState())) { + if (!atException || !isEvaluatingExpression(getState(), thread)) { await dispatch(evaluateExpressions()); } }; diff --git a/src/actions/pause/resumed.js b/src/actions/pause/resumed.js index 2ab468cc5a..56e6ddbbd9 100644 --- a/src/actions/pause/resumed.js +++ b/src/actions/pause/resumed.js @@ -19,11 +19,12 @@ import type { ResumedPacket } from "../../client/firefox/types"; */ export function resumed(packet: ResumedPacket) { return async ({ dispatch, client, getState }: ThunkArgs) => { - const why = getPauseReason(getState()); + const thread = packet.from; + const why = getPauseReason(getState(), thread); const wasPausedInEval = inDebuggerEval(why); - const wasStepping = isStepping(getState()); + const wasStepping = isStepping(getState(), thread); - dispatch({ type: "RESUME", thread: packet.from, wasStepping }); + dispatch({ type: "RESUME", thread, wasStepping }); if (!wasStepping && !wasPausedInEval) { await dispatch(evaluateExpressions()); diff --git a/src/actions/pause/selectFrame.js b/src/actions/pause/selectFrame.js index 0483c6cdb6..103dd94a3e 100644 --- a/src/actions/pause/selectFrame.js +++ b/src/actions/pause/selectFrame.js @@ -7,7 +7,6 @@ import { selectLocation } from "../sources"; import { evaluateExpressions } from "../expressions"; import { fetchScopes } from "./fetchScopes"; -import { getCurrentThread } from "../../selectors"; import type { Frame } from "../../types"; import type { ThunkArgs } from "../types"; @@ -20,13 +19,13 @@ export function selectFrame(frame: Frame) { return async ({ dispatch, client, getState, sourceMaps }: ThunkArgs) => { dispatch({ type: "SELECT_FRAME", - thread: getCurrentThread(getState()), + thread: frame.thread, frame }); dispatch(selectLocation(frame.location)); dispatch(evaluateExpressions()); - dispatch(fetchScopes()); + dispatch(fetchScopes(frame.thread)); }; } diff --git a/src/actions/pause/setPopupObjectProperties.js b/src/actions/pause/setPopupObjectProperties.js index 9cffee228b..e3de7a0eeb 100644 --- a/src/actions/pause/setPopupObjectProperties.js +++ b/src/actions/pause/setPopupObjectProperties.js @@ -4,7 +4,7 @@ // @flow -import { getCurrentThread, getPopupObjectProperties } from "../../selectors"; +import { getPopupObjectProperties, getCurrentThread } from "../../selectors"; import type { ThunkArgs } from "../types"; /** @@ -14,12 +14,12 @@ import type { ThunkArgs } from "../types"; export function setPopupObjectProperties(object: any, properties: Object) { return ({ dispatch, client, getState }: ThunkArgs) => { const objectId = object.actor || object.objectId; + const thread = getCurrentThread(getState()); - if (getPopupObjectProperties(getState(), object.actor)) { + if (getPopupObjectProperties(getState(), thread, object.actor)) { return; } - const thread = getCurrentThread(getState()); dispatch({ type: "SET_POPUP_OBJECT_PROPERTIES", thread, diff --git a/src/actions/pause/tests/pause.spec.js b/src/actions/pause/tests/pause.spec.js index 0a6dd76ed4..a8f9b2c651 100644 --- a/src/actions/pause/tests/pause.spec.js +++ b/src/actions/pause/tests/pause.spec.js @@ -110,13 +110,13 @@ describe("pause", () => { await dispatch(actions.newSource(makeSource("foo1"))); await dispatch(actions.paused(mockPauseInfo)); const stepped = dispatch(actions.stepIn()); - expect(isStepping(getState())).toBeTruthy(); + expect(isStepping(getState(), "FakeThread")).toBeTruthy(); if (!stepInResolve) { throw new Error("no stepInResolve"); } await stepInResolve(); await stepped; - expect(isStepping(getState())).toBeFalsy(); + expect(isStepping(getState(), "FakeThread")).toBeFalsy(); }); it("should only step when paused", async () => { @@ -134,7 +134,7 @@ describe("pause", () => { await dispatch(actions.newSource(makeSource("foo1"))); await dispatch(actions.paused(mockPauseInfo)); dispatch(actions.stepIn()); - expect(isStepping(getState())).toBeTruthy(); + expect(isStepping(getState(), "FakeThread")).toBeTruthy(); }); it("should step over when paused", async () => { @@ -147,7 +147,7 @@ describe("pause", () => { const getNextStepSpy = jest.spyOn(parser, "getNextStep"); dispatch(actions.stepOver()); expect(getNextStepSpy).not.toBeCalled(); - expect(isStepping(getState())).toBeTruthy(); + expect(isStepping(getState(), "FakeThread")).toBeTruthy(); }); it("should step over when paused before an await", async () => { @@ -211,18 +211,19 @@ describe("pause", () => { await dispatch(actions.loadSourceText(source)); await dispatch(actions.paused(mockPauseInfo)); - expect(selectors.getFrames(getState())).toEqual([ + expect(selectors.getFrames(getState(), "FakeThread")).toEqual([ { generatedLocation: { column: 0, line: 1, sourceId: "foo" }, id: mockFrameId, location: { column: 0, line: 1, sourceId: "foo" }, scope: { bindings: { arguments: [{ a: {} }], variables: { b: {} } } - } + }, + thread: "FakeThread" } ]); - expect(selectors.getFrameScopes(getState())).toEqual({ + expect(selectors.getFrameScopes(getState(), "FakeThread")).toEqual({ generated: { "1": { pending: false, @@ -235,10 +236,9 @@ describe("pause", () => { original: { "1": { pending: false, scope: null } } }); - expect(selectors.getSelectedFrameBindings(getState())).toEqual([ - "b", - "a" - ]); + expect( + selectors.getSelectedFrameBindings(getState(), "FakeThread") + ).toEqual(["b", "a"]); }); it("maps frame locations and names to original source", async () => { @@ -256,6 +256,7 @@ describe("pause", () => { const sourceMapsMock = { getOriginalLocation: () => Promise.resolve(originalLocation), + getOriginalLocations: async items => items, getOriginalSourceText: async () => ({ source: "\n\nfunction fooOriginal() {\n return -5;\n}", contentType: "text/javascript" @@ -276,13 +277,14 @@ describe("pause", () => { await dispatch(actions.setSymbols("foo-original")); await dispatch(actions.paused(mockPauseInfo)); - expect(selectors.getFrames(getState())).toEqual([ + expect(selectors.getFrames(getState(), "FakeThread")).toEqual([ { generatedLocation: { column: 0, line: 1, sourceId: "foo" }, id: mockFrameId, location: { column: 0, line: 3, sourceId: "foo-original" }, originalDisplayName: "fooOriginal", - scope: { bindings: { arguments: [], variables: {} } } + scope: { bindings: { arguments: [], variables: {} } }, + thread: "FakeThread" } ]); }); @@ -307,17 +309,20 @@ describe("pause", () => { const originStackFrames = [ { - displayName: "fooBar" + displayName: "fooBar", + thread: "FakeThread" }, { displayName: "barZoo", - location: originalLocation2 + location: originalLocation2, + thread: "FakeThread" } ]; const sourceMapsMock = { getOriginalStackFrames: loc => Promise.resolve(originStackFrames), getOriginalLocation: () => Promise.resolve(originalLocation), + getOriginalLocations: async items => items, getOriginalSourceText: async () => ({ source: "fn fooBar() {}\nfn barZoo() { fooBar() }", contentType: "text/rust" @@ -338,7 +343,7 @@ describe("pause", () => { await dispatch(actions.loadSourceText(originalSource)); await dispatch(actions.paused(mockPauseInfo)); - expect(selectors.getFrames(getState())).toEqual([ + expect(selectors.getFrames(getState(), "FakeThread")).toEqual([ { displayName: "fooBar", generatedLocation: { column: 0, line: 1, sourceId: "foo-wasm" }, @@ -349,7 +354,7 @@ describe("pause", () => { scope: { bindings: { arguments: [], variables: {} } }, source: null, this: undefined, - thread: undefined + thread: "FakeThread" }, { displayName: "barZoo", @@ -361,7 +366,7 @@ describe("pause", () => { scope: { bindings: { arguments: [], variables: {} } }, source: null, this: undefined, - thread: undefined + thread: "FakeThread" } ]); }); diff --git a/src/actions/preview.js b/src/actions/preview.js index cbc9299851..99cfe6ec16 100644 --- a/src/actions/preview.js +++ b/src/actions/preview.js @@ -15,7 +15,8 @@ import { isSelectedFrameVisible, getSelectedSource, getSelectedFrame, - getSymbols + getSymbols, + getCurrentThread } from "../selectors"; import { getMappedExpression } from "./expressions"; @@ -86,7 +87,8 @@ export function setPreview( return; } - const selectedFrame = getSelectedFrame(getState()); + const thread = getCurrentThread(getState()); + const selectedFrame = getSelectedFrame(getState(), thread); if (location && isOriginal(source)) { const mapResult = await dispatch(getMappedExpression(expression)); @@ -101,7 +103,7 @@ export function setPreview( const { result } = await client.evaluateInFrame(expression, { frameId: selectedFrame.id, - thread: selectedFrame.thread + thread }); // Error case occurs for a token that follows an errored evaluation diff --git a/src/actions/sources/loadSourceText.js b/src/actions/sources/loadSourceText.js index f2f7f63ede..92083953f9 100644 --- a/src/actions/sources/loadSourceText.js +++ b/src/actions/sources/loadSourceText.js @@ -5,14 +5,19 @@ // @flow import { PROMISE } from "../utils/middleware/promise"; -import { getGeneratedSource, getSource } from "../../selectors"; +import { + getSource, + getGeneratedSource, + getSourcesEpoch +} from "../../selectors"; import { setBreakpointPositions } from "../breakpoints"; +import { prettyPrintSource } from "./prettyPrint"; + import * as parser from "../../workers/parser"; -import { isLoaded, isOriginal } from "../../utils/source"; +import { isLoaded, isOriginal, isPretty } from "../../utils/source"; import { Telemetry } from "devtools-modules"; -import defer from "../../utils/defer"; import type { ThunkArgs } from "../types"; import type { Source } from "../../types"; @@ -23,80 +28,105 @@ const requests = new Map(); const loadSourceHistogram = "DEVTOOLS_DEBUGGER_LOAD_SOURCE_MS"; const telemetry = new Telemetry(); -async function loadSource(state, source: Source, { sourceMaps, client }) { +async function loadSource( + state, + source: Source, + { sourceMaps, client } +): Promise { + if (isPretty(source) && isOriginal(source)) { + const generatedSource = getGeneratedSource(state, source); + return prettyPrintSource(sourceMaps, source, generatedSource); + } + if (isOriginal(source)) { - return sourceMaps.getOriginalSourceText(source); + const result = await sourceMaps.getOriginalSourceText(source); + if (!result) { + // The way we currently try to load and select a pending + // selected location, it is possible that we will try to fetch the + // original source text right after the source map has been cleared + // after a navigation event. + throw new Error("Original source text unavailable"); + } + return result; } if (!source.actors.length) { throw new Error("No source actor for loadSource"); } + telemetry.start(loadSourceHistogram, source); const response = await client.sourceContents(source.actors[0]); telemetry.finish(loadSourceHistogram, source); return { - id: source.id, text: response.source, contentType: response.contentType || "text/javascript" }; } -/** - * @memberof actions/sources - * @static - */ -export function loadSourceText(source: ?Source) { - return async ({ dispatch, getState, client, sourceMaps }: ThunkArgs) => { - if (!source) { - return; - } +async function loadSourceTextPromise( + source: Source, + epoch: number, + { dispatch, getState, client, sourceMaps }: ThunkArgs +): Promise { + if (isLoaded(source)) { + return source; + } - const id = source.id; - // Fetch the source text only once. - if (requests.has(id)) { - return requests.get(id); - } + await dispatch({ + type: "LOAD_SOURCE_TEXT", + sourceId: source.id, + epoch, + [PROMISE]: loadSource(getState(), source, { sourceMaps, client }) + }); - if (isLoaded(source)) { - return Promise.resolve(); - } + const newSource = getSource(getState(), source.id); + if (!newSource) { + return; + } - const deferred = defer(); - requests.set(id, deferred.promise); - - telemetry.start(loadSourceHistogram, source); - try { - await dispatch({ - type: "LOAD_SOURCE_TEXT", - sourceId: source.id, - [PROMISE]: loadSource(getState(), source, { sourceMaps, client }) - }); - } catch (e) { - deferred.resolve(); - requests.delete(id); - return; - } + if (!newSource.isWasm && isLoaded(newSource)) { + parser.setSource(newSource); + await dispatch(setBreakpointPositions(newSource.id)); + } - const newSource = getSource(getState(), source.id); - if (!newSource) { - return; - } + return newSource; +} - if (isOriginal(newSource) && !newSource.isWasm) { - const generatedSource = getGeneratedSource(getState(), source); - await dispatch(loadSourceText(generatedSource)); +/** + * @memberof actions/sources + * @static + */ +export function loadSourceText(inputSource: ?Source) { + return async (thunkArgs: ThunkArgs) => { + if (!inputSource) { + return; } - - if (!newSource.isWasm && isLoaded(newSource)) { - await parser.setSource(newSource); - await dispatch(setBreakpointPositions(newSource.id)); + // This ensures that the falsy check above is preserved into the IIFE + // below in a way that Flow is happy with. + const source = inputSource; + + const epoch = getSourcesEpoch(thunkArgs.getState()); + + const id = `${epoch}:${source.id}`; + let promise = requests.get(id); + if (!promise) { + promise = (async () => { + try { + return await loadSourceTextPromise(source, epoch, thunkArgs); + } catch (e) { + // TODO: This swallows errors for now. Ideally we would get rid of + // this once we have a better handle on our async state management. + } finally { + requests.delete(id); + } + })(); + requests.set(id, promise); } - // signal that the action is finished - deferred.resolve(); - requests.delete(id); - - return source; + return promise; }; } diff --git a/src/actions/sources/newSources.js b/src/actions/sources/newSources.js index 618def61d1..73dbb7bcc9 100644 --- a/src/actions/sources/newSources.js +++ b/src/actions/sources/newSources.js @@ -13,7 +13,7 @@ import { generatedToOriginalId } from "devtools-source-map"; import { flatten } from "lodash"; import { toggleBlackBox } from "./blackbox"; -import { syncBreakpoint } from "../breakpoints"; +import { syncBreakpoint, addBreakpoint } from "../breakpoints"; import { loadSourceText } from "./loadSourceText"; import { togglePrettyPrint } from "./prettyPrint"; import { selectLocation } from "../sources"; @@ -186,8 +186,19 @@ function checkPendingBreakpoints(sourceId: string) { // load the source text if there is a pending breakpoint for it await dispatch(loadSourceText(source)); + // Matching pending breakpoints could have either the same generated or the + // same original source. We expect the generated source to appear first and + // will add a breakpoint at that location initially. If the original source + // appears later then we use syncBreakpoint to see if the generated location + // changed and we need to remove the breakpoint we added earlier. await Promise.all( - pendingBreakpoints.map(bp => dispatch(syncBreakpoint(sourceId, bp))) + pendingBreakpoints.map(bp => { + if (source.url == bp.location.sourceUrl) { + return dispatch(syncBreakpoint(sourceId, bp)); + } + const { line, column } = bp.generatedLocation; + return dispatch(addBreakpoint({ sourceId, line, column }, bp.options)); + }) ); }; } diff --git a/src/actions/sources/prettyPrint.js b/src/actions/sources/prettyPrint.js index c5d2ad4012..48d5332e1e 100644 --- a/src/actions/sources/prettyPrint.js +++ b/src/actions/sources/prettyPrint.js @@ -6,11 +6,10 @@ import assert from "../../utils/assert"; import { recordEvent } from "../../utils/telemetry"; -import { remapBreakpoints, setBreakpointPositions } from "../breakpoints"; +import { remapBreakpoints } from "../breakpoints"; import { setSymbols } from "../ast"; import { prettyPrint } from "../../workers/pretty-print"; -import { setSource } from "../../workers/parser"; import { getPrettySourceURL, isLoaded } from "../../utils/source"; import { loadSourceText } from "./loadSourceText"; import { mapFrames } from "../pause"; @@ -19,13 +18,38 @@ import { selectSpecificLocation } from "../sources"; import { getSource, getSourceFromId, + getSourceThreads, getSourceByURL, getSelectedLocation } from "../../selectors"; import type { Action, ThunkArgs } from "../types"; import { selectSource } from "./select"; -import type { JsSource } from "../../types"; +import type { JsSource, Source } from "../../types"; + +export async function prettyPrintSource( + sourceMaps: any, + prettySource: Source, + generatedSource: any +) { + const url = getPrettySourceURL(generatedSource.url); + const { code, mappings } = await prettyPrint({ + source: generatedSource, + url: url + }); + await sourceMaps.applySourceMap(generatedSource.id, url, code, mappings); + + // The source map URL service used by other devtools listens to changes to + // sources based on their actor IDs, so apply the mapping there too. + for (const sourceActor of generatedSource.actors) { + await sourceMaps.applySourceMap(sourceActor.actor, url, code, mappings); + } + return { + id: prettySource.id, + text: code, + contentType: "text/javascript" + }; +} export function createPrettySource(sourceId: string) { return async ({ dispatch, getState, sourceMaps }: ThunkArgs) => { @@ -48,26 +72,7 @@ export function createPrettySource(sourceId: string) { }; dispatch(({ type: "ADD_SOURCE", source: prettySource }: Action)); - dispatch(selectSource(prettySource.id)); - - const { code, mappings } = await prettyPrint({ source, url }); - await sourceMaps.applySourceMap(source.id, url, code, mappings); - - // The source map URL service used by other devtools listens to changes to - // sources based on their actor IDs, so apply the mapping there too. - for (const sourceActor of source.actors) { - await sourceMaps.applySourceMap(sourceActor.actor, url, code, mappings); - } - - const loadedPrettySource: JsSource = { - ...prettySource, - text: code, - loadedState: "loaded" - }; - - setSource(loadedPrettySource); - dispatch(({ type: "UPDATE_SOURCE", source: loadedPrettySource }: Action)); - await dispatch(setBreakpointPositions(loadedPrettySource.id)); + await dispatch(selectSource(prettySource.id)); return prettySource; }; @@ -124,7 +129,10 @@ export function togglePrettyPrint(sourceId: string) { const newPrettySource = await dispatch(createPrettySource(sourceId)); await dispatch(remapBreakpoints(sourceId)); - await dispatch(mapFrames()); + + const threads = getSourceThreads(getState(), source); + await Promise.all(threads.map(thread => dispatch(mapFrames(thread)))); + await dispatch(setSymbols(newPrettySource.id)); dispatch( diff --git a/src/actions/sources/tests/loadSource.spec.js b/src/actions/sources/tests/loadSource.spec.js index 1bb67e9809..ff61d980a8 100644 --- a/src/actions/sources/tests/loadSource.spec.js +++ b/src/actions/sources/tests/loadSource.spec.js @@ -7,6 +7,7 @@ import { actions, selectors, + watchForState, createStore, makeSource } from "../../../utils/test-head"; @@ -117,13 +118,20 @@ describe("loadSourceText", () => { }); it("should indicate a loading source", async () => { - const { dispatch, getState } = createStore(sourceThreadClient); + const store = createStore(sourceThreadClient); + const { dispatch } = store; - // Don't block on this so we can check the loading state. - const source = makeSource("foo1"); - dispatch(actions.loadSourceText(source)); - const fooSource = selectors.getSource(getState(), "foo1"); - expect(fooSource && fooSource.loadedState).toEqual("loading"); + const source = makeSource("foo2"); + await dispatch(actions.newSource(source)); + + const wasLoading = watchForState(store, state => { + const fooSource = selectors.getSource(state, "foo2"); + return fooSource && fooSource.loadedState === "loading"; + }); + + await dispatch(actions.loadSourceText(source)); + + expect(wasLoading()).toBe(true); }); it("should indicate an errored source text", async () => { diff --git a/src/actions/sources/tests/newSources.spec.js b/src/actions/sources/tests/newSources.spec.js index d47ad02f1d..9bcd971c98 100644 --- a/src/actions/sources/tests/newSources.spec.js +++ b/src/actions/sources/tests/newSources.spec.js @@ -61,7 +61,8 @@ describe("sources - new sources", () => { threadClient, {}, { - getOriginalURLs: async () => ["magic.js"] + getOriginalURLs: async () => ["magic.js"], + getOriginalLocations: async items => items } ); @@ -74,25 +75,27 @@ describe("sources - new sources", () => { // eslint-disable-next-line it("should not attempt to fetch original sources if it's missing a source map url", async () => { const getOriginalURLs = jest.fn(); - const { dispatch } = createStore(threadClient, {}, { getOriginalURLs }); + const { dispatch } = createStore( + threadClient, + {}, + { + getOriginalURLs, + getOriginalLocations: async items => items + } + ); await dispatch(actions.newSource(makeSource("base.js"))); expect(getOriginalURLs).not.toHaveBeenCalled(); }); - it("should not fail if there isn't a source map service", async () => { - const store = createStore(threadClient, {}, null); - await store.dispatch(actions.newSource(makeSource("base.js"))); - expect(getSourceCount(store.getState())).toEqual(1); - }); - // eslint-disable-next-line it("should process new sources immediately, without waiting for source maps to be fetched first", async () => { const { dispatch, getState } = createStore( threadClient, {}, { - getOriginalURLs: async () => new Promise(_ => {}) + getOriginalURLs: async () => new Promise(_ => {}), + getOriginalLocations: async items => items } ); const baseSource = makeSource("base.js", { sourceMapURL: "base.js.map" }); @@ -116,6 +119,7 @@ describe("sources - new sources", () => { return [source.id.replace(".js", ".cljs")]; }, + getOriginalLocations: async items => items, getGeneratedLocation: location => location } ); @@ -132,27 +136,4 @@ describe("sources - new sources", () => { const bazzCljs = getSourceByURL(getState(), "bazz.cljs"); expect(bazzCljs && bazzCljs.url).toEqual("bazz.cljs"); }); - - describe("sources - sources with querystrings", () => { - it(`should find two sources when same source with - querystring`, async () => { - const { getSourcesUrlsInSources } = selectors; - const { dispatch, getState } = createStore(threadClient); - await dispatch(actions.newSource(makeSource("base.js?v=1"))); - await dispatch(actions.newSource(makeSource("base.js?v=2"))); - await dispatch(actions.newSource(makeSource("diff.js?v=1"))); - - const base1 = "http://localhost:8000/examples/base.js?v=1"; - const diff1 = "http://localhost:8000/examples/diff.js?v=1"; - const diff2 = "http://localhost:8000/examples/diff.js?v=1"; - - expect(getSourcesUrlsInSources(getState(), base1)).toHaveLength(2); - expect(getSourcesUrlsInSources(getState(), base1)).toMatchSnapshot(); - - expect(getSourcesUrlsInSources(getState(), diff1)).toHaveLength(1); - await dispatch(actions.newSource(makeSource("diff.js?v=2"))); - expect(getSourcesUrlsInSources(getState(), diff2)).toHaveLength(2); - expect(getSourcesUrlsInSources(getState(), diff1)).toHaveLength(2); - }); - }); }); diff --git a/src/actions/sources/tests/querystrings.spec.js b/src/actions/sources/tests/querystrings.spec.js new file mode 100644 index 0000000000..31290df14e --- /dev/null +++ b/src/actions/sources/tests/querystrings.spec.js @@ -0,0 +1,38 @@ +/* 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 . */ + +// @flow + +import { + actions, + selectors, + createStore, + makeSource +} from "../../../utils/test-head"; +const { getSourcesUrlsInSources } = selectors; + +// eslint-disable-next-line max-len +import { sourceThreadClient as threadClient } from "../../tests/helpers/threadClient.js"; + +describe("sources - sources with querystrings", () => { + it("should find two sources when same source with querystring", async () => { + const { dispatch, getState } = createStore(threadClient); + await dispatch(actions.newSource(makeSource("base.js?v=1"))); + await dispatch(actions.newSource(makeSource("base.js?v=2"))); + await dispatch(actions.newSource(makeSource("diff.js?v=1"))); + + expect( + getSourcesUrlsInSources( + getState(), + "http://localhost:8000/examples/base.js?v=1" + ) + ).toHaveLength(2); + expect( + getSourcesUrlsInSources( + getState(), + "http://localhost:8000/examples/diff.js?v=1" + ) + ).toHaveLength(1); + }); +}); diff --git a/src/actions/sources/tests/select.spec.js b/src/actions/sources/tests/select.spec.js index bb7b55d9f7..891bc271c4 100644 --- a/src/actions/sources/tests/select.spec.js +++ b/src/actions/sources/tests/select.spec.js @@ -229,6 +229,7 @@ describe("sources", () => { {}, { getOriginalLocation: async location => ({ ...location, line: 12 }), + getOriginalLocations: async items => items, getGeneratedLocation: async location => ({ ...location, line: 12 }), getOriginalSourceText: async () => ({ source: "" }), getGeneratedRangesForOriginal: async () => [] @@ -257,6 +258,7 @@ describe("sources", () => { {}, { getOriginalLocation: async location => ({ ...location, line: 12 }), + getOriginalLocations: async items => items, getGeneratedRangesForOriginal: async () => [], getOriginalSourceText: async () => ({ source: "" }) } diff --git a/src/actions/tests/ast.spec.js b/src/actions/tests/ast.spec.js index dff0c36b69..c57c555b02 100644 --- a/src/actions/tests/ast.spec.js +++ b/src/actions/tests/ast.spec.js @@ -45,7 +45,8 @@ const sourceMaps = { text: sourceTexts[id], contentType: "text/javascript" }), - getGeneratedRangesForOriginal: async () => [] + getGeneratedRangesForOriginal: async () => [], + getOriginalLocations: async items => items }; const sourceTexts = { @@ -176,10 +177,13 @@ describe("ast", () => { await dispatch(actions.selectSource("base.js")); const locations = getOutOfScopeLocations(getState()); - const lines = getInScopeLines(getState()); + // const lines = getInScopeLines(getState()); expect(locations).toEqual(null); - expect(lines).toEqual([1]); + + // This check is disabled as locations that are in/out of scope may not + // have completed yet when the selectSource promise finishes. + // expect(lines).toEqual([1]); }); }); }); diff --git a/src/actions/tests/pending-breakpoints.spec.js b/src/actions/tests/pending-breakpoints.spec.js index 1b1b61d446..4df44ddfee 100644 --- a/src/actions/tests/pending-breakpoints.spec.js +++ b/src/actions/tests/pending-breakpoints.spec.js @@ -68,7 +68,8 @@ function mockSourceMaps() { }), getGeneratedRangesForOriginal: async () => [ { start: { line: 0, column: 0 }, end: { line: 10, column: 10 } } - ] + ], + getOriginalLocations: async items => items }; } @@ -379,7 +380,8 @@ describe("adding sources", () => { getOriginalLocation: async location => location, getGeneratedRangesForOriginal: async () => [ { start: { line: 0, column: 0 }, end: { line: 10, column: 10 } } - ] + ], + getOriginalLocations: async items => items }); const { getState, dispatch } = store; diff --git a/src/actions/tests/project-text-search.spec.js b/src/actions/tests/project-text-search.spec.js index 3a31b1a195..7dcbc18a41 100644 --- a/src/actions/tests/project-text-search.spec.js +++ b/src/actions/tests/project-text-search.spec.js @@ -77,7 +77,8 @@ describe("project text search", () => { contentType: "text/javascript" }), getOriginalURLs: async () => [source2.url], - getGeneratedRangesForOriginal: async () => [] + getGeneratedRangesForOriginal: async () => [], + getOriginalLocations: async items => items }; const { dispatch, getState } = createStore(threadClient, {}, mockMaps); diff --git a/src/actions/types/BreakpointAction.js b/src/actions/types/BreakpointAction.js index 43e07f9ceb..ba064dd5c4 100644 --- a/src/actions/types/BreakpointAction.js +++ b/src/actions/types/BreakpointAction.js @@ -8,6 +8,7 @@ import type { Breakpoint, SourceLocation, XHRBreakpoint, + Source, BreakpointPositions } from "../../types"; @@ -95,5 +96,5 @@ export type BreakpointAction = | {| type: "ADD_BREAKPOINT_POSITIONS", positions: BreakpointPositions, - sourceId: string + source: Source |}; diff --git a/src/actions/types/PauseAction.js b/src/actions/types/PauseAction.js index d1e3889028..9eb654f155 100644 --- a/src/actions/types/PauseAction.js +++ b/src/actions/types/PauseAction.js @@ -32,7 +32,6 @@ export type PauseAction = |} | {| +type: "PAUSE_ON_EXCEPTIONS", - +thread: string, +shouldPauseOnExceptions: boolean, +shouldPauseOnCaughtExceptions: boolean |} diff --git a/src/actions/types/SourceAction.js b/src/actions/types/SourceAction.js index ff20f3c782..9704b68135 100644 --- a/src/actions/types/SourceAction.js +++ b/src/actions/types/SourceAction.js @@ -10,7 +10,8 @@ import type { PromiseAction } from "../utils/middleware/promise"; export type LoadSourceAction = PromiseAction< {| +type: "LOAD_SOURCE_TEXT", - +sourceId: string + +sourceId: string, + +epoch: number |}, Source >; diff --git a/src/actions/types/index.js b/src/actions/types/index.js index 1285d332e9..7773b4bf0f 100644 --- a/src/actions/types/index.js +++ b/src/actions/types/index.js @@ -4,7 +4,7 @@ // @flow -import type { Frame, Scope, Why, WorkerList, MainThread } from "../../types"; +import type { WorkerList, MainThread } from "../../types"; import type { State } from "../../reducers/types"; import type { MatchedLocations } from "../../reducers/file-search"; import type { TreeNode } from "../../utils/sources-tree/types"; @@ -64,25 +64,6 @@ type UpdateTabAction = {| +sourceId?: string |}; -type ReplayAction = - | {| - +type: "TRAVEL_TO", - +data: { - paused: { - why: Why, - scopes: Scope[], - frames: Frame[], - selectedFrameId: string, - loadedObjects: Object - }, - expressions?: Object[] - }, - +position: number - |} - | {| - +type: "CLEAR_HISTORY" - |}; - type NavigateAction = | {| +type: "CONNECT", +mainThread: MainThread, +canRewind: boolean |} | {| +type: "NAVIGATE", +mainThread: MainThread |}; @@ -176,5 +157,4 @@ export type Action = | FileTextSearchAction | ProjectTextSearchAction | DebugeeAction - | ReplayAction | SourceTreeAction; diff --git a/src/client/firefox/commands.js b/src/client/firefox/commands.js index 19fe1d8bda..e975de06e9 100644 --- a/src/client/firefox/commands.js +++ b/src/client/firefox/commands.js @@ -309,17 +309,23 @@ async function getFrameScopes(frame: Frame): Promise<*> { return sourceThreadClient.getEnvironment(frame.id); } -function pauseOnExceptions( - thread: string, +async function pauseOnExceptions( shouldPauseOnExceptions: boolean, shouldPauseOnCaughtExceptions: boolean ): Promise<*> { - return lookupThreadClient(thread).pauseOnExceptions( + await threadClient.pauseOnExceptions( shouldPauseOnExceptions, // Providing opposite value because server // uses "shouldIgnoreCaughtExceptions" !shouldPauseOnCaughtExceptions ); + + await forEachWorkerThread(thread => + thread.pauseOnExceptions( + shouldPauseOnExceptions, + !shouldPauseOnCaughtExceptions + ) + ); } async function blackBox( diff --git a/src/client/firefox/types.js b/src/client/firefox/types.js index 9371c876dd..fe70366034 100644 --- a/src/client/firefox/types.js +++ b/src/client/firefox/types.js @@ -157,16 +157,12 @@ export type FramesResponse = { export type TabPayload = { actor: ActorId, animationsActor: ActorId, - callWatcherActor: ActorId, - canvasActor: ActorId, consoleActor: ActorId, cssPropertiesActor: ActorId, - cssUsageActor: ActorId, directorManagerActor: ActorId, emulationActor: ActorId, eventLoopLagActor: ActorId, framerateActor: ActorId, - gcliActor: ActorId, inspectorActor: ActorId, memoryActor: ActorId, monitorActor: ActorId, @@ -182,9 +178,7 @@ export type TabPayload = { timelineActor: ActorId, title: string, url: URL, - webExtensionInspectedWindowActor: ActorId, - webaudioActor: ActorId, - webglActor: ActorId + webExtensionInspectedWindowActor: ActorId }; /** diff --git a/src/client/index.js b/src/client/index.js index 743d28a3dd..c0aa5fe8b6 100644 --- a/src/client/index.js +++ b/src/client/index.js @@ -7,7 +7,7 @@ import * as firefox from "./firefox"; import * as chrome from "./chrome"; -import { prefs, asyncStore } from "../utils/prefs"; +import { asyncStore, verifyPrefSchema } from "../utils/prefs"; import { setupHelper } from "../utils/dbg"; import { @@ -19,16 +19,6 @@ import { initialBreakpointsState } from "../reducers/breakpoints"; import type { Panel } from "./firefox/types"; -function loadFromPrefs(actions: Object) { - const { pauseOnExceptions, pauseOnCaughtExceptions } = prefs; - if (pauseOnExceptions || pauseOnCaughtExceptions) { - return actions.pauseOnExceptions( - pauseOnExceptions, - pauseOnCaughtExceptions - ); - } -} - async function syncBreakpoints() { const breakpoints = await asyncStore.pendingBreakpoints; const breakpointValues = (Object.values(breakpoints): any); @@ -77,6 +67,8 @@ export async function onConnect( return; } + verifyPrefSchema(); + const client = getClient(connection); const commands = client.clientCommands; @@ -92,7 +84,6 @@ export async function onConnect( const workers = bootstrapWorkers(); await client.onConnect(connection, actions); - await loadFromPrefs(actions); syncBreakpoints(); syncXHRBreakpoints(); setupHelper({ diff --git a/src/components/App.js b/src/components/App.js index 9f47551271..822f8f2212 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -56,7 +56,6 @@ import Editor from "./Editor"; import SecondaryPanes from "./SecondaryPanes"; import WelcomeBox from "./WelcomeBox"; import EditorTabs from "./Editor/Tabs"; -import EditorFooter from "./Editor/Footer"; import QuickOpenModal from "./QuickOpenModal"; type Props = { @@ -235,7 +234,6 @@ class App extends Component { toggleShortcutsModal={() => this.toggleShortcutsModal()} /> ) : null} - diff --git a/src/components/Editor/Breakpoints.css b/src/components/Editor/Breakpoints.css new file mode 100644 index 0000000000..ee15faabb5 --- /dev/null +++ b/src/components/Editor/Breakpoints.css @@ -0,0 +1,154 @@ +/* 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 . */ + + .theme-light { + --gutter-hover-background-color: #dde1e4; + + --breakpoint-fill: var(--blue-50); + --breakpoint-stroke: var(--blue-60); + + --breakpoint-disabled-fill: var(--blue-55); + --breakpoint-disabled-stroke: var(--blue-55); +} + +.theme-dark { + --gutter-hover-background-color: #414141; + + --breakpoint-fill: var(--blue-55); + --breakpoint-stroke: var(--blue-40); + + --breakpoint-disabled-fill: var(--blue-55); + --breakpoint-disabled-stroke: var(--blue-55); +} + +.theme-light, .theme-dark { + --logpoint-fill: var(--theme-graphs-purple); + --logpoint-stroke: var(--purple-60); + --breakpoint-condition-fill: var(--theme-graphs-yellow); + --breakpoint-condition-stroke: var(--theme-graphs-orange); + --breakpoint-inactive-opacity: 0.3; + --breakpoint-disabled-opacity: 0.6; +} + +/* Standard gutter breakpoints */ +.editor-wrapper .breakpoints { + position: absolute; + top: 0; + left: 0; +} + +.new-breakpoint .CodeMirror-linenumber { + pointer-events: none; +} + +:not(.empty-line):not(.new-breakpoint) + > .CodeMirror-gutter-wrapper:hover + > .CodeMirror-linenumber::after { + content: ""; + position: absolute; + /* paint below the number */ + z-index: -1; + top: 0; + left: 0; + right: -7px; + bottom: 0; + height: 15px; + background-color: var(--gutter-hover-background-color); + mask: url(/images/breakpoint.svg) no-repeat; + mask-size: auto 15px; + mask-position: right; +} + +.editor.new-breakpoint svg { + fill: var(--breakpoint-fill); + stroke: var(--breakpoint-stroke); + width: 60px; + height: 15px; + position: absolute; + top: 0px; + right: -4px; +} + +.editor .breakpoint { + position: absolute; + right: -2px; +} + +.editor.new-breakpoint.folding-enabled svg { + right: -16px; +} + +.new-breakpoint.has-condition .CodeMirror-gutter-wrapper svg { + fill: var(--breakpoint-condition-fill); + stroke: var(--breakpoint-condition-stroke); +} + +.new-breakpoint.has-log .CodeMirror-gutter-wrapper svg { + fill: var(--logpoint-fill); + stroke: var(--logpoint-stroke); +} + +.editor.new-breakpoint.breakpoint-disabled svg { + fill-opacity: var(--breakpoint-disabled-opacity); + stroke-opacity: var(--breakpoint-disabled-opacity); +} + +/* Columnn breakpoints */ +.column-breakpoint { + display: inline; + padding-inline-start: 1px; + padding-inline-end: 1px; +} + +.column-breakpoint:hover { + background-color: transparent; +} + +.column-breakpoint svg { + display: inline-block; + cursor: pointer; + height: 13px; + width: 11px; + vertical-align: top; + fill: var(--breakpoint-fill); + stroke: var(--breakpoint-stroke); + fill-opacity: var(--breakpoint-inactive-opacity); + stroke-opacity: var(--breakpoint-inactive-opacity); +} + +.column-breakpoint.active svg { + fill: var(--breakpoint-fill); + stroke: var(--breakpoint-stroke); + fill-opacity: 1; + stroke-opacity: 1; +} + +.column-breakpoint.disabled svg { + fill-opacity: var(--breakpoint-disabled-opacity); + stroke-opacity: var(--breakpoint-disabled-opacity); +} + +.column-breakpoint.has-log.disabled svg { + fill-opacity: 0.5; + stroke-opacity: 0.5; +} + +.column-breakpoint.disabled:not(.has-condition):not(.has-log) svg { + fill: var(--breakpoint-disabled-fill); + stroke: var(--breakpoint-disabled-stroke); +} + +.column-breakpoint.has-condition svg { + fill: var(--breakpoint-condition-fill); + stroke: var(--breakpoint-condition-stroke); +} + +.column-breakpoint.has-log svg { + fill: var(--logpoint-fill); + stroke: var(--logpoint-stroke); +} + +.img.column-marker { + background-image: url(/images/column-marker.svg); +} diff --git a/src/components/Editor/ColumnBreakpoint.js b/src/components/Editor/ColumnBreakpoint.js index 7a38da3fb2..1d003e66a0 100644 --- a/src/components/Editor/ColumnBreakpoint.js +++ b/src/components/Editor/ColumnBreakpoint.js @@ -34,6 +34,7 @@ function makeBookmark({ breakpoint }, { onClick, onContextMenu }) { const bp = breakpointImg.cloneNode(true); const isActive = breakpoint && !breakpoint.disabled; + const isDisabled = breakpoint && breakpoint.disabled; const condition = breakpoint && breakpoint.options.condition; const logValue = breakpoint && breakpoint.options.logValue; @@ -41,7 +42,7 @@ function makeBookmark({ breakpoint }, { onClick, onContextMenu }) { "has-condition": condition, "has-log": logValue, active: isActive, - disabled: !isActive + disabled: isDisabled }); if (condition) { diff --git a/src/components/Editor/ColumnBreakpoints.js b/src/components/Editor/ColumnBreakpoints.js index 6a4d501825..f0762e9d61 100644 --- a/src/components/Editor/ColumnBreakpoints.js +++ b/src/components/Editor/ColumnBreakpoints.js @@ -7,7 +7,6 @@ import React, { Component } from "react"; import ColumnBreakpoint from "./ColumnBreakpoint"; -import "./ColumnBreakpoints.css"; import { getSelectedSource, visibleColumnBreakpoints } from "../../selectors"; import { connect } from "../../utils/connect"; diff --git a/src/components/Editor/DebugLine.js b/src/components/Editor/DebugLine.js index 1ceeaca819..f8d263191f 100644 --- a/src/components/Editor/DebugLine.js +++ b/src/components/Editor/DebugLine.js @@ -18,7 +18,8 @@ import { connect } from "../../utils/connect"; import { getVisibleSelectedFrame, getPauseReason, - getSourceFromId + getSourceFromId, + getCurrentThread } from "../../selectors"; import type { Frame, Why, Source } from "../../types"; @@ -118,7 +119,7 @@ const mapStateToProps = state => { return { frame, source: frame && getSourceFromId(state, frame.location.sourceId), - why: getPauseReason(state) + why: getPauseReason(state, getCurrentThread(state)) }; }; diff --git a/src/components/Editor/Editor.css b/src/components/Editor/Editor.css index 0487ae437c..08c5bbeda6 100644 --- a/src/components/Editor/Editor.css +++ b/src/components/Editor/Editor.css @@ -44,9 +44,9 @@ */ .editor-wrapper { position: absolute; + height: calc(100% - var(--editor-header-height)); width: calc(100% - 1px); top: var(--editor-header-height); - bottom: var(--editor-footer-height); left: 0px; } @@ -54,18 +54,6 @@ html[dir="rtl"] .editor-mount { direction: ltr; } -.theme-light { - --gutter-hover-background-color: #dde1e4; - --breakpoint-fill: var(--blue-50); - --breakpoint-stroke: var(--blue-60); -} - -.theme-dark { - --gutter-hover-background-color: #414141; - --breakpoint-fill: var(--blue-55); - --breakpoint-stroke: var(--blue-40); -} - .theme-light .cm-s-mozilla .empty-line .CodeMirror-linenumber { color: var(--grey-40); } @@ -74,34 +62,6 @@ html[dir="rtl"] .editor-mount { color: var(--grey-50); } -.new-breakpoint .CodeMirror-linenumber { - pointer-events: none; -} - -:not(.empty-line):not(.new-breakpoint) - > .CodeMirror-gutter-wrapper:hover - > .CodeMirror-linenumber::after { - content: ""; - position: absolute; - /* paint below the number */ - z-index: -1; - top: 0; - left: 0; - right: -7px; - bottom: 0; - height: 15px; - background-color: var(--gutter-hover-background-color); - mask: url(/images/breakpoint.svg) no-repeat; - mask-size: auto 15px; - mask-position: right; -} - -.editor-wrapper .breakpoints { - position: absolute; - top: 0; - left: 0; -} - .function-search { max-height: 300px; overflow: hidden; @@ -119,50 +79,6 @@ html[dir="rtl"] .editor-mount { background: var(--theme-selection-background-hover); } -.editor.new-breakpoint svg { - fill: var(--breakpoint-fill); - stroke: var(--breakpoint-stroke); - width: 60px; - height: 15px; - position: absolute; - top: 0px; - right: -4px; -} - -.inline-bp { - background-color: #9ddfff; - width: 20px; - padding: 0px 5px; - margin: 0px 4px; - border-radius: 5px; - border-color: blue; - border: 1px solid #00b6ff; -} - -.editor .breakpoint { - position: absolute; - right: -2px; -} - -.editor.new-breakpoint.folding-enabled svg { - right: -16px; -} - -.new-breakpoint.has-condition .CodeMirror-gutter-wrapper svg { - fill: var(--theme-graphs-yellow); - stroke: var(--theme-graphs-orange); -} - -.new-breakpoint.has-log .CodeMirror-gutter-wrapper svg { - fill: var(--theme-graphs-purple); - stroke: var(--purple-60); -} - -.editor.new-breakpoint.breakpoint-disabled svg { - fill-opacity: 0.7; - stroke-opacity: 0.7; -} - .CodeMirror { width: 100%; height: 100%; @@ -264,8 +180,3 @@ debug-expression-error { .download-anchor { display: none; } - -/* This is not used, but it is needed to copy over column-marker.svg */ -.column-marker { - mask: url(/images/column-marker.svg) no-repeat; -} diff --git a/src/components/Editor/EditorMenu.js b/src/components/Editor/EditorMenu.js index b9b55e4276..953e1c63f9 100644 --- a/src/components/Editor/EditorMenu.js +++ b/src/components/Editor/EditorMenu.js @@ -9,7 +9,11 @@ import { connect } from "../../utils/connect"; import { showMenu } from "devtools-contextmenu"; import { getSourceLocationFromMouseEvent } from "../../utils/editor"; -import { getPrettySource, getIsPaused } from "../../selectors"; +import { + getPrettySource, + getIsPaused, + getCurrentThread +} from "../../selectors"; import { editorMenuItems, editorItemActions } from "./menus/editor"; @@ -74,7 +78,7 @@ class EditorMenu extends Component { } const mapStateToProps = (state, props) => ({ - isPaused: getIsPaused(state), + isPaused: getIsPaused(state, getCurrentThread(state)), hasPrettySource: !!getPrettySource(state, props.selectedSource.id) }); diff --git a/src/components/Editor/Footer.css b/src/components/Editor/Footer.css index b8ecf16a1d..71639cefd9 100644 --- a/src/components/Editor/Footer.css +++ b/src/components/Editor/Footer.css @@ -16,12 +16,12 @@ user-select: none; height: var(--editor-footer-height); box-sizing: border-box; + justify-content: space-between; } .source-footer .commands { display: flex; align-items: center; - justify-self: start; } .source-footer .commands * { @@ -65,7 +65,6 @@ .source-footer .cursor-position { color: var(--theme-body-color); padding-right: 2.5px; - margin-left: auto; } .source-footer .mapped-source { diff --git a/src/components/Editor/Footer.js b/src/components/Editor/Footer.js index 1575b0c3b4..e29dea035d 100644 --- a/src/components/Editor/Footer.js +++ b/src/components/Editor/Footer.js @@ -22,7 +22,7 @@ import { shouldBlackbox } from "../../utils/source"; import { getGeneratedSource } from "../../reducers/sources"; -import { shouldShowPrettyPrint } from "../../utils/editor"; +import { shouldShowFooter, shouldShowPrettyPrint } from "../../utils/editor"; import { PaneToggleButton } from "../shared/Button"; import AccessibleImage from "../shared/AccessibleImage"; @@ -40,6 +40,7 @@ type Props = { selectedSource: Source, mappedSource: Source, endPanelCollapsed: boolean, + editor: Object, horizontal: boolean, togglePrettyPrint: typeof actions.togglePrettyPrint, toggleBlackBox: typeof actions.toggleBlackBox, @@ -55,40 +56,22 @@ class SourceFooter extends PureComponent { constructor() { super(); - this.state = { cursorPosition: { line: 0, column: 0 } }; + this.state = { cursorPosition: { line: 1, column: 1 } }; } - componentDidUpdate() { - const eventDoc = document.querySelector(".editor-mount .CodeMirror"); - // querySelector can return null - if (eventDoc) { - this.toggleCodeMirror(eventDoc, true); - } + componentDidMount() { + const { editor } = this.props; + editor.codeMirror.on("cursorActivity", this.onCursorChange); } componentWillUnmount() { - const eventDoc = document.querySelector(".editor-mount .CodeMirror"); - - if (eventDoc) { - this.toggleCodeMirror(eventDoc, false); - } - } - - toggleCodeMirror(eventDoc: Object, toggle: boolean) { - if (toggle === true) { - eventDoc.CodeMirror.on("cursorActivity", this.onCursorChange); - } else { - eventDoc.CodeMirror.off("cursorActivity", this.onCursorChange); - } + const { editor } = this.props; + editor.codeMirror.off("cursorActivity", this.onCursorChange); } prettyPrintButton() { const { selectedSource, togglePrettyPrint } = this.props; - if (!selectedSource) { - return; - } - if (isLoading(selectedSource) && selectedSource.isPrettyPrinted) { return (
@@ -125,20 +108,13 @@ class SourceFooter extends PureComponent { const { selectedSource, toggleBlackBox } = this.props; const sourceLoaded = selectedSource && isLoaded(selectedSource); - if (!selectedSource) { - return; - } - if (!shouldBlackbox(selectedSource)) { return; } const blackboxed = selectedSource.isBlackBoxed; - const tooltip = blackboxed - ? L10N.getStr("sourceFooter.unblackbox") - : L10N.getStr("sourceFooter.blackbox"); - + const tooltip = L10N.getStr("sourceFooter.blackbox"); const type = "black-box"; return ( @@ -174,7 +150,7 @@ class SourceFooter extends PureComponent { } renderCommands() { - const commands = [this.blackBoxButton(), this.prettyPrintButton()].filter( + const commands = [this.prettyPrintButton(), this.blackBoxButton()].filter( Boolean ); @@ -216,30 +192,32 @@ class SourceFooter extends PureComponent { }; renderCursorPosition() { - if (!this.props.selectedSource) { - return null; - } - - const { line, column } = this.state.cursorPosition; + const { cursorPosition } = this.state; const text = L10N.getFormatStr( "sourceFooter.currentCursorPosition", - line + 1, - column + 1 + cursorPosition.line + 1, + cursorPosition.column + 1 ); const title = L10N.getFormatStr( "sourceFooter.currentCursorPosition.tooltip", - line + 1, - column + 1 + cursorPosition.line + 1, + cursorPosition.column + 1 ); return ( -
+ {text} -
+ ); } render() { + const { selectedSource, horizontal } = this.props; + + if (!shouldShowFooter(selectedSource, horizontal)) { + return null; + } + return (
{this.renderCommands()} diff --git a/src/components/Editor/HighlightLine.js b/src/components/Editor/HighlightLine.js index 1aa904e20d..5872fca052 100644 --- a/src/components/Editor/HighlightLine.js +++ b/src/components/Editor/HighlightLine.js @@ -13,7 +13,8 @@ import { getVisibleSelectedFrame, getSelectedLocation, getSelectedSource, - getPauseCommand + getPauseCommand, + getCurrentThread } from "../../selectors"; import type { @@ -168,7 +169,7 @@ export class HighlightLine extends Component { } export default connect(state => ({ - pauseCommand: getPauseCommand(state), + pauseCommand: getPauseCommand(state, getCurrentThread(state)), selectedFrame: getVisibleSelectedFrame(state), selectedLocation: getSelectedLocation(state), selectedSource: getSelectedSource(state) diff --git a/src/components/Editor/Preview/Popup.js b/src/components/Editor/Preview/Popup.js index deb64bfdc7..cf2850f7b3 100644 --- a/src/components/Editor/Preview/Popup.js +++ b/src/components/Editor/Preview/Popup.js @@ -22,7 +22,10 @@ const { } = utils; import actions from "../../../actions"; -import { getAllPopupObjectProperties } from "../../../selectors"; +import { + getAllPopupObjectProperties, + getCurrentThread +} from "../../../selectors"; import Popover from "../../shared/Popover"; import PreviewFunction from "../../shared/PreviewFunction"; @@ -318,7 +321,10 @@ export class Popup extends Component { } const mapStateToProps = state => ({ - popupObjectProperties: getAllPopupObjectProperties(state) + popupObjectProperties: getAllPopupObjectProperties( + state, + getCurrentThread(state) + ) }); const { diff --git a/src/components/Editor/Preview/index.js b/src/components/Editor/Preview/index.js index b155e06c4a..3fc9064f78 100644 --- a/src/components/Editor/Preview/index.js +++ b/src/components/Editor/Preview/index.js @@ -9,7 +9,12 @@ import { connect } from "../../../utils/connect"; import Popup from "./Popup"; -import { getPreview, getSelectedSource, getIsPaused } from "../../../selectors"; +import { + getPreview, + getSelectedSource, + getIsPaused, + getCurrentThread +} from "../../../selectors"; import actions from "../../../actions"; import { toEditorRange } from "../../../utils/editor"; @@ -179,7 +184,7 @@ class Preview extends PureComponent { const mapStateToProps = state => ({ preview: getPreview(state), - isPaused: getIsPaused(state), + isPaused: getIsPaused(state, getCurrentThread(state)), selectedSource: getSelectedSource(state) }); diff --git a/src/components/Editor/SearchBar.css b/src/components/Editor/SearchBar.css index ea23ac078c..0d94085328 100644 --- a/src/components/Editor/SearchBar.css +++ b/src/components/Editor/SearchBar.css @@ -19,7 +19,7 @@ left: 0; right: 0; bottom: -1px; - border: solid 1px var(--blue-50); + border: solid 1px var(--blue-50); pointer-events: none; opacity: 0; transition: opacity 150ms ease-out; diff --git a/src/components/Editor/Tabs.js b/src/components/Editor/Tabs.js index b95370f96c..a17a993c25 100644 --- a/src/components/Editor/Tabs.js +++ b/src/components/Editor/Tabs.js @@ -10,7 +10,8 @@ import { connect } from "../../utils/connect"; import { getSelectedSource, getSourcesForTabs, - getIsPaused + getIsPaused, + getCurrentThread } from "../../selectors"; import { isVisible } from "../../utils/ui"; @@ -227,7 +228,7 @@ class Tabs extends PureComponent { const mapStateToProps = state => ({ selectedSource: getSelectedSource(state), tabSources: getSourcesForTabs(state), - isPaused: getIsPaused(state) + isPaused: getIsPaused(state, getCurrentThread(state)) }); export default connect( diff --git a/src/components/Editor/index.js b/src/components/Editor/index.js index 1cae91095e..0b6afc1e25 100644 --- a/src/components/Editor/index.js +++ b/src/components/Editor/index.js @@ -10,7 +10,7 @@ import { bindActionCreators } from "redux"; import ReactDOM from "react-dom"; import { connect } from "../../utils/connect"; import classnames from "classnames"; -import { debounce } from "lodash"; +import { throttle } from "lodash"; import { isLoaded } from "../../utils/source"; import { isFirefox } from "devtools-environment"; @@ -34,12 +34,14 @@ import { getSelectedSource, getConditionalPanelLocation, getSymbols, - getIsPaused + getIsPaused, + getCurrentThread } from "../../selectors"; // Redux actions import actions from "../../actions"; +import Footer from "./Footer"; import SearchBar from "./SearchBar"; import HighlightLines from "./HighlightLines"; import Preview from "./Preview"; @@ -56,6 +58,7 @@ import { updateDocument, showLoading, showErrorMessage, + shouldShowFooter, getEditor, clearEditor, getCursorLine, @@ -74,6 +77,7 @@ import { import { resizeToggleButton, resizeBreakpointGutter } from "../../utils/ui"; import "./Editor.css"; +import "./Breakpoints.css"; import "./Highlight.css"; import type SourceEditor from "../../utils/editor/source-editor"; @@ -81,13 +85,15 @@ import type { SymbolDeclarations } from "../../workers/parser"; import type { SourceLocation, Source } from "../../types"; const cssVars = { - searchbarHeight: "var(--editor-searchbar-height)" + searchbarHeight: "var(--editor-searchbar-height)", + footerHeight: "var(--editor-footer-height)" }; export type Props = { selectedLocation: ?SourceLocation, selectedSource: ?Source, searchOn: boolean, + horizontal: boolean, startPanelSize: number, endPanelSize: number, conditionalPanelLocation: SourceLocation, @@ -201,7 +207,6 @@ class Editor extends PureComponent { codeMirror.on("scroll", this.onEditorScroll); this.onEditorScroll(); - this.setState({ editor }); return editor; } @@ -216,11 +221,7 @@ class Editor extends PureComponent { shortcuts.on(L10N.getStr("toggleBreakpoint.key"), this.onToggleBreakpoint); shortcuts.on( - L10N.getStr("toggleCondPanel.breakpoint.key"), - this.onToggleConditionalPanel - ); - shortcuts.on( - L10N.getStr("toggleCondPanel.logPoint.key"), + L10N.getStr("toggleCondPanel.key"), this.onToggleConditionalPanel ); shortcuts.on(L10N.getStr("sourceTabs.closeTab.key"), this.onClosePress); @@ -252,8 +253,7 @@ class Editor extends PureComponent { const shortcuts = this.context.shortcuts; shortcuts.off(L10N.getStr("sourceTabs.closeTab.key")); shortcuts.off(L10N.getStr("toggleBreakpoint.key")); - shortcuts.off(L10N.getStr("toggleCondPanel.breakpoint.key")); - shortcuts.off(L10N.getStr("toggleCondPanel.logPoint.key")); + shortcuts.off(L10N.getStr("toggleCondPanel.key")); shortcuts.off(searchAgainPrevKey); shortcuts.off(searchAgainKey); } @@ -272,6 +272,10 @@ class Editor extends PureComponent { this.setSize(this.props); } } + + if (prevProps.selectedSource != selectedSource) { + this.props.updateViewport(); + } } getCurrentLine() { @@ -301,16 +305,14 @@ class Editor extends PureComponent { e.stopPropagation(); e.preventDefault(); const line = this.getCurrentLine(); - if (typeof line !== "number") { return; } - const isLog = key === L10N.getStr("toggleCondPanel.logPoint.key"); - this.toggleConditionalPanel(line, isLog); + this.toggleConditionalPanel(line); }; - onEditorScroll = debounce(this.props.updateViewport, 200); + onEditorScroll = throttle(this.props.updateViewport, 100); onKeyDown(e: KeyboardEvent) { const { codeMirror } = this.state.editor; @@ -456,7 +458,7 @@ class Editor extends PureComponent { } } - toggleConditionalPanel = (line, log: boolean = false) => { + toggleConditionalPanel = line => { const { conditionalPanelLocation, closeConditionalPanel, @@ -472,14 +474,11 @@ class Editor extends PureComponent { return; } - return openConditionalPanel( - { - line: line, - sourceId: selectedSource.id, - sourceUrl: selectedSource.url - }, - log - ); + return openConditionalPanel({ + line: line, + sourceId: selectedSource.id, + sourceUrl: selectedSource.url + }); }; shouldScrollToLocation(nextProps) { @@ -578,21 +577,28 @@ class Editor extends PureComponent { } getInlineEditorStyles() { - const { searchOn } = this.props; + const { selectedSource, horizontal, searchOn } = this.props; + + const subtractions = []; + + if (shouldShowFooter(selectedSource, horizontal)) { + subtractions.push(cssVars.footerHeight); + } if (searchOn) { - return { - height: `calc(100% - ${cssVars.searchbarHeight})` - }; + subtractions.push(cssVars.searchbarHeight); } return { - height: "100%" + height: + subtractions.length === 0 + ? "100%" + : `calc(100% - ${subtractions.join(" - ")})` }; } renderItems() { - const { selectedSource, conditionalPanelLocation } = this.props; + const { horizontal, selectedSource, conditionalPanelLocation } = this.props; const { editor, contextMenu } = this.state; if (!selectedSource || !editor || !getDocument(selectedSource.id)) { @@ -606,6 +612,7 @@ class Editor extends PureComponent { +
{ { renderSearchBar() { const { editor } = this.state; - if (!this.props.selectedSource) { + if (!editor) { return null; } @@ -663,7 +670,7 @@ const mapStateToProps = state => { searchOn: getActiveSearch(state) === "file", conditionalPanelLocation: getConditionalPanelLocation(state), symbols: getSymbols(state, selectedSource), - isPaused: getIsPaused(state) + isPaused: getIsPaused(state, getCurrentThread(state)) }; }; diff --git a/src/components/Editor/menus/breakpoints.js b/src/components/Editor/menus/breakpoints.js index 116d1dbf52..f798d9f14e 100644 --- a/src/components/Editor/menus/breakpoints.js +++ b/src/components/Editor/menus/breakpoints.js @@ -39,7 +39,7 @@ export const addConditionalBreakpointItem = ( ) => ({ id: "node-menu-add-conditional-breakpoint", label: L10N.getStr("editor.addConditionBreakpoint"), - accelerator: L10N.getStr("toggleCondPanel.breakpoint.key"), + accelerator: L10N.getStr("toggleCondPanel.key"), accesskey: L10N.getStr("editor.addConditionBreakpoint.accesskey"), disabled: false, click: () => breakpointActions.openConditionalPanel(location) @@ -51,7 +51,7 @@ export const editConditionalBreakpointItem = ( ) => ({ id: "node-menu-edit-conditional-breakpoint", label: L10N.getStr("editor.editConditionBreakpoint"), - accelerator: L10N.getStr("toggleCondPanel.breakpoint.key"), + accelerator: L10N.getStr("toggleCondPanel.key"), accesskey: L10N.getStr("editor.addConditionBreakpoint.accesskey"), disabled: false, click: () => breakpointActions.openConditionalPanel(location) @@ -79,7 +79,7 @@ export const addLogPointItem = ( accesskey: L10N.getStr("editor.addLogPoint.accesskey"), disabled: false, click: () => breakpointActions.openConditionalPanel(location, true), - accelerator: L10N.getStr("toggleCondPanel.logPoint.key") + accelerator: L10N.getStr("toggleCondPanel.key") }); export const editLogPointItem = ( @@ -91,7 +91,7 @@ export const editLogPointItem = ( accesskey: L10N.getStr("editor.addLogPoint.accesskey"), disabled: false, click: () => breakpointActions.openConditionalPanel(location, true), - accelerator: L10N.getStr("toggleCondPanel.logPoint.key") + accelerator: L10N.getStr("toggleCondPanel.key") }); export const logPointItem = ( diff --git a/src/components/Editor/tests/__snapshots__/Editor.spec.js.snap b/src/components/Editor/tests/__snapshots__/Editor.spec.js.snap index 31aaa84b96..76a6d5b993 100644 --- a/src/components/Editor/tests/__snapshots__/Editor.spec.js.snap +++ b/src/components/Editor/tests/__snapshots__/Editor.spec.js.snap @@ -8,7 +8,7 @@ exports[`Editor When empty should render 1`] = ` className="editor-mount devtools-monospace" style={ Object { - "height": "100%", + "height": "calc(100% - var(--editor-footer-height))", } } /> diff --git a/src/components/Editor/tests/__snapshots__/Footer.spec.js.snap b/src/components/Editor/tests/__snapshots__/Footer.spec.js.snap index a2ec2a6de4..88060f8c8b 100644 --- a/src/components/Editor/tests/__snapshots__/Footer.spec.js.snap +++ b/src/components/Editor/tests/__snapshots__/Footer.spec.js.snap @@ -4,12 +4,12 @@ exports[`SourceFooter Component default case should render 1`] = `
-
- (1, 1) -
+ (2, 2) + -
(6, 11) -
+ { return this.renderPane( this.renderThreadHeader(), - this.isEmpty() ? ( - this.renderEmptyElement(L10N.getStr("noSourcesText")) - ) : ( -
- {this.renderTree()} -
- ) +
+ {this.renderTree()} +
); } } diff --git a/src/components/PrimaryPanes/tests/SourcesTree.spec.js b/src/components/PrimaryPanes/tests/SourcesTree.spec.js index f29f49d79a..9fa1480358 100644 --- a/src/components/PrimaryPanes/tests/SourcesTree.spec.js +++ b/src/components/PrimaryPanes/tests/SourcesTree.spec.js @@ -28,14 +28,6 @@ describe("SourcesTree", () => { expect(component).toMatchSnapshot(); }); - it("Should show a 'No Sources' message if there are no sources", async () => { - const { component, defaultState } = render(); - const sourceTree = defaultState.sourceTree; - sourceTree.contents = []; - component.setState({ sourceTree: sourceTree }); - expect(component).toMatchSnapshot(); - }); - describe("When loading initial source", () => { it("Shows the tree with one.js, two.js and three.js expanded", async () => { const { component, props } = render(); diff --git a/src/components/PrimaryPanes/tests/__snapshots__/SourcesTree.spec.js.snap b/src/components/PrimaryPanes/tests/__snapshots__/SourcesTree.spec.js.snap index c93adeac15..f99bf83511 100644 --- a/src/components/PrimaryPanes/tests/__snapshots__/SourcesTree.spec.js.snap +++ b/src/components/PrimaryPanes/tests/__snapshots__/SourcesTree.spec.js.snap @@ -48,32 +48,6 @@ exports[`SourcesTree After changing expanded nodes Shows the tree with four.js,
`; -exports[`SourcesTree Should show a 'No Sources' message if there are no sources 1`] = ` -
-
- - - Main Thread - -
-
- This page has no sources. -
-
-`; - exports[`SourcesTree Should show the tree with nothing expanded 1`] = `
{ const { selectedIndex, results } = this.state; const resultCount = this.getResultCount(); const index = selectedIndex + direction; - const nextIndex = (index + resultCount) % resultCount || 0; + const nextIndex = (index + resultCount) % resultCount; this.setState({ selectedIndex: nextIndex }); diff --git a/src/components/SecondaryPanes/Breakpoints/Breakpoint.js b/src/components/SecondaryPanes/Breakpoints/Breakpoint.js index a38b78200e..aa7e683ce5 100644 --- a/src/components/SecondaryPanes/Breakpoints/Breakpoint.js +++ b/src/components/SecondaryPanes/Breakpoints/Breakpoint.js @@ -37,7 +37,8 @@ type FormattedFrame = Frame & { import { getBreakpointsList, getSelectedFrame, - getSelectedSource + getSelectedSource, + getCurrentThread } from "../../../selectors"; type Props = { @@ -211,7 +212,7 @@ const getFormattedFrame = createSelector( const mapStateToProps = state => ({ breakpoints: getBreakpointsList(state), - frame: getFormattedFrame(state) + frame: getFormattedFrame(state, getCurrentThread(state)) }); export default connect( diff --git a/src/components/SecondaryPanes/Breakpoints/Breakpoints.css b/src/components/SecondaryPanes/Breakpoints/Breakpoints.css index 1a4c64800e..e1d643f2bb 100644 --- a/src/components/SecondaryPanes/Breakpoints/Breakpoints.css +++ b/src/components/SecondaryPanes/Breakpoints/Breakpoints.css @@ -6,24 +6,20 @@ margin: 2px 3px; } -.breakpoints-list * { - user-select: none; +.pane.breakpoints-list { + padding-bottom: 0.35em; } -.breakpoints-list { - margin-top: 4px; - margin-bottom: 4px; +.breakpoints-list * { + user-select: none; } .breakpoints-list .breakpoint-heading { text-overflow: ellipsis; + overflow: hidden; + display: flex; width: 100%; - font-size: 12px; - line-height: 16px; -} - -.breakpoint-heading:not(:first-child) { - margin-top: 2px; + align-items: center; } .breakpoints-list .breakpoint-heading .filename { @@ -38,8 +34,10 @@ .breakpoints-list .breakpoint-heading, .breakpoints-list .breakpoint { + font-size: 12px; color: var(--theme-content-color1); position: relative; + transition: all 0.25s ease; cursor: pointer; } @@ -47,56 +45,53 @@ .breakpoints-list .breakpoint, .breakpoints-exceptions, .breakpoints-exceptions-caught { - display: flex; - align-items: center; - overflow: hidden; - padding-top: 2px; - padding-bottom: 2px; - padding-inline-start: 16px; - padding-inline-end: 12px; + padding: 0.25em 1em; } .breakpoints-exceptions { - padding-bottom: 3px; - padding-top: 3px; + padding-bottom: 0.5em; + padding-top: 0.5em; user-select: none; } -.breakpoints-exceptions-caught { - padding-bottom: 3px; - padding-top: 3px; - padding-inline-start: 36px; +.breakpoints-list .breakpoint { + min-height: var(--breakpoint-expression-height); + overflow: hidden; } -.breakpoints-exceptions-options { - padding-top: 4px; - padding-bottom: 4px; +.breakpoints-exceptions-caught { + padding: 0 1em 0.5em 3em; + margin-top: -0.25em; } -.xhr-breakpoints-pane .breakpoints-exceptions-options { - border-bottom: 1px solid var(--theme-splitter-color); +html[dir="rtl"] .breakpoints-exceptions-caught { + padding: 0 3em 0.5em 1em; } .breakpoints-exceptions-options:not(.empty) { border-bottom: 1px solid var(--theme-splitter-color); + margin-bottom: 3px; } .breakpoints-exceptions input, .breakpoints-exceptions-caught input { padding-inline-start: 2px; - margin-top: 0px; - margin-bottom: 0px; margin-inline-start: 0; - margin-inline-end: 2px; vertical-align: text-bottom; } .breakpoint-exceptions-label { - line-height: 14px; + padding-top: 2px; + padding-inline-start: 2px; padding-inline-end: 8px; cursor: default; - overflow: hidden; - text-overflow: ellipsis; +} + +.breakpoints-list .breakpoint, +.breakpoints-exceptions, +.breakpoints-exceptions-caught { + display: flex; + align-items: center; } html[dir="rtl"] .breakpoints-list .breakpoint, @@ -133,20 +128,15 @@ html .breakpoints-list .breakpoint.paused { background-color: var(--search-overlays-semitransparent); } -.breakpoint-line-close { - margin-inline-start: 4px; -} - .breakpoints-list .breakpoint .breakpoint-line { font-size: 11px; color: var(--theme-comment); min-width: 16px; text-align: end; - padding-top: 1px; - padding-bottom: 1px; } -.breakpoints-list .breakpoint:hover .breakpoint-line { +.breakpoints-list .breakpoint:hover .breakpoint-line, +.breakpoints-list .breakpoint-line-close:focus-within .breakpoint-line { color: transparent; } @@ -155,24 +145,24 @@ html .breakpoints-list .breakpoint.paused { } .breakpoints-list .breakpoint-label { + max-width: calc(100% - var(--breakpoint-expression-right-clear-space)); display: inline-block; + padding-inline-end: 8px; cursor: pointer; flex-grow: 1; text-overflow: ellipsis; overflow: hidden; + padding-top: 2px; font-size: 11px; } -.breakpoints-list .breakpoint-label span, +.breakpoints-list .breakpoint-label, .breakpoint-line-close { - display: inline; - line-height: 14px; + line-height: 1.4em; } .breakpoint-checkbox { - margin-inline-start: 0px; - margin-top: 0px; - margin-bottom: 0px; + margin-inline-start: 0; vertical-align: text-bottom; } @@ -191,17 +181,10 @@ html .breakpoints-list .breakpoint.paused { } .breakpoint .close-btn { + inset-inline-end: 15px; + inset-inline-start: auto; position: absolute; - /* hide button outside of row until hovered or focused */ - top: -100px; -} - -[dir="ltr"] .breakpoint .close-btn { - right: 12px; -} - -[dir="rtl"] .breakpoint .close-btn { - left: 12px; + top: -100px; /*For hiding button outside of row until hovered or focused*/ } .breakpoint:hover .close-btn, diff --git a/src/components/SecondaryPanes/Breakpoints/BreakpointsContextMenu.js b/src/components/SecondaryPanes/Breakpoints/BreakpointsContextMenu.js index 7fc9544d51..dcd08d011c 100644 --- a/src/components/SecondaryPanes/Breakpoints/BreakpointsContextMenu.js +++ b/src/components/SecondaryPanes/Breakpoints/BreakpointsContextMenu.js @@ -205,8 +205,7 @@ export default function showContextMenu(props: Props) { click: () => { selectSpecificLocation(selectedLocation); openConditionalPanel(selectedLocation); - }, - accelerator: L10N.getStr("toggleCondPanel.breakpoint.key") + } }; const editConditionItem = { @@ -216,8 +215,7 @@ export default function showContextMenu(props: Props) { click: () => { selectSpecificLocation(selectedLocation); openConditionalPanel(selectedLocation); - }, - accelerator: L10N.getStr("toggleCondPanel.breakpoint.key") + } }; const addLogPointItem = { @@ -226,7 +224,7 @@ export default function showContextMenu(props: Props) { accesskey: L10N.getStr("editor.addLogPoint.accesskey"), disabled: false, click: () => openConditionalPanel(selectedLocation, true), - accelerator: L10N.getStr("toggleCondPanel.logPoint.key") + accelerator: L10N.getStr("toggleCondPanel.key") }; const editLogPointItem = { @@ -235,7 +233,7 @@ export default function showContextMenu(props: Props) { accesskey: L10N.getStr("editor.addLogPoint.accesskey"), disabled: false, click: () => openConditionalPanel(selectedLocation, true), - accelerator: L10N.getStr("toggleCondPanel.logPoint.key") + accelerator: L10N.getStr("toggleCondPanel.key") }; const removeLogPointItem = { diff --git a/src/components/SecondaryPanes/Breakpoints/index.js b/src/components/SecondaryPanes/Breakpoints/index.js index 66cbb8782e..0e4151e6fb 100644 --- a/src/components/SecondaryPanes/Breakpoints/index.js +++ b/src/components/SecondaryPanes/Breakpoints/index.js @@ -77,49 +77,43 @@ class Breakpoints extends Component { renderBreakpoints() { const { breakpointSources, selectedSource } = this.props; - if (!breakpointSources.length) { - return null; - } - const sources = [ ...breakpointSources.map(({ source, breakpoints }) => source) ]; - return ( -
- {breakpointSources.map(({ source, breakpoints, i }) => { - const path = getDisplayPath(source, sources); - const sortedBreakpoints = sortSelectedBreakpoints( - breakpoints, - selectedSource - ); - - return [ - { + const path = getDisplayPath(source, sources); + const sortedBreakpoints = sortSelectedBreakpoints( + breakpoints, + selectedSource + ); + + return [ + , + ...sortedBreakpoints.map(breakpoint => ( + , - ...sortedBreakpoints.map(breakpoint => ( - - )) - ]; - })} -
- ); + selectedSource={selectedSource} + key={makeBreakpointId( + getSelectedLocation(breakpoint, selectedSource) + )} + /> + )) + ]; + }) + ]; } render() { return ( -
+
{this.renderExceptionsOptions()} {this.renderBreakpoints()}
diff --git a/src/components/SecondaryPanes/Breakpoints/tests/__snapshots__/Breakpoint.spec.js.snap b/src/components/SecondaryPanes/Breakpoints/tests/__snapshots__/Breakpoint.spec.js.snap index ff2b61ca24..03aef3ad6f 100644 --- a/src/components/SecondaryPanes/Breakpoints/tests/__snapshots__/Breakpoint.spec.js.snap +++ b/src/components/SecondaryPanes/Breakpoints/tests/__snapshots__/Breakpoint.spec.js.snap @@ -35,7 +35,7 @@ exports[`Breakpoint disabled 1`] = `
- 53 + 53:73
- 53 + 53:73
- 53 + 53:73
- 5 + 5:7
- 53 + 53:73
({ - isPaused: getIsPaused(state), - isWaitingOnBreak: getIsWaitingOnBreak(state), + isPaused: getIsPaused(state, getCurrentThread(state)), + isWaitingOnBreak: getIsWaitingOnBreak(state, getCurrentThread(state)), canRewind: getCanRewind(state), skipPausing: getSkipPausing(state) }); diff --git a/src/components/SecondaryPanes/EventListeners.css b/src/components/SecondaryPanes/EventListeners.css index 65f8114c03..27697ec4ad 100644 --- a/src/components/SecondaryPanes/EventListeners.css +++ b/src/components/SecondaryPanes/EventListeners.css @@ -3,56 +3,56 @@ * file, You can obtain one at . */ .event-listeners-content { - padding-top: 4px; - padding-bottom: 4px; - padding-inline-start: 14px; - padding-inline-end: 20px; + padding-top: 4px; + padding-bottom: 4px; + padding-inline-start: 14px; + padding-inline-end: 20px; } .event-listeners-content ul { - padding: 0; - list-style-type: none; + padding: 0; + list-style-type: none; } .event-listener-group { - user-select: none; + user-select: none; } .event-listener-header { - display: flex; - align-items: center; + display: flex; + align-items: center; } .event-listener-expand { - border: none; - background: none; - padding: 4px 5px; - line-height: 12px; + border: none; + background: none; + padding: 4px 5px; + line-height: 12px; } .event-listener-expand:hover { - background: transparent; + background: transparent; } .event-listener-group input[type="checkbox"] { - margin: 0px; - margin-inline-end: 4px; + margin: 0px; + margin-inline-end: 4px; } .event-listener-label { - display: flex; - align-items: center; - padding-inline-start: 2px; - padding-inline-end: 10px; + display: flex; + align-items: center; + padding-inline-start: 2px; + padding-inline-end: 10px; } .event-listener-category { - padding: 3px 0px; - line-height: 14px; + padding: 3px 0px; + line-height: 14px; } .event-listeners-content .arrow { - margin-inline-end: 0; + margin-inline-end: 0; } html[dir="ltr"] .event-listeners-content .arrow.expanded { @@ -64,19 +64,19 @@ html[dir="rtl"] .event-listeners-content .arrow.expanded { } .event-listener-event { - display: flex; - align-items: center; - margin-inline-start: 30px; + display: flex; + align-items: center; + margin-inline-start: 30px; } .event-listener-name { - line-height: 14px; - padding: 3px 0px; + line-height: 14px; + padding: 3px 0px; } .event-listener-event input { - margin-inline-end: 4px; - margin-inline-start: 0px; - margin-top: 0px; - margin-bottom: 0px; + margin-inline-end: 4px; + margin-inline-start: 0px; + margin-top: 0px; + margin-bottom: 0px; } diff --git a/src/components/SecondaryPanes/Expressions.css b/src/components/SecondaryPanes/Expressions.css index a586ad3ec9..5dfa3496ee 100644 --- a/src/components/SecondaryPanes/Expressions.css +++ b/src/components/SecondaryPanes/Expressions.css @@ -44,7 +44,7 @@ .expressions-list { /* TODO: add normalize */ margin: 0; - padding: 4px 0px; + padding: 0; } .expression-input-container { @@ -72,7 +72,6 @@ background-color: var(--theme-body-background); display: block; position: relative; - overflow: hidden; min-height: var(--breakpoint-expression-height); } @@ -95,21 +94,8 @@ .expression-container__close-btn { position: absolute; - /* hiding button outside of row until hovered or focused */ - top: -100px; -} - -.expression-container:hover .expression-container__close-btn, -.expression-container__close-btn:focus-within { - top: calc(50% - 8px); -} - -[dir="ltr"] .expression-container__close-btn { - right: 0; -} - -[dir="rtl"] .expression-container__close-btn { - left: 0; + inset-inline-end: 0px; + top: 1px; } .expression-content { diff --git a/src/components/SecondaryPanes/Frames/index.js b/src/components/SecondaryPanes/Frames/index.js index 5991d7b314..7243483afa 100644 --- a/src/components/SecondaryPanes/Frames/index.js +++ b/src/components/SecondaryPanes/Frames/index.js @@ -23,7 +23,8 @@ import { getFrameworkGroupingState, getSelectedFrame, getCallStackFrames, - getPauseReason + getPauseReason, + getCurrentThread } from "../../../selectors"; import "./Frames.css"; @@ -216,9 +217,9 @@ Frames.contextTypes = { l10n: PropTypes.object }; const mapStateToProps = state => ({ frames: getCallStackFrames(state), - why: getPauseReason(state), + why: getPauseReason(state, getCurrentThread(state)), frameworkGroupingOn: getFrameworkGroupingState(state), - selectedFrame: getSelectedFrame(state) + selectedFrame: getSelectedFrame(state, getCurrentThread(state)) }); export default connect( diff --git a/src/components/SecondaryPanes/Scopes.css b/src/components/SecondaryPanes/Scopes.css index 23d7b81255..24a90c27b8 100644 --- a/src/components/SecondaryPanes/Scopes.css +++ b/src/components/SecondaryPanes/Scopes.css @@ -2,40 +2,25 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at . */ -.scopes-content .toggle-map-scopes { - border-bottom: 1px solid var(--theme-splitter-color); - margin-bottom: 3px; - margin-left: 10px; - padding: 0.5em 0; -} - -.scopes-content .toggle-map-scopes input { - padding-inline-start: 2px; - margin-inline-start: 0; - vertical-align: text-bottom; -} + .secondary-panes .map-scopes-header { + padding-inline-end: 3px; + } -.scopes-content .toggle-map-scopes-label { - padding-inline-start: 2px; - padding-inline-end: 8px; - cursor: default; - flex-grow: 1; - -moz-user-select: none; +.secondary-panes .header-buttons .img.shortcuts { + width: 14px; + height: 14px; + /* Better vertical centering of the icon */ + margin-top: -2px; } .scopes-content .toggle-map-scopes a.mdn { - padding-inline-end: 10px; + padding-inline-start: 3px; } .scopes-content .toggle-map-scopes .img.shortcuts { background: var(--theme-comment); } -.scopes-content .toggle-map-scopes { - display: flex; - align-items: center; -} - .object-node.default-property { opacity: 0.6; } diff --git a/src/components/SecondaryPanes/Scopes.js b/src/components/SecondaryPanes/Scopes.js index 2d83dd7932..5bbd317db4 100644 --- a/src/components/SecondaryPanes/Scopes.js +++ b/src/components/SecondaryPanes/Scopes.js @@ -4,9 +4,7 @@ // @flow import React, { PureComponent } from "react"; -import { isGeneratedId } from "devtools-source-map"; import { connect } from "../../utils/connect"; -import { features } from "../../utils/prefs"; import actions from "../../actions"; import { createObjectClient } from "../../client/firefox"; @@ -15,22 +13,20 @@ import { getSelectedFrame, getGeneratedFrameScope, getOriginalFrameScope, - isPaused as getIsPaused, + getIsPaused, getPauseReason, - getMapScopes + getMapScopes, + getCurrentThread } from "../../selectors"; import { getScopes } from "../../utils/pause/scopes"; import { objectInspector } from "devtools-reps"; -import AccessibleImage from "../shared/AccessibleImage"; import type { Why } from "../../types"; import type { NamedValue } from "../../utils/pause/scopes/types"; import "./Scopes.css"; -const mdnLink = "https://developer.mozilla.org/en-US/docs/Tools/Debugger"; - const { ObjectInspector } = objectInspector; type Props = { @@ -109,30 +105,6 @@ class Scopes extends PureComponent { this.props.toggleMapScopes(); }; - renderMapScopes() { - const { selectedFrame, shouldMapScopes } = this.props; - - if (!features.mapScopes || isGeneratedId(selectedFrame.location.sourceId)) { - return null; - } - - return ( -
- e.stopPropagation() && this.onToggleMapScopes()} - /> -
- {L10N.getStr("scopes.mapScopes")} -
- - - -
- ); - } - renderScopesList() { const { isPaused, @@ -160,20 +132,6 @@ class Scopes extends PureComponent { onDOMNodeClick={grip => openElementInInspector(grip)} onInspectIconClick={grip => openElementInInspector(grip)} /> - {originalScopes && shouldMapScopes ? ( -
- -
- ) : null} ); } @@ -195,17 +153,13 @@ class Scopes extends PureComponent { } render() { - return ( -
- {this.renderMapScopes()} - {this.renderScopesList()} -
- ); + return
{this.renderScopesList()}
; } } const mapStateToProps = state => { - const selectedFrame = getSelectedFrame(state); + const thread = getCurrentThread(state); + const selectedFrame = getSelectedFrame(state, thread); const selectedSource = getSelectedSource(state); const { @@ -213,6 +167,7 @@ const mapStateToProps = state => { pending: originalPending } = getOriginalFrameScope( state, + thread, selectedSource && selectedSource.id, selectedFrame && selectedFrame.id ) || { scope: null, pending: false }; @@ -220,7 +175,11 @@ const mapStateToProps = state => { const { scope: generatedFrameScopes, pending: generatedPending - } = getGeneratedFrameScope(state, selectedFrame && selectedFrame.id) || { + } = getGeneratedFrameScope( + state, + thread, + selectedFrame && selectedFrame.id + ) || { scope: null, pending: false }; @@ -228,9 +187,9 @@ const mapStateToProps = state => { return { selectedFrame, shouldMapScopes: getMapScopes(state), - isPaused: getIsPaused(state), + isPaused: getIsPaused(state, thread), isLoading: generatedPending || originalPending, - why: getPauseReason(state), + why: getPauseReason(state, thread), originalFrameScopes, generatedFrameScopes }; diff --git a/src/components/SecondaryPanes/SecondaryPanes.css b/src/components/SecondaryPanes/SecondaryPanes.css index 89aadefc15..9686b11cd5 100644 --- a/src/components/SecondaryPanes/SecondaryPanes.css +++ b/src/components/SecondaryPanes/SecondaryPanes.css @@ -57,8 +57,3 @@ width: 20em; overflow: auto; } - -.secondary-panes input[type="checkbox"] { - margin: 0; - margin-inline-end: 4px; -} diff --git a/src/components/SecondaryPanes/Worker.js b/src/components/SecondaryPanes/Worker.js index 36c8dc1fbe..f51245d0d8 100644 --- a/src/components/SecondaryPanes/Worker.js +++ b/src/components/SecondaryPanes/Worker.js @@ -9,7 +9,7 @@ import { connect } from "../../utils/connect"; import classnames from "classnames"; import actions from "../../actions"; -import { getCurrentThread, getThreadIsPaused } from "../../selectors"; +import { getCurrentThread, getIsPaused } from "../../selectors"; import { getDisplayName, isWorker } from "../../utils/workers"; import AccessibleImage from "../shared/AccessibleImage"; @@ -59,7 +59,7 @@ export class Worker extends Component { const mapStateToProps = (state, props: Props) => ({ currentThread: getCurrentThread(state), - isPaused: getThreadIsPaused(state, props.thread.actor) + isPaused: getIsPaused(state, props.thread.actor) }); export default connect( diff --git a/src/components/SecondaryPanes/XHRBreakpoints.css b/src/components/SecondaryPanes/XHRBreakpoints.css index d53cb87faf..3e8f082933 100644 --- a/src/components/SecondaryPanes/XHRBreakpoints.css +++ b/src/components/SecondaryPanes/XHRBreakpoints.css @@ -3,7 +3,7 @@ * file, You can obtain one at . */ .xhr-input-container { - display: flex; + display: block; border: 1px solid transparent; } @@ -19,25 +19,27 @@ border: 1px solid red; } +.xhr-container label { + display: flex; +} + .xhr-input-form { display: inline-flex; width: 100%; - padding-inline-start: 20px; - padding-inline-end: 12px; + padding: 0.5em 1em 0.5em 1em; } .xhr-checkbox { margin-inline-start: 0; - margin-inline-end: 4px; } .xhr-input-url { border: 1px; - padding: 3px 0px; + padding: 0em 0.6em 0em 0.6em; flex-grow: 1; background-color: var(--theme-sidebar-background); - font-size: inherit; - line-height: 14px; + font-size: 12px; + line-height: 18px; color: var(--theme-body-color); } @@ -55,26 +57,26 @@ border-left: 4px solid transparent; width: 100%; color: var(--theme-body-color); - padding-inline-start: 16px; - padding-inline-end: 12px; + padding: 0.25em 1em; background-color: var(--theme-body-background); display: flex; align-items: center; position: relative; + min-height: var(--breakpoint-expression-height); } :root.theme-light .xhr-container:hover { - background-color: var(--search-overlays-semitransparent); + background-color: var(--theme-selection-background-hover); } :root.theme-dark .xhr-container:hover { - background-color: var(--search-overlays-semitransparent); + background-color: var(--theme-selection-background-hover); } .xhr-label-method { - line-height: 14px; + padding: 0px 2px 0px 2px; + line-height: 15px; display: inline-block; - margin-inline-end: 2px; } .xhr-input-method { @@ -94,18 +96,21 @@ text-overflow: ellipsis; overflow: hidden; padding: 0px 2px 0px 2px; - line-height: 14px; + line-height: 15px; + font-size: 11px; } .xhr-container label { flex-grow: 1; display: flex; + padding-inline-end: 36px; align-items: center; overflow-x: hidden; } .xhr-container__close-btn { - display: flex; - padding-top: 2px; - padding-bottom: 2px; + inset-inline-end: 12px; + inset-inline-start: auto; + position: absolute; + top: 8px; } diff --git a/src/components/SecondaryPanes/index.js b/src/components/SecondaryPanes/index.js index 6f53e4eebc..772e77796b 100644 --- a/src/components/SecondaryPanes/index.js +++ b/src/components/SecondaryPanes/index.js @@ -5,6 +5,7 @@ // @flow import React, { Component } from "react"; +import { isGeneratedId } from "devtools-source-map"; import { connect } from "../../utils/connect"; import { List } from "immutable"; @@ -16,9 +17,12 @@ import { getBreakpointsLoading, getExpressions, getIsWaitingOnBreak, + getMapScopes, + getSelectedFrame, getShouldPauseOnExceptions, getShouldPauseOnCaughtExceptions, - getWorkers + getWorkers, + getCurrentThread } from "../../selectors"; import AccessibleImage from "../shared/AccessibleImage"; @@ -39,7 +43,7 @@ import Scopes from "./Scopes"; import "./SecondaryPanes.css"; -import type { Expression, WorkerList } from "../../types"; +import type { Expression, Frame, WorkerList } from "../../types"; type AccordionPaneItem = { header: string, @@ -73,19 +77,25 @@ type Props = { hasFrames: boolean, horizontal: boolean, breakpoints: Object, + selectedFrame: ?Frame, breakpointsDisabled: boolean, breakpointsLoading: boolean, isWaitingOnBreak: boolean, + shouldMapScopes: boolean, shouldPauseOnExceptions: boolean, shouldPauseOnCaughtExceptions: boolean, workers: WorkerList, toggleShortcutsModal: () => void, toggleAllBreakpoints: typeof actions.toggleAllBreakpoints, + toggleMapScopes: typeof actions.toggleMapScopes, evaluateExpressions: typeof actions.evaluateExpressions, pauseOnExceptions: typeof actions.pauseOnExceptions, breakOnNext: typeof actions.breakOnNext }; +const mdnLink = + "https://developer.mozilla.org/docs/Tools/Debugger/Using_the_Debugger_map_scopes_feature?utm_source=devtools&utm_medium=debugger-map-scopes"; + class SecondaryPanes extends Component { constructor(props: Props) { super(props); @@ -207,12 +217,51 @@ class SecondaryPanes extends Component { className: "scopes-pane", component: , opened: prefs.scopesVisible, + buttons: this.getScopesButtons(), onToggle: opened => { prefs.scopesVisible = opened; } }; } + getScopesButtons() { + const { selectedFrame, shouldMapScopes } = this.props; + + if ( + !features.mapScopes || + !selectedFrame || + isGeneratedId(selectedFrame.location.sourceId) + ) { + return null; + } + + return [ +
+ + e.stopPropagation()} + title={L10N.getStr("scopes.helpTooltip.label")} + > + + +
+ ]; + } + getWatchItem(): AccordionPaneItem { return { header: L10N.getStr("watchExpressions.header"), @@ -412,17 +461,23 @@ class SecondaryPanes extends Component { } } -const mapStateToProps = state => ({ - expressions: getExpressions(state), - hasFrames: !!getTopFrame(state), - breakpoints: getBreakpointsList(state), - breakpointsDisabled: getBreakpointsDisabled(state), - breakpointsLoading: getBreakpointsLoading(state), - isWaitingOnBreak: getIsWaitingOnBreak(state), - shouldPauseOnExceptions: getShouldPauseOnExceptions(state), - shouldPauseOnCaughtExceptions: getShouldPauseOnCaughtExceptions(state), - workers: getWorkers(state) -}); +const mapStateToProps = state => { + const thread = getCurrentThread(state); + + return { + expressions: getExpressions(state), + hasFrames: !!getTopFrame(state, thread), + breakpoints: getBreakpointsList(state), + breakpointsDisabled: getBreakpointsDisabled(state), + breakpointsLoading: getBreakpointsLoading(state), + isWaitingOnBreak: getIsWaitingOnBreak(state, thread), + selectedFrame: getSelectedFrame(state, thread), + shouldMapScopes: getMapScopes(state), + shouldPauseOnExceptions: getShouldPauseOnExceptions(state), + shouldPauseOnCaughtExceptions: getShouldPauseOnCaughtExceptions(state), + workers: getWorkers(state) + }; +}; export default connect( mapStateToProps, @@ -430,6 +485,7 @@ export default connect( toggleAllBreakpoints: actions.toggleAllBreakpoints, evaluateExpressions: actions.evaluateExpressions, pauseOnExceptions: actions.pauseOnExceptions, + toggleMapScopes: actions.toggleMapScopes, breakOnNext: actions.breakOnNext } )(SecondaryPanes); diff --git a/src/components/SecondaryPanes/tests/XHRBreakpoints.spec.js b/src/components/SecondaryPanes/tests/XHRBreakpoints.spec.js index 110f8b76b5..8147ab6876 100644 --- a/src/components/SecondaryPanes/tests/XHRBreakpoints.spec.js +++ b/src/components/SecondaryPanes/tests/XHRBreakpoints.spec.js @@ -129,7 +129,7 @@ describe("XHR Breakpoints", function() { const xhrBreakpointsComponent = renderXHRBreakpointsComponent(); xhrBreakpointsComponent.find(".xhr-input-url").simulate("focus"); - const xhrInputContainer = xhrBreakpointsComponent.find( + var xhrInputContainer = xhrBreakpointsComponent.find( ".xhr-input-container" ); expect(xhrInputContainer.hasClass("focused")).toBeTruthy(); @@ -159,7 +159,7 @@ describe("XHR Breakpoints", function() { propsOverride ); xhrBreakpointsComponent.find(".xhr-input-url").simulate("focus"); - let xhrInputContainer = xhrBreakpointsComponent.find( + var xhrInputContainer = xhrBreakpointsComponent.find( ".xhr-input-container" ); expect(xhrInputContainer.hasClass("focused")).toBeTruthy(); @@ -201,7 +201,7 @@ describe("XHR Breakpoints", function() { expect(xhrBreakpointsComponent.state("clickedOnFormElement")).toBe(false); xhrBreakpointsComponent.find(".xhr-input-method").simulate("click"); - const xhrInputContainer = xhrBreakpointsComponent.find( + var xhrInputContainer = xhrBreakpointsComponent.find( ".xhr-input-container" ); expect(xhrInputContainer.hasClass("focused")).toBeTruthy(); diff --git a/src/components/ShortcutsModal.js b/src/components/ShortcutsModal.js index 6ab67bf958..e130cbbefa 100644 --- a/src/components/ShortcutsModal.js +++ b/src/components/ShortcutsModal.js @@ -46,12 +46,8 @@ export class ShortcutsModal extends Component { formatKeyShortcut(L10N.getStr("toggleBreakpoint.key")) )} {this.renderShorcutItem( - L10N.getStr("shortcuts.toggleCondPanel.breakpoint"), - formatKeyShortcut(L10N.getStr("toggleCondPanel.breakpoint.key")) - )} - {this.renderShorcutItem( - L10N.getStr("shortcuts.toggleCondPanel.logPoint"), - formatKeyShortcut(L10N.getStr("toggleCondPanel.logPoint.key")) + L10N.getStr("shortcuts.toggleCondPanel"), + formatKeyShortcut(L10N.getStr("toggleCondPanel.key")) )} ); diff --git a/src/components/WelcomeBox.css b/src/components/WelcomeBox.css index 3aa065a8e3..f405d5952a 100644 --- a/src/components/WelcomeBox.css +++ b/src/components/WelcomeBox.css @@ -6,7 +6,7 @@ position: absolute; top: var(--editor-header-height); left: 0; - bottom: var(--editor-footer-height); + bottom: 1px; width: calc(100% - 1px); padding: 50px 0 0 0; text-align: center; @@ -20,6 +20,14 @@ background-color: var(--theme-body-background); } +.welcomebox .command-bar-button { + position: absolute; + top: auto; + inset-inline-end: 0; + inset-inline-start: auto; + bottom: 0; +} + .alignlabel { display: flex; white-space: nowrap; @@ -81,3 +89,7 @@ padding: 10px 5px; display: table-cell; } + +html .welcomebox .toggle-button-end.collapsed { + bottom: 1px; +} diff --git a/src/components/WelcomeBox.js b/src/components/WelcomeBox.js index 9d1b1fc4ec..6b642aabe5 100644 --- a/src/components/WelcomeBox.js +++ b/src/components/WelcomeBox.js @@ -11,6 +11,7 @@ import actions from "../actions"; import { getPaneCollapse } from "../selectors"; import { formatKeyShortcut } from "../utils/text"; +import { PaneToggleButton } from "./shared/Button"; import type { ActiveSearchType } from "../reducers/ui"; import "./WelcomeBox.css"; @@ -25,6 +26,22 @@ type Props = { }; export class WelcomeBox extends Component { + renderToggleButton() { + const { horizontal, endPanelCollapsed, togglePaneCollapse } = this.props; + if (horizontal) { + return; + } + + return ( + + ); + } + render() { const searchSourcesShortcut = formatKeyShortcut( L10N.getStr("sources.search.key2") @@ -76,6 +93,7 @@ export class WelcomeBox extends Component {

+ {this.renderToggleButton()} ); } diff --git a/src/components/shared/Accordion.css b/src/components/shared/Accordion.css index 91d3498f16..d715f480a1 100644 --- a/src/components/shared/Accordion.css +++ b/src/components/shared/Accordion.css @@ -74,7 +74,7 @@ .accordion ._content { border-bottom: 1px solid var(--theme-splitter-color); - font-size: var(--theme-body-font-size); + font-size: 12px; } .accordion div:last-child ._content { diff --git a/src/components/shared/SearchInput.css b/src/components/shared/SearchInput.css index 1e38789d7c..1f59ede1b0 100644 --- a/src/components/shared/SearchInput.css +++ b/src/components/shared/SearchInput.css @@ -32,27 +32,18 @@ } .search-field .img.search { - --icon-mask-size: 12px; - --icon-inset-inline-start: 6px; position: absolute; z-index: 1; + inset-inline-start: 6px; top: calc(50% - 8px); - mask-size: var(--icon-mask-size); + mask-size: 12px; background-color: var(--theme-icon-dimmed-color); pointer-events: none; } .search-field.big .img.search { - --icon-mask-size: 16px; - --icon-inset-inline-start: 12px; -} - -[dir="ltr"] .search-field .img.search { - left: var(--icon-inset-inline-start); -} - -[dir="rtl"] .search-field .img.search { - right: var(--icon-inset-inline-start); + inset-inline-start: 12px; + mask-size: 16px; } .search-field .img.loader { diff --git a/src/components/test/QuickOpenModal.spec.js b/src/components/test/QuickOpenModal.spec.js index b8baceb6e6..32a697a8d5 100644 --- a/src/components/test/QuickOpenModal.spec.js +++ b/src/components/test/QuickOpenModal.spec.js @@ -636,7 +636,7 @@ describe("QuickOpenModal", () => { })); wrapper.find("SearchInput").simulate("keydown", event); expect(event.preventDefault).toHaveBeenCalled(); - expect(wrapper.state().selectedIndex).toEqual(0); + expect(wrapper.state().selectedIndex).toEqual(NaN); expect(props.selectSpecificLocation).not.toHaveBeenCalledWith(); expect(props.highlightLineRange).not.toHaveBeenCalled(); }); diff --git a/src/components/test/__snapshots__/ShortcutsModal.spec.js.snap b/src/components/test/__snapshots__/ShortcutsModal.spec.js.snap index 9986b2b3cc..944317324a 100644 --- a/src/components/test/__snapshots__/ShortcutsModal.spec.js.snap +++ b/src/components/test/__snapshots__/ShortcutsModal.spec.js.snap @@ -33,7 +33,7 @@ exports[`ShortcutsModal renders when enabled 1`] = `
  • - Edit Conditional Breakpoint + Toggle Conditional Panel
  • -
  • - - Edit Log Point - - - - Ctrl+Shift+Y - - -
  • + `; diff --git a/src/reducers/ast.js b/src/reducers/ast.js index 4ec2b06d7f..4e63ae97aa 100644 --- a/src/reducers/ast.js +++ b/src/reducers/ast.js @@ -166,15 +166,6 @@ export function getPreview(state: OuterState) { return state.ast.preview; } -export function isEmptyLineInSource( - state: OuterState, - line: number, - selectedSourceId: string -) { - const emptyLines = getEmptyLines(state, selectedSourceId); - return emptyLines && emptyLines.includes(line); -} - const emptySourceMetaData = {}; export function getSourceMetaData(state: OuterState, sourceId: string) { return state.ast.sourceMetaData[sourceId] || emptySourceMetaData; @@ -193,12 +184,4 @@ export function isLineInScope(state: OuterState, line: number) { return linesInScope && linesInScope.includes(line); } -export function getEmptyLines(state: OuterState, sourceId: string) { - if (!sourceId) { - return null; - } - - return state.ast.emptyLines[sourceId]; -} - export default update; diff --git a/src/reducers/breakpoints.js b/src/reducers/breakpoints.js index 4f86c39f15..1eed06b82b 100644 --- a/src/reducers/breakpoints.js +++ b/src/reducers/breakpoints.js @@ -13,6 +13,10 @@ import { isGeneratedId, isOriginalId } from "devtools-source-map"; import { isEqual } from "lodash"; import { makeBreakpointId } from "../utils/breakpoint"; +import { findEmptyLines } from "../utils/empty-lines"; + +// eslint-disable-next-line max-len +import { getBreakpointsList as getBreakpointsListSelector } from "../selectors/breakpoints"; import type { XHRBreakpoint, @@ -31,7 +35,8 @@ export type BreakpointsState = { breakpoints: BreakpointsMap, breakpointPositions: BreakpointPositionsMap, xhrBreakpoints: XHRBreakpointsList, - breakpointsDisabled: boolean + breakpointsDisabled: boolean, + emptyLines: { [string]: number[] } }; export function initialBreakpointsState( @@ -41,7 +46,8 @@ export function initialBreakpointsState( breakpoints: {}, xhrBreakpoints: xhrBreakpoints, breakpointPositions: {}, - breakpointsDisabled: false + breakpointsDisabled: false, + emptyLines: {} }; } @@ -111,12 +117,18 @@ function update( } case "ADD_BREAKPOINT_POSITIONS": { - const { sourceId, positions } = action; + const { source, positions } = action; + const emptyLines = findEmptyLines(source, positions); + return { ...state, breakpointPositions: { ...state.breakpointPositions, - [sourceId]: positions + [source.id]: positions + }, + emptyLines: { + ...state.emptyLines, + [source.id]: emptyLines } }; } @@ -284,7 +296,7 @@ export function getBreakpointsMap(state: OuterState): BreakpointsMap { } export function getBreakpointsList(state: OuterState): Breakpoint[] { - return (Object.values(getBreakpointsMap(state)): any); + return getBreakpointsListSelector((state: any)); } export function getBreakpointCount(state: OuterState): number { @@ -383,4 +395,21 @@ export function getBreakpointPositionsForLine( }); } +export function isEmptyLineInSource( + state: OuterState, + line: number, + selectedSourceId: string +) { + const emptyLines = getEmptyLines(state, selectedSourceId); + return emptyLines && emptyLines.includes(line); +} + +export function getEmptyLines(state: OuterState, sourceId: string) { + if (!sourceId) { + return null; + } + + return state.breakpoints.emptyLines[sourceId]; +} + export default update; diff --git a/src/reducers/expressions.js b/src/reducers/expressions.js index a791007c3e..7ca53e78f2 100644 --- a/src/reducers/expressions.js +++ b/src/reducers/expressions.js @@ -84,10 +84,6 @@ function update( case "CLEAR_EXPRESSION_ERROR": return state.set("expressionError", false); - // respond to time travel - case "TRAVEL_TO": - return travelTo(state, action); - case "AUTOCOMPLETE": const { matchProp, matches } = action.result; @@ -104,22 +100,6 @@ function update( return state; } -function travelTo(state, action) { - const { expressions } = action.data; - if (!expressions) { - return state; - } - return expressions.reduce( - (finalState, previousState) => - updateExpressionInList(finalState, previousState.input, { - input: previousState.input, - value: previousState.value, - updating: false - }), - state - ); -} - function restoreExpressions() { const exprs = prefs.expressions; if (exprs.length == 0) { diff --git a/src/reducers/pause.js b/src/reducers/pause.js index a1465dceb8..ce58981530 100644 --- a/src/reducers/pause.js +++ b/src/reducers/pause.js @@ -10,22 +10,22 @@ * @module reducers/pause */ -import { createSelector } from "reselect"; import { isGeneratedId } from "devtools-source-map"; import { prefs } from "../utils/prefs"; import { getSelectedSourceId } from "./sources"; +import { getSelectedFrame } from "../selectors/pause"; import type { OriginalScope } from "../utils/pause/mapScopes"; import type { Action } from "../actions/types"; -import type { Selector, State } from "./types"; +import type { State } from "./types"; import type { Why, Scope, SourceId, ChromeFrame, - Frame, FrameId, - MappedLocation + MappedLocation, + ThreadId } from "../types"; export type Command = @@ -65,8 +65,6 @@ type ThreadPauseState = { }, selectedFrameId: ?string, loadedObjects: Object, - shouldPauseOnExceptions: boolean, - shouldPauseOnCaughtExceptions: boolean, command: Command, lastCommand: Command, wasStepping: boolean, @@ -75,11 +73,13 @@ type ThreadPauseState = { // Pause state describing all threads. export type PauseState = { - currentThread: string, + currentThread: ThreadId, canRewind: boolean, - threads: { [string]: ThreadPauseState }, + threads: { [ThreadId]: ThreadPauseState }, skipPausing: boolean, - mapScopes: boolean + mapScopes: boolean, + shouldPauseOnExceptions: boolean, + shouldPauseOnCaughtExceptions: boolean }; export const createPauseState = (): PauseState => ({ @@ -87,7 +87,9 @@ export const createPauseState = (): PauseState => ({ threads: {}, canRewind: false, skipPausing: prefs.skipPausing, - mapScopes: prefs.mapScopes + mapScopes: prefs.mapScopes, + shouldPauseOnExceptions: prefs.pauseOnExceptions, + shouldPauseOnCaughtExceptions: prefs.pauseOnCaughtExceptions }); const resumedPauseState = { @@ -105,15 +107,13 @@ const resumedPauseState = { const createInitialPauseState = () => ({ ...resumedPauseState, isWaitingOnBreak: false, - shouldPauseOnExceptions: prefs.pauseOnExceptions, - shouldPauseOnCaughtExceptions: prefs.pauseOnCaughtExceptions, canRewind: false, command: null, lastCommand: null, previousLocation: null }); -function getThreadPauseState(state: PauseState, thread: string) { +function getThreadPauseState(state: PauseState, thread: ThreadId) { // Thread state is lazily initialized so that we don't have to keep track of // the current set of worker threads. return state.threads[thread] || createInitialPauseState(); @@ -194,9 +194,6 @@ function update( }); } - case "TRAVEL_TO": - return updateThreadState({ ...action.data.paused }); - case "MAP_SCOPES": { const { frame, status, value } = action; const selectedFrameId = frame.id; @@ -258,10 +255,11 @@ function update( // Preserving for the old debugger prefs.ignoreCaughtExceptions = !shouldPauseOnCaughtExceptions; - return updateThreadState({ + return { + ...state, shouldPauseOnExceptions, shouldPauseOnCaughtExceptions - }); + }; } case "COMMAND": @@ -350,77 +348,80 @@ function getPauseLocation(state, action) { // (right now) to type those wrapped functions. type OuterState = State; -function getCurrentPauseState(state: OuterState): ThreadPauseState { - return getThreadPauseState(state.pause, state.pause.currentThread); +export function getAllPopupObjectProperties( + state: OuterState, + thread: ThreadId +) { + return getThreadPauseState(state.pause, thread).loadedObjects; } -export const getAllPopupObjectProperties: Selector<{}> = createSelector( - getCurrentPauseState, - pauseWrapper => pauseWrapper.loadedObjects -); - -export function getPauseReason(state: OuterState): ?Why { - return getCurrentPauseState(state).why; +export function getPauseReason(state: OuterState, thread: ThreadId): ?Why { + return getThreadPauseState(state.pause, thread).why; } -export function getPauseCommand(state: OuterState): Command { - return getCurrentPauseState(state).command; +export function getPauseCommand(state: OuterState, thread: ThreadId): Command { + return getThreadPauseState(state.pause, thread).command; } -export function wasStepping(state: OuterState): boolean { - return getCurrentPauseState(state).wasStepping; +export function wasStepping(state: OuterState, thread: ThreadId): boolean { + return getThreadPauseState(state.pause, thread).wasStepping; } -export function isStepping(state: OuterState) { - return ["stepIn", "stepOver", "stepOut"].includes(getPauseCommand(state)); +export function isStepping(state: OuterState, thread: ThreadId) { + return ["stepIn", "stepOver", "stepOut"].includes( + getPauseCommand(state, thread) + ); } export function getCurrentThread(state: OuterState) { return state.pause.currentThread; } -export function getThreadIsPaused(state: OuterState, thread: string) { +export function getIsPaused(state: OuterState, thread: ThreadId) { return !!getThreadPauseState(state.pause, thread).frames; } -export function isPaused(state: OuterState) { - return !!getFrames(state); -} - -export function getIsPaused(state: OuterState) { - return !!getFrames(state); -} - -export function getPreviousPauseFrameLocation(state: OuterState) { - return getCurrentPauseState(state).previousLocation; +export function getPreviousPauseFrameLocation( + state: OuterState, + thread: ThreadId +) { + return getThreadPauseState(state.pause, thread).previousLocation; } -export function isEvaluatingExpression(state: OuterState) { - return getCurrentPauseState(state).command === "expression"; +export function isEvaluatingExpression(state: OuterState, thread: ThreadId) { + return getThreadPauseState(state.pause, thread).command === "expression"; } -export function getPopupObjectProperties(state: OuterState, objectId: string) { - return getAllPopupObjectProperties(state)[objectId]; +export function getPopupObjectProperties( + state: OuterState, + thread: ThreadId, + objectId: string +) { + return getAllPopupObjectProperties(state, thread)[objectId]; } -export function getIsWaitingOnBreak(state: OuterState) { - return getCurrentPauseState(state).isWaitingOnBreak; +export function getIsWaitingOnBreak(state: OuterState, thread: ThreadId) { + return getThreadPauseState(state.pause, thread).isWaitingOnBreak; } export function getShouldPauseOnExceptions(state: OuterState) { - return getCurrentPauseState(state).shouldPauseOnExceptions; + return state.pause.shouldPauseOnExceptions; } export function getShouldPauseOnCaughtExceptions(state: OuterState) { - return getCurrentPauseState(state).shouldPauseOnCaughtExceptions; + return state.pause.shouldPauseOnCaughtExceptions; } export function getCanRewind(state: OuterState) { return state.pause.canRewind; } -export function getFrames(state: OuterState) { - return getCurrentPauseState(state).frames; +export function getFrames(state: OuterState, thread: ThreadId) { + return getThreadPauseState(state.pause, thread).frames; +} + +export function getCurrentThreadFrames(state: OuterState) { + return getThreadPauseState(state.pause, getCurrentThread(state)).frames; } function getGeneratedFrameId(frameId: string): string { @@ -431,16 +432,21 @@ function getGeneratedFrameId(frameId: string): string { return frameId; } -export function getGeneratedFrameScope(state: OuterState, frameId: ?string) { +export function getGeneratedFrameScope( + state: OuterState, + thread: ThreadId, + frameId: ?string +) { if (!frameId) { return null; } - return getFrameScopes(state).generated[getGeneratedFrameId(frameId)]; + return getFrameScopes(state, thread).generated[getGeneratedFrameId(frameId)]; } export function getOriginalFrameScope( state: OuterState, + thread: ThreadId, sourceId: ?SourceId, frameId: ?string ): ?{ @@ -452,7 +458,9 @@ export function getOriginalFrameScope( } const isGenerated = isGeneratedId(sourceId); - const original = getFrameScopes(state).original[getGeneratedFrameId(frameId)]; + const original = getFrameScopes(state, thread).original[ + getGeneratedFrameId(frameId) + ]; if (!isGenerated && original && (original.pending || original.scope)) { return original; @@ -461,13 +469,13 @@ export function getOriginalFrameScope( return null; } -export function getFrameScopes(state: OuterState) { - return getCurrentPauseState(state).frameScopes; +export function getFrameScopes(state: OuterState, thread: ThreadId) { + return getThreadPauseState(state.pause, thread).frameScopes; } -export function getSelectedFrameBindings(state: OuterState) { - const scopes = getFrameScopes(state); - const selectedFrameId = getSelectedFrameId(state); +export function getSelectedFrameBindings(state: OuterState, thread: ThreadId) { + const scopes = getFrameScopes(state, thread); + const selectedFrameId = getSelectedFrameId(state, thread); if (!scopes || !selectedFrameId) { return null; } @@ -498,6 +506,7 @@ export function getSelectedFrameBindings(state: OuterState) { export function getFrameScope( state: OuterState, + thread: ThreadId, sourceId: ?SourceId, frameId: ?string ): ?{ @@ -505,16 +514,16 @@ export function getFrameScope( +scope: OriginalScope | Scope } { return ( - getOriginalFrameScope(state, sourceId, frameId) || - getGeneratedFrameScope(state, frameId) + getOriginalFrameScope(state, thread, sourceId, frameId) || + getGeneratedFrameScope(state, thread, frameId) ); } -export function getSelectedScope(state: OuterState) { +export function getSelectedScope(state: OuterState, thread: ThreadId) { const sourceId = getSelectedSourceId(state); - const frameId = getSelectedFrameId(state); + const frameId = getSelectedFrameId(state, thread); - const frameScope = getFrameScope(state, sourceId, frameId); + const frameScope = getFrameScope(state, thread, sourceId, frameId); if (!frameScope) { return null; } @@ -522,51 +531,40 @@ export function getSelectedScope(state: OuterState) { return frameScope.scope || null; } -export function getSelectedOriginalScope(state: OuterState) { +export function getSelectedOriginalScope(state: OuterState, thread: ThreadId) { const sourceId = getSelectedSourceId(state); - const frameId = getSelectedFrameId(state); - return getOriginalFrameScope(state, sourceId, frameId); + const frameId = getSelectedFrameId(state, thread); + return getOriginalFrameScope(state, thread, sourceId, frameId); } -export function getSelectedGeneratedScope(state: OuterState) { - const frameId = getSelectedFrameId(state); - return getGeneratedFrameScope(state, frameId); +export function getSelectedGeneratedScope(state: OuterState, thread: ThreadId) { + const frameId = getSelectedFrameId(state, thread); + return getGeneratedFrameScope(state, thread, frameId); } export function getSelectedScopeMappings( - state: OuterState + state: OuterState, + thread: ThreadId ): { [string]: string | null } | null { - const frameId = getSelectedFrameId(state); + const frameId = getSelectedFrameId(state, thread); if (!frameId) { return null; } - return getFrameScopes(state).mappings[frameId]; + return getFrameScopes(state, thread).mappings[frameId]; } -export function getSelectedFrameId(state: OuterState) { - return getCurrentPauseState(state).selectedFrameId; +export function getSelectedFrameId(state: OuterState, thread: ThreadId) { + return getThreadPauseState(state.pause, thread).selectedFrameId; } -export function getTopFrame(state: OuterState) { - const frames = getFrames(state); +export function getTopFrame(state: OuterState, thread: ThreadId) { + const frames = getFrames(state, thread); return frames && frames[0]; } -export const getSelectedFrame: Selector = createSelector( - getSelectedFrameId, - getFrames, - (selectedFrameId, frames) => { - if (!frames) { - return null; - } - - return frames.find(frame => frame.id == selectedFrameId); - } -); - export function getSkipPausing(state: OuterState) { return state.pause.skipPausing; } @@ -576,8 +574,8 @@ export function getMapScopes(state: OuterState) { } // NOTE: currently only used for chrome -export function getChromeScopes(state: OuterState) { - const frame: ?ChromeFrame = (getSelectedFrame(state): any); +export function getChromeScopes(state: OuterState, thread: ThreadId) { + const frame: ?ChromeFrame = (getSelectedFrame(state, thread): any); return frame ? frame.scopeChain : undefined; } diff --git a/src/reducers/pending-breakpoints.js b/src/reducers/pending-breakpoints.js index e3a4ac96a2..b9e505cba0 100644 --- a/src/reducers/pending-breakpoints.js +++ b/src/reducers/pending-breakpoints.js @@ -161,9 +161,12 @@ export function getPendingBreakpointsForSource( return []; } - return getPendingBreakpointList(state).filter( - pendingBreakpoint => pendingBreakpoint.location.sourceUrl === source.url - ); + return getPendingBreakpointList(state).filter(pendingBreakpoint => { + return ( + pendingBreakpoint.location.sourceUrl === source.url || + pendingBreakpoint.generatedLocation.sourceUrl == source.url + ); + }); } export default update; diff --git a/src/reducers/sources.js b/src/reducers/sources.js index d53ebf521e..7092d8ec80 100644 --- a/src/reducers/sources.js +++ b/src/reducers/sources.js @@ -16,8 +16,7 @@ import { getRelativeUrl, isGenerated, isOriginal as isOriginalSource, - isUrlExtension, - getPlainUrl + isUrlExtension } from "../utils/source"; import { originalToGeneratedId } from "devtools-source-map"; @@ -27,7 +26,7 @@ import type { Source, SourceId, SourceLocation, ThreadId } from "../types"; import type { PendingSelectedLocation, Selector } from "./types"; import type { Action, DonePromiseAction, FocusItem } from "../actions/types"; import type { LoadSourceAction } from "../actions/types/SourceAction"; -import { mapValues, uniqBy } from "lodash"; +import { mapValues, uniqBy, uniq } from "lodash"; export type SourcesMap = { [SourceId]: Source }; export type SourcesMapByThread = { [ThreadId]: SourcesMap }; @@ -35,9 +34,10 @@ export type SourcesMapByThread = { [ThreadId]: SourcesMap }; type UrlsMap = { [string]: SourceId[] }; type DisplayedSources = { [ThreadId]: { [SourceId]: boolean } }; type GetDisplayedSourcesSelector = OuterState => { [ThreadId]: SourcesMap }; -type PlainUrlsMap = { [string]: string[] }; export type SourcesState = { + epoch: number, + // All known sources. sources: SourcesMap, @@ -45,11 +45,6 @@ export type SourcesState = { // sources can have the same URL. urls: UrlsMap, - // All full URLs belonging to a given plain (query string stripped) URL. - // Query strings are only shown in the Sources tab if they are required for - // disambiguation. - plainUrls: PlainUrlsMap, - // For each thread, all sources in that thread that are under the project root // and should be shown in the editor's sources pane. displayed: DisplayedSources, @@ -64,13 +59,13 @@ export type SourcesState = { const emptySources = { sources: {}, urls: {}, - plainUrls: {}, displayed: {} }; export function initialSourcesState(): SourcesState { return { ...emptySources, + epoch: 1, selectedLocation: undefined, pendingSelectedLocation: prefs.pendingSelectedLocation, projectDirectoryRoot: prefs.projectDirectoryRoot, @@ -172,7 +167,10 @@ function update( return updateProjectDirectoryRoot(state, action.url); case "NAVIGATE": - return initialSourcesState(); + return { + ...initialSourcesState(), + epoch: state.epoch + 1 + }; case "SET_FOCUSED_SOURCE_ITEM": return { ...state, focusedItem: action.item }; @@ -187,6 +185,15 @@ function update( */ function updateSource(state: SourcesState, source: Object) { const existingSource = state.sources[source.id]; + + // If there is no existing version of the source, it means that we probably + // ended up here as a result of an async action, and the sources were cleared + // between the action starting and the source being updated. + if (!existingSource) { + // TODO: We may want to consider throwing here once we have a better + // handle on async action flow control. + return state; + } return { ...state, sources: { @@ -220,7 +227,6 @@ function addSources(state: SourcesState, sources: Source[]) { ...state, sources: { ...state.sources }, urls: { ...state.urls }, - plainUrls: { ...state.plainUrls }, displayed: { ...state.displayed } }; @@ -247,16 +253,7 @@ function addSources(state: SourcesState, sources: Source[]) { state.urls[source.url] = [...existing, source.id]; } - // 3. Update the plain url map - if (source.url) { - const plainUrl = getPlainUrl(source.url); - const existingPlainUrls = state.plainUrls[plainUrl] || []; - if (!existingPlainUrls.includes(source.url)) { - state.plainUrls[plainUrl] = [...existingPlainUrls, source.url]; - } - } - - // 4. Update the displayed actor map + // 3. Update the displayed actor map if ( underRoot(source, state.projectDirectoryRoot) && (!source.isExtension || @@ -305,19 +302,24 @@ function updateProjectDirectoryRoot(state: SourcesState, root: string) { * Update a source's loaded state fields * i.e. loadedState, text, error */ -function updateLoadedState(state, action: LoadSourceAction): SourcesState { +function updateLoadedState( + state: SourcesState, + action: LoadSourceAction +): SourcesState { const { sourceId } = action; let source; + // If there was a navigation between the time the action was started and + // completed, we don't want to update the store. + if (action.epoch !== state.epoch) { + return state; + } + if (action.status === "start") { source = { id: sourceId, loadedState: "loading" }; } else if (action.status === "error") { source = { id: sourceId, error: action.error, loadedState: "loaded" }; } else { - if (!action.value) { - return state; - } - source = { id: sourceId, text: action.value.text, @@ -369,6 +371,15 @@ function getSourceActors(state, source) { return generatedSource ? generatedSource.actors : []; } +export function getSourceThreads( + state: OuterState, + source: Source +): ThreadId[] { + return uniq( + getSourceActors(state.sources, source).map(actor => actor.thread) + ); +} + export function getSourceInSources(sources: SourcesMap, id: string): ?Source { return sources[id]; } @@ -497,14 +508,17 @@ export function hasPrettySource(state: OuterState, id: string) { export function getSourcesUrlsInSources( state: OuterState, - url: ?string + url: string ): string[] { - if (!url) { + const urls = getUrls(state); + if (!url || !urls[url]) { return []; } + const plainUrl = url.split("?")[0]; - const plainUrl = getPlainUrl(url); - return getPlainUrls(state)[plainUrl] || []; + return Object.keys(urls) + .filter(Boolean) + .filter(sourceUrl => sourceUrl.split("?")[0] === plainUrl); } export function getHasSiblingOfSameName(state: OuterState, source: ?Source) { @@ -519,12 +533,12 @@ export function getSources(state: OuterState) { return state.sources.sources; } -export function getUrls(state: OuterState) { - return state.sources.urls; +export function getSourcesEpoch(state: OuterState) { + return state.sources.epoch; } -export function getPlainUrls(state: OuterState) { - return state.sources.plainUrls; +export function getUrls(state: OuterState) { + return state.sources.urls; } export function getSourceList(state: OuterState): Source[] { diff --git a/src/selectors/breakpointSources.js b/src/selectors/breakpointSources.js index f6354a22fb..227ce881eb 100644 --- a/src/selectors/breakpointSources.js +++ b/src/selectors/breakpointSources.js @@ -13,6 +13,7 @@ import { } from "../selectors"; import { getFilename } from "../utils/source"; import { getSelectedLocation } from "../utils/source-maps"; +import { sortSelectedBreakpoints } from "../utils/breakpoint"; import type { Source, Breakpoint } from "../types"; import type { Selector, SourcesMap } from "../reducers/types"; @@ -27,12 +28,7 @@ function getBreakpointsForSource( selectedSource: ?Source, breakpoints: Breakpoint[] ) { - return breakpoints - .sort( - (a, b) => - getSelectedLocation(a, selectedSource).line - - getSelectedLocation(b, selectedSource).line - ) + return sortSelectedBreakpoints(breakpoints, selectedSource) .filter( bp => !bp.options.hidden && diff --git a/src/selectors/breakpoints.js b/src/selectors/breakpoints.js index 985e14d252..f068dedd37 100644 --- a/src/selectors/breakpoints.js +++ b/src/selectors/breakpoints.js @@ -11,6 +11,7 @@ import type { XHRBreakpointsList } from "../reducers/breakpoints"; import type { Selector } from "../reducers/types"; +import type { Breakpoint } from "../types"; type OuterState = { breakpoints: BreakpointsState }; @@ -29,3 +30,8 @@ export const shouldPauseOnAnyXHR: Selector = createSelector( return !emptyBp.disabled; } ); + +export const getBreakpointsList: Selector = createSelector( + (state: OuterState) => state.breakpoints.breakpoints, + breakpoints => (Object.values(breakpoints): any) +); diff --git a/src/selectors/getCallStackFrames.js b/src/selectors/getCallStackFrames.js index 6f1425ea9e..36e0e605ba 100644 --- a/src/selectors/getCallStackFrames.js +++ b/src/selectors/getCallStackFrames.js @@ -9,7 +9,7 @@ import { getSelectedSource, getSourceInSources } from "../reducers/sources"; -import { getFrames } from "../reducers/pause"; +import { getCurrentThreadFrames } from "../reducers/pause"; import { annotateFrames } from "../utils/pause/frames"; import { isOriginal } from "../utils/source"; import { get } from "lodash"; @@ -65,7 +65,7 @@ export function formatCallStackFrames( // eslint-disable-next-line export const getCallStackFrames: State => Frame[] = (createSelector: any)( - getFrames, + getCurrentThreadFrames, getSources, getSelectedSource, formatCallStackFrames diff --git a/src/selectors/inComponent.js b/src/selectors/inComponent.js index 5cc9f5c7a0..99ef466874 100644 --- a/src/selectors/inComponent.js +++ b/src/selectors/inComponent.js @@ -4,14 +4,15 @@ // @flow -import { getSymbols, getSource, getSelectedFrame } from "."; +import { getSymbols, getSource, getSelectedFrame, getCurrentThread } from "."; import { findClosestClass } from "../utils/ast"; import { getSourceMetaData } from "../reducers/ast"; import type { State } from "../reducers/types"; export function inComponent(state: State) { - const selectedFrame = getSelectedFrame(state); + const thread = getCurrentThread(state); + const selectedFrame = getSelectedFrame(state, thread); if (!selectedFrame) { return; } diff --git a/src/selectors/index.js b/src/selectors/index.js index b09e3a6ce8..411b675c52 100644 --- a/src/selectors/index.js +++ b/src/selectors/index.js @@ -35,10 +35,14 @@ export { export { inComponent } from "./inComponent"; export { isSelectedFrameVisible } from "./isSelectedFrameVisible"; export { getCallStackFrames } from "./getCallStackFrames"; -export { getVisibleSelectedFrame } from "./visibleSelectedFrame"; export { getBreakpointSources } from "./breakpointSources"; export { getXHRBreakpoints, shouldPauseOnAnyXHR } from "./breakpoints"; export * from "./visibleColumnBreakpoints"; +export { + getSelectedFrame, + getSelectedFrames, + getVisibleSelectedFrame +} from "./pause"; import { objectInspector } from "devtools-reps"; diff --git a/src/selectors/isSelectedFrameVisible.js b/src/selectors/isSelectedFrameVisible.js index 7742623e8d..581ce78f73 100644 --- a/src/selectors/isSelectedFrameVisible.js +++ b/src/selectors/isSelectedFrameVisible.js @@ -5,8 +5,7 @@ // @flow import { originalToGeneratedId, isOriginalId } from "devtools-source-map"; -import { getSelectedFrame } from "../reducers/pause"; -import { getSelectedLocation } from "../reducers/sources"; +import { getSelectedFrame, getSelectedLocation, getCurrentThread } from "."; import type { State } from "../reducers/types"; function getGeneratedId(sourceId) { @@ -22,8 +21,9 @@ function getGeneratedId(sourceId) { * selected. */ export function isSelectedFrameVisible(state: State) { + const thread = getCurrentThread(state); const selectedLocation = getSelectedLocation(state); - const selectedFrame = getSelectedFrame(state); + const selectedFrame = getSelectedFrame(state, thread); if (!selectedFrame || !selectedLocation) { return false; diff --git a/src/selectors/pause.js b/src/selectors/pause.js new file mode 100644 index 0000000000..62644b8c04 --- /dev/null +++ b/src/selectors/pause.js @@ -0,0 +1,57 @@ +/* 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 . */ + +import { getCurrentThread } from "../reducers/pause"; +import { getSelectedLocation } from "../reducers/sources"; + +// eslint-disable-next-line +import { getSelectedLocation as _getSelectedLocation } from "../utils/source-maps"; +import { createSelector } from "reselect"; + +import type { Frame, SourceLocation } from "../types"; +import type { Selector, State } from "../reducers/types"; + +export const getSelectedFrames: Selector<{ [string]: Frame }> = createSelector( + state => state.pause, + pauseState => { + const selectedFrames = {}; + for (const thread in pauseState.threads) { + const pausedThread = pauseState.threads[thread]; + const { selectedFrameId, frames } = pausedThread; + if (frames) { + selectedFrames[thread] = frames.find( + frame => frame.id == selectedFrameId + ); + } + } + return selectedFrames; + } +); + +export function getSelectedFrame(state: State, thread: ThreadId) { + const selectedFrames = getSelectedFrames(state); + return selectedFrames[thread]; +} + +export const getVisibleSelectedFrame: Selector = createSelector( + getSelectedLocation, + getSelectedFrames, + getCurrentThread, + (selectedLocation, selectedFrames, thread) => { + const selectedFrame = selectedFrames[thread]; + if (!selectedFrame) { + return null; + } + + const { id } = selectedFrame; + + return { + id, + location: _getSelectedLocation(selectedFrame, selectedLocation) + }; + } +); diff --git a/src/selectors/visibleColumnBreakpoints.js b/src/selectors/visibleColumnBreakpoints.js index c96df4c8fd..22740e3b5f 100644 --- a/src/selectors/visibleColumnBreakpoints.js +++ b/src/selectors/visibleColumnBreakpoints.js @@ -44,16 +44,13 @@ function contains(location: PartialPosition, range: Range) { ); } -function inViewport(viewport, location) { - return viewport && contains(location, viewport); -} - function groupBreakpoints(breakpoints, selectedSource) { if (!breakpoints) { return {}; } + const map: any = groupBy( - breakpoints, + breakpoints.filter(breakpoint => !breakpoint.options.hidden), breakpoint => getSelectedLocation(breakpoint, selectedSource).line ); @@ -97,7 +94,7 @@ function filterByLineCount(positions, selectedSource) { function filterVisible(positions, selectedSource, viewport) { return positions.filter(columnBreakpoint => { const location = getSelectedLocation(columnBreakpoint, selectedSource); - return inViewport(viewport, location); + return viewport && contains(location, viewport); }); } @@ -177,19 +174,3 @@ export function getFirstBreakpointPosition( position => getSelectedLocation(position, source).line == line ); } - -export function getFirstVisibleBreakpointPosition( - state: State, - { line }: SourceLocation -) { - const positions = getVisibleBreakpointPositions(state); - const selectedSource = getSelectedSource(state); - - if (!selectedSource || !positions) { - return; - } - - return positions.find( - position => getSelectedLocation(position, selectedSource).line == line - ); -} diff --git a/src/utils/breakpoint/breakpointPositions.js b/src/utils/breakpoint/breakpointPositions.js index b5046cb111..f4e6d5b6a5 100644 --- a/src/utils/breakpoint/breakpointPositions.js +++ b/src/utils/breakpoint/breakpointPositions.js @@ -5,19 +5,18 @@ * file, You can obtain one at . */ import { comparePosition } from "../location"; -import type { - BreakpointPositions, - SourceLocation, - Position -} from "../../types"; +import { getSelectedLocation } from "../source-maps"; +import type { BreakpointPositions, SourceLocation } from "../../types"; export function findPosition( positions: ?BreakpointPositions, - location: Position | SourceLocation + location: SourceLocation ) { if (!positions) { return null; } - return positions.find(pos => comparePosition(pos.location, location)); + return positions.find(pos => + comparePosition(getSelectedLocation(pos, location), location) + ); } diff --git a/src/utils/breakpoint/index.js b/src/utils/breakpoint/index.js index 44ea08d81d..128b7fa1ba 100644 --- a/src/utils/breakpoint/index.js +++ b/src/utils/breakpoint/index.js @@ -232,7 +232,7 @@ export function getSelectedText( export function sortSelectedBreakpoints( breakpoints: Breakpoint[], - selectedSource: Source + selectedSource: ?Source ): Breakpoint[] { return sortBy(breakpoints, [ // Priority: line number, undefined column, column number diff --git a/src/utils/dbg.js b/src/utils/dbg.js index bca5021996..5ca5f390db 100644 --- a/src/utils/dbg.js +++ b/src/utils/dbg.js @@ -46,12 +46,24 @@ function getCM() { return cm && cm.CodeMirror; } -function _formatColumnBreapoints(dbg: Object) { - console.log( - dbg.selectors.formatColumnBreakpoints( - dbg.selectors.visibleColumnBreakpoints() - ) +function formatMappedLocation(mappedLocation) { + const { location, generatedLocation } = mappedLocation; + return { + original: `(${location.line}, ${location.column})`, + generated: `(${generatedLocation.line}, ${generatedLocation.column})` + }; +} + +function formatMappedLocations(locations) { + return console.table(locations.map(loc => formatMappedLocation(loc))); +} + +function formatSelectedColumnBreakpoints(dbg) { + const positions = dbg.selectors.getBreakpointPositionsForSource( + dbg.selectors.getSelectedSource().id ); + + return formatMappedLocations(positions); } export function setupHelper(obj: Object) { @@ -73,7 +85,9 @@ export function setupHelper(obj: Object) { dumpThread: () => sendPacketToThread(dbg, { type: "dumpThread" }) }, formatters: { - visibleColumnBreakpoints: () => _formatColumnBreapoints(dbg) + mappedLocations: locations => formatMappedLocations(locations), + mappedLocation: location => formatMappedLocation(location), + selectedColumnBreakpoints: () => formatSelectedColumnBreakpoints(dbg) }, _telemetry: { events: {} diff --git a/src/utils/editor/index.js b/src/utils/editor/index.js index 6b699a5f89..84e3b3ee64 100644 --- a/src/utils/editor/index.js +++ b/src/utils/editor/index.js @@ -11,7 +11,7 @@ export * from "../ui"; export { onMouseOver } from "./token-events"; import { createEditor } from "./create-editor"; -import { shouldPrettyPrint } from "../source"; +import { shouldPrettyPrint, isOriginal } from "../source"; import { findNext, findPrev } from "./source-search"; import { isWasm, lineToWasmOffset, wasmOffsetToLine } from "../wasm"; @@ -62,6 +62,16 @@ export function shouldShowPrettyPrint(source: Source) { return shouldPrettyPrint(source); } +export function shouldShowFooter(source: ?Source, horizontal: boolean) { + if (!horizontal) { + return true; + } + if (!source) { + return false; + } + return shouldShowPrettyPrint(source) || isOriginal(source); +} + export function traverseResults( e: Event, ctx: any, @@ -152,6 +162,12 @@ function isVisible(codeMirror: any, top: number, left: number) { export function getLocationsInViewport({ codeMirror }: Object) { // Get scroll position + if (!codeMirror) { + return { + start: { line: 0, column: 0 }, + end: { line: 0, column: 0 } + }; + } const charWidth = codeMirror.defaultCharWidth(); const scrollArea = codeMirror.getScrollInfo(); const { scrollLeft } = codeMirror.doc; diff --git a/src/utils/editor/tests/editor.spec.js b/src/utils/editor/tests/editor.spec.js index 9be3cdb12c..5c10e1dac4 100644 --- a/src/utils/editor/tests/editor.spec.js +++ b/src/utils/editor/tests/editor.spec.js @@ -6,6 +6,7 @@ import { shouldShowPrettyPrint, + shouldShowFooter, traverseResults, toEditorLine, toEditorPosition, @@ -24,6 +25,15 @@ import { import { makeMockSource } from "../../test-mockup"; +function makeSource() { + return makeMockSource( + "http://example.com/index.js", + "test-id-123/originalSource", + "text/javascript", + "some text here" + ); +} + describe("shouldShowPrettyPrint", () => { it("shows pretty print for a source", () => { const source = makeMockSource( @@ -36,6 +46,20 @@ describe("shouldShowPrettyPrint", () => { }); }); +describe("shouldShowFooter", () => { + it("shows footer when not horizontal", () => { + expect(shouldShowFooter(makeSource(), false)).toEqual(true); + }); + + it("does not show footer when no source is selected", () => { + expect(shouldShowFooter(null, true)).toEqual(false); + }); + + it("shows if pretty print should show", () => { + expect(shouldShowFooter(makeSource(), true)).toEqual(true); + }); +}); + describe("traverseResults", () => { const e: any = { stopPropagation: jest.fn(), preventDefault: jest.fn() }; const ctx = {}; diff --git a/src/utils/empty-lines.js b/src/utils/empty-lines.js new file mode 100644 index 0000000000..15c922316e --- /dev/null +++ b/src/utils/empty-lines.js @@ -0,0 +1,30 @@ +/* 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 . */ + +// @flow + +import { xor, range } from "lodash"; +import { getSelectedLocation } from "./source-maps"; +import type { BreakpointPositions, Source } from "../types"; + +export function findEmptyLines( + source: Source, + breakpointPositions: BreakpointPositions +): number[] { + if (!breakpointPositions || source.isWasm) { + return []; + } + + const sourceText = source.text || ""; + const lineCount = sourceText.split("\n").length; + const sourceLines = range(1, lineCount + 1); + + const breakpointLines = breakpointPositions + .map(point => getSelectedLocation(point, source).line) + // NOTE: at the moment it is possible the location is an unmapped generated + // line which could be greater than the line count. + .filter(line => line <= lineCount); + + return xor(sourceLines, breakpointLines); +} diff --git a/src/utils/prefs.js b/src/utils/prefs.js index 7c8611fec2..2489355557 100644 --- a/src/utils/prefs.js +++ b/src/utils/prefs.js @@ -5,11 +5,11 @@ // @flow import { PrefsHelper } from "devtools-modules"; -import { isDevelopment, isTesting } from "devtools-environment"; +import { isDevelopment } from "devtools-environment"; import Services from "devtools-services"; import { asyncStoreHelper } from "./asyncStoreHelper"; -const prefsSchemaVersion = "1.0.8"; +const prefsSchemaVersion = "1.0.9"; const pref = Services.pref; if (isDevelopment()) { @@ -54,7 +54,7 @@ if (isDevelopment()) { pref("devtools.debugger.features.remove-command-bar-options", true); pref("devtools.debugger.features.code-folding", false); pref("devtools.debugger.features.outline", true); - pref("devtools.debugger.features.column-breakpoints", false); + pref("devtools.debugger.features.column-breakpoints", true); pref("devtools.debugger.features.skip-pausing", true); pref("devtools.debugger.features.component-pane", false); pref("devtools.debugger.features.autocomplete-expressions", false); @@ -132,10 +132,12 @@ export const asyncStore = asyncStoreHelper("debugger", { eventListenerBreakpoints: ["event-listener-breakpoints", []] }); -if (!isTesting && prefs.debuggerPrefsSchemaVersion !== prefsSchemaVersion) { - // clear pending Breakpoints - asyncStore.pendingBreakpoints = {}; - asyncStore.tabs = []; - asyncStore.xhrBreakpoints = []; - prefs.debuggerPrefsSchemaVersion = prefsSchemaVersion; +export function verifyPrefSchema() { + if (prefs.debuggerPrefsSchemaVersion !== prefsSchemaVersion) { + // clear pending Breakpoints + asyncStore.pendingBreakpoints = {}; + asyncStore.tabs = []; + asyncStore.xhrBreakpoints = []; + prefs.debuggerPrefsSchemaVersion = prefsSchemaVersion; + } } diff --git a/src/utils/source-maps.js b/src/utils/source-maps.js index 0090c74782..dd70b22528 100644 --- a/src/utils/source-maps.js +++ b/src/utils/source-maps.js @@ -6,7 +6,6 @@ import { isOriginalId } from "devtools-source-map"; import { getSource } from "../selectors"; -import { isGenerated } from "../utils/source"; import type { SourceLocation, MappedLocation, Source } from "../types"; import typeof SourceMaps from "../../packages/devtools-source-map/src"; @@ -104,9 +103,15 @@ export function isOriginalSource(source: ?Source) { export function getSelectedLocation( mappedLocation: MappedLocation, - selectedSource: ?Source + context: ?(Source | SourceLocation) ): SourceLocation { - return selectedSource && isGenerated(selectedSource) - ? mappedLocation.generatedLocation - : mappedLocation.location; + if (!context) { + return mappedLocation.location; + } + + // $FlowIgnore + const sourceId = context.sourceId || context.id; + return isOriginalId(sourceId) + ? mappedLocation.location + : mappedLocation.generatedLocation; } diff --git a/src/utils/source.js b/src/utils/source.js index 106ac5ce25..eb55bbdbd7 100644 --- a/src/utils/source.js +++ b/src/utils/source.js @@ -488,8 +488,3 @@ export function getSourceQueryString(source: ?Source) { export function isUrlExtension(url: string) { return /^(chrome|moz)-extension:\//.test(url); } - -export function getPlainUrl(url: string): string { - const queryStart = url.indexOf("?"); - return queryStart !== -1 ? url.slice(0, queryStart) : url; -} diff --git a/src/utils/test-head.js b/src/utils/test-head.js index 3d71d8fc0d..6567adfbbc 100644 --- a/src/utils/test-head.js +++ b/src/utils/test-head.js @@ -69,6 +69,7 @@ function makeFrame({ id, sourceId }: Object, opts: Object = {}) { id, scope: { bindings: { variables: {}, arguments: [] } }, location: { sourceId, line: 4 }, + thread: "FakeThread", ...opts }; } @@ -103,7 +104,8 @@ function makeSource(name: string, props: any = {}): Source { function makeOriginalSource(name: string, props?: Object): Source { const rv = { ...makeSourceRaw(name, props), - id: `${name}/originalSource` + id: `${name}/originalSource`, + actors: [] }; return (rv: any); } @@ -157,6 +159,33 @@ function waitForState(store: any, predicate: any): Promise { }); } +function watchForState(store: any, predicate: any): () => boolean { + let sawState = false; + const checkState = function() { + if (!sawState && predicate(store.getState())) { + sawState = true; + } + return sawState; + }; + + let unsubscribe; + if (!checkState()) { + unsubscribe = store.subscribe(() => { + if (checkState()) { + unsubscribe(); + } + }); + } + + return function read() { + if (unsubscribe) { + unsubscribe(); + } + + return sawState; + }; +} + function getTelemetryEvents(eventName: string) { return window.dbg._telemetry.events[eventName] || []; } @@ -173,5 +202,6 @@ export { makeOriginalSource, makeSymbolDeclaration, waitForState, + watchForState, getHistory }; diff --git a/src/utils/tests/empty-lines.spec.js b/src/utils/tests/empty-lines.spec.js new file mode 100644 index 0000000000..9dccb9184e --- /dev/null +++ b/src/utils/tests/empty-lines.spec.js @@ -0,0 +1,33 @@ +/* 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 . */ + +// @flow + +import { findEmptyLines } from "../empty-lines"; +import { makeSource } from "../test-head"; + +function ml(gLine) { + const generatedLocation = { line: gLine, column: 0, sourceId: "foo" }; + return { generatedLocation, location: generatedLocation }; +} + +describe("emptyLines", () => { + it("no positions", () => { + const source = makeSource("foo", { text: "\n" }); + const lines = findEmptyLines(source, []); + expect(lines).toEqual([1, 2]); + }); + + it("one position", () => { + const source = makeSource("foo", { text: "\n" }); + const lines = findEmptyLines(source, [ml(1)]); + expect(lines).toEqual([2]); + }); + + it("outside positions are not included", () => { + const source = makeSource("foo", { text: "\n" }); + const lines = findEmptyLines(source, [ml(10)]); + expect(lines).toEqual([1, 2]); + }); +}); diff --git a/src/workers/parser/getSymbols.js b/src/workers/parser/getSymbols.js index 78ba366016..6618e85e27 100644 --- a/src/workers/parser/getSymbols.js +++ b/src/workers/parser/getSymbols.js @@ -100,9 +100,15 @@ function getUniqueIdentifiers(identifiers) { } /* eslint-disable complexity */ -function extractSymbol(path: SimplePath, symbols) { +function extractSymbol(path: SimplePath, symbols, state) { if (isFunction(path)) { const name = getFunctionName(path.node, path.parent); + + if (!state.fnCounts[name]) { + state.fnCounts[name] = 0; + } + const index = state.fnCounts[name]++; + symbols.functions.push({ name, klass: inferClassName(path), @@ -112,7 +118,7 @@ function extractSymbol(path: SimplePath, symbols) { // indicates the occurence of the function in a file // e.g { name: foo, ... index: 4 } is the 4th foo function // in the file - index: symbols.functions.filter(f => f.name === name).length + index }); } @@ -251,8 +257,7 @@ function extractSymbol(path: SimplePath, symbols) { if (t.isVariableDeclarator(path)) { const nodeId = path.node.id; - const ids = getPatternIdentifiers(nodeId); - symbols.identifiers = [...symbols.identifiers, ...ids]; + symbols.identifiers.push(...getPatternIdentifiers(nodeId)); } } @@ -274,12 +279,16 @@ function extractSymbols(sourceId): SymbolDeclarations { loading: false }; + const state = { + fnCounts: Object.create(null) + }; + const ast = traverseAst(sourceId, { enter(node: Node, ancestors: TraversalAncestors) { try { const path = createSimplePath(ancestors); if (path) { - extractSymbol(path, symbols); + extractSymbol(path, symbols, state); } } catch (e) { console.error(e); diff --git a/test/mochitest/browser.ini b/test/mochitest/browser.ini index 42326569b5..8f1ee32076 100644 --- a/test/mochitest/browser.ini +++ b/test/mochitest/browser.ini @@ -593,6 +593,7 @@ support-files = examples/doc-xhr.html examples/doc-xhr-run-to-completion.html examples/doc-scroll-run-to-completion.html + examples/pretty.js examples/sum/sum.js examples/sum/sum.min.js examples/sum/sum.min.js.map @@ -606,6 +607,7 @@ support-files = examples/doc-async.html examples/doc-asm.html examples/doc-duplicate-functions.html + examples/doc-pretty.html examples/doc-sourcemapped.html examples/doc-content-script-sources.html examples/doc-scripts.html @@ -712,6 +714,7 @@ skip-if = ccov && os == 'win' # Bug 1443132 [browser_dbg-keyboard-shortcuts.js] skip-if = os == "linux" # bug 1351952 [browser_dbg-layout-changes.js] +[browser_dbg-log-points.js] [browser_dbg-outline.js] skip-if = verify [browser_dbg-outline-pretty.js] @@ -722,9 +725,10 @@ skip-if = !debug && (os == "win" && os_version == "6.1") # Bug 1456441 [browser_dbg-pause-ux.js] skip-if = os == "win" [browser_dbg-navigation.js] -skip-if = (verify && debug && (os == 'mac')) +skip-if = (verify && debug && (os == 'mac')) || (os == 'linux' && debug && bits == 64) || (os == 'mac' && debug) # Bug 1307249 [browser_dbg-minified.js] [browser_dbg-pretty-print.js] +[browser_dbg-pretty-print-breakpoints.js] [browser_dbg-pretty-print-console.js] [browser_dbg-pretty-print-paused.js] [browser_dbg-preview.js] diff --git a/test/mochitest/browser_dbg-breakpoints-actions.js b/test/mochitest/browser_dbg-breakpoints-actions.js index 97f45151c7..bf94279a86 100644 --- a/test/mochitest/browser_dbg-breakpoints-actions.js +++ b/test/mochitest/browser_dbg-breakpoints-actions.js @@ -1,11 +1,11 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ +/* 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 . */ -function openFirstBreakpointContextMenu(dbg){ - rightClickElement(dbg, "breakpointItem", 2); +function openFirstBreakpointContextMenu(dbg) { + rightClickElement(dbg, "breakpointItem", 3); } - // Tests to see if we can trigger a breakpoint action via the context menu add_task(async function() { const dbg = await initDebugger("doc-scripts.html", "simple2"); @@ -14,11 +14,14 @@ add_task(async function() { await addBreakpoint(dbg, "simple2", 3); - openFirstBreakpointContextMenu(dbg) + openFirstBreakpointContextMenu(dbg); // select "Remove breakpoint" selectContextMenuItem(dbg, selectors.breakpointContextMenu.remove); - await waitForState(dbg, state => dbg.selectors.getBreakpointCount(state) === 0); + await waitForState( + dbg, + state => dbg.selectors.getBreakpointCount(state) === 0 + ); ok("successfully removed the breakpoint"); }); @@ -39,10 +42,11 @@ add_task(async function() { // which promises get resolved. The problem seems to indicate a coverage gap // in waitUntilService(). Workaround this by only waiting for one dispatch, // though this is fragile and could break again in the future. - let dispatched = waitForDispatch(dbg, "DISABLE_BREAKPOINT", /*2*/ 1); + let dispatched = waitForDispatch(dbg, "DISABLE_BREAKPOINT", /* 2*/ 1); selectContextMenuItem(dbg, selectors.breakpointContextMenu.disableOthers); await waitForState(dbg, state => - dbg.selectors.getBreakpointsList(state) + dbg.selectors + .getBreakpointsList(state) .every(bp => (bp.location.line !== 4) === bp.disabled) ); await dispatched; @@ -63,7 +67,8 @@ add_task(async function() { dispatched = waitForDispatch(dbg, "ENABLE_BREAKPOINT", 2); selectContextMenuItem(dbg, selectors.breakpointContextMenu.enableOthers); await waitForState(dbg, state => - dbg.selectors.getBreakpointsList(state) + dbg.selectors + .getBreakpointsList(state) .every(bp => (bp.location.line === 4) === bp.disabled) ); await dispatched; @@ -73,9 +78,11 @@ add_task(async function() { // select "Remove Others" dispatched = waitForDispatch(dbg, "REMOVE_BREAKPOINT", 2); selectContextMenuItem(dbg, selectors.breakpointContextMenu.removeOthers); - await waitForState(dbg, state => - dbg.selectors.getBreakpointsList(state).length === 1 && - dbg.selectors.getBreakpointsList(state)[0].location.line === 4 + await waitForState( + dbg, + state => + dbg.selectors.getBreakpointsList(state).length === 1 && + dbg.selectors.getBreakpointsList(state)[0].location.line === 4 ); await dispatched; ok("remaining breakpoint should be on line 4"); diff --git a/test/mochitest/browser_dbg-breakpoints-cond.js b/test/mochitest/browser_dbg-breakpoints-cond.js index e6da2bba8b..8030993790 100644 --- a/test/mochitest/browser_dbg-breakpoints-cond.js +++ b/test/mochitest/browser_dbg-breakpoints-cond.js @@ -144,7 +144,7 @@ add_task(async function() { is(bp.options.condition, "1", "breakpoint is created with the condition"); assertEditorBreakpoint(dbg, 5, { hasCondition: true }); - rightClickElement(dbg, "breakpointItem", 2); + rightClickElement(dbg, "breakpointItem", 3); info('select "remove condition"'); selectContextMenuItem(dbg, selectors.breakpointContextMenu.removeCondition); await waitForBreakpointWithoutCondition(dbg, "simple2", 5); diff --git a/test/mochitest/browser_dbg-breakpoints.js b/test/mochitest/browser_dbg-breakpoints.js index c80e2a15d3..b7868d0d01 100644 --- a/test/mochitest/browser_dbg-breakpoints.js +++ b/test/mochitest/browser_dbg-breakpoints.js @@ -35,6 +35,20 @@ function enableBreakpoints(dbg, count) { return enabled; } +function every(array, predicate) { + return !array.some(item => !predicate(item)); +} + +function subset(subArray, superArray) { + return every(subArray, subItem => superArray.includes(subItem)); +} + +function assertEmptyLines(dbg, lines) { + const sourceId = dbg.selectors.getSelectedSourceId(dbg.store.getState()); + const emptyLines = dbg.selectors.getEmptyLines(dbg.store.getState(), sourceId); + ok(subset(lines, emptyLines), 'empty lines should match'); +} + // Test enabling and disabling a breakpoint using the check boxes add_task(async function() { const dbg = await initDebugger("doc-scripts.html", "simple2"); @@ -65,7 +79,9 @@ add_task(async function() { await addBreakpoint(dbg, "simple2", 3); await addBreakpoint(dbg, "simple2", 5); - rightClickElement(dbg, "breakpointItem", 2); + assertEmptyLines(dbg, [1,2]); + + rightClickElement(dbg, "breakpointItem", 3); const disableBreakpointDispatch = waitForDispatch(dbg, "DISABLE_BREAKPOINT"); selectContextMenuItem(dbg, selectors.breakpointContextMenu.disableSelf); await disableBreakpointDispatch; @@ -75,7 +91,7 @@ add_task(async function() { is(bp1.disabled, true, "first breakpoint is disabled"); is(bp2.disabled, false, "second breakpoint is enabled"); - rightClickElement(dbg, "breakpointItem", 2); + rightClickElement(dbg, "breakpointItem", 3); const enableBreakpointDispatch = waitForDispatch(dbg, "ENABLE_BREAKPOINT"); selectContextMenuItem(dbg, selectors.breakpointContextMenu.enableSelf); await enableBreakpointDispatch; diff --git a/test/mochitest/browser_dbg-call-stack.js b/test/mochitest/browser_dbg-call-stack.js index ca5690abb2..9768a0627d 100644 --- a/test/mochitest/browser_dbg-call-stack.js +++ b/test/mochitest/browser_dbg-call-stack.js @@ -4,7 +4,13 @@ // checks to see if the frame is selected and the title is correct function isFrameSelected(dbg, index, title) { const $frame = findElement(dbg, "frame", index); - const frame = dbg.selectors.getSelectedFrame(dbg.getState()); + + const { + selectors: { getSelectedFrame, getCurrentThread }, + getState, + } = dbg; + + const frame = getSelectedFrame(getState(), getCurrentThread(getState())); const elSelected = $frame.classList.contains("selected"); const titleSelected = frame.displayName == title; diff --git a/test/mochitest/browser_dbg-console-eval.js b/test/mochitest/browser_dbg-console-eval.js index dad75d9ff2..40f552e761 100644 --- a/test/mochitest/browser_dbg-console-eval.js +++ b/test/mochitest/browser_dbg-console-eval.js @@ -16,20 +16,6 @@ function waitForConsolePanelChange(dbg) { }); } -function findMessages(win, query) { - return Array.prototype.filter.call( - win.document.querySelectorAll(".message"), - e => e.innerText.includes(query) - ); -} - -async function hasMessage(dbg, msg) { - const webConsole = await dbg.toolbox.getPanel("webconsole"); - return waitFor( - async () => findMessages(webConsole._frameWindow, msg).length > 0 - ); -} - add_task(async function() { const dbg = await initDebugger("doc-scripts.html", "simple2"); @@ -44,5 +30,5 @@ add_task(async function() { selectContextMenuItem(dbg, "#node-menu-evaluate-in-console"); await waitForConsolePanelChange(dbg); - await hasMessage(dbg, "undefined"); + await hasConsoleMessage(dbg, "undefined"); }); diff --git a/test/mochitest/browser_dbg-console.js b/test/mochitest/browser_dbg-console.js index cc5d2acd52..e400562dae 100644 --- a/test/mochitest/browser_dbg-console.js +++ b/test/mochitest/browser_dbg-console.js @@ -1,3 +1,7 @@ +/* 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 . */ + add_task(async function() { Services.prefs.setBoolPref("devtools.toolbox.splitconsoleEnabled", true); const dbg = await initDebugger("doc-script-switching.html", "switching-01"); diff --git a/test/mochitest/browser_dbg-debug-line.js b/test/mochitest/browser_dbg-debug-line.js index 641a364459..ad15917f2c 100644 --- a/test/mochitest/browser_dbg-debug-line.js +++ b/test/mochitest/browser_dbg-debug-line.js @@ -36,4 +36,4 @@ add_task(async function() { 0, "Debug line no longer exists!" ); -}); +}); \ No newline at end of file diff --git a/test/mochitest/browser_dbg-editor-gutter.js b/test/mochitest/browser_dbg-editor-gutter.js index 744ea5e628..5705f41c05 100644 --- a/test/mochitest/browser_dbg-editor-gutter.js +++ b/test/mochitest/browser_dbg-editor-gutter.js @@ -16,22 +16,6 @@ function clickGutter(dbg, line) { clickElement(dbg, "gutter", line); } -function getLineEl(dbg, line) { - const lines = dbg.win.document.querySelectorAll(".CodeMirror-code > div"); - return lines[line - 1]; -} - -function assertEditorBreakpoint(dbg, line, shouldExist) { - const exists = !!getLineEl(dbg, line).querySelector(".new-breakpoint"); - ok( - exists === shouldExist, - "Breakpoint " + - (shouldExist ? "exists" : "does not exist") + - " on line " + - line - ); -} - add_task(async function() { const dbg = await initDebugger("doc-scripts.html", "simple1.js"); const { diff --git a/test/mochitest/browser_dbg-expressions-focus.js b/test/mochitest/browser_dbg-expressions-focus.js index b3129a4461..e61debf1c4 100644 --- a/test/mochitest/browser_dbg-expressions-focus.js +++ b/test/mochitest/browser_dbg-expressions-focus.js @@ -5,13 +5,17 @@ // Ensures the input is displayed and focused when "+" is clicked add_task(async function() { const dbg = await initDebugger("doc-script-switching.html"); - // Close the panel + + info(">> Close the panel"); clickElementWithSelector(dbg, ".watch-expressions-pane ._header"); - // Click + to add the new expression + + info(">> Click + to add the new expression"); clickElementWithSelector(dbg, ".watch-expressions-pane ._header .plus"); - // Ensure element gets focused + + info(">> Ensure element gets focused"); await waitForElementWithSelector(dbg, ".expression-input-container.focused"); - // Ensure the element is focused + + info(">> Ensure the element is focused"); is( dbg.win.document.activeElement.classList.contains("input-expression"), true diff --git a/test/mochitest/browser_dbg-log-points.js b/test/mochitest/browser_dbg-log-points.js new file mode 100644 index 0000000000..10e09adc3f --- /dev/null +++ b/test/mochitest/browser_dbg-log-points.js @@ -0,0 +1,29 @@ +/* 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 . */ + +/* + * Tests that log points are correctly logged to the console + */ + +add_task(async function() { + Services.prefs.setBoolPref("devtools.toolbox.splitconsoleEnabled", true); + const dbg = await initDebugger("doc-script-switching.html", "switching-01"); + + const source = findSource(dbg, "switching-01") + await selectSource(dbg, "switching-01"); + + await getDebuggerSplitConsole(dbg); + + await dbg.actions.addBreakpoint( + { line: 5, sourceId: source.id }, + { logValue: "'a', 'b', 'c'" } + ); + invokeInTab("firstCall"); + await waitForPaused(dbg); + + await hasConsoleMessage(dbg, "a b c"); + const { link, value } = await findConsoleMessage(dbg, "a b c"); + is(link, "script-switching-01.js:5:2", "logs should have the relevant link"); + is(value, "a b c", "logs should have multiple values"); +}); diff --git a/test/mochitest/browser_dbg-navigation.js b/test/mochitest/browser_dbg-navigation.js index 859b6f7300..c2e8c60266 100644 --- a/test/mochitest/browser_dbg-navigation.js +++ b/test/mochitest/browser_dbg-navigation.js @@ -21,7 +21,7 @@ const sources = [ add_task(async function() { const dbg = await initDebugger("doc-script-switching.html"); const { - selectors: { getSelectedSource, isPaused }, + selectors: { getSelectedSource, getIsPaused, getCurrentThread }, getState } = dbg; @@ -41,7 +41,7 @@ add_task(async function() { await navigate(dbg, "doc-scripts.html", ...sources); is(countSources(dbg), 5, "5 sources are loaded."); - ok(!isPaused(getState()), "Is not paused"); + ok(!getIsPaused(getState(), getCurrentThread(getState())), "Is not paused"); await navigate(dbg, "doc-scripts.html", ...sources); is(countSources(dbg), 5, "5 sources are loaded."); diff --git a/test/mochitest/browser_dbg-pause-on-next.js b/test/mochitest/browser_dbg-pause-on-next.js index 08db9a4ae8..5d64c545b7 100644 --- a/test/mochitest/browser_dbg-pause-on-next.js +++ b/test/mochitest/browser_dbg-pause-on-next.js @@ -5,9 +5,12 @@ add_task(async function() { const dbg = await initDebugger("doc-scripts.html"); + const { + selectors: { getIsWaitingOnBreak, getCurrentThread } + } = dbg; clickElement(dbg, "pause"); - await waitForState(dbg, state => dbg.selectors.getIsWaitingOnBreak(state)); + await waitForState(dbg, state => getIsWaitingOnBreak(state, getCurrentThread(state))); invokeInTab("simple"); await waitForPaused(dbg, "simple3"); diff --git a/test/mochitest/browser_dbg-pause-points.js b/test/mochitest/browser_dbg-pause-points.js index 7025d757b7..195ae83d17 100644 --- a/test/mochitest/browser_dbg-pause-points.js +++ b/test/mochitest/browser_dbg-pause-points.js @@ -18,8 +18,12 @@ async function testCase(dbg, { name, steps }) { invokeInTab(name); let locations = []; + const { + selectors: { getTopFrame, getCurrentThread } + } = dbg; + await stepOvers(dbg, steps.length, state => { - const {line, column} = dbg.selectors.getTopFrame(state).location + const {line, column} = getTopFrame(state, getCurrentThread(state)).location locations.push([line, column]); }); diff --git a/test/mochitest/browser_dbg-pretty-print-breakpoints.js b/test/mochitest/browser_dbg-pretty-print-breakpoints.js new file mode 100644 index 0000000000..0d87609471 --- /dev/null +++ b/test/mochitest/browser_dbg-pretty-print-breakpoints.js @@ -0,0 +1,21 @@ +/* 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 . */ + +// Tests that breakpoints appear when you reload a page +// with pretty-printed files. +add_task(async function() { + const dbg = await initDebugger("doc-pretty.html", "pretty.js"); + + await selectSource(dbg, "pretty.js") + await prettyPrint(dbg); + + await addBreakpoint(dbg, "pretty.js:formatted", 5); + await closeTab(dbg, "pretty.js:formatted"); + + await reload(dbg, "pretty.js"); + invokeInTab("stuff"); + + await waitForPaused(dbg); + assertEditorBreakpoint(dbg, 4, true); +}); diff --git a/test/mochitest/browser_dbg-react-app.js b/test/mochitest/browser_dbg-react-app.js index 6621af9e87..2bc9aab4e7 100644 --- a/test/mochitest/browser_dbg-react-app.js +++ b/test/mochitest/browser_dbg-react-app.js @@ -7,9 +7,14 @@ add_task(async function() { info('Test previewing an immutable Map inside of a react component') invokeInTab("clickButton"); await waitForPaused(dbg); + + const { + selectors: { getSelectedScopeMappings, getCurrentThread } + } = dbg; + await waitForState( dbg, - state => dbg.selectors.getSelectedScopeMappings(state) + state => getSelectedScopeMappings(state, getCurrentThread(state)) ); await assertPreviewTextValue(dbg, 10, 22, { diff --git a/test/mochitest/browser_dbg-scroll-run-to-completion.js b/test/mochitest/browser_dbg-scroll-run-to-completion.js index b2128c4890..d0882aea0e 100644 --- a/test/mochitest/browser_dbg-scroll-run-to-completion.js +++ b/test/mochitest/browser_dbg-scroll-run-to-completion.js @@ -8,8 +8,8 @@ add_task(async function() { await waitForPaused(dbg); assertPausedLocation(dbg); - const threadClient = dbg.toolbox.threadClient; - await checkEvaluateInTopFrame(threadClient, 'window.scrollBy(0, 10);', undefined); + const target = dbg.toolbox.target; + await checkEvaluateInTopFrame(target, 'window.scrollBy(0, 10);', undefined); // checkEvaluateInTopFrame does an implicit resume for some reason. await waitForPaused(dbg); diff --git a/test/mochitest/browser_dbg-tabs-without-urls.js b/test/mochitest/browser_dbg-tabs-without-urls.js index 697119c433..f240e99307 100644 --- a/test/mochitest/browser_dbg-tabs-without-urls.js +++ b/test/mochitest/browser_dbg-tabs-without-urls.js @@ -24,4 +24,11 @@ add_task(async function() { // Test reloading the debugger await reload(dbg, "simple1", "simple2"); is(countTabs(dbg), 2); + + // TODO: This is here to make this test less flakey because otherwise the + // test will end while the files are still loading, which will stop all + // in-progress requests causing uncaught rejections when querying + // 'getBreakpointPositionsCompressed'. + await selectSource(dbg, "simple1"); + await selectSource(dbg, "simple2"); }); diff --git a/test/mochitest/browser_dbg-windowless-workers.js b/test/mochitest/browser_dbg-windowless-workers.js index 987e2fceec..3592506769 100644 --- a/test/mochitest/browser_dbg-windowless-workers.js +++ b/test/mochitest/browser_dbg-windowless-workers.js @@ -89,6 +89,18 @@ add_task(async function() { info("Test pausing in both workers"); await addBreakpoint(dbg, "simple-worker", 10); invokeInTab("sayHello"); + + // Wait for both workers to pause. When a thread pauses the current thread + // changes, and we don't want to get confused. + const { + selectors: { getIsPaused }, + getState + } = dbg; + await waitFor(() => { + const state = getState(); + return getIsPaused(state, worker1Thread) && getIsPaused(state, worker2Thread); + }); + dbg.actions.selectThread(worker1Thread); await waitForPaused(dbg); diff --git a/test/mochitest/examples/doc-pretty.html b/test/mochitest/examples/doc-pretty.html new file mode 100644 index 0000000000..99db330793 --- /dev/null +++ b/test/mochitest/examples/doc-pretty.html @@ -0,0 +1,14 @@ + + + + + + Test a file that changes when pretty printed + + + + + + diff --git a/test/mochitest/examples/pretty.js b/test/mochitest/examples/pretty.js new file mode 100644 index 0000000000..2507e0ec01 --- /dev/null +++ b/test/mochitest/examples/pretty.js @@ -0,0 +1,7 @@ +function a() {} + +function stuff() { + a(); a(); + a(); a(); + debugger +} \ No newline at end of file diff --git a/test/mochitest/helpers.js b/test/mochitest/helpers.js index a05592b5de..e9f7f2d869 100644 --- a/test/mochitest/helpers.js +++ b/test/mochitest/helpers.js @@ -96,7 +96,7 @@ function _afterDispatchDone(store, type) { function waitForDispatch(dbg, type, eventRepeat = 1) { let count = 0; - return Task.spawn(function* () { + return Task.spawn(function*() { info(`Waiting for ${type} to dispatch ${eventRepeat} time(s)`); while (count < eventRepeat) { yield _afterDispatchDone(dbg.store, type); @@ -402,10 +402,10 @@ function assertHighlightLocation(dbg, source, line) { */ function isPaused(dbg) { const { - selectors: { isPaused }, + selectors: { getIsPaused, getCurrentThread }, getState } = dbg; - return !!isPaused(getState()); + return getIsPaused(getState(), getCurrentThread(getState())); } // Make sure the debugger is paused at a certain source ID and line. @@ -413,11 +413,11 @@ function assertPausedAtSourceAndLine(dbg, expectedSourceId, expectedLine) { assertPaused(dbg); const { - selectors: { getWorkers, getFrames }, + selectors: { getCurrentThreadFrames }, getState } = dbg; - const frames = getFrames(getState()); + const frames = getCurrentThreadFrames(getState()); ok(frames.length >= 1, "Got at least one frame"); const { sourceId, line } = frames[0].location; ok(sourceId == expectedSourceId, "Frame has correct source"); @@ -451,11 +451,11 @@ async function waitForLoadedScopes(dbg) { * @static */ async function waitForPaused(dbg, url) { - const { getSelectedScope } = dbg.selectors; + const { getSelectedScope, getCurrentThread } = dbg.selectors; await waitForState( dbg, - state => isPaused(dbg) && !!getSelectedScope(state), + state => isPaused(dbg) && !!getSelectedScope(state, getCurrentThread(state)), "paused" ); @@ -863,6 +863,11 @@ async function invokeWithBreakpoint( await invokeResult; } +function prettyPrint(dbg) { + const sourceId = dbg.selectors.getSelectedSourceId(dbg.store.getState()); + return dbg.actions.togglePrettyPrint(sourceId); +} + async function expandAllScopes(dbg) { const scopes = await waitForElement(dbg, "scopes"); const scopeElements = scopes.querySelectorAll( @@ -931,20 +936,17 @@ async function togglePauseOnExceptions( pauseOnExceptions, pauseOnCaughtExceptions ) { - const command = dbg.actions.pauseOnExceptions( + return dbg.actions.pauseOnExceptions( pauseOnExceptions, pauseOnCaughtExceptions ); - - if (!isPaused(dbg)) { - await waitForThreadEvents(dbg, "resumed"); - } - - return command; } function waitForActive(dbg) { - return waitForState(dbg, state => !dbg.selectors.isPaused(state), "active"); + const { + selectors: { getIsPaused, getCurrentThread }, + } = dbg; + return waitForState(dbg, state => !getIsPaused(state, getCurrentThread(state)), "active"); } // Helpers @@ -962,11 +964,11 @@ function waitForActive(dbg) { */ function invokeInTab(fnc, ...args) { info(`Invoking in tab: ${fnc}(${args.map(uneval).join(",")})`); - return ContentTask.spawn(gBrowser.selectedBrowser, { fnc, args }, function* ({ + return ContentTask.spawn(gBrowser.selectedBrowser, { fnc, args }, function*({ fnc, args }) { - return content.wrappedJSObject[fnc](...args); // eslint-disable-line mozilla/no-cpows-in-tests, max-len + return content.wrappedJSObject[fnc](...args); // max-len }); } @@ -1091,6 +1093,22 @@ function isVisible(outerEl, innerEl) { return visible; } +function getEditorLineEl(dbg, line) { + const lines = dbg.win.document.querySelectorAll(".CodeMirror-code > div"); + return lines[line - 1]; +} + +function assertEditorBreakpoint(dbg, line, shouldExist) { + const exists = !!getEditorLineEl(dbg, line).querySelector(".new-breakpoint"); + ok( + exists === shouldExist, + "Breakpoint " + + (shouldExist ? "exists" : "does not exist") + + " on line " + + line + ); +} + const selectors = { callStackHeader: ".call-stack-pane ._header", callStackBody: ".call-stack-pane .pane", @@ -1601,27 +1619,42 @@ function hideConsoleContextMenu(hud) { // Return a promise that resolves with the result of a thread evaluating a // string in the topmost frame. -async function evaluateInTopFrame(threadClient, text) { +async function evaluateInTopFrame(target, text) { + const threadClient = target.threadClient; + const consoleFront = await target.getFront("console"); const { frames } = await threadClient.getFrames(0, 1); ok(frames.length == 1, "Got one frame"); - const response = await threadClient.eval(frames[0].actor, text); - ok(response.type == "resumed", "Got resume response from eval"); - let rval; - await threadClient.addOneTimeListener("paused", function(event, packet) { - ok( - packet.type == "paused" && - packet.why.type == "clientEvaluated" && - "return" in packet.why.frameFinished, - "Eval returned a value" - ); - rval = packet.why.frameFinished.return; - }); - return rval.type == "undefined" ? undefined : rval; + const options = { thread: threadClient.actor, frameActor: frames[0].actor }; + const response = await consoleFront.evaluateJS(text, options); + return response.result.type == "undefined" ? undefined : response.result; } // Return a promise that resolves when a thread evaluates a string in the // topmost frame, ensuring the result matches the expected value. -async function checkEvaluateInTopFrame(threadClient, text, expected) { - const rval = await evaluateInTopFrame(threadClient, text); +async function checkEvaluateInTopFrame(target, text, expected) { + const rval = await evaluateInTopFrame(target, text); ok(rval == expected, `Eval returned ${expected}`); } + +async function findConsoleMessage(dbg, query) { + const [message,] = await findConsoleMessages(dbg, query); + const value = message.querySelector(".message-body").innerText; + const link = message.querySelector(".frame-link-source-inner").innerText; + return { value, link }; +} + +async function findConsoleMessages(dbg, query) { + const webConsole = await dbg.toolbox.getPanel("webconsole"); + const win = webConsole._frameWindow; + return Array.prototype.filter.call( + win.document.querySelectorAll(".message"), + e => e.innerText.includes(query) + ); +} + +async function hasConsoleMessage(dbg, msg) { + return waitFor(async () => { + const messages = await findConsoleMessages(dbg, msg); + return messages.length > 0; + }) +}