diff options
Diffstat (limited to 'devtools/client/debugger/src/selectors')
30 files changed, 2576 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/selectors/ast.js b/devtools/client/debugger/src/selectors/ast.js new file mode 100644 index 0000000000..f3384fdc58 --- /dev/null +++ b/devtools/client/debugger/src/selectors/ast.js @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +import { makeBreakpointId } from "../utils/breakpoint"; + +export function getSymbols(state, location) { + if (!location) { + return null; + } + if (location.source.isOriginal) { + return ( + state.ast.mutableOriginalSourcesSymbols[location.source.id]?.value || null + ); + } + if (!location.sourceActor) { + throw new Error( + "Expects a location with a source actor when passing non-original sources to getSymbols" + ); + } + return ( + state.ast.mutableSourceActorSymbols[location.sourceActor.id]?.value || null + ); +} + +export function getInScopeLines(state, location) { + return state.ast.mutableInScopeLines[makeBreakpointId(location)]?.lines; +} + +export function hasInScopeLines(state, location) { + return !!getInScopeLines(state, location); +} diff --git a/devtools/client/debugger/src/selectors/breakpointAtLocation.js b/devtools/client/debugger/src/selectors/breakpointAtLocation.js new file mode 100644 index 0000000000..c661894dbb --- /dev/null +++ b/devtools/client/debugger/src/selectors/breakpointAtLocation.js @@ -0,0 +1,121 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +import { getSelectedSource, getBreakpointPositionsForLine } from "./sources"; +import { getBreakpointsList } from "./breakpoints"; +import { isGenerated } from "../utils/source"; + +function getColumn(column, selectedSource) { + if (column) { + return column; + } + + return isGenerated(selectedSource) ? undefined : 0; +} + +function getLocation(bp, selectedSource) { + return isGenerated(selectedSource) + ? bp.generatedLocation || bp.location + : bp.location; +} + +function getBreakpointsForSource(state, selectedSource) { + const breakpoints = getBreakpointsList(state); + + return breakpoints.filter(bp => { + const location = getLocation(bp, selectedSource); + return location.sourceId === selectedSource.id; + }); +} + +function findBreakpointAtLocation( + breakpoints, + selectedSource, + { line, column } +) { + return breakpoints.find(breakpoint => { + const location = getLocation(breakpoint, selectedSource); + const sameLine = location.line === line; + if (!sameLine) { + return false; + } + + if (column === undefined) { + return true; + } + + return location.column === getColumn(column, selectedSource); + }); +} + +// returns the closest active column breakpoint +function findClosestBreakpoint(breakpoints, column) { + if (!breakpoints || !breakpoints.length) { + return null; + } + + const firstBreakpoint = breakpoints[0]; + return breakpoints.reduce((closestBp, currentBp) => { + const currentColumn = currentBp.generatedLocation.column; + const closestColumn = closestBp.generatedLocation.column; + // check that breakpoint has a column. + if (column && currentColumn && closestColumn) { + const currentDistance = Math.abs(currentColumn - column); + const closestDistance = Math.abs(closestColumn - column); + + return currentDistance < closestDistance ? currentBp : closestBp; + } + return closestBp; + }, firstBreakpoint); +} + +/* + * Finds a breakpoint at a location (line, column) of the + * selected source. + * + * This is useful for finding a breakpoint when the + * user clicks in the gutter or on a token. + */ +export function getBreakpointAtLocation(state, location) { + const selectedSource = getSelectedSource(state); + if (!selectedSource) { + throw new Error("no selectedSource"); + } + const breakpoints = getBreakpointsForSource(state, selectedSource); + + return findBreakpointAtLocation(breakpoints, selectedSource, location); +} + +export function getBreakpointsAtLine(state, line) { + const selectedSource = getSelectedSource(state); + if (!selectedSource) { + throw new Error("no selectedSource"); + } + const breakpoints = getBreakpointsForSource(state, selectedSource); + + return breakpoints.filter( + breakpoint => getLocation(breakpoint, selectedSource).line === line + ); +} + +export function getClosestBreakpoint(state, position) { + const columnBreakpoints = getBreakpointsAtLine(state, position.line); + const breakpoint = findClosestBreakpoint(columnBreakpoints, position.column); + return breakpoint; +} + +export function getClosestBreakpointPosition(state, position) { + const selectedSource = getSelectedSource(state); + if (!selectedSource) { + throw new Error("no selectedSource"); + } + + const columnBreakpoints = getBreakpointPositionsForLine( + state, + selectedSource.id, + position.line + ); + + return findClosestBreakpoint(columnBreakpoints, position.column); +} diff --git a/devtools/client/debugger/src/selectors/breakpointSources.js b/devtools/client/debugger/src/selectors/breakpointSources.js new file mode 100644 index 0000000000..6de2772521 --- /dev/null +++ b/devtools/client/debugger/src/selectors/breakpointSources.js @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +import { createSelector } from "reselect"; +import { getSelectedSource } from "./sources"; +import { getBreakpointsList } from "./breakpoints"; +import { getFilename } from "../utils/source"; +import { getSelectedLocation } from "../utils/selected-location"; + +// Returns a list of sources with their related breakpoints: +// [{ source, breakpoints: [breakpoint1, ...] }, ...] +// +// This only returns sources for which we have a visible breakpoint. +// This will return either generated or original source based on the currently +// selected source. +export const getBreakpointSources = createSelector( + getBreakpointsList, + getSelectedSource, + (breakpoints, selectedSource) => { + const visibleBreakpoints = breakpoints.filter( + bp => + !bp.options.hidden && + (bp.text || bp.originalText || bp.options.condition || bp.disabled) + ); + + const sources = new Map(); + for (const breakpoint of visibleBreakpoints) { + // Depending on the selected source, this will match the original or generated + // location of the given selected source. + const location = getSelectedLocation(breakpoint, selectedSource); + const { source } = location; + + // We may have more than one breakpoint per source, + // so use the map to have a unique entry per source. + if (!sources.has(source)) { + sources.set(source, { + source, + breakpoints: [breakpoint], + filename: getFilename(source), + }); + } else { + sources.get(source).breakpoints.push(breakpoint); + } + } + + // Returns an array of breakpoints info per source, sorted by source's filename + return [...sources.values()].sort((a, b) => + a.filename.localeCompare(b.filename) + ); + } +); diff --git a/devtools/client/debugger/src/selectors/breakpoints.js b/devtools/client/debugger/src/selectors/breakpoints.js new file mode 100644 index 0000000000..38b39f71f3 --- /dev/null +++ b/devtools/client/debugger/src/selectors/breakpoints.js @@ -0,0 +1,86 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +import { createSelector } from "reselect"; + +import { isGeneratedId } from "devtools/client/shared/source-map-loader/index"; +import { makeBreakpointId } from "../utils/breakpoint"; + +// This method is only used from the main test helper +export function getBreakpointsMap(state) { + return state.breakpoints.breakpoints; +} + +export const getBreakpointsList = createSelector( + state => state.breakpoints.breakpoints, + breakpoints => Object.values(breakpoints) +); + +export function getBreakpointCount(state) { + return getBreakpointsList(state).length; +} + +export function getBreakpoint(state, location) { + if (!location) { + return undefined; + } + + const breakpoints = getBreakpointsMap(state); + return breakpoints[makeBreakpointId(location)]; +} + +/** + * Gets the breakpoints on a line or within a range of lines + * @param {Object} state + * @param {Number} sourceId + * @param {Number|Object} lines - line or an object with a start and end range of lines + * @returns {Array} breakpoints + */ +export function getBreakpointsForSource(state, sourceId, lines) { + if (!sourceId) { + return []; + } + + const isGeneratedSource = isGeneratedId(sourceId); + const breakpoints = getBreakpointsList(state); + return breakpoints.filter(bp => { + const location = isGeneratedSource ? bp.generatedLocation : bp.location; + + if (lines) { + const isOnLineOrWithinRange = + typeof lines == "number" + ? location.line == lines + : location.line >= lines.start.line && + location.line <= lines.end.line; + return location.sourceId === sourceId && isOnLineOrWithinRange; + } + return location.sourceId === sourceId; + }); +} + +export function getHiddenBreakpoint(state) { + const breakpoints = getBreakpointsList(state); + return breakpoints.find(bp => bp.options.hidden); +} + +export function hasLogpoint(state, location) { + const breakpoint = getBreakpoint(state, location); + return breakpoint?.options.logValue; +} + +export function getXHRBreakpoints(state) { + return state.breakpoints.xhrBreakpoints; +} + +export const shouldPauseOnAnyXHR = createSelector( + getXHRBreakpoints, + xhrBreakpoints => { + const emptyBp = xhrBreakpoints.find(({ path }) => path.length === 0); + if (!emptyBp) { + return false; + } + + return !emptyBp.disabled; + } +); diff --git a/devtools/client/debugger/src/selectors/event-listeners.js b/devtools/client/debugger/src/selectors/event-listeners.js new file mode 100644 index 0000000000..dcbcd8109f --- /dev/null +++ b/devtools/client/debugger/src/selectors/event-listeners.js @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +export function getActiveEventListeners(state) { + return state.eventListenerBreakpoints.active; +} + +export function getEventListenerBreakpointTypes(state) { + return state.eventListenerBreakpoints.categories; +} + +export function getEventListenerExpanded(state) { + return state.eventListenerBreakpoints.expanded; +} + +export function shouldLogEventBreakpoints(state) { + return state.eventListenerBreakpoints.logEventBreakpoints; +} diff --git a/devtools/client/debugger/src/selectors/exceptions.js b/devtools/client/debugger/src/selectors/exceptions.js new file mode 100644 index 0000000000..30230706cd --- /dev/null +++ b/devtools/client/debugger/src/selectors/exceptions.js @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +import { createSelector } from "reselect"; +import { shallowEqual, arrayShallowEqual } from "../utils/shallow-equal"; + +import { getSelectedSource, getSourceActorsForSource } from "./"; + +export const getSelectedSourceExceptions = createSelector( + getSelectedSourceActors, + // Do not retrieve mutableExceptionsMap as it will never change and createSelector would + // prevent re-running the selector in case of modification. state.exception is the `state` + // in the reducer, which we take care of cloning in case of new exception. + state => state.exceptions, + (sourceActors, exceptionsState) => { + const { mutableExceptionsMap } = exceptionsState; + const sourceExceptions = []; + + for (const sourceActor of sourceActors) { + const exceptions = mutableExceptionsMap.get(sourceActor.id); + if (exceptions) { + sourceExceptions.push(...exceptions); + } + } + + return sourceExceptions; + }, + // Shallow compare both input and output because of arrays being possibly always + // different instance but with same content. + { + memoizeOptions: { + equalityCheck: shallowEqual, + resultEqualityCheck: arrayShallowEqual, + }, + } +); + +function getSelectedSourceActors(state) { + const selectedSource = getSelectedSource(state); + if (!selectedSource) { + return []; + } + return getSourceActorsForSource(state, selectedSource.id); +} + +export function getSelectedException(state, line, column) { + const sourceExceptions = getSelectedSourceExceptions(state); + + if (!sourceExceptions) { + return undefined; + } + + return sourceExceptions.find( + sourceExc => + sourceExc.lineNumber === line && sourceExc.columnNumber === column + ); +} diff --git a/devtools/client/debugger/src/selectors/expressions.js b/devtools/client/debugger/src/selectors/expressions.js new file mode 100644 index 0000000000..6cbe829943 --- /dev/null +++ b/devtools/client/debugger/src/selectors/expressions.js @@ -0,0 +1,34 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +import { createSelector } from "reselect"; + +const getExpressionsWrapper = state => state.expressions; + +export const getExpressions = createSelector( + getExpressionsWrapper, + expressions => expressions.expressions +); + +const getAutocompleteMatches = createSelector( + getExpressionsWrapper, + expressions => expressions.autocompleteMatches +); + +export function getExpression(state, input) { + return getExpressions(state).find(exp => exp.input == input); +} + +export function getAutocompleteMatchset(state) { + const input = state.expressions.currentAutocompleteInput; + if (!input) { + return null; + } + return getAutocompleteMatches(state)[input]; +} + +export const getExpressionError = createSelector( + getExpressionsWrapper, + expressions => expressions.expressionError +); diff --git a/devtools/client/debugger/src/selectors/getCallStackFrames.js b/devtools/client/debugger/src/selectors/getCallStackFrames.js new file mode 100644 index 0000000000..558e07aa17 --- /dev/null +++ b/devtools/client/debugger/src/selectors/getCallStackFrames.js @@ -0,0 +1,53 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +import { getSelectedSource } from "./sources"; +import { getBlackBoxRanges } from "./source-blackbox"; +import { getCurrentThreadFrames } from "./pause"; +import { annotateFrames } from "../utils/pause/frames"; +import { isFrameBlackBoxed } from "../utils/source"; +import { createSelector } from "reselect"; + +function getLocation(frame, isGeneratedSource) { + return isGeneratedSource + ? frame.generatedLocation || frame.location + : frame.location; +} + +function getSourceForFrame(frame, isGeneratedSource) { + return getLocation(frame, isGeneratedSource).source; +} + +function appendSource(frame, selectedSource) { + const isGeneratedSource = selectedSource && !selectedSource.isOriginal; + return { + ...frame, + location: getLocation(frame, isGeneratedSource), + source: getSourceForFrame(frame, isGeneratedSource), + }; +} + +export function formatCallStackFrames( + frames, + selectedSource, + blackboxedRanges +) { + if (!frames) { + return null; + } + + const formattedFrames = frames + .filter(frame => getSourceForFrame(frame)) + .map(frame => appendSource(frame, selectedSource)) + .filter(frame => !isFrameBlackBoxed(frame, blackboxedRanges)); + + return annotateFrames(formattedFrames); +} + +export const getCallStackFrames = createSelector( + getCurrentThreadFrames, + getSelectedSource, + getBlackBoxRanges, + formatCallStackFrames +); diff --git a/devtools/client/debugger/src/selectors/index.js b/devtools/client/debugger/src/selectors/index.js new file mode 100644 index 0000000000..66220ec101 --- /dev/null +++ b/devtools/client/debugger/src/selectors/index.js @@ -0,0 +1,51 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +export * from "./ast"; +export * from "./breakpoints"; +export { + getClosestBreakpoint, + getBreakpointAtLocation, + getBreakpointsAtLine, + getClosestBreakpointPosition, +} from "./breakpointAtLocation"; +export { getBreakpointSources } from "./breakpointSources"; +export * from "./event-listeners"; +export * from "./exceptions"; +export * from "./expressions"; +export { getCallStackFrames } from "./getCallStackFrames"; +export { isLineInScope } from "./isLineInScope"; +export { isSelectedFrameVisible } from "./isSelectedFrameVisible"; +export * from "./pause"; +export * from "./pending-breakpoints"; +export * from "./preview"; +export * from "./project-text-search"; +export * from "./quick-open"; +export * from "./source-actors"; +export * from "./source-blackbox"; +export * from "./sources-content"; +export * from "./sources-tree"; +export * from "./sources"; +export * from "./tabs"; +export * from "./threads"; +export * from "./ui"; +export { + getVisibleBreakpoints, + getFirstVisibleBreakpoints, +} from "./visibleBreakpoints"; +export * from "./visibleColumnBreakpoints"; + +import { objectInspector } from "devtools/client/shared/components/reps/index"; + +const { reducer } = objectInspector; + +Object.keys(reducer).forEach(function (key) { + if (key === "default" || key === "__esModule") { + return; + } + Object.defineProperty(exports, key, { + enumerable: true, + get: reducer[key], + }); +}); diff --git a/devtools/client/debugger/src/selectors/isLineInScope.js b/devtools/client/debugger/src/selectors/isLineInScope.js new file mode 100644 index 0000000000..f8ca089b81 --- /dev/null +++ b/devtools/client/debugger/src/selectors/isLineInScope.js @@ -0,0 +1,22 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +import { getInScopeLines } from "./ast"; +import { getVisibleSelectedFrame } from "./pause"; + +// Checks if a line is considered in scope +// We consider all lines in scope, if we do not have lines in scope. +export function isLineInScope(state, line) { + const frame = getVisibleSelectedFrame(state); + if (!frame) { + return false; + } + + const lines = getInScopeLines(state, frame.location); + if (!lines) { + return true; + } + + return lines.includes(line); +} diff --git a/devtools/client/debugger/src/selectors/isSelectedFrameVisible.js b/devtools/client/debugger/src/selectors/isSelectedFrameVisible.js new file mode 100644 index 0000000000..bd0dc7a456 --- /dev/null +++ b/devtools/client/debugger/src/selectors/isSelectedFrameVisible.js @@ -0,0 +1,40 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +import { + originalToGeneratedId, + isOriginalId, +} from "devtools/client/shared/source-map-loader/index"; +import { getSelectedFrame, getSelectedLocation, getCurrentThread } from "."; + +function getGeneratedId(sourceId) { + if (isOriginalId(sourceId)) { + return originalToGeneratedId(sourceId); + } + + return sourceId; +} + +/* + * Checks to if the selected frame's source is currently + * selected. + */ +export function isSelectedFrameVisible(state) { + const thread = getCurrentThread(state); + const selectedLocation = getSelectedLocation(state); + const selectedFrame = getSelectedFrame(state, thread); + + if (!selectedFrame || !selectedLocation) { + return false; + } + + if (isOriginalId(selectedLocation.sourceId)) { + return selectedLocation.sourceId === selectedFrame.location.sourceId; + } + + return ( + selectedLocation.sourceId === + getGeneratedId(selectedFrame.location.sourceId) + ); +} diff --git a/devtools/client/debugger/src/selectors/moz.build b/devtools/client/debugger/src/selectors/moz.build new file mode 100644 index 0000000000..e58b638f0f --- /dev/null +++ b/devtools/client/debugger/src/selectors/moz.build @@ -0,0 +1,35 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += [] + +CompiledModules( + "ast.js", + "breakpointAtLocation.js", + "breakpoints.js", + "breakpointSources.js", + "event-listeners.js", + "exceptions.js", + "expressions.js", + "getCallStackFrames.js", + "index.js", + "isLineInScope.js", + "isSelectedFrameVisible.js", + "pause.js", + "pending-breakpoints.js", + "preview.js", + "project-text-search.js", + "quick-open.js", + "source-actors.js", + "source-blackbox.js", + "sources-tree.js", + "sources-content.js", + "sources.js", + "tabs.js", + "threads.js", + "visibleBreakpoints.js", + "visibleColumnBreakpoints.js", + "ui.js", +) diff --git a/devtools/client/debugger/src/selectors/pause.js b/devtools/client/debugger/src/selectors/pause.js new file mode 100644 index 0000000000..61900e9f8c --- /dev/null +++ b/devtools/client/debugger/src/selectors/pause.js @@ -0,0 +1,267 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +import { getThreadPauseState } from "../reducers/pause"; +import { getSelectedSourceId, getSelectedLocation } from "./sources"; + +import { isGeneratedId } from "devtools/client/shared/source-map-loader/index"; + +// eslint-disable-next-line +import { getSelectedLocation as _getSelectedLocation } from "../utils/selected-location"; +import { createSelector } from "reselect"; + +export const getSelectedFrame = createSelector( + (state, thread) => state.pause.threads[thread], + threadPauseState => { + if (!threadPauseState) return null; + const { selectedFrameId, frames } = threadPauseState; + if (frames) { + return frames.find(frame => frame.id == selectedFrameId); + } + return null; + } +); + +export const getVisibleSelectedFrame = createSelector( + getSelectedLocation, + state => getSelectedFrame(state, getCurrentThread(state)), + (selectedLocation, selectedFrame) => { + if (!selectedFrame) { + return null; + } + + const { id, displayName } = selectedFrame; + + return { + id, + displayName, + location: _getSelectedLocation(selectedFrame, selectedLocation), + }; + } +); + +export function getContext(state) { + return state.pause.cx; +} + +export function getThreadContext(state) { + return state.pause.threadcx; +} + +export function getPauseReason(state, thread) { + return getThreadPauseState(state.pause, thread).why; +} + +export function getShouldBreakpointsPaneOpenOnPause(state, thread) { + return getThreadPauseState(state.pause, thread) + .shouldBreakpointsPaneOpenOnPause; +} + +export function getPauseCommand(state, thread) { + return getThreadPauseState(state.pause, thread).command; +} + +export function isStepping(state, thread) { + return ["stepIn", "stepOver", "stepOut"].includes( + getPauseCommand(state, thread) + ); +} + +export function getCurrentThread(state) { + return getThreadContext(state).thread; +} + +export function getIsPaused(state, thread) { + return getThreadPauseState(state.pause, thread).isPaused; +} + +export function getIsCurrentThreadPaused(state) { + return getIsPaused(state, getCurrentThread(state)); +} + +export function isEvaluatingExpression(state, thread) { + return getThreadPauseState(state.pause, thread).command === "expression"; +} + +export function getIsWaitingOnBreak(state, thread) { + return getThreadPauseState(state.pause, thread).isWaitingOnBreak; +} + +export function getShouldPauseOnExceptions(state) { + return state.pause.shouldPauseOnExceptions; +} + +export function getShouldPauseOnCaughtExceptions(state) { + return state.pause.shouldPauseOnCaughtExceptions; +} + +export function getFrames(state, thread) { + const { frames, framesLoading } = getThreadPauseState(state.pause, thread); + return framesLoading ? null : frames; +} + +export function getCurrentThreadFrames(state) { + const { frames, framesLoading } = getThreadPauseState( + state.pause, + getCurrentThread(state) + ); + return framesLoading ? null : frames; +} + +function getGeneratedFrameId(frameId) { + if (frameId.includes("-originalFrame")) { + // The mapFrames can add original stack frames -- get generated frameId. + return frameId.substr(0, frameId.lastIndexOf("-originalFrame")); + } + return frameId; +} + +export function getGeneratedFrameScope(state, thread, frameId) { + if (!frameId) { + return null; + } + + return getFrameScopes(state, thread).generated[getGeneratedFrameId(frameId)]; +} + +export function getOriginalFrameScope(state, thread, sourceId, frameId) { + if (!frameId || !sourceId) { + return null; + } + + const isGenerated = isGeneratedId(sourceId); + const original = getFrameScopes(state, thread).original[ + getGeneratedFrameId(frameId) + ]; + + if (!isGenerated && original && (original.pending || original.scope)) { + return original; + } + + return null; +} + +// This is only used by tests +export function getFrameScopes(state, thread) { + return getThreadPauseState(state.pause, thread).frameScopes; +} + +export function getSelectedFrameBindings(state, thread) { + const scopes = getFrameScopes(state, thread); + const selectedFrameId = getSelectedFrameId(state, thread); + if (!scopes || !selectedFrameId) { + return null; + } + + const frameScope = scopes.generated[selectedFrameId]; + if (!frameScope || frameScope.pending) { + return null; + } + + let currentScope = frameScope.scope; + let frameBindings = []; + while (currentScope && currentScope.type != "object") { + if (currentScope.bindings) { + const bindings = Object.keys(currentScope.bindings.variables); + const args = [].concat( + ...currentScope.bindings.arguments.map(argument => + Object.keys(argument) + ) + ); + + frameBindings = [...frameBindings, ...bindings, ...args]; + } + currentScope = currentScope.parent; + } + + return frameBindings; +} + +function getFrameScope(state, thread, sourceId, frameId) { + return ( + getOriginalFrameScope(state, thread, sourceId, frameId) || + getGeneratedFrameScope(state, thread, frameId) + ); +} + +// This is only used by tests +export function getSelectedScope(state, thread) { + const sourceId = getSelectedSourceId(state); + const frameId = getSelectedFrameId(state, thread); + + const frameScope = getFrameScope(state, thread, sourceId, frameId); + if (!frameScope) { + return null; + } + + return frameScope.scope || null; +} + +export function getSelectedOriginalScope(state, thread) { + const sourceId = getSelectedSourceId(state); + const frameId = getSelectedFrameId(state, thread); + return getOriginalFrameScope(state, thread, sourceId, frameId); +} + +export function getSelectedGeneratedScope(state, thread) { + const frameId = getSelectedFrameId(state, thread); + return getGeneratedFrameScope(state, thread, frameId); +} + +export function getSelectedScopeMappings(state, thread) { + const frameId = getSelectedFrameId(state, thread); + if (!frameId) { + return null; + } + + return getFrameScopes(state, thread).mappings[frameId]; +} + +export function getSelectedFrameId(state, thread) { + return getThreadPauseState(state.pause, thread).selectedFrameId; +} + +export function isTopFrameSelected(state, thread) { + const selectedFrameId = getSelectedFrameId(state, thread); + const topFrame = getTopFrame(state, thread); + return selectedFrameId == topFrame?.id; +} + +export function getTopFrame(state, thread) { + const frames = getFrames(state, thread); + return frames?.[0]; +} + +export function getSkipPausing(state) { + return state.pause.skipPausing; +} + +export function getHighlightedCalls(state, thread) { + return getThreadPauseState(state.pause, thread).highlightedCalls; +} + +export function isMapScopesEnabled(state) { + return state.pause.mapScopes; +} + +export function getInlinePreviews(state, thread, frameId) { + return getThreadPauseState(state.pause, thread).inlinePreview[ + getGeneratedFrameId(frameId) + ]; +} + +// This is only used by tests +export function getSelectedInlinePreviews(state) { + const thread = getCurrentThread(state); + const frameId = getSelectedFrameId(state, thread); + if (!frameId) { + return null; + } + + return getInlinePreviews(state, thread, frameId); +} + +export function getLastExpandedScopes(state, thread) { + return getThreadPauseState(state.pause, thread).lastExpandedScopes; +} diff --git a/devtools/client/debugger/src/selectors/pending-breakpoints.js b/devtools/client/debugger/src/selectors/pending-breakpoints.js new file mode 100644 index 0000000000..a05c43477d --- /dev/null +++ b/devtools/client/debugger/src/selectors/pending-breakpoints.js @@ -0,0 +1,20 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +export function getPendingBreakpoints(state) { + return state.pendingBreakpoints; +} + +export function getPendingBreakpointList(state) { + return Object.values(getPendingBreakpoints(state)); +} + +export function getPendingBreakpointsForSource(state, source) { + return getPendingBreakpointList(state).filter(pendingBreakpoint => { + return ( + pendingBreakpoint.location.sourceUrl === source.url || + pendingBreakpoint.generatedLocation.sourceUrl == source.url + ); + }); +} diff --git a/devtools/client/debugger/src/selectors/preview.js b/devtools/client/debugger/src/selectors/preview.js new file mode 100644 index 0000000000..adfee09002 --- /dev/null +++ b/devtools/client/debugger/src/selectors/preview.js @@ -0,0 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +export function getPreview(state) { + return state.preview.preview; +} + +export function getPreviewCount(state) { + return state.preview.previewCount; +} diff --git a/devtools/client/debugger/src/selectors/project-text-search.js b/devtools/client/debugger/src/selectors/project-text-search.js new file mode 100644 index 0000000000..3679fd931f --- /dev/null +++ b/devtools/client/debugger/src/selectors/project-text-search.js @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +export function getProjectSearchOperation(state) { + return state.projectTextSearch.ongoingSearch; +} + +export function getProjectSearchResults(state) { + return state.projectTextSearch.results; +} + +export function getProjectSearchStatus(state) { + return state.projectTextSearch.status; +} + +export function getProjectSearchQuery(state) { + return state.projectTextSearch.query; +} diff --git a/devtools/client/debugger/src/selectors/quick-open.js b/devtools/client/debugger/src/selectors/quick-open.js new file mode 100644 index 0000000000..8364c7bbf3 --- /dev/null +++ b/devtools/client/debugger/src/selectors/quick-open.js @@ -0,0 +1,15 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +export function getQuickOpenEnabled(state) { + return state.quickOpen.enabled; +} + +export function getQuickOpenQuery(state) { + return state.quickOpen.query; +} + +export function getQuickOpenType(state) { + return state.quickOpen.searchType; +} diff --git a/devtools/client/debugger/src/selectors/source-actors.js b/devtools/client/debugger/src/selectors/source-actors.js new file mode 100644 index 0000000000..4d7f915da2 --- /dev/null +++ b/devtools/client/debugger/src/selectors/source-actors.js @@ -0,0 +1,111 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +/** + * Tells if a given Source Actor is registered in the redux store + * + * @param {Object} state + * @param {String} sourceActorId + * Source Actor ID + * @return {Boolean} + */ +export function hasSourceActor(state, sourceActorId) { + return state.sourceActors.mutableSourceActors.has(sourceActorId); +} + +/** + * Get the Source Actor object. See create.js:createSourceActor() + * + * @param {Object} state + * @param {String} sourceActorId + * Source Actor ID + * @return {Object} + * The Source Actor object (if registered) + */ +export function getSourceActor(state, sourceActorId) { + return state.sourceActors.mutableSourceActors.get(sourceActorId); +} + +/** + * Reports if the Source Actor relates to a valid source map / original source. + * + * @param {Object} state + * @param {String} sourceActorId + * Source Actor ID + * @return {Boolean} + * True if it has a valid source map/original object. + */ +export function isSourceActorWithSourceMap(state, sourceActorId) { + return state.sourceActors.mutableSourceActorsWithSourceMap.has(sourceActorId); +} + +// Used by threads selectors +/** + * Get all Source Actor objects for a given thread. See create.js:createSourceActor() + * + * @param {Object} state + * @param {Array<String>} threadActorIDs + * List of Thread IDs + * @return {Array<Object>} + */ +export function getSourceActorsForThread(state, threadActorIDs) { + if (!Array.isArray(threadActorIDs)) { + threadActorIDs = [threadActorIDs]; + } + const actors = []; + for (const sourceActor of state.sourceActors.mutableSourceActors.values()) { + if (threadActorIDs.includes(sourceActor.thread)) { + actors.push(sourceActor); + } + } + return actors; +} + +/** + * Get the list of all breakable lines for a given source actor. + * + * @param {Object} state + * @param {String} sourceActorId + * Source Actor ID + * @return {AsyncValue<Array<Number>>} + * List of all the breakable lines. + */ +export function getSourceActorBreakableLines(state, sourceActorId) { + return state.sourceActors.mutableBreakableLines.get(sourceActorId); +} + +// Used by sources selectors +/** + * Get the list of all breakable lines for a set of source actors. + * + * This is typically used to fetch the breakable lines of HTML sources + * which are made of multiple source actors (one per inline script). + * + * @param {Object} state + * @param {Array<String>} sourceActors + * List of Source Actors + * @param {Boolean} isHTML + * True, if we are fetching the breakable lines for an HTML source. + * For them, we have to aggregate the lines of each source actors. + * Otherwise, we might still have many source actors, but one per thread. + * In this case, we simply return the first source actor to have the lines ready. + * @return {Array<Number>} + * List of all the breakable lines. + */ +export function getBreakableLinesForSourceActors(state, sourceActors, isHTML) { + const allBreakableLines = []; + for (const sourceActor of sourceActors) { + const breakableLines = state.sourceActors.mutableBreakableLines.get( + sourceActor.id + ); + if (breakableLines) { + if (isHTML) { + allBreakableLines.push(...breakableLines); + } else { + return breakableLines; + } + } + } + return allBreakableLines; +} diff --git a/devtools/client/debugger/src/selectors/source-blackbox.js b/devtools/client/debugger/src/selectors/source-blackbox.js new file mode 100644 index 0000000000..afd5695f18 --- /dev/null +++ b/devtools/client/debugger/src/selectors/source-blackbox.js @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +export function getBlackBoxRanges(state) { + return state.sourceBlackBox.blackboxedRanges; +} + +export function isSourceBlackBoxed(state, source) { + // Only sources with a URL can be blackboxed. + if (!source.url) { + return false; + } + return state.sourceBlackBox.blackboxedSet.has(source.url); +} + +export function isSourceOnSourceMapIgnoreList(state, source) { + if (!source) { + return false; + } + return getIgnoreListSourceUrls(state).includes(source.url); +} + +export function getIgnoreListSourceUrls(state) { + return state.sourceBlackBox.sourceMapIgnoreListUrls; +} diff --git a/devtools/client/debugger/src/selectors/sources-content.js b/devtools/client/debugger/src/selectors/sources-content.js new file mode 100644 index 0000000000..b7442fb555 --- /dev/null +++ b/devtools/client/debugger/src/selectors/sources-content.js @@ -0,0 +1,48 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +import { asSettled } from "../utils/async-value"; + +import { + getSelectedLocation, + getFirstSourceActorForGeneratedSource, +} from "../selectors/sources"; + +export function getSourceTextContent(state, location) { + if (location.source.isOriginal) { + return state.sourcesContent.mutableOriginalSourceTextContentMapBySourceId.get( + location.source.id + ); + } + + let { sourceActor } = location; + if (!sourceActor) { + sourceActor = getFirstSourceActorForGeneratedSource( + state, + location.source.id + ); + } + return state.sourcesContent.mutableGeneratedSourceTextContentMapBySourceActorId.get( + sourceActor.id + ); +} + +export function getSettledSourceTextContent(state, location) { + const content = getSourceTextContent(state, location); + return asSettled(content); +} + +export function getSelectedSourceTextContent(state) { + const location = getSelectedLocation(state); + + if (!location) { + return null; + } + + return getSourceTextContent(state, location); +} + +export function getSourcesEpoch(state) { + return state.sourcesContent.epoch; +} diff --git a/devtools/client/debugger/src/selectors/sources-tree.js b/devtools/client/debugger/src/selectors/sources-tree.js new file mode 100644 index 0000000000..8ef67c93ba --- /dev/null +++ b/devtools/client/debugger/src/selectors/sources-tree.js @@ -0,0 +1,151 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +import { createSelector } from "reselect"; + +/** + * Main selector to build the SourceTree, + * but this is also the source of data for the QuickOpen dialog. + * + * If no project directory root is set, this will return the thread items. + * Otherwise this will return the items where we set the directory root. + */ +export const getSourcesTreeSources = createSelector( + getProjectDirectoryRoot, + state => state.sourcesTree.threadItems, + (projectDirectoryRoot, threadItems) => { + // Only accept thread which have their thread attribute set. + // This may come late, if we receive ADD_SOURCES before INSERT_THREAD. + // Also filter out threads which have no sources, in case we had + // INSERT_THREAD with no ADD_SOURCES. + threadItems = threadItems.filter( + item => !!item.thread && !!item.children.length + ); + + if (projectDirectoryRoot) { + const directory = getDirectoryForUniquePath( + projectDirectoryRoot, + threadItems + ); + if (directory) { + return directory.children; + } + return []; + } + + return threadItems; + } +); + +// This is used by QuickOpen UI +/** + * Main selector for the QuickOpen dialog. + * + * The returns the list of all the reducer's source objects + * that are possibly displayed in the Source Tree. + * This doesn't return Source Tree Items, but the source objects. + */ +export const getDisplayedSourcesList = createSelector( + getSourcesTreeSources, + roots => { + const sources = []; + function walk(item) { + if (item.type == "source") { + sources.push(item.source); + } else { + for (const child of item.children) { + walk(child); + } + } + } + for (const root of roots) { + walk(root); + } + return sources; + } +); + +export function getExpandedState(state) { + return state.sourcesTree.expanded; +} + +export function getFocusedSourceItem(state) { + return state.sourcesTree.focusedItem; +} + +export function getProjectDirectoryRoot(state) { + return state.sourcesTree.projectDirectoryRoot; +} + +export function getProjectDirectoryRootName(state) { + return state.sourcesTree.projectDirectoryRootName; +} + +/** + * Lookup for project root item, matching the given "unique path". + */ +function getDirectoryForUniquePath(projectRoot, threadItems) { + const sections = projectRoot.split("|"); + const thread = sections.shift(); + + const threadItem = threadItems.find(item => { + return ( + item.uniquePath == thread || + (thread == "top-level" && item.thread.isTopLevel) + ); + }); + if (!threadItem) { + dump( + `No thread item for: ${projectRoot} -- ${thread} -- ${Object.keys( + threadItems + )}\n` + ); + return null; + } + + // If we selected a thread, the project root is for a Thread Item + // and it only contains `${thread}` + if (!sections.length) { + return threadItem; + } + + const group = sections.shift(); + for (const child of threadItem.children) { + if (child.groupName != group) { + continue; + } + // In case we picked a group, return it... + // project root looked like this `${thread}|${group}` + if (!sections.length) { + return child; + } + // ..otherwise, we picked a directory, so look for it by traversing the tree + // project root looked like this `${thread}|${group}|${directoryPath}` + const path = sections.shift(); + return findPathInDirectory(child, path); + } + dump(` Unable to find group: ${group}\n`); + return null; + + function findPathInDirectory(directory, path) { + for (const child of directory.children) { + if (child.type == "directory") { + // `path` should be the absolute path from the group/domain + if (child.path == path) { + return child; + } + // Ignore folders which doesn't match the beginning of the lookup path + if (!path.startsWith(child.path)) { + continue; + } + const match = findPathInDirectory(child, path); + if (match) { + return match; + } + } + } + dump(`Unable to find directory: ${path}\n`); + return null; + } +} diff --git a/devtools/client/debugger/src/selectors/sources.js b/devtools/client/debugger/src/selectors/sources.js new file mode 100644 index 0000000000..4d36a75865 --- /dev/null +++ b/devtools/client/debugger/src/selectors/sources.js @@ -0,0 +1,358 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +import { createSelector } from "reselect"; + +import { + getPrettySourceURL, + isGenerated, + isPretty, + isJavaScript, +} from "../utils/source"; + +import { findPosition } from "../utils/breakpoint/breakpointPositions"; +import { isFulfilled } from "../utils/async-value"; + +import { originalToGeneratedId } from "devtools/client/shared/source-map-loader/index"; +import { prefs } from "../utils/prefs"; + +import { + hasSourceActor, + getSourceActor, + getBreakableLinesForSourceActors, + isSourceActorWithSourceMap, +} from "./source-actors"; +import { getSourceTextContent } from "./sources-content"; + +export function hasSource(state, id) { + return state.sources.mutableSources.has(id); +} + +export function getSource(state, id) { + return state.sources.mutableSources.get(id); +} + +export function getSourceFromId(state, id) { + const source = getSource(state, id); + if (!source) { + console.warn(`source ${id} does not exist`); + } + return source; +} + +export function getSourceByActorId(state, actorId) { + if (!hasSourceActor(state, actorId)) { + return null; + } + + return getSource(state, getSourceActor(state, actorId).source); +} + +function getSourcesByURL(state, url) { + return state.sources.mutableSourcesPerUrl.get(url) || []; +} + +export function getSourceByURL(state, url) { + const foundSources = getSourcesByURL(state, url); + return foundSources[0]; +} + +// This is used by tabs selectors +export function getSpecificSourceByURL(state, url, isOriginal) { + const foundSources = getSourcesByURL(state, url); + return foundSources.find(source => source.isOriginal == isOriginal); +} + +function getOriginalSourceByURL(state, url) { + return getSpecificSourceByURL(state, url, true); +} + +export function getGeneratedSourceByURL(state, url) { + return getSpecificSourceByURL(state, url, false); +} + +export function getGeneratedSource(state, source) { + if (!source) { + return null; + } + + if (isGenerated(source)) { + return source; + } + + return getSourceFromId(state, originalToGeneratedId(source.id)); +} + +export function getPendingSelectedLocation(state) { + return state.sources.pendingSelectedLocation; +} + +export function getPrettySource(state, id) { + if (!id) { + return null; + } + + const source = getSource(state, id); + if (!source) { + return null; + } + + return getOriginalSourceByURL(state, getPrettySourceURL(source.url)); +} + +// This is only used by Project Search and tests. +export function getSourceList(state) { + return [...state.sources.mutableSources.values()]; +} + +// This is only used by tests and create.js +export function getSourceCount(state) { + return state.sources.mutableSources.size; +} + +export function getSelectedLocation(state) { + return state.sources.selectedLocation; +} + +export const getSelectedSource = createSelector( + getSelectedLocation, + selectedLocation => { + if (!selectedLocation) { + return undefined; + } + + return selectedLocation.source; + } +); + +// This is used by tests and pause reducers +export function getSelectedSourceId(state) { + const source = getSelectedSource(state); + return source?.id; +} + +export function getShouldSelectOriginalLocation(state) { + return state.sources.shouldSelectOriginalLocation; +} + +/** + * Gets the first source actor for the source and/or thread + * provided. + * + * @param {Object} state + * @param {String} sourceId + * The source used + * @param {String} [threadId] + * The thread to check, this is optional. + * @param {Object} sourceActor + * + */ +export function getFirstSourceActorForGeneratedSource( + state, + sourceId, + threadId +) { + let source = getSource(state, sourceId); + // The source may have been removed if we are being called by async code + if (!source) { + return null; + } + if (source.isOriginal) { + source = getSource(state, originalToGeneratedId(source.id)); + } + const actors = getSourceActorsForSource(state, source.id); + if (threadId) { + return actors.find(actorInfo => actorInfo.thread == threadId) || null; + } + return actors[0] || null; +} + +/** + * Get the source actor of the source + * + * @param {Object} state + * @param {String} id + * The source id + * @return {Array<Object>} + * List of source actors + */ +export function getSourceActorsForSource(state, id) { + return state.sources.mutableSourceActors.get(id) || []; +} + +export function isSourceWithMap(state, id) { + const actors = getSourceActorsForSource(state, id); + return actors.some(actor => isSourceActorWithSourceMap(state, actor.id)); +} + +export function canPrettyPrintSource(state, location) { + const { sourceId } = location; + const source = getSource(state, sourceId); + if ( + !source || + isPretty(source) || + source.isOriginal || + (prefs.clientSourceMapsEnabled && isSourceWithMap(state, sourceId)) + ) { + return false; + } + + const content = getSourceTextContent(state, location); + const sourceContent = content && isFulfilled(content) ? content.value : null; + + if ( + !sourceContent || + (!isJavaScript(source, sourceContent) && !source.isHTML) + ) { + return false; + } + + return true; +} + +export function getPrettyPrintMessage(state, location) { + const source = location.source; + if (!source) { + return L10N.getStr("sourceTabs.prettyPrint"); + } + + if (isPretty(source)) { + return L10N.getStr("sourceFooter.prettyPrint.isPrettyPrintedMessage"); + } + + if (source.isOriginal) { + return L10N.getStr("sourceFooter.prettyPrint.isOriginalMessage"); + } + + if (prefs.clientSourceMapsEnabled && isSourceWithMap(state, source.id)) { + return L10N.getStr("sourceFooter.prettyPrint.hasSourceMapMessage"); + } + + const content = getSourceTextContent(state, location); + + const sourceContent = content && isFulfilled(content) ? content.value : null; + if (!sourceContent) { + return L10N.getStr("sourceFooter.prettyPrint.noContentMessage"); + } + + if (!isJavaScript(source, sourceContent) && !source.isHTML) { + return L10N.getStr("sourceFooter.prettyPrint.isNotJavascriptMessage"); + } + + return L10N.getStr("sourceTabs.prettyPrint"); +} + +export function getBreakpointPositionsForSource(state, sourceId) { + return state.sources.mutableBreakpointPositions.get(sourceId); +} + +// This is only used by one test +export function hasBreakpointPositions(state, sourceId) { + return !!getBreakpointPositionsForSource(state, sourceId); +} + +export function getBreakpointPositionsForLine(state, sourceId, line) { + const positions = getBreakpointPositionsForSource(state, sourceId); + return positions?.[line]; +} + +export function getBreakpointPositionsForLocation(state, location) { + const { sourceId } = location; + const positions = getBreakpointPositionsForSource(state, sourceId); + return findPosition(positions, location); +} + +export function getBreakableLines(state, sourceId) { + if (!sourceId) { + return null; + } + const source = getSource(state, sourceId); + if (!source) { + return null; + } + + if (source.isOriginal) { + return state.sources.mutableOriginalBreakableLines.get(sourceId); + } + + const sourceActors = getSourceActorsForSource(state, sourceId); + if (!sourceActors.length) { + return null; + } + + // We pull generated file breakable lines directly from the source actors + // so that breakable lines can be added as new source actors on HTML loads. + return getBreakableLinesForSourceActors(state, sourceActors, source.isHTML); +} + +export const getSelectedBreakableLines = createSelector( + state => { + const sourceId = getSelectedSourceId(state); + return sourceId && getBreakableLines(state, sourceId); + }, + breakableLines => new Set(breakableLines || []) +); + +export function isSourceOverridden(state, source) { + if (!source || !source.url) { + return false; + } + return state.sources.mutableOverrideSources.has(source.url); +} + +/** + * Compute the list of source actors and source objects to be removed + * when removing a given target/thread. + * + * @param {String} threadActorID + * The thread to be removed. + * @return {Object} + * An object with two arrays: + * - actors: list of source actor objects to remove + * - sources: list of source objects to remove + */ +export function getSourcesToRemoveForThread(state, threadActorID) { + const sourcesToRemove = []; + const actorsToRemove = []; + + for (const [ + sourceId, + actorsForSource, + ] of state.sources.mutableSourceActors.entries()) { + let removedActorsCount = 0; + // Find all actors for the current source which belongs to the given thread actor + for (const actor of actorsForSource) { + if (actor.thread == threadActorID) { + actorsToRemove.push(actor); + removedActorsCount++; + } + } + + // If we are about to remove all source actors for the current source, + // or if for some unexpected reason we have a source with no actors, + // notify the caller to also remove this source. + if ( + removedActorsCount == actorsForSource.length || + !actorsForSource.length + ) { + sourcesToRemove.push(state.sources.mutableSources.get(sourceId)); + + // Also remove any original sources related to this generated source + const originalSourceIds = + state.sources.mutableOriginalSources.get(sourceId); + if (originalSourceIds?.length > 0) { + for (const originalSourceId of originalSourceIds) { + sourcesToRemove.push( + state.sources.mutableSources.get(originalSourceId) + ); + } + } + } + } + + return { + actors: actorsToRemove, + sources: sourcesToRemove, + }; +} diff --git a/devtools/client/debugger/src/selectors/tabs.js b/devtools/client/debugger/src/selectors/tabs.js new file mode 100644 index 0000000000..de2655756e --- /dev/null +++ b/devtools/client/debugger/src/selectors/tabs.js @@ -0,0 +1,90 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +import { createSelector } from "reselect"; +import { getPrettySourceURL } from "../utils/source"; + +import { getSpecificSourceByURL } from "./sources"; +import { isOriginalId } from "devtools/client/shared/source-map-loader/index"; +import { isSimilarTab } from "../utils/tabs"; + +export const getTabs = state => state.tabs.tabs; + +// Return the list of tabs which relates to an active source +export const getSourceTabs = createSelector(getTabs, tabs => + tabs.filter(tab => tab.source) +); + +export const getSourcesForTabs = createSelector(getSourceTabs, sourceTabs => { + return sourceTabs.map(tab => tab.source); +}); + +export function tabExists(state, sourceId) { + return !!getSourceTabs(state).find(tab => tab.source.id == sourceId); +} + +export function hasPrettyTab(state, sourceUrl) { + const prettyUrl = getPrettySourceURL(sourceUrl); + return !!getSourceTabs(state).find(tab => tab.url === prettyUrl); +} + +/** + * Gets the next tab to select when a tab closes. Heuristics: + * 1. if the selected tab is available, it remains selected + * 2. if it is gone, the next available tab to the left should be active + * 3. if the first tab is active and closed, select the second tab + */ +export function getNewSelectedSource(state, tabList) { + const { selectedLocation } = state.sources; + const availableTabs = getTabs(state); + if (!selectedLocation) { + return null; + } + + const selectedSource = selectedLocation.source; + if (!selectedSource) { + return null; + } + + const matchingTab = availableTabs.find(tab => + isSimilarTab(tab, selectedSource.url, isOriginalId(selectedSource.id)) + ); + + if (matchingTab) { + const specificSelectedSource = getSpecificSourceByURL( + state, + selectedSource.url, + selectedSource.isOriginal + ); + + if (specificSelectedSource) { + return specificSelectedSource; + } + + return null; + } + + const tabUrls = tabList.map(tab => tab.url); + const leftNeighborIndex = Math.max( + tabUrls.indexOf(selectedSource.url) - 1, + 0 + ); + const lastAvailbleTabIndex = availableTabs.length - 1; + const newSelectedTabIndex = Math.min(leftNeighborIndex, lastAvailbleTabIndex); + const availableTab = availableTabs[newSelectedTabIndex]; + + if (availableTab) { + const tabSource = getSpecificSourceByURL( + state, + availableTab.url, + availableTab.isOriginal + ); + + if (tabSource) { + return tabSource; + } + } + + return null; +} diff --git a/devtools/client/debugger/src/selectors/test/__snapshots__/visibleColumnBreakpoints.spec.js.snap b/devtools/client/debugger/src/selectors/test/__snapshots__/visibleColumnBreakpoints.spec.js.snap new file mode 100644 index 0000000000..845d228d41 --- /dev/null +++ b/devtools/client/debugger/src/selectors/test/__snapshots__/visibleColumnBreakpoints.spec.js.snap @@ -0,0 +1,165 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`visible column breakpoints doesnt show breakpoints to the right 1`] = ` +Array [ + Object { + "breakpoint": Object { + "disabled": false, + "generatedLocation": Object { + "column": 1, + "line": 1, + "source": Object { + "id": "foo", + }, + "sourceId": "foo", + }, + "id": "breakpoint", + "location": Object { + "column": 1, + "line": 1, + "source": Object { + "id": "foo", + }, + "sourceId": "foo", + }, + "options": Object {}, + "originalText": "text", + "text": "text", + }, + "location": Object { + "column": 1, + "line": 1, + "sourceId": "foo", + }, + }, +] +`; + +exports[`visible column breakpoints ignores single breakpoints 1`] = ` +Array [ + Object { + "breakpoint": Object { + "disabled": false, + "generatedLocation": Object { + "column": 1, + "line": 1, + "source": Object { + "id": "foo", + }, + "sourceId": "foo", + }, + "id": "breakpoint", + "location": Object { + "column": 1, + "line": 1, + "source": Object { + "id": "foo", + }, + "sourceId": "foo", + }, + "options": Object {}, + "originalText": "text", + "text": "text", + }, + "location": Object { + "column": 1, + "line": 1, + "sourceId": "foo", + }, + }, + Object { + "breakpoint": null, + "location": Object { + "column": 3, + "line": 1, + "sourceId": "foo", + }, + }, +] +`; + +exports[`visible column breakpoints only shows visible breakpoints 1`] = ` +Array [ + Object { + "breakpoint": Object { + "disabled": false, + "generatedLocation": Object { + "column": 1, + "line": 1, + "source": Object { + "id": "foo", + }, + "sourceId": "foo", + }, + "id": "breakpoint", + "location": Object { + "column": 1, + "line": 1, + "source": Object { + "id": "foo", + }, + "sourceId": "foo", + }, + "options": Object {}, + "originalText": "text", + "text": "text", + }, + "location": Object { + "column": 1, + "line": 1, + "sourceId": "foo", + }, + }, + Object { + "breakpoint": null, + "location": Object { + "column": 3, + "line": 1, + "sourceId": "foo", + }, + }, +] +`; + +exports[`visible column breakpoints simple 1`] = ` +Array [ + Object { + "breakpoint": Object { + "disabled": false, + "generatedLocation": Object { + "column": 1, + "line": 1, + "source": Object { + "id": "foo", + }, + "sourceId": "foo", + }, + "id": "breakpoint", + "location": Object { + "column": 1, + "line": 1, + "source": Object { + "id": "foo", + }, + "sourceId": "foo", + }, + "options": Object {}, + "originalText": "text", + "text": "text", + }, + "location": Object { + "column": 1, + "line": 1, + "sourceId": "foo", + }, + }, + Object { + "breakpoint": null, + "location": Object { + "column": 5, + "line": 1, + "sourceId": "foo", + }, + }, +] +`; diff --git a/devtools/client/debugger/src/selectors/test/getCallStackFrames.spec.js b/devtools/client/debugger/src/selectors/test/getCallStackFrames.spec.js new file mode 100644 index 0000000000..7d31446e48 --- /dev/null +++ b/devtools/client/debugger/src/selectors/test/getCallStackFrames.spec.js @@ -0,0 +1,166 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +import { getCallStackFrames } from "../getCallStackFrames"; + +describe("getCallStackFrames selector", () => { + describe("library annotation", () => { + it("annotates React frames", () => { + const source1 = { id: "source1", url: "webpack:///src/App.js" }; + const source2 = { + id: "source2", + url: "webpack:///foo/node_modules/react-dom/lib/ReactCompositeComponent.js", + }; + const state = { + frames: [ + { location: { sourceId: "source1", source: source1 } }, + { location: { sourceId: "source2", source: source2 } }, + { location: { sourceId: "source2", source: source2 } }, + ], + selectedSource: { + id: "sourceId-originalSource", + isOriginal: true, + }, + }; + + const frames = getCallStackFrames.resultFunc( + state.frames, + state.selectedSource, + {} + ); + + expect(frames[0]).not.toHaveProperty("library"); + expect(frames[1]).toHaveProperty("library", "React"); + expect(frames[2]).toHaveProperty("library", "React"); + }); + + // Multiple Babel async frame groups occur when you have an async function + // calling another async function (a common case). + // + // There are two possible frame groups that can occur depending on whether + // one sets a breakpoint before or after an await + it("annotates frames related to Babel async transforms", () => { + const appSource = { id: "app", url: "webpack///app.js" }; + const bundleSource = { id: "bundle", url: "https://foo.com/bundle.js" }; + const regeneratorSource = { + id: "regenerator", + url: "webpack:///foo/node_modules/regenerator-runtime/runtime.js", + }; + const microtaskSource = { + id: "microtask", + url: "webpack:///foo/node_modules/core-js/modules/_microtask.js", + }; + const promiseSource = { + id: "promise", + url: "webpack///foo/node_modules/core-js/modules/es6.promise.js", + }; + const preAwaitGroup = [ + { + displayName: "asyncAppFunction", + location: { source: bundleSource }, + }, + { + displayName: "tryCatch", + location: { source: regeneratorSource }, + }, + { + displayName: "invoke", + location: { source: regeneratorSource }, + }, + { + displayName: "defineIteratorMethods/</prototype[method]", + location: { source: regeneratorSource }, + }, + { + displayName: "step", + location: { source: bundleSource }, + }, + { + displayName: "_asyncToGenerator/</<", + location: { source: bundleSource }, + }, + { + displayName: "Promise", + location: { source: promiseSource }, + }, + { + displayName: "_asyncToGenerator/<", + location: { source: bundleSource }, + }, + { + displayName: "asyncAppFunction", + location: { source: appSource }, + }, + ]; + + const postAwaitGroup = [ + { + displayName: "asyncAppFunction", + location: { source: bundleSource }, + }, + { + displayName: "tryCatch", + location: { source: regeneratorSource }, + }, + { + displayName: "invoke", + location: { source: regeneratorSource }, + }, + { + displayName: "defineIteratorMethods/</prototype[method]", + location: { source: regeneratorSource }, + }, + { + displayName: "step", + location: { source: bundleSource }, + }, + { + displayName: "step/<", + location: { source: bundleSource }, + }, + { + displayName: "run", + location: { source: bundleSource }, + }, + { + displayName: "notify/<", + location: { source: bundleSource }, + }, + { + displayName: "flush", + location: { source: microtaskSource }, + }, + ]; + + const state = { + frames: [...preAwaitGroup, ...postAwaitGroup], + selectedSource: { + id: "sourceId-originalSource", + isOriginal: true, + }, + }; + + const frames = getCallStackFrames.resultFunc( + state.frames, + state.selectedSource, + {} + ); + + // frames from 1-8 and 10-17 are babel frames. + const babelFrames = [...frames.slice(1, 7), ...frames.slice(10, 7)]; + const otherFrames = frames.filter(frame => !babelFrames.includes(frame)); + + expect(babelFrames).toEqual( + Array(babelFrames.length).fill( + expect.objectContaining({ library: "Babel" }) + ) + ); + expect(otherFrames).not.toEqual( + Array(babelFrames.length).fill( + expect.objectContaining({ library: "Babel" }) + ) + ); + }); + }); +}); diff --git a/devtools/client/debugger/src/selectors/test/visibleColumnBreakpoints.spec.js b/devtools/client/debugger/src/selectors/test/visibleColumnBreakpoints.spec.js new file mode 100644 index 0000000000..276851b1ef --- /dev/null +++ b/devtools/client/debugger/src/selectors/test/visibleColumnBreakpoints.spec.js @@ -0,0 +1,145 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +import { actions, createStore, makeSource } from "../../utils/test-head"; +import { createLocation } from "../../utils/location"; + +import { + getColumnBreakpoints, + getFirstBreakpointPosition, +} from "../visibleColumnBreakpoints"; +import { + makeMockSource, + makeMockSourceWithContent, + makeMockBreakpoint, +} from "../../utils/test-mockup"; + +function pp(line, column) { + return { + location: { sourceId: "foo", line, column }, + generatedLocation: { sourceId: "foo", line, column }, + }; +} + +function defaultSource() { + return makeMockSource(undefined, "foo"); +} + +function bp(line, column) { + return makeMockBreakpoint(defaultSource(), line, column); +} + +const source = makeMockSourceWithContent( + undefined, + "foo.js", + undefined, + `function foo() { + console.log("hello"); +} +console.log('bye'); +` +); + +describe("visible column breakpoints", () => { + it("simple", () => { + const viewport = { + start: { line: 1, column: 0 }, + end: { line: 10, column: 10 }, + }; + const pausePoints = [pp(1, 1), pp(1, 5), pp(3, 1)]; + const breakpoints = [bp(1, 1), bp(4, 0), bp(4, 3)]; + + const columnBps = getColumnBreakpoints( + pausePoints, + breakpoints, + viewport, + source, + source.content + ); + expect(columnBps).toMatchSnapshot(); + }); + + it("ignores single breakpoints", () => { + const viewport = { + start: { line: 1, column: 0 }, + end: { line: 10, column: 10 }, + }; + const pausePoints = [pp(1, 1), pp(1, 3), pp(2, 1)]; + const breakpoints = [bp(1, 1)]; + const columnBps = getColumnBreakpoints( + pausePoints, + breakpoints, + viewport, + source, + source.content + ); + expect(columnBps).toMatchSnapshot(); + }); + + it("only shows visible breakpoints", () => { + const viewport = { + start: { line: 1, column: 0 }, + end: { line: 10, column: 10 }, + }; + const pausePoints = [pp(1, 1), pp(1, 3), pp(20, 1)]; + const breakpoints = [bp(1, 1)]; + + const columnBps = getColumnBreakpoints( + pausePoints, + breakpoints, + viewport, + source, + source.content + ); + expect(columnBps).toMatchSnapshot(); + }); + + it("doesnt show breakpoints to the right", () => { + const viewport = { + start: { line: 1, column: 0 }, + end: { line: 10, column: 10 }, + }; + const pausePoints = [pp(1, 1), pp(1, 15), pp(20, 1)]; + const breakpoints = [bp(1, 1), bp(1, 15)]; + + const columnBps = getColumnBreakpoints( + pausePoints, + breakpoints, + viewport, + source, + source.content + ); + expect(columnBps).toMatchSnapshot(); + }); +}); + +describe("getFirstBreakpointPosition", () => { + it("sorts the positions by column", async () => { + const store = createStore(); + const { dispatch, getState } = store; + + const fooSource = await dispatch( + actions.newGeneratedSource(makeSource("foo1")) + ); + + dispatch({ + type: "ADD_BREAKPOINT_POSITIONS", + positions: [pp(1, 5), pp(1, 3)], + source: fooSource, + }); + + const position = getFirstBreakpointPosition( + getState(), + createLocation({ + line: 1, + source: fooSource, + }) + ); + + if (!position) { + throw new Error("There should be a position"); + } + expect(position.location.column).toEqual(3); + }); +}); diff --git a/devtools/client/debugger/src/selectors/threads.js b/devtools/client/debugger/src/selectors/threads.js new file mode 100644 index 0000000000..8e3054ce7a --- /dev/null +++ b/devtools/client/debugger/src/selectors/threads.js @@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +import { createSelector } from "reselect"; +import { parse } from "../utils/url"; + +export const getThreads = createSelector( + state => state.threads.threads, + threads => threads.filter(thread => !isMainThread(thread)) +); + +export const getAllThreads = createSelector( + getMainThread, + getThreads, + (mainThread, threads) => { + const orderedThreads = Array.from(threads).sort((threadA, threadB) => { + if (threadA.name === threadB.name) { + return 0; + } + return threadA.name < threadB.name ? -1 : 1; + }); + return [mainThread, ...orderedThreads].filter(Boolean); + } +); + +function isMainThread(thread) { + return thread.isTopLevel; +} + +export function getMainThread(state) { + return state.threads.threads.find(isMainThread); +} + +/* + * Gets domain from the main thread url (without www prefix) + */ +export function getMainThreadHost(state) { + const url = getMainThread(state)?.url; + if (!url) { + return null; + } + const { host } = parse(url); + if (!host) { + return null; + } + return host.startsWith("www.") ? host.substring("www.".length) : host; +} + +export function getThread(state, threadActor) { + return getAllThreads(state).find(thread => thread.actor === threadActor); +} + +export function getIsThreadCurrentlyTracing(state, thread) { + return state.threads.mutableTracingThreads.has(thread); +} diff --git a/devtools/client/debugger/src/selectors/ui.js b/devtools/client/debugger/src/selectors/ui.js new file mode 100644 index 0000000000..635a41d985 --- /dev/null +++ b/devtools/client/debugger/src/selectors/ui.js @@ -0,0 +1,85 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +import { getSelectedSource } from "./sources"; + +export function getSelectedPrimaryPaneTab(state) { + return state.ui.selectedPrimaryPaneTab; +} + +export function getActiveSearch(state) { + return state.ui.activeSearch; +} + +export function getFrameworkGroupingState(state) { + return state.ui.frameworkGroupingOn; +} + +export function getPaneCollapse(state, position) { + if (position == "start") { + return state.ui.startPanelCollapsed; + } + + return state.ui.endPanelCollapsed; +} + +export function getHighlightedLineRangeForSelectedSource(state) { + const selectedSource = getSelectedSource(state); + if (!selectedSource) { + return null; + } + // Only return the highlighted line range if it matches the selected source + const highlightedLineRange = state.ui.highlightedLineRange; + if ( + highlightedLineRange && + selectedSource.id == highlightedLineRange.sourceId + ) { + return highlightedLineRange; + } + return null; +} + +export function getConditionalPanelLocation(state) { + return state.ui.conditionalPanelLocation; +} + +export function getLogPointStatus(state) { + return state.ui.isLogPoint; +} + +export function getOrientation(state) { + return state.ui.orientation; +} + +export function getViewport(state) { + return state.ui.viewport; +} + +export function getCursorPosition(state) { + return state.ui.cursorPosition; +} + +export function getInlinePreview(state) { + return state.ui.inlinePreviewEnabled; +} + +export function getEditorWrapping(state) { + return state.ui.editorWrappingEnabled; +} + +export function getJavascriptTracingLogMethod(state) { + return state.ui.javascriptTracingLogMethod; +} + +export function getSearchOptions(state, searchKey) { + return state.ui.mutableSearchOptions[searchKey]; +} + +export function getHideIgnoredSources(state) { + return state.ui.hideIgnoredSources; +} + +export function isSourceMapIgnoreListEnabled(state) { + return state.ui.sourceMapIgnoreListEnabled; +} diff --git a/devtools/client/debugger/src/selectors/visibleBreakpoints.js b/devtools/client/debugger/src/selectors/visibleBreakpoints.js new file mode 100644 index 0000000000..2e4a8f7212 --- /dev/null +++ b/devtools/client/debugger/src/selectors/visibleBreakpoints.js @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +import { createSelector } from "reselect"; + +import { getBreakpointsList } from "./breakpoints"; +import { getSelectedSource } from "./sources"; + +import { sortSelectedBreakpoints } from "../utils/breakpoint"; +import { getSelectedLocation } from "../utils/selected-location"; + +/* + * Finds the breakpoints, which appear in the selected source. + */ +export const getVisibleBreakpoints = createSelector( + getSelectedSource, + getBreakpointsList, + (selectedSource, breakpoints) => { + if (!selectedSource) { + return null; + } + + return breakpoints.filter( + bp => + selectedSource && + getSelectedLocation(bp, selectedSource).sourceId === selectedSource.id + ); + } +); + +/* + * Finds the first breakpoint per line, which appear in the selected source. + */ +export const getFirstVisibleBreakpoints = createSelector( + getVisibleBreakpoints, + getSelectedSource, + (breakpoints, selectedSource) => { + if (!breakpoints || !selectedSource) { + return []; + } + + // Filter the array so it only return the first breakpoint when there's multiple + // breakpoints on the same line. + const handledLines = new Set(); + return sortSelectedBreakpoints(breakpoints, selectedSource).filter(bp => { + const line = getSelectedLocation(bp, selectedSource).line; + if (handledLines.has(line)) { + return false; + } + handledLines.add(line); + return true; + }); + } +); diff --git a/devtools/client/debugger/src/selectors/visibleColumnBreakpoints.js b/devtools/client/debugger/src/selectors/visibleColumnBreakpoints.js new file mode 100644 index 0000000000..5ed391c7e4 --- /dev/null +++ b/devtools/client/debugger/src/selectors/visibleColumnBreakpoints.js @@ -0,0 +1,185 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +import { createSelector } from "reselect"; + +import { + getViewport, + getSelectedSource, + getSelectedSourceTextContent, + getBreakpointPositionsForSource, +} from "./index"; +import { getVisibleBreakpoints } from "./visibleBreakpoints"; +import { getSelectedLocation } from "../utils/selected-location"; +import { sortSelectedLocations } from "../utils/location"; +import { getLineText } from "../utils/source"; + +function contains(location, range) { + return ( + location.line >= range.start.line && + location.line <= range.end.line && + (!location.column || + (location.column >= range.start.column && + location.column <= range.end.column)) + ); +} + +function groupBreakpoints(breakpoints, selectedSource) { + const breakpointsMap = {}; + if (!breakpoints) { + return breakpointsMap; + } + + for (const breakpoint of breakpoints) { + if (breakpoint.options.hidden) { + continue; + } + const location = getSelectedLocation(breakpoint, selectedSource); + const { line, column } = location; + + if (!breakpointsMap[line]) { + breakpointsMap[line] = {}; + } + + if (!breakpointsMap[line][column]) { + breakpointsMap[line][column] = []; + } + + breakpointsMap[line][column].push(breakpoint); + } + + return breakpointsMap; +} + +function findBreakpoint(location, breakpointMap) { + const { line, column } = location; + const breakpoints = breakpointMap[line]?.[column]; + + if (!breakpoints) { + return null; + } + return breakpoints[0]; +} + +function filterByLineCount(positions, selectedSource) { + const lineCount = {}; + + for (const breakpoint of positions) { + const { line } = getSelectedLocation(breakpoint, selectedSource); + if (!lineCount[line]) { + lineCount[line] = 0; + } + lineCount[line] = lineCount[line] + 1; + } + + return positions.filter( + breakpoint => + lineCount[getSelectedLocation(breakpoint, selectedSource).line] > 1 + ); +} + +function filterVisible(positions, selectedSource, viewport) { + return positions.filter(columnBreakpoint => { + const location = getSelectedLocation(columnBreakpoint, selectedSource); + return viewport && contains(location, viewport); + }); +} + +function filterByBreakpoints(positions, selectedSource, breakpointMap) { + return positions.filter(position => { + const location = getSelectedLocation(position, selectedSource); + return breakpointMap[location.line]; + }); +} + +// Filters out breakpoints to the right of the line. (bug 1552039) +function filterInLine(positions, selectedSource, selectedContent) { + return positions.filter(position => { + const location = getSelectedLocation(position, selectedSource); + const lineText = getLineText( + selectedSource.id, + selectedContent, + location.line + ); + + return lineText.length >= (location.column || 0); + }); +} + +function formatPositions(positions, selectedSource, breakpointMap) { + return positions.map(position => { + const location = getSelectedLocation(position, selectedSource); + return { + location, + breakpoint: findBreakpoint(location, breakpointMap), + }; + }); +} + +function convertToList(breakpointPositions) { + return [].concat(...Object.values(breakpointPositions)); +} + +export function getColumnBreakpoints( + positions, + breakpoints, + viewport, + selectedSource, + selectedSourceTextContent +) { + if (!positions || !selectedSource) { + return []; + } + + // We only want to show a column breakpoint if several conditions are matched + // - it is the first breakpoint to appear at an the original location + // - the position is in the current viewport + // - there is atleast one other breakpoint on that line + // - there is a breakpoint on that line + const breakpointMap = groupBreakpoints(breakpoints, selectedSource); + positions = filterByLineCount(positions, selectedSource); + positions = filterVisible(positions, selectedSource, viewport); + positions = filterInLine( + positions, + selectedSource, + selectedSourceTextContent + ); + positions = filterByBreakpoints(positions, selectedSource, breakpointMap); + + return formatPositions(positions, selectedSource, breakpointMap); +} + +const getVisibleBreakpointPositions = createSelector( + state => { + const source = getSelectedSource(state); + if (!source) { + return null; + } + return getBreakpointPositionsForSource(state, source.id); + }, + sourcePositions => { + return convertToList(sourcePositions || []); + } +); + +export const visibleColumnBreakpoints = createSelector( + getVisibleBreakpointPositions, + getVisibleBreakpoints, + getViewport, + getSelectedSource, + getSelectedSourceTextContent, + getColumnBreakpoints +); + +export function getFirstBreakpointPosition(state, location) { + const positions = getBreakpointPositionsForSource(state, location.sourceId); + if (!positions) { + return null; + } + + return sortSelectedLocations(convertToList(positions), location.source).find( + position => + getSelectedLocation(position, location.source).line == location.line + ); +} |