mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-09 13:18:29 +08:00
Currently the devtools-frontend doesn't really distinguish between UI locations that point to the first column (represented as column number 0) and UI locations that refer to the whole line, and somehow we managed to get away with this mixed state. But for the language plugins to really provide a consistent stepping / breakpoint experience we need to preserve the information of whether a location refers to the whole line or to the first column. The communication to the language plugin continues to pass numbers for backwards compatibility, using `-1` to state the absence of column information (i.e. indicating that the location refers to the line as a whole). Also-By: pfaffe@chromium.org Bug: chromium:1153123, chromium:1055327 Change-Id: I030da16e34ac02c259bfd9539e50b77ca18347db Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2562350 Commit-Queue: Benedikt Meurer <bmeurer@chromium.org> Auto-Submit: Benedikt Meurer <bmeurer@chromium.org> Reviewed-by: Philip Pfaffe <pfaffe@chromium.org>
539 lines
19 KiB
JavaScript
539 lines
19 KiB
JavaScript
// Copyright (c) 2015 The Chromium Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import * as Bindings from '../bindings/bindings.js';
|
|
import * as Common from '../common/common.js';
|
|
import * as Platform from '../platform/platform.js';
|
|
import * as SDK from '../sdk/sdk.js';
|
|
import * as TextUtils from '../text_utils/text_utils.js';
|
|
import * as UI from '../ui/ui.js';
|
|
import * as Workspace from '../workspace/workspace.js';
|
|
|
|
/**
|
|
* @implements {UI.ContextFlavorListener.ContextFlavorListener}
|
|
* @implements {UI.ListControl.ListDelegate<!BreakpointItem>}
|
|
*/
|
|
export class JavaScriptBreakpointsSidebarPane extends UI.ThrottledWidget.ThrottledWidget {
|
|
constructor() {
|
|
super(true);
|
|
this.registerRequiredCSS('sources/javaScriptBreakpointsSidebarPane.css', {enableLegacyPatching: true});
|
|
|
|
this._breakpointManager = Bindings.BreakpointManager.BreakpointManager.instance();
|
|
this._breakpointManager.addEventListener(Bindings.BreakpointManager.Events.BreakpointAdded, this.update, this);
|
|
this._breakpointManager.addEventListener(Bindings.BreakpointManager.Events.BreakpointRemoved, this.update, this);
|
|
Common.Settings.Settings.instance().moduleSetting('breakpointsActive').addChangeListener(this.update, this);
|
|
|
|
/** @type {!UI.ListModel.ListModel.<!BreakpointItem>} */
|
|
this._breakpoints = new UI.ListModel.ListModel();
|
|
/** @type {!UI.ListControl.ListControl.<!BreakpointItem>} */
|
|
this._list = new UI.ListControl.ListControl(this._breakpoints, this, UI.ListControl.ListMode.NonViewport);
|
|
UI.ARIAUtils.markAsList(this._list.element);
|
|
this.contentElement.appendChild(this._list.element);
|
|
|
|
this._emptyElement = this.contentElement.createChild('div', 'gray-info-message');
|
|
this._emptyElement.textContent = ls`No breakpoints`;
|
|
this._emptyElement.tabIndex = -1;
|
|
|
|
this.update();
|
|
}
|
|
|
|
/**
|
|
* @returns {!Array<!BreakpointLocation>}
|
|
*/
|
|
_getBreakpointLocations() {
|
|
const locations = this._breakpointManager.allBreakpointLocations().filter(
|
|
breakpointLocation =>
|
|
breakpointLocation.uiLocation.uiSourceCode.project().type() !== Workspace.Workspace.projectTypes.Debugger);
|
|
|
|
locations.sort((item1, item2) => item1.uiLocation.compareTo(item2.uiLocation));
|
|
|
|
const result = [];
|
|
let lastBreakpoint = null;
|
|
let lastLocation = null;
|
|
for (const location of locations) {
|
|
if (location.breakpoint !== lastBreakpoint || (lastLocation && location.uiLocation.compareTo(lastLocation))) {
|
|
result.push(location);
|
|
lastBreakpoint = location.breakpoint;
|
|
lastLocation = location.uiLocation;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
_hideList() {
|
|
this._list.element.classList.add('hidden');
|
|
this._emptyElement.classList.remove('hidden');
|
|
}
|
|
|
|
_ensureListShown() {
|
|
this._list.element.classList.remove('hidden');
|
|
this._emptyElement.classList.add('hidden');
|
|
}
|
|
|
|
/**
|
|
* @param {!Array<!BreakpointLocation>} breakpointLocations
|
|
* @return {!Array<!Array<!BreakpointLocation>>}
|
|
*/
|
|
_groupBreakpointLocationsById(breakpointLocations) {
|
|
/** @type {!Platform.Multimap<string, !BreakpointLocation>} */
|
|
const map = new Platform.Multimap();
|
|
for (const breakpointLocation of breakpointLocations) {
|
|
const uiLocation = breakpointLocation.uiLocation;
|
|
map.set(uiLocation.id(), breakpointLocation);
|
|
}
|
|
/** @type {!Array<!Array<!BreakpointLocation>>} */
|
|
const arr = [];
|
|
for (const id of map.keysArray()) {
|
|
const locations = Array.from(map.get(id));
|
|
if (locations.length) {
|
|
arr.push(locations);
|
|
}
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
/**
|
|
* @param {!Array<!BreakpointLocation>} breakpointLocations
|
|
* @return {!Platform.Multimap<string, string>}
|
|
*/
|
|
_getLocationIdsByLineId(breakpointLocations) {
|
|
/** @type {!Platform.Multimap<string, string>} */
|
|
const result = new Platform.Multimap();
|
|
|
|
for (const breakpointLocation of breakpointLocations) {
|
|
const uiLocation = breakpointLocation.uiLocation;
|
|
result.set(uiLocation.lineId(), uiLocation.id());
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* @return {!Promise<?Workspace.UISourceCode.UILocation>}
|
|
*/
|
|
async _getSelectedUILocation() {
|
|
const details = UI.Context.Context.instance().flavor(SDK.DebuggerModel.DebuggerPausedDetails);
|
|
if (details && details.callFrames.length) {
|
|
return await Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().rawLocationToUILocation(
|
|
details.callFrames[0].location());
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param {!Array<!Array<!BreakpointLocation>>} locations
|
|
* @return {!Promise<!Array<!TextUtils.Text.Text>>}
|
|
*/
|
|
_getContent(locations) {
|
|
// Use a cache to share the Text objects between all breakpoints. This way
|
|
// we share the cached line ending information that Text calculates. This
|
|
// was very slow to calculate with a lot of breakpoints in the same very
|
|
// large source file.
|
|
/** @type {!Map<string, !TextUtils.Text.Text>} */
|
|
const contentToTextMap = new Map();
|
|
|
|
return Promise.all(locations.map(async ([{uiLocation: {uiSourceCode}}]) => {
|
|
if (uiSourceCode.mimeType() === 'application/wasm') {
|
|
// We could mirror the logic from `SourceFrame._ensureContentLoaded()` here
|
|
// (and if so, ideally share that code somewhere), but that's quite heavy
|
|
// logic just to display a single Wasm instruction. Also not really clear
|
|
// how much value this would add. So let's keep it simple for now and don't
|
|
// display anything additional for Wasm breakpoints, and if there's demand
|
|
// to display some text preview, we could look into selectively disassemb-
|
|
// ling the part of the text that we need here.
|
|
// Relevant crbug: https://crbug.com/1090256
|
|
return new TextUtils.Text.Text('');
|
|
}
|
|
const {content} = await uiSourceCode.requestContent();
|
|
const contentText = content || '';
|
|
if (contentToTextMap.has(contentText)) {
|
|
return /** @type {!TextUtils.Text.Text} */ (contentToTextMap.get(contentText));
|
|
}
|
|
const text = new TextUtils.Text.Text(contentText);
|
|
contentToTextMap.set(contentText, text);
|
|
return text;
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @return {!Promise<?>}
|
|
*/
|
|
async doUpdate() {
|
|
const hadFocus = this.hasFocus();
|
|
const breakpointLocations = this._getBreakpointLocations();
|
|
if (!breakpointLocations.length) {
|
|
this._hideList();
|
|
this._setBreakpointItems([]);
|
|
return this._didUpdateForTest();
|
|
}
|
|
this._ensureListShown();
|
|
|
|
const locationsGroupedById = this._groupBreakpointLocationsById(breakpointLocations);
|
|
const locationIdsByLineId = this._getLocationIdsByLineId(breakpointLocations);
|
|
const content = await this._getContent(locationsGroupedById);
|
|
const selectedUILocation = await this._getSelectedUILocation();
|
|
const breakpoints = [];
|
|
for (let idx = 0; idx < locationsGroupedById.length; idx++) {
|
|
const locations = locationsGroupedById[idx];
|
|
const breakpointLocation = locations[0];
|
|
const uiLocation = breakpointLocation.uiLocation;
|
|
const isSelected =
|
|
!!selectedUILocation && locations.some(location => location.uiLocation.id() === selectedUILocation.id());
|
|
const showColumn = locationIdsByLineId.get(uiLocation.lineId()).size > 1;
|
|
const text = /** @type {!TextUtils.Text.Text} */ (content[idx]);
|
|
breakpoints.push(new BreakpointItem(locations, text, isSelected, showColumn));
|
|
}
|
|
|
|
if (breakpoints.some(breakpoint => breakpoint.isSelected)) {
|
|
UI.ViewManager.ViewManager.instance().showView('sources.jsBreakpoints');
|
|
}
|
|
|
|
this._list.element.classList.toggle(
|
|
'breakpoints-list-deactivated', !Common.Settings.Settings.instance().moduleSetting('breakpointsActive').get());
|
|
|
|
this._setBreakpointItems(breakpoints);
|
|
|
|
if (hadFocus) {
|
|
this.focus();
|
|
}
|
|
|
|
return this._didUpdateForTest();
|
|
}
|
|
|
|
/**
|
|
* If the number of breakpoint items is the same,
|
|
* we expect only minor changes and it implies that only
|
|
* few items should be updated
|
|
*
|
|
* @param {!Array<!BreakpointItem>} breakpointItems
|
|
*/
|
|
_setBreakpointItems(breakpointItems) {
|
|
if (this._breakpoints.length === breakpointItems.length) {
|
|
for (let i = 0; i < this._breakpoints.length; i++) {
|
|
if (!this._breakpoints.at(i).isSimilar(breakpointItems[i])) {
|
|
this._breakpoints.replace(i, breakpointItems[i]);
|
|
}
|
|
}
|
|
} else {
|
|
this._breakpoints.replaceAll(breakpointItems);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {!BreakpointItem} item
|
|
* @return {!Element}
|
|
*/
|
|
createElementForItem(item) {
|
|
const element = document.createElement('div');
|
|
element.classList.add('breakpoint-entry');
|
|
UI.ARIAUtils.markAsListitem(element);
|
|
element.tabIndex = this._list.selectedItem() === item ? 0 : -1;
|
|
element.addEventListener('contextmenu', this._breakpointContextMenu.bind(this), true);
|
|
element.addEventListener('click', this._revealLocation.bind(this, element), false);
|
|
const checkboxLabel = UI.UIUtils.CheckboxLabel.create('');
|
|
|
|
const uiLocation = item.locations[0].uiLocation;
|
|
const hasEnabled = item.locations.some(location => location.breakpoint.enabled());
|
|
const hasDisabled = item.locations.some(location => !location.breakpoint.enabled());
|
|
checkboxLabel.textElement.textContent = uiLocation.linkText() +
|
|
(item.showColumn && typeof uiLocation.columnNumber === 'number' ? ':' + (uiLocation.columnNumber + 1) : '');
|
|
checkboxLabel.checkboxElement.checked = hasEnabled;
|
|
checkboxLabel.checkboxElement.indeterminate = hasEnabled && hasDisabled;
|
|
checkboxLabel.checkboxElement.tabIndex = -1;
|
|
checkboxLabel.addEventListener('click', this._breakpointCheckboxClicked.bind(this), false);
|
|
element.appendChild(checkboxLabel);
|
|
let checkedDescription = hasEnabled ? ls`checked` : ls`unchecked`;
|
|
if (hasEnabled && hasDisabled) {
|
|
checkedDescription = ls`mixed`;
|
|
}
|
|
if (item.isSelected) {
|
|
UI.ARIAUtils.setDescription(element, ls`${checkedDescription} breakpoint hit`);
|
|
element.classList.add('breakpoint-hit');
|
|
this.setDefaultFocusedElement(element);
|
|
} else {
|
|
UI.ARIAUtils.setDescription(element, checkedDescription);
|
|
}
|
|
|
|
element.addEventListener('keydown', event => {
|
|
if (event.key === ' ') {
|
|
checkboxLabel.checkboxElement.click();
|
|
event.consume(true);
|
|
}
|
|
});
|
|
|
|
const snippetElement = element.createChild('div', 'source-text monospace');
|
|
const lineNumber = uiLocation.lineNumber;
|
|
|
|
if (item.text && lineNumber < item.text.lineCount()) {
|
|
const lineText = item.text.lineAt(lineNumber);
|
|
const maxSnippetLength = 200;
|
|
snippetElement.textContent = lineText.substring(item.showColumn ? (uiLocation.columnNumber || 0) : 0)
|
|
.trimEndWithMaxLength(maxSnippetLength);
|
|
}
|
|
|
|
elementToBreakpointMap.set(element, item.locations);
|
|
elementToUILocationMap.set(element, uiLocation);
|
|
return element;
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {!BreakpointItem} item
|
|
* @return {number}
|
|
*/
|
|
heightForItem(item) {
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {!BreakpointItem} item
|
|
* @return {boolean}
|
|
*/
|
|
isItemSelectable(item) {
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {?BreakpointItem} from
|
|
* @param {?BreakpointItem} to
|
|
* @param {?HTMLElement} fromElement
|
|
* @param {?HTMLElement} toElement
|
|
*/
|
|
selectedItemChanged(from, to, fromElement, toElement) {
|
|
if (fromElement) {
|
|
fromElement.tabIndex = -1;
|
|
}
|
|
if (toElement) {
|
|
toElement.tabIndex = 0;
|
|
this.setDefaultFocusedElement(toElement);
|
|
if (this.hasFocus()) {
|
|
toElement.focus();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {?Element} fromElement
|
|
* @param {?Element} toElement
|
|
* @return {boolean}
|
|
*/
|
|
updateSelectedItemARIA(fromElement, toElement) {
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
* @return {!Array<!Bindings.BreakpointManager.BreakpointLocation>}
|
|
*/
|
|
_breakpointLocations(event) {
|
|
if (event.target instanceof Element) {
|
|
return this._breakpointLocationsForElement(event.target);
|
|
}
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* @param {!Element} element
|
|
* @return {!Array<!Bindings.BreakpointManager.BreakpointLocation>}
|
|
*/
|
|
_breakpointLocationsForElement(element) {
|
|
const node = element.enclosingNodeOrSelfWithClass('breakpoint-entry');
|
|
if (!node) {
|
|
return [];
|
|
}
|
|
return elementToBreakpointMap.get(node) || [];
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
*/
|
|
_breakpointCheckboxClicked(event) {
|
|
const hadFocus = this.hasFocus();
|
|
const breakpoints = this._breakpointLocations(event).map(breakpointLocation => breakpointLocation.breakpoint);
|
|
const newState = /** @type {!UI.UIUtils.CheckboxLabel} */ (event.target).checkboxElement.checked;
|
|
for (const breakpoint of breakpoints) {
|
|
breakpoint.setEnabled(newState);
|
|
const item =
|
|
this._breakpoints.find(breakpointItem => breakpointItem.locations.some(loc => loc.breakpoint === breakpoint));
|
|
if (item) {
|
|
this._list.refreshItem(item);
|
|
}
|
|
}
|
|
if (hadFocus) {
|
|
this.focus();
|
|
}
|
|
event.consume();
|
|
}
|
|
|
|
/**
|
|
* @param {!Element} element
|
|
*/
|
|
_revealLocation(element) {
|
|
const uiLocations =
|
|
this._breakpointLocationsForElement(element).map(breakpointLocation => breakpointLocation.uiLocation);
|
|
let uiLocation = null;
|
|
for (const uiLocationCandidate of uiLocations) {
|
|
if (!uiLocation || uiLocationCandidate.compareTo(uiLocation) < 0) {
|
|
uiLocation = uiLocationCandidate;
|
|
}
|
|
}
|
|
if (uiLocation) {
|
|
Common.Revealer.reveal(uiLocation);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
*/
|
|
_breakpointContextMenu(event) {
|
|
const breakpoints = this._breakpointLocations(event).map(breakpointLocation => breakpointLocation.breakpoint);
|
|
|
|
const contextMenu = new UI.ContextMenu.ContextMenu(event);
|
|
const removeEntryTitle = breakpoints.length > 1 ? Common.UIString.UIString('Remove all breakpoints in line') :
|
|
Common.UIString.UIString('Remove breakpoint');
|
|
contextMenu.defaultSection().appendItem(
|
|
removeEntryTitle, () => breakpoints.map(breakpoint => breakpoint.remove(false /* keepInStorage */)));
|
|
if (event.target instanceof Element) {
|
|
contextMenu.defaultSection().appendItem(ls`Reveal location`, this._revealLocation.bind(this, event.target));
|
|
}
|
|
|
|
const breakpointActive = Common.Settings.Settings.instance().moduleSetting('breakpointsActive').get();
|
|
const breakpointActiveTitle = breakpointActive ? Common.UIString.UIString('Deactivate breakpoints') :
|
|
Common.UIString.UIString('Activate breakpoints');
|
|
contextMenu.defaultSection().appendItem(
|
|
breakpointActiveTitle,
|
|
() => Common.Settings.Settings.instance().moduleSetting('breakpointsActive').set(!breakpointActive));
|
|
|
|
if (breakpoints.some(breakpoint => !breakpoint.enabled())) {
|
|
const enableTitle = Common.UIString.UIString('Enable all breakpoints');
|
|
contextMenu.defaultSection().appendItem(enableTitle, this._toggleAllBreakpoints.bind(this, true));
|
|
if (event.target instanceof Element) {
|
|
const enableInFileTitle = Common.UIString.UIString('Enable breakpoints in file');
|
|
contextMenu.defaultSection().appendItem(
|
|
enableInFileTitle, this._toggleAllBreakpointsInFile.bind(this, event.target, true));
|
|
}
|
|
}
|
|
if (breakpoints.some(breakpoint => breakpoint.enabled())) {
|
|
const disableTitle = Common.UIString.UIString('Disable all breakpoints');
|
|
contextMenu.defaultSection().appendItem(disableTitle, this._toggleAllBreakpoints.bind(this, false));
|
|
if (event.target instanceof Element) {
|
|
const disableInFileTitle = Common.UIString.UIString('Disable breakpoints in file');
|
|
contextMenu.defaultSection().appendItem(
|
|
disableInFileTitle, this._toggleAllBreakpointsInFile.bind(this, event.target, false));
|
|
}
|
|
}
|
|
|
|
const removeAllTitle = Common.UIString.UIString('Remove all breakpoints');
|
|
contextMenu.defaultSection().appendItem(removeAllTitle, this._removeAllBreakpoints.bind(this));
|
|
const removeOtherTitle = Common.UIString.UIString('Remove other breakpoints');
|
|
contextMenu.defaultSection().appendItem(
|
|
removeOtherTitle, this._removeOtherBreakpoints.bind(this, new Set(breakpoints)));
|
|
contextMenu.show();
|
|
}
|
|
|
|
/**
|
|
* @param {!Element} element
|
|
* @param {boolean} toggleState
|
|
*/
|
|
_toggleAllBreakpointsInFile(element, toggleState) {
|
|
const breakpointLocations = this._getBreakpointLocations();
|
|
const selectedBreakpointLocations = this._breakpointLocationsForElement(element);
|
|
breakpointLocations.forEach(breakpointLocation => {
|
|
const matchesLocation = selectedBreakpointLocations.some(
|
|
selectedBreakpointLocation =>
|
|
selectedBreakpointLocation.breakpoint.url() === breakpointLocation.breakpoint.url());
|
|
if (matchesLocation) {
|
|
breakpointLocation.breakpoint.setEnabled(toggleState);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} toggleState
|
|
*/
|
|
_toggleAllBreakpoints(toggleState) {
|
|
for (const breakpointLocation of this._breakpointManager.allBreakpointLocations()) {
|
|
breakpointLocation.breakpoint.setEnabled(toggleState);
|
|
}
|
|
}
|
|
|
|
_removeAllBreakpoints() {
|
|
for (const breakpointLocation of this._breakpointManager.allBreakpointLocations()) {
|
|
breakpointLocation.breakpoint.remove(false /* keepInStorage */);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!Set<!Bindings.BreakpointManager.Breakpoint>} selectedBreakpoints
|
|
*/
|
|
_removeOtherBreakpoints(selectedBreakpoints) {
|
|
for (const breakpointLocation of this._breakpointManager.allBreakpointLocations()) {
|
|
if (!selectedBreakpoints.has(breakpointLocation.breakpoint)) {
|
|
breakpointLocation.breakpoint.remove(false /* keepInStorage */);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {?Object} object
|
|
*/
|
|
flavorChanged(object) {
|
|
this.update();
|
|
}
|
|
|
|
_didUpdateForTest() {
|
|
}
|
|
}
|
|
|
|
class BreakpointItem {
|
|
/**
|
|
* @param {!Array<!BreakpointLocation>} locations
|
|
* @param {?TextUtils.Text.Text} text
|
|
* @param {boolean} isSelected
|
|
* @param {boolean} showColumn
|
|
*/
|
|
constructor(locations, text, isSelected, showColumn) {
|
|
this.locations = locations;
|
|
this.text = text;
|
|
this.isSelected = isSelected;
|
|
this.showColumn = showColumn;
|
|
}
|
|
|
|
/**
|
|
* Checks if this item has not changed compared with the other
|
|
* Used to cache model items between re-renders
|
|
* @param {!BreakpointItem} other
|
|
*/
|
|
isSimilar(other) {
|
|
return this.locations.length === other.locations.length &&
|
|
this.locations.every((l, idx) => l.uiLocation === other.locations[idx].uiLocation) &&
|
|
this.locations.every((l, idx) => l.breakpoint === other.locations[idx].breakpoint) &&
|
|
((this.text === other.text) || (this.text && other.text && this.text.value() === other.text.value())) &&
|
|
this.isSelected === other.isSelected && this.showColumn === other.showColumn;
|
|
}
|
|
}
|
|
|
|
/** @type {!WeakMap<!Element, !Workspace.UISourceCode.UILocation>} */
|
|
const elementToUILocationMap = new WeakMap();
|
|
|
|
/**
|
|
* @param {!Element} element
|
|
*/
|
|
export function retrieveLocationForElement(element) {
|
|
return elementToUILocationMap.get(element);
|
|
}
|
|
|
|
/** @type {!WeakMap<!Element, !Array<!BreakpointLocation>>} */
|
|
const elementToBreakpointMap = new WeakMap();
|
|
|
|
/** @typedef {{breakpoint: !Bindings.BreakpointManager.Breakpoint, uiLocation: !Workspace.UISourceCode.UILocation}} */
|
|
// @ts-ignore typedef
|
|
export let BreakpointLocation;
|