/* 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 { groupBy } from "lodash"; import { createSelector } from "reselect"; import { getViewport, getSource, getSelectedSource, getSelectedSourceWithContent, getBreakpointPositions, getBreakpointPositionsForSource, } from "../selectors"; import { getVisibleBreakpoints } from "./visibleBreakpoints"; import { getSelectedLocation } from "../utils/selected-location"; import { sortSelectedLocations } from "../utils/location"; import { getLineText } from "../utils/source"; import type { Selector, State } from "../reducers/types"; import type { Source, SourceLocation, PartialPosition, Breakpoint, Range, BreakpointPositions, BreakpointPosition, SourceWithContent, } from "../types"; export type ColumnBreakpoint = {| +location: SourceLocation, +breakpoint: ?Breakpoint, |}; export type ColumnBreakpoints = Array; type BreakpointMap = | {| +line: Breakpoint[], |} | {}; function contains(location: PartialPosition, range: 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: ?(Breakpoint[]), selectedSource: Source ): BreakpointMap { if (!breakpoints) { return {}; } const map: any = groupBy( breakpoints.filter(breakpoint => !breakpoint.options.hidden), breakpoint => getSelectedLocation(breakpoint, selectedSource).line ); for (const line in map) { map[line] = groupBy( map[line], breakpoint => getSelectedLocation(breakpoint, selectedSource).column ); } return map; } function findBreakpoint( location: SourceLocation, breakpointMap: BreakpointMap ): ?Breakpoint { const { line, column } = location; const breakpoints = breakpointMap[line]?.[column]; if (breakpoints) { return breakpoints[0]; } } function filterByLineCount( positions: BreakpointPosition[], selectedSource: Source ): BreakpointPosition[] { 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: BreakpointPosition[], selectedSource: Source, viewport ) { return positions.filter(columnBreakpoint => { const location = getSelectedLocation(columnBreakpoint, selectedSource); return viewport && contains(location, viewport); }); } function filterByBreakpoints( positions: BreakpointPosition[], selectedSource: Source, breakpointMap: 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: BreakpointPosition[], selectedSource: Source, selectedContent ): BreakpointPosition[] { 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: BreakpointPosition[], selectedSource: Source, breakpointMap: BreakpointMap ) { return (positions: any).map((position: BreakpointPosition) => { const location = getSelectedLocation(position, selectedSource); return { location, breakpoint: findBreakpoint(location, breakpointMap), }; }); } function convertToList( breakpointPositions: BreakpointPositions ): BreakpointPosition[] { return ([].concat(...Object.values(breakpointPositions)): any); } export function getColumnBreakpoints( positions: BreakpointPosition[], breakpoints: ?(Breakpoint[]), viewport: ?Range, selectedSource: ?SourceWithContent ) { 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, selectedSource.content); positions = filterByBreakpoints(positions, selectedSource, breakpointMap); return formatPositions(positions, selectedSource, breakpointMap); } const getVisibleBreakpointPositions = createSelector( getSelectedSource, getBreakpointPositions, (source, positions) => { if (!source) { return []; } const sourcePositions = positions[source.id]; if (!sourcePositions) { return []; } return convertToList(sourcePositions); } ); export const visibleColumnBreakpoints: Selector = createSelector( getVisibleBreakpointPositions, getVisibleBreakpoints, getViewport, getSelectedSourceWithContent, getColumnBreakpoints ); export function getFirstBreakpointPosition( state: State, { line, sourceId }: SourceLocation ): ?BreakpointPosition { const positions = getBreakpointPositionsForSource(state, sourceId); const source = getSource(state, sourceId); if (!source || !positions) { return; } return sortSelectedLocations(convertToList(positions), source).find( position => getSelectedLocation(position, source).line == line ); }