summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/src
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/debugger/src')
-rw-r--r--devtools/client/debugger/src/.eslintignore5
-rw-r--r--devtools/client/debugger/src/.eslintrc.js363
-rw-r--r--devtools/client/debugger/src/actions/README.md26
-rw-r--r--devtools/client/debugger/src/actions/ast/index.js5
-rw-r--r--devtools/client/debugger/src/actions/ast/moz.build11
-rw-r--r--devtools/client/debugger/src/actions/ast/setInScopeLines.js98
-rw-r--r--devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js331
-rw-r--r--devtools/client/debugger/src/actions/breakpoints/index.js395
-rw-r--r--devtools/client/debugger/src/actions/breakpoints/modify.js368
-rw-r--r--devtools/client/debugger/src/actions/breakpoints/moz.build13
-rw-r--r--devtools/client/debugger/src/actions/breakpoints/syncBreakpoint.js126
-rw-r--r--devtools/client/debugger/src/actions/breakpoints/tests/__snapshots__/breakpoints.spec.js.snap165
-rw-r--r--devtools/client/debugger/src/actions/breakpoints/tests/breakpoints.spec.js476
-rw-r--r--devtools/client/debugger/src/actions/context-menus/breakpoint-heading.js78
-rw-r--r--devtools/client/debugger/src/actions/context-menus/breakpoint.js396
-rw-r--r--devtools/client/debugger/src/actions/context-menus/editor-breakpoint.js273
-rw-r--r--devtools/client/debugger/src/actions/context-menus/editor.js436
-rw-r--r--devtools/client/debugger/src/actions/context-menus/frame.js97
-rw-r--r--devtools/client/debugger/src/actions/context-menus/index.js12
-rw-r--r--devtools/client/debugger/src/actions/context-menus/moz.build16
-rw-r--r--devtools/client/debugger/src/actions/context-menus/outline.js54
-rw-r--r--devtools/client/debugger/src/actions/context-menus/source-tree-item.js281
-rw-r--r--devtools/client/debugger/src/actions/context-menus/tab.js128
-rw-r--r--devtools/client/debugger/src/actions/event-listeners.js77
-rw-r--r--devtools/client/debugger/src/actions/exceptions.js30
-rw-r--r--devtools/client/debugger/src/actions/expressions.js210
-rw-r--r--devtools/client/debugger/src/actions/file-search.js51
-rw-r--r--devtools/client/debugger/src/actions/index.js50
-rw-r--r--devtools/client/debugger/src/actions/moz.build32
-rw-r--r--devtools/client/debugger/src/actions/navigation.js57
-rw-r--r--devtools/client/debugger/src/actions/pause/breakOnNext.js20
-rw-r--r--devtools/client/debugger/src/actions/pause/commands.js147
-rw-r--r--devtools/client/debugger/src/actions/pause/continueToHere.js63
-rw-r--r--devtools/client/debugger/src/actions/pause/expandScopes.js16
-rw-r--r--devtools/client/debugger/src/actions/pause/fetchFrames.js22
-rw-r--r--devtools/client/debugger/src/actions/pause/fetchScopes.js37
-rw-r--r--devtools/client/debugger/src/actions/pause/index.js32
-rw-r--r--devtools/client/debugger/src/actions/pause/inlinePreview.js239
-rw-r--r--devtools/client/debugger/src/actions/pause/mapFrames.js161
-rw-r--r--devtools/client/debugger/src/actions/pause/mapScopes.js201
-rw-r--r--devtools/client/debugger/src/actions/pause/moz.build26
-rw-r--r--devtools/client/debugger/src/actions/pause/pauseOnDebuggerStatement.js17
-rw-r--r--devtools/client/debugger/src/actions/pause/pauseOnExceptions.js34
-rw-r--r--devtools/client/debugger/src/actions/pause/paused.js69
-rw-r--r--devtools/client/debugger/src/actions/pause/resetBreakpointsPaneState.js18
-rw-r--r--devtools/client/debugger/src/actions/pause/resumed.js31
-rw-r--r--devtools/client/debugger/src/actions/pause/selectFrame.js37
-rw-r--r--devtools/client/debugger/src/actions/pause/skipPausing.js33
-rw-r--r--devtools/client/debugger/src/actions/pause/tests/__snapshots__/pauseOnExceptions.spec.js.snap10
-rw-r--r--devtools/client/debugger/src/actions/pause/tests/pause.spec.js290
-rw-r--r--devtools/client/debugger/src/actions/pause/tests/pauseOnExceptions.spec.js24
-rw-r--r--devtools/client/debugger/src/actions/pause/tests/skipPausing.spec.js18
-rw-r--r--devtools/client/debugger/src/actions/preview.js159
-rw-r--r--devtools/client/debugger/src/actions/project-text-search.js142
-rw-r--r--devtools/client/debugger/src/actions/quick-open.js21
-rw-r--r--devtools/client/debugger/src/actions/source-actors.js12
-rw-r--r--devtools/client/debugger/src/actions/sources-tree.js43
-rw-r--r--devtools/client/debugger/src/actions/sources/blackbox.js211
-rw-r--r--devtools/client/debugger/src/actions/sources/breakableLines.js68
-rw-r--r--devtools/client/debugger/src/actions/sources/index.js40
-rw-r--r--devtools/client/debugger/src/actions/sources/loadSourceText.js252
-rw-r--r--devtools/client/debugger/src/actions/sources/moz.build17
-rw-r--r--devtools/client/debugger/src/actions/sources/newSources.js382
-rw-r--r--devtools/client/debugger/src/actions/sources/prettyPrint.js358
-rw-r--r--devtools/client/debugger/src/actions/sources/select.js368
-rw-r--r--devtools/client/debugger/src/actions/sources/symbols.js62
-rw-r--r--devtools/client/debugger/src/actions/sources/tests/blackbox.spec.js247
-rw-r--r--devtools/client/debugger/src/actions/sources/tests/loadSource.spec.js216
-rw-r--r--devtools/client/debugger/src/actions/sources/tests/newSources.spec.js103
-rw-r--r--devtools/client/debugger/src/actions/sources/tests/select.spec.js195
-rw-r--r--devtools/client/debugger/src/actions/tabs.js104
-rw-r--r--devtools/client/debugger/src/actions/tests/__snapshots__/expressions.spec.js.snap11
-rw-r--r--devtools/client/debugger/src/actions/tests/__snapshots__/pending-breakpoints.spec.js.snap30
-rw-r--r--devtools/client/debugger/src/actions/tests/expressions.spec.js136
-rw-r--r--devtools/client/debugger/src/actions/tests/helpers/breakpoints.js65
-rw-r--r--devtools/client/debugger/src/actions/tests/helpers/mockCommandClient.js49
-rw-r--r--devtools/client/debugger/src/actions/tests/helpers/readFixture.js14
-rw-r--r--devtools/client/debugger/src/actions/tests/pending-breakpoints.spec.js251
-rw-r--r--devtools/client/debugger/src/actions/tests/ui.spec.js88
-rw-r--r--devtools/client/debugger/src/actions/threads.js58
-rw-r--r--devtools/client/debugger/src/actions/toolbox.js43
-rw-r--r--devtools/client/debugger/src/actions/tracing.js44
-rw-r--r--devtools/client/debugger/src/actions/ui.js282
-rw-r--r--devtools/client/debugger/src/actions/utils/create-store.js75
-rw-r--r--devtools/client/debugger/src/actions/utils/middleware/context.js90
-rw-r--r--devtools/client/debugger/src/actions/utils/middleware/log.js111
-rw-r--r--devtools/client/debugger/src/actions/utils/middleware/moz.build15
-rw-r--r--devtools/client/debugger/src/actions/utils/middleware/promise.js61
-rw-r--r--devtools/client/debugger/src/actions/utils/middleware/thunk.js22
-rw-r--r--devtools/client/debugger/src/actions/utils/middleware/timing.js26
-rw-r--r--devtools/client/debugger/src/actions/utils/middleware/wait-service.js62
-rw-r--r--devtools/client/debugger/src/actions/utils/moz.build12
-rw-r--r--devtools/client/debugger/src/client/README.md47
-rw-r--r--devtools/client/debugger/src/client/firefox.js206
-rw-r--r--devtools/client/debugger/src/client/firefox/commands.js515
-rw-r--r--devtools/client/debugger/src/client/firefox/create.js431
-rw-r--r--devtools/client/debugger/src/client/firefox/moz.build11
-rw-r--r--devtools/client/debugger/src/client/moz.build12
-rw-r--r--devtools/client/debugger/src/components/App.css151
-rw-r--r--devtools/client/debugger/src/components/App.js396
-rw-r--r--devtools/client/debugger/src/components/Editor/BlackboxLines.js139
-rw-r--r--devtools/client/debugger/src/components/Editor/Breakpoint.js157
-rw-r--r--devtools/client/debugger/src/components/Editor/Breakpoints.css153
-rw-r--r--devtools/client/debugger/src/components/Editor/Breakpoints.js78
-rw-r--r--devtools/client/debugger/src/components/Editor/ColumnBreakpoint.js128
-rw-r--r--devtools/client/debugger/src/components/Editor/ColumnBreakpoints.js94
-rw-r--r--devtools/client/debugger/src/components/Editor/ConditionalPanel.css39
-rw-r--r--devtools/client/debugger/src/components/Editor/ConditionalPanel.js280
-rw-r--r--devtools/client/debugger/src/components/Editor/DebugLine.js137
-rw-r--r--devtools/client/debugger/src/components/Editor/Editor.css216
-rw-r--r--devtools/client/debugger/src/components/Editor/EmptyLines.js91
-rw-r--r--devtools/client/debugger/src/components/Editor/Exception.js102
-rw-r--r--devtools/client/debugger/src/components/Editor/Exceptions.js66
-rw-r--r--devtools/client/debugger/src/components/Editor/Footer.css85
-rw-r--r--devtools/client/debugger/src/components/Editor/Footer.js288
-rw-r--r--devtools/client/debugger/src/components/Editor/HighlightLine.js194
-rw-r--r--devtools/client/debugger/src/components/Editor/HighlightLines.js74
-rw-r--r--devtools/client/debugger/src/components/Editor/InlinePreview.css29
-rw-r--r--devtools/client/debugger/src/components/Editor/InlinePreview.js74
-rw-r--r--devtools/client/debugger/src/components/Editor/InlinePreviewRow.js103
-rw-r--r--devtools/client/debugger/src/components/Editor/InlinePreviews.js80
-rw-r--r--devtools/client/debugger/src/components/Editor/Preview.css111
-rw-r--r--devtools/client/debugger/src/components/Editor/Preview/ExceptionPopup.js142
-rw-r--r--devtools/client/debugger/src/components/Editor/Preview/Popup.css174
-rw-r--r--devtools/client/debugger/src/components/Editor/Preview/Popup.js277
-rw-r--r--devtools/client/debugger/src/components/Editor/Preview/index.js128
-rw-r--r--devtools/client/debugger/src/components/Editor/Preview/moz.build12
-rw-r--r--devtools/client/debugger/src/components/Editor/Preview/tests/Popup.spec.js99
-rw-r--r--devtools/client/debugger/src/components/Editor/SearchInFileBar.css32
-rw-r--r--devtools/client/debugger/src/components/Editor/SearchInFileBar.js368
-rw-r--r--devtools/client/debugger/src/components/Editor/Tab.js148
-rw-r--r--devtools/client/debugger/src/components/Editor/Tabs.css127
-rw-r--r--devtools/client/debugger/src/components/Editor/Tabs.js320
-rw-r--r--devtools/client/debugger/src/components/Editor/index.js795
-rw-r--r--devtools/client/debugger/src/components/Editor/moz.build31
-rw-r--r--devtools/client/debugger/src/components/Editor/tests/Breakpoints.spec.js53
-rw-r--r--devtools/client/debugger/src/components/Editor/tests/ConditionalPanel.spec.js77
-rw-r--r--devtools/client/debugger/src/components/Editor/tests/DebugLine.spec.js88
-rw-r--r--devtools/client/debugger/src/components/Editor/tests/Footer.spec.js70
-rw-r--r--devtools/client/debugger/src/components/Editor/tests/__snapshots__/Breakpoints.spec.js.snap32
-rw-r--r--devtools/client/debugger/src/components/Editor/tests/__snapshots__/ConditionalPanel.spec.js.snap747
-rw-r--r--devtools/client/debugger/src/components/Editor/tests/__snapshots__/Footer.spec.js.snap93
-rw-r--r--devtools/client/debugger/src/components/PrimaryPanes/Outline.css158
-rw-r--r--devtools/client/debugger/src/components/PrimaryPanes/Outline.js388
-rw-r--r--devtools/client/debugger/src/components/PrimaryPanes/OutlineFilter.css23
-rw-r--r--devtools/client/debugger/src/components/PrimaryPanes/OutlineFilter.js68
-rw-r--r--devtools/client/debugger/src/components/PrimaryPanes/ProjectSearch.css227
-rw-r--r--devtools/client/debugger/src/components/PrimaryPanes/ProjectSearch.js480
-rw-r--r--devtools/client/debugger/src/components/PrimaryPanes/Sources.css244
-rw-r--r--devtools/client/debugger/src/components/PrimaryPanes/SourcesTree.js352
-rw-r--r--devtools/client/debugger/src/components/PrimaryPanes/SourcesTreeItem.js249
-rw-r--r--devtools/client/debugger/src/components/PrimaryPanes/index.js133
-rw-r--r--devtools/client/debugger/src/components/PrimaryPanes/moz.build15
-rw-r--r--devtools/client/debugger/src/components/QuickOpenModal.css28
-rw-r--r--devtools/client/debugger/src/components/QuickOpenModal.js508
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/Breakpoint.js235
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/BreakpointHeading.js84
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/Breakpoints.css235
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/ExceptionOption.js40
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/index.js172
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/moz.build13
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/tests/ExceptionOption.spec.js22
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/tests/__snapshots__/ExceptionOption.spec.js.snap18
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/CommandBar.css33
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/CommandBar.js502
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/DOMMutationBreakpoints.css76
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/DOMMutationBreakpoints.js189
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/EventListeners.css155
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/EventListeners.js339
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Expressions.css171
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Expressions.js486
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/Frame.js232
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/FrameIndent.js18
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/Frames.css185
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/Group.css38
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/Group.js191
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/index.js220
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/moz.build13
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Frame.spec.js110
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Frames.spec.js270
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Group.spec.js93
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/__snapshots__/Frame.spec.js.snap1172
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/__snapshots__/Frames.spec.js.snap1001
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/__snapshots__/Group.spec.js.snap2286
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Scopes.css100
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Scopes.js413
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/SecondaryPanes.css98
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Thread.js81
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Threads.css64
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/Threads.js40
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/WhyPaused.css53
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/WhyPaused.js218
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.css108
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.js383
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/index.js548
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/moz.build22
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/tests/CommandBar.spec.js78
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/tests/EventListeners.spec.js136
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/tests/Expressions.spec.js77
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/tests/XHRBreakpoints.spec.js341
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/tests/__snapshots__/EventListeners.spec.js.snap408
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/tests/__snapshots__/Expressions.spec.js.snap203
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/tests/__snapshots__/XHRBreakpoints.spec.js.snap619
-rw-r--r--devtools/client/debugger/src/components/ShortcutsModal.css47
-rw-r--r--devtools/client/debugger/src/components/ShortcutsModal.js155
-rw-r--r--devtools/client/debugger/src/components/WelcomeBox.css83
-rw-r--r--devtools/client/debugger/src/components/WelcomeBox.js137
-rw-r--r--devtools/client/debugger/src/components/moz.build18
-rw-r--r--devtools/client/debugger/src/components/shared/AccessibleImage.css201
-rw-r--r--devtools/client/debugger/src/components/shared/AccessibleImage.js21
-rw-r--r--devtools/client/debugger/src/components/shared/Accordion.css107
-rw-r--r--devtools/client/debugger/src/components/shared/Accordion.js89
-rw-r--r--devtools/client/debugger/src/components/shared/Badge.css16
-rw-r--r--devtools/client/debugger/src/components/shared/Badge.js30
-rw-r--r--devtools/client/debugger/src/components/shared/BracketArrow.css64
-rw-r--r--devtools/client/debugger/src/components/shared/BracketArrow.js28
-rw-r--r--devtools/client/debugger/src/components/shared/Button/CloseButton.js30
-rw-r--r--devtools/client/debugger/src/components/shared/Button/CommandBarButton.js55
-rw-r--r--devtools/client/debugger/src/components/shared/Button/PaneToggleButton.js58
-rw-r--r--devtools/client/debugger/src/components/shared/Button/index.js9
-rw-r--r--devtools/client/debugger/src/components/shared/Button/moz.build15
-rw-r--r--devtools/client/debugger/src/components/shared/Button/styles/CloseButton.css35
-rw-r--r--devtools/client/debugger/src/components/shared/Button/styles/CommandBarButton.css73
-rw-r--r--devtools/client/debugger/src/components/shared/Button/styles/PaneToggleButton.css29
-rw-r--r--devtools/client/debugger/src/components/shared/Button/styles/moz.build8
-rw-r--r--devtools/client/debugger/src/components/shared/Button/tests/CloseButton.spec.js31
-rw-r--r--devtools/client/debugger/src/components/shared/Button/tests/CommandBarButton.spec.js44
-rw-r--r--devtools/client/debugger/src/components/shared/Button/tests/PaneToggleButton.spec.js51
-rw-r--r--devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/CloseButton.spec.js.snap13
-rw-r--r--devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/CommandBarButton.spec.js.snap18
-rw-r--r--devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/PaneToggleButton.spec.js.snap13
-rw-r--r--devtools/client/debugger/src/components/shared/Dropdown.css97
-rw-r--r--devtools/client/debugger/src/components/shared/Dropdown.js74
-rw-r--r--devtools/client/debugger/src/components/shared/Modal.css47
-rw-r--r--devtools/client/debugger/src/components/shared/Modal.js45
-rw-r--r--devtools/client/debugger/src/components/shared/Popover.css32
-rw-r--r--devtools/client/debugger/src/components/shared/Popover.js324
-rw-r--r--devtools/client/debugger/src/components/shared/PreviewFunction.css23
-rw-r--r--devtools/client/debugger/src/components/shared/PreviewFunction.js108
-rw-r--r--devtools/client/debugger/src/components/shared/ResultList.css131
-rw-r--r--devtools/client/debugger/src/components/shared/ResultList.js102
-rw-r--r--devtools/client/debugger/src/components/shared/SearchInput.css223
-rw-r--r--devtools/client/debugger/src/components/shared/SearchInput.js362
-rw-r--r--devtools/client/debugger/src/components/shared/SmartGap.js170
-rw-r--r--devtools/client/debugger/src/components/shared/SourceIcon.css176
-rw-r--r--devtools/client/debugger/src/components/shared/SourceIcon.js71
-rw-r--r--devtools/client/debugger/src/components/shared/menu.css55
-rw-r--r--devtools/client/debugger/src/components/shared/moz.build23
-rw-r--r--devtools/client/debugger/src/components/shared/tests/Accordion.spec.js47
-rw-r--r--devtools/client/debugger/src/components/shared/tests/Badge.spec.js19
-rw-r--r--devtools/client/debugger/src/components/shared/tests/BracketArrow.spec.js24
-rw-r--r--devtools/client/debugger/src/components/shared/tests/Dropdown.spec.js21
-rw-r--r--devtools/client/debugger/src/components/shared/tests/Modal.spec.js56
-rw-r--r--devtools/client/debugger/src/components/shared/tests/Popover.spec.js212
-rw-r--r--devtools/client/debugger/src/components/shared/tests/PreviewFunction.spec.js130
-rw-r--r--devtools/client/debugger/src/components/shared/tests/ResultList.spec.js48
-rw-r--r--devtools/client/debugger/src/components/shared/tests/SearchInput.spec.js126
-rw-r--r--devtools/client/debugger/src/components/shared/tests/__snapshots__/Accordion.spec.js.snap81
-rw-r--r--devtools/client/debugger/src/components/shared/tests/__snapshots__/Badge.spec.js.snap9
-rw-r--r--devtools/client/debugger/src/components/shared/tests/__snapshots__/BracketArrow.spec.js.snap27
-rw-r--r--devtools/client/debugger/src/components/shared/tests/__snapshots__/Dropdown.spec.js.snap34
-rw-r--r--devtools/client/debugger/src/components/shared/tests/__snapshots__/Modal.spec.js.snap13
-rw-r--r--devtools/client/debugger/src/components/shared/tests/__snapshots__/Popover.spec.js.snap549
-rw-r--r--devtools/client/debugger/src/components/shared/tests/__snapshots__/PreviewFunction.spec.js.snap23
-rw-r--r--devtools/client/debugger/src/components/shared/tests/__snapshots__/ResultList.spec.js.snap55
-rw-r--r--devtools/client/debugger/src/components/shared/tests/__snapshots__/SearchInput.spec.js.snap267
-rw-r--r--devtools/client/debugger/src/components/test/QuickOpenModal.spec.js803
-rw-r--r--devtools/client/debugger/src/components/test/WelcomeBox.spec.js58
-rw-r--r--devtools/client/debugger/src/components/test/WhyPaused.spec.js61
-rw-r--r--devtools/client/debugger/src/components/test/__snapshots__/QuickOpenModal.spec.js.snap1777
-rw-r--r--devtools/client/debugger/src/components/test/__snapshots__/WelcomeBox.spec.js.snap67
-rw-r--r--devtools/client/debugger/src/components/test/__snapshots__/WhyPaused.spec.js.snap103
-rw-r--r--devtools/client/debugger/src/components/variables.css45
-rw-r--r--devtools/client/debugger/src/constants.js15
-rw-r--r--devtools/client/debugger/src/context-menu/menu.js43
-rw-r--r--devtools/client/debugger/src/context-menu/moz.build8
-rw-r--r--devtools/client/debugger/src/debugger.css57
-rw-r--r--devtools/client/debugger/src/main.js149
-rw-r--r--devtools/client/debugger/src/moz.build20
-rw-r--r--devtools/client/debugger/src/reducers/ast.js92
-rw-r--r--devtools/client/debugger/src/reducers/breakpoints.js149
-rw-r--r--devtools/client/debugger/src/reducers/event-listeners.js38
-rw-r--r--devtools/client/debugger/src/reducers/exceptions.js78
-rw-r--r--devtools/client/debugger/src/reducers/expressions.js124
-rw-r--r--devtools/client/debugger/src/reducers/index.js76
-rw-r--r--devtools/client/debugger/src/reducers/moz.build26
-rw-r--r--devtools/client/debugger/src/reducers/pause.js427
-rw-r--r--devtools/client/debugger/src/reducers/pending-breakpoints.js135
-rw-r--r--devtools/client/debugger/src/reducers/quick-open.js41
-rw-r--r--devtools/client/debugger/src/reducers/source-actors.js109
-rw-r--r--devtools/client/debugger/src/reducers/source-blackbox.js145
-rw-r--r--devtools/client/debugger/src/reducers/sources-content.js139
-rw-r--r--devtools/client/debugger/src/reducers/sources-tree.js704
-rw-r--r--devtools/client/debugger/src/reducers/sources.js395
-rw-r--r--devtools/client/debugger/src/reducers/tabs.js200
-rw-r--r--devtools/client/debugger/src/reducers/tests/breakpoints.spec.js73
-rw-r--r--devtools/client/debugger/src/reducers/tests/quick-open.spec.js59
-rw-r--r--devtools/client/debugger/src/reducers/tests/ui.spec.js30
-rw-r--r--devtools/client/debugger/src/reducers/threads.js69
-rw-r--r--devtools/client/debugger/src/reducers/ui.js253
-rw-r--r--devtools/client/debugger/src/selectors/ast.js32
-rw-r--r--devtools/client/debugger/src/selectors/breakpointAtLocation.js120
-rw-r--r--devtools/client/debugger/src/selectors/breakpointSources.js52
-rw-r--r--devtools/client/debugger/src/selectors/breakpoints.js84
-rw-r--r--devtools/client/debugger/src/selectors/event-listeners.js19
-rw-r--r--devtools/client/debugger/src/selectors/exceptions.js58
-rw-r--r--devtools/client/debugger/src/selectors/expressions.js29
-rw-r--r--devtools/client/debugger/src/selectors/index.js48
-rw-r--r--devtools/client/debugger/src/selectors/isLineInScope.js22
-rw-r--r--devtools/client/debugger/src/selectors/isSelectedFrameVisible.js37
-rw-r--r--devtools/client/debugger/src/selectors/moz.build32
-rw-r--r--devtools/client/debugger/src/selectors/pause.js293
-rw-r--r--devtools/client/debugger/src/selectors/pending-breakpoints.js20
-rw-r--r--devtools/client/debugger/src/selectors/quick-open.js15
-rw-r--r--devtools/client/debugger/src/selectors/source-actors.js119
-rw-r--r--devtools/client/debugger/src/selectors/source-blackbox.js26
-rw-r--r--devtools/client/debugger/src/selectors/sources-content.js48
-rw-r--r--devtools/client/debugger/src/selectors/sources-tree.js151
-rw-r--r--devtools/client/debugger/src/selectors/sources.js410
-rw-r--r--devtools/client/debugger/src/selectors/tabs.js89
-rw-r--r--devtools/client/debugger/src/selectors/test/__snapshots__/visibleColumnBreakpoints.spec.js.snap269
-rw-r--r--devtools/client/debugger/src/selectors/test/visibleColumnBreakpoints.spec.js145
-rw-r--r--devtools/client/debugger/src/selectors/threads.js56
-rw-r--r--devtools/client/debugger/src/selectors/ui.js117
-rw-r--r--devtools/client/debugger/src/selectors/visibleBreakpoints.js55
-rw-r--r--devtools/client/debugger/src/selectors/visibleColumnBreakpoints.js149
-rw-r--r--devtools/client/debugger/src/test/__mocks__/request-animation-frame.js8
-rw-r--r--devtools/client/debugger/src/test/fixtures/README.md3
-rw-r--r--devtools/client/debugger/src/test/fixtures/foobar.json56
-rw-r--r--devtools/client/debugger/src/test/fixtures/index.js3
-rw-r--r--devtools/client/debugger/src/test/shim.js31
-rw-r--r--devtools/client/debugger/src/test/tests-setup.js61
-rw-r--r--devtools/client/debugger/src/utils/DevToolsUtils.js16
-rw-r--r--devtools/client/debugger/src/utils/assert.js22
-rw-r--r--devtools/client/debugger/src/utils/ast.js64
-rw-r--r--devtools/client/debugger/src/utils/async-value.js27
-rw-r--r--devtools/client/debugger/src/utils/bootstrap.js142
-rw-r--r--devtools/client/debugger/src/utils/breakpoint/breakpointPositions.js20
-rw-r--r--devtools/client/debugger/src/utils/breakpoint/index.js71
-rw-r--r--devtools/client/debugger/src/utils/breakpoint/moz.build11
-rw-r--r--devtools/client/debugger/src/utils/breakpoint/tests/index.spec.js28
-rw-r--r--devtools/client/debugger/src/utils/build-query.js80
-rw-r--r--devtools/client/debugger/src/utils/clipboard.js19
-rw-r--r--devtools/client/debugger/src/utils/context.js143
-rw-r--r--devtools/client/debugger/src/utils/dbg.js100
-rw-r--r--devtools/client/debugger/src/utils/editor/create-editor.js44
-rw-r--r--devtools/client/debugger/src/utils/editor/get-expression.js54
-rw-r--r--devtools/client/debugger/src/utils/editor/index.js222
-rw-r--r--devtools/client/debugger/src/utils/editor/moz.build15
-rw-r--r--devtools/client/debugger/src/utils/editor/source-documents.js256
-rw-r--r--devtools/client/debugger/src/utils/editor/source-editor.css271
-rw-r--r--devtools/client/debugger/src/utils/editor/source-search.js327
-rw-r--r--devtools/client/debugger/src/utils/editor/tests/__snapshots__/create-editor.spec.js.snapbin0 -> 3275 bytes
-rw-r--r--devtools/client/debugger/src/utils/editor/tests/create-editor.spec.js22
-rw-r--r--devtools/client/debugger/src/utils/editor/tests/editor.spec.js186
-rw-r--r--devtools/client/debugger/src/utils/editor/tests/source-documents.spec.js213
-rw-r--r--devtools/client/debugger/src/utils/editor/tests/source-search.spec.js182
-rw-r--r--devtools/client/debugger/src/utils/editor/tokens.js178
-rw-r--r--devtools/client/debugger/src/utils/environment.js15
-rw-r--r--devtools/client/debugger/src/utils/evaluation-result.js19
-rw-r--r--devtools/client/debugger/src/utils/expressions.js67
-rw-r--r--devtools/client/debugger/src/utils/function.js37
-rw-r--r--devtools/client/debugger/src/utils/indentation.js40
-rw-r--r--devtools/client/debugger/src/utils/isMinified.js58
-rw-r--r--devtools/client/debugger/src/utils/location.js124
-rw-r--r--devtools/client/debugger/src/utils/log.js30
-rw-r--r--devtools/client/debugger/src/utils/memoizableAction.js75
-rw-r--r--devtools/client/debugger/src/utils/memoize.js63
-rw-r--r--devtools/client/debugger/src/utils/memoizeLast.js27
-rw-r--r--devtools/client/debugger/src/utils/moz.build53
-rw-r--r--devtools/client/debugger/src/utils/path.js24
-rw-r--r--devtools/client/debugger/src/utils/pause/frames/annotateFrames.js68
-rw-r--r--devtools/client/debugger/src/utils/pause/frames/collapseFrames.js58
-rw-r--r--devtools/client/debugger/src/utils/pause/frames/displayName.js114
-rw-r--r--devtools/client/debugger/src/utils/pause/frames/getLibraryFromUrl.js140
-rw-r--r--devtools/client/debugger/src/utils/pause/frames/index.js8
-rw-r--r--devtools/client/debugger/src/utils/pause/frames/moz.build14
-rw-r--r--devtools/client/debugger/src/utils/pause/frames/tests/__snapshots__/collapseFrames.spec.js.snap87
-rw-r--r--devtools/client/debugger/src/utils/pause/frames/tests/collapseFrames.spec.js43
-rw-r--r--devtools/client/debugger/src/utils/pause/frames/tests/displayName.spec.js129
-rw-r--r--devtools/client/debugger/src/utils/pause/frames/tests/getLibraryFromUrl.spec.js127
-rw-r--r--devtools/client/debugger/src/utils/pause/index.js5
-rw-r--r--devtools/client/debugger/src/utils/pause/mapScopes/README.md191
-rw-r--r--devtools/client/debugger/src/utils/pause/mapScopes/buildGeneratedBindingList.js141
-rw-r--r--devtools/client/debugger/src/utils/pause/mapScopes/filtering.js45
-rw-r--r--devtools/client/debugger/src/utils/pause/mapScopes/findGeneratedBindingFromPosition.js305
-rw-r--r--devtools/client/debugger/src/utils/pause/mapScopes/getApplicableBindingsForOriginalPosition.js112
-rw-r--r--devtools/client/debugger/src/utils/pause/mapScopes/index.js586
-rw-r--r--devtools/client/debugger/src/utils/pause/mapScopes/locColumn.js13
-rw-r--r--devtools/client/debugger/src/utils/pause/mapScopes/mappingContains.js12
-rw-r--r--devtools/client/debugger/src/utils/pause/mapScopes/moz.build19
-rw-r--r--devtools/client/debugger/src/utils/pause/mapScopes/optimizedOut.js15
-rw-r--r--devtools/client/debugger/src/utils/pause/mapScopes/positionCmp.js24
-rw-r--r--devtools/client/debugger/src/utils/pause/mapScopes/rangeMetadata.js117
-rw-r--r--devtools/client/debugger/src/utils/pause/moz.build15
-rw-r--r--devtools/client/debugger/src/utils/pause/scopes.js283
-rw-r--r--devtools/client/debugger/src/utils/pause/tests/scopes.spec.js150
-rw-r--r--devtools/client/debugger/src/utils/pause/why.js40
-rw-r--r--devtools/client/debugger/src/utils/prefs.js170
-rw-r--r--devtools/client/debugger/src/utils/preview.js7
-rw-r--r--devtools/client/debugger/src/utils/quick-open.js113
-rw-r--r--devtools/client/debugger/src/utils/result-list.js26
-rw-r--r--devtools/client/debugger/src/utils/selected-location.js15
-rw-r--r--devtools/client/debugger/src/utils/shallow-equal.js51
-rw-r--r--devtools/client/debugger/src/utils/source-maps.js122
-rw-r--r--devtools/client/debugger/src/utils/source-queue.js40
-rw-r--r--devtools/client/debugger/src/utils/source.js534
-rw-r--r--devtools/client/debugger/src/utils/sources-tree/getURL.js180
-rw-r--r--devtools/client/debugger/src/utils/sources-tree/moz.build9
-rw-r--r--devtools/client/debugger/src/utils/sources-tree/tests/getUrl.spec.js50
-rw-r--r--devtools/client/debugger/src/utils/sources-tree/utils.js44
-rw-r--r--devtools/client/debugger/src/utils/tabs.js121
-rw-r--r--devtools/client/debugger/src/utils/task.js44
-rw-r--r--devtools/client/debugger/src/utils/telemetry.js72
-rw-r--r--devtools/client/debugger/src/utils/test-head.js283
-rw-r--r--devtools/client/debugger/src/utils/test-mockup.js268
-rw-r--r--devtools/client/debugger/src/utils/tests/DevToolsUtils.spec.js41
-rw-r--r--devtools/client/debugger/src/utils/tests/__snapshots__/expressions.spec.js.snap25
-rw-r--r--devtools/client/debugger/src/utils/tests/__snapshots__/function.spec.js.snap25
-rw-r--r--devtools/client/debugger/src/utils/tests/__snapshots__/indentation.spec.js.snap27
-rw-r--r--devtools/client/debugger/src/utils/tests/assert.spec.js30
-rw-r--r--devtools/client/debugger/src/utils/tests/build-query.spec.js256
-rw-r--r--devtools/client/debugger/src/utils/tests/clipboard.spec.js45
-rw-r--r--devtools/client/debugger/src/utils/tests/expressions.spec.js67
-rw-r--r--devtools/client/debugger/src/utils/tests/function.spec.js61
-rw-r--r--devtools/client/debugger/src/utils/tests/indentation.spec.js61
-rw-r--r--devtools/client/debugger/src/utils/tests/isMinified.spec.js18
-rw-r--r--devtools/client/debugger/src/utils/tests/location.spec.js31
-rw-r--r--devtools/client/debugger/src/utils/tests/log.spec.js35
-rw-r--r--devtools/client/debugger/src/utils/tests/memoize.spec.js48
-rw-r--r--devtools/client/debugger/src/utils/tests/memoizeLast.spec.js31
-rw-r--r--devtools/client/debugger/src/utils/tests/path.spec.js49
-rw-r--r--devtools/client/debugger/src/utils/tests/quick-open.spec.js35
-rw-r--r--devtools/client/debugger/src/utils/tests/result-list.spec.js32
-rw-r--r--devtools/client/debugger/src/utils/tests/source.spec.js367
-rw-r--r--devtools/client/debugger/src/utils/tests/telemetry.spec.js13
-rw-r--r--devtools/client/debugger/src/utils/tests/text.spec.js20
-rw-r--r--devtools/client/debugger/src/utils/tests/ui.spec.js15
-rw-r--r--devtools/client/debugger/src/utils/tests/url.spec.js89
-rw-r--r--devtools/client/debugger/src/utils/tests/utils.spec.js87
-rw-r--r--devtools/client/debugger/src/utils/tests/wasm.spec.js96
-rw-r--r--devtools/client/debugger/src/utils/text.js58
-rw-r--r--devtools/client/debugger/src/utils/ui.js48
-rw-r--r--devtools/client/debugger/src/utils/url.js75
-rw-r--r--devtools/client/debugger/src/utils/utils.js59
-rw-r--r--devtools/client/debugger/src/utils/wasm.js166
-rw-r--r--devtools/client/debugger/src/utils/worker.js49
-rw-r--r--devtools/client/debugger/src/workers/moz.build12
-rw-r--r--devtools/client/debugger/src/workers/parser/findBestMatchExpression.js37
-rw-r--r--devtools/client/debugger/src/workers/parser/findOutOfScopeLocations.js132
-rw-r--r--devtools/client/debugger/src/workers/parser/frameworks.js60
-rw-r--r--devtools/client/debugger/src/workers/parser/getScopes/index.js65
-rw-r--r--devtools/client/debugger/src/workers/parser/getScopes/visitor.js909
-rw-r--r--devtools/client/debugger/src/workers/parser/getSymbols.js567
-rw-r--r--devtools/client/debugger/src/workers/parser/index.js55
-rw-r--r--devtools/client/debugger/src/workers/parser/mapAwaitExpression.js208
-rw-r--r--devtools/client/debugger/src/workers/parser/mapBindings.js120
-rw-r--r--devtools/client/debugger/src/workers/parser/mapExpression.js50
-rw-r--r--devtools/client/debugger/src/workers/parser/mapOriginalExpression.js106
-rw-r--r--devtools/client/debugger/src/workers/parser/moz.build10
-rw-r--r--devtools/client/debugger/src/workers/parser/sources.js24
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/__snapshots__/findOutOfScopeLocations.spec.js.snap47
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/__snapshots__/getScopes.spec.js.snap18895
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/__snapshots__/getSymbols.spec.js.snap169
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/__snapshots__/validate.spec.js.snap3
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/contains.spec.js250
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/findOutOfScopeLocations.spec.js80
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/allSymbols.js33
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/async.js10
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/call-sites.js4
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/callExpressions.js7
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/calls.js21
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/class.js28
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/component.js84
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/computed-props.js8
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/control-flow.js39
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/decorators.js2
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/destructuring.js16
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/es6.js1
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/expression.js25
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/flow.js5
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/angular1FalsePositive.js11
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/angular1Module.js4
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/plainJavascript.js8
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/reactComponent.js3
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/reactComponentEs5.js3
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/reactLibrary.js19
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/reduxLibrary.js39
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/vueFileComponent.js3
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/vueFileDeclarative.js6
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/func.js50
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/functionNames.js50
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/generators.js4
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/jsx.js5
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/math.js15
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/modules.js10
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/object-expressions.js6
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/optional-chaining.js3
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/outOfScope.js62
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/outOfScopeComment.js4
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/parseScriptTags.html42
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/private-fields.js24
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/proto.js14
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/regexp.js1
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/resolveToken.js40
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/arrow-function.js11
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/binding-types.js24
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/block-statement.js13
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/class-declaration.js14
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/class-expression.js11
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/class-property.js10
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/complex-nesting.js29
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/expressions.js6
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/flowtype-bindings.js11
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/fn-body-lex-and-nonlex.js23
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/for-loops.js13
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/function-declaration.js11
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/function-expression.js7
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/jsx-component.js6
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/out-of-order-declarations.js21
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/pattern-declarations.js2
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/simple-module.js11
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/switch-statement.js22
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/try-catch.js9
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/ts-sample.ts41
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/tsx-sample.tsx41
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/vue-sample.vue26
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/statements.js40
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/thisExpression.js11
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/fixtures/var.js21
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/framework.spec.js63
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/getScopes.spec.js227
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/getSymbols.spec.js51
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/helpers/index.js86
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/mapBindings.spec.js161
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/mapExpression.spec.js796
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/mapOriginalExpression.spec.js93
-rw-r--r--devtools/client/debugger/src/workers/parser/tests/sources.spec.js14
-rw-r--r--devtools/client/debugger/src/workers/parser/utils/ast.js230
-rw-r--r--devtools/client/debugger/src/workers/parser/utils/contains.js29
-rw-r--r--devtools/client/debugger/src/workers/parser/utils/formatSymbols.js65
-rw-r--r--devtools/client/debugger/src/workers/parser/utils/getFunctionName.js96
-rw-r--r--devtools/client/debugger/src/workers/parser/utils/helpers.js232
-rw-r--r--devtools/client/debugger/src/workers/parser/utils/inferClassName.js93
-rw-r--r--devtools/client/debugger/src/workers/parser/utils/parse-script-tags/customParse.js132
-rw-r--r--devtools/client/debugger/src/workers/parser/utils/parse-script-tags/index.js60
-rw-r--r--devtools/client/debugger/src/workers/parser/utils/parse-script-tags/parseScriptFragment.js155
-rw-r--r--devtools/client/debugger/src/workers/parser/utils/simple-path.js104
-rw-r--r--devtools/client/debugger/src/workers/parser/utils/tests/ast.spec.js41
-rw-r--r--devtools/client/debugger/src/workers/parser/worker.js39
-rw-r--r--devtools/client/debugger/src/workers/pretty-print/LICENSE.md23
-rw-r--r--devtools/client/debugger/src/workers/pretty-print/index.js30
-rw-r--r--devtools/client/debugger/src/workers/pretty-print/moz.build10
-rw-r--r--devtools/client/debugger/src/workers/pretty-print/pretty-fast.js1178
-rw-r--r--devtools/client/debugger/src/workers/pretty-print/tests/__snapshots__/prettyFast.spec.js.snap1974
-rw-r--r--devtools/client/debugger/src/workers/pretty-print/tests/prettyFast.spec.js435
-rw-r--r--devtools/client/debugger/src/workers/pretty-print/worker.js98
-rw-r--r--devtools/client/debugger/src/workers/search/get-matches.js45
-rw-r--r--devtools/client/debugger/src/workers/search/index.js17
-rw-r--r--devtools/client/debugger/src/workers/search/moz.build10
-rw-r--r--devtools/client/debugger/src/workers/search/project-search.js70
-rw-r--r--devtools/client/debugger/src/workers/search/tests/get-matches.spec.js99
-rw-r--r--devtools/client/debugger/src/workers/search/worker.js9
563 files changed, 89140 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/.eslintignore b/devtools/client/debugger/src/.eslintignore
new file mode 100644
index 0000000000..32aadf77f5
--- /dev/null
+++ b/devtools/client/debugger/src/.eslintignore
@@ -0,0 +1,5 @@
+test/examples/**
+test/integration/**
+test/unit-sources/**
+**/fixtures/**
+test/mochitest/**
diff --git a/devtools/client/debugger/src/.eslintrc.js b/devtools/client/debugger/src/.eslintrc.js
new file mode 100644
index 0000000000..b71126767c
--- /dev/null
+++ b/devtools/client/debugger/src/.eslintrc.js
@@ -0,0 +1,363 @@
+/* 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/>. */
+
+module.exports = {
+ plugins: ["react", "mozilla", "import", "file-header"],
+ globals: {
+ atob: true,
+ btoa: true,
+ Cc: true,
+ Ci: true,
+ Components: true,
+ console: true,
+ Cr: true,
+ Cu: true,
+ devtools: true,
+ dump: true,
+ EventEmitter: true,
+ isWorker: true,
+ loader: true,
+ Services: true,
+ Task: true,
+ XPCOMUtils: true,
+ _Iterator: true,
+ __dirname: true,
+ process: true,
+ global: true,
+ L10N: true,
+ },
+ extends: ["prettier", "plugin:jest/recommended"],
+ parserOptions: {
+ ecmaVersion: "latest",
+ sourceType: "module",
+ ecmaFeatures: { jsx: true },
+ },
+ env: {
+ es6: true,
+ browser: true,
+ commonjs: true,
+ jest: true,
+ },
+ rules: {
+ // These are the rules that have been configured so far to match the
+ // devtools coding style.
+
+ // Rules from the mozilla plugin
+ "mozilla/mark-test-function-used": "error",
+ "mozilla/no-aArgs": "error",
+ // See bug 1224289.
+ "mozilla/reject-importGlobalProperties": "error",
+ "mozilla/var-only-at-top-level": "error",
+
+ // Rules from the React plugin
+ "react/jsx-uses-react": "error",
+ "react/jsx-uses-vars": "error",
+ "react/no-danger": "error",
+ "react/no-did-mount-set-state": "error",
+ "react/no-did-update-set-state": "error",
+ "react/no-direct-mutation-state": "error",
+ "react/no-unknown-property": "error",
+ "react/prop-types": "off",
+ "react/sort-comp": [
+ "error",
+ {
+ order: ["propTypes", "everything-else", "render"],
+ },
+ ],
+
+ // Check for import errors.
+ "import/no-duplicates": "error",
+ "import/named": "error",
+ "import/export": "error",
+
+ // Incompatible with jest-in-case cases. See related GitHub issue
+ // https://github.com/jest-community/eslint-plugin-jest/issues/534
+ "jest/no-standalone-expect": "off",
+
+ // Disallow flow control that escapes from "finally".
+ "no-unsafe-finally": "error",
+
+ // Disallow using variables outside the blocks they are defined (especially
+ // since only let and const are used, see "no-var").
+ "block-scoped-var": 2,
+ // Require camel case names
+ camelcase: ["error", { properties: "never" }],
+ // Warn about cyclomatic complexity in functions.
+ complexity: ["error", { max: 22 }],
+ // Don't warn for inconsistent naming when capturing this (not so important
+ // with auto-binding fat arrow functions).
+ "consistent-this": 0,
+ // Don't require a default case in switch statements. Avoid being forced to
+ // add a bogus default when you know all possible cases are handled.
+ "default-case": 0,
+ // Encourage the use of dot notation whenever possible.
+ "dot-notation": 2,
+ // Allow using == instead of ===, in the interest of landing something since
+ // the devtools codebase is split on convention here.
+ eqeqeq: 0,
+ // Don't require function expressions to have a name.
+ // This makes the code more verbose and hard to read. Our engine already
+ // does a fantastic job assigning a name to the function, which includes
+ // the enclosing function name, and worst case you have a line number that
+ // you can just look up.
+ "func-names": 0,
+ // Allow use of function declarations and expressions.
+ "func-style": 0,
+ // Deprecated, will be removed in 1.0.
+ "global-strict": 0,
+ // Only useful in a node environment.
+ "handle-callback-err": 0,
+ // Don't enforce the maximum depth that blocks can be nested. The complexity
+ // rule is a better rule to check this.
+ "max-depth": 0,
+ // Maximum depth callbacks can be nested.
+ "max-nested-callbacks": [2, 4],
+ // Don't limit the number of parameters that can be used in a function.
+ "max-params": 0,
+ // Don't limit the maximum number of statement allowed in a function. We
+ // already have the complexity rule that's a better measurement.
+ "max-statements": 0,
+ // Require a capital letter for constructors, only check if all new
+ // operators are followed by a capital letter. Don't warn when capitalized
+ // functions are used without the new operator.
+ "new-cap": [2, { capIsNew: false }],
+ // Disallow use of the Array constructor.
+ "no-array-constructor": 2,
+ // Allow use of bitwise operators.
+ "no-bitwise": 0,
+ // Disallow use of arguments.caller or arguments.callee.
+ "no-caller": 2,
+ // Disallow the catch clause parameter name being the same as a variable in
+ // the outer scope, to avoid confusion.
+ "no-catch-shadow": 2,
+ // Disallow assignment in conditional expressions.
+ "no-cond-assign": 2,
+ // Allow using the console API.
+ "no-console": 0,
+ // Allow using constant expressions in conditions like while (true)
+ "no-constant-condition": 0,
+ // Allow use of the continue statement.
+ "no-continue": 0,
+ // Disallow control characters in regular expressions.
+ "no-control-regex": 2,
+ // Disallow use of debugger.
+ "no-debugger": 2,
+ // Disallow deletion of variables (deleting properties is fine).
+ "no-delete-var": 2,
+ // Allow division operators explicitly at beginning of regular expression.
+ "no-div-regex": 0,
+ // Disallow duplicate arguments in functions.
+ "no-dupe-args": 2,
+ // Disallow a duplicate case label.
+ "no-duplicate-case": 2,
+ // Disallow else after a return in an if. The else around the second return
+ // here is useless:
+ // if (something) { return false; } else { return true; }
+ "no-else-return": 2,
+ // Disallow empty statements. This will report an error for:
+ // try { something(); } catch (e) {}
+ // but will not report it for:
+ // try { something(); } catch (e) { /* Silencing the error because ...*/ }
+ // which is a valid use case.
+ "no-empty": 2,
+ // Disallow the use of empty character classes in regular expressions.
+ "no-empty-character-class": 2,
+ // Disallow use of labels for anything other then loops and switches.
+ "no-labels": 2,
+ // Disallow use of eval(). We have other APIs to evaluate code in content.
+ "no-eval": 2,
+ // Disallow assigning to the exception in a catch block.
+ "no-ex-assign": 2,
+ // Disallow adding to native types
+ "no-extend-native": 2,
+ // Disallow unnecessary function binding.
+ "no-extra-bind": 2,
+ // Disallow double-negation boolean casts in a boolean context.
+ "no-extra-boolean-cast": 2,
+ // Deprecated, will be removed in 1.0.
+ "no-extra-strict": 0,
+ // Disallow comments inline after code.
+ "no-inline-comments": 2,
+ // Disallow if as the only statement in an else block.
+ "no-lonely-if": 2,
+ // Allow mixing regular variable and require declarations (not a node env).
+ "no-mixed-requires": 0,
+ // Disallow use of multiline strings (use template strings instead).
+ "no-multi-str": 2,
+ "prefer-template": "error",
+ "prefer-const": [
+ "error",
+ {
+ destructuring: "all",
+ ignoreReadBeforeAssign: false,
+ },
+ ],
+ // Disallow reassignments of native objects.
+ "no-native-reassign": 2,
+ // Disallow nested ternary expressions, they make the code hard to read.
+ "no-nested-ternary": 2,
+ // Allow use of new operator with the require function.
+ "no-new-require": 0,
+ // Disallow use of octal literals.
+ "no-octal": 2,
+ // Allow reassignment of function parameters.
+ "no-param-reassign": 0,
+ // Allow string concatenation with __dirname and __filename (not a node env).
+ "no-path-concat": 0,
+ // Allow use of unary operators, ++ and --.
+ "no-plusplus": 0,
+ // Allow using process.env (not a node environment).
+ "no-process-env": 0,
+ // Allow using process.exit (not a node environment).
+ "no-process-exit": 0,
+ // Disallow usage of __proto__ property.
+ "no-proto": 2,
+ // Disallow declaring the same variable more than once (we use let anyway).
+ "no-redeclare": 2,
+ // Disallow multiple spaces in a regular expression literal.
+ "no-regex-spaces": 2,
+ // Don't restrict usage of specified node modules (not a node environment).
+ "no-restricted-modules": 0,
+ // Disallow use of assignment in return statement. It is preferable for a
+ // single line of code to have only one easily predictable effect.
+ "no-return-assign": 2,
+ // Allow use of javascript: urls.
+ "no-script-url": 0,
+ // Disallow comparisons where both sides are exactly the same.
+ "no-self-compare": 2,
+ // Disallow use of comma operator.
+ "no-sequences": 2,
+ // Warn about declaration of variables already declared in the outer scope.
+ // This isn't an error because it sometimes is useful to use the same name
+ // in a small helper function rather than having to come up with another
+ // random name.
+ // Still, making this a warning can help people avoid being confused.
+ "no-shadow": 2,
+ // Disallow sparse arrays, eg. let arr = [,,2].
+ // Array destructuring is fine though:
+ // for (let [, breakpointPromise] of aPromises)
+ "no-sparse-arrays": 2,
+ // Allow use of synchronous methods (not a node environment).
+ "no-sync": 0,
+ // Allow the use of ternary operators.
+ "no-ternary": 0,
+ // Disallow throwing literals (eg. throw "error" instead of
+ // throw new Error("error")).
+ "no-throw-literal": 2,
+ // Disallow use of undeclared variables unless mentioned in a /*global */
+ // block. Note that globals from head.js are automatically imported in tests
+ // by the import-headjs-globals rule form the mozilla eslint plugin.
+ "no-undef": 2,
+ // Allow dangling underscores in identifiers (for privates).
+ "no-underscore-dangle": 0,
+ // Allow use of undefined variable.
+ "no-undefined": 0,
+ // Disallow the use of Boolean literals in conditional expressions.
+ "no-unneeded-ternary": 2,
+ // Disallow unreachable statements after a return, throw, continue, or break
+ // statement.
+ "no-unreachable": 2,
+ // Disallow global and local variables that arent used, but allow unused function arguments.
+ "no-unused-vars": [2, { vars: "all", args: "none" }],
+ // Allow using variables before they are defined.
+ "no-use-before-define": 0,
+ // We use var-only-at-top-level instead of no-var as we allow top level
+ // vars.
+ "no-var": 0,
+ // Allow using TODO/FIXME comments.
+ "no-warning-comments": 0,
+ // Dont require method and property shorthand syntax for object literals.
+ // We use this in the code a lot, but not consistently, and this seems more
+ // like something to check at code review time.
+ "object-shorthand": 0,
+ // Allow more than one variable declaration per function.
+ "one-var": 0,
+ // Require use of the second argument for parseInt().
+ radix: 2,
+ // Dont require to sort variables within the same declaration block.
+ // Anyway, one-var is disabled.
+ "sort-vars": 0,
+ // Require "use strict" to be defined globally in the script.
+ strict: [2, "global"],
+ // Disallow comparisons with the value NaN.
+ "use-isnan": 2,
+ // Ensure that the results of typeof are compared against a valid string.
+ "valid-typeof": 2,
+ // Allow vars to be declared anywhere in the scope.
+ "vars-on-top": 0,
+ // Disallow Yoda conditions (where literal value comes first).
+ yoda: 2,
+
+ // And these are the rules that haven't been discussed so far, and that are
+ // disabled for now until we introduce them, one at a time.
+
+ // Require for-in loops to have an if statement.
+ "guard-for-in": 0,
+ // allow/disallow an empty newline after var statement
+ "newline-after-var": 0,
+ // disallow the use of alert, confirm, and prompt
+ "no-alert": 0,
+ // disallow comparisons to null without a type-checking operator
+ "no-eq-null": 0,
+ // disallow overwriting functions written as function declarations
+ "no-func-assign": 0,
+ // disallow use of eval()-like methods
+ "no-implied-eval": 0,
+ // disallow function or variable declarations in nested blocks
+ "no-inner-declarations": 0,
+ // disallow invalid regular expression strings in the RegExp constructor
+ "no-invalid-regexp": 0,
+ // disallow irregular whitespace outside of strings and comments
+ "no-irregular-whitespace": 0,
+ // disallow labels that share a name with a variable
+ "no-label-var": 0,
+ // disallow unnecessary nested blocks
+ "no-lone-blocks": 0,
+ // disallow creation of functions within loops
+ "no-loop-func": 0,
+ // disallow negation of the left operand of an in expression
+ "no-negated-in-lhs": 0,
+ // disallow use of new operator when not part of the assignment or
+ // comparison
+ "no-new": 0,
+ // disallow use of new operator for Function object
+ "no-new-func": 0,
+ // disallow use of the Object constructor
+ "no-new-object": 0,
+ // disallows creating new instances of String,Number, and Boolean
+ "no-new-wrappers": 0,
+ // disallow the use of object properties of the global object (Math and
+ // JSON) as functions
+ "no-obj-calls": 0,
+ // disallow use of octal escape sequences in string literals, such as
+ // var foo = "Copyright \251";
+ "no-octal-escape": 0,
+ // disallow use of undefined when initializing variables
+ "no-undef-init": 0,
+ // disallow usage of expressions in statement position
+ "no-unused-expressions": 0,
+ // disallow use of void operator
+ "no-void": 0,
+ // require assignment operator shorthand where possible or prohibit it
+ // entirely
+ "operator-assignment": 0,
+
+ "file-header/file-header": [
+ "error",
+ [
+ "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/>.",
+ ],
+ "block",
+ ["-\\*-(.*)-\\*-", "eslint(.*)", "vim(.*)"],
+ ],
+ },
+ settings: {
+ jest: {
+ // Keep in sync with "jest" version from debugger's package.json
+ version: 27,
+ },
+ },
+};
diff --git a/devtools/client/debugger/src/actions/README.md b/devtools/client/debugger/src/actions/README.md
new file mode 100644
index 0000000000..d919247838
--- /dev/null
+++ b/devtools/client/debugger/src/actions/README.md
@@ -0,0 +1,26 @@
+## Actions
+
+### Best Practices
+
+#### Scheduling Async Actions
+
+There are several use-cases with async actions that involve scheduling:
+
+* we do one action and cancel subsequent actions
+* we do one action and subsequent calls wait on the initial call
+* we start an action and show a loading state
+
+If you want to wait on subsequent calls you need to store action promises.
+[ex][req]
+
+If you just want to cancel subsequent calls, you can keep track of a pending
+state in the store. [ex][state]
+
+The advantage of adding the pending state to the store is that we can use that
+in the UI:
+
+* disable/hide the pretty print button
+* show a progress ui
+
+[req]: https://github.com/firefox-devtools/debugger/blob/master/src/actions/sources/loadSourceText.js
+[state]: https://github.com/firefox-devtools/debugger/blob/master/src/reducers/sources.js
diff --git a/devtools/client/debugger/src/actions/ast/index.js b/devtools/client/debugger/src/actions/ast/index.js
new file mode 100644
index 0000000000..ec2c1ae84c
--- /dev/null
+++ b/devtools/client/debugger/src/actions/ast/index.js
@@ -0,0 +1,5 @@
+/* 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 { setInScopeLines } from "./setInScopeLines";
diff --git a/devtools/client/debugger/src/actions/ast/moz.build b/devtools/client/debugger/src/actions/ast/moz.build
new file mode 100644
index 0000000000..5b0152d2ad
--- /dev/null
+++ b/devtools/client/debugger/src/actions/ast/moz.build
@@ -0,0 +1,11 @@
+# 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(
+ "index.js",
+ "setInScopeLines.js",
+)
diff --git a/devtools/client/debugger/src/actions/ast/setInScopeLines.js b/devtools/client/debugger/src/actions/ast/setInScopeLines.js
new file mode 100644
index 0000000000..72bd33b59f
--- /dev/null
+++ b/devtools/client/debugger/src/actions/ast/setInScopeLines.js
@@ -0,0 +1,98 @@
+/* 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 {
+ hasInScopeLines,
+ getSourceTextContent,
+ getVisibleSelectedFrame,
+} from "../../selectors/index";
+
+import { getSourceLineCount } from "../../utils/source";
+
+import { isFulfilled } from "../../utils/async-value";
+
+function getOutOfScopeLines(outOfScopeLocations) {
+ if (!outOfScopeLocations) {
+ return null;
+ }
+
+ const uniqueLines = new Set();
+ for (const location of outOfScopeLocations) {
+ for (let i = location.start.line; i < location.end.line; i++) {
+ uniqueLines.add(i);
+ }
+ }
+
+ return uniqueLines;
+}
+
+async function getInScopeLines(
+ location,
+ sourceTextContent,
+ { dispatch, getState, parserWorker }
+) {
+ let locations = null;
+ if (location.line && parserWorker.isLocationSupported(location)) {
+ locations = await parserWorker.findOutOfScopeLocations(location);
+ }
+
+ const linesOutOfScope = getOutOfScopeLines(locations);
+ const sourceNumLines =
+ !sourceTextContent || !isFulfilled(sourceTextContent)
+ ? 0
+ : getSourceLineCount(sourceTextContent.value);
+
+ const noLinesOutOfScope =
+ linesOutOfScope == null || linesOutOfScope.size == 0;
+
+ // This operation can be very costly for large files so we sacrifice a bit of readability
+ // for performance sake.
+ // We initialize an array with a fixed size and we'll directly assign value for lines
+ // that are not out of scope. This is much faster than having an empty array and pushing
+ // into it.
+ const sourceLines = new Array(sourceNumLines);
+ for (let i = 0; i < sourceNumLines; i++) {
+ const line = i + 1;
+ if (noLinesOutOfScope || !linesOutOfScope.has(line)) {
+ sourceLines[i] = line;
+ }
+ }
+
+ // Finally we need to remove any undefined values, i.e. the ones that were matching
+ // out of scope lines.
+ return sourceLines.filter(i => i != undefined);
+}
+
+export function setInScopeLines() {
+ return async thunkArgs => {
+ const { getState, dispatch } = thunkArgs;
+ const visibleFrame = getVisibleSelectedFrame(getState());
+
+ if (!visibleFrame) {
+ return;
+ }
+
+ const { location } = visibleFrame;
+ const sourceTextContent = getSourceTextContent(getState(), location);
+
+ // Ignore if in scope lines have already be computed, or if the selected location
+ // doesn't have its content already fully fetched.
+ // The ParserWorker will only have the source text content once the source text content is fulfilled.
+ if (
+ hasInScopeLines(getState(), location) ||
+ !sourceTextContent ||
+ !isFulfilled(sourceTextContent)
+ ) {
+ return;
+ }
+
+ const lines = await getInScopeLines(location, sourceTextContent, thunkArgs);
+
+ dispatch({
+ type: "IN_SCOPE_LINES",
+ location,
+ lines,
+ });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js b/devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js
new file mode 100644
index 0000000000..3729fd2741
--- /dev/null
+++ b/devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js
@@ -0,0 +1,331 @@
+/* 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 } from "devtools/client/shared/source-map-loader/index";
+
+import {
+ getSource,
+ getSourceFromId,
+ getBreakpointPositionsForSource,
+ getSourceActorsForSource,
+} from "../../selectors/index";
+
+import { makeBreakpointId } from "../../utils/breakpoint/index";
+import { memoizeableAction } from "../../utils/memoizableAction";
+import { fulfilled } from "../../utils/async-value";
+import {
+ sourceMapToDebuggerLocation,
+ createLocation,
+} from "../../utils/location";
+import { validateSource } from "../../utils/context";
+
+/**
+ * Helper function which consumes breakpoints positions sent by the server
+ * and map them to location objects.
+ * During this process, the SourceMapLoader will be queried to map the positions from generated to original locations.
+ *
+ * @param {Object} breakpointPositions
+ * The positions to map related to the generated source:
+ * {
+ * 1: [ 2, 6 ], // Line 1 is breakable on column 2 and 6
+ * 2: [ 2 ], // Line 2 is only breakable on column 2
+ * }
+ * @param {Object} generatedSource
+ * @param {Object} location
+ * The current location we are computing breakable positions.
+ * @param {Object} thunk arguments
+ * @return {Object}
+ * The mapped breakable locations in the original source:
+ * {
+ * 1: [ { source, line: 1, column: 2} , { source, line: 1, column 6 } ], // Line 1 is not mapped as location are same as breakpointPositions.
+ * 10: [ { source, line: 10, column: 28 } ], // Line 2 is mapped and locations and line key refers to the original source positions.
+ * }
+ */
+async function mapToLocations(
+ breakpointPositions,
+ generatedSource,
+ mappedLocation,
+ { getState, sourceMapLoader }
+) {
+ // Map breakable positions from generated to original locations.
+ let mappedBreakpointPositions = await sourceMapLoader.getOriginalLocations(
+ breakpointPositions,
+ generatedSource.id
+ );
+ // The Source Map Loader will return null when there is no source map for that generated source.
+ // Consider the map as unrelated to source map and process the source actor positions as-is.
+ if (!mappedBreakpointPositions) {
+ mappedBreakpointPositions = breakpointPositions;
+ }
+
+ const positions = {};
+
+ // Ensure that we have an entry for the line fetched
+ if (typeof mappedLocation.line === "number") {
+ positions[mappedLocation.line] = [];
+ }
+
+ const handledBreakpointIds = new Set();
+ const isOriginal = mappedLocation.source.isOriginal;
+ const originalSourceId = mappedLocation.source.id;
+
+ for (let line in mappedBreakpointPositions) {
+ // createLocation expects a number and not a string.
+ line = parseInt(line, 10);
+ for (const columnOrSourceMapLocation of mappedBreakpointPositions[line]) {
+ let location, generatedLocation;
+
+ // When processing a source unrelated to source map, `mappedBreakpointPositions` will be equal to `breakpointPositions`.
+ // and columnOrSourceMapLocation will always be a number.
+ // But it will also be a number if we process a source mapped file and SourceMapLoader didn't find any valid mapping
+ // for the current position (line and column).
+ // When this happen to be a number it means it isn't mapped and columnOrSourceMapLocation refers to the column index.
+ if (typeof columnOrSourceMapLocation == "number") {
+ // If columnOrSourceMapLocation is a number, it means that this location doesn't mapped to an original source.
+ // So if we are currently computation positions for an original source, we can skip this breakable positions.
+ if (isOriginal) {
+ continue;
+ }
+ location = generatedLocation = createLocation({
+ line,
+ column: columnOrSourceMapLocation,
+ source: generatedSource,
+ });
+ } else {
+ // Otherwise, for sources which are mapped. `columnOrSourceMapLocation` will be a SourceMapLoader location object.
+ // This location object will refer to the location where the current column (columnOrSourceMapLocation.generatedColumn)
+ // mapped in the original file.
+
+ // When computing positions for an original source, ignore the location if that mapped to another original source.
+ if (
+ isOriginal &&
+ columnOrSourceMapLocation.sourceId != originalSourceId
+ ) {
+ continue;
+ }
+
+ location = sourceMapToDebuggerLocation(
+ getState(),
+ columnOrSourceMapLocation
+ );
+
+ // Merge positions that refer to duplicated positions.
+ // Some sourcemaped positions might refer to the exact same source/line/column triple.
+ const breakpointId = makeBreakpointId(location);
+ if (handledBreakpointIds.has(breakpointId)) {
+ continue;
+ }
+ handledBreakpointIds.add(breakpointId);
+
+ generatedLocation = createLocation({
+ line,
+ column: columnOrSourceMapLocation.generatedColumn,
+ source: generatedSource,
+ });
+ }
+
+ // The positions stored in redux will be keyed by original source's line (if we are
+ // computing the original source positions), or the generated source line.
+ // Note that when we compute the bundle positions, location may refer to the original source,
+ // but we still want to use the generated location as key.
+ const keyLocation = isOriginal ? location : generatedLocation;
+ const keyLine = keyLocation.line;
+ if (!positions[keyLine]) {
+ positions[keyLine] = [];
+ }
+ positions[keyLine].push({ location, generatedLocation });
+ }
+ }
+
+ return positions;
+}
+
+async function _setBreakpointPositions(location, thunkArgs) {
+ const { client, dispatch, getState, sourceMapLoader } = thunkArgs;
+ const results = {};
+ let generatedSource = location.source;
+ if (location.source.isOriginal) {
+ const ranges = await sourceMapLoader.getGeneratedRangesForOriginal(
+ location.source.id,
+ true
+ );
+ const generatedSourceId = originalToGeneratedId(location.source.id);
+ generatedSource = getSourceFromId(getState(), generatedSourceId);
+
+ // Note: While looping here may not look ideal, in the vast majority of
+ // cases, the number of ranges here should be very small, and is quite
+ // likely to only be a single range.
+ for (const range of ranges) {
+ // Wrap infinite end positions to the next line to keep things simple
+ // and because we know we don't care about the end-line whitespace
+ // in this case.
+ if (range.end.column === Infinity) {
+ range.end = {
+ line: range.end.line + 1,
+ column: 0,
+ };
+ }
+
+ // Retrieve the positions for all the source actors for the related generated source.
+ // There might be many if it is loaded many times.
+ // We limit the retrieval of positions within the given range, so that we don't
+ // retrieve the whole bundle positions.
+ const allActorsPositions = await Promise.all(
+ getSourceActorsForSource(getState(), generatedSourceId).map(actor =>
+ client.getSourceActorBreakpointPositions(actor, range)
+ )
+ );
+
+ // `allActorsPositions` looks like this:
+ // [
+ // { // Positions for the first source actor
+ // 1: [ 2, 6 ], // Line 1 is breakable on column 2 and 6
+ // 2: [ 2 ], // Line 2 is only breakable on column 2
+ // },
+ // {...} // Positions for another source actor
+ // ]
+ for (const actorPositions of allActorsPositions) {
+ for (const rangeLine in actorPositions) {
+ const columns = actorPositions[rangeLine];
+
+ // Merge all actors's breakable columns and avoid duplication of columns reported as breakable
+ const existing = results[rangeLine];
+ if (existing) {
+ for (const column of columns) {
+ if (!existing.includes(column)) {
+ existing.push(column);
+ }
+ }
+ } else {
+ results[rangeLine] = columns;
+ }
+ }
+ }
+ }
+ } else {
+ const { line } = location;
+ if (typeof line !== "number") {
+ throw new Error("Line is required for generated sources");
+ }
+
+ // We only retrieve the positions for the given requested line, that, for each source actor.
+ // There might be many source actor, if it is loaded many times.
+ // Or if this is an html page, with many inline scripts.
+ const allActorsBreakableColumns = await Promise.all(
+ getSourceActorsForSource(getState(), location.source.id).map(
+ async actor => {
+ const positions = await client.getSourceActorBreakpointPositions(
+ actor,
+ {
+ // Only retrieve positions for the given line
+ start: { line, column: 0 },
+ end: { line: line + 1, column: 0 },
+ }
+ );
+ return positions[line] || [];
+ }
+ )
+ );
+
+ for (const columns of allActorsBreakableColumns) {
+ // Merge all actors's breakable columns and avoid duplication of columns reported as breakable
+ const existing = results[line];
+ if (existing) {
+ for (const column of columns) {
+ if (!existing.includes(column)) {
+ existing.push(column);
+ }
+ }
+ } else {
+ results[line] = columns;
+ }
+ }
+ }
+
+ const positions = await mapToLocations(
+ results,
+ generatedSource,
+ location,
+ thunkArgs
+ );
+ // `mapToLocations` may compute for a little while asynchronously,
+ // ensure that the location is still valid before continuing.
+ validateSource(getState(), location.source);
+
+ dispatch({
+ type: "ADD_BREAKPOINT_POSITIONS",
+ source: location.source,
+ positions,
+ });
+}
+
+function generatedSourceActorKey(state, source) {
+ const generatedSource = getSource(
+ state,
+ source.isOriginal ? originalToGeneratedId(source.id) : source.id
+ );
+ const actors = generatedSource
+ ? getSourceActorsForSource(state, generatedSource.id).map(
+ ({ actor }) => actor
+ )
+ : [];
+ return [source.id, ...actors].join(":");
+}
+
+/**
+ * This method will force retrieving the breakable positions for a given source, on a given line.
+ * If this data has already been computed, it will returned the cached data.
+ *
+ * For original sources, this will query the SourceMap worker.
+ * For generated sources, this will query the DevTools server and the related source actors.
+ *
+ * @param Object options
+ * Dictionary object with many arguments:
+ * @param String options.sourceId
+ * The source we want to fetch breakable positions
+ * @param Number options.line
+ * The line we want to know which columns are breakable.
+ * (note that this seems to be optional for original sources)
+ * @return Array<Object>
+ * The list of all breakable positions, each object of this array will be like this:
+ * {
+ * line: Number
+ * column: Number
+ * source: Source object
+ * }
+ */
+export const setBreakpointPositions = memoizeableAction(
+ "setBreakpointPositions",
+ {
+ getValue: (location, { getState }) => {
+ const positions = getBreakpointPositionsForSource(
+ getState(),
+ location.source.id
+ );
+ if (!positions) {
+ return null;
+ }
+
+ if (
+ !location.source.isOriginal &&
+ location.line &&
+ !positions[location.line]
+ ) {
+ // We always return the full position dataset, but if a given line is
+ // not available, we treat the whole set as loading.
+ return null;
+ }
+
+ return fulfilled(positions);
+ },
+ createKey(location, { getState }) {
+ const key = generatedSourceActorKey(getState(), location.source);
+ return !location.source.isOriginal && location.line
+ ? `${key}-${location.line}`
+ : key;
+ },
+ action: async (location, thunkArgs) =>
+ _setBreakpointPositions(location, thunkArgs),
+ }
+);
diff --git a/devtools/client/debugger/src/actions/breakpoints/index.js b/devtools/client/debugger/src/actions/breakpoints/index.js
new file mode 100644
index 0000000000..2125ec9ec7
--- /dev/null
+++ b/devtools/client/debugger/src/actions/breakpoints/index.js
@@ -0,0 +1,395 @@
+/* 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/>. */
+
+/**
+ * Redux actions for breakpoints
+ * @module actions/breakpoints
+ */
+
+import { PROMISE } from "../utils/middleware/promise";
+import { asyncStore } from "../../utils/prefs";
+import { createLocation } from "../../utils/location";
+import {
+ getBreakpointsList,
+ getXHRBreakpoints,
+ getSelectedSource,
+ getBreakpointAtLocation,
+ getBreakpointsForSource,
+ getBreakpointsAtLine,
+} from "../../selectors/index";
+import { createXHRBreakpoint } from "../../utils/breakpoint/index";
+import {
+ addBreakpoint,
+ removeBreakpoint,
+ enableBreakpoint,
+ disableBreakpoint,
+} from "./modify";
+import { getOriginalLocation } from "../../utils/source-maps";
+
+export * from "./breakpointPositions";
+export * from "./modify";
+export * from "./syncBreakpoint";
+
+export function addHiddenBreakpoint(location) {
+ return ({ dispatch }) => {
+ return dispatch(addBreakpoint(location, { hidden: true }));
+ };
+}
+
+/**
+ * Disable all breakpoints in a source
+ *
+ * @memberof actions/breakpoints
+ * @static
+ */
+export function disableBreakpointsInSource(source) {
+ return async ({ dispatch, getState, client }) => {
+ const breakpoints = getBreakpointsForSource(getState(), source);
+ for (const breakpoint of breakpoints) {
+ if (!breakpoint.disabled) {
+ dispatch(disableBreakpoint(breakpoint));
+ }
+ }
+ };
+}
+
+/**
+ * Enable all breakpoints in a source
+ *
+ * @memberof actions/breakpoints
+ * @static
+ */
+export function enableBreakpointsInSource(source) {
+ return async ({ dispatch, getState, client }) => {
+ const breakpoints = getBreakpointsForSource(getState(), source);
+ for (const breakpoint of breakpoints) {
+ if (breakpoint.disabled) {
+ dispatch(enableBreakpoint(breakpoint));
+ }
+ }
+ };
+}
+
+/**
+ * Toggle All Breakpoints
+ *
+ * @memberof actions/breakpoints
+ * @static
+ */
+export function toggleAllBreakpoints(shouldDisableBreakpoints) {
+ return async ({ dispatch, getState, client }) => {
+ const breakpoints = getBreakpointsList(getState());
+
+ for (const breakpoint of breakpoints) {
+ if (shouldDisableBreakpoints) {
+ dispatch(disableBreakpoint(breakpoint));
+ } else {
+ dispatch(enableBreakpoint(breakpoint));
+ }
+ }
+ };
+}
+
+/**
+ * Toggle Breakpoints
+ *
+ * @memberof actions/breakpoints
+ * @static
+ */
+export function toggleBreakpoints(shouldDisableBreakpoints, breakpoints) {
+ return async ({ dispatch }) => {
+ const promises = breakpoints.map(breakpoint =>
+ shouldDisableBreakpoints
+ ? dispatch(disableBreakpoint(breakpoint))
+ : dispatch(enableBreakpoint(breakpoint))
+ );
+
+ await Promise.all(promises);
+ };
+}
+
+export function toggleBreakpointsAtLine(shouldDisableBreakpoints, line) {
+ return async ({ dispatch, getState }) => {
+ const breakpoints = getBreakpointsAtLine(getState(), line);
+ return dispatch(toggleBreakpoints(shouldDisableBreakpoints, breakpoints));
+ };
+}
+
+/**
+ * Removes all breakpoints
+ *
+ * @memberof actions/breakpoints
+ * @static
+ */
+export function removeAllBreakpoints() {
+ return async ({ dispatch, getState }) => {
+ const breakpointList = getBreakpointsList(getState());
+ await Promise.all(breakpointList.map(bp => dispatch(removeBreakpoint(bp))));
+ dispatch({ type: "CLEAR_BREAKPOINTS" });
+ };
+}
+
+/**
+ * Removes breakpoints
+ *
+ * @memberof actions/breakpoints
+ * @static
+ */
+export function removeBreakpoints(breakpoints) {
+ return async ({ dispatch }) => {
+ return Promise.all(breakpoints.map(bp => dispatch(removeBreakpoint(bp))));
+ };
+}
+
+/**
+ * Removes all breakpoints in a source
+ *
+ * @memberof actions/breakpoints
+ * @static
+ */
+export function removeBreakpointsInSource(source) {
+ return async ({ dispatch, getState, client }) => {
+ const breakpoints = getBreakpointsForSource(getState(), source);
+ for (const breakpoint of breakpoints) {
+ dispatch(removeBreakpoint(breakpoint));
+ }
+ };
+}
+
+/**
+ * Update the original location information of breakpoints.
+
+/*
+ * Update breakpoints for a source that just got pretty printed.
+ * This method maps the breakpoints currently set only against the
+ * non-pretty-printed (generated) source to the related pretty-printed
+ * (original) source by querying the SourceMap service.
+ *
+ * @param {String} source - the generated source
+ */
+export function updateBreakpointsForNewPrettyPrintedSource(source) {
+ return async thunkArgs => {
+ const { dispatch, getState } = thunkArgs;
+ if (source.isOriginal) {
+ console.error("Can't update breakpoints on original sources");
+ return;
+ }
+ const breakpoints = getBreakpointsForSource(getState(), source);
+ // Remap the breakpoints with the original location information from
+ // the pretty-printed source.
+ const newBreakpoints = await Promise.all(
+ breakpoints.map(async breakpoint => {
+ const location = await getOriginalLocation(
+ breakpoint.generatedLocation,
+ thunkArgs
+ );
+ return { ...breakpoint, location };
+ })
+ );
+
+ // Normally old breakpoints will be clobbered if we re-add them, but when
+ // remapping we have changed the source maps and the old breakpoints will
+ // have different locations than the new ones. Manually remove the
+ // old breakpoints before adding the new ones.
+ for (const bp of breakpoints) {
+ dispatch(removeBreakpoint(bp));
+ }
+
+ for (const bp of newBreakpoints) {
+ await dispatch(addBreakpoint(bp.location, bp.options, bp.disabled));
+ }
+ };
+}
+
+export function toggleBreakpointAtLine(line) {
+ return ({ dispatch, getState }) => {
+ const state = getState();
+ const selectedSource = getSelectedSource(state);
+
+ if (!selectedSource) {
+ return null;
+ }
+
+ const bp = getBreakpointAtLocation(state, { line, column: undefined });
+ if (bp) {
+ return dispatch(removeBreakpoint(bp));
+ }
+ return dispatch(
+ addBreakpoint(
+ createLocation({
+ source: selectedSource,
+ line,
+ })
+ )
+ );
+ };
+}
+
+export function addBreakpointAtLine(line, shouldLog = false, disabled = false) {
+ return ({ dispatch, getState }) => {
+ const state = getState();
+ const source = getSelectedSource(state);
+
+ if (!source) {
+ return null;
+ }
+ const breakpointLocation = createLocation({
+ source,
+ column: undefined,
+ line,
+ });
+
+ const options = {};
+ if (shouldLog) {
+ options.logValue = "displayName";
+ }
+
+ return dispatch(addBreakpoint(breakpointLocation, options, disabled));
+ };
+}
+
+export function removeBreakpointsAtLine(source, line) {
+ return ({ dispatch, getState }) => {
+ const breakpointsAtLine = getBreakpointsForSource(getState(), source, line);
+ return dispatch(removeBreakpoints(breakpointsAtLine));
+ };
+}
+
+export function disableBreakpointsAtLine(source, line) {
+ return ({ dispatch, getState }) => {
+ const breakpointsAtLine = getBreakpointsForSource(getState(), source, line);
+ return dispatch(toggleBreakpoints(true, breakpointsAtLine));
+ };
+}
+
+export function enableBreakpointsAtLine(source, line) {
+ return ({ dispatch, getState }) => {
+ const breakpointsAtLine = getBreakpointsForSource(getState(), source, line);
+ return dispatch(toggleBreakpoints(false, breakpointsAtLine));
+ };
+}
+
+export function toggleDisabledBreakpoint(breakpoint) {
+ return ({ dispatch, getState }) => {
+ if (!breakpoint.disabled) {
+ return dispatch(disableBreakpoint(breakpoint));
+ }
+ return dispatch(enableBreakpoint(breakpoint));
+ };
+}
+
+export function enableXHRBreakpoint(index, bp) {
+ return ({ dispatch, getState, client }) => {
+ const xhrBreakpoints = getXHRBreakpoints(getState());
+ const breakpoint = bp || xhrBreakpoints[index];
+ const enabledBreakpoint = {
+ ...breakpoint,
+ disabled: false,
+ };
+
+ return dispatch({
+ type: "ENABLE_XHR_BREAKPOINT",
+ breakpoint: enabledBreakpoint,
+ index,
+ [PROMISE]: client.setXHRBreakpoint(breakpoint.path, breakpoint.method),
+ });
+ };
+}
+
+export function disableXHRBreakpoint(index, bp) {
+ return ({ dispatch, getState, client }) => {
+ const xhrBreakpoints = getXHRBreakpoints(getState());
+ const breakpoint = bp || xhrBreakpoints[index];
+ const disabledBreakpoint = {
+ ...breakpoint,
+ disabled: true,
+ };
+
+ return dispatch({
+ type: "DISABLE_XHR_BREAKPOINT",
+ breakpoint: disabledBreakpoint,
+ index,
+ [PROMISE]: client.removeXHRBreakpoint(breakpoint.path, breakpoint.method),
+ });
+ };
+}
+
+export function updateXHRBreakpoint(index, path, method) {
+ return ({ dispatch, getState, client }) => {
+ const xhrBreakpoints = getXHRBreakpoints(getState());
+ const breakpoint = xhrBreakpoints[index];
+
+ const updatedBreakpoint = {
+ ...breakpoint,
+ path,
+ method,
+ text: L10N.getFormatStr("xhrBreakpoints.item.label", path),
+ };
+
+ return dispatch({
+ type: "UPDATE_XHR_BREAKPOINT",
+ breakpoint: updatedBreakpoint,
+ index,
+ [PROMISE]: Promise.all([
+ client.removeXHRBreakpoint(breakpoint.path, breakpoint.method),
+ client.setXHRBreakpoint(path, method),
+ ]),
+ });
+ };
+}
+export function togglePauseOnAny() {
+ return ({ dispatch, getState }) => {
+ const xhrBreakpoints = getXHRBreakpoints(getState());
+ const index = xhrBreakpoints.findIndex(({ path }) => path.length === 0);
+ if (index < 0) {
+ return dispatch(setXHRBreakpoint("", "ANY"));
+ }
+
+ const bp = xhrBreakpoints[index];
+ if (bp.disabled) {
+ return dispatch(enableXHRBreakpoint(index, bp));
+ }
+
+ return dispatch(disableXHRBreakpoint(index, bp));
+ };
+}
+
+export function setXHRBreakpoint(path, method) {
+ return ({ dispatch, getState, client }) => {
+ const breakpoint = createXHRBreakpoint(path, method);
+
+ return dispatch({
+ type: "SET_XHR_BREAKPOINT",
+ breakpoint,
+ [PROMISE]: client.setXHRBreakpoint(path, method),
+ });
+ };
+}
+
+export function removeAllXHRBreakpoints() {
+ return async ({ dispatch, getState, client }) => {
+ const xhrBreakpoints = getXHRBreakpoints(getState());
+ const promises = xhrBreakpoints.map(breakpoint =>
+ client.removeXHRBreakpoint(breakpoint.path, breakpoint.method)
+ );
+ await dispatch({
+ type: "CLEAR_XHR_BREAKPOINTS",
+ [PROMISE]: Promise.all(promises),
+ });
+ asyncStore.xhrBreakpoints = [];
+ };
+}
+
+export function removeXHRBreakpoint(index) {
+ return ({ dispatch, getState, client }) => {
+ const xhrBreakpoints = getXHRBreakpoints(getState());
+ const breakpoint = xhrBreakpoints[index];
+ return dispatch({
+ type: "REMOVE_XHR_BREAKPOINT",
+ breakpoint,
+ index,
+ [PROMISE]: client.removeXHRBreakpoint(breakpoint.path, breakpoint.method),
+ });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/breakpoints/modify.js b/devtools/client/debugger/src/actions/breakpoints/modify.js
new file mode 100644
index 0000000000..b083baf7ea
--- /dev/null
+++ b/devtools/client/debugger/src/actions/breakpoints/modify.js
@@ -0,0 +1,368 @@
+/* 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 { createBreakpoint } from "../../client/firefox/create";
+import {
+ makeBreakpointServerLocation,
+ makeBreakpointId,
+} from "../../utils/breakpoint/index";
+import {
+ getBreakpoint,
+ getBreakpointPositionsForLocation,
+ getFirstBreakpointPosition,
+ getSettledSourceTextContent,
+ getBreakpointsList,
+ getPendingBreakpointList,
+ isMapScopesEnabled,
+ getBlackBoxRanges,
+ isSourceMapIgnoreListEnabled,
+ isSourceOnSourceMapIgnoreList,
+} from "../../selectors/index";
+
+import { setBreakpointPositions } from "./breakpointPositions";
+import { setSkipPausing } from "../pause/skipPausing";
+
+import { PROMISE } from "../utils/middleware/promise";
+import { recordEvent } from "../../utils/telemetry";
+import { comparePosition } from "../../utils/location";
+import { getTextAtPosition, isLineBlackboxed } from "../../utils/source";
+import { getMappedScopesForLocation } from "../pause/mapScopes";
+import { validateBreakpoint } from "../../utils/context";
+
+// This file has the primitive operations used to modify individual breakpoints
+// and keep them in sync with the breakpoints installed on server threads. These
+// are collected here to make it easier to preserve the following invariant:
+//
+// Breakpoints are included in reducer state if they are disabled or requests
+// have been dispatched to set them in all server threads.
+//
+// To maintain this property, updates to the reducer and installed breakpoints
+// must happen with no intervening await. Using await allows other operations to
+// modify the breakpoint state in the interim and potentially cause breakpoint
+// state to go out of sync.
+//
+// The reducer is optimistically updated when users set or remove a breakpoint,
+// but it might take a little while before the breakpoints have been set or
+// removed in each thread. Once all outstanding requests sent to a thread have
+// been processed, the reducer and server threads will be in sync.
+//
+// There is another exception to the above invariant when first connecting to
+// the server: breakpoints have been installed on all generated locations in the
+// pending breakpoints, but no breakpoints have been added to the reducer. When
+// a matching source appears, either the server breakpoint will be removed or a
+// breakpoint will be added to the reducer, to restore the above invariant.
+// See syncBreakpoint.js for more.
+
+async function clientSetBreakpoint(client, { getState, dispatch }, breakpoint) {
+ const breakpointServerLocation = makeBreakpointServerLocation(
+ getState(),
+ breakpoint.generatedLocation
+ );
+ const shouldMapBreakpointExpressions =
+ isMapScopesEnabled(getState()) &&
+ breakpoint.location.source.isOriginal &&
+ (breakpoint.options.logValue || breakpoint.options.condition);
+
+ if (shouldMapBreakpointExpressions) {
+ breakpoint = await dispatch(updateBreakpointSourceMapping(breakpoint));
+ }
+ return client.setBreakpoint(breakpointServerLocation, breakpoint.options);
+}
+
+function clientRemoveBreakpoint(client, state, generatedLocation) {
+ const breakpointServerLocation = makeBreakpointServerLocation(
+ state,
+ generatedLocation
+ );
+ return client.removeBreakpoint(breakpointServerLocation);
+}
+
+export function enableBreakpoint(initialBreakpoint) {
+ return thunkArgs => {
+ const { dispatch, getState, client } = thunkArgs;
+ const state = getState();
+ const breakpoint = getBreakpoint(state, initialBreakpoint.location);
+ const blackboxedRanges = getBlackBoxRanges(state);
+ const isSourceOnIgnoreList =
+ isSourceMapIgnoreListEnabled(state) &&
+ isSourceOnSourceMapIgnoreList(state, breakpoint.location.source);
+ if (
+ !breakpoint ||
+ !breakpoint.disabled ||
+ isLineBlackboxed(
+ blackboxedRanges[breakpoint.location.source.url],
+ breakpoint.location.line,
+ isSourceOnIgnoreList
+ )
+ ) {
+ return null;
+ }
+
+ dispatch(setSkipPausing(false));
+ return dispatch({
+ type: "SET_BREAKPOINT",
+ breakpoint: createBreakpoint({ ...breakpoint, disabled: false }),
+ [PROMISE]: clientSetBreakpoint(client, thunkArgs, breakpoint),
+ });
+ };
+}
+
+export function addBreakpoint(
+ initialLocation,
+ options = {},
+ disabled,
+ shouldCancel = () => false
+) {
+ return async thunkArgs => {
+ const { dispatch, getState, client } = thunkArgs;
+ recordEvent("add_breakpoint");
+
+ await dispatch(setBreakpointPositions(initialLocation));
+
+ const position = initialLocation.column
+ ? getBreakpointPositionsForLocation(getState(), initialLocation)
+ : getFirstBreakpointPosition(getState(), initialLocation);
+
+ // No position is found if the `initialLocation` is on a non-breakable line or
+ // the line no longer exists.
+ if (!position) {
+ return null;
+ }
+
+ const { location, generatedLocation } = position;
+
+ if (!location.source || !generatedLocation.source) {
+ return null;
+ }
+
+ const originalContent = getSettledSourceTextContent(getState(), location);
+ const originalText = getTextAtPosition(
+ location.source.id,
+ originalContent,
+ location
+ );
+
+ const content = getSettledSourceTextContent(getState(), generatedLocation);
+ const text = getTextAtPosition(
+ generatedLocation.source.id,
+ content,
+ generatedLocation
+ );
+
+ const id = makeBreakpointId(location);
+ const breakpoint = createBreakpoint({
+ id,
+ disabled,
+ options,
+ location,
+ generatedLocation,
+ text,
+ originalText,
+ });
+
+ if (shouldCancel()) {
+ return null;
+ }
+
+ dispatch(setSkipPausing(false));
+ return dispatch({
+ type: "SET_BREAKPOINT",
+ breakpoint,
+ // If we just clobbered an enabled breakpoint with a disabled one, we need
+ // to remove any installed breakpoint in the server.
+ [PROMISE]: disabled
+ ? clientRemoveBreakpoint(client, getState(), generatedLocation)
+ : clientSetBreakpoint(client, thunkArgs, breakpoint),
+ });
+ };
+}
+
+/**
+ * Remove a single breakpoint
+ *
+ * @memberof actions/breakpoints
+ * @static
+ */
+export function removeBreakpoint(initialBreakpoint) {
+ return ({ dispatch, getState, client }) => {
+ recordEvent("remove_breakpoint");
+
+ const breakpoint = getBreakpoint(getState(), initialBreakpoint.location);
+ if (!breakpoint) {
+ return null;
+ }
+
+ dispatch(setSkipPausing(false));
+ return dispatch({
+ type: "REMOVE_BREAKPOINT",
+ breakpoint,
+ // If the breakpoint is disabled then it is not installed in the server.
+ [PROMISE]: breakpoint.disabled
+ ? Promise.resolve()
+ : clientRemoveBreakpoint(
+ client,
+ getState(),
+ breakpoint.generatedLocation
+ ),
+ });
+ };
+}
+
+/**
+ * Remove all installed, pending, and client breakpoints associated with a
+ * target generated location.
+ *
+ * @param {Object} target
+ * Location object where to remove breakpoints.
+ */
+export function removeBreakpointAtGeneratedLocation(target) {
+ return ({ dispatch, getState, client }) => {
+ // remove breakpoint from the server
+ const onBreakpointRemoved = clientRemoveBreakpoint(
+ client,
+ getState(),
+ target
+ );
+ // Remove any breakpoints matching the generated location.
+ const breakpoints = getBreakpointsList(getState());
+ for (const breakpoint of breakpoints) {
+ const { generatedLocation } = breakpoint;
+ if (
+ generatedLocation.source.id == target.source.id &&
+ comparePosition(generatedLocation, target)
+ ) {
+ dispatch({
+ type: "REMOVE_BREAKPOINT",
+ breakpoint,
+ [PROMISE]: onBreakpointRemoved,
+ });
+ }
+ }
+
+ // Remove any remaining pending breakpoints matching the generated location.
+ const pending = getPendingBreakpointList(getState());
+ for (const pendingBreakpoint of pending) {
+ const { generatedLocation } = pendingBreakpoint;
+ if (
+ generatedLocation.sourceUrl == target.source.url &&
+ comparePosition(generatedLocation, target)
+ ) {
+ dispatch({
+ type: "REMOVE_PENDING_BREAKPOINT",
+ pendingBreakpoint,
+ });
+ }
+ }
+ return onBreakpointRemoved;
+ };
+}
+
+/**
+ * Disable a single breakpoint
+ *
+ * @memberof actions/breakpoints
+ * @static
+ */
+export function disableBreakpoint(initialBreakpoint) {
+ return ({ dispatch, getState, client }) => {
+ const breakpoint = getBreakpoint(getState(), initialBreakpoint.location);
+ if (!breakpoint || breakpoint.disabled) {
+ return null;
+ }
+
+ dispatch(setSkipPausing(false));
+ return dispatch({
+ type: "SET_BREAKPOINT",
+ breakpoint: createBreakpoint({ ...breakpoint, disabled: true }),
+ [PROMISE]: clientRemoveBreakpoint(
+ client,
+ getState(),
+ breakpoint.generatedLocation
+ ),
+ });
+ };
+}
+
+/**
+ * Update the options of a breakpoint.
+ *
+ * @throws {Error} "not implemented"
+ * @memberof actions/breakpoints
+ * @static
+ * @param {SourceLocation} location
+ * @see DebuggerController.Breakpoints.addBreakpoint
+ * @param {Object} options
+ * Any options to set on the breakpoint
+ */
+export function setBreakpointOptions(location, options = {}) {
+ return thunkArgs => {
+ const { dispatch, getState, client } = thunkArgs;
+ let breakpoint = getBreakpoint(getState(), location);
+ if (!breakpoint) {
+ return dispatch(addBreakpoint(location, options));
+ }
+
+ // Note: setting a breakpoint's options implicitly enables it.
+ breakpoint = createBreakpoint({ ...breakpoint, disabled: false, options });
+
+ return dispatch({
+ type: "SET_BREAKPOINT",
+ breakpoint,
+ [PROMISE]: clientSetBreakpoint(client, thunkArgs, breakpoint),
+ });
+ };
+}
+
+async function updateExpression(parserWorker, mappings, originalExpression) {
+ const mapped = await parserWorker.mapExpression(
+ originalExpression,
+ mappings,
+ [],
+ false,
+ false
+ );
+ if (!mapped) {
+ return originalExpression;
+ }
+ if (!originalExpression.trimEnd().endsWith(";")) {
+ return mapped.expression.replace(/;$/, "");
+ }
+ return mapped.expression;
+}
+
+function updateBreakpointSourceMapping(breakpoint) {
+ return async ({ getState, dispatch, parserWorker }) => {
+ const options = { ...breakpoint.options };
+
+ const mappedScopes = await dispatch(
+ getMappedScopesForLocation(breakpoint.location)
+ );
+ if (!mappedScopes) {
+ return breakpoint;
+ }
+ const { mappings } = mappedScopes;
+
+ if (options.condition) {
+ options.condition = await updateExpression(
+ parserWorker,
+ mappings,
+ options.condition
+ );
+ }
+ if (options.logValue) {
+ options.logValue = await updateExpression(
+ parserWorker,
+ mappings,
+ options.logValue
+ );
+ }
+
+ // As we waited for lots of asynchronous operations,
+ // verify that the breakpoint is still valid before
+ // trying to set/update it on the server.
+ validateBreakpoint(getState(), breakpoint);
+
+ return { ...breakpoint, options };
+ };
+}
diff --git a/devtools/client/debugger/src/actions/breakpoints/moz.build b/devtools/client/debugger/src/actions/breakpoints/moz.build
new file mode 100644
index 0000000000..65910c4ef2
--- /dev/null
+++ b/devtools/client/debugger/src/actions/breakpoints/moz.build
@@ -0,0 +1,13 @@
+# 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(
+ "breakpointPositions.js",
+ "index.js",
+ "modify.js",
+ "syncBreakpoint.js",
+)
diff --git a/devtools/client/debugger/src/actions/breakpoints/syncBreakpoint.js b/devtools/client/debugger/src/actions/breakpoints/syncBreakpoint.js
new file mode 100644
index 0000000000..b24912de58
--- /dev/null
+++ b/devtools/client/debugger/src/actions/breakpoints/syncBreakpoint.js
@@ -0,0 +1,126 @@
+/* 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 { setBreakpointPositions } from "./breakpointPositions";
+import {
+ findPosition,
+ makeBreakpointServerLocation,
+} from "../../utils/breakpoint/index";
+
+import { comparePosition, createLocation } from "../../utils/location";
+
+import { originalToGeneratedId } from "devtools/client/shared/source-map-loader/index";
+import { getSource } from "../../selectors/index";
+import { addBreakpoint, removeBreakpointAtGeneratedLocation } from "./modify";
+
+async function findBreakpointPosition({ getState, dispatch }, location) {
+ const positions = await dispatch(setBreakpointPositions(location));
+
+ const position = findPosition(positions, location);
+ return position;
+}
+
+// Breakpoint syncing occurs when a source is found that matches either the
+// original or generated URL of a pending breakpoint. A new breakpoint is
+// constructed that might have a different original and/or generated location,
+// if the original source has changed since the pending breakpoint was created.
+// There are a couple subtle aspects to syncing:
+//
+// - We handle both the original and generated source because there is no
+// guarantee that seeing the generated source means we will also see the
+// original source. When connecting, a breakpoint will be installed in the
+// client for the generated location in the pending breakpoint, and we need
+// to make sure that either a breakpoint is added to the reducer or that this
+// client breakpoint is deleted.
+//
+// - If we see both the original and generated sources and the source mapping
+// has changed, we need to make sure that only a single breakpoint is added
+// to the reducer for the new location corresponding to the original location
+// in the pending breakpoint.
+export function syncPendingBreakpoint(source, pendingBreakpoint) {
+ return async thunkArgs => {
+ const { getState, client, dispatch } = thunkArgs;
+
+ const generatedSourceId = source.isOriginal
+ ? originalToGeneratedId(source.id)
+ : source.id;
+
+ const generatedSource = getSource(getState(), generatedSourceId);
+
+ if (!source || !generatedSource) {
+ return null;
+ }
+
+ // /!\ Pending breakpoint locations come only with sourceUrl, line and column attributes.
+ // We have to map it to a specific source object and avoid trying to query its non-existent 'source' attribute.
+ const { location, generatedLocation } = pendingBreakpoint;
+ const isPendingBreakpointWithSourceMap =
+ location.sourceUrl != generatedLocation.sourceUrl;
+ const sourceGeneratedLocation = createLocation({
+ ...generatedLocation,
+ source: generatedSource,
+ });
+
+ if (source == generatedSource && isPendingBreakpointWithSourceMap) {
+ // We are handling the generated source and the pending breakpoint has a
+ // source mapping. Supply a cancellation callback that will abort the
+ // breakpoint if the original source was synced to a different location,
+ // in which case the client breakpoint has been removed.
+ const breakpointServerLocation = makeBreakpointServerLocation(
+ getState(),
+ sourceGeneratedLocation
+ );
+ return dispatch(
+ addBreakpoint(
+ sourceGeneratedLocation,
+ pendingBreakpoint.options,
+ pendingBreakpoint.disabled,
+ () => !client.hasBreakpoint(breakpointServerLocation)
+ )
+ );
+ }
+
+ const originalLocation = createLocation({
+ ...location,
+ source,
+ });
+
+ const newPosition = await findBreakpointPosition(
+ thunkArgs,
+ originalLocation
+ );
+
+ const newGeneratedLocation = newPosition?.generatedLocation;
+ if (!newGeneratedLocation) {
+ // We couldn't find a new mapping for the breakpoint. If there is a source
+ // mapping, remove any breakpoints for the generated location, as if the
+ // breakpoint moved. If the old generated location still maps to an
+ // original location then we don't want to add a breakpoint for it.
+ if (isPendingBreakpointWithSourceMap) {
+ dispatch(removeBreakpointAtGeneratedLocation(sourceGeneratedLocation));
+ }
+ return null;
+ }
+
+ const isSameLocation = comparePosition(
+ generatedLocation,
+ newGeneratedLocation
+ );
+
+ // If the new generated location has changed from that in the pending
+ // breakpoint, remove any breakpoint associated with the old generated
+ // location.
+ if (!isSameLocation) {
+ dispatch(removeBreakpointAtGeneratedLocation(sourceGeneratedLocation));
+ }
+
+ return dispatch(
+ addBreakpoint(
+ newGeneratedLocation,
+ pendingBreakpoint.options,
+ pendingBreakpoint.disabled
+ )
+ );
+ };
+}
diff --git a/devtools/client/debugger/src/actions/breakpoints/tests/__snapshots__/breakpoints.spec.js.snap b/devtools/client/debugger/src/actions/breakpoints/tests/__snapshots__/breakpoints.spec.js.snap
new file mode 100644
index 0000000000..e6abad25d5
--- /dev/null
+++ b/devtools/client/debugger/src/actions/breakpoints/tests/__snapshots__/breakpoints.spec.js.snap
@@ -0,0 +1,165 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`breakpoints should add a breakpoint 1`] = `
+Array [
+ Object {
+ "breakpoints": Array [
+ Object {
+ "disabled": false,
+ "generatedLocation": Object {
+ "column": 1,
+ "line": 2,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "a",
+ "group": "localhost:8000",
+ "path": "/examples/a",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "a",
+ "isExtension": false,
+ "isHTML": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "url": "http://localhost:8000/examples/a",
+ },
+ "sourceActor": null,
+ "sourceActorId": undefined,
+ },
+ "id": "a:2:1",
+ "location": Object {
+ "column": 1,
+ "line": 2,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "a",
+ "group": "localhost:8000",
+ "path": "/examples/a",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "a",
+ "isExtension": false,
+ "isHTML": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "url": "http://localhost:8000/examples/a",
+ },
+ "sourceActor": null,
+ "sourceActorId": undefined,
+ },
+ "options": Object {},
+ "originalText": "return a",
+ "text": "return a",
+ "thread": undefined,
+ },
+ ],
+ "filename": "a",
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "a",
+ "group": "localhost:8000",
+ "path": "/examples/a",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "a",
+ "isExtension": false,
+ "isHTML": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "url": "http://localhost:8000/examples/a",
+ },
+ },
+]
+`;
+
+exports[`breakpoints should not show a breakpoint that does not have text 1`] = `Array []`;
+
+exports[`breakpoints should show a disabled breakpoint that does not have text 1`] = `
+Array [
+ Object {
+ "breakpoints": Array [
+ Object {
+ "disabled": true,
+ "generatedLocation": Object {
+ "column": 1,
+ "line": 5,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "a",
+ "group": "localhost:8000",
+ "path": "/examples/a",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "a",
+ "isExtension": false,
+ "isHTML": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "url": "http://localhost:8000/examples/a",
+ },
+ "sourceActor": null,
+ "sourceActorId": undefined,
+ },
+ "id": "a:5:1",
+ "location": Object {
+ "column": 1,
+ "line": 5,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "a",
+ "group": "localhost:8000",
+ "path": "/examples/a",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "a",
+ "isExtension": false,
+ "isHTML": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "url": "http://localhost:8000/examples/a",
+ },
+ "sourceActor": null,
+ "sourceActorId": undefined,
+ },
+ "options": Object {},
+ "originalText": "",
+ "text": "",
+ "thread": undefined,
+ },
+ ],
+ "filename": "a",
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "a",
+ "group": "localhost:8000",
+ "path": "/examples/a",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "a",
+ "isExtension": false,
+ "isHTML": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "url": "http://localhost:8000/examples/a",
+ },
+ },
+]
+`;
diff --git a/devtools/client/debugger/src/actions/breakpoints/tests/breakpoints.spec.js b/devtools/client/debugger/src/actions/breakpoints/tests/breakpoints.spec.js
new file mode 100644
index 0000000000..8096379429
--- /dev/null
+++ b/devtools/client/debugger/src/actions/breakpoints/tests/breakpoints.spec.js
@@ -0,0 +1,476 @@
+/* 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 {
+ createStore,
+ selectors,
+ actions,
+ makeSource,
+ getTelemetryEvents,
+} from "../../../utils/test-head";
+
+import { mockCommandClient } from "../../tests/helpers/mockCommandClient";
+import { createLocation } from "../../../utils/location";
+
+jest.mock("../../../utils/prefs", () => ({
+ prefs: {
+ expressions: [],
+ },
+ asyncStore: {
+ pendingBreakpoints: {},
+ },
+ features: {
+ inlinePreview: true,
+ },
+}));
+
+function mockClient(positionsResponse = {}) {
+ return {
+ ...mockCommandClient,
+ setSkipPausing: jest.fn(),
+ getSourceActorBreakpointPositions: async () => positionsResponse,
+ getSourceActorBreakableLines: async () => [],
+ };
+}
+
+describe("breakpoints", () => {
+ it("should add a breakpoint", async () => {
+ const { dispatch, getState } = createStore(mockClient({ 2: [1] }));
+ const source = await dispatch(actions.newGeneratedSource(makeSource("a")));
+ source.url = "http://localhost:8000/examples/a";
+ const loc1 = createLocation({
+ source,
+ line: 2,
+ column: 1,
+ });
+ await dispatch(
+ actions.selectLocation(
+ createLocation({
+ source,
+ line: 1,
+ column: 1,
+ })
+ )
+ );
+
+ await dispatch(actions.addBreakpoint(loc1));
+
+ expect(selectors.getBreakpointCount(getState())).toEqual(1);
+ const bp = selectors.getBreakpoint(getState(), loc1);
+ expect(bp && bp.location).toEqual(loc1);
+ expect(getTelemetryEvents("add_breakpoint")).toHaveLength(1);
+
+ const bpSources = selectors.getBreakpointSources(getState());
+ expect(bpSources).toMatchSnapshot();
+ });
+
+ it("should not show a breakpoint that does not have text", async () => {
+ const { dispatch, getState } = createStore(mockClient({ 5: [1] }));
+ const source = await dispatch(actions.newGeneratedSource(makeSource("a")));
+ source.url = "http://localhost:8000/examples/a";
+ const loc1 = createLocation({
+ source,
+ line: 5,
+ column: 1,
+ });
+ await dispatch(
+ actions.selectLocation(
+ createLocation({
+ source,
+ line: 1,
+ column: 1,
+ })
+ )
+ );
+
+ await dispatch(actions.addBreakpoint(loc1));
+
+ expect(selectors.getBreakpointCount(getState())).toEqual(1);
+ const bp = selectors.getBreakpoint(getState(), loc1);
+ expect(bp && bp.location).toEqual(loc1);
+ expect(selectors.getBreakpointSources(getState())).toMatchSnapshot();
+ });
+
+ it("should show a disabled breakpoint that does not have text", async () => {
+ const { dispatch, getState } = createStore(mockClient({ 5: [1] }));
+ const source = await dispatch(actions.newGeneratedSource(makeSource("a")));
+ source.url = "http://localhost:8000/examples/a";
+ const loc1 = createLocation({
+ source,
+ line: 5,
+ column: 1,
+ });
+ await dispatch(
+ actions.selectLocation(
+ createLocation({
+ source,
+ line: 1,
+ column: 1,
+ })
+ )
+ );
+
+ await dispatch(actions.addBreakpoint(loc1));
+ const breakpoint = selectors.getBreakpoint(getState(), loc1);
+ if (!breakpoint) {
+ throw new Error("no breakpoint");
+ }
+
+ await dispatch(actions.disableBreakpoint(breakpoint));
+
+ expect(selectors.getBreakpointCount(getState())).toEqual(1);
+ const bp = selectors.getBreakpoint(getState(), loc1);
+ expect(bp && bp.location).toEqual(loc1);
+ expect(selectors.getBreakpointSources(getState())).toMatchSnapshot();
+ });
+
+ it("should not re-add a breakpoint", async () => {
+ const { dispatch, getState } = createStore(mockClient({ 5: [1] }));
+ const source = await dispatch(actions.newGeneratedSource(makeSource("a")));
+ source.url = "http://localhost:8000/examples/a";
+ const loc1 = createLocation({
+ source,
+ line: 5,
+ column: 1,
+ });
+ await dispatch(
+ actions.selectLocation(
+ createLocation({
+ source,
+ line: 1,
+ column: 1,
+ })
+ )
+ );
+
+ await dispatch(actions.addBreakpoint(loc1));
+ expect(selectors.getBreakpointCount(getState())).toEqual(1);
+ const bp = selectors.getBreakpoint(getState(), loc1);
+ expect(bp && bp.location).toEqual(loc1);
+
+ await dispatch(actions.addBreakpoint(loc1));
+ expect(selectors.getBreakpointCount(getState())).toEqual(1);
+ });
+
+ it("should remove a breakpoint", async () => {
+ const { dispatch, getState } = createStore(mockClient({ 5: [1], 6: [2] }));
+
+ const aSource = await dispatch(actions.newGeneratedSource(makeSource("a")));
+ aSource.url = "http://localhost:8000/examples/a";
+
+ const bSource = await dispatch(actions.newGeneratedSource(makeSource("b")));
+ bSource.url = "http://localhost:8000/examples/b";
+
+ const loc1 = createLocation({
+ source: aSource,
+ line: 5,
+ column: 1,
+ });
+
+ const loc2 = createLocation({
+ source: bSource,
+ line: 6,
+ column: 2,
+ });
+ const bSourceActor = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ bSource.id
+ );
+
+ await dispatch(actions.loadGeneratedSourceText(bSourceActor));
+
+ await dispatch(
+ actions.selectLocation(
+ createLocation({
+ source: aSource,
+ line: 1,
+ column: 1,
+ })
+ )
+ );
+
+ await dispatch(actions.addBreakpoint(loc1));
+ await dispatch(actions.addBreakpoint(loc2));
+
+ const bp = selectors.getBreakpoint(getState(), loc1);
+ if (!bp) {
+ throw new Error("no bp");
+ }
+ await dispatch(actions.removeBreakpoint(bp));
+
+ expect(selectors.getBreakpointCount(getState())).toEqual(1);
+ });
+
+ it("should disable a breakpoint", async () => {
+ const { dispatch, getState } = createStore(mockClient({ 5: [1], 6: [2] }));
+
+ const aSource = await dispatch(actions.newGeneratedSource(makeSource("a")));
+ aSource.url = "http://localhost:8000/examples/a";
+ const aSourceActor = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ aSource.id
+ );
+ await dispatch(actions.loadGeneratedSourceText(aSourceActor));
+
+ const bSource = await dispatch(actions.newGeneratedSource(makeSource("b")));
+ bSource.url = "http://localhost:8000/examples/b";
+ const bSourceActor = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ bSource.id
+ );
+ await dispatch(actions.loadGeneratedSourceText(bSourceActor));
+
+ const loc1 = createLocation({
+ source: aSource,
+ line: 5,
+ column: 1,
+ });
+
+ const loc2 = createLocation({
+ source: bSource,
+ line: 6,
+ column: 2,
+ });
+ await dispatch(actions.addBreakpoint(loc1));
+ await dispatch(actions.addBreakpoint(loc2));
+
+ const breakpoint = selectors.getBreakpoint(getState(), loc1);
+ if (!breakpoint) {
+ throw new Error("no breakpoint");
+ }
+
+ await dispatch(actions.disableBreakpoint(breakpoint));
+
+ const bp = selectors.getBreakpoint(getState(), loc1);
+ expect(bp && bp.disabled).toBe(true);
+ });
+
+ it("should enable breakpoint", async () => {
+ const { dispatch, getState } = createStore(mockClient({ 5: [1], 6: [2] }));
+
+ const aSource = await dispatch(actions.newGeneratedSource(makeSource("a")));
+ aSource.url = "http://localhost:8000/examples/a";
+ const loc = createLocation({
+ source: aSource,
+ line: 5,
+ column: 1,
+ });
+ const aSourceActor = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ aSource.id
+ );
+ await dispatch(actions.loadGeneratedSourceText(aSourceActor));
+
+ await dispatch(actions.addBreakpoint(loc));
+ let bp = selectors.getBreakpoint(getState(), loc);
+ if (!bp) {
+ throw new Error("no breakpoint");
+ }
+
+ await dispatch(actions.disableBreakpoint(bp));
+
+ bp = selectors.getBreakpoint(getState(), loc);
+ if (!bp) {
+ throw new Error("no breakpoint");
+ }
+
+ expect(bp && bp.disabled).toBe(true);
+
+ await dispatch(actions.enableBreakpoint(bp));
+
+ bp = selectors.getBreakpoint(getState(), loc);
+ expect(bp && !bp.disabled).toBe(true);
+ });
+
+ it("should toggle all the breakpoints", async () => {
+ const { dispatch, getState } = createStore(mockClient({ 5: [1], 6: [2] }));
+
+ const aSource = await dispatch(actions.newGeneratedSource(makeSource("a")));
+ aSource.url = "http://localhost:8000/examples/a";
+ const aSourceActor = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ aSource.id
+ );
+ await dispatch(actions.loadGeneratedSourceText(aSourceActor));
+
+ const bSource = await dispatch(actions.newGeneratedSource(makeSource("b")));
+ bSource.url = "http://localhost:8000/examples/b";
+ const bSourceActor = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ bSource.id
+ );
+ await dispatch(actions.loadGeneratedSourceText(bSourceActor));
+
+ const loc1 = createLocation({
+ source: aSource,
+ line: 5,
+ column: 1,
+ });
+
+ const loc2 = createLocation({
+ source: bSource,
+ line: 6,
+ column: 2,
+ });
+
+ await dispatch(actions.addBreakpoint(loc1));
+ await dispatch(actions.addBreakpoint(loc2));
+
+ await dispatch(actions.toggleAllBreakpoints(true));
+
+ let bp1 = selectors.getBreakpoint(getState(), loc1);
+ let bp2 = selectors.getBreakpoint(getState(), loc2);
+
+ expect(bp1 && bp1.disabled).toBe(true);
+ expect(bp2 && bp2.disabled).toBe(true);
+
+ await dispatch(actions.toggleAllBreakpoints(false));
+
+ bp1 = selectors.getBreakpoint(getState(), loc1);
+ bp2 = selectors.getBreakpoint(getState(), loc2);
+ expect(bp1 && bp1.disabled).toBe(false);
+ expect(bp2 && bp2.disabled).toBe(false);
+ });
+
+ it("should toggle a breakpoint at a location", async () => {
+ const { dispatch, getState } = createStore(mockClient({ 5: [1] }));
+
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("foo1"))
+ );
+ const loc = createLocation({ source, line: 5, column: 1 });
+ const getBp = () => selectors.getBreakpoint(getState(), loc);
+ await dispatch(actions.selectLocation(loc));
+
+ await dispatch(actions.toggleBreakpointAtLine(5));
+ const bp = getBp();
+ expect(bp && !bp.disabled).toBe(true);
+
+ await dispatch(actions.toggleBreakpointAtLine(5));
+ expect(getBp()).toBe(undefined);
+ });
+
+ it("should disable/enable a breakpoint at a location", async () => {
+ const { dispatch, getState } = createStore(mockClient({ 5: [1] }));
+
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("foo1"))
+ );
+ const location = createLocation({ source, line: 5, column: 1 });
+ const getBp = () => selectors.getBreakpoint(getState(), location);
+ await dispatch(actions.selectLocation(createLocation({ source, line: 1 })));
+
+ await dispatch(actions.toggleBreakpointAtLine(5));
+ let bp = getBp();
+ expect(bp && !bp.disabled).toBe(true);
+ bp = getBp();
+ if (!bp) {
+ throw new Error("no bp");
+ }
+ await dispatch(actions.toggleDisabledBreakpoint(bp));
+ bp = getBp();
+ expect(bp && bp.disabled).toBe(true);
+ });
+
+ it("should set the breakpoint condition", async () => {
+ const { dispatch, getState } = createStore(mockClient({ 5: [1] }));
+
+ const source = await dispatch(actions.newGeneratedSource(makeSource("a")));
+ source.url = "http://localhost:8000/examples/a";
+ const loc = createLocation({
+ source,
+ line: 5,
+ column: 1,
+ });
+ const sourceActor = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ source.id
+ );
+ await dispatch(actions.loadGeneratedSourceText(sourceActor));
+
+ await dispatch(actions.addBreakpoint(loc));
+
+ let bp = selectors.getBreakpoint(getState(), loc);
+ expect(bp && bp.options.condition).toBe(undefined);
+
+ await dispatch(
+ actions.setBreakpointOptions(loc, {
+ condition: "const foo = 0",
+ getTextForLine: () => {},
+ })
+ );
+
+ bp = selectors.getBreakpoint(getState(), loc);
+ expect(bp && bp.options.condition).toBe("const foo = 0");
+ });
+
+ it("should set the condition and enable a breakpoint", async () => {
+ const { dispatch, getState } = createStore(mockClient({ 5: [1] }));
+
+ const source = await dispatch(actions.newGeneratedSource(makeSource("a")));
+ source.url = "http://localhost:8000/examples/a";
+ const loc = createLocation({
+ source,
+ line: 5,
+ column: 1,
+ });
+ const sourceActor = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ source.id
+ );
+ await dispatch(actions.loadGeneratedSourceText(sourceActor));
+
+ await dispatch(actions.addBreakpoint(loc));
+ let bp = selectors.getBreakpoint(getState(), loc);
+ if (!bp) {
+ throw new Error("no breakpoint");
+ }
+
+ await dispatch(actions.disableBreakpoint(bp));
+
+ bp = selectors.getBreakpoint(getState(), loc);
+ expect(bp && bp.options.condition).toBe(undefined);
+
+ await dispatch(
+ actions.setBreakpointOptions(loc, {
+ condition: "const foo = 0",
+ getTextForLine: () => {},
+ })
+ );
+ const newBreakpoint = selectors.getBreakpoint(getState(), loc);
+ expect(newBreakpoint && !newBreakpoint.disabled).toBe(true);
+ expect(newBreakpoint && newBreakpoint.options.condition).toBe(
+ "const foo = 0"
+ );
+ });
+
+ it("should remove the pretty-printed breakpoint that was added", async () => {
+ const { dispatch, getState } = createStore(mockClient({ 1: [0] }));
+
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("a.js"))
+ );
+ source.url = "http://localhost:8000/examples/a";
+ const loc = createLocation({
+ source,
+ line: 1,
+ column: 0,
+ });
+ const sourceActor = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ source.id
+ );
+ await dispatch(actions.loadGeneratedSourceText(sourceActor));
+
+ await dispatch(actions.addBreakpoint(loc));
+ await dispatch(actions.prettyPrintAndSelectSource("a.js"));
+
+ const breakpoint = selectors.getBreakpointsList(getState())[0];
+
+ await dispatch(actions.removeBreakpoint(breakpoint));
+
+ const breakpointList = selectors.getPendingBreakpointList(getState());
+ expect(breakpointList.length).toBe(0);
+ });
+});
diff --git a/devtools/client/debugger/src/actions/context-menus/breakpoint-heading.js b/devtools/client/debugger/src/actions/context-menus/breakpoint-heading.js
new file mode 100644
index 0000000000..bded531cfe
--- /dev/null
+++ b/devtools/client/debugger/src/actions/context-menus/breakpoint-heading.js
@@ -0,0 +1,78 @@
+/* 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 { buildMenu, showMenu } from "../../context-menu/menu";
+
+import { getBreakpointsForSource } from "../../selectors/index";
+
+import {
+ disableBreakpointsInSource,
+ enableBreakpointsInSource,
+ removeBreakpointsInSource,
+} from "../../actions/breakpoints/index";
+
+export function showBreakpointHeadingContextMenu(event, source) {
+ return async ({ dispatch, getState }) => {
+ const state = getState();
+ const breakpointsForSource = getBreakpointsForSource(state, source);
+
+ const enableInSourceLabel = L10N.getStr(
+ "breakpointHeadingsMenuItem.enableInSource.label"
+ );
+ const disableInSourceLabel = L10N.getStr(
+ "breakpointHeadingsMenuItem.disableInSource.label"
+ );
+ const removeInSourceLabel = L10N.getStr(
+ "breakpointHeadingsMenuItem.removeInSource.label"
+ );
+ const enableInSourceKey = L10N.getStr(
+ "breakpointHeadingsMenuItem.enableInSource.accesskey"
+ );
+ const disableInSourceKey = L10N.getStr(
+ "breakpointHeadingsMenuItem.disableInSource.accesskey"
+ );
+ const removeInSourceKey = L10N.getStr(
+ "breakpointHeadingsMenuItem.removeInSource.accesskey"
+ );
+
+ const disableInSourceItem = {
+ id: "node-menu-disable-in-source",
+ label: disableInSourceLabel,
+ accesskey: disableInSourceKey,
+ disabled: false,
+ click: () => dispatch(disableBreakpointsInSource(source)),
+ };
+
+ const enableInSourceItem = {
+ id: "node-menu-enable-in-source",
+ label: enableInSourceLabel,
+ accesskey: enableInSourceKey,
+ disabled: false,
+ click: () => dispatch(enableBreakpointsInSource(source)),
+ };
+
+ const removeInSourceItem = {
+ id: "node-menu-enable-in-source",
+ label: removeInSourceLabel,
+ accesskey: removeInSourceKey,
+ disabled: false,
+ click: () => dispatch(removeBreakpointsInSource(source)),
+ };
+
+ const hideDisableInSourceItem = breakpointsForSource.every(
+ breakpoint => breakpoint.disabled
+ );
+ const hideEnableInSourceItem = breakpointsForSource.every(
+ breakpoint => !breakpoint.disabled
+ );
+
+ const items = [
+ { item: disableInSourceItem, hidden: () => hideDisableInSourceItem },
+ { item: enableInSourceItem, hidden: () => hideEnableInSourceItem },
+ { item: removeInSourceItem, hidden: () => false },
+ ];
+
+ showMenu(event, buildMenu(items));
+ };
+}
diff --git a/devtools/client/debugger/src/actions/context-menus/breakpoint.js b/devtools/client/debugger/src/actions/context-menus/breakpoint.js
new file mode 100644
index 0000000000..d70254130c
--- /dev/null
+++ b/devtools/client/debugger/src/actions/context-menus/breakpoint.js
@@ -0,0 +1,396 @@
+/* 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 { buildMenu, showMenu } from "../../context-menu/menu";
+import { getSelectedLocation } from "../../utils/selected-location";
+import { isLineBlackboxed } from "../../utils/source";
+import { features } from "../../utils/prefs";
+import { formatKeyShortcut } from "../../utils/text";
+
+import {
+ getBreakpointsList,
+ getSelectedSource,
+ getBlackBoxRanges,
+ isSourceMapIgnoreListEnabled,
+ isSourceOnSourceMapIgnoreList,
+} from "../../selectors/index";
+
+import {
+ removeBreakpoint,
+ setBreakpointOptions,
+} from "../../actions/breakpoints/modify";
+import {
+ removeBreakpoints,
+ removeAllBreakpoints,
+ toggleBreakpoints,
+ toggleAllBreakpoints,
+ toggleDisabledBreakpoint,
+} from "../../actions/breakpoints/index";
+import { selectSpecificLocation } from "../../actions/sources/select";
+import { openConditionalPanel } from "../../actions/ui";
+
+export function showBreakpointContextMenu(event, breakpoint, source) {
+ return async ({ dispatch, getState }) => {
+ const state = getState();
+ const breakpoints = getBreakpointsList(state);
+ const blackboxedRanges = getBlackBoxRanges(state);
+ const blackboxedRangesForSource = blackboxedRanges[source.url];
+ const checkSourceOnIgnoreList = _source =>
+ isSourceMapIgnoreListEnabled(state) &&
+ isSourceOnSourceMapIgnoreList(state, _source);
+ const selectedSource = getSelectedSource(state);
+
+ const deleteSelfLabel = L10N.getStr("breakpointMenuItem.deleteSelf2.label");
+ const deleteAllLabel = L10N.getStr("breakpointMenuItem.deleteAll2.label");
+ const deleteOthersLabel = L10N.getStr(
+ "breakpointMenuItem.deleteOthers2.label"
+ );
+ const enableSelfLabel = L10N.getStr("breakpointMenuItem.enableSelf2.label");
+ const enableAllLabel = L10N.getStr("breakpointMenuItem.enableAll2.label");
+ const enableOthersLabel = L10N.getStr(
+ "breakpointMenuItem.enableOthers2.label"
+ );
+ const disableSelfLabel = L10N.getStr(
+ "breakpointMenuItem.disableSelf2.label"
+ );
+ const disableAllLabel = L10N.getStr("breakpointMenuItem.disableAll2.label");
+ const disableOthersLabel = L10N.getStr(
+ "breakpointMenuItem.disableOthers2.label"
+ );
+ const enableDbgStatementLabel = L10N.getStr(
+ "breakpointMenuItem.enabledbg.label"
+ );
+ const disableDbgStatementLabel = L10N.getStr(
+ "breakpointMenuItem.disabledbg.label"
+ );
+ const removeConditionLabel = L10N.getStr(
+ "breakpointMenuItem.removeCondition2.label"
+ );
+ const addConditionLabel = L10N.getStr(
+ "breakpointMenuItem.addCondition2.label"
+ );
+ const editConditionLabel = L10N.getStr(
+ "breakpointMenuItem.editCondition2.label"
+ );
+
+ const deleteSelfKey = L10N.getStr(
+ "breakpointMenuItem.deleteSelf2.accesskey"
+ );
+ const deleteAllKey = L10N.getStr("breakpointMenuItem.deleteAll2.accesskey");
+ const deleteOthersKey = L10N.getStr(
+ "breakpointMenuItem.deleteOthers2.accesskey"
+ );
+ const enableSelfKey = L10N.getStr(
+ "breakpointMenuItem.enableSelf2.accesskey"
+ );
+ const enableAllKey = L10N.getStr("breakpointMenuItem.enableAll2.accesskey");
+ const enableOthersKey = L10N.getStr(
+ "breakpointMenuItem.enableOthers2.accesskey"
+ );
+ const disableSelfKey = L10N.getStr(
+ "breakpointMenuItem.disableSelf2.accesskey"
+ );
+ const disableAllKey = L10N.getStr(
+ "breakpointMenuItem.disableAll2.accesskey"
+ );
+ const disableOthersKey = L10N.getStr(
+ "breakpointMenuItem.disableOthers2.accesskey"
+ );
+ const removeConditionKey = L10N.getStr(
+ "breakpointMenuItem.removeCondition2.accesskey"
+ );
+ const editConditionKey = L10N.getStr(
+ "breakpointMenuItem.editCondition2.accesskey"
+ );
+ const addConditionKey = L10N.getStr(
+ "breakpointMenuItem.addCondition2.accesskey"
+ );
+
+ const selectedLocation = getSelectedLocation(breakpoint, selectedSource);
+ const otherBreakpoints = breakpoints.filter(b => b.id !== breakpoint.id);
+ const enabledBreakpoints = breakpoints.filter(b => !b.disabled);
+ const disabledBreakpoints = breakpoints.filter(b => b.disabled);
+ const otherEnabledBreakpoints = breakpoints.filter(
+ b => !b.disabled && b.id !== breakpoint.id
+ );
+ const otherDisabledBreakpoints = breakpoints.filter(
+ b => b.disabled && b.id !== breakpoint.id
+ );
+
+ const deleteSelfItem = {
+ id: "node-menu-delete-self",
+ label: deleteSelfLabel,
+ accesskey: deleteSelfKey,
+ disabled: false,
+ click: () => {
+ dispatch(removeBreakpoint(breakpoint));
+ },
+ };
+
+ const deleteAllItem = {
+ id: "node-menu-delete-all",
+ label: deleteAllLabel,
+ accesskey: deleteAllKey,
+ disabled: false,
+ click: () => dispatch(removeAllBreakpoints()),
+ };
+
+ const deleteOthersItem = {
+ id: "node-menu-delete-other",
+ label: deleteOthersLabel,
+ accesskey: deleteOthersKey,
+ disabled: false,
+ click: () => dispatch(removeBreakpoints(otherBreakpoints)),
+ };
+
+ const enableSelfItem = {
+ id: "node-menu-enable-self",
+ label: enableSelfLabel,
+ accesskey: enableSelfKey,
+ disabled: isLineBlackboxed(
+ blackboxedRangesForSource,
+ breakpoint.location.line,
+ checkSourceOnIgnoreList(breakpoint.location.source)
+ ),
+ click: () => {
+ dispatch(toggleDisabledBreakpoint(breakpoint));
+ },
+ };
+
+ const enableAllItem = {
+ id: "node-menu-enable-all",
+ label: enableAllLabel,
+ accesskey: enableAllKey,
+ disabled: isLineBlackboxed(
+ blackboxedRangesForSource,
+ breakpoint.location.line,
+ checkSourceOnIgnoreList(breakpoint.location.source)
+ ),
+ click: () => dispatch(toggleAllBreakpoints(false)),
+ };
+
+ const enableOthersItem = {
+ id: "node-menu-enable-others",
+ label: enableOthersLabel,
+ accesskey: enableOthersKey,
+ disabled: isLineBlackboxed(
+ blackboxedRangesForSource,
+ breakpoint.location.line,
+ checkSourceOnIgnoreList(breakpoint.location.source)
+ ),
+ click: () => dispatch(toggleBreakpoints(false, otherDisabledBreakpoints)),
+ };
+
+ const disableSelfItem = {
+ id: "node-menu-disable-self",
+ label: disableSelfLabel,
+ accesskey: disableSelfKey,
+ disabled: false,
+ click: () => {
+ dispatch(toggleDisabledBreakpoint(breakpoint));
+ },
+ };
+
+ const disableAllItem = {
+ id: "node-menu-disable-all",
+ label: disableAllLabel,
+ accesskey: disableAllKey,
+ disabled: false,
+ click: () => dispatch(toggleAllBreakpoints(true)),
+ };
+
+ const disableOthersItem = {
+ id: "node-menu-disable-others",
+ label: disableOthersLabel,
+ accesskey: disableOthersKey,
+ click: () => dispatch(toggleBreakpoints(true, otherEnabledBreakpoints)),
+ };
+
+ const enableDbgStatementItem = {
+ id: "node-menu-enable-dbgStatement",
+ label: enableDbgStatementLabel,
+ disabled: false,
+ click: () =>
+ dispatch(
+ setBreakpointOptions(selectedLocation, {
+ ...breakpoint.options,
+ condition: null,
+ })
+ ),
+ };
+
+ const disableDbgStatementItem = {
+ id: "node-menu-disable-dbgStatement",
+ label: disableDbgStatementLabel,
+ disabled: false,
+ click: () =>
+ dispatch(
+ setBreakpointOptions(selectedLocation, {
+ ...breakpoint.options,
+ condition: "false",
+ })
+ ),
+ };
+
+ const removeConditionItem = {
+ id: "node-menu-remove-condition",
+ label: removeConditionLabel,
+ accesskey: removeConditionKey,
+ disabled: false,
+ click: () =>
+ dispatch(
+ setBreakpointOptions(selectedLocation, {
+ ...breakpoint.options,
+ condition: null,
+ })
+ ),
+ };
+
+ const addConditionItem = {
+ id: "node-menu-add-condition",
+ label: addConditionLabel,
+ accesskey: addConditionKey,
+ click: async () => {
+ await dispatch(selectSpecificLocation(selectedLocation));
+ await dispatch(openConditionalPanel(selectedLocation));
+ },
+ accelerator: formatKeyShortcut(
+ L10N.getStr("toggleCondPanel.breakpoint.key")
+ ),
+ };
+
+ const editConditionItem = {
+ id: "node-menu-edit-condition",
+ label: editConditionLabel,
+ accesskey: editConditionKey,
+ click: async () => {
+ await dispatch(selectSpecificLocation(selectedLocation));
+ await dispatch(openConditionalPanel(selectedLocation));
+ },
+ accelerator: formatKeyShortcut(
+ L10N.getStr("toggleCondPanel.breakpoint.key")
+ ),
+ };
+
+ const addLogPointItem = {
+ id: "node-menu-add-log-point",
+ label: L10N.getStr("editor.addLogPoint"),
+ accesskey: L10N.getStr("editor.addLogPoint.accesskey"),
+ disabled: false,
+ click: async () => {
+ await dispatch(selectSpecificLocation(selectedLocation));
+ await dispatch(openConditionalPanel(selectedLocation, true));
+ },
+ accelerator: formatKeyShortcut(
+ L10N.getStr("toggleCondPanel.logPoint.key")
+ ),
+ };
+
+ const editLogPointItem = {
+ id: "node-menu-edit-log-point",
+ label: L10N.getStr("editor.editLogPoint"),
+ accesskey: L10N.getStr("editor.editLogPoint.accesskey"),
+ disabled: false,
+ click: async () => {
+ await dispatch(selectSpecificLocation(selectedLocation));
+ await dispatch(openConditionalPanel(selectedLocation, true));
+ },
+ accelerator: formatKeyShortcut(
+ L10N.getStr("toggleCondPanel.logPoint.key")
+ ),
+ };
+
+ const removeLogPointItem = {
+ id: "node-menu-remove-log",
+ label: L10N.getStr("editor.removeLogPoint.label"),
+ accesskey: L10N.getStr("editor.removeLogPoint.accesskey"),
+ disabled: false,
+ click: () =>
+ dispatch(
+ setBreakpointOptions(selectedLocation, {
+ ...breakpoint.options,
+ logValue: null,
+ })
+ ),
+ };
+
+ const logPointItem = breakpoint.options.logValue
+ ? editLogPointItem
+ : addLogPointItem;
+
+ const hideEnableSelfItem = !breakpoint.disabled;
+ const hideEnableAllItem = disabledBreakpoints.length === 0;
+ const hideEnableOthersItem = otherDisabledBreakpoints.length === 0;
+ const hideDisableAllItem = enabledBreakpoints.length === 0;
+ const hideDisableOthersItem = otherEnabledBreakpoints.length === 0;
+ const hideDisableSelfItem = breakpoint.disabled;
+ const hideEnableDbgStatementItem =
+ !breakpoint.originalText.startsWith("debugger") ||
+ (breakpoint.originalText.startsWith("debugger") &&
+ breakpoint.options.condition !== "false");
+ const hideDisableDbgStatementItem =
+ !breakpoint.originalText.startsWith("debugger") ||
+ (breakpoint.originalText.startsWith("debugger") &&
+ breakpoint.options.condition === "false");
+ const items = [
+ { item: enableSelfItem, hidden: () => hideEnableSelfItem },
+ { item: enableAllItem, hidden: () => hideEnableAllItem },
+ { item: enableOthersItem, hidden: () => hideEnableOthersItem },
+ {
+ item: { type: "separator" },
+ hidden: () =>
+ hideEnableSelfItem && hideEnableAllItem && hideEnableOthersItem,
+ },
+ { item: deleteSelfItem },
+ { item: deleteAllItem },
+ { item: deleteOthersItem, hidden: () => breakpoints.length === 1 },
+ {
+ item: { type: "separator" },
+ hidden: () =>
+ hideDisableSelfItem && hideDisableAllItem && hideDisableOthersItem,
+ },
+
+ { item: disableSelfItem, hidden: () => hideDisableSelfItem },
+ { item: disableAllItem, hidden: () => hideDisableAllItem },
+ { item: disableOthersItem, hidden: () => hideDisableOthersItem },
+ {
+ item: { type: "separator" },
+ },
+ {
+ item: enableDbgStatementItem,
+ hidden: () => hideEnableDbgStatementItem,
+ },
+ {
+ item: disableDbgStatementItem,
+ hidden: () => hideDisableDbgStatementItem,
+ },
+ {
+ item: { type: "separator" },
+ hidden: () => hideDisableDbgStatementItem && hideEnableDbgStatementItem,
+ },
+ {
+ item: addConditionItem,
+ hidden: () => breakpoint.options.condition,
+ },
+ {
+ item: editConditionItem,
+ hidden: () => !breakpoint.options.condition,
+ },
+ {
+ item: removeConditionItem,
+ hidden: () => !breakpoint.options.condition,
+ },
+ {
+ item: logPointItem,
+ hidden: () => !features.logPoints,
+ },
+ {
+ item: removeLogPointItem,
+ hidden: () => !features.logPoints || !breakpoint.options.logValue,
+ },
+ ];
+
+ showMenu(event, buildMenu(items));
+ };
+}
diff --git a/devtools/client/debugger/src/actions/context-menus/editor-breakpoint.js b/devtools/client/debugger/src/actions/context-menus/editor-breakpoint.js
new file mode 100644
index 0000000000..39ec2f1589
--- /dev/null
+++ b/devtools/client/debugger/src/actions/context-menus/editor-breakpoint.js
@@ -0,0 +1,273 @@
+/* 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 { showMenu } from "../../context-menu/menu";
+import { getSelectedLocation } from "../../utils/selected-location";
+import { features } from "../../utils/prefs";
+import { formatKeyShortcut } from "../../utils/text";
+import { isLineBlackboxed } from "../../utils/source";
+
+import {
+ getSelectedSource,
+ getBlackBoxRanges,
+ isSourceMapIgnoreListEnabled,
+ isSourceOnSourceMapIgnoreList,
+} from "../../selectors/index";
+import {
+ addBreakpoint,
+ removeBreakpoint,
+ setBreakpointOptions,
+} from "../../actions/breakpoints/modify";
+import {
+ enableBreakpointsAtLine,
+ disableBreakpointsAtLine,
+ toggleDisabledBreakpoint,
+ removeBreakpointsAtLine,
+} from "../../actions/breakpoints/index";
+import { openConditionalPanel } from "../../actions/ui";
+
+export function showEditorEditBreakpointContextMenu(event, breakpoint) {
+ return async ({ dispatch, getState }) => {
+ const state = getState();
+ const selectedSource = getSelectedSource(state);
+ const selectedLocation = getSelectedLocation(breakpoint, selectedSource);
+ const blackboxedRanges = getBlackBoxRanges(state);
+ const blackboxedRangesForSelectedSource =
+ blackboxedRanges[selectedSource.url];
+ const isSelectedSourceOnIgnoreList =
+ selectedSource &&
+ isSourceMapIgnoreListEnabled(state) &&
+ isSourceOnSourceMapIgnoreList(state, selectedSource);
+
+ const items = [
+ removeBreakpointItem(breakpoint, dispatch),
+ toggleDisabledBreakpointItem(
+ breakpoint,
+ blackboxedRangesForSelectedSource,
+ isSelectedSourceOnIgnoreList,
+ dispatch
+ ),
+ ];
+
+ if (breakpoint.originalText.startsWith("debugger")) {
+ items.push(
+ { type: "separator" },
+ toggleDbgStatementItem(selectedLocation, breakpoint, dispatch)
+ );
+ }
+
+ items.push(
+ { type: "separator" },
+ removeBreakpointsOnLineItem(selectedLocation, dispatch),
+ breakpoint.disabled
+ ? enableBreakpointsOnLineItem(
+ selectedLocation,
+ blackboxedRangesForSelectedSource,
+ isSelectedSourceOnIgnoreList,
+ dispatch
+ )
+ : disableBreakpointsOnLineItem(selectedLocation, dispatch),
+ { type: "separator" }
+ );
+
+ items.push(
+ conditionalBreakpointItem(breakpoint, selectedLocation, dispatch)
+ );
+ items.push(logPointItem(breakpoint, selectedLocation, dispatch));
+
+ showMenu(event, items);
+ };
+}
+
+export function showEditorCreateBreakpointContextMenu(
+ event,
+ location,
+ lineText
+) {
+ return async ({ dispatch, getState }) => {
+ const items = createBreakpointItems(location, lineText, dispatch);
+
+ showMenu(event, items);
+ };
+}
+
+export function createBreakpointItems(location, lineText, dispatch) {
+ const items = [
+ addBreakpointItem(location, dispatch),
+ addConditionalBreakpointItem(location, dispatch),
+ ];
+
+ if (features.logPoints) {
+ items.push(addLogPointItem(location, dispatch));
+ }
+
+ if (lineText && lineText.startsWith("debugger")) {
+ items.push(toggleDbgStatementItem(location, null, dispatch));
+ }
+ return items;
+}
+
+const addBreakpointItem = (location, dispatch) => ({
+ id: "node-menu-add-breakpoint",
+ label: L10N.getStr("editor.addBreakpoint"),
+ accesskey: L10N.getStr("shortcuts.toggleBreakpoint.accesskey"),
+ disabled: false,
+ click: () => dispatch(addBreakpoint(location)),
+ accelerator: formatKeyShortcut(L10N.getStr("toggleBreakpoint.key")),
+});
+
+const removeBreakpointItem = (breakpoint, dispatch) => ({
+ id: "node-menu-remove-breakpoint",
+ label: L10N.getStr("editor.removeBreakpoint"),
+ accesskey: L10N.getStr("shortcuts.toggleBreakpoint.accesskey"),
+ disabled: false,
+ click: () => dispatch(removeBreakpoint(breakpoint)),
+ accelerator: formatKeyShortcut(L10N.getStr("toggleBreakpoint.key")),
+});
+
+const addConditionalBreakpointItem = (location, dispatch) => ({
+ id: "node-menu-add-conditional-breakpoint",
+ label: L10N.getStr("editor.addConditionBreakpoint"),
+ accelerator: formatKeyShortcut(L10N.getStr("toggleCondPanel.breakpoint.key")),
+ accesskey: L10N.getStr("editor.addConditionBreakpoint.accesskey"),
+ disabled: false,
+ click: () => dispatch(openConditionalPanel(location)),
+});
+
+const editConditionalBreakpointItem = (location, dispatch) => ({
+ id: "node-menu-edit-conditional-breakpoint",
+ label: L10N.getStr("editor.editConditionBreakpoint"),
+ accelerator: formatKeyShortcut(L10N.getStr("toggleCondPanel.breakpoint.key")),
+ accesskey: L10N.getStr("editor.addConditionBreakpoint.accesskey"),
+ disabled: false,
+ click: () => dispatch(openConditionalPanel(location)),
+});
+
+const conditionalBreakpointItem = (breakpoint, location, dispatch) => {
+ const {
+ options: { condition },
+ } = breakpoint;
+ return condition
+ ? editConditionalBreakpointItem(location, dispatch)
+ : addConditionalBreakpointItem(location, dispatch);
+};
+
+const addLogPointItem = (location, dispatch) => ({
+ id: "node-menu-add-log-point",
+ label: L10N.getStr("editor.addLogPoint"),
+ accesskey: L10N.getStr("editor.addLogPoint.accesskey"),
+ disabled: false,
+ click: () => dispatch(openConditionalPanel(location, true)),
+ accelerator: formatKeyShortcut(L10N.getStr("toggleCondPanel.logPoint.key")),
+});
+
+const editLogPointItem = (location, dispatch) => ({
+ id: "node-menu-edit-log-point",
+ label: L10N.getStr("editor.editLogPoint"),
+ accesskey: L10N.getStr("editor.editLogPoint.accesskey"),
+ disabled: false,
+ click: () => dispatch(openConditionalPanel(location, true)),
+ accelerator: formatKeyShortcut(L10N.getStr("toggleCondPanel.logPoint.key")),
+});
+
+const logPointItem = (breakpoint, location, dispatch) => {
+ const {
+ options: { logValue },
+ } = breakpoint;
+ return logValue
+ ? editLogPointItem(location, dispatch)
+ : addLogPointItem(location, dispatch);
+};
+
+const toggleDisabledBreakpointItem = (
+ breakpoint,
+ blackboxedRangesForSelectedSource,
+ isSelectedSourceOnIgnoreList,
+ dispatch
+) => {
+ return {
+ accesskey: L10N.getStr("editor.disableBreakpoint.accesskey"),
+ disabled: isLineBlackboxed(
+ blackboxedRangesForSelectedSource,
+ breakpoint.location.line,
+ isSelectedSourceOnIgnoreList
+ ),
+ click: () => dispatch(toggleDisabledBreakpoint(breakpoint)),
+ ...(breakpoint.disabled
+ ? {
+ id: "node-menu-enable-breakpoint",
+ label: L10N.getStr("editor.enableBreakpoint"),
+ }
+ : {
+ id: "node-menu-disable-breakpoint",
+ label: L10N.getStr("editor.disableBreakpoint"),
+ }),
+ };
+};
+
+const toggleDbgStatementItem = (location, breakpoint, dispatch) => {
+ if (breakpoint && breakpoint.options.condition === "false") {
+ return {
+ disabled: false,
+ id: "node-menu-enable-dbgStatement",
+ label: L10N.getStr("breakpointMenuItem.enabledbg.label"),
+ click: () =>
+ dispatch(
+ setBreakpointOptions(location, {
+ ...breakpoint.options,
+ condition: null,
+ })
+ ),
+ };
+ }
+
+ return {
+ disabled: false,
+ id: "node-menu-disable-dbgStatement",
+ label: L10N.getStr("breakpointMenuItem.disabledbg.label"),
+ click: () =>
+ dispatch(
+ setBreakpointOptions(location, {
+ condition: "false",
+ })
+ ),
+ };
+};
+
+// ToDo: Only enable if there are more than one breakpoints on a line?
+const removeBreakpointsOnLineItem = (location, dispatch) => ({
+ id: "node-menu-remove-breakpoints-on-line",
+ label: L10N.getStr("breakpointMenuItem.removeAllAtLine.label"),
+ accesskey: L10N.getStr("breakpointMenuItem.removeAllAtLine.accesskey"),
+ disabled: false,
+ click: () =>
+ dispatch(removeBreakpointsAtLine(location.source, location.line)),
+});
+
+const enableBreakpointsOnLineItem = (
+ location,
+ blackboxedRangesForSelectedSource,
+ isSelectedSourceOnIgnoreList,
+ dispatch
+) => ({
+ id: "node-menu-remove-breakpoints-on-line",
+ label: L10N.getStr("breakpointMenuItem.enableAllAtLine.label"),
+ accesskey: L10N.getStr("breakpointMenuItem.enableAllAtLine.accesskey"),
+ disabled: isLineBlackboxed(
+ blackboxedRangesForSelectedSource,
+ location.line,
+ isSelectedSourceOnIgnoreList
+ ),
+ click: () =>
+ dispatch(enableBreakpointsAtLine(location.source, location.line)),
+});
+
+const disableBreakpointsOnLineItem = (location, dispatch) => ({
+ id: "node-menu-remove-breakpoints-on-line",
+ label: L10N.getStr("breakpointMenuItem.disableAllAtLine.label"),
+ accesskey: L10N.getStr("breakpointMenuItem.disableAllAtLine.accesskey"),
+ disabled: false,
+ click: () =>
+ dispatch(disableBreakpointsAtLine(location.source, location.line)),
+});
diff --git a/devtools/client/debugger/src/actions/context-menus/editor.js b/devtools/client/debugger/src/actions/context-menus/editor.js
new file mode 100644
index 0000000000..1125790a9b
--- /dev/null
+++ b/devtools/client/debugger/src/actions/context-menus/editor.js
@@ -0,0 +1,436 @@
+/* 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 { showMenu } from "../../context-menu/menu";
+
+import { copyToTheClipboard } from "../../utils/clipboard";
+import {
+ isPretty,
+ getRawSourceURL,
+ getFilename,
+ shouldBlackbox,
+ findBlackBoxRange,
+} from "../../utils/source";
+import { toSourceLine } from "../../utils/editor/index";
+import { downloadFile } from "../../utils/utils";
+import { features } from "../../utils/prefs";
+import { isFulfilled } from "../../utils/async-value";
+
+import { createBreakpointItems } from "./editor-breakpoint";
+
+import {
+ getPrettySource,
+ getIsCurrentThreadPaused,
+ isSourceWithMap,
+ getBlackBoxRanges,
+ isSourceOnSourceMapIgnoreList,
+ isSourceMapIgnoreListEnabled,
+ getEditorWrapping,
+} from "../../selectors/index";
+
+import { continueToHere } from "../../actions/pause/continueToHere";
+import { jumpToMappedLocation } from "../../actions/sources/select";
+import {
+ showSource,
+ toggleInlinePreview,
+ toggleEditorWrapping,
+} from "../../actions/ui";
+import { toggleBlackBox } from "../../actions/sources/blackbox";
+import { addExpression } from "../../actions/expressions";
+import { evaluateInConsole } from "../../actions/toolbox";
+
+export function showEditorContextMenu(event, editor, location) {
+ return async ({ dispatch, getState }) => {
+ const { source } = location;
+ const state = getState();
+ const blackboxedRanges = getBlackBoxRanges(state);
+ const isPaused = getIsCurrentThreadPaused(state);
+ const hasMappedLocation =
+ (source.isOriginal ||
+ isSourceWithMap(state, source.id) ||
+ isPretty(source)) &&
+ !getPrettySource(state, source.id);
+ const isSourceOnIgnoreList =
+ isSourceMapIgnoreListEnabled(state) &&
+ isSourceOnSourceMapIgnoreList(state, source);
+ const editorWrappingEnabled = getEditorWrapping(state);
+
+ showMenu(
+ event,
+ editorMenuItems({
+ blackboxedRanges,
+ hasMappedLocation,
+ location,
+ isPaused,
+ editorWrappingEnabled,
+ selectionText: editor.codeMirror.getSelection().trim(),
+ isTextSelected: editor.codeMirror.somethingSelected(),
+ editor,
+ isSourceOnIgnoreList,
+ dispatch,
+ })
+ );
+ };
+}
+
+export function showEditorGutterContextMenu(event, editor, location, lineText) {
+ return async ({ dispatch, getState }) => {
+ const { source } = location;
+ const state = getState();
+ const blackboxedRanges = getBlackBoxRanges(state);
+ const isPaused = getIsCurrentThreadPaused(state);
+ const isSourceOnIgnoreList =
+ isSourceMapIgnoreListEnabled(state) &&
+ isSourceOnSourceMapIgnoreList(state, source);
+
+ showMenu(event, [
+ ...createBreakpointItems(location, lineText, dispatch),
+ { type: "separator" },
+ continueToHereItem(location, isPaused, dispatch),
+ { type: "separator" },
+ blackBoxLineMenuItem(
+ source,
+ editor,
+ blackboxedRanges,
+ isSourceOnIgnoreList,
+ location.line,
+ dispatch
+ ),
+ ]);
+ };
+}
+
+// Menu Items
+const continueToHereItem = (location, isPaused, dispatch) => ({
+ accesskey: L10N.getStr("editor.continueToHere.accesskey"),
+ disabled: !isPaused,
+ click: () => dispatch(continueToHere(location)),
+ id: "node-menu-continue-to-here",
+ label: L10N.getStr("editor.continueToHere.label"),
+});
+
+const copyToClipboardItem = selectionText => ({
+ id: "node-menu-copy-to-clipboard",
+ label: L10N.getStr("copyToClipboard.label"),
+ accesskey: L10N.getStr("copyToClipboard.accesskey"),
+ disabled: selectionText.length === 0,
+ click: () => copyToTheClipboard(selectionText),
+});
+
+const copySourceItem = selectedContent => ({
+ id: "node-menu-copy-source",
+ label: L10N.getStr("copySource.label"),
+ accesskey: L10N.getStr("copySource.accesskey"),
+ disabled: false,
+ click: () =>
+ selectedContent.type === "text" &&
+ copyToTheClipboard(selectedContent.value),
+});
+
+const copySourceUri2Item = selectedSource => ({
+ id: "node-menu-copy-source-url",
+ label: L10N.getStr("copySourceUri2"),
+ accesskey: L10N.getStr("copySourceUri2.accesskey"),
+ disabled: !selectedSource.url,
+ click: () => copyToTheClipboard(getRawSourceURL(selectedSource.url)),
+});
+
+const jumpToMappedLocationItem = (location, hasMappedLocation, dispatch) => ({
+ id: "node-menu-jump",
+ label: L10N.getFormatStr(
+ "editor.jumpToMappedLocation1",
+ location.source.isOriginal
+ ? L10N.getStr("generated")
+ : L10N.getStr("original")
+ ),
+ accesskey: L10N.getStr("editor.jumpToMappedLocation1.accesskey"),
+ disabled: !hasMappedLocation,
+ click: () => dispatch(jumpToMappedLocation(location)),
+});
+
+const showSourceMenuItem = (selectedSource, dispatch) => ({
+ id: "node-menu-show-source",
+ label: L10N.getStr("sourceTabs.revealInTree"),
+ accesskey: L10N.getStr("sourceTabs.revealInTree.accesskey"),
+ disabled: !selectedSource.url,
+ click: () => dispatch(showSource(selectedSource.id)),
+});
+
+const blackBoxMenuItem = (
+ selectedSource,
+ blackboxedRanges,
+ isSourceOnIgnoreList,
+ dispatch
+) => {
+ const isBlackBoxed = !!blackboxedRanges[selectedSource.url];
+ return {
+ id: "node-menu-blackbox",
+ label: isBlackBoxed
+ ? L10N.getStr("ignoreContextItem.unignore")
+ : L10N.getStr("ignoreContextItem.ignore"),
+ accesskey: isBlackBoxed
+ ? L10N.getStr("ignoreContextItem.unignore.accesskey")
+ : L10N.getStr("ignoreContextItem.ignore.accesskey"),
+ disabled: isSourceOnIgnoreList || !shouldBlackbox(selectedSource),
+ click: () => dispatch(toggleBlackBox(selectedSource)),
+ };
+};
+
+const blackBoxLineMenuItem = (
+ selectedSource,
+ editor,
+ blackboxedRanges,
+ isSourceOnIgnoreList,
+ // the clickedLine is passed when the context menu
+ // is opened from the gutter, it is not available when the
+ // the context menu is opened from the editor.
+ clickedLine = null,
+ dispatch
+) => {
+ const { codeMirror } = editor;
+ const from = codeMirror.getCursor("from");
+ const to = codeMirror.getCursor("to");
+
+ const startLine = clickedLine ?? toSourceLine(selectedSource.id, from.line);
+ const endLine = clickedLine ?? toSourceLine(selectedSource.id, to.line);
+
+ const blackboxRange = findBlackBoxRange(selectedSource, blackboxedRanges, {
+ start: startLine,
+ end: endLine,
+ });
+
+ const selectedLineIsBlackBoxed = !!blackboxRange;
+
+ const isSingleLine = selectedLineIsBlackBoxed
+ ? blackboxRange.start.line == blackboxRange.end.line
+ : startLine == endLine;
+
+ const isSourceFullyBlackboxed =
+ blackboxedRanges[selectedSource.url] &&
+ !blackboxedRanges[selectedSource.url].length;
+
+ // The ignore/unignore line context menu item should be disabled when
+ // 1) The source is on the sourcemap ignore list
+ // 2) The whole source is blackboxed or
+ // 3) Multiple lines are blackboxed or
+ // 4) Multiple lines are selected in the editor
+ const shouldDisable =
+ isSourceOnIgnoreList || isSourceFullyBlackboxed || !isSingleLine;
+
+ return {
+ id: "node-menu-blackbox-line",
+ label: !selectedLineIsBlackBoxed
+ ? L10N.getStr("ignoreContextItem.ignoreLine")
+ : L10N.getStr("ignoreContextItem.unignoreLine"),
+ accesskey: !selectedLineIsBlackBoxed
+ ? L10N.getStr("ignoreContextItem.ignoreLine.accesskey")
+ : L10N.getStr("ignoreContextItem.unignoreLine.accesskey"),
+ disabled: shouldDisable,
+ click: () => {
+ const selectionRange = {
+ start: {
+ line: startLine,
+ column: clickedLine == null ? from.ch : 0,
+ },
+ end: {
+ line: endLine,
+ column: clickedLine == null ? to.ch : 0,
+ },
+ };
+
+ dispatch(
+ toggleBlackBox(
+ selectedSource,
+ !selectedLineIsBlackBoxed,
+ selectedLineIsBlackBoxed ? [blackboxRange] : [selectionRange]
+ )
+ );
+ },
+ };
+};
+
+const blackBoxLinesMenuItem = (
+ selectedSource,
+ editor,
+ blackboxedRanges,
+ isSourceOnIgnoreList,
+ clickedLine = null,
+ dispatch
+) => {
+ const { codeMirror } = editor;
+ const from = codeMirror.getCursor("from");
+ const to = codeMirror.getCursor("to");
+
+ const startLine = toSourceLine(selectedSource.id, from.line);
+ const endLine = toSourceLine(selectedSource.id, to.line);
+
+ const blackboxRange = findBlackBoxRange(selectedSource, blackboxedRanges, {
+ start: startLine,
+ end: endLine,
+ });
+
+ const selectedLinesAreBlackBoxed = !!blackboxRange;
+
+ return {
+ id: "node-menu-blackbox-lines",
+ label: !selectedLinesAreBlackBoxed
+ ? L10N.getStr("ignoreContextItem.ignoreLines")
+ : L10N.getStr("ignoreContextItem.unignoreLines"),
+ accesskey: !selectedLinesAreBlackBoxed
+ ? L10N.getStr("ignoreContextItem.ignoreLines.accesskey")
+ : L10N.getStr("ignoreContextItem.unignoreLines.accesskey"),
+ disabled: isSourceOnIgnoreList,
+ click: () => {
+ const selectionRange = {
+ start: {
+ line: startLine,
+ column: from.ch,
+ },
+ end: {
+ line: endLine,
+ column: to.ch,
+ },
+ };
+
+ dispatch(
+ toggleBlackBox(
+ selectedSource,
+ !selectedLinesAreBlackBoxed,
+ selectedLinesAreBlackBoxed ? [blackboxRange] : [selectionRange]
+ )
+ );
+ },
+ };
+};
+
+const watchExpressionItem = (selectedSource, selectionText, dispatch) => ({
+ id: "node-menu-add-watch-expression",
+ label: L10N.getStr("expressions.label"),
+ accesskey: L10N.getStr("expressions.accesskey"),
+ click: () => dispatch(addExpression(selectionText)),
+});
+
+const evaluateInConsoleItem = (selectedSource, selectionText, dispatch) => ({
+ id: "node-menu-evaluate-in-console",
+ label: L10N.getStr("evaluateInConsole.label"),
+ click: () => dispatch(evaluateInConsole(selectionText)),
+});
+
+const downloadFileItem = (selectedSource, selectedContent) => ({
+ id: "node-menu-download-file",
+ label: L10N.getStr("downloadFile.label"),
+ accesskey: L10N.getStr("downloadFile.accesskey"),
+ click: () => downloadFile(selectedContent, getFilename(selectedSource)),
+});
+
+const inlinePreviewItem = dispatch => ({
+ id: "node-menu-inline-preview",
+ label: features.inlinePreview
+ ? L10N.getStr("inlinePreview.hide.label")
+ : L10N.getStr("inlinePreview.show.label"),
+ click: () => dispatch(toggleInlinePreview(!features.inlinePreview)),
+});
+
+const editorWrappingItem = (editorWrappingEnabled, dispatch) => ({
+ id: "node-menu-editor-wrapping",
+ label: editorWrappingEnabled
+ ? L10N.getStr("editorWrapping.hide.label")
+ : L10N.getStr("editorWrapping.show.label"),
+ click: () => dispatch(toggleEditorWrapping(!editorWrappingEnabled)),
+});
+
+function editorMenuItems({
+ blackboxedRanges,
+ location,
+ selectionText,
+ hasMappedLocation,
+ isTextSelected,
+ isPaused,
+ editorWrappingEnabled,
+ editor,
+ isSourceOnIgnoreList,
+ dispatch,
+}) {
+ const items = [];
+
+ const { source } = location;
+
+ const content =
+ source.content && isFulfilled(source.content) ? source.content.value : null;
+
+ items.push(
+ jumpToMappedLocationItem(location, hasMappedLocation, dispatch),
+ continueToHereItem(location, isPaused, dispatch),
+ { type: "separator" },
+ copyToClipboardItem(selectionText),
+ ...(!source.isWasm
+ ? [
+ ...(content ? [copySourceItem(content)] : []),
+ copySourceUri2Item(source),
+ ]
+ : []),
+ ...(content ? [downloadFileItem(source, content)] : []),
+ { type: "separator" },
+ showSourceMenuItem(source, dispatch),
+ { type: "separator" },
+ blackBoxMenuItem(source, blackboxedRanges, isSourceOnIgnoreList, dispatch)
+ );
+
+ const startLine = toSourceLine(
+ source.id,
+ editor.codeMirror.getCursor("from").line
+ );
+ const endLine = toSourceLine(
+ source.id,
+ editor.codeMirror.getCursor("to").line
+ );
+
+ // Find any blackbox ranges that exist for the selected lines
+ const blackboxRange = findBlackBoxRange(source, blackboxedRanges, {
+ start: startLine,
+ end: endLine,
+ });
+
+ const isMultiLineSelection = blackboxRange
+ ? blackboxRange.start.line !== blackboxRange.end.line
+ : startLine !== endLine;
+
+ // When the range is defined and is an empty array,
+ // the whole source is blackboxed
+ const theWholeSourceIsBlackBoxed =
+ blackboxedRanges[source.url] && !blackboxedRanges[source.url].length;
+
+ if (!theWholeSourceIsBlackBoxed) {
+ const blackBoxSourceLinesMenuItem = isMultiLineSelection
+ ? blackBoxLinesMenuItem
+ : blackBoxLineMenuItem;
+
+ items.push(
+ blackBoxSourceLinesMenuItem(
+ source,
+ editor,
+ blackboxedRanges,
+ isSourceOnIgnoreList,
+ null,
+ dispatch
+ )
+ );
+ }
+
+ if (isTextSelected) {
+ items.push(
+ { type: "separator" },
+ watchExpressionItem(source, selectionText, dispatch),
+ evaluateInConsoleItem(source, selectionText, dispatch)
+ );
+ }
+
+ items.push(
+ { type: "separator" },
+ inlinePreviewItem(dispatch),
+ editorWrappingItem(editorWrappingEnabled, dispatch)
+ );
+
+ return items;
+}
diff --git a/devtools/client/debugger/src/actions/context-menus/frame.js b/devtools/client/debugger/src/actions/context-menus/frame.js
new file mode 100644
index 0000000000..1d287b1028
--- /dev/null
+++ b/devtools/client/debugger/src/actions/context-menus/frame.js
@@ -0,0 +1,97 @@
+/* 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 { showMenu } from "../../context-menu/menu";
+import { copyToTheClipboard } from "../../utils/clipboard";
+import {
+ getShouldSelectOriginalLocation,
+ getCurrentThreadFrames,
+ getFrameworkGroupingState,
+} from "../../selectors/index";
+import { toggleFrameworkGrouping } from "../../actions/ui";
+import { restart, toggleBlackBox } from "../../actions/pause/index";
+import { formatCopyName } from "../../utils/pause/frames/index";
+
+function formatMenuElement(labelString, click, disabled = false) {
+ const label = L10N.getStr(labelString);
+ const accesskey = L10N.getStr(`${labelString}.accesskey`);
+ const id = `node-menu-${labelString}`;
+ return {
+ id,
+ label,
+ accesskey,
+ disabled,
+ click,
+ };
+}
+
+function isValidRestartFrame(frame, callbacks) {
+ // Any frame state than 'on-stack' is either dismissed by the server
+ // or can potentially cause unexpected errors.
+ // Global frame has frame.callee equal to null and can't be restarted.
+ return frame.type === "call" && frame.state === "on-stack";
+}
+
+function copyStackTrace() {
+ return async ({ dispatch, getState }) => {
+ const frames = getCurrentThreadFrames(getState());
+ const shouldDisplayOriginalLocation = getShouldSelectOriginalLocation(
+ getState()
+ );
+
+ const framesToCopy = frames
+ .map(frame => formatCopyName(frame, L10N, shouldDisplayOriginalLocation))
+ .join("\n");
+ copyToTheClipboard(framesToCopy);
+ };
+}
+
+export function showFrameContextMenu(event, frame, hideRestart = false) {
+ return async ({ dispatch, getState }) => {
+ const items = [];
+
+ // Hides 'Restart Frame' item for call stack groups context menu,
+ // otherwise can be misleading for the user which frame gets restarted.
+ if (!hideRestart && isValidRestartFrame(frame)) {
+ items.push(
+ formatMenuElement("restartFrame", () => dispatch(restart(frame)))
+ );
+ }
+
+ const toggleFrameWorkL10nLabel = getFrameworkGroupingState(getState())
+ ? "framework.disableGrouping"
+ : "framework.enableGrouping";
+ items.push(
+ formatMenuElement(toggleFrameWorkL10nLabel, () =>
+ dispatch(
+ toggleFrameworkGrouping(!getFrameworkGroupingState(getState()))
+ )
+ )
+ );
+
+ const { source } = frame;
+ if (frame.source) {
+ items.push(
+ formatMenuElement("copySourceUri2", () =>
+ copyToTheClipboard(source.url)
+ )
+ );
+
+ const toggleBlackBoxL10nLabel = source.isBlackBoxed
+ ? "ignoreContextItem.unignore"
+ : "ignoreContextItem.ignore";
+ items.push(
+ formatMenuElement(toggleBlackBoxL10nLabel, () =>
+ dispatch(toggleBlackBox(source))
+ )
+ );
+ }
+
+ items.push(
+ formatMenuElement("copyStackTrace", () => dispatch(copyStackTrace()))
+ );
+
+ showMenu(event, items);
+ };
+}
diff --git a/devtools/client/debugger/src/actions/context-menus/index.js b/devtools/client/debugger/src/actions/context-menus/index.js
new file mode 100644
index 0000000000..c988d94ccc
--- /dev/null
+++ b/devtools/client/debugger/src/actions/context-menus/index.js
@@ -0,0 +1,12 @@
+/* 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 "./breakpoint";
+export * from "./breakpoint-heading";
+export * from "./frame";
+export * from "./editor";
+export * from "./editor-breakpoint";
+export * from "./outline";
+export * from "./source-tree-item";
+export * from "./tab";
diff --git a/devtools/client/debugger/src/actions/context-menus/moz.build b/devtools/client/debugger/src/actions/context-menus/moz.build
new file mode 100644
index 0000000000..776cb436f9
--- /dev/null
+++ b/devtools/client/debugger/src/actions/context-menus/moz.build
@@ -0,0 +1,16 @@
+# 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/.
+
+CompiledModules(
+ "breakpoint.js",
+ "breakpoint-heading.js",
+ "frame.js",
+ "editor.js",
+ "editor-breakpoint.js",
+ "index.js",
+ "outline.js",
+ "source-tree-item.js",
+ "tab.js",
+)
diff --git a/devtools/client/debugger/src/actions/context-menus/outline.js b/devtools/client/debugger/src/actions/context-menus/outline.js
new file mode 100644
index 0000000000..4ba0fe8f6f
--- /dev/null
+++ b/devtools/client/debugger/src/actions/context-menus/outline.js
@@ -0,0 +1,54 @@
+/* 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 { showMenu } from "../../context-menu/menu";
+import { copyToTheClipboard } from "../../utils/clipboard";
+import { findFunctionText } from "../../utils/function";
+
+import { flashLineRange } from "../../actions/ui";
+
+import {
+ getSelectedSource,
+ getSelectedSourceTextContent,
+} from "../../selectors/index";
+
+export function showOutlineContextMenu(event, func, symbols) {
+ return async ({ dispatch, getState }) => {
+ const state = getState();
+
+ const selectedSource = getSelectedSource(state);
+ if (!selectedSource) {
+ return;
+ }
+ const selectedSourceTextContent = getSelectedSourceTextContent(state);
+
+ const sourceLine = func.location.start.line;
+ const functionText = findFunctionText(
+ sourceLine,
+ selectedSource,
+ selectedSourceTextContent,
+ symbols
+ );
+
+ const copyFunctionItem = {
+ id: "node-menu-copy-function",
+ label: L10N.getStr("copyFunction.label"),
+ accesskey: L10N.getStr("copyFunction.accesskey"),
+ disabled: !functionText,
+ click: () => {
+ dispatch(
+ flashLineRange({
+ start: sourceLine,
+ end: func.location.end.line,
+ sourceId: selectedSource.id,
+ })
+ );
+ return copyToTheClipboard(functionText);
+ },
+ };
+
+ const items = [copyFunctionItem];
+ showMenu(event, items);
+ };
+}
diff --git a/devtools/client/debugger/src/actions/context-menus/source-tree-item.js b/devtools/client/debugger/src/actions/context-menus/source-tree-item.js
new file mode 100644
index 0000000000..1b7bc37dc3
--- /dev/null
+++ b/devtools/client/debugger/src/actions/context-menus/source-tree-item.js
@@ -0,0 +1,281 @@
+/* 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 { showMenu } from "../../context-menu/menu";
+
+import {
+ isSourceOverridden,
+ isSourceMapIgnoreListEnabled,
+ isSourceOnSourceMapIgnoreList,
+ getProjectDirectoryRoot,
+ getSourcesTreeSources,
+ getBlackBoxRanges,
+} from "../../selectors/index";
+
+import { setOverrideSource, removeOverrideSource } from "../sources/index";
+import { loadSourceText } from "../sources/loadSourceText";
+import { toggleBlackBox, blackBoxSources } from "../sources/blackbox";
+import {
+ setProjectDirectoryRoot,
+ clearProjectDirectoryRoot,
+} from "../sources-tree";
+
+import { shouldBlackbox } from "../../utils/source";
+import { copyToTheClipboard } from "../../utils/clipboard";
+import { saveAsLocalFile } from "../../utils/utils";
+
+/**
+ * Show the context menu of SourceTreeItem.
+ *
+ * @param {object} event
+ * The context-menu DOM event.
+ * @param {object} item
+ * Source Tree Item object.
+ */
+export function showSourceTreeItemContextMenu(
+ event,
+ item,
+ depth,
+ setExpanded,
+ itemName
+) {
+ return async ({ dispatch, getState }) => {
+ const copySourceUri2Label = L10N.getStr("copySourceUri2");
+ const copySourceUri2Key = L10N.getStr("copySourceUri2.accesskey");
+ const setDirectoryRootLabel = L10N.getStr("setDirectoryRoot.label");
+ const setDirectoryRootKey = L10N.getStr("setDirectoryRoot.accesskey");
+ const removeDirectoryRootLabel = L10N.getStr("removeDirectoryRoot.label");
+
+ const menuOptions = [];
+
+ const state = getState();
+ const isOverridden = isSourceOverridden(state, item.source);
+ const isSourceOnIgnoreList =
+ isSourceMapIgnoreListEnabled(state) &&
+ isSourceOnSourceMapIgnoreList(state, item.source);
+ const projectRoot = getProjectDirectoryRoot(state);
+
+ if (item.type == "source") {
+ const { source } = item;
+ const copySourceUri2 = {
+ id: "node-menu-copy-source",
+ label: copySourceUri2Label,
+ accesskey: copySourceUri2Key,
+ disabled: false,
+ click: () => copyToTheClipboard(source.url),
+ };
+
+ const ignoreStr = item.isBlackBoxed ? "unignore" : "ignore";
+ const blackBoxMenuItem = {
+ id: "node-menu-blackbox",
+ label: L10N.getStr(`ignoreContextItem.${ignoreStr}`),
+ accesskey: L10N.getStr(`ignoreContextItem.${ignoreStr}.accesskey`),
+ disabled: isSourceOnIgnoreList || !shouldBlackbox(source),
+ click: () => dispatch(toggleBlackBox(source)),
+ };
+ const downloadFileItem = {
+ id: "node-menu-download-file",
+ label: L10N.getStr("downloadFile.label"),
+ accesskey: L10N.getStr("downloadFile.accesskey"),
+ disabled: false,
+ click: () => saveLocalFile(dispatch, source),
+ };
+
+ const overrideStr = !isOverridden ? "override" : "removeOverride";
+ const overridesItem = {
+ id: "node-menu-overrides",
+ label: L10N.getStr(`overridesContextItem.${overrideStr}`),
+ accesskey: L10N.getStr(`overridesContextItem.${overrideStr}.accesskey`),
+ disabled: !!source.isHTML,
+ click: () => handleLocalOverride(dispatch, source, isOverridden),
+ };
+
+ menuOptions.push(
+ copySourceUri2,
+ blackBoxMenuItem,
+ downloadFileItem,
+ overridesItem
+ );
+ }
+
+ // All other types other than source are folder-like
+ if (item.type != "source") {
+ addCollapseExpandAllOptions(menuOptions, item, setExpanded);
+
+ if (projectRoot == item.uniquePath) {
+ menuOptions.push({
+ id: "node-remove-directory-root",
+ label: removeDirectoryRootLabel,
+ disabled: false,
+ click: () => dispatch(clearProjectDirectoryRoot()),
+ });
+ } else {
+ menuOptions.push({
+ id: "node-set-directory-root",
+ label: setDirectoryRootLabel,
+ accesskey: setDirectoryRootKey,
+ disabled: false,
+ click: () =>
+ dispatch(setProjectDirectoryRoot(item.uniquePath, itemName)),
+ });
+ }
+
+ addBlackboxAllOption(dispatch, state, menuOptions, item, depth);
+ }
+
+ showMenu(event, menuOptions);
+ };
+}
+
+async function saveLocalFile(dispatch, source) {
+ if (!source) {
+ return null;
+ }
+
+ const data = await dispatch(loadSourceText(source));
+ if (!data) {
+ return null;
+ }
+ return saveAsLocalFile(data.value, source.displayURL.filename);
+}
+
+async function handleLocalOverride(dispatch, source, isOverridden) {
+ if (!isOverridden) {
+ const localPath = await saveLocalFile(dispatch, source);
+ if (localPath) {
+ dispatch(setOverrideSource(source, localPath));
+ }
+ } else {
+ dispatch(removeOverrideSource(source));
+ }
+}
+
+function addBlackboxAllOption(dispatch, state, menuOptions, item, depth) {
+ const {
+ sourcesInside,
+ sourcesOutside,
+ allInsideBlackBoxed,
+ allOutsideBlackBoxed,
+ } = getBlackBoxSourcesGroups(state, item);
+ const projectRoot = getProjectDirectoryRoot(state);
+
+ let blackBoxInsideMenuItemLabel;
+ let blackBoxOutsideMenuItemLabel;
+ if (depth === 0 || (depth === 1 && projectRoot === "")) {
+ blackBoxInsideMenuItemLabel = allInsideBlackBoxed
+ ? L10N.getStr("unignoreAllInGroup.label")
+ : L10N.getStr("ignoreAllInGroup.label");
+ if (sourcesOutside.length) {
+ blackBoxOutsideMenuItemLabel = allOutsideBlackBoxed
+ ? L10N.getStr("unignoreAllOutsideGroup.label")
+ : L10N.getStr("ignoreAllOutsideGroup.label");
+ }
+ } else {
+ blackBoxInsideMenuItemLabel = allInsideBlackBoxed
+ ? L10N.getStr("unignoreAllInDir.label")
+ : L10N.getStr("ignoreAllInDir.label");
+ if (sourcesOutside.length) {
+ blackBoxOutsideMenuItemLabel = allOutsideBlackBoxed
+ ? L10N.getStr("unignoreAllOutsideDir.label")
+ : L10N.getStr("ignoreAllOutsideDir.label");
+ }
+ }
+
+ const blackBoxInsideMenuItem = {
+ id: allInsideBlackBoxed
+ ? "node-unblackbox-all-inside"
+ : "node-blackbox-all-inside",
+ label: blackBoxInsideMenuItemLabel,
+ disabled: false,
+ click: () => dispatch(blackBoxSources(sourcesInside, !allInsideBlackBoxed)),
+ };
+
+ if (sourcesOutside.length) {
+ menuOptions.push({
+ id: "node-blackbox-all",
+ label: L10N.getStr("ignoreAll.label"),
+ submenu: [
+ blackBoxInsideMenuItem,
+ {
+ id: allOutsideBlackBoxed
+ ? "node-unblackbox-all-outside"
+ : "node-blackbox-all-outside",
+ label: blackBoxOutsideMenuItemLabel,
+ disabled: false,
+ click: () =>
+ dispatch(blackBoxSources(sourcesOutside, !allOutsideBlackBoxed)),
+ },
+ ],
+ });
+ } else {
+ menuOptions.push(blackBoxInsideMenuItem);
+ }
+}
+
+function addCollapseExpandAllOptions(menuOptions, item, setExpanded) {
+ menuOptions.push({
+ id: "node-menu-collapse-all",
+ label: L10N.getStr("collapseAll.label"),
+ disabled: false,
+ click: () => setExpanded(item, false, true),
+ });
+
+ menuOptions.push({
+ id: "node-menu-expand-all",
+ label: L10N.getStr("expandAll.label"),
+ disabled: false,
+ click: () => setExpanded(item, true, true),
+ });
+}
+
+/**
+ * Computes 4 lists:
+ * - `sourcesInside`: the list of all Source Items that are
+ * children of the current item (can be thread/group/directory).
+ * This include any nested level of children.
+ * - `sourcesOutside`: all other Source Items.
+ * i.e. all sources that are in any other folder of any group/thread.
+ * - `allInsideBlackBoxed`, all sources of `sourcesInside` which are currently
+ * blackboxed.
+ * - `allOutsideBlackBoxed`, all sources of `sourcesOutside` which are currently
+ * blackboxed.
+ */
+function getBlackBoxSourcesGroups(state, item) {
+ const allSources = [];
+ function collectAllSources(list, _item) {
+ if (_item.children) {
+ _item.children.forEach(i => collectAllSources(list, i));
+ }
+ if (_item.type == "source") {
+ list.push(_item.source);
+ }
+ }
+
+ const rootItems = getSourcesTreeSources(state);
+ const blackBoxRanges = getBlackBoxRanges(state);
+
+ for (const rootItem of rootItems) {
+ collectAllSources(allSources, rootItem);
+ }
+
+ const sourcesInside = [];
+ collectAllSources(sourcesInside, item);
+
+ const sourcesOutside = allSources.filter(
+ source => !sourcesInside.includes(source)
+ );
+ const allInsideBlackBoxed = sourcesInside.every(
+ source => blackBoxRanges[source.url]
+ );
+ const allOutsideBlackBoxed = sourcesOutside.every(
+ source => blackBoxRanges[source.url]
+ );
+
+ return {
+ sourcesInside,
+ sourcesOutside,
+ allInsideBlackBoxed,
+ allOutsideBlackBoxed,
+ };
+}
diff --git a/devtools/client/debugger/src/actions/context-menus/tab.js b/devtools/client/debugger/src/actions/context-menus/tab.js
new file mode 100644
index 0000000000..193396a746
--- /dev/null
+++ b/devtools/client/debugger/src/actions/context-menus/tab.js
@@ -0,0 +1,128 @@
+/* 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 { showMenu, buildMenu } from "../../context-menu/menu";
+import { getTabMenuItems } from "../../utils/tabs";
+
+import {
+ getSelectedLocation,
+ getSourcesForTabs,
+ isSourceBlackBoxed,
+ isSourceMapIgnoreListEnabled,
+ isSourceOnSourceMapIgnoreList,
+} from "../../selectors/index";
+
+import { toggleBlackBox } from "../sources/blackbox";
+import { prettyPrintAndSelectSource } from "../sources/prettyPrint";
+import { copyToClipboard, showSource } from "../ui";
+import { closeTab, closeTabs } from "../tabs";
+
+import { getRawSourceURL, isPretty, shouldBlackbox } from "../../utils/source";
+import { copyToTheClipboard } from "../../utils/clipboard";
+
+/**
+ * Show the context menu of Tab.
+ *
+ * @param {object} event
+ * The context-menu DOM event.
+ * @param {object} source
+ * Source object of the related Tab.
+ */
+export function showTabContextMenu(event, source) {
+ return async ({ dispatch, getState }) => {
+ const state = getState();
+ const selectedLocation = getSelectedLocation(state);
+
+ const isBlackBoxed = isSourceBlackBoxed(state, source);
+ const isSourceOnIgnoreList =
+ isSourceMapIgnoreListEnabled(state) &&
+ isSourceOnSourceMapIgnoreList(state, source);
+ const tabsSources = getSourcesForTabs(state);
+
+ const otherTabsSources = tabsSources.filter(s => s !== source);
+ const tabIndex = tabsSources.findIndex(s => s === source);
+ const followingTabsSources = tabsSources.slice(tabIndex + 1);
+
+ const tabMenuItems = getTabMenuItems();
+ const items = [
+ {
+ item: {
+ ...tabMenuItems.closeTab,
+ click: () => dispatch(closeTab(source)),
+ },
+ },
+ {
+ item: {
+ ...tabMenuItems.closeOtherTabs,
+ disabled: otherTabsSources.length === 0,
+ click: () => dispatch(closeTabs(otherTabsSources)),
+ },
+ },
+ {
+ item: {
+ ...tabMenuItems.closeTabsToEnd,
+ disabled: followingTabsSources.length === 0,
+ click: () => {
+ dispatch(closeTabs(followingTabsSources));
+ },
+ },
+ },
+ {
+ item: {
+ ...tabMenuItems.closeAllTabs,
+ click: () => dispatch(closeTabs(tabsSources)),
+ },
+ },
+ { item: { type: "separator" } },
+ {
+ item: {
+ ...tabMenuItems.copySource,
+ // Only enable when this is the selected source as this requires the source to be loaded,
+ // which may not be the case if the tab wasn't ever selected.
+ //
+ // Note that when opening the debugger, you may have tabs opened from a previous session,
+ // but no selected location.
+ disabled: selectedLocation?.source.id !== source.id,
+ click: () => {
+ dispatch(copyToClipboard(selectedLocation));
+ },
+ },
+ },
+ {
+ item: {
+ ...tabMenuItems.copySourceUri2,
+ disabled: !source.url,
+ click: () => copyToTheClipboard(getRawSourceURL(source.url)),
+ },
+ },
+ {
+ item: {
+ ...tabMenuItems.showSource,
+ // Source Tree only shows sources with URL
+ disabled: !source.url,
+ click: () => dispatch(showSource(source.id)),
+ },
+ },
+ {
+ item: {
+ ...tabMenuItems.toggleBlackBox,
+ label: isBlackBoxed
+ ? L10N.getStr("ignoreContextItem.unignore")
+ : L10N.getStr("ignoreContextItem.ignore"),
+ disabled: isSourceOnIgnoreList || !shouldBlackbox(source),
+ click: () => dispatch(toggleBlackBox(source)),
+ },
+ },
+ {
+ item: {
+ ...tabMenuItems.prettyPrint,
+ disabled: isPretty(source),
+ click: () => dispatch(prettyPrintAndSelectSource(source)),
+ },
+ },
+ ];
+
+ showMenu(event, buildMenu(items));
+ };
+}
diff --git a/devtools/client/debugger/src/actions/event-listeners.js b/devtools/client/debugger/src/actions/event-listeners.js
new file mode 100644
index 0000000000..6eca5d7e9c
--- /dev/null
+++ b/devtools/client/debugger/src/actions/event-listeners.js
@@ -0,0 +1,77 @@
+/* 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 {
+ getActiveEventListeners,
+ getEventListenerExpanded,
+ shouldLogEventBreakpoints,
+} from "../selectors/index";
+
+async function updateBreakpoints(dispatch, client, newEvents) {
+ await client.setEventListenerBreakpoints(newEvents);
+ dispatch({ type: "UPDATE_EVENT_LISTENERS", active: newEvents });
+}
+
+async function updateExpanded(dispatch, newExpanded) {
+ dispatch({
+ type: "UPDATE_EVENT_LISTENER_EXPANDED",
+ expanded: newExpanded,
+ });
+}
+
+export function addEventListenerBreakpoints(eventsToAdd) {
+ return async ({ dispatch, client, getState }) => {
+ const activeListenerBreakpoints = await getActiveEventListeners(getState());
+
+ const newEvents = [
+ ...new Set([...eventsToAdd, ...activeListenerBreakpoints]),
+ ];
+ await updateBreakpoints(dispatch, client, newEvents);
+ };
+}
+
+export function removeEventListenerBreakpoints(eventsToRemove) {
+ return async ({ dispatch, client, getState }) => {
+ const activeListenerBreakpoints = await getActiveEventListeners(getState());
+
+ const newEvents = activeListenerBreakpoints.filter(
+ event => !eventsToRemove.includes(event)
+ );
+
+ await updateBreakpoints(dispatch, client, newEvents);
+ };
+}
+
+export function toggleEventLogging() {
+ return async ({ dispatch, getState, client }) => {
+ const logEventBreakpoints = !shouldLogEventBreakpoints(getState());
+ await client.toggleEventLogging(logEventBreakpoints);
+ dispatch({ type: "TOGGLE_EVENT_LISTENERS", logEventBreakpoints });
+ };
+}
+
+export function addEventListenerExpanded(category) {
+ return async ({ dispatch, getState }) => {
+ const expanded = await getEventListenerExpanded(getState());
+ const newExpanded = [...new Set([...expanded, category])];
+ await updateExpanded(dispatch, newExpanded);
+ };
+}
+
+export function removeEventListenerExpanded(category) {
+ return async ({ dispatch, getState }) => {
+ const expanded = await getEventListenerExpanded(getState());
+
+ const newExpanded = expanded.filter(expand => expand != category);
+
+ updateExpanded(dispatch, newExpanded);
+ };
+}
+
+export function getEventListenerBreakpointTypes() {
+ return async ({ dispatch, client }) => {
+ const categories = await client.getEventListenerBreakpointTypes();
+ dispatch({ type: "RECEIVE_EVENT_LISTENER_TYPES", categories });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/exceptions.js b/devtools/client/debugger/src/actions/exceptions.js
new file mode 100644
index 0000000000..f1746ec2bb
--- /dev/null
+++ b/devtools/client/debugger/src/actions/exceptions.js
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+export function addExceptionFromResources(resources) {
+ return async function ({ dispatch }) {
+ for (const resource of resources) {
+ const { pageError } = resource;
+ if (!pageError.error) {
+ continue;
+ }
+ const { columnNumber, lineNumber, sourceId, errorMessage } = pageError;
+ const stacktrace = pageError.stacktrace || [];
+
+ const exception = {
+ columnNumber,
+ lineNumber,
+ sourceActorId: sourceId,
+ errorMessage,
+ stacktrace,
+ threadActorId: resource.targetFront.targetForm.threadActor,
+ };
+
+ dispatch({
+ type: "ADD_EXCEPTION",
+ exception,
+ });
+ }
+ };
+}
diff --git a/devtools/client/debugger/src/actions/expressions.js b/devtools/client/debugger/src/actions/expressions.js
new file mode 100644
index 0000000000..8ccb6013ba
--- /dev/null
+++ b/devtools/client/debugger/src/actions/expressions.js
@@ -0,0 +1,210 @@
+/* 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 {
+ getExpression,
+ getExpressions,
+ getSelectedSource,
+ getSelectedScopeMappings,
+ getSelectedFrameBindings,
+ getIsPaused,
+ getSelectedFrame,
+ getCurrentThread,
+ isMapScopesEnabled,
+} from "../selectors/index";
+import { PROMISE } from "./utils/middleware/promise";
+import { wrapExpression } from "../utils/expressions";
+import { features } from "../utils/prefs";
+
+/**
+ * Add expression for debugger to watch
+ *
+ * @param {string} input
+ */
+export function addExpression(input) {
+ return async ({ dispatch, getState, parserWorker }) => {
+ if (!input) {
+ return null;
+ }
+
+ // If the expression already exists, only update its evaluation
+ let expression = getExpression(getState(), input);
+ if (!expression) {
+ // This will only display the expression input,
+ // evaluateExpression will update its value.
+ dispatch({ type: "ADD_EXPRESSION", input });
+
+ expression = getExpression(getState(), input);
+ // When there is an expression error, we won't store the expression
+ if (!expression) {
+ return null;
+ }
+ }
+
+ return dispatch(evaluateExpression(expression));
+ };
+}
+
+export function autocomplete(input, cursor) {
+ return async ({ dispatch, getState, client }) => {
+ if (!input) {
+ return;
+ }
+ const thread = getCurrentThread(getState());
+ const selectedFrame = getSelectedFrame(getState(), thread);
+ const result = await client.autocomplete(input, cursor, selectedFrame?.id);
+ // Pass both selectedFrame and thread in case selectedFrame is null
+ dispatch({ type: "AUTOCOMPLETE", selectedFrame, thread, input, result });
+ };
+}
+
+export function clearAutocomplete() {
+ return { type: "CLEAR_AUTOCOMPLETE" };
+}
+
+export function updateExpression(input, expression) {
+ return async ({ getState, dispatch, parserWorker }) => {
+ if (!input) {
+ return;
+ }
+
+ dispatch({
+ type: "UPDATE_EXPRESSION",
+ expression,
+ input,
+ });
+
+ await dispatch(evaluateExpressionsForCurrentContext());
+ };
+}
+
+/**
+ *
+ * @param {object} expression
+ * @param {string} expression.input
+ */
+export function deleteExpression(expression) {
+ return {
+ type: "DELETE_EXPRESSION",
+ input: expression.input,
+ };
+}
+
+export function evaluateExpressionsForCurrentContext() {
+ return async ({ getState, dispatch }) => {
+ const thread = getCurrentThread(getState());
+ const selectedFrame = getSelectedFrame(getState(), thread);
+ await dispatch(evaluateExpressions(selectedFrame));
+ };
+}
+
+/**
+ * Update all the expressions by querying the server for updated values.
+ *
+ * @param {object} selectedFrame
+ * If defined, will evaluate the expression against this given frame,
+ * otherwise it will use the global scope of the thread.
+ */
+export function evaluateExpressions(selectedFrame) {
+ return async function ({ dispatch, getState, client }) {
+ const expressions = getExpressions(getState());
+ const inputs = expressions.map(({ input }) => input);
+ // Fallback to global scope of the current thread when selectedFrame is null
+ const thread = selectedFrame?.thread || getCurrentThread(getState());
+ const results = await client.evaluateExpressions(inputs, {
+ // We will only have a specific frame when passing a Selected frame context.
+ frameId: selectedFrame?.id,
+ threadId: thread,
+ });
+ // Pass both selectedFrame and thread in case selectedFrame is null
+ dispatch({
+ type: "EVALUATE_EXPRESSIONS",
+
+ selectedFrame,
+ // As `selectedFrame` can be null, pass `thread` to help
+ // the reducer know what is the related thread of this action.
+ thread,
+
+ inputs,
+ results,
+ });
+ };
+}
+
+function evaluateExpression(expression) {
+ return async function (thunkArgs) {
+ let { input } = expression;
+ if (!input) {
+ console.warn("Expressions should not be empty");
+ return null;
+ }
+
+ const { dispatch, getState, client } = thunkArgs;
+ const thread = getCurrentThread(getState());
+ const selectedFrame = getSelectedFrame(getState(), thread);
+
+ const selectedSource = getSelectedSource(getState());
+ // Only map when we are paused and if the currently selected source is original,
+ // and the paused location is also original.
+ if (
+ selectedFrame &&
+ selectedSource &&
+ selectedFrame.location.source.isOriginal &&
+ selectedSource.isOriginal
+ ) {
+ const mapResult = await getMappedExpression(
+ input,
+ selectedFrame.thread,
+ thunkArgs
+ );
+ if (mapResult) {
+ input = mapResult.expression;
+ }
+ }
+
+ // Pass both selectedFrame and thread in case selectedFrame is null
+ return dispatch({
+ type: "EVALUATE_EXPRESSION",
+ selectedFrame,
+ // When we aren't passing a frame, we have to pass a thread to the pause reducer
+ thread: selectedFrame ? null : thread,
+ input: expression.input,
+ [PROMISE]: client.evaluate(wrapExpression(input), {
+ // When evaluating against the global scope (when not paused)
+ // frameId will be null here.
+ frameId: selectedFrame?.id,
+ }),
+ });
+ };
+}
+
+/**
+ * Gets information about original variable names from the source map
+ * and replaces all possible generated names.
+ */
+export function getMappedExpression(expression, thread, thunkArgs) {
+ const { getState, parserWorker } = thunkArgs;
+ const mappings = getSelectedScopeMappings(getState(), thread);
+ const bindings = getSelectedFrameBindings(getState(), thread);
+
+ // We bail early if we do not need to map the expression. This is important
+ // because mapping an expression can be slow if the parserWorker
+ // worker is busy doing other work.
+ //
+ // 1. there are no mappings - we do not need to map original expressions
+ // 2. does not contain `await` - we do not need to map top level awaits
+ // 3. does not contain `=` - we do not need to map assignments
+ const shouldMapScopes = isMapScopesEnabled(getState()) && mappings;
+ if (!shouldMapScopes && !expression.match(/(await|=)/)) {
+ return null;
+ }
+
+ return parserWorker.mapExpression(
+ expression,
+ mappings,
+ bindings || [],
+ features.mapExpressionBindings && getIsPaused(getState(), thread),
+ features.mapAwaitExpression
+ );
+}
diff --git a/devtools/client/debugger/src/actions/file-search.js b/devtools/client/debugger/src/actions/file-search.js
new file mode 100644
index 0000000000..cc5794d7ab
--- /dev/null
+++ b/devtools/client/debugger/src/actions/file-search.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/>. */
+
+import { searchSourceForHighlight } from "../utils/editor/index";
+
+import {
+ getSelectedSourceTextContent,
+ getSearchOptions,
+} from "../selectors/index";
+
+import { closeActiveSearch, clearHighlightLineRange } from "./ui";
+
+export function doSearchForHighlight(query, editor, line, ch) {
+ return async ({ getState, dispatch }) => {
+ const sourceTextContent = getSelectedSourceTextContent(getState());
+ if (!sourceTextContent) {
+ return;
+ }
+
+ dispatch(searchContentsForHighlight(query, editor, line, ch));
+ };
+}
+
+// Expose an action to the React component, so that it can call the searchWorker.
+export function querySearchWorker(query, text, modifiers) {
+ return ({ searchWorker }) => {
+ return searchWorker.getMatches(query, text, modifiers);
+ };
+}
+
+export function searchContentsForHighlight(query, editor, line, ch) {
+ return async ({ getState, dispatch }) => {
+ const modifiers = getSearchOptions(getState(), "file-search");
+ const sourceTextContent = getSelectedSourceTextContent(getState());
+
+ if (!query || !editor || !sourceTextContent || !modifiers) {
+ return;
+ }
+
+ const ctx = { ed: editor, cm: editor.codeMirror };
+ searchSourceForHighlight(ctx, false, query, true, modifiers, line, ch);
+ };
+}
+
+export function closeFileSearch() {
+ return ({ dispatch }) => {
+ dispatch(closeActiveSearch());
+ dispatch(clearHighlightLineRange());
+ };
+}
diff --git a/devtools/client/debugger/src/actions/index.js b/devtools/client/debugger/src/actions/index.js
new file mode 100644
index 0000000000..cc568f9681
--- /dev/null
+++ b/devtools/client/debugger/src/actions/index.js
@@ -0,0 +1,50 @@
+/* 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 * as ast from "./ast/index";
+import * as breakpoints from "./breakpoints/index";
+import * as exceptions from "./exceptions";
+import * as expressions from "./expressions";
+import * as eventListeners from "./event-listeners";
+import * as pause from "./pause/index";
+import * as navigation from "./navigation";
+import * as ui from "./ui";
+import * as fileSearch from "./file-search";
+import * as projectTextSearch from "./project-text-search";
+import * as quickOpen from "./quick-open";
+import * as sourcesTree from "./sources-tree";
+import * as sources from "./sources/index";
+import * as sourcesActors from "./source-actors";
+import * as tabs from "./tabs";
+import * as threads from "./threads";
+import * as toolbox from "./toolbox";
+import * as preview from "./preview";
+import * as tracing from "./tracing";
+import * as contextMenu from "./context-menus/index";
+
+import { objectInspector } from "devtools/client/shared/components/reps/index";
+
+export default {
+ ...ast,
+ ...navigation,
+ ...breakpoints,
+ ...exceptions,
+ ...expressions,
+ ...eventListeners,
+ ...sources,
+ ...sourcesActors,
+ ...tabs,
+ ...pause,
+ ...ui,
+ ...fileSearch,
+ ...objectInspector.actions,
+ ...projectTextSearch,
+ ...quickOpen,
+ ...sourcesTree,
+ ...threads,
+ ...toolbox,
+ ...preview,
+ ...tracing,
+ ...contextMenu,
+};
diff --git a/devtools/client/debugger/src/actions/moz.build b/devtools/client/debugger/src/actions/moz.build
new file mode 100644
index 0000000000..72b1fac04c
--- /dev/null
+++ b/devtools/client/debugger/src/actions/moz.build
@@ -0,0 +1,32 @@
+# 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 += [
+ "ast",
+ "breakpoints",
+ "context-menus",
+ "pause",
+ "sources",
+ "utils",
+]
+
+CompiledModules(
+ "event-listeners.js",
+ "exceptions.js",
+ "expressions.js",
+ "file-search.js",
+ "index.js",
+ "navigation.js",
+ "preview.js",
+ "project-text-search.js",
+ "quick-open.js",
+ "source-actors.js",
+ "sources-tree.js",
+ "tabs.js",
+ "toolbox.js",
+ "tracing.js",
+ "threads.js",
+ "ui.js",
+)
diff --git a/devtools/client/debugger/src/actions/navigation.js b/devtools/client/debugger/src/actions/navigation.js
new file mode 100644
index 0000000000..1b437837c2
--- /dev/null
+++ b/devtools/client/debugger/src/actions/navigation.js
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+import sourceQueue from "../utils/source-queue";
+
+import { clearWasmStates } from "../utils/wasm";
+import { getMainThread } from "../selectors/index";
+import { evaluateExpressionsForCurrentContext } from "../actions/expressions";
+
+/**
+ * Redux actions for the navigation state
+ * @module actions/navigation
+ */
+
+/**
+ * @memberof actions/navigation
+ * @static
+ */
+export function willNavigate(event) {
+ return async function ({
+ dispatch,
+ getState,
+ client,
+ sourceMapLoader,
+ parserWorker,
+ }) {
+ sourceQueue.clear();
+ sourceMapLoader.clearSourceMaps();
+ clearWasmStates();
+ const thread = getMainThread(getState());
+
+ dispatch({
+ type: "NAVIGATE",
+ mainThread: { ...thread, url: event.url },
+ });
+ };
+}
+
+/**
+ * @memberof actions/navigation
+ * @static
+ */
+export function navigated() {
+ return async function ({ getState, dispatch, panel }) {
+ try {
+ // Update the watched expressions once the page is fully loaded
+ await dispatch(evaluateExpressionsForCurrentContext());
+ } catch (e) {
+ // This may throw if we resume during the page load.
+ // browser_dbg-debugger-buttons.js highlights this, especially on MacOS or when ran many times
+ console.error("Failed to update expression on navigation", e);
+ }
+
+ panel.emit("reloaded");
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/breakOnNext.js b/devtools/client/debugger/src/actions/pause/breakOnNext.js
new file mode 100644
index 0000000000..13f9f5f79c
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/breakOnNext.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/>. */
+
+import { getCurrentThread } from "../../selectors/index";
+/**
+ * Debugger breakOnNext command.
+ * It's different from the comand action because we also want to
+ * highlight the pause icon.
+ *
+ * @memberof actions/pause
+ * @static
+ */
+export function breakOnNext() {
+ return async ({ dispatch, getState, client }) => {
+ const thread = getCurrentThread(getState());
+ await client.breakOnNext(thread);
+ return dispatch({ type: "BREAK_ON_NEXT", thread });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/commands.js b/devtools/client/debugger/src/actions/pause/commands.js
new file mode 100644
index 0000000000..0bb371bf6e
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/commands.js
@@ -0,0 +1,147 @@
+/* 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 {
+ getSelectedFrame,
+ getCurrentThread,
+ getIsCurrentThreadPaused,
+ getIsPaused,
+} from "../../selectors/index";
+import { PROMISE } from "../utils/middleware/promise";
+import { evaluateExpressions } from "../expressions";
+import { selectLocation } from "../sources/index";
+import { fetchScopes } from "./fetchScopes";
+import { fetchFrames } from "./fetchFrames";
+import { recordEvent } from "../../utils/telemetry";
+import { validateFrame } from "../../utils/context";
+
+export function selectThread(thread) {
+ return async ({ dispatch, getState, client }) => {
+ if (getCurrentThread(getState()) === thread) {
+ return;
+ }
+ dispatch({ type: "SELECT_THREAD", thread });
+
+ const selectedFrame = getSelectedFrame(getState(), thread);
+
+ const serverRequests = [];
+ // Update the watched expressions as we may never have evaluated them against this thread
+ // Note that selectedFrame may be null if the thread isn't paused.
+ serverRequests.push(dispatch(evaluateExpressions(selectedFrame)));
+
+ // If we were paused on the newly selected thread, ensure:
+ // - select the source where we are paused,
+ // - fetching the paused stackframes,
+ // - fetching the paused scope, so that variable preview are working on the selected source.
+ // (frames and scopes is supposed to be fetched on pause,
+ // but if two threads pause concurrently, it might be cancelled)
+ if (selectedFrame) {
+ serverRequests.push(dispatch(selectLocation(selectedFrame.location)));
+ serverRequests.push(dispatch(fetchFrames(thread)));
+
+ serverRequests.push(dispatch(fetchScopes(selectedFrame)));
+ }
+
+ await Promise.all(serverRequests);
+ };
+}
+
+/**
+ * Debugger commands like stepOver, stepIn, stepOut, resume
+ *
+ * @param string type
+ */
+export function command(type) {
+ return async ({ dispatch, getState, client }) => {
+ if (!type) {
+ return null;
+ }
+ // For now, all commands are by default against the currently selected thread
+ const thread = getCurrentThread(getState());
+
+ const frame = getSelectedFrame(getState(), thread);
+
+ return dispatch({
+ type: "COMMAND",
+ command: type,
+ thread,
+ [PROMISE]: client[type](thread, frame?.id),
+ });
+ };
+}
+
+/**
+ * StepIn
+ *
+ * @returns {Function} {@link command}
+ */
+export function stepIn() {
+ return ({ dispatch, getState }) => {
+ if (!getIsCurrentThreadPaused(getState())) {
+ return null;
+ }
+ return dispatch(command("stepIn"));
+ };
+}
+
+/**
+ * stepOver
+ *
+ * @returns {Function} {@link command}
+ */
+export function stepOver() {
+ return ({ dispatch, getState }) => {
+ if (!getIsCurrentThreadPaused(getState())) {
+ return null;
+ }
+ return dispatch(command("stepOver"));
+ };
+}
+
+/**
+ * stepOut
+ *
+ * @returns {Function} {@link command}
+ */
+export function stepOut() {
+ return ({ dispatch, getState }) => {
+ if (!getIsCurrentThreadPaused(getState())) {
+ return null;
+ }
+ return dispatch(command("stepOut"));
+ };
+}
+
+/**
+ * resume
+ *
+ * @returns {Function} {@link command}
+ */
+export function resume() {
+ return ({ dispatch, getState }) => {
+ if (!getIsCurrentThreadPaused(getState())) {
+ return null;
+ }
+ recordEvent("continue");
+ return dispatch(command("resume"));
+ };
+}
+
+/**
+ * restart frame
+ */
+export function restart(frame) {
+ return async ({ dispatch, getState, client }) => {
+ if (!getIsPaused(getState(), frame.thread)) {
+ return null;
+ }
+ validateFrame(getState(), frame);
+ return dispatch({
+ type: "COMMAND",
+ command: "restart",
+ thread: frame.thread,
+ [PROMISE]: client.restart(frame.thread, frame.id),
+ });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/continueToHere.js b/devtools/client/debugger/src/actions/pause/continueToHere.js
new file mode 100644
index 0000000000..046ad4d69a
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/continueToHere.js
@@ -0,0 +1,63 @@
+/* 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,
+ getSelectedFrame,
+ getClosestBreakpointPosition,
+ getBreakpoint,
+ getCurrentThread,
+} from "../../selectors/index";
+import { createLocation } from "../../utils/location";
+import { addHiddenBreakpoint } from "../breakpoints/index";
+import { setBreakpointPositions } from "../breakpoints/breakpointPositions";
+
+import { resume } from "./commands";
+
+export function continueToHere(location) {
+ return async function ({ dispatch, getState }) {
+ const { line, column } = location;
+ const thread = getCurrentThread(getState());
+ const selectedSource = getSelectedSource(getState());
+ const selectedFrame = getSelectedFrame(getState(), thread);
+
+ if (!selectedFrame || !selectedSource) {
+ return;
+ }
+
+ const debugLine = selectedFrame.location.line;
+ // If the user selects a line to continue to,
+ // it must be different than the currently paused line.
+ if (!column && debugLine == line) {
+ return;
+ }
+
+ await dispatch(setBreakpointPositions(location));
+ const position = getClosestBreakpointPosition(getState(), location);
+
+ // If the user selects a location in the editor,
+ // there must be a place we can pause on that line.
+ if (column && !position) {
+ return;
+ }
+
+ const pauseLocation = column && position ? position.location : location;
+
+ // Set a hidden breakpoint if we do not already have a breakpoint
+ // at the closest position
+ if (!getBreakpoint(getState(), pauseLocation)) {
+ await dispatch(
+ addHiddenBreakpoint(
+ createLocation({
+ source: selectedSource,
+ line: pauseLocation.line,
+ column: pauseLocation.column,
+ })
+ )
+ );
+ }
+
+ dispatch(resume());
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/expandScopes.js b/devtools/client/debugger/src/actions/pause/expandScopes.js
new file mode 100644
index 0000000000..af95f16fea
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/expandScopes.js
@@ -0,0 +1,16 @@
+/* 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 { getScopeItemPath } from "../../utils/pause/scopes";
+
+export function setExpandedScope(selectedFrame, item, expanded) {
+ return function ({ dispatch, getState }) {
+ return dispatch({
+ type: "SET_EXPANDED_SCOPE",
+ selectedFrame,
+ path: getScopeItemPath(item),
+ expanded,
+ });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/fetchFrames.js b/devtools/client/debugger/src/actions/pause/fetchFrames.js
new file mode 100644
index 0000000000..8db22a852e
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/fetchFrames.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 { getIsPaused } from "../../selectors/index";
+
+export function fetchFrames(thread) {
+ return async function ({ dispatch, client, getState }) {
+ let frames;
+ try {
+ frames = await client.getFrames(thread);
+ } catch (e) {
+ // getFrames will fail if the thread has resumed. In this case the thread
+ // should no longer be valid and the frames we would have fetched would be
+ // discarded anyways.
+ if (getIsPaused(getState(), thread)) {
+ throw e;
+ }
+ }
+ dispatch({ type: "FETCHED_FRAMES", thread, frames });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/fetchScopes.js b/devtools/client/debugger/src/actions/pause/fetchScopes.js
new file mode 100644
index 0000000000..1de472d1ed
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/fetchScopes.js
@@ -0,0 +1,37 @@
+/* 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 {
+ getGeneratedFrameScope,
+ getOriginalFrameScope,
+} from "../../selectors/index";
+import { mapScopes } from "./mapScopes";
+import { generateInlinePreview } from "./inlinePreview";
+import { PROMISE } from "../utils/middleware/promise";
+
+export function fetchScopes(selectedFrame) {
+ return async function ({ dispatch, getState, client }) {
+ // See if we already fetched the scopes.
+ // We may have pause on multiple thread and re-select a paused thread
+ // for which we already fetched the scopes.
+ // Ignore pending scopes as the previous action may have been cancelled
+ // by context assertions.
+ let scopes = getGeneratedFrameScope(getState(), selectedFrame);
+ if (!scopes?.scope) {
+ scopes = dispatch({
+ type: "ADD_SCOPES",
+ selectedFrame,
+ [PROMISE]: client.getFrameScopes(selectedFrame),
+ });
+
+ scopes.then(() => {
+ dispatch(generateInlinePreview(selectedFrame));
+ });
+ }
+
+ if (!getOriginalFrameScope(getState(), selectedFrame)) {
+ await dispatch(mapScopes(selectedFrame, scopes));
+ }
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/index.js b/devtools/client/debugger/src/actions/pause/index.js
new file mode 100644
index 0000000000..6ead921921
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/index.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/>. */
+
+/**
+ * Redux actions for the pause state
+ * @module actions/pause
+ */
+
+export {
+ selectThread,
+ stepIn,
+ stepOver,
+ stepOut,
+ resume,
+ restart,
+} from "./commands";
+export { fetchFrames } from "./fetchFrames";
+export { fetchScopes } from "./fetchScopes";
+export { paused } from "./paused";
+export { resumed } from "./resumed";
+export { continueToHere } from "./continueToHere";
+export { breakOnNext } from "./breakOnNext";
+export { resetBreakpointsPaneState } from "./resetBreakpointsPaneState";
+export { mapFrames } from "./mapFrames";
+export { pauseOnDebuggerStatement } from "./pauseOnDebuggerStatement";
+export { pauseOnExceptions } from "./pauseOnExceptions";
+export { selectFrame } from "./selectFrame";
+export { toggleSkipPausing, setSkipPausing } from "./skipPausing";
+export { toggleMapScopes } from "./mapScopes";
+export { setExpandedScope } from "./expandScopes";
+export { generateInlinePreview } from "./inlinePreview";
diff --git a/devtools/client/debugger/src/actions/pause/inlinePreview.js b/devtools/client/debugger/src/actions/pause/inlinePreview.js
new file mode 100644
index 0000000000..4f32ff6292
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/inlinePreview.js
@@ -0,0 +1,239 @@
+/* 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 {
+ getOriginalFrameScope,
+ getGeneratedFrameScope,
+ getInlinePreviews,
+ getSelectedLocation,
+} from "../../selectors/index";
+import { features } from "../../utils/prefs";
+import { validateSelectedFrame } from "../../utils/context";
+
+// We need to display all variables in the current functional scope so
+// include all data for block scopes until the first functional scope
+function getLocalScopeLevels(originalAstScopes) {
+ let levels = 0;
+ while (
+ originalAstScopes[levels] &&
+ originalAstScopes[levels].type === "block"
+ ) {
+ levels++;
+ }
+ return levels;
+}
+
+export function generateInlinePreview(selectedFrame) {
+ return async function ({ dispatch, getState, parserWorker, client }) {
+ if (!features.inlinePreview) {
+ return null;
+ }
+
+ // Avoid regenerating inline previews when we already have preview data
+ if (getInlinePreviews(getState(), selectedFrame.thread, selectedFrame.id)) {
+ return null;
+ }
+
+ const originalFrameScopes = getOriginalFrameScope(
+ getState(),
+ selectedFrame
+ );
+
+ const generatedFrameScopes = getGeneratedFrameScope(
+ getState(),
+ selectedFrame
+ );
+
+ let scopes = originalFrameScopes?.scope || generatedFrameScopes?.scope;
+
+ if (!scopes || !scopes.bindings) {
+ return null;
+ }
+
+ // It's important to use selectedLocation, because we don't know
+ // if we'll be viewing the original or generated frame location
+ const selectedLocation = getSelectedLocation(getState());
+ if (!selectedLocation) {
+ return null;
+ }
+
+ if (!parserWorker.isLocationSupported(selectedLocation)) {
+ return null;
+ }
+
+ const originalAstScopes = await parserWorker.getScopes(selectedLocation);
+ validateSelectedFrame(getState(), selectedFrame);
+
+ if (!originalAstScopes) {
+ return null;
+ }
+
+ const allPreviews = [];
+ const pausedOnLine = selectedLocation.line;
+ const levels = getLocalScopeLevels(originalAstScopes);
+
+ for (
+ let curLevel = 0;
+ curLevel <= levels && scopes && scopes.bindings;
+ curLevel++
+ ) {
+ const bindings = { ...scopes.bindings.variables };
+ scopes.bindings.arguments.forEach(argument => {
+ Object.keys(argument).forEach(key => {
+ bindings[key] = argument[key];
+ });
+ });
+
+ const previewBindings = Object.keys(bindings).map(async name => {
+ // We want to show values of properties of objects only and not
+ // function calls on other data types like someArr.forEach etc..
+ let properties = null;
+ const objectGrip = bindings[name].value;
+ if (objectGrip.actor && objectGrip.class === "Object") {
+ properties = await client.loadObjectProperties(
+ {
+ name,
+ path: name,
+ contents: { value: objectGrip },
+ },
+ selectedFrame.thread
+ );
+ }
+
+ const previewsFromBindings = getBindingValues(
+ originalAstScopes,
+ pausedOnLine,
+ name,
+ bindings[name].value,
+ curLevel,
+ properties
+ );
+
+ allPreviews.push(...previewsFromBindings);
+ });
+ await Promise.all(previewBindings);
+
+ scopes = scopes.parent;
+ }
+
+ // Sort previews by line and column so they're displayed in the right order in the editor
+ allPreviews.sort((previewA, previewB) => {
+ if (previewA.line < previewB.line) {
+ return -1;
+ }
+ if (previewA.line > previewB.line) {
+ return 1;
+ }
+ // If we have the same line number
+ return previewA.column < previewB.column ? -1 : 1;
+ });
+
+ const previews = {};
+ for (const preview of allPreviews) {
+ const { line } = preview;
+ if (!previews[line]) {
+ previews[line] = [];
+ }
+ previews[line].push(preview);
+ }
+
+ return dispatch({
+ type: "ADD_INLINE_PREVIEW",
+ selectedFrame,
+ previews,
+ });
+ };
+}
+
+function getBindingValues(
+ originalAstScopes,
+ pausedOnLine,
+ name,
+ value,
+ curLevel,
+ properties
+) {
+ const previews = [];
+
+ const binding = originalAstScopes[curLevel]?.bindings[name];
+ if (!binding) {
+ return previews;
+ }
+
+ // Show a variable only once ( an object and it's child property are
+ // counted as different )
+ const identifiers = new Set();
+
+ // We start from end as we want to show values besides variable
+ // located nearest to the breakpoint
+ for (let i = binding.refs.length - 1; i >= 0; i--) {
+ const ref = binding.refs[i];
+ // Subtracting 1 from line as codemirror lines are 0 indexed
+ const line = ref.start.line - 1;
+ const column = ref.start.column;
+ // We don't want to render inline preview below the paused line
+ if (line >= pausedOnLine - 1) {
+ continue;
+ }
+
+ const { displayName, displayValue } = getExpressionNameAndValue(
+ name,
+ value,
+ ref,
+ properties
+ );
+
+ // Variable with same name exists, display value of current or
+ // closest to the current scope's variable
+ if (identifiers.has(displayName)) {
+ continue;
+ }
+ identifiers.add(displayName);
+
+ previews.push({
+ line,
+ column,
+ name: displayName,
+ value: displayValue,
+ });
+ }
+ return previews;
+}
+
+function getExpressionNameAndValue(
+ name,
+ value,
+ // TODO: Add data type to ref
+ ref,
+ properties
+) {
+ let displayName = name;
+ let displayValue = value;
+
+ // Only variables of type Object will have properties
+ if (properties) {
+ let { meta } = ref;
+ // Presence of meta property means expression contains child property
+ // reference eg: objName.propName
+ while (meta) {
+ // Initially properties will be an array, after that it will be an object
+ if (displayValue === value) {
+ const property = properties.find(prop => prop.name === meta.property);
+ displayValue = property?.contents.value;
+ displayName += `.${meta.property}`;
+ } else if (displayValue?.preview?.ownProperties) {
+ const { ownProperties } = displayValue.preview;
+ Object.keys(ownProperties).forEach(prop => {
+ if (prop === meta.property) {
+ displayValue = ownProperties[prop].value;
+ displayName += `.${meta.property}`;
+ }
+ });
+ }
+ meta = meta.parent;
+ }
+ }
+
+ return { displayName, displayValue };
+}
diff --git a/devtools/client/debugger/src/actions/pause/mapFrames.js b/devtools/client/debugger/src/actions/pause/mapFrames.js
new file mode 100644
index 0000000000..9ce7052db1
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/mapFrames.js
@@ -0,0 +1,161 @@
+/* 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 {
+ getFrames,
+ getBlackBoxRanges,
+ getSelectedFrame,
+} from "../../selectors/index";
+
+import { isFrameBlackBoxed } from "../../utils/source";
+
+import assert from "../../utils/assert";
+import { getOriginalLocation } from "../../utils/source-maps";
+import {
+ debuggerToSourceMapLocation,
+ sourceMapToDebuggerLocation,
+} from "../../utils/location";
+import { annotateFramesWithLibrary } from "../../utils/pause/frames/annotateFrames";
+import { createWasmOriginalFrame } from "../../client/firefox/create";
+
+import { getOriginalFunctionDisplayName } from "../sources/index";
+
+function getSelectedFrameId(state, thread, frames) {
+ let selectedFrame = getSelectedFrame(state, thread);
+ const blackboxedRanges = getBlackBoxRanges(state);
+
+ if (selectedFrame && !isFrameBlackBoxed(selectedFrame, blackboxedRanges)) {
+ return selectedFrame.id;
+ }
+
+ selectedFrame = frames.find(frame => {
+ return !isFrameBlackBoxed(frame, blackboxedRanges);
+ });
+ return selectedFrame?.id;
+}
+
+async function updateFrameLocationAndDisplayName(frame, thunkArgs) {
+ // Ignore WASM original sources
+ if (frame.isOriginal) {
+ return frame;
+ }
+
+ const location = await getOriginalLocation(frame.location, thunkArgs, {
+ waitForSource: true,
+ });
+ // Avoid instantiating new frame objects if the frame location isn't mapped
+ if (location == frame.location) {
+ return frame;
+ }
+
+ // As we now know that this frame relates to an original source...
+ // Fetch the symbols for it and compute the frame's originalDisplayName.
+ const originalDisplayName = await thunkArgs.dispatch(
+ getOriginalFunctionDisplayName(location)
+ );
+
+ // As we modify frame object, fork it to force causing re-renders
+ return {
+ ...frame,
+ location,
+ generatedLocation: frame.generatedLocation || frame.location,
+ originalDisplayName,
+ };
+}
+
+function isWasmOriginalSourceFrame(frame) {
+ if (!frame.location.source.isOriginal) {
+ return false;
+ }
+
+ return Boolean(frame.generatedLocation?.source.isWasm);
+}
+
+/**
+ * Wasm Source Maps can come with an non-standard "xScopes" attribute
+ * which allows mapping the scope of a given location.
+ */
+async function expandWasmFrames(frames, { getState, sourceMapLoader }) {
+ const result = [];
+ for (let i = 0; i < frames.length; ++i) {
+ const frame = frames[i];
+ if (frame.isOriginal || !isWasmOriginalSourceFrame(frame)) {
+ result.push(frame);
+ continue;
+ }
+ const originalFrames = await sourceMapLoader.getOriginalStackFrames(
+ debuggerToSourceMapLocation(frame.generatedLocation)
+ );
+ if (!originalFrames) {
+ result.push(frame);
+ continue;
+ }
+
+ assert(!!originalFrames.length, "Expected at least one original frame");
+ // First entry has no specific location -- use one from the generated frame.
+ originalFrames[0].location = frame.location;
+
+ originalFrames.forEach((originalFrame, j) => {
+ if (!originalFrame.location) {
+ return;
+ }
+
+ // Keep outer most frame with true actor ID, and generate unique
+ // one for the nested frames.
+ const id = j == 0 ? frame.id : `${frame.id}-originalFrame${j}`;
+ const originalFrameLocation = sourceMapToDebuggerLocation(
+ getState(),
+ originalFrame.location
+ );
+ result.push(
+ createWasmOriginalFrame(frame, id, originalFrame, originalFrameLocation)
+ );
+ });
+ }
+ return result;
+}
+
+/**
+ * Map call stack frame locations and display names to originals.
+ * e.g.
+ * 1. When the debuggee pauses
+ * 2. When a source is pretty printed
+ * 3. When symbols are loaded
+ * @memberof actions/pause
+ * @static
+ */
+export function mapFrames(thread) {
+ return async function (thunkArgs) {
+ const { dispatch, getState } = thunkArgs;
+ const frames = getFrames(getState(), thread);
+ if (!frames || !frames.length) {
+ return;
+ }
+
+ // Update frame's location/generatedLocation/originalDisplayNames in case it relates to an original source
+ let mappedFrames = await Promise.all(
+ frames.map(frame => updateFrameLocationAndDisplayName(frame, thunkArgs))
+ );
+
+ mappedFrames = await expandWasmFrames(mappedFrames, thunkArgs);
+
+ // Add the "library" attribute on all frame objects (if relevant)
+ annotateFramesWithLibrary(mappedFrames);
+
+ // After having mapped the frames, we should update the selected frame
+ // just in case the selected frame is now set on a blackboxed original source
+ const selectedFrameId = getSelectedFrameId(
+ getState(),
+ thread,
+ mappedFrames
+ );
+
+ dispatch({
+ type: "MAP_FRAMES",
+ thread,
+ frames: mappedFrames,
+ selectedFrameId,
+ });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/mapScopes.js b/devtools/client/debugger/src/actions/pause/mapScopes.js
new file mode 100644
index 0000000000..8cb096c0a8
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/mapScopes.js
@@ -0,0 +1,201 @@
+/* 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 {
+ getSettledSourceTextContent,
+ isMapScopesEnabled,
+ getSelectedFrame,
+ getGeneratedFrameScope,
+ getOriginalFrameScope,
+ getFirstSourceActorForGeneratedSource,
+ getCurrentThread,
+} from "../../selectors/index";
+import {
+ loadOriginalSourceText,
+ loadGeneratedSourceText,
+} from "../sources/loadSourceText";
+import { validateSelectedFrame } from "../../utils/context";
+import { PROMISE } from "../utils/middleware/promise";
+
+import { log } from "../../utils/log";
+
+import { buildMappedScopes } from "../../utils/pause/mapScopes/index";
+import { isFulfilled } from "../../utils/async-value";
+
+import { getMappedLocation } from "../../utils/source-maps";
+
+const expressionRegex = /\bfp\(\)/g;
+
+export async function buildOriginalScopes(
+ selectedFrame,
+ client,
+ generatedScopes
+) {
+ if (!selectedFrame.originalVariables) {
+ throw new TypeError("(frame.originalVariables: XScopeVariables)");
+ }
+ const originalVariables = selectedFrame.originalVariables;
+ const frameBase = originalVariables.frameBase || "";
+
+ const inputs = [];
+ for (let i = 0; i < originalVariables.vars.length; i++) {
+ const { expr } = originalVariables.vars[i];
+ const expression = expr
+ ? expr.replace(expressionRegex, frameBase)
+ : "void 0";
+
+ inputs[i] = expression;
+ }
+
+ const results = await client.evaluateExpressions(inputs, {
+ frameId: selectedFrame.id,
+ });
+
+ const variables = {};
+ for (let i = 0; i < originalVariables.vars.length; i++) {
+ const { name } = originalVariables.vars[i];
+ variables[name] = { value: results[i].result };
+ }
+
+ const bindings = {
+ arguments: [],
+ variables,
+ };
+
+ const { actor } = await generatedScopes;
+ const scope = {
+ type: "function",
+ scopeKind: "",
+ actor,
+ bindings,
+ parent: null,
+ function: null,
+ block: null,
+ };
+ return {
+ mappings: {},
+ scope,
+ };
+}
+
+export function toggleMapScopes() {
+ return async function ({ dispatch, getState }) {
+ if (isMapScopesEnabled(getState())) {
+ dispatch({ type: "TOGGLE_MAP_SCOPES", mapScopes: false });
+ return;
+ }
+
+ dispatch({ type: "TOGGLE_MAP_SCOPES", mapScopes: true });
+
+ // Ignore the call if there is no selected frame (we are not paused?)
+ const state = getState();
+ const selectedFrame = getSelectedFrame(state, getCurrentThread(state));
+ if (!selectedFrame) {
+ return;
+ }
+
+ if (getOriginalFrameScope(getState(), selectedFrame)) {
+ return;
+ }
+
+ // Also ignore the call if we didn't fetch the scopes for the selected frame
+ const scopes = getGeneratedFrameScope(getState(), selectedFrame);
+ if (!scopes) {
+ return;
+ }
+
+ dispatch(mapScopes(selectedFrame, Promise.resolve(scopes.scope)));
+ };
+}
+
+export function mapScopes(selectedFrame, scopes) {
+ return async function (thunkArgs) {
+ const { getState, dispatch, client } = thunkArgs;
+
+ await dispatch({
+ type: "MAP_SCOPES",
+ selectedFrame,
+ [PROMISE]: (async function () {
+ if (selectedFrame.isOriginal && selectedFrame.originalVariables) {
+ return buildOriginalScopes(selectedFrame, client, scopes);
+ }
+
+ // getMappedScopes is only specific to the sources where we map the variables
+ // in scope and so only need a thread context. Assert that we are on the same thread
+ // before retrieving a thread context.
+ validateSelectedFrame(getState(), selectedFrame);
+
+ return dispatch(getMappedScopes(scopes, selectedFrame));
+ })(),
+ });
+ };
+}
+
+/**
+ * Get scopes mapped for a precise location.
+ *
+ * @param {Promise} scopes
+ * Can be null. Result of Commands.js's client.getFrameScopes
+ * @param {Objects locations
+ * Frame object, or custom object with 'location' and 'generatedLocation' attributes.
+ */
+export function getMappedScopes(scopes, locations) {
+ return async function (thunkArgs) {
+ const { getState, dispatch } = thunkArgs;
+ const generatedSource = locations.generatedLocation.source;
+ const source = locations.location.source;
+
+ if (
+ !isMapScopesEnabled(getState()) ||
+ !source ||
+ !generatedSource ||
+ generatedSource.isWasm ||
+ source.isPrettyPrinted ||
+ !source.isOriginal
+ ) {
+ return null;
+ }
+
+ // Load source text for the original source
+ await dispatch(loadOriginalSourceText(source));
+
+ const generatedSourceActor = getFirstSourceActorForGeneratedSource(
+ getState(),
+ generatedSource.id
+ );
+
+ // Also load source text for its corresponding generated source
+ await dispatch(loadGeneratedSourceText(generatedSourceActor));
+
+ try {
+ const content =
+ // load original source text content
+ getSettledSourceTextContent(getState(), locations.location);
+
+ return await buildMappedScopes(
+ source,
+ content && isFulfilled(content)
+ ? content.value
+ : { type: "text", value: "", contentType: undefined },
+ locations,
+ await scopes,
+ thunkArgs
+ );
+ } catch (e) {
+ log(e);
+ return null;
+ }
+ };
+}
+
+/**
+ * Used to map variables used within conditional and log breakpoints.
+ */
+export function getMappedScopesForLocation(location) {
+ return async function (thunkArgs) {
+ const { dispatch } = thunkArgs;
+ const mappedLocation = await getMappedLocation(location, thunkArgs);
+ return dispatch(getMappedScopes(null, mappedLocation));
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/moz.build b/devtools/client/debugger/src/actions/pause/moz.build
new file mode 100644
index 0000000000..e8f65996ed
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/moz.build
@@ -0,0 +1,26 @@
+# 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(
+ "breakOnNext.js",
+ "commands.js",
+ "continueToHere.js",
+ "expandScopes.js",
+ "fetchFrames.js",
+ "fetchScopes.js",
+ "index.js",
+ "inlinePreview.js",
+ "mapFrames.js",
+ "mapScopes.js",
+ "paused.js",
+ "pauseOnDebuggerStatement.js",
+ "pauseOnExceptions.js",
+ "resetBreakpointsPaneState.js",
+ "resumed.js",
+ "selectFrame.js",
+ "skipPausing.js",
+)
diff --git a/devtools/client/debugger/src/actions/pause/pauseOnDebuggerStatement.js b/devtools/client/debugger/src/actions/pause/pauseOnDebuggerStatement.js
new file mode 100644
index 0000000000..7b2b1d70cb
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/pauseOnDebuggerStatement.js
@@ -0,0 +1,17 @@
+/* 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 { PROMISE } from "../utils/middleware/promise";
+
+export function pauseOnDebuggerStatement(shouldPauseOnDebuggerStatement) {
+ return ({ dispatch, getState, client }) => {
+ return dispatch({
+ type: "PAUSE_ON_DEBUGGER_STATEMENT",
+ shouldPauseOnDebuggerStatement,
+ [PROMISE]: client.pauseOnDebuggerStatement(
+ shouldPauseOnDebuggerStatement
+ ),
+ });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/pauseOnExceptions.js b/devtools/client/debugger/src/actions/pause/pauseOnExceptions.js
new file mode 100644
index 0000000000..e7c04ded61
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/pauseOnExceptions.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 { PROMISE } from "../utils/middleware/promise";
+import { recordEvent } from "../../utils/telemetry";
+
+/**
+ *
+ * @memberof actions/pause
+ * @static
+ */
+export function pauseOnExceptions(
+ shouldPauseOnExceptions,
+ shouldPauseOnCaughtExceptions
+) {
+ return ({ dispatch, getState, client }) => {
+ recordEvent("pause_on_exceptions", {
+ exceptions: shouldPauseOnExceptions,
+ // There's no "n" in the key below (#1463117)
+ ["caught_exceptio"]: shouldPauseOnCaughtExceptions,
+ });
+
+ return dispatch({
+ type: "PAUSE_ON_EXCEPTIONS",
+ shouldPauseOnExceptions,
+ shouldPauseOnCaughtExceptions,
+ [PROMISE]: client.pauseOnExceptions(
+ shouldPauseOnExceptions,
+ shouldPauseOnCaughtExceptions
+ ),
+ });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/paused.js b/devtools/client/debugger/src/actions/pause/paused.js
new file mode 100644
index 0000000000..a7a631c28c
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/paused.js
@@ -0,0 +1,69 @@
+/* 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 {
+ getHiddenBreakpoint,
+ isEvaluatingExpression,
+ getSelectedFrame,
+} from "../../selectors/index";
+
+import { mapFrames, fetchFrames } from "./index";
+import { removeBreakpoint } from "../breakpoints/index";
+import { evaluateExpressions } from "../expressions";
+import { selectLocation } from "../sources/index";
+import { validateSelectedFrame } from "../../utils/context";
+
+import { fetchScopes } from "./fetchScopes";
+
+/**
+ * Debugger has just paused
+ *
+ * @param {object} pauseInfo
+ * See `createPause` method.
+ */
+export function paused(pauseInfo) {
+ return async function ({ dispatch, getState }) {
+ const { thread, frame, why } = pauseInfo;
+
+ dispatch({ type: "PAUSED", thread, why, topFrame: frame });
+
+ // When we use "continue to here" feature we register an "hidden" breakpoint
+ // that should be removed on the next paused, even if we didn't hit it and
+ // paused for any other reason.
+ const hiddenBreakpoint = getHiddenBreakpoint(getState());
+ if (hiddenBreakpoint) {
+ dispatch(removeBreakpoint(hiddenBreakpoint));
+ }
+
+ // The THREAD_STATE's "paused" resource only passes the top level stack frame,
+ // we dispatch the PAUSED action with it so that we can right away
+ // display it and update the UI to be paused.
+ // But we then fetch all the other frames:
+ await dispatch(fetchFrames(thread));
+ // And map them to original source locations.
+ // Note that this will wait for all related original sources to be loaded in the reducers.
+ // So this step may pause for a little while.
+ await dispatch(mapFrames(thread));
+
+ // If we paused on a particular frame, automatically select the related source
+ // and highlight the paused line
+ const selectedFrame = getSelectedFrame(getState(), thread);
+ if (selectedFrame) {
+ await dispatch(selectLocation(selectedFrame.location));
+ // We might have resumed while opening the location.
+ // Prevent further computation if this happens.
+ validateSelectedFrame(getState(), selectedFrame);
+
+ // Fetch the previews for variables visible in the currently selected paused stackframe
+ await dispatch(fetchScopes(selectedFrame));
+
+ // Run after fetching scoping data so that it may make use of the sourcemap
+ // expression mappings for local variables.
+ const atException = why.type == "exception";
+ if (!atException || !isEvaluatingExpression(getState(), thread)) {
+ await dispatch(evaluateExpressions(selectedFrame));
+ }
+ }
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/resetBreakpointsPaneState.js b/devtools/client/debugger/src/actions/pause/resetBreakpointsPaneState.js
new file mode 100644
index 0000000000..a602c58896
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/resetBreakpointsPaneState.js
@@ -0,0 +1,18 @@
+/* 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/>. */
+
+/**
+ * Action for the breakpoints panel while paused.
+ *
+ * @memberof actions/pause
+ * @static
+ */
+export function resetBreakpointsPaneState(thread) {
+ return async ({ dispatch }) => {
+ dispatch({
+ type: "RESET_BREAKPOINTS_PANE_STATE",
+ thread,
+ });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/resumed.js b/devtools/client/debugger/src/actions/pause/resumed.js
new file mode 100644
index 0000000000..47d55f84ca
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/resumed.js
@@ -0,0 +1,31 @@
+/* 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 {
+ isStepping,
+ getPauseReason,
+ getSelectedFrame,
+} from "../../selectors/index";
+import { evaluateExpressions } from "../expressions";
+import { inDebuggerEval } from "../../utils/pause/index";
+
+/**
+ * Debugger has just resumed.
+ */
+export function resumed(thread) {
+ return async ({ dispatch, client, getState }) => {
+ const why = getPauseReason(getState(), thread);
+ const wasPausedInEval = inDebuggerEval(why);
+ const wasStepping = isStepping(getState(), thread);
+
+ dispatch({ type: "RESUME", thread, wasStepping });
+
+ // Avoid updating expression if we are stepping and would re-pause right after,
+ // the expression will be updated on next pause.
+ if (!wasStepping && !wasPausedInEval) {
+ const selectedFrame = getSelectedFrame(getState(), thread);
+ await dispatch(evaluateExpressions(selectedFrame));
+ }
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/selectFrame.js b/devtools/client/debugger/src/actions/pause/selectFrame.js
new file mode 100644
index 0000000000..49ebacffe5
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/selectFrame.js
@@ -0,0 +1,37 @@
+/* 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 { selectLocation } from "../sources/index";
+import { evaluateExpressions } from "../expressions";
+import { fetchScopes } from "./fetchScopes";
+import { validateSelectedFrame } from "../../utils/context";
+
+/**
+ * @memberof actions/pause
+ * @static
+ */
+export function selectFrame(frame) {
+ return async ({ dispatch, getState }) => {
+ // Frames that aren't on-stack do not support evalling and may not
+ // have live inspectable scopes, so we do not allow selecting them.
+ if (frame.state !== "on-stack") {
+ dispatch(selectLocation(frame.location));
+ return;
+ }
+
+ dispatch({
+ type: "SELECT_FRAME",
+ frame,
+ });
+
+ // It's important that we wait for selectLocation to finish because
+ // we rely on the source being loaded and symbols fetched below.
+ await dispatch(selectLocation(frame.location));
+ validateSelectedFrame(getState(), frame);
+
+ await dispatch(evaluateExpressions(frame));
+
+ await dispatch(fetchScopes(frame));
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/skipPausing.js b/devtools/client/debugger/src/actions/pause/skipPausing.js
new file mode 100644
index 0000000000..a9f1550dc1
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/skipPausing.js
@@ -0,0 +1,33 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+import { getSkipPausing } from "../../selectors/index";
+
+/**
+ * @memberof actions/pause
+ * @static
+ */
+export function toggleSkipPausing() {
+ return async ({ dispatch, client, getState }) => {
+ const skipPausing = !getSkipPausing(getState());
+ await client.setSkipPausing(skipPausing);
+ dispatch({ type: "TOGGLE_SKIP_PAUSING", skipPausing });
+ };
+}
+
+/**
+ * @memberof actions/pause
+ * @static
+ */
+export function setSkipPausing(skipPausing) {
+ return async ({ dispatch, client, getState }) => {
+ const currentlySkipping = getSkipPausing(getState());
+ if (currentlySkipping === skipPausing) {
+ return;
+ }
+
+ await client.setSkipPausing(skipPausing);
+ dispatch({ type: "TOGGLE_SKIP_PAUSING", skipPausing });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/tests/__snapshots__/pauseOnExceptions.spec.js.snap b/devtools/client/debugger/src/actions/pause/tests/__snapshots__/pauseOnExceptions.spec.js.snap
new file mode 100644
index 0000000000..55b8d3e724
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/tests/__snapshots__/pauseOnExceptions.spec.js.snap
@@ -0,0 +1,10 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`pauseOnExceptions should track telemetry for pauseOnException changes 1`] = `
+Array [
+ Object {
+ "caught_exceptio": false,
+ "exceptions": true,
+ },
+]
+`;
diff --git a/devtools/client/debugger/src/actions/pause/tests/pause.spec.js b/devtools/client/debugger/src/actions/pause/tests/pause.spec.js
new file mode 100644
index 0000000000..f8bd87375a
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/tests/pause.spec.js
@@ -0,0 +1,290 @@
+/* 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,
+ selectors,
+ createStore,
+ createSourceObject,
+ makeSource,
+ makeOriginalSource,
+ makeFrame,
+} from "../../../utils/test-head";
+
+import { makeWhyNormal } from "../../../utils/test-mockup";
+import { createLocation } from "../../../utils/location";
+
+const mockCommandClient = {
+ stepIn: () => new Promise(),
+ stepOver: () => new Promise(_resolve => _resolve),
+ evaluate: async () => {},
+ evaluateExpressions: async () => [],
+ resume: async () => {},
+ getFrameScopes: async frame => frame.scope,
+ getFrames: async () => [],
+ setBreakpoint: () => new Promise(_resolve => {}),
+ sourceContents: ({ source }) => {
+ return new Promise((resolve, reject) => {
+ switch (source) {
+ case "foo1":
+ return resolve({
+ source: "function foo1() {\n return 5;\n}",
+ contentType: "text/javascript",
+ });
+ case "await":
+ return resolve({
+ source: "async function aWait() {\n await foo(); return 5;\n}",
+ contentType: "text/javascript",
+ });
+
+ case "foo":
+ return resolve({
+ source: "function foo() {\n return -5;\n}",
+ contentType: "text/javascript",
+ });
+ case "foo-original":
+ return resolve({
+ source: "\n\nfunction fooOriginal() {\n return -5;\n}",
+ contentType: "text/javascript",
+ });
+ case "foo-wasm":
+ return resolve({
+ source: { binary: new ArrayBuffer(0) },
+ contentType: "application/wasm",
+ });
+ case "foo-wasm/originalSource":
+ return resolve({
+ source: "fn fooBar() {}\nfn barZoo() { fooBar() }",
+ contentType: "text/rust",
+ });
+ }
+
+ return resolve();
+ });
+ },
+ getSourceActorBreakpointPositions: async () => ({}),
+ getSourceActorBreakableLines: async () => [],
+ actorID: "threadActorID",
+};
+
+const mockFrameId = "1";
+
+function createPauseInfo(
+ frameLocation = createLocation({
+ source: createSourceObject("foo1"),
+ line: 2,
+ }),
+ frameOpts = {}
+) {
+ const frames = [
+ makeFrame(
+ { id: mockFrameId, sourceId: frameLocation.source.id },
+ {
+ location: frameLocation,
+ generatedLocation: frameLocation,
+ ...frameOpts,
+ }
+ ),
+ ];
+ return {
+ thread: "FakeThread",
+ frame: frames[0],
+ frames,
+ loadedObjects: [],
+ why: makeWhyNormal(),
+ };
+}
+
+function debuggerToSourceMapLocation(l) {
+ return {
+ sourceId: l.source.id,
+ line: l.line,
+ column: l.column,
+ };
+}
+
+describe("pause", () => {
+ describe("stepping", () => {
+ it("should only step when paused", async () => {
+ const client = { stepIn: jest.fn() };
+ const { dispatch } = createStore(client);
+
+ dispatch(actions.stepIn());
+ expect(client.stepIn.mock.calls).toHaveLength(0);
+ });
+
+ it("getting frame scopes with bindings", async () => {
+ const client = { ...mockCommandClient };
+ const store = createStore(client, {});
+ const { dispatch, getState } = store;
+
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("foo"))
+ );
+ const generatedLocation = createLocation({
+ source,
+ line: 1,
+ column: 0,
+ sourceActor: selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ source.id
+ ),
+ });
+ const mockPauseInfo = createPauseInfo(generatedLocation, {
+ scope: {
+ bindings: {
+ variables: { b: { value: {} } },
+ arguments: [{ a: { value: {} } }],
+ },
+ },
+ });
+
+ const { frames } = mockPauseInfo;
+ client.getFrames = async () => frames;
+ await dispatch(actions.newOriginalSources([makeOriginalSource(source)]));
+
+ await dispatch(actions.paused(mockPauseInfo));
+ expect(selectors.getFrames(getState(), "FakeThread")).toEqual([
+ {
+ id: mockFrameId,
+ generatedLocation,
+ location: generatedLocation,
+ library: null,
+ scope: {
+ bindings: {
+ arguments: [{ a: { value: {} } }],
+ variables: { b: { value: {} } },
+ },
+ },
+ thread: "FakeThread",
+ },
+ ]);
+
+ expect(selectors.getFrameScopes(getState(), "FakeThread")).toEqual({
+ generated: {
+ 1: {
+ pending: false,
+ scope: {
+ bindings: {
+ arguments: [{ a: { value: {} } }],
+ variables: { b: { value: {} } },
+ },
+ },
+ },
+ },
+ mappings: { 1: undefined },
+ original: { 1: { pending: false, scope: undefined } },
+ });
+
+ expect(
+ selectors.getSelectedFrameBindings(getState(), "FakeThread")
+ ).toEqual(["b", "a"]);
+ });
+
+ it("maps frame to original frames", async () => {
+ const sourceMapLoaderMock = {
+ getOriginalStackFrames: loc => Promise.resolve(originStackFrames),
+ getOriginalLocation: () =>
+ Promise.resolve(debuggerToSourceMapLocation(originalLocation)),
+ getOriginalLocations: async items =>
+ items.map(debuggerToSourceMapLocation),
+ getOriginalSourceText: async () => ({
+ text: "fn fooBar() {}\nfn barZoo() { fooBar() }",
+ contentType: "text/rust",
+ }),
+ getGeneratedRangesForOriginal: async () => [],
+ };
+
+ const client = { ...mockCommandClient };
+ const store = createStore(client, {}, sourceMapLoaderMock);
+ const { dispatch, getState } = store;
+
+ const generatedSource = await dispatch(
+ actions.newGeneratedSource(
+ makeSource("foo-wasm", { introductionType: "wasm" })
+ )
+ );
+
+ const generatedLocation = createLocation({
+ source: generatedSource,
+ line: 1,
+ column: 0,
+ sourceActor: selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ generatedSource.id
+ ),
+ });
+ const mockPauseInfo = createPauseInfo(generatedLocation);
+ const { frames } = mockPauseInfo;
+ client.getFrames = async () => frames;
+
+ const [originalSource] = await dispatch(
+ actions.newOriginalSources([makeOriginalSource(generatedSource)])
+ );
+
+ const originalLocation = createLocation({
+ source: originalSource,
+ line: 1,
+ column: 1,
+ });
+ const originalLocation2 = createLocation({
+ source: originalSource,
+ line: 2,
+ column: 14,
+ sourceActor: selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ originalSource.id
+ ),
+ });
+
+ const originStackFrames = [
+ {
+ displayName: "fooBar",
+ thread: "FakeThread",
+ },
+ {
+ displayName: "barZoo",
+ location: originalLocation2,
+ thread: "FakeThread",
+ },
+ ];
+
+ await dispatch(actions.paused(mockPauseInfo));
+ expect(selectors.getFrames(getState(), "FakeThread")).toEqual([
+ {
+ asyncCause: undefined,
+ displayName: "fooBar",
+ generatedLocation,
+ id: "1",
+ index: undefined,
+ isOriginal: true,
+ library: null,
+ location: originalLocation,
+ originalDisplayName: "fooBar",
+ originalVariables: undefined,
+ state: undefined,
+ this: undefined,
+ thread: "FakeThread",
+ type: undefined,
+ },
+ {
+ asyncCause: undefined,
+ displayName: "barZoo",
+ generatedLocation,
+ id: "1-originalFrame1",
+ index: undefined,
+ isOriginal: true,
+ library: null,
+ location: originalLocation2,
+ originalDisplayName: "barZoo",
+ originalVariables: undefined,
+ state: undefined,
+ this: undefined,
+ thread: "FakeThread",
+ type: undefined,
+ },
+ ]);
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/actions/pause/tests/pauseOnExceptions.spec.js b/devtools/client/debugger/src/actions/pause/tests/pauseOnExceptions.spec.js
new file mode 100644
index 0000000000..bc8d000697
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/tests/pauseOnExceptions.spec.js
@@ -0,0 +1,24 @@
+/* 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,
+ getTelemetryEvents,
+} from "../../../utils/test-head";
+
+import {
+ getShouldPauseOnExceptions,
+ getShouldPauseOnCaughtExceptions,
+} from "../../../selectors/pause";
+
+describe("pauseOnExceptions", () => {
+ it("should track telemetry for pauseOnException changes", async () => {
+ const { dispatch, getState } = createStore({ pauseOnExceptions: () => {} });
+ dispatch(actions.pauseOnExceptions(true, false));
+ expect(getTelemetryEvents("pause_on_exceptions")).toMatchSnapshot();
+ expect(getShouldPauseOnExceptions(getState())).toBe(true);
+ expect(getShouldPauseOnCaughtExceptions(getState())).toBe(false);
+ });
+});
diff --git a/devtools/client/debugger/src/actions/pause/tests/skipPausing.spec.js b/devtools/client/debugger/src/actions/pause/tests/skipPausing.spec.js
new file mode 100644
index 0000000000..83006c3089
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/tests/skipPausing.spec.js
@@ -0,0 +1,18 @@
+/* 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, selectors, createStore } from "../../../utils/test-head";
+
+describe("sources - pretty print", () => {
+ it("returns a pretty source for a minified file", async () => {
+ const client = { setSkipPausing: jest.fn() };
+ const { dispatch, getState } = createStore(client);
+
+ await dispatch(actions.toggleSkipPausing());
+ expect(selectors.getSkipPausing(getState())).toBe(true);
+
+ await dispatch(actions.toggleSkipPausing());
+ expect(selectors.getSkipPausing(getState())).toBe(false);
+ });
+});
diff --git a/devtools/client/debugger/src/actions/preview.js b/devtools/client/debugger/src/actions/preview.js
new file mode 100644
index 0000000000..c3bc8dbffd
--- /dev/null
+++ b/devtools/client/debugger/src/actions/preview.js
@@ -0,0 +1,159 @@
+/* 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 { isConsole } from "../utils/preview";
+import { getGrip, getFront } from "../utils/evaluation-result";
+import { getExpressionFromCoords } from "../utils/editor/get-expression";
+
+import {
+ isLineInScope,
+ isSelectedFrameVisible,
+ getSelectedSource,
+ getSelectedLocation,
+ getSelectedFrame,
+ getCurrentThread,
+ getSelectedException,
+} from "../selectors/index";
+
+import { getMappedExpression } from "./expressions";
+
+async function findExpressionMatch(state, parserWorker, codeMirror, tokenPos) {
+ const location = getSelectedLocation(state);
+ if (!location) {
+ return null;
+ }
+
+ // Fallback on expression from codemirror cursor if parser worker misses symbols
+ // or is unable to find a match.
+ const match = await parserWorker.findBestMatchExpression(
+ location.source.id,
+ tokenPos
+ );
+ if (match) {
+ return match;
+ }
+ return getExpressionFromCoords(codeMirror, tokenPos);
+}
+
+export function getPreview(target, tokenPos, codeMirror) {
+ return async thunkArgs => {
+ const { getState, client, parserWorker } = thunkArgs;
+ if (
+ !isSelectedFrameVisible(getState()) ||
+ !isLineInScope(getState(), tokenPos.line)
+ ) {
+ return null;
+ }
+
+ const source = getSelectedSource(getState());
+ if (!source) {
+ return null;
+ }
+ const thread = getCurrentThread(getState());
+ const selectedFrame = getSelectedFrame(getState(), thread);
+ if (!selectedFrame) {
+ return null;
+ }
+
+ const match = await findExpressionMatch(
+ getState(),
+ parserWorker,
+ codeMirror,
+ tokenPos
+ );
+ if (!match) {
+ return null;
+ }
+
+ let { expression, location } = match;
+
+ if (isConsole(expression)) {
+ return null;
+ }
+
+ if (location && source.isOriginal) {
+ const mapResult = await getMappedExpression(
+ expression,
+ thread,
+ thunkArgs
+ );
+ if (mapResult) {
+ expression = mapResult.expression;
+ }
+ }
+
+ const { result } = await client.evaluate(expression, {
+ frameId: selectedFrame.id,
+ });
+
+ const resultGrip = getGrip(result);
+
+ // Error case occurs for a token that follows an errored evaluation
+ // https://github.com/firefox-devtools/debugger/pull/8056
+ // Accommodating for null allows us to show preview for falsy values
+ // line "", false, null, Nan, and more
+ if (resultGrip === null) {
+ return null;
+ }
+
+ // Handle cases where the result is invisible to the debugger
+ // and not possible to preview. Bug 1548256
+ if (
+ resultGrip &&
+ resultGrip.class &&
+ typeof resultGrip.class === "string" &&
+ resultGrip.class.includes("InvisibleToDebugger")
+ ) {
+ return null;
+ }
+
+ const root = {
+ path: expression,
+ contents: {
+ value: resultGrip,
+ front: getFront(result),
+ },
+ };
+
+ return {
+ target,
+ tokenPos,
+ cursorPos: target.getBoundingClientRect(),
+ expression,
+ root,
+ resultGrip,
+ };
+ };
+}
+
+export function getExceptionPreview(target, tokenPos, codeMirror) {
+ return async ({ dispatch, getState, parserWorker }) => {
+ const match = await findExpressionMatch(
+ getState(),
+ parserWorker,
+ codeMirror,
+ tokenPos
+ );
+ if (!match) {
+ return null;
+ }
+
+ const tokenColumnStart = match.location.start.column + 1;
+ const exception = getSelectedException(
+ getState(),
+ tokenPos.line,
+ tokenColumnStart
+ );
+ if (!exception) {
+ return null;
+ }
+
+ return {
+ target,
+ tokenPos,
+ cursorPos: target.getBoundingClientRect(),
+ exception,
+ };
+ };
+}
diff --git a/devtools/client/debugger/src/actions/project-text-search.js b/devtools/client/debugger/src/actions/project-text-search.js
new file mode 100644
index 0000000000..70a74d560c
--- /dev/null
+++ b/devtools/client/debugger/src/actions/project-text-search.js
@@ -0,0 +1,142 @@
+/* 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/>. */
+
+/**
+ * Redux actions for the search state
+ * @module actions/search
+ */
+
+import { isFulfilled } from "../utils/async-value";
+import {
+ getFirstSourceActorForGeneratedSource,
+ getSourceList,
+ getSettledSourceTextContent,
+ isSourceBlackBoxed,
+ getSearchOptions,
+} from "../selectors/index";
+import { createLocation } from "../utils/location";
+import { matchesGlobPatterns } from "../utils/source";
+import { loadSourceText } from "./sources/loadSourceText";
+import { searchKeys } from "../constants";
+
+export function searchSources(query, onUpdatedResults, signal) {
+ return async ({ dispatch, getState, searchWorker }) => {
+ dispatch({
+ type: "SET_PROJECT_SEARCH_QUERY",
+ query,
+ });
+
+ const searchOptions = getSearchOptions(
+ getState(),
+ searchKeys.PROJECT_SEARCH
+ );
+ const validSources = getSourceList(getState()).filter(
+ source =>
+ !isSourceBlackBoxed(getState(), source) &&
+ !matchesGlobPatterns(source, searchOptions.excludePatterns)
+ );
+ // Sort original entries first so that search results are more useful.
+ // Deprioritize third-party scripts, so their results show last.
+ validSources.sort((a, b) => {
+ function isThirdParty(source) {
+ return (
+ source?.url &&
+ (source.url.includes("node_modules") ||
+ source.url.includes("bower_components"))
+ );
+ }
+
+ if (a.isOriginal && !isThirdParty(a)) {
+ return -1;
+ }
+
+ if (b.isOriginal && !isThirdParty(b)) {
+ return 1;
+ }
+
+ if (!isThirdParty(a) && isThirdParty(b)) {
+ return -1;
+ }
+ if (isThirdParty(a) && !isThirdParty(b)) {
+ return 1;
+ }
+ return 0;
+ });
+ const results = [];
+ for (const source of validSources) {
+ const sourceActor = getFirstSourceActorForGeneratedSource(
+ getState(),
+ source.id
+ );
+ await dispatch(loadSourceText(source, sourceActor));
+
+ // This is the only asynchronous call in this method.
+ // We may have stopped the search by closing the search panel or changing the query.
+ // Avoid any further unecessary computation when the React Component tells us the query was cancelled.
+ if (signal.aborted) {
+ return;
+ }
+
+ const result = await searchSource(source, sourceActor, query, {
+ getState,
+ searchWorker,
+ });
+ if (signal.aborted) {
+ return;
+ }
+
+ if (result) {
+ results.push(result);
+ onUpdatedResults(results, false, signal);
+ }
+ }
+ onUpdatedResults(results, true, signal);
+ };
+}
+
+export async function searchSource(
+ source,
+ sourceActor,
+ query,
+ { getState, searchWorker }
+) {
+ const state = getState();
+ const location = createLocation({
+ source,
+ sourceActor,
+ });
+
+ const content = getSettledSourceTextContent(state, location);
+ let matches = [];
+
+ if (content && isFulfilled(content) && content.value.type === "text") {
+ const options = getSearchOptions(state, searchKeys.PROJECT_SEARCH);
+ matches = await searchWorker.findSourceMatches(
+ content.value,
+ query,
+ options
+ );
+ }
+ if (!matches.length) {
+ return null;
+ }
+ return {
+ type: "RESULT",
+ location,
+ // `matches` are generated by project-search worker's `findSourceMatches` method
+ matches: matches.map(m => ({
+ type: "MATCH",
+ location: createLocation({
+ ...location,
+ // `matches` only contain line and column
+ // `location` will already refer to the right source/sourceActor
+ line: m.line,
+ column: m.column,
+ }),
+ matchIndex: m.matchIndex,
+ match: m.match,
+ value: m.value,
+ })),
+ };
+}
diff --git a/devtools/client/debugger/src/actions/quick-open.js b/devtools/client/debugger/src/actions/quick-open.js
new file mode 100644
index 0000000000..e5f5352292
--- /dev/null
+++ b/devtools/client/debugger/src/actions/quick-open.js
@@ -0,0 +1,21 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+export function setQuickOpenQuery(query) {
+ return {
+ type: "SET_QUICK_OPEN_QUERY",
+ query,
+ };
+}
+
+export function openQuickOpen(query) {
+ if (query != null) {
+ return { type: "OPEN_QUICK_OPEN", query };
+ }
+ return { type: "OPEN_QUICK_OPEN" };
+}
+
+export function closeQuickOpen() {
+ return { type: "CLOSE_QUICK_OPEN" };
+}
diff --git a/devtools/client/debugger/src/actions/source-actors.js b/devtools/client/debugger/src/actions/source-actors.js
new file mode 100644
index 0000000000..9782e493b3
--- /dev/null
+++ b/devtools/client/debugger/src/actions/source-actors.js
@@ -0,0 +1,12 @@
+/* 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 insertSourceActors(sourceActors) {
+ return function ({ dispatch }) {
+ dispatch({
+ type: "INSERT_SOURCE_ACTORS",
+ sourceActors,
+ });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/sources-tree.js b/devtools/client/debugger/src/actions/sources-tree.js
new file mode 100644
index 0000000000..9d54c08ac9
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources-tree.js
@@ -0,0 +1,43 @@
+/* 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 { getMainThread } from "../selectors/index";
+
+export function setExpandedState(expanded) {
+ return { type: "SET_EXPANDED_STATE", expanded };
+}
+
+export function focusItem(item) {
+ return { type: "SET_FOCUSED_SOURCE_ITEM", item };
+}
+
+export function setProjectDirectoryRoot(newRootItemUniquePath, newName) {
+ return ({ dispatch, getState }) => {
+ // If the new project root is against the top level thread,
+ // replace its thread ID with "top-level", so that later,
+ // getDirectoryForUniquePath could match the project root,
+ // even after a page reload where the new top level thread actor ID
+ // will be different.
+ const mainThread = getMainThread(getState());
+ if (mainThread && newRootItemUniquePath.startsWith(mainThread.actor)) {
+ newRootItemUniquePath = newRootItemUniquePath.replace(
+ mainThread.actor,
+ "top-level"
+ );
+ }
+ dispatch({
+ type: "SET_PROJECT_DIRECTORY_ROOT",
+ uniquePath: newRootItemUniquePath,
+ name: newName,
+ });
+ };
+}
+
+export function clearProjectDirectoryRoot() {
+ return {
+ type: "SET_PROJECT_DIRECTORY_ROOT",
+ uniquePath: "",
+ name: "",
+ };
+}
diff --git a/devtools/client/debugger/src/actions/sources/blackbox.js b/devtools/client/debugger/src/actions/sources/blackbox.js
new file mode 100644
index 0000000000..3cf4df2a70
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources/blackbox.js
@@ -0,0 +1,211 @@
+/* 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/>. */
+
+/**
+ * Redux actions for the sources state
+ * @module actions/sources
+ */
+
+import { originalToGeneratedId } from "devtools/client/shared/source-map-loader/index";
+import { recordEvent } from "../../utils/telemetry";
+import { toggleBreakpoints } from "../breakpoints/index";
+import {
+ getSourceActorsForSource,
+ isSourceBlackBoxed,
+ getBlackBoxRanges,
+ getBreakpointsForSource,
+} from "../../selectors/index";
+
+export async function blackboxSourceActorsForSource(
+ thunkArgs,
+ source,
+ shouldBlackBox,
+ ranges = []
+) {
+ const { getState, client, sourceMapLoader } = thunkArgs;
+ let sourceId = source.id;
+ // If the source is the original, then get the source id of its generated file
+ // and the range for where the original is represented in the generated file
+ // (which might be a bundle including other files).
+ if (source.isOriginal) {
+ sourceId = originalToGeneratedId(source.id);
+ const range = await sourceMapLoader.getFileGeneratedRange(source.id);
+ ranges = [];
+ if (range) {
+ ranges.push(range);
+ // TODO bug 1752108: Investigate blackboxing lines in original files,
+ // there is likely to be issues as the whole genrated file
+ // representing the original file will always be blackboxed.
+ console.warn(
+ "The might be unxpected issues when ignoring lines in an original file. " +
+ "The whole original source is being blackboxed."
+ );
+ } else {
+ throw new Error(
+ `Unable to retrieve generated ranges for original source ${source.url}`
+ );
+ }
+ }
+
+ for (const actor of getSourceActorsForSource(getState(), sourceId)) {
+ await client.blackBox(actor, shouldBlackBox, ranges);
+ }
+}
+
+/**
+ * Toggle blackboxing for the whole source or for specific lines in a source
+ *
+ * @param {Object} source - The source to be blackboxed/unblackboxed.
+ * @param {Boolean} [shouldBlackBox] - Specifies if the source should be blackboxed (true
+ * or unblackboxed (false). When this is not provided
+ * option is decided based on the blackboxed state
+ * of the source.
+ * @param {Array} [ranges] - List of line/column offsets to blackbox, these
+ * are provided only when blackboxing lines.
+ * The range structure:
+ * const range = {
+ * start: { line: 1, column: 5 },
+ * end: { line: 3, column: 4 },
+ * }
+ */
+export function toggleBlackBox(source, shouldBlackBox, ranges = []) {
+ return async thunkArgs => {
+ const { dispatch, getState } = thunkArgs;
+
+ shouldBlackBox =
+ typeof shouldBlackBox == "boolean"
+ ? shouldBlackBox
+ : !isSourceBlackBoxed(getState(), source);
+
+ await blackboxSourceActorsForSource(
+ thunkArgs,
+ source,
+ shouldBlackBox,
+ ranges
+ );
+
+ if (shouldBlackBox) {
+ recordEvent("blackbox");
+ // If ranges is an empty array, it would mean we are blackboxing the whole
+ // source. To do that lets reset the content to an empty array.
+ if (!ranges.length) {
+ dispatch({ type: "BLACKBOX_WHOLE_SOURCES", sources: [source] });
+ await toggleBreakpointsInBlackboxedSources({
+ thunkArgs,
+ shouldDisable: true,
+ sources: [source],
+ });
+ } else {
+ const currentRanges = getBlackBoxRanges(getState())[source.url] || [];
+ ranges = ranges.filter(newRange => {
+ // To avoid adding duplicate ranges make sure
+ // no range already exists with same start and end lines.
+ const duplicate = currentRanges.findIndex(
+ r =>
+ r.start.line == newRange.start.line &&
+ r.end.line == newRange.end.line
+ );
+ return duplicate == -1;
+ });
+ dispatch({ type: "BLACKBOX_SOURCE_RANGES", source, ranges });
+ await toggleBreakpointsInRangesForBlackboxedSource({
+ thunkArgs,
+ shouldDisable: true,
+ source,
+ ranges,
+ });
+ }
+ } else {
+ // if there are no ranges to blackbox, then we are unblackboxing
+ // the whole source
+ // eslint-disable-next-line no-lonely-if
+ if (!ranges.length) {
+ dispatch({ type: "UNBLACKBOX_WHOLE_SOURCES", sources: [source] });
+ toggleBreakpointsInBlackboxedSources({
+ thunkArgs,
+ shouldDisable: false,
+ sources: [source],
+ });
+ } else {
+ dispatch({ type: "UNBLACKBOX_SOURCE_RANGES", source, ranges });
+ const blackboxRanges = getBlackBoxRanges(getState());
+ if (!blackboxRanges[source.url].length) {
+ dispatch({ type: "UNBLACKBOX_WHOLE_SOURCES", sources: [source] });
+ }
+ await toggleBreakpointsInRangesForBlackboxedSource({
+ thunkArgs,
+ shouldDisable: false,
+ source,
+ ranges,
+ });
+ }
+ }
+ };
+}
+
+async function toggleBreakpointsInRangesForBlackboxedSource({
+ thunkArgs,
+ shouldDisable,
+ source,
+ ranges,
+}) {
+ const { dispatch, getState } = thunkArgs;
+ for (const range of ranges) {
+ const breakpoints = getBreakpointsForSource(getState(), source, range);
+ await dispatch(toggleBreakpoints(shouldDisable, breakpoints));
+ }
+}
+
+async function toggleBreakpointsInBlackboxedSources({
+ thunkArgs,
+ shouldDisable,
+ sources,
+}) {
+ const { dispatch, getState } = thunkArgs;
+ for (const source of sources) {
+ const breakpoints = getBreakpointsForSource(getState(), source);
+ await dispatch(toggleBreakpoints(shouldDisable, breakpoints));
+ }
+}
+
+/*
+ * Blackboxes a group of sources together
+ *
+ * @param {Array} sourcesToBlackBox - The list of sources to blackbox
+ * @param {Boolean} shouldBlackbox - Specifies if the sources should blackboxed (true)
+ * or unblackboxed (false).
+ */
+export function blackBoxSources(sourcesToBlackBox, shouldBlackBox) {
+ return async thunkArgs => {
+ const { dispatch, getState } = thunkArgs;
+
+ const sources = sourcesToBlackBox.filter(
+ source => isSourceBlackBoxed(getState(), source) !== shouldBlackBox
+ );
+
+ if (!sources.length) {
+ return;
+ }
+
+ for (const source of sources) {
+ await blackboxSourceActorsForSource(thunkArgs, source, shouldBlackBox);
+ }
+
+ if (shouldBlackBox) {
+ recordEvent("blackbox");
+ }
+
+ dispatch({
+ type: shouldBlackBox
+ ? "BLACKBOX_WHOLE_SOURCES"
+ : "UNBLACKBOX_WHOLE_SOURCES",
+ sources,
+ });
+ await toggleBreakpointsInBlackboxedSources({
+ thunkArgs,
+ shouldDisable: shouldBlackBox,
+ sources,
+ });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/sources/breakableLines.js b/devtools/client/debugger/src/actions/sources/breakableLines.js
new file mode 100644
index 0000000000..98d0b49a37
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources/breakableLines.js
@@ -0,0 +1,68 @@
+/* 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 {
+ getBreakableLines,
+ getSourceActorBreakableLines,
+} from "../../selectors/index";
+import { setBreakpointPositions } from "../breakpoints/breakpointPositions";
+
+function calculateBreakableLines(positions) {
+ const lines = [];
+ for (const line in positions) {
+ if (positions[line].length) {
+ lines.push(Number(line));
+ }
+ }
+
+ return lines;
+}
+
+/**
+ * Ensure that breakable lines for a given source are fetched.
+ *
+ * @param Object location
+ */
+export function setBreakableLines(location) {
+ return async ({ getState, dispatch, client }) => {
+ let breakableLines;
+ if (location.source.isOriginal) {
+ const positions = await dispatch(setBreakpointPositions(location));
+ breakableLines = calculateBreakableLines(positions);
+
+ const existingBreakableLines = getBreakableLines(
+ getState(),
+ location.source.id
+ );
+ if (existingBreakableLines) {
+ breakableLines = [
+ ...new Set([...existingBreakableLines, ...breakableLines]),
+ ];
+ }
+
+ dispatch({
+ type: "SET_ORIGINAL_BREAKABLE_LINES",
+ source: location.source,
+ breakableLines,
+ });
+ } else {
+ // Ignore re-fetching the breakable lines for source actor we already fetched
+ breakableLines = getSourceActorBreakableLines(
+ getState(),
+ location.sourceActor.id
+ );
+ if (breakableLines) {
+ return;
+ }
+ breakableLines = await client.getSourceActorBreakableLines(
+ location.sourceActor
+ );
+ dispatch({
+ type: "SET_SOURCE_ACTOR_BREAKABLE_LINES",
+ sourceActor: location.sourceActor,
+ breakableLines,
+ });
+ }
+ };
+}
diff --git a/devtools/client/debugger/src/actions/sources/index.js b/devtools/client/debugger/src/actions/sources/index.js
new file mode 100644
index 0000000000..9e2041dd31
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources/index.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/>. */
+
+export * from "./blackbox";
+export * from "./breakableLines";
+export * from "./loadSourceText";
+export * from "./newSources";
+export * from "./prettyPrint";
+export * from "./select";
+export * from "./symbols";
+
+export function setOverrideSource(source, path) {
+ return ({ client, dispatch }) => {
+ if (!source || !source.url) {
+ return;
+ }
+ const { url } = source;
+ client.setOverride(url, path);
+ dispatch({
+ type: "SET_OVERRIDE",
+ url,
+ path,
+ });
+ };
+}
+
+export function removeOverrideSource(source) {
+ return ({ client, dispatch }) => {
+ if (!source || !source.url) {
+ return;
+ }
+ const { url } = source;
+ client.removeOverride(url);
+ dispatch({
+ type: "REMOVE_OVERRIDE",
+ url,
+ });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/sources/loadSourceText.js b/devtools/client/debugger/src/actions/sources/loadSourceText.js
new file mode 100644
index 0000000000..d3bbd53871
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources/loadSourceText.js
@@ -0,0 +1,252 @@
+/* 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 { PROMISE } from "../utils/middleware/promise";
+import {
+ getSourceTextContent,
+ getSettledSourceTextContent,
+ getGeneratedSource,
+ getSourcesEpoch,
+ getBreakpointsForSource,
+ getSourceActorsForSource,
+ getFirstSourceActorForGeneratedSource,
+} from "../../selectors/index";
+import { addBreakpoint } from "../breakpoints/index";
+
+import { prettyPrintSourceTextContent } from "./prettyPrint";
+import { isFulfilled, fulfilled } from "../../utils/async-value";
+
+import { isPretty } from "../../utils/source";
+import { createLocation } from "../../utils/location";
+import { memoizeableAction } from "../../utils/memoizableAction";
+
+async function loadGeneratedSource(sourceActor, { client }) {
+ // If no source actor can be found then the text for the
+ // source cannot be loaded.
+ if (!sourceActor) {
+ throw new Error("Source actor is null or not defined");
+ }
+
+ let response;
+ try {
+ response = await client.sourceContents(sourceActor);
+ } catch (e) {
+ throw new Error(`sourceContents failed: ${e}`);
+ }
+
+ return {
+ text: response.source,
+ contentType: response.contentType || "text/javascript",
+ };
+}
+
+async function loadOriginalSource(
+ source,
+ { getState, client, sourceMapLoader, prettyPrintWorker }
+) {
+ if (isPretty(source)) {
+ const generatedSource = getGeneratedSource(getState(), source);
+ if (!generatedSource) {
+ throw new Error("Unable to find minified original.");
+ }
+
+ const content = getSettledSourceTextContent(
+ getState(),
+ createLocation({
+ source: generatedSource,
+ })
+ );
+
+ return prettyPrintSourceTextContent(
+ sourceMapLoader,
+ prettyPrintWorker,
+ generatedSource,
+ content,
+ getSourceActorsForSource(getState(), generatedSource.id)
+ );
+ }
+
+ const result = await sourceMapLoader.getOriginalSourceText(source.id);
+ if (!result) {
+ // The way we currently try to load and select a pending
+ // selected location, it is possible that we will try to fetch the
+ // original source text right after the source map has been cleared
+ // after a navigation event.
+ throw new Error("Original source text unavailable");
+ }
+ return result;
+}
+
+async function loadGeneratedSourceTextPromise(sourceActor, thunkArgs) {
+ const { dispatch, getState } = thunkArgs;
+ const epoch = getSourcesEpoch(getState());
+
+ await dispatch({
+ type: "LOAD_GENERATED_SOURCE_TEXT",
+ sourceActor,
+ epoch,
+ [PROMISE]: loadGeneratedSource(sourceActor, thunkArgs),
+ });
+
+ await onSourceTextContentAvailable(
+ sourceActor.sourceObject,
+ sourceActor,
+ thunkArgs
+ );
+}
+
+async function loadOriginalSourceTextPromise(source, thunkArgs) {
+ const { dispatch, getState } = thunkArgs;
+ const epoch = getSourcesEpoch(getState());
+ await dispatch({
+ type: "LOAD_ORIGINAL_SOURCE_TEXT",
+ source,
+ epoch,
+ [PROMISE]: loadOriginalSource(source, thunkArgs),
+ });
+
+ await onSourceTextContentAvailable(source, null, thunkArgs);
+}
+
+/**
+ * Function called everytime a new original or generated source gets its text content
+ * fetched from the server and registered in the reducer.
+ *
+ * @param {Object} source
+ * @param {Object} sourceActor (optional)
+ * If this is a generated source, we expect a precise source actor.
+ * @param {Object} thunkArgs
+ */
+async function onSourceTextContentAvailable(
+ source,
+ sourceActor,
+ { dispatch, getState, parserWorker }
+) {
+ const location = createLocation({
+ source,
+ sourceActor,
+ });
+ const content = getSettledSourceTextContent(getState(), location);
+ if (!content) {
+ return;
+ }
+
+ if (parserWorker.isLocationSupported(location)) {
+ parserWorker.setSource(
+ source.id,
+ isFulfilled(content)
+ ? content.value
+ : { type: "text", value: "", contentType: undefined }
+ );
+ }
+
+ // Update the text in any breakpoints for this source by re-adding them.
+ const breakpoints = getBreakpointsForSource(getState(), source);
+ for (const breakpoint of breakpoints) {
+ await dispatch(
+ addBreakpoint(
+ breakpoint.location,
+ breakpoint.options,
+ breakpoint.disabled
+ )
+ );
+ }
+}
+
+/**
+ * Loads the source text for the generated source based of the source actor
+ * @param {Object} sourceActor
+ * There can be more than one source actor per source
+ * so the source actor needs to be specified. This is
+ * required for generated sources but will be null for
+ * original/pretty printed sources.
+ */
+export const loadGeneratedSourceText = memoizeableAction(
+ "loadGeneratedSourceText",
+ {
+ getValue: (sourceActor, { getState }) => {
+ if (!sourceActor) {
+ return null;
+ }
+
+ const sourceTextContent = getSourceTextContent(
+ getState(),
+ createLocation({
+ source: sourceActor.sourceObject,
+ sourceActor,
+ })
+ );
+
+ if (!sourceTextContent || sourceTextContent.state === "pending") {
+ return sourceTextContent;
+ }
+
+ // This currently swallows source-load-failure since we return fulfilled
+ // here when content.state === "rejected". In an ideal world we should
+ // propagate that error upward.
+ return fulfilled(sourceTextContent);
+ },
+ createKey: (sourceActor, { getState }) => {
+ const epoch = getSourcesEpoch(getState());
+ return `${epoch}:${sourceActor.actor}`;
+ },
+ action: (sourceActor, thunkArgs) =>
+ loadGeneratedSourceTextPromise(sourceActor, thunkArgs),
+ }
+);
+
+/**
+ * Loads the source text for an original source and source actor
+ * @param {Object} source
+ * The original source to load the source text
+ */
+export const loadOriginalSourceText = memoizeableAction(
+ "loadOriginalSourceText",
+ {
+ getValue: (source, { getState }) => {
+ if (!source) {
+ return null;
+ }
+
+ const sourceTextContent = getSourceTextContent(
+ getState(),
+ createLocation({
+ source,
+ })
+ );
+ if (!sourceTextContent || sourceTextContent.state === "pending") {
+ return sourceTextContent;
+ }
+
+ // This currently swallows source-load-failure since we return fulfilled
+ // here when content.state === "rejected". In an ideal world we should
+ // propagate that error upward.
+ return fulfilled(sourceTextContent);
+ },
+ createKey: (source, { getState }) => {
+ const epoch = getSourcesEpoch(getState());
+ return `${epoch}:${source.id}`;
+ },
+ action: (source, thunkArgs) =>
+ loadOriginalSourceTextPromise(source, thunkArgs),
+ }
+);
+
+export function loadSourceText(source, sourceActor) {
+ return async ({ dispatch, getState }) => {
+ if (!source) {
+ return null;
+ }
+ if (source.isOriginal) {
+ return dispatch(loadOriginalSourceText(source));
+ }
+ if (!sourceActor) {
+ sourceActor = getFirstSourceActorForGeneratedSource(
+ getState(),
+ source.id
+ );
+ }
+ return dispatch(loadGeneratedSourceText(sourceActor));
+ };
+}
diff --git a/devtools/client/debugger/src/actions/sources/moz.build b/devtools/client/debugger/src/actions/sources/moz.build
new file mode 100644
index 0000000000..9972e9f09b
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources/moz.build
@@ -0,0 +1,17 @@
+# 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(
+ "blackbox.js",
+ "breakableLines.js",
+ "index.js",
+ "loadSourceText.js",
+ "newSources.js",
+ "prettyPrint.js",
+ "select.js",
+ "symbols.js",
+)
diff --git a/devtools/client/debugger/src/actions/sources/newSources.js b/devtools/client/debugger/src/actions/sources/newSources.js
new file mode 100644
index 0000000000..4d9c2cd5f7
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources/newSources.js
@@ -0,0 +1,382 @@
+/* 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/>. */
+
+/**
+ * Redux actions for the sources state
+ * @module actions/sources
+ */
+import { insertSourceActors } from "../../actions/source-actors";
+import {
+ makeSourceId,
+ createGeneratedSource,
+ createSourceMapOriginalSource,
+ createSourceActor,
+} from "../../client/firefox/create";
+import { toggleBlackBox } from "./blackbox";
+import { syncPendingBreakpoint } from "../breakpoints/index";
+import { loadSourceText } from "./loadSourceText";
+import { prettyPrintAndSelectSource } from "./prettyPrint";
+import { toggleSourceMapIgnoreList } from "../ui";
+import { selectLocation, setBreakableLines } from "../sources/index";
+
+import { getRawSourceURL, isPrettyURL } from "../../utils/source";
+import { createLocation } from "../../utils/location";
+import {
+ getBlackBoxRanges,
+ getSource,
+ getSourceFromId,
+ hasSourceActor,
+ getSourceByActorId,
+ getPendingSelectedLocation,
+ getPendingBreakpointsForSource,
+} from "../../selectors/index";
+
+import { prefs } from "../../utils/prefs";
+import sourceQueue from "../../utils/source-queue";
+import { validateSourceActor, ContextError } from "../../utils/context";
+
+function loadSourceMaps(sources) {
+ return async function ({ dispatch }) {
+ try {
+ const sourceList = await Promise.all(
+ sources.map(async sourceActor => {
+ const originalSourcesInfo = await dispatch(
+ loadSourceMap(sourceActor)
+ );
+ originalSourcesInfo.forEach(
+ sourcesInfo => (sourcesInfo.sourceActor = sourceActor)
+ );
+ sourceQueue.queueOriginalSources(originalSourcesInfo);
+ return originalSourcesInfo;
+ })
+ );
+
+ await sourceQueue.flush();
+ return sourceList.flat();
+ } catch (error) {
+ if (!(error instanceof ContextError)) {
+ throw error;
+ }
+ }
+ return [];
+ };
+}
+
+/**
+ * @memberof actions/sources
+ * @static
+ */
+function loadSourceMap(sourceActor) {
+ return async function ({ dispatch, getState, sourceMapLoader, panel }) {
+ if (!prefs.clientSourceMapsEnabled || !sourceActor.sourceMapURL) {
+ return [];
+ }
+
+ let sources, ignoreListUrls, resolvedSourceMapURL, exception;
+ try {
+ // Ignore sourceMapURL on scripts that are part of HTML files, since
+ // we currently treat sourcemaps as Source-wide, not SourceActor-specific.
+ const source = getSourceByActorId(getState(), sourceActor.id);
+ if (source) {
+ ({ sources, ignoreListUrls, resolvedSourceMapURL, exception } =
+ await sourceMapLoader.loadSourceMap({
+ // Using source ID here is historical and eventually we'll want to
+ // switch to all of this being per-source-actor.
+ id: source.id,
+ url: sourceActor.url || "",
+ sourceMapBaseURL: sourceActor.sourceMapBaseURL || "",
+ sourceMapURL: sourceActor.sourceMapURL || "",
+ isWasm: sourceActor.introductionType === "wasm",
+ }));
+ }
+ } catch (e) {
+ exception = `Internal error: ${e.message}`;
+ }
+
+ if (resolvedSourceMapURL) {
+ dispatch({
+ type: "RESOLVED_SOURCEMAP_URL",
+ sourceActor,
+ resolvedSourceMapURL,
+ });
+ }
+
+ if (ignoreListUrls?.length) {
+ dispatch({
+ type: "ADD_SOURCEMAP_IGNORE_LIST_SOURCES",
+ ignoreListUrls,
+ });
+ }
+
+ if (exception) {
+ // Catch all errors and log them to the Web Console for users to see.
+ const message = L10N.getFormatStr(
+ "toolbox.sourceMapFailure",
+ exception,
+ sourceActor.url,
+ sourceActor.sourceMapURL
+ );
+ panel.toolbox.commands.targetCommand.targetFront.logWarningInPage(
+ message,
+ "source map",
+ resolvedSourceMapURL
+ );
+
+ dispatch({
+ type: "SOURCE_MAP_ERROR",
+ sourceActor,
+ errorMessage: exception,
+ });
+
+ // If this source doesn't have a sourcemap or there are no original files
+ // existing, enable it for pretty printing
+ dispatch({
+ type: "CLEAR_SOURCE_ACTOR_MAP_URL",
+ sourceActor,
+ });
+ return [];
+ }
+
+ // Before dispatching this action, ensure that the related sourceActor is still registered
+ validateSourceActor(getState(), sourceActor);
+ return sources;
+ };
+}
+
+// If a request has been made to show this source, go ahead and
+// select it.
+function checkSelectedSource(sourceId) {
+ return async ({ dispatch, getState }) => {
+ const state = getState();
+ const pendingLocation = getPendingSelectedLocation(state);
+
+ if (!pendingLocation || !pendingLocation.url) {
+ return;
+ }
+
+ const source = getSource(state, sourceId);
+
+ if (!source || !source.url) {
+ return;
+ }
+
+ const pendingUrl = pendingLocation.url;
+ const rawPendingUrl = getRawSourceURL(pendingUrl);
+
+ if (rawPendingUrl === source.url) {
+ if (isPrettyURL(pendingUrl)) {
+ const prettySource = await dispatch(prettyPrintAndSelectSource(source));
+ dispatch(checkPendingBreakpoints(prettySource, null));
+ return;
+ }
+
+ await dispatch(
+ selectLocation(
+ createLocation({
+ source,
+ line:
+ typeof pendingLocation.line === "number"
+ ? pendingLocation.line
+ : 0,
+ column: pendingLocation.column,
+ })
+ )
+ );
+ }
+ };
+}
+
+function checkPendingBreakpoints(source, sourceActor) {
+ return async ({ dispatch, getState }) => {
+ const pendingBreakpoints = getPendingBreakpointsForSource(
+ getState(),
+ source
+ );
+
+ if (pendingBreakpoints.length === 0) {
+ return;
+ }
+
+ // load the source text if there is a pending breakpoint for it
+ await dispatch(loadSourceText(source, sourceActor));
+ await dispatch(setBreakableLines(createLocation({ source, sourceActor })));
+
+ await Promise.all(
+ pendingBreakpoints.map(pendingBp => {
+ return dispatch(syncPendingBreakpoint(source, pendingBp));
+ })
+ );
+ };
+}
+
+function restoreBlackBoxedSources(sources) {
+ return async ({ dispatch, getState }) => {
+ const currentRanges = getBlackBoxRanges(getState());
+
+ if (!Object.keys(currentRanges).length) {
+ return;
+ }
+
+ for (const source of sources) {
+ const ranges = currentRanges[source.url];
+ if (ranges) {
+ // If the ranges is an empty then the whole source was blackboxed.
+ await dispatch(toggleBlackBox(source, true, ranges));
+ }
+ }
+
+ if (prefs.sourceMapIgnoreListEnabled) {
+ await dispatch(toggleSourceMapIgnoreList(true));
+ }
+ };
+}
+
+export function newOriginalSources(originalSourcesInfo) {
+ return async ({ dispatch, getState }) => {
+ const state = getState();
+ const seen = new Set();
+
+ const actors = [];
+ const actorsSources = {};
+
+ for (const { id, url, sourceActor } of originalSourcesInfo) {
+ if (seen.has(id) || getSource(state, id)) {
+ continue;
+ }
+ seen.add(id);
+
+ if (!actorsSources[sourceActor.actor]) {
+ actors.push(sourceActor);
+ actorsSources[sourceActor.actor] = [];
+ }
+
+ actorsSources[sourceActor.actor].push(
+ createSourceMapOriginalSource(id, url)
+ );
+ }
+
+ // Add the original sources per the generated source actors that
+ // they are primarily from.
+ actors.forEach(sourceActor => {
+ dispatch({
+ type: "ADD_ORIGINAL_SOURCES",
+ originalSources: actorsSources[sourceActor.actor],
+ generatedSourceActor: sourceActor,
+ });
+ });
+
+ // Accumulate the sources back into one list
+ const actorsSourcesValues = Object.values(actorsSources);
+ let sources = [];
+ if (actorsSourcesValues.length) {
+ sources = actorsSourcesValues.reduce((acc, sourceList) =>
+ acc.concat(sourceList)
+ );
+ }
+
+ await dispatch(checkNewSources(sources));
+
+ for (const source of sources) {
+ dispatch(checkPendingBreakpoints(source, null));
+ }
+
+ return sources;
+ };
+}
+
+// Wrapper around newGeneratedSources, only used by tests
+export function newGeneratedSource(sourceInfo) {
+ return async ({ dispatch }) => {
+ const sources = await dispatch(newGeneratedSources([sourceInfo]));
+ return sources[0];
+ };
+}
+
+export function newGeneratedSources(sourceResources) {
+ return async ({ dispatch, getState, client }) => {
+ if (!sourceResources.length) {
+ return [];
+ }
+
+ const resultIds = [];
+ const newSourcesObj = {};
+ const newSourceActors = [];
+
+ for (const sourceResource of sourceResources) {
+ // By the time we process the sources, the related target
+ // might already have been destroyed. It means that the sources
+ // are also about to be destroyed, so ignore them.
+ // (This is covered by browser_toolbox_backward_forward_navigation.js)
+ if (sourceResource.targetFront.isDestroyed()) {
+ continue;
+ }
+ const id = makeSourceId(sourceResource);
+
+ if (!getSource(getState(), id) && !newSourcesObj[id]) {
+ newSourcesObj[id] = createGeneratedSource(sourceResource);
+ }
+
+ const actorId = sourceResource.actor;
+
+ // We are sometimes notified about a new source multiple times if we
+ // request a new source list and also get a source event from the server.
+ if (!hasSourceActor(getState(), actorId)) {
+ newSourceActors.push(
+ createSourceActor(
+ sourceResource,
+ getSource(getState(), id) || newSourcesObj[id]
+ )
+ );
+ }
+
+ resultIds.push(id);
+ }
+
+ const newSources = Object.values(newSourcesObj);
+
+ dispatch({ type: "ADD_SOURCES", sources: newSources });
+ dispatch(insertSourceActors(newSourceActors));
+
+ await dispatch(checkNewSources(newSources));
+
+ (async () => {
+ await dispatch(loadSourceMaps(newSourceActors));
+
+ // We would like to sync breakpoints after we are done
+ // loading source maps as sometimes generated and original
+ // files share the same paths.
+ for (const sourceActor of newSourceActors) {
+ // For HTML pages, we fetch all new incoming inline script,
+ // which will be related to one dedicated source actor.
+ // Whereas, for regular sources, if we have many source actors,
+ // this is for the same URL. And code expecting to have breakable lines
+ // will request breakable lines for that particular source actor.
+ if (sourceActor.sourceObject.isHTML) {
+ await dispatch(
+ setBreakableLines(
+ createLocation({ source: sourceActor.sourceObject, sourceActor })
+ )
+ );
+ }
+ dispatch(
+ checkPendingBreakpoints(sourceActor.sourceObject, sourceActor)
+ );
+ }
+ })();
+
+ return resultIds.map(id => getSourceFromId(getState(), id));
+ };
+}
+
+function checkNewSources(sources) {
+ return async ({ dispatch, getState }) => {
+ for (const source of sources) {
+ dispatch(checkSelectedSource(source.id));
+ }
+
+ await dispatch(restoreBlackBoxedSources(sources));
+
+ return sources;
+ };
+}
diff --git a/devtools/client/debugger/src/actions/sources/prettyPrint.js b/devtools/client/debugger/src/actions/sources/prettyPrint.js
new file mode 100644
index 0000000000..6a12a34240
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources/prettyPrint.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 { generatedToOriginalId } from "devtools/client/shared/source-map-loader/index";
+
+import assert from "../../utils/assert";
+import { recordEvent } from "../../utils/telemetry";
+import { updateBreakpointsForNewPrettyPrintedSource } from "../breakpoints/index";
+
+import { getPrettySourceURL, isJavaScript } from "../../utils/source";
+import { isFulfilled, fulfilled } from "../../utils/async-value";
+import { getOriginalLocation } from "../../utils/source-maps";
+import { prefs } from "../../utils/prefs";
+import {
+ loadGeneratedSourceText,
+ loadOriginalSourceText,
+} from "./loadSourceText";
+import { mapFrames } from "../pause/index";
+import { selectSpecificLocation } from "../sources/index";
+import { createPrettyPrintOriginalSource } from "../../client/firefox/create";
+
+import {
+ getFirstSourceActorForGeneratedSource,
+ getSourceFromId,
+ getSelectedLocation,
+} from "../../selectors/index";
+
+import { selectSource } from "./select";
+import { memoizeableAction } from "../../utils/memoizableAction";
+
+import DevToolsUtils from "devtools/shared/DevToolsUtils";
+
+const LINE_BREAK_REGEX = /\r\n?|\n|\u2028|\u2029/g;
+function matchAllLineBreaks(str) {
+ return Array.from(str.matchAll(LINE_BREAK_REGEX));
+}
+
+function getPrettyOriginalSourceURL(generatedSource) {
+ return getPrettySourceURL(generatedSource.url || generatedSource.id);
+}
+
+export async function prettyPrintSourceTextContent(
+ sourceMapLoader,
+ prettyPrintWorker,
+ generatedSource,
+ content,
+ actors
+) {
+ if (!content || !isFulfilled(content)) {
+ throw new Error("Cannot pretty-print a file that has not loaded");
+ }
+
+ const contentValue = content.value;
+ if (
+ (!isJavaScript(generatedSource, contentValue) && !generatedSource.isHTML) ||
+ contentValue.type !== "text"
+ ) {
+ throw new Error(
+ `Can't prettify ${contentValue.contentType} files, only HTML and Javascript.`
+ );
+ }
+
+ const url = getPrettyOriginalSourceURL(generatedSource);
+
+ let prettyPrintWorkerResult;
+ if (generatedSource.isHTML) {
+ prettyPrintWorkerResult = await prettyPrintHtmlFile({
+ prettyPrintWorker,
+ generatedSource,
+ content,
+ actors,
+ });
+ } else {
+ prettyPrintWorkerResult = await prettyPrintWorker.prettyPrint({
+ sourceText: contentValue.value,
+ indent: " ".repeat(prefs.indentSize),
+ url,
+ });
+ }
+
+ // The source map URL service used by other devtools listens to changes to
+ // sources based on their actor IDs, so apply the sourceMap there too.
+ const generatedSourceIds = [
+ generatedSource.id,
+ ...actors.map(item => item.actor),
+ ];
+ await sourceMapLoader.setSourceMapForGeneratedSources(
+ generatedSourceIds,
+ prettyPrintWorkerResult.sourceMap
+ );
+
+ return {
+ text: prettyPrintWorkerResult.code,
+ contentType: contentValue.contentType,
+ };
+}
+
+/**
+ * Pretty print inline script inside an HTML file
+ *
+ * @param {Object} options
+ * @param {PrettyPrintDispatcher} options.prettyPrintWorker: The prettyPrint worker
+ * @param {Object} options.generatedSource: The HTML source we want to pretty print
+ * @param {Object} options.content
+ * @param {Array} options.actors: An array of the HTML file inline script sources data
+ *
+ * @returns Promise<Object> A promise that resolves with an object of the following shape:
+ * - {String} code: The prettified HTML text
+ * - {Object} sourceMap: The sourceMap object
+ */
+async function prettyPrintHtmlFile({
+ prettyPrintWorker,
+ generatedSource,
+ content,
+ actors,
+}) {
+ const url = getPrettyOriginalSourceURL(generatedSource);
+ const contentValue = content.value;
+ const htmlFileText = contentValue.value;
+ const prettyPrintWorkerResult = { code: htmlFileText };
+
+ const allLineBreaks = matchAllLineBreaks(htmlFileText);
+ let lineCountDelta = 0;
+
+ // Sort inline script actors so they are in the same order as in the html document.
+ actors.sort((a, b) => {
+ if (a.sourceStartLine === b.sourceStartLine) {
+ return a.sourceStartColumn > b.sourceStartColumn;
+ }
+ return a.sourceStartLine > b.sourceStartLine;
+ });
+
+ const prettyPrintTaskId = generatedSource.id;
+
+ // We don't want to replace part of the HTML document in the loop since it would require
+ // to account for modified lines for each iteration.
+ // Instead, we'll put each sections to replace in this array, where elements will be
+ // objects of the following shape:
+ // {Integer} startIndex: The start index in htmlFileText of the section we want to replace
+ // {Integer} endIndex: The end index in htmlFileText of the section we want to replace
+ // {String} prettyText: The pretty text we'll replace the original section with
+ // Once we iterated over all the inline scripts, we'll do the replacements (on the html
+ // file text) in reverse order, so we don't need have to care about the modified lines
+ // for each iteration.
+ const replacements = [];
+
+ const seenLocations = new Set();
+
+ for (const sourceInfo of actors) {
+ // We can get duplicate source actors representing the same inline script which will
+ // cause trouble in the pretty printing here. This should be fixed on the server (see
+ // Bug 1824979), but in the meantime let's not handle the same location twice so the
+ // pretty printing is not impacted.
+ const location = `${sourceInfo.sourceStartLine}:${sourceInfo.sourceStartColumn}`;
+ if (!sourceInfo.sourceLength || seenLocations.has(location)) {
+ continue;
+ }
+ seenLocations.add(location);
+ // Here we want to get the index of the last line break before the script tag.
+ // In allLineBreaks, this would be the item at (script tag line - 1)
+ // Since sourceInfo.sourceStartLine is 1-based, we need to get the item at (sourceStartLine - 2)
+ const indexAfterPreviousLineBreakInHtml =
+ sourceInfo.sourceStartLine > 1
+ ? allLineBreaks[sourceInfo.sourceStartLine - 2].index + 1
+ : 0;
+ const startIndex =
+ indexAfterPreviousLineBreakInHtml + sourceInfo.sourceStartColumn;
+ const endIndex = startIndex + sourceInfo.sourceLength;
+ const scriptText = htmlFileText.substring(startIndex, endIndex);
+ DevToolsUtils.assert(
+ scriptText.length == sourceInfo.sourceLength,
+ "script text has expected length"
+ );
+
+ // Here we're going to pretty print each inline script content.
+ // Since we want to have a sourceMap that we'll apply to the whole HTML file,
+ // we'll only collect the sourceMap once we handled all inline scripts.
+ // `taskId` allows us to signal to the worker that all those calls are part of the
+ // same bigger file, and we'll use it later to get the sourceMap.
+ const prettyText = await prettyPrintWorker.prettyPrintInlineScript({
+ taskId: prettyPrintTaskId,
+ sourceText: scriptText,
+ indent: " ".repeat(prefs.indentSize),
+ url,
+ originalStartLine: sourceInfo.sourceStartLine,
+ originalStartColumn: sourceInfo.sourceStartColumn,
+ // The generated line will be impacted by the previous inline scripts that were
+ // pretty printed, which is why we offset with lineCountDelta
+ generatedStartLine: sourceInfo.sourceStartLine + lineCountDelta,
+ generatedStartColumn: sourceInfo.sourceStartColumn,
+ lineCountDelta,
+ });
+
+ // We need to keep track of the line added/removed in order to properly offset
+ // the mapping of the pretty-print text
+ lineCountDelta +=
+ matchAllLineBreaks(prettyText).length -
+ matchAllLineBreaks(scriptText).length;
+
+ replacements.push({
+ startIndex,
+ endIndex,
+ prettyText,
+ });
+ }
+
+ // `getSourceMap` allow us to collect the computed source map resulting of the calls
+ // to `prettyPrint` with the same taskId.
+ prettyPrintWorkerResult.sourceMap = await prettyPrintWorker.getSourceMap(
+ prettyPrintTaskId
+ );
+
+ // Sort replacement in reverse order so we can replace code in the HTML file more easily
+ replacements.sort((a, b) => a.startIndex < b.startIndex);
+ for (const { startIndex, endIndex, prettyText } of replacements) {
+ prettyPrintWorkerResult.code =
+ prettyPrintWorkerResult.code.substring(0, startIndex) +
+ prettyText +
+ prettyPrintWorkerResult.code.substring(endIndex);
+ }
+
+ return prettyPrintWorkerResult;
+}
+
+function createPrettySource(source, sourceActor) {
+ return async ({ dispatch, sourceMapLoader, getState }) => {
+ const url = getPrettyOriginalSourceURL(source);
+ const id = generatedToOriginalId(source.id, url);
+ const prettySource = createPrettyPrintOriginalSource(id, url);
+
+ dispatch({
+ type: "ADD_ORIGINAL_SOURCES",
+ originalSources: [prettySource],
+ generatedSourceActor: sourceActor,
+ });
+ return prettySource;
+ };
+}
+
+function selectPrettyLocation(prettySource) {
+ return async thunkArgs => {
+ const { dispatch, getState } = thunkArgs;
+ let location = getSelectedLocation(getState());
+
+ // If we were selecting a particular line in the minified/generated source,
+ // try to select the matching line in the prettified/original source.
+ if (
+ location &&
+ location.line >= 1 &&
+ getPrettySourceURL(location.source.url) == prettySource.url
+ ) {
+ // Note that it requires to have called `prettyPrintSourceTextContent` and `sourceMapLoader.setSourceMapForGeneratedSources`
+ // to be functional and so to be called after `loadOriginalSourceText` completed.
+ location = await getOriginalLocation(location, thunkArgs);
+
+ // If the precise line/column correctly mapped to the pretty printed source, select that precise location.
+ // Otherwise fallback to selectSource in order to select the first line instead of the current line within the bundle.
+ if (location.source == prettySource) {
+ return dispatch(selectSpecificLocation(location));
+ }
+ }
+
+ return dispatch(selectSource(prettySource));
+ };
+}
+
+/**
+ * Toggle the pretty printing of a source's text.
+ * Nothing will happen for non-javascript files.
+ *
+ * @param Object source
+ * The source object for the minified/generated source.
+ * @returns Promise
+ * A promise that resolves to the Pretty print/original source object.
+ */
+export async function prettyPrintSource(source, thunkArgs) {
+ const { dispatch, getState } = thunkArgs;
+ recordEvent("pretty_print");
+
+ assert(
+ !source.isOriginal,
+ "Pretty-printing only allowed on generated sources"
+ );
+
+ const sourceActor = getFirstSourceActorForGeneratedSource(
+ getState(),
+ source.id
+ );
+
+ await dispatch(loadGeneratedSourceText(sourceActor));
+
+ const newPrettySource = await dispatch(
+ createPrettySource(source, sourceActor)
+ );
+
+ // Force loading the pretty source/original text.
+ // This will end up calling prettyPrintSourceTextContent() of this module, and
+ // more importantly, will populate the sourceMapLoader, which is used by selectPrettyLocation.
+ await dispatch(loadOriginalSourceText(newPrettySource));
+
+ // Update frames to the new pretty/original source (in case we were paused).
+ // Map the frames before selecting the pretty source in order to avoid
+ // having bundle/generated source for frames (we may compute scope things for the bundle).
+ await dispatch(mapFrames(sourceActor.thread));
+
+ // Update breakpoints locations to the new pretty/original source
+ await dispatch(updateBreakpointsForNewPrettyPrintedSource(source));
+
+ // A mutated flag, only meant to be used within this module
+ // to know when we are done loading the pretty printed source.
+ // This is important for the callsite in `selectLocation`
+ // in order to ensure all action are completed and especially `mapFrames`.
+ // Otherwise we may use generated frames there.
+ newPrettySource._loaded = true;
+
+ return newPrettySource;
+}
+
+// Use memoization in order to allow calling this actions many times
+// while ensuring creating the pretty source only once.
+const memoizedPrettyPrintSource = memoizeableAction("setSymbols", {
+ getValue: (source, { getState }) => {
+ // Lookup for an already existing pretty source
+ const url = getPrettyOriginalSourceURL(source);
+ const id = generatedToOriginalId(source.id, url);
+ const s = getSourceFromId(getState(), id);
+ // Avoid returning it if doTogglePrettyPrint isn't completed.
+ if (!s || !s._loaded) {
+ return undefined;
+ }
+ return fulfilled(s);
+ },
+ createKey: source => source.id,
+ action: (source, thunkArgs) => prettyPrintSource(source, thunkArgs),
+});
+
+export function prettyPrintAndSelectSource(source) {
+ return async ({ dispatch, sourceMapLoader, getState }) => {
+ const prettySource = await dispatch(memoizedPrettyPrintSource(source));
+
+ // Select the pretty/original source based on the location we may
+ // have had against the minified/generated source.
+ // This uses source map to map locations.
+ // Also note that selecting a location force many things:
+ // * opening tabs
+ // * fetching symbols/inline scope
+ // * fetching breakable lines
+ //
+ // This isn't part of memoizedTogglePrettyPrint/doTogglePrettyPrint
+ // because if the source is already pretty printed, the memoization
+ // would avoid trying to update to the mapped location based
+ // on current location on the minified source.
+ await dispatch(selectPrettyLocation(prettySource));
+
+ return prettySource;
+ };
+}
diff --git a/devtools/client/debugger/src/actions/sources/select.js b/devtools/client/debugger/src/actions/sources/select.js
new file mode 100644
index 0000000000..63200a398a
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources/select.js
@@ -0,0 +1,368 @@
+/* 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/>. */
+
+/**
+ * Redux actions for the sources state
+ * @module actions/sources
+ */
+
+import { setSymbols } from "./symbols";
+import { setInScopeLines } from "../ast/index";
+import { prettyPrintAndSelectSource } from "./prettyPrint";
+import { addTab, closeTab } from "../tabs";
+import { loadSourceText } from "./loadSourceText";
+import { setBreakableLines } from "./breakableLines";
+
+import { prefs } from "../../utils/prefs";
+import { isMinified } from "../../utils/source";
+import { createLocation } from "../../utils/location";
+import {
+ getRelatedMapLocation,
+ getOriginalLocation,
+} from "../../utils/source-maps";
+
+import {
+ getSource,
+ getFirstSourceActorForGeneratedSource,
+ getSourceByURL,
+ getPrettySource,
+ getSelectedLocation,
+ getShouldSelectOriginalLocation,
+ canPrettyPrintSource,
+ getSourceTextContent,
+ tabExists,
+ hasSource,
+ hasSourceActor,
+ hasPrettyTab,
+ isSourceActorWithSourceMap,
+} from "../../selectors/index";
+
+// This is only used by jest tests (and within this module)
+export const setSelectedLocation = (
+ location,
+ shouldSelectOriginalLocation,
+ shouldHighlightSelectedLocation
+) => ({
+ type: "SET_SELECTED_LOCATION",
+ location,
+ shouldSelectOriginalLocation,
+ shouldHighlightSelectedLocation,
+});
+
+// This is only used by jest tests (and within this module)
+export const setPendingSelectedLocation = (url, options) => ({
+ type: "SET_PENDING_SELECTED_LOCATION",
+ url,
+ line: options?.line,
+ column: options?.column,
+});
+
+// This is only used by jest tests (and within this module)
+export const clearSelectedLocation = () => ({
+ type: "CLEAR_SELECTED_LOCATION",
+});
+
+/**
+ * Deterministically select a source that has a given URL. This will
+ * work regardless of the connection status or if the source exists
+ * yet.
+ *
+ * This exists mostly for external things to interact with the
+ * debugger.
+ */
+export function selectSourceURL(url, options) {
+ return async ({ dispatch, getState }) => {
+ const source = getSourceByURL(getState(), url);
+ if (!source) {
+ return dispatch(setPendingSelectedLocation(url, options));
+ }
+
+ const location = createLocation({ ...options, source });
+ return dispatch(selectLocation(location));
+ };
+}
+
+/**
+ * Wrapper around selectLocation, which creates the location object for us.
+ * Note that it ignores the currently selected source and will select
+ * the precise generated/original source passed as argument.
+ *
+ * @param {String} source
+ * The precise source to select.
+ * @param {String} sourceActor
+ * The specific source actor of the source to
+ * select the source text. This is optional.
+ */
+export function selectSource(source, sourceActor) {
+ return async ({ dispatch }) => {
+ // `createLocation` requires a source object, but we may use selectSource to close the last tab,
+ // where source will be null and the location will be an empty object.
+ const location = source ? createLocation({ source, sourceActor }) : {};
+
+ return dispatch(selectSpecificLocation(location));
+ };
+}
+
+/**
+ * Select a new location.
+ * This will automatically select the source in the source tree (if visible)
+ * and open the source (a new tab and the source editor)
+ * as well as highlight a precise line in the editor.
+ *
+ * Note that by default, this may map your passed location to the original
+ * or generated location based on the selected source state. (see keepContext)
+ *
+ * @param {Object} location
+ * @param {Object} options
+ * @param {boolean} options.keepContext
+ * If false, this will ignore the currently selected source
+ * and select the generated or original location, even if we
+ * were currently selecting the other source type.
+ * @param {boolean} options.highlight
+ * True by default. To be set to false in order to preveng highlighting the selected location in the editor.
+ * We will only show the location, but do not put a special background on the line.
+ */
+export function selectLocation(
+ location,
+ { keepContext = true, highlight = true } = {}
+) {
+ return async thunkArgs => {
+ const { dispatch, getState, client } = thunkArgs;
+
+ if (!client) {
+ // No connection, do nothing. This happens when the debugger is
+ // shut down too fast and it tries to display a default source.
+ return;
+ }
+
+ let source = location.source;
+
+ if (!source) {
+ // If there is no source we deselect the current selected source
+ dispatch(clearSelectedLocation());
+ return;
+ }
+
+ // Preserve the current source map context (original / generated)
+ // when navigating to a new location.
+ // i.e. if keepContext isn't manually overriden to false,
+ // we will convert the source we want to select to either
+ // original/generated in order to match the currently selected one.
+ // If the currently selected source is original, we will
+ // automatically map `location` to refer to the original source,
+ // even if that used to refer only to the generated source.
+ let shouldSelectOriginalLocation = getShouldSelectOriginalLocation(
+ getState()
+ );
+ if (keepContext) {
+ // Pretty print source may not be registered yet and getRelatedMapLocation may not return it.
+ // Wait for the pretty print source to be fully processed.
+ if (
+ !location.source.isOriginal &&
+ shouldSelectOriginalLocation &&
+ hasPrettyTab(getState(), location.source)
+ ) {
+ // Note that prettyPrintAndSelectSource has already been called a bit before when this generated source has been added
+ // but it is a slow operation and is most likely not resolved yet.
+ // prettyPrintAndSelectSource uses memoization to avoid doing the operation more than once, while waiting from both callsites.
+ await dispatch(prettyPrintAndSelectSource(location.source));
+ }
+ if (shouldSelectOriginalLocation != location.source.isOriginal) {
+ // getRelatedMapLocation will convert to the related generated/original location.
+ // i.e if the original location is passed, the related generated location will be returned and vice versa.
+ location = await getRelatedMapLocation(location, thunkArgs);
+ // Note that getRelatedMapLocation may return the exact same location.
+ // For example, if the source-map is half broken, it may return a generated location
+ // while we were selecting original locations. So we may be seeing bundles intermittently
+ // when stepping through broken source maps. And we will see original sources when stepping
+ // through functional original sources.
+
+ source = location.source;
+ }
+ } else {
+ shouldSelectOriginalLocation = location.source.isOriginal;
+ }
+
+ let sourceActor = location.sourceActor;
+ if (!sourceActor) {
+ sourceActor = getFirstSourceActorForGeneratedSource(
+ getState(),
+ source.id
+ );
+ location = createLocation({ ...location, sourceActor });
+ }
+
+ if (!tabExists(getState(), source.id)) {
+ dispatch(addTab(source, sourceActor));
+ }
+
+ dispatch(
+ setSelectedLocation(location, shouldSelectOriginalLocation, highlight)
+ );
+
+ await dispatch(loadSourceText(source, sourceActor));
+
+ // Stop the async work if we started selecting another location
+ if (getSelectedLocation(getState()) != location) {
+ return;
+ }
+
+ await dispatch(setBreakableLines(location));
+
+ // Stop the async work if we started selecting another location
+ if (getSelectedLocation(getState()) != location) {
+ return;
+ }
+
+ const loadedSource = getSource(getState(), source.id);
+
+ if (!loadedSource) {
+ // If there was a navigation while we were loading the loadedSource
+ return;
+ }
+
+ const sourceTextContent = getSourceTextContent(getState(), location);
+
+ if (
+ keepContext &&
+ prefs.autoPrettyPrint &&
+ !getPrettySource(getState(), loadedSource.id) &&
+ canPrettyPrintSource(getState(), location) &&
+ isMinified(source, sourceTextContent)
+ ) {
+ await dispatch(prettyPrintAndSelectSource(loadedSource));
+ dispatch(closeTab(loadedSource));
+ }
+
+ await dispatch(setSymbols(location));
+
+ // Stop the async work if we started selecting another location
+ if (getSelectedLocation(getState()) != location) {
+ return;
+ }
+
+ // /!\ we don't historicaly wait for this async action
+ dispatch(setInScopeLines());
+
+ // When we select a generated source which has a sourcemap,
+ // asynchronously fetch the related original location in order to display
+ // the mapped location in the editor's footer.
+ if (
+ !location.source.isOriginal &&
+ isSourceActorWithSourceMap(getState(), sourceActor.id)
+ ) {
+ let originalLocation = await getOriginalLocation(location, thunkArgs, {
+ looseSearch: true,
+ });
+ // We pass a null original location when the location doesn't map
+ // in order to know when we are done processing the source map.
+ // * `getOriginalLocation` would return the exact same location if it doesn't map
+ // * `getOriginalLocation` may also return a distinct location object,
+ // but refering to the same `source` object (which is the bundle) when it doesn't
+ // map to any known original location.
+ if (originalLocation.source === location.source) {
+ originalLocation = null;
+ }
+ dispatch({
+ type: "SET_ORIGINAL_SELECTED_LOCATION",
+ location,
+ originalLocation,
+ });
+ }
+ };
+}
+
+/**
+ * Select a location while ignoring the currently selected source.
+ * This will select the generated location even if the currently
+ * select source is an original source. And the other way around.
+ *
+ * @param {Object} location
+ * The location to select, object which includes enough
+ * information to specify a precise source, line and column.
+ */
+export function selectSpecificLocation(location) {
+ return selectLocation(location, { keepContext: false });
+}
+
+/**
+ * Similar to `selectSpecificLocation`, but if the precise Source object
+ * is missing, this will fallback to select any source having the same URL.
+ * In this fallback scenario, sources without a URL will be ignored.
+ *
+ * This is typically used when trying to select a source (e.g. in project search result)
+ * after reload, because the source objects are new on each new page load, but source
+ * with the same URL may still exist.
+ *
+ * @param {Object} location
+ * The location to select.
+ * @return {function}
+ * The action will return true if a matching source was found.
+ */
+export function selectSpecificLocationOrSameUrl(location) {
+ return async ({ dispatch, getState }) => {
+ // If this particular source no longer exists, open any matching URL.
+ // This will typically happen on reload.
+ if (!hasSource(getState(), location.source.id)) {
+ // Some sources, like evaled script won't have a URL attribute
+ // and can't be re-selected if we don't find the exact same source object.
+ if (!location.source.url) {
+ return false;
+ }
+ const source = getSourceByURL(getState(), location.source.url);
+ if (!source) {
+ return false;
+ }
+ // Also reset the sourceActor, as it won't match the same source.
+ const sourceActor = getFirstSourceActorForGeneratedSource(
+ getState(),
+ location.source.id
+ );
+ location = createLocation({ ...location, source, sourceActor });
+ } else if (!hasSourceActor(getState(), location.sourceActor.id)) {
+ // If the specific source actor no longer exists, match any still available.
+ const sourceActor = getFirstSourceActorForGeneratedSource(
+ getState(),
+ location.source.id
+ );
+ location = createLocation({ ...location, sourceActor });
+ }
+ await dispatch(selectSpecificLocation(location));
+ return true;
+ };
+}
+
+/**
+ * Select the "mapped location".
+ *
+ * If the passed location is on a generated source, select the
+ * related location in the original source.
+ * If the passed location is on an original source, select the
+ * related location in the generated source.
+ */
+export function jumpToMappedLocation(location) {
+ return async function (thunkArgs) {
+ const { client, dispatch } = thunkArgs;
+ if (!client) {
+ return null;
+ }
+
+ // Map to either an original or a generated source location
+ const pairedLocation = await getRelatedMapLocation(location, thunkArgs);
+
+ return dispatch(selectSpecificLocation(pairedLocation));
+ };
+}
+
+// This is only used by tests
+export function jumpToMappedSelectedLocation() {
+ return async function ({ dispatch, getState }) {
+ const location = getSelectedLocation(getState());
+ if (!location) {
+ return;
+ }
+
+ await dispatch(jumpToMappedLocation(location));
+ };
+}
diff --git a/devtools/client/debugger/src/actions/sources/symbols.js b/devtools/client/debugger/src/actions/sources/symbols.js
new file mode 100644
index 0000000000..c7b9132c32
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources/symbols.js
@@ -0,0 +1,62 @@
+/* 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 { getSymbols } from "../../selectors/index";
+
+import { PROMISE } from "../utils/middleware/promise";
+import { loadSourceText } from "./loadSourceText";
+
+import { memoizeableAction } from "../../utils/memoizableAction";
+import { fulfilled } from "../../utils/async-value";
+
+async function doSetSymbols(location, { dispatch, getState, parserWorker }) {
+ await dispatch(loadSourceText(location.source, location.sourceActor));
+
+ await dispatch({
+ type: "SET_SYMBOLS",
+ location,
+ [PROMISE]: parserWorker.getSymbols(location.source.id),
+ });
+}
+
+export const setSymbols = memoizeableAction("setSymbols", {
+ getValue: (location, { getState, parserWorker }) => {
+ if (!parserWorker.isLocationSupported(location)) {
+ return fulfilled(null);
+ }
+
+ const symbols = getSymbols(getState(), location);
+ if (!symbols) {
+ return null;
+ }
+
+ return fulfilled(symbols);
+ },
+ createKey: location => location.source.id,
+ action: (location, thunkArgs) => doSetSymbols(location, thunkArgs),
+});
+
+export function getOriginalFunctionDisplayName(location) {
+ return async ({ parserWorker, dispatch }) => {
+ // Make sure the source for the symbols exist in the parser worker.
+ await dispatch(loadSourceText(location.source, location.sourceActor));
+ return parserWorker.getClosestFunctionName(location);
+ };
+}
+
+export function getFunctionSymbols(location, maxResults) {
+ return async ({ parserWorker, dispatch }) => {
+ // Make sure the source for the symbols exist in the parser worker.
+ await dispatch(loadSourceText(location.source, location.sourceActor));
+ return parserWorker.getFunctionSymbols(location.source.id, maxResults);
+ };
+}
+
+export function getClassSymbols(location) {
+ return async ({ parserWorker, dispatch }) => {
+ // See comment in getFunctionSymbols
+ await dispatch(loadSourceText(location.source, location.sourceActor));
+ return parserWorker.getClassSymbols(location.source.id);
+ };
+}
diff --git a/devtools/client/debugger/src/actions/sources/tests/blackbox.spec.js b/devtools/client/debugger/src/actions/sources/tests/blackbox.spec.js
new file mode 100644
index 0000000000..9a7c69ee32
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources/tests/blackbox.spec.js
@@ -0,0 +1,247 @@
+/* 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,
+ selectors,
+ createStore,
+ makeSource,
+} from "../../../utils/test-head";
+
+import { initialSourceBlackBoxState } from "../../../reducers/source-blackbox";
+
+describe("blackbox", () => {
+ it("should blackbox and unblackbox a source based on the current state of the source", async () => {
+ const store = createStore({
+ blackBox: async () => true,
+ getSourceActorBreakableLines: async () => [],
+ });
+ const { dispatch, getState } = store;
+
+ const fooSource = await dispatch(
+ actions.newGeneratedSource(makeSource("foo"))
+ );
+ await dispatch(actions.toggleBlackBox(fooSource));
+
+ expect(selectors.isSourceBlackBoxed(getState(), fooSource)).toEqual(true);
+
+ let blackboxRanges = selectors.getBlackBoxRanges(getState());
+ expect(blackboxRanges[fooSource.url]).toEqual([]);
+
+ await dispatch(actions.toggleBlackBox(fooSource));
+
+ expect(selectors.isSourceBlackBoxed(getState(), fooSource)).toEqual(false);
+
+ blackboxRanges = selectors.getBlackBoxRanges(getState());
+ expect(blackboxRanges[fooSource.url]).toEqual(undefined);
+ });
+
+ it("should blackbox and unblackbox a source when explicilty specified", async () => {
+ const store = createStore({
+ blackBox: async () => true,
+ getSourceActorBreakableLines: async () => [],
+ });
+ const { dispatch, getState } = store;
+
+ const fooSource = await dispatch(
+ actions.newGeneratedSource(makeSource("foo"))
+ );
+
+ // check the state before trying to blackbox
+ expect(selectors.isSourceBlackBoxed(getState(), fooSource)).toEqual(false);
+
+ let blackboxRanges = selectors.getBlackBoxRanges(getState());
+ expect(blackboxRanges[fooSource.url]).toEqual(undefined);
+
+ // should blackbox the whole source
+ await dispatch(actions.toggleBlackBox(fooSource, true, []));
+
+ expect(selectors.isSourceBlackBoxed(getState(), fooSource)).toEqual(true);
+
+ blackboxRanges = selectors.getBlackBoxRanges(getState());
+ expect(blackboxRanges[fooSource.url]).toEqual([]);
+
+ // should unblackbox the whole source
+ await dispatch(actions.toggleBlackBox(fooSource, false, []));
+
+ expect(selectors.isSourceBlackBoxed(getState(), fooSource)).toEqual(false);
+
+ blackboxRanges = selectors.getBlackBoxRanges(getState());
+ expect(blackboxRanges[fooSource.url]).toEqual(undefined);
+ });
+
+ it("should blackbox and unblackbox lines in a source", async () => {
+ const store = createStore({
+ blackBox: async () => true,
+ getSourceActorBreakableLines: async () => [],
+ });
+ const { dispatch, getState } = store;
+
+ const fooSource = await dispatch(
+ actions.newGeneratedSource(makeSource("foo"))
+ );
+
+ const range1 = {
+ start: { line: 10, column: 3 },
+ end: { line: 15, column: 4 },
+ };
+
+ const range2 = {
+ start: { line: 5, column: 3 },
+ end: { line: 7, column: 6 },
+ };
+
+ await dispatch(actions.toggleBlackBox(fooSource, true, [range1]));
+
+ expect(selectors.isSourceBlackBoxed(getState(), fooSource)).toEqual(true);
+
+ let blackboxRanges = selectors.getBlackBoxRanges(getState());
+ expect(blackboxRanges[fooSource.url]).toEqual([range1]);
+
+ // add new blackbox lines in the second range
+ await dispatch(actions.toggleBlackBox(fooSource, true, [range2]));
+
+ expect(selectors.isSourceBlackBoxed(getState(), fooSource)).toEqual(true);
+
+ blackboxRanges = selectors.getBlackBoxRanges(getState());
+ // ranges are stored asc order
+ expect(blackboxRanges[fooSource.url]).toEqual([range2, range1]);
+
+ // un-blackbox lines in the first range
+ await dispatch(actions.toggleBlackBox(fooSource, false, [range1]));
+
+ expect(selectors.isSourceBlackBoxed(getState(), fooSource)).toEqual(true);
+
+ blackboxRanges = selectors.getBlackBoxRanges(getState());
+ expect(blackboxRanges[fooSource.url]).toEqual([range2]);
+
+ // un-blackbox lines in the second range
+ await dispatch(actions.toggleBlackBox(fooSource, false, [range2]));
+
+ expect(selectors.isSourceBlackBoxed(getState(), fooSource)).toEqual(false);
+
+ blackboxRanges = selectors.getBlackBoxRanges(getState());
+ expect(blackboxRanges[fooSource.url]).toEqual(undefined);
+ });
+
+ it("should undo blackboxed lines when whole source unblackboxed", async () => {
+ const store = createStore({
+ blackBox: async () => true,
+ getSourceActorBreakableLines: async () => [],
+ });
+ const { dispatch, getState } = store;
+
+ const fooSource = await dispatch(
+ actions.newGeneratedSource(makeSource("foo"))
+ );
+
+ const range1 = {
+ start: { line: 1, column: 5 },
+ end: { line: 3, column: 4 },
+ };
+
+ const range2 = {
+ start: { line: 5, column: 3 },
+ end: { line: 7, column: 6 },
+ };
+
+ await dispatch(actions.toggleBlackBox(fooSource, true, [range1, range2]));
+
+ expect(selectors.isSourceBlackBoxed(getState(), fooSource)).toEqual(true);
+
+ let blackboxRanges = selectors.getBlackBoxRanges(getState());
+ // The ranges are ordered in based on the lines & cols in ascending
+ expect(blackboxRanges[fooSource.url]).toEqual([range2, range1]);
+
+ // un-blackbox the whole source
+ await dispatch(actions.toggleBlackBox(fooSource));
+
+ expect(selectors.isSourceBlackBoxed(getState(), fooSource)).toEqual(false);
+
+ blackboxRanges = selectors.getBlackBoxRanges(getState());
+ expect(blackboxRanges[fooSource.url]).toEqual(undefined);
+ });
+
+ it("should restore the blackboxed state correctly debugger load", async () => {
+ const mockAsyncStoreBlackBoxedRanges = {
+ "http://localhost:8000/examples/foo": [
+ {
+ start: { line: 1, column: 5 },
+ end: { line: 3, column: 4 },
+ },
+ ],
+ };
+
+ function loadInitialState() {
+ const blackboxedRanges = mockAsyncStoreBlackBoxedRanges;
+ return {
+ sourceBlackBox: initialSourceBlackBoxState({ blackboxedRanges }),
+ };
+ }
+ const store = createStore(
+ {
+ blackBox: async () => true,
+ getSourceActorBreakableLines: async () => [],
+ },
+ loadInitialState()
+ );
+ const { dispatch, getState } = store;
+
+ const fooSource = await dispatch(
+ actions.newGeneratedSource(makeSource("foo"))
+ );
+
+ expect(selectors.isSourceBlackBoxed(getState(), fooSource)).toEqual(true);
+
+ const blackboxRanges = selectors.getBlackBoxRanges(getState());
+ const mockFooSourceRange = mockAsyncStoreBlackBoxedRanges[fooSource.url];
+ expect(blackboxRanges[fooSource.url]).toEqual(mockFooSourceRange);
+ });
+
+ it("should unblackbox lines after blackboxed state has been restored", async () => {
+ const mockAsyncStoreBlackBoxedRanges = {
+ "http://localhost:8000/examples/foo": [
+ {
+ start: { line: 1, column: 5 },
+ end: { line: 3, column: 4 },
+ },
+ ],
+ };
+
+ function loadInitialState() {
+ const blackboxedRanges = mockAsyncStoreBlackBoxedRanges;
+ return {
+ sourceBlackBox: initialSourceBlackBoxState({ blackboxedRanges }),
+ };
+ }
+ const store = createStore(
+ {
+ blackBox: async () => true,
+ getSourceActorBreakableLines: async () => [],
+ },
+ loadInitialState()
+ );
+ const { dispatch, getState } = store;
+
+ const fooSource = await dispatch(
+ actions.newGeneratedSource(makeSource("foo"))
+ );
+
+ expect(selectors.isSourceBlackBoxed(getState(), fooSource)).toEqual(true);
+
+ let blackboxRanges = selectors.getBlackBoxRanges(getState());
+ const mockFooSourceRange = mockAsyncStoreBlackBoxedRanges[fooSource.url];
+ expect(blackboxRanges[fooSource.url]).toEqual(mockFooSourceRange);
+
+ //unblackbox the blackboxed line
+ await dispatch(
+ actions.toggleBlackBox(fooSource, false, mockFooSourceRange)
+ );
+
+ expect(selectors.isSourceBlackBoxed(getState(), fooSource)).toEqual(false);
+
+ blackboxRanges = selectors.getBlackBoxRanges(getState());
+ expect(blackboxRanges[fooSource.url]).toEqual(undefined);
+ });
+});
diff --git a/devtools/client/debugger/src/actions/sources/tests/loadSource.spec.js b/devtools/client/debugger/src/actions/sources/tests/loadSource.spec.js
new file mode 100644
index 0000000000..8de08eb8a8
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources/tests/loadSource.spec.js
@@ -0,0 +1,216 @@
+/* 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,
+ selectors,
+ watchForState,
+ createStore,
+ makeSource,
+} from "../../../utils/test-head";
+import { mockCommandClient } from "../../tests/helpers/mockCommandClient";
+import { isFulfilled, isRejected } from "../../../utils/async-value";
+import { createLocation } from "../../../utils/location";
+
+describe("loadGeneratedSourceText", () => {
+ it("should load source text", async () => {
+ const store = createStore(mockCommandClient);
+ const { dispatch, getState } = store;
+
+ const foo1Source = await dispatch(
+ actions.newGeneratedSource(makeSource("foo1"))
+ );
+ const foo1SourceActor = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ foo1Source.id
+ );
+ await dispatch(actions.loadGeneratedSourceText(foo1SourceActor));
+
+ const foo1Content = selectors.getSettledSourceTextContent(
+ getState(),
+ createLocation({
+ source: foo1Source,
+ sourceActor: foo1SourceActor,
+ })
+ );
+
+ expect(
+ foo1Content &&
+ isFulfilled(foo1Content) &&
+ foo1Content.value.type === "text"
+ ? foo1Content.value.value.indexOf("return foo1")
+ : -1
+ ).not.toBe(-1);
+
+ const foo2Source = await dispatch(
+ actions.newGeneratedSource(makeSource("foo2"))
+ );
+ const foo2SourceActor = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ foo2Source.id
+ );
+
+ await dispatch(actions.loadGeneratedSourceText(foo2SourceActor));
+
+ const foo2Content = selectors.getSettledSourceTextContent(
+ getState(),
+ createLocation({
+ source: foo2Source,
+ sourceActor: foo2SourceActor,
+ })
+ );
+
+ expect(
+ foo2Content &&
+ isFulfilled(foo2Content) &&
+ foo2Content.value.type === "text"
+ ? foo2Content.value.value.indexOf("return foo2")
+ : -1
+ ).not.toBe(-1);
+ });
+
+ it("loads two sources w/ one request", async () => {
+ let resolve;
+ let count = 0;
+ const { dispatch, getState } = createStore({
+ sourceContents: () =>
+ new Promise(r => {
+ count++;
+ resolve = r;
+ }),
+ getSourceActorBreakpointPositions: async () => ({}),
+ getSourceActorBreakableLines: async () => [],
+ });
+ const id = "foo";
+
+ const source = await dispatch(actions.newGeneratedSource(makeSource(id)));
+ const sourceActor = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ source.id
+ );
+
+ dispatch(actions.loadGeneratedSourceText(sourceActor));
+
+ const loading = dispatch(actions.loadGeneratedSourceText(sourceActor));
+
+ if (!resolve) {
+ throw new Error("no resolve");
+ }
+ resolve({ source: "yay", contentType: "text/javascript" });
+ await loading;
+ expect(count).toEqual(1);
+
+ const content = selectors.getSettledSourceTextContent(
+ getState(),
+ createLocation({
+ source,
+ sourceActor,
+ })
+ );
+ expect(
+ content &&
+ isFulfilled(content) &&
+ content.value.type === "text" &&
+ content.value.value
+ ).toEqual("yay");
+ });
+
+ it("doesn't re-load loaded sources", async () => {
+ let resolve;
+ let count = 0;
+ const { dispatch, getState } = createStore({
+ sourceContents: () =>
+ new Promise(r => {
+ count++;
+ resolve = r;
+ }),
+ getSourceActorBreakpointPositions: async () => ({}),
+ getSourceActorBreakableLines: async () => [],
+ });
+ const id = "foo";
+
+ const source = await dispatch(actions.newGeneratedSource(makeSource(id)));
+ const sourceActor = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ source.id
+ );
+ const loading = dispatch(actions.loadGeneratedSourceText(sourceActor));
+
+ if (!resolve) {
+ throw new Error("no resolve");
+ }
+ resolve({ source: "yay", contentType: "text/javascript" });
+ await loading;
+
+ await dispatch(actions.loadGeneratedSourceText(sourceActor));
+ expect(count).toEqual(1);
+
+ const content = selectors.getSettledSourceTextContent(
+ getState(),
+ createLocation({
+ source,
+ sourceActor,
+ })
+ );
+ expect(
+ content &&
+ isFulfilled(content) &&
+ content.value.type === "text" &&
+ content.value.value
+ ).toEqual("yay");
+ });
+
+ it("should indicate a loading source", async () => {
+ const store = createStore(mockCommandClient);
+ const { dispatch, getState } = store;
+
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("foo2"))
+ );
+
+ const sourceActor = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ source.id
+ );
+
+ const wasLoading = watchForState(store, state => {
+ return !selectors.getSettledSourceTextContent(
+ state,
+ createLocation({
+ source,
+ sourceActor,
+ })
+ );
+ });
+ await dispatch(actions.loadGeneratedSourceText(sourceActor));
+
+ expect(wasLoading()).toBe(true);
+ });
+
+ it("should indicate an errored source text", async () => {
+ const { dispatch, getState } = createStore(mockCommandClient);
+
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("bad-id"))
+ );
+ const sourceActor = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ source.id
+ );
+ await dispatch(actions.loadGeneratedSourceText(sourceActor));
+
+ const content = selectors.getSettledSourceTextContent(
+ getState(),
+ createLocation({
+ source,
+ sourceActor,
+ })
+ );
+ expect(
+ content && isRejected(content) && typeof content.value === "string"
+ ? content.value.indexOf("sourceContents failed")
+ : -1
+ ).not.toBe(-1);
+ });
+});
diff --git a/devtools/client/debugger/src/actions/sources/tests/newSources.spec.js b/devtools/client/debugger/src/actions/sources/tests/newSources.spec.js
new file mode 100644
index 0000000000..cef9eca31e
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources/tests/newSources.spec.js
@@ -0,0 +1,103 @@
+/* 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,
+ selectors,
+ createStore,
+ makeSource,
+ makeSourceURL,
+ makeOriginalSource,
+} from "../../../utils/test-head";
+const { getSource, getSourceCount, getSelectedSource } = selectors;
+
+import { mockCommandClient } from "../../tests/helpers/mockCommandClient";
+
+describe("sources - new sources", () => {
+ it("should add sources to state", async () => {
+ const { dispatch, getState } = createStore(mockCommandClient);
+ await dispatch(actions.newGeneratedSource(makeSource("base.js")));
+ await dispatch(actions.newGeneratedSource(makeSource("jquery.js")));
+
+ expect(getSourceCount(getState())).toEqual(2);
+ const base = getSource(getState(), "base.js");
+ const jquery = getSource(getState(), "jquery.js");
+ expect(base && base.id).toEqual("base.js");
+ expect(jquery && jquery.id).toEqual("jquery.js");
+ });
+
+ it("should not add multiple identical generated sources", async () => {
+ const { dispatch, getState } = createStore(mockCommandClient);
+
+ const generated = await dispatch(
+ actions.newGeneratedSource(makeSource("base.js"))
+ );
+
+ await dispatch(actions.newOriginalSources([makeOriginalSource(generated)]));
+ await dispatch(actions.newOriginalSources([makeOriginalSource(generated)]));
+
+ expect(getSourceCount(getState())).toEqual(2);
+ });
+
+ it("should not add multiple identical original sources", async () => {
+ const { dispatch, getState } = createStore(mockCommandClient);
+
+ await dispatch(actions.newGeneratedSource(makeSource("base.js")));
+ await dispatch(actions.newGeneratedSource(makeSource("base.js")));
+
+ expect(getSourceCount(getState())).toEqual(1);
+ });
+
+ it("should automatically select a pending source", async () => {
+ const { dispatch, getState } = createStore(mockCommandClient);
+ const baseSourceURL = makeSourceURL("base.js");
+ await dispatch(actions.selectSourceURL(baseSourceURL));
+
+ expect(getSelectedSource(getState())).toBe(undefined);
+ const baseSource = await dispatch(
+ actions.newGeneratedSource(makeSource("base.js"))
+ );
+
+ const selected = getSelectedSource(getState());
+ expect(selected && selected.url).toBe(baseSource.url);
+ });
+
+ // eslint-disable-next-line
+ it("should not attempt to fetch original sources if it's missing a source map url", async () => {
+ const loadSourceMap = jest.fn();
+ const { dispatch } = createStore(
+ mockCommandClient,
+ {},
+ {
+ loadSourceMap,
+ getOriginalLocations: async items => items,
+ getOriginalLocation: location => location,
+ }
+ );
+
+ await dispatch(actions.newGeneratedSource(makeSource("base.js")));
+ expect(loadSourceMap).not.toHaveBeenCalled();
+ });
+
+ // eslint-disable-next-line
+ it("should process new sources immediately, without waiting for source maps to be fetched first", async () => {
+ const { dispatch, getState } = createStore(
+ mockCommandClient,
+ {},
+ {
+ loadSourceMap: async () => new Promise(_ => {}),
+ getOriginalLocations: async items => items,
+ getOriginalLocation: location => location,
+ }
+ );
+ await dispatch(
+ actions.newGeneratedSource(
+ makeSource("base.js", { sourceMapURL: "base.js.map" })
+ )
+ );
+ expect(getSourceCount(getState())).toEqual(1);
+ const base = getSource(getState(), "base.js");
+ expect(base && base.id).toEqual("base.js");
+ });
+});
diff --git a/devtools/client/debugger/src/actions/sources/tests/select.spec.js b/devtools/client/debugger/src/actions/sources/tests/select.spec.js
new file mode 100644
index 0000000000..5f4feba6c0
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources/tests/select.spec.js
@@ -0,0 +1,195 @@
+/* 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,
+ selectors,
+ createStore,
+ createSourceObject,
+ makeSource,
+ makeSourceURL,
+ waitForState,
+ makeOriginalSource,
+} from "../../../utils/test-head";
+import {
+ getSourceCount,
+ getSelectedSource,
+ getSourceTabs,
+ getSelectedLocation,
+ getSymbols,
+} from "../../../selectors/";
+import { createLocation } from "../../../utils/location";
+
+import { mockCommandClient } from "../../tests/helpers/mockCommandClient";
+
+process.on("unhandledRejection", (reason, p) => {});
+
+function initialLocation(sourceId) {
+ return createLocation({ source: createSourceObject(sourceId), line: 1 });
+}
+
+describe("sources", () => {
+ it("should open a tab for the source", async () => {
+ const { dispatch, getState } = createStore(mockCommandClient);
+ await dispatch(actions.newGeneratedSource(makeSource("foo.js")));
+ await dispatch(actions.selectLocation(initialLocation("foo.js")));
+
+ const tabs = getSourceTabs(getState());
+ expect(tabs).toHaveLength(1);
+ expect(tabs[0].url).toEqual("http://localhost:8000/examples/foo.js");
+ });
+
+ it("should keep the selected source when other tab closed", async () => {
+ const { dispatch, getState } = createStore(mockCommandClient);
+
+ await dispatch(actions.newGeneratedSource(makeSource("foo.js")));
+ await dispatch(actions.newGeneratedSource(makeSource("bar.js")));
+ const bazSource = await dispatch(
+ actions.newGeneratedSource(makeSource("baz.js"))
+ );
+
+ // 3rd tab
+ await dispatch(actions.selectLocation(initialLocation("foo.js")));
+
+ // 2nd tab
+ await dispatch(actions.selectLocation(initialLocation("bar.js")));
+
+ // 1st tab
+ await dispatch(actions.selectLocation(initialLocation("baz.js")));
+
+ // 3rd tab is reselected
+ await dispatch(actions.selectLocation(initialLocation("foo.js")));
+ await dispatch(actions.closeTab(bazSource));
+
+ const selected = getSelectedSource(getState());
+ expect(selected && selected.id).toBe("foo.js");
+ expect(getSourceTabs(getState())).toHaveLength(2);
+ });
+
+ it("should not select new sources that lack a URL", async () => {
+ const { dispatch, getState } = createStore(mockCommandClient);
+
+ await dispatch(
+ actions.newGeneratedSource({
+ ...makeSource("foo"),
+ url: "",
+ })
+ );
+
+ expect(getSourceCount(getState())).toEqual(1);
+ const selectedLocation = getSelectedLocation(getState());
+ expect(selectedLocation).toEqual(undefined);
+ });
+
+ it("sets and clears selected location correctly", async () => {
+ const { dispatch, getState } = createStore(mockCommandClient);
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("testSource"))
+ );
+ const location = createLocation({ source });
+
+ // set value
+ dispatch(actions.setSelectedLocation(location));
+ expect(getSelectedLocation(getState())).toEqual({
+ ...location,
+ });
+
+ // clear value
+ dispatch(actions.clearSelectedLocation());
+ expect(getSelectedLocation(getState())).toEqual(null);
+ });
+
+ it("sets and clears pending selected location correctly", () => {
+ const { dispatch, getState } = createStore(mockCommandClient);
+ const url = "testURL";
+ const options = { line: "testLine", column: "testColumn" };
+
+ // set value
+ dispatch(actions.setPendingSelectedLocation(url, options));
+ const setResult = getState().sources.pendingSelectedLocation;
+ expect(setResult).toEqual({
+ url,
+ line: options.line,
+ column: options.column,
+ });
+
+ // clear value
+ dispatch(actions.clearSelectedLocation());
+ const clearResult = getState().sources.pendingSelectedLocation;
+ expect(clearResult).toEqual({ url: "" });
+ });
+
+ it("should keep the generated the viewing context", async () => {
+ const store = createStore(mockCommandClient);
+ const { dispatch, getState } = store;
+ const baseSource = await dispatch(
+ actions.newGeneratedSource(makeSource("base.js"))
+ );
+ const sourceActor = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ baseSource.id
+ );
+
+ const location = createLocation({
+ source: baseSource,
+ line: 1,
+ sourceActor,
+ });
+ await dispatch(actions.selectLocation(location));
+
+ const selected = getSelectedSource(getState());
+ expect(selected && selected.id).toBe(baseSource.id);
+ await waitForState(store, state => getSymbols(state, location));
+ });
+
+ it("should change the original the viewing context", async () => {
+ const { dispatch, getState } = createStore(
+ mockCommandClient,
+ {},
+ {
+ getOriginalLocation: async location => ({ ...location, line: 12 }),
+ getOriginalLocations: async items => items,
+ getGeneratedRangesForOriginal: async () => [],
+ getOriginalSourceText: async () => ({ text: "" }),
+ }
+ );
+
+ const baseGenSource = await dispatch(
+ actions.newGeneratedSource(makeSource("base.js"))
+ );
+
+ const baseSources = await dispatch(
+ actions.newOriginalSources([makeOriginalSource(baseGenSource)])
+ );
+ await dispatch(actions.selectSource(baseSources[0]));
+
+ await dispatch(
+ actions.selectSpecificLocation(
+ createLocation({
+ source: baseSources[0],
+ line: 1,
+ })
+ )
+ );
+
+ const selected = getSelectedLocation(getState());
+ expect(selected && selected.line).toBe(1);
+ });
+
+ describe("selectSourceURL", () => {
+ it("should automatically select a pending source", async () => {
+ const { dispatch, getState } = createStore(mockCommandClient);
+ const baseSourceURL = makeSourceURL("base.js");
+ await dispatch(actions.selectSourceURL(baseSourceURL));
+
+ expect(getSelectedSource(getState())).toBe(undefined);
+ const baseSource = await dispatch(
+ actions.newGeneratedSource(makeSource("base.js"))
+ );
+
+ const selected = getSelectedSource(getState());
+ expect(selected && selected.url).toBe(baseSource.url);
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/actions/tabs.js b/devtools/client/debugger/src/actions/tabs.js
new file mode 100644
index 0000000000..c397b30919
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tabs.js
@@ -0,0 +1,104 @@
+/* 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/>. */
+
+/**
+ * Redux actions for the editor tabs
+ */
+
+import { removeDocument } from "../utils/editor/index";
+import { selectSource } from "./sources/index";
+
+import { getSelectedLocation, getSourcesForTabs } from "../selectors/index";
+
+export function addTab(source, sourceActor) {
+ return {
+ type: "ADD_TAB",
+ source,
+ sourceActor,
+ };
+}
+
+export function moveTab(url, tabIndex) {
+ return {
+ type: "MOVE_TAB",
+ url,
+ tabIndex,
+ };
+}
+
+export function moveTabBySourceId(sourceId, tabIndex) {
+ return {
+ type: "MOVE_TAB_BY_SOURCE_ID",
+ sourceId,
+ tabIndex,
+ };
+}
+
+export function closeTab(source) {
+ return closeTabs([source]);
+}
+
+export function closeTabs(sources) {
+ return ({ dispatch, getState }) => {
+ if (!sources.length) {
+ return;
+ }
+
+ for (const source of sources) {
+ removeDocument(source.id);
+ }
+
+ // If we are removing the tabs for the selected location,
+ // we need to select another source
+ const newSourceToSelect = getNewSourceToSelect(getState(), sources);
+
+ dispatch({ type: "CLOSE_TABS", sources });
+
+ dispatch(selectSource(newSourceToSelect));
+ };
+}
+
+/**
+ * Compute the potential new source to select while closing tabs for a given set of sources.
+ *
+ * @param {Object} state
+ * Redux state object.
+ * @param {Array<Source>} closedTabsSources
+ * Ordered list of source object for which tabs should be closed.
+ * Should be a consecutive list of source matching the order of tabs reducer.
+ */
+function getNewSourceToSelect(state, closedTabsSources) {
+ const selectedLocation = getSelectedLocation(state);
+ // Do not try to select any source if none was selected before
+ if (!selectedLocation) {
+ return null;
+ }
+ // Keep selecting the same source if we aren't removing the currently selected source
+ if (!closedTabsSources.includes(selectedLocation.source)) {
+ return selectedLocation.source;
+ }
+ const tabsSources = getSourcesForTabs(state);
+ // Assume that `sources` is a consecutive list of tab's sources
+ // ordered in the same way as `tabsSources`.
+ const lastRemovedTabSource = closedTabsSources.at(-1);
+ const lastRemovedTabIndex = tabsSources.indexOf(lastRemovedTabSource);
+ if (lastRemovedTabIndex == -1) {
+ // This is unexpected, do not try to select any source.
+ return null;
+ }
+ // If there is some tabs after the last removed tab, select the first one.
+ if (lastRemovedTabIndex + 1 < tabsSources.length) {
+ return tabsSources[lastRemovedTabIndex + 1];
+ }
+
+ // If there is some tabs before the first removed tab, select the last one.
+ const firstRemovedTabIndex =
+ lastRemovedTabIndex - (closedTabsSources.length - 1);
+ if (firstRemovedTabIndex > 0) {
+ return tabsSources[firstRemovedTabIndex - 1];
+ }
+
+ // It looks like we removed all the tabs
+ return null;
+}
diff --git a/devtools/client/debugger/src/actions/tests/__snapshots__/expressions.spec.js.snap b/devtools/client/debugger/src/actions/tests/__snapshots__/expressions.spec.js.snap
new file mode 100644
index 0000000000..f27eb26f50
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tests/__snapshots__/expressions.spec.js.snap
@@ -0,0 +1,11 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`expressions should get the autocomplete matches for the input 1`] = `
+Array [
+ "toLocaleString",
+ "toSource",
+ "toString",
+ "toolbar",
+ "top",
+]
+`;
diff --git a/devtools/client/debugger/src/actions/tests/__snapshots__/pending-breakpoints.spec.js.snap b/devtools/client/debugger/src/actions/tests/__snapshots__/pending-breakpoints.spec.js.snap
new file mode 100644
index 0000000000..4119cbc875
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tests/__snapshots__/pending-breakpoints.spec.js.snap
@@ -0,0 +1,30 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`initializing when pending breakpoints exist in prefs syncs pending breakpoints 1`] = `
+Object {
+ "http://localhost:8000/examples/bar.js:5:2": Object {
+ "astLocation": Object {
+ "index": 0,
+ "name": undefined,
+ "offset": Object {
+ "line": 5,
+ },
+ },
+ "disabled": false,
+ "generatedLocation": Object {
+ "column": 2,
+ "line": 5,
+ "sourceUrl": "http://localhost:8000/examples/bar.js",
+ },
+ "location": Object {
+ "column": 2,
+ "line": 5,
+ "sourceUrl": "http://localhost:8000/examples/bar.js",
+ },
+ "options": Object {
+ "condition": null,
+ "hidden": false,
+ },
+ },
+}
+`;
diff --git a/devtools/client/debugger/src/actions/tests/expressions.spec.js b/devtools/client/debugger/src/actions/tests/expressions.spec.js
new file mode 100644
index 0000000000..c69276fa40
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tests/expressions.spec.js
@@ -0,0 +1,136 @@
+/* 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, selectors, createStore } from "../../utils/test-head";
+
+const mockThreadFront = {
+ evaluate: (script, { frameId }) =>
+ new Promise((resolve, reject) => {
+ if (!frameId) {
+ resolve("bla");
+ } else {
+ resolve("boo");
+ }
+ }),
+ evaluateExpressions: (inputs, { frameId }) =>
+ Promise.all(
+ inputs.map(
+ input =>
+ new Promise((resolve, reject) => {
+ if (!frameId) {
+ resolve("bla");
+ } else {
+ resolve("boo");
+ }
+ })
+ )
+ ),
+ getFrameScopes: async () => {},
+ getFrames: async () => [],
+ sourceContents: () => ({ source: "", contentType: "text/javascript" }),
+ getSourceActorBreakpointPositions: async () => ({}),
+ getSourceActorBreakableLines: async () => [],
+ autocomplete: () => {
+ return new Promise(resolve => {
+ resolve({
+ from: "foo",
+ matches: ["toLocaleString", "toSource", "toString", "toolbar", "top"],
+ matchProp: "to",
+ });
+ });
+ },
+};
+
+describe("expressions", () => {
+ it("should add an expression", async () => {
+ const { dispatch, getState } = createStore(mockThreadFront);
+
+ await dispatch(actions.addExpression("foo"));
+ expect(selectors.getExpressions(getState())).toHaveLength(1);
+ });
+
+ it("should not add empty expressions", () => {
+ const { dispatch, getState } = createStore(mockThreadFront);
+
+ dispatch(actions.addExpression(undefined));
+ dispatch(actions.addExpression(""));
+ expect(selectors.getExpressions(getState())).toHaveLength(0);
+ });
+
+ it("should add invalid expressions", async () => {
+ const { dispatch, getState } = createStore(mockThreadFront);
+ await dispatch(actions.addExpression("foo#"));
+ const state = getState();
+ expect(selectors.getExpressions(state)).toHaveLength(1);
+ });
+
+ it("should update an expression", async () => {
+ const { dispatch, getState } = createStore(mockThreadFront);
+
+ await dispatch(actions.addExpression("foo"));
+ const expression = selectors.getExpression(getState(), "foo");
+ if (!expression) {
+ throw new Error("expression must exist");
+ }
+
+ await dispatch(actions.updateExpression("bar", expression));
+ const bar = selectors.getExpression(getState(), "bar");
+
+ expect(bar && bar.input).toBe("bar");
+ });
+
+ it("should not update an expression w/ invalid code", async () => {
+ const { dispatch, getState } = createStore(mockThreadFront);
+
+ await dispatch(actions.addExpression("foo"));
+ const expression = selectors.getExpression(getState(), "foo");
+ if (!expression) {
+ throw new Error("expression must exist");
+ }
+ await dispatch(actions.updateExpression("#bar", expression));
+ expect(selectors.getExpression(getState(), "bar")).toBeUndefined();
+ });
+
+ it("should delete an expression", async () => {
+ const { dispatch, getState } = createStore(mockThreadFront);
+
+ await dispatch(actions.addExpression("foo"));
+ await dispatch(actions.addExpression("bar"));
+ expect(selectors.getExpressions(getState())).toHaveLength(2);
+
+ const expression = selectors.getExpression(getState(), "foo");
+
+ if (!expression) {
+ throw new Error("expression must exist");
+ }
+
+ const bar = selectors.getExpression(getState(), "bar");
+ dispatch(actions.deleteExpression(expression));
+ expect(selectors.getExpressions(getState())).toHaveLength(1);
+ expect(bar && bar.input).toBe("bar");
+ });
+
+ it("should evaluate expressions global scope", async () => {
+ const { dispatch, getState } = createStore(mockThreadFront);
+ await dispatch(actions.addExpression("foo"));
+ await dispatch(actions.addExpression("bar"));
+
+ let foo = selectors.getExpression(getState(), "foo");
+ let bar = selectors.getExpression(getState(), "bar");
+ expect(foo && foo.value).toBe("bla");
+ expect(bar && bar.value).toBe("bla");
+
+ await dispatch(actions.evaluateExpressions(null));
+ foo = selectors.getExpression(getState(), "foo");
+ bar = selectors.getExpression(getState(), "bar");
+ expect(foo && foo.value).toBe("bla");
+ expect(bar && bar.value).toBe("bla");
+ });
+
+ it("should get the autocomplete matches for the input", async () => {
+ const { dispatch, getState } = createStore(mockThreadFront);
+ await dispatch(actions.autocomplete("to", 2));
+ expect(selectors.getAutocompleteMatchset(getState())).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/debugger/src/actions/tests/helpers/breakpoints.js b/devtools/client/debugger/src/actions/tests/helpers/breakpoints.js
new file mode 100644
index 0000000000..84eb83e23f
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tests/helpers/breakpoints.js
@@ -0,0 +1,65 @@
+/* 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 { createLocation } from "../../../utils/location";
+
+export function mockPendingBreakpoint(overrides = {}) {
+ const { sourceUrl, line, column, condition, disabled, hidden } = overrides;
+ return {
+ location: {
+ sourceUrl: sourceUrl || "http://localhost:8000/examples/bar.js",
+ line: line || 5,
+ column: column || 1,
+ },
+ generatedLocation: {
+ sourceUrl: sourceUrl || "http://localhost:8000/examples/bar.js",
+ line: line || 5,
+ column: column || 1,
+ },
+ astLocation: {
+ name: undefined,
+ offset: {
+ line: line || 5,
+ },
+ index: 0,
+ },
+ options: {
+ condition: condition || null,
+ hidden: hidden || false,
+ },
+ disabled: disabled || false,
+ };
+}
+
+export function generateBreakpoint(filename, line = 5, column = 0) {
+ return {
+ id: "breakpoint",
+ originalText: "",
+ text: "",
+ location: createLocation({
+ source: {
+ url: `http://localhost:8000/examples/${filename}`,
+ id: filename,
+ },
+ sourceId: filename,
+ line,
+ column,
+ }),
+ generatedLocation: createLocation({
+ source: {
+ url: `http://localhost:8000/examples/${filename}`,
+ id: filename,
+ },
+ sourceId: filename,
+ line,
+ column,
+ }),
+ astLocation: undefined,
+ options: {
+ condition: "",
+ hidden: false,
+ },
+ disabled: false,
+ };
+}
diff --git a/devtools/client/debugger/src/actions/tests/helpers/mockCommandClient.js b/devtools/client/debugger/src/actions/tests/helpers/mockCommandClient.js
new file mode 100644
index 0000000000..38dd55c274
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tests/helpers/mockCommandClient.js
@@ -0,0 +1,49 @@
+/* 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 createSource(name, code) {
+ name = name.replace(/\..*$/, "");
+ return {
+ source: code || `function ${name}() {\n return ${name} \n}`,
+ contentType: "text/javascript",
+ };
+}
+
+const sources = [
+ "a",
+ "b",
+ "foo",
+ "bar",
+ "foo1",
+ "foo2",
+ "a.js",
+ "baz.js",
+ "foobar.js",
+ "barfoo.js",
+ "foo.js",
+ "bar.js",
+ "base.js",
+ "bazz.js",
+ "jquery.js",
+];
+
+export const mockCommandClient = {
+ sourceContents: function ({ source }) {
+ return new Promise((resolve, reject) => {
+ if (sources.includes(source)) {
+ resolve(createSource(source));
+ }
+
+ reject(`unknown source: ${source}`);
+ });
+ },
+ setBreakpoint: async () => {},
+ removeBreakpoint: _id => Promise.resolve(),
+ threadFront: async () => {},
+ getFrameScopes: async () => {},
+ getFrames: async () => [],
+ evaluateExpressions: async () => {},
+ getSourceActorBreakpointPositions: async () => ({}),
+ getSourceActorBreakableLines: async () => [],
+};
diff --git a/devtools/client/debugger/src/actions/tests/helpers/readFixture.js b/devtools/client/debugger/src/actions/tests/helpers/readFixture.js
new file mode 100644
index 0000000000..6c23641226
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tests/helpers/readFixture.js
@@ -0,0 +1,14 @@
+/* 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 fs from "fs";
+import path from "path";
+
+export default function readFixture(name) {
+ const text = fs.readFileSync(
+ path.join(__dirname, `../fixtures/${name}`),
+ "utf8"
+ );
+ return text;
+}
diff --git a/devtools/client/debugger/src/actions/tests/pending-breakpoints.spec.js b/devtools/client/debugger/src/actions/tests/pending-breakpoints.spec.js
new file mode 100644
index 0000000000..c51ad7e6e5
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tests/pending-breakpoints.spec.js
@@ -0,0 +1,251 @@
+/* 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/>. */
+
+// TODO: we would like to mock this in the local tests
+import {
+ generateBreakpoint,
+ mockPendingBreakpoint,
+} from "./helpers/breakpoints.js";
+
+import { mockCommandClient } from "./helpers/mockCommandClient";
+import { asyncStore } from "../../utils/prefs";
+
+function loadInitialState(opts = {}) {
+ const mockedPendingBreakpoint = mockPendingBreakpoint({ ...opts, column: 2 });
+ const l = mockedPendingBreakpoint.location;
+ const id = `${l.sourceUrl}:${l.line}:${l.column}`;
+ asyncStore.pendingBreakpoints = { [id]: mockedPendingBreakpoint };
+
+ return { pendingBreakpoints: asyncStore.pendingBreakpoints };
+}
+
+jest.mock("../../utils/prefs", () => ({
+ prefs: {
+ clientSourceMapsEnabled: true,
+ expressions: [],
+ },
+ asyncStore: {
+ pendingBreakpoints: {},
+ },
+ clear: jest.fn(),
+ features: {
+ inlinePreview: true,
+ },
+}));
+
+import {
+ createStore,
+ selectors,
+ actions,
+ makeSource,
+ waitForState,
+} from "../../utils/test-head";
+
+import sourceMapLoader from "devtools/client/shared/source-map-loader/index";
+
+function mockClient(bpPos = {}) {
+ return {
+ ...mockCommandClient,
+ setSkipPausing: jest.fn(),
+ getSourceActorBreakpointPositions: async () => bpPos,
+ getSourceActorBreakableLines: async () => [],
+ };
+}
+
+function mockSourceMaps() {
+ return {
+ ...sourceMapLoader,
+ getOriginalSourceText: async id => ({
+ id,
+ text: "",
+ contentType: "text/javascript",
+ }),
+ getGeneratedRangesForOriginal: async () => [
+ { start: { line: 0, column: 0 }, end: { line: 10, column: 10 } },
+ ],
+ getOriginalLocations: async items => items,
+ };
+}
+
+describe("when adding breakpoints", () => {
+ it("a corresponding pending breakpoint should be added", async () => {
+ const { dispatch, getState } = createStore(
+ mockClient({ 5: [1] }),
+ loadInitialState(),
+ mockSourceMaps()
+ );
+
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("foo.js"))
+ );
+ const sourceActor = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ source.id
+ );
+
+ await dispatch(actions.loadGeneratedSourceText(sourceActor));
+
+ const bp = generateBreakpoint("foo.js", 5, 1);
+
+ await dispatch(actions.addBreakpoint(bp.location));
+
+ expect(selectors.getPendingBreakpointList(getState())).toHaveLength(2);
+ });
+});
+
+describe("initializing when pending breakpoints exist in prefs", () => {
+ it("syncs pending breakpoints", async () => {
+ const { getState } = createStore(
+ mockClient({ 5: [0] }),
+ loadInitialState(),
+ mockSourceMaps()
+ );
+ const bps = selectors.getPendingBreakpoints(getState());
+ expect(bps).toMatchSnapshot();
+ });
+
+ it("re-adding breakpoints update existing pending breakpoints", async () => {
+ const { dispatch, getState } = createStore(
+ mockClient({ 5: [1, 2] }),
+ loadInitialState(),
+ mockSourceMaps()
+ );
+ const bar = generateBreakpoint("bar.js", 5, 1);
+
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("bar.js"))
+ );
+ const sourceActor = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ source.id
+ );
+
+ await dispatch(actions.loadGeneratedSourceText(sourceActor));
+ await dispatch(actions.addBreakpoint(bar.location));
+
+ const bps = selectors.getPendingBreakpointList(getState());
+ expect(bps).toHaveLength(2);
+ });
+
+ it("adding bps doesn't remove existing pending breakpoints", async () => {
+ const { dispatch, getState } = createStore(
+ mockClient({ 5: [0] }),
+ loadInitialState(),
+ mockSourceMaps()
+ );
+ const bp = generateBreakpoint("foo.js");
+
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("foo.js"))
+ );
+ const sourceActor = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ source.id
+ );
+
+ await dispatch(actions.loadGeneratedSourceText(sourceActor));
+
+ await dispatch(actions.addBreakpoint(bp.location));
+
+ const bps = selectors.getPendingBreakpointList(getState());
+ expect(bps).toHaveLength(2);
+ });
+});
+
+describe("initializing with disabled pending breakpoints in prefs", () => {
+ it("syncs breakpoints with pending breakpoints", async () => {
+ const store = createStore(
+ mockClient({ 5: [2] }),
+ loadInitialState({ disabled: true }),
+ mockSourceMaps()
+ );
+
+ const { getState, dispatch } = store;
+
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("bar.js"))
+ );
+ const sourceActor = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ source.id
+ );
+
+ await dispatch(actions.loadGeneratedSourceText(sourceActor));
+
+ await waitForState(store, state => {
+ const bps = selectors.getBreakpointsForSource(state, source);
+ return bps && !!Object.values(bps).length;
+ });
+
+ const bp = selectors.getBreakpointsList(getState()).find(({ location }) => {
+ return (
+ location.line == 5 &&
+ location.column == 2 &&
+ location.source.id == source.id
+ );
+ });
+
+ if (!bp) {
+ throw new Error("no bp");
+ }
+ expect(bp.location.source.id).toEqual(source.id);
+ expect(bp.disabled).toEqual(true);
+ });
+});
+
+describe("adding sources", () => {
+ it("corresponding breakpoints are added for a single source", async () => {
+ const store = createStore(
+ mockClient({ 5: [2] }),
+ loadInitialState({ disabled: true }),
+ mockSourceMaps()
+ );
+ const { getState, dispatch } = store;
+
+ expect(selectors.getBreakpointCount(getState())).toEqual(0);
+
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("bar.js"))
+ );
+ const sourceActor = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ source.id
+ );
+
+ await dispatch(actions.loadGeneratedSourceText(sourceActor));
+
+ await waitForState(store, state => selectors.getBreakpointCount(state) > 0);
+
+ expect(selectors.getBreakpointCount(getState())).toEqual(1);
+ });
+
+ it("add corresponding breakpoints for multiple sources", async () => {
+ const store = createStore(
+ mockClient({ 5: [2] }),
+ loadInitialState({ disabled: true }),
+ mockSourceMaps()
+ );
+ const { getState, dispatch } = store;
+
+ expect(selectors.getBreakpointCount(getState())).toEqual(0);
+
+ const [source1, source2] = await dispatch(
+ actions.newGeneratedSources([makeSource("bar.js"), makeSource("foo.js")])
+ );
+ const sourceActor1 = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ source1.id
+ );
+ const sourceActor2 = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ source2.id
+ );
+
+ await dispatch(actions.loadGeneratedSourceText(sourceActor1));
+ await dispatch(actions.loadGeneratedSourceText(sourceActor2));
+
+ await waitForState(store, state => selectors.getBreakpointCount(state) > 0);
+ expect(selectors.getBreakpointCount(getState())).toEqual(1);
+ });
+});
diff --git a/devtools/client/debugger/src/actions/tests/ui.spec.js b/devtools/client/debugger/src/actions/tests/ui.spec.js
new file mode 100644
index 0000000000..712fac4996
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tests/ui.spec.js
@@ -0,0 +1,88 @@
+/* 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 {
+ createStore,
+ selectors,
+ actions,
+ makeSource,
+} from "../../utils/test-head";
+import { createLocation } from "../../utils/location";
+import { mockCommandClient } from "./helpers/mockCommandClient";
+
+const {
+ getActiveSearch,
+ getFrameworkGroupingState,
+ getPaneCollapse,
+ getHighlightedLineRangeForSelectedSource,
+} = selectors;
+
+describe("ui", () => {
+ it("should toggle the visible state of file search", () => {
+ const { dispatch, getState } = createStore();
+ expect(getActiveSearch(getState())).toBe(null);
+ dispatch(actions.setActiveSearch("file"));
+ expect(getActiveSearch(getState())).toBe("file");
+ });
+
+ it("should close file search", () => {
+ const { dispatch, getState } = createStore();
+ expect(getActiveSearch(getState())).toBe(null);
+ dispatch(actions.setActiveSearch("file"));
+ dispatch(actions.closeActiveSearch());
+ expect(getActiveSearch(getState())).toBe(null);
+ });
+
+ it("should toggle the collapse state of a pane", () => {
+ const { dispatch, getState } = createStore();
+ expect(getPaneCollapse(getState(), "start")).toBe(false);
+ dispatch(actions.togglePaneCollapse("start", true));
+ expect(getPaneCollapse(getState(), "start")).toBe(true);
+ });
+
+ it("should toggle the collapsed state of frameworks in the callstack", () => {
+ const { dispatch, getState } = createStore();
+ const currentState = getFrameworkGroupingState(getState());
+ dispatch(actions.toggleFrameworkGrouping(!currentState));
+ expect(getFrameworkGroupingState(getState())).toBe(!currentState);
+ });
+
+ it("should highlight lines", async () => {
+ const { dispatch, getState } = createStore(mockCommandClient);
+ const base = await dispatch(
+ actions.newGeneratedSource(makeSource("base.js"))
+ );
+ const sourceActor = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ base.id
+ );
+ //await dispatch(actions.selectSource(base, sourceActor));
+ const location = createLocation({
+ source: base,
+ line: 1,
+ sourceActor,
+ });
+ await dispatch(actions.selectLocation(location));
+
+ const range = { start: 3, end: 5, sourceId: base.id };
+ dispatch(actions.highlightLineRange(range));
+ expect(getHighlightedLineRangeForSelectedSource(getState())).toEqual(range);
+ });
+
+ it("should clear highlight lines", async () => {
+ const { dispatch, getState } = createStore(mockCommandClient);
+ const base = await dispatch(
+ actions.newGeneratedSource(makeSource("base.js"))
+ );
+ const sourceActor = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ base.id
+ );
+ await dispatch(actions.selectSource(base, sourceActor));
+ const range = { start: 3, end: 5, sourceId: "2" };
+ dispatch(actions.highlightLineRange(range));
+ dispatch(actions.clearHighlightLineRange());
+ expect(getHighlightedLineRangeForSelectedSource(getState())).toEqual(null);
+ });
+});
diff --git a/devtools/client/debugger/src/actions/threads.js b/devtools/client/debugger/src/actions/threads.js
new file mode 100644
index 0000000000..82132a144c
--- /dev/null
+++ b/devtools/client/debugger/src/actions/threads.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 { createThread } from "../client/firefox/create";
+import { getSourcesToRemoveForThread } from "../selectors/index";
+import { clearDocumentsForSources } from "../utils/editor/source-documents";
+
+export function addTarget(targetFront) {
+ return { type: "INSERT_THREAD", newThread: createThread(targetFront) };
+}
+
+export function removeTarget(targetFront) {
+ return ({ getState, dispatch, parserWorker }) => {
+ const threadActorID = targetFront.targetForm.threadActor;
+
+ // Just before emitting the REMOVE_THREAD action,
+ // synchronously compute the list of source and source actor objects
+ // which should be removed as that one target get removed.
+ //
+ // The list of source objects isn't trivial to compute as these objects
+ // are shared across targets/threads.
+ const { actors, sources } = getSourcesToRemoveForThread(
+ getState(),
+ threadActorID
+ );
+
+ // CodeMirror documents aren't stored in redux reducer,
+ // so we need this manual function call in order to ensure clearing them.
+ clearDocumentsForSources(sources);
+
+ // Notify the reducers that a target/thread is being removed
+ // and that all related resources should be cleared.
+ // This action receives the list of related source actors and source objects
+ // related to that to-be-removed target.
+ // This will be fired on navigation for all existing targets.
+ // That except the top target, when pausing on unload, where the top target may still hold longer.
+ // Also except for service worker targets, which may be kept alive.
+ dispatch({
+ type: "REMOVE_THREAD",
+ threadActorID,
+ actors,
+ sources,
+ });
+
+ parserWorker.clearSources(sources.map(source => source.id));
+ };
+}
+
+export function toggleJavaScriptEnabled(enabled) {
+ return async ({ dispatch, client }) => {
+ await client.toggleJavaScriptEnabled(enabled);
+ dispatch({
+ type: "TOGGLE_JAVASCRIPT_ENABLED",
+ value: enabled,
+ });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/toolbox.js b/devtools/client/debugger/src/actions/toolbox.js
new file mode 100644
index 0000000000..a343c92863
--- /dev/null
+++ b/devtools/client/debugger/src/actions/toolbox.js
@@ -0,0 +1,43 @@
+/* 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/>. */
+
+/**
+ * @memberof actions/toolbox
+ * @static
+ */
+export function openLink(url) {
+ return async function ({ panel }) {
+ return panel.openLink(url);
+ };
+}
+
+export function evaluateInConsole(inputString) {
+ return async ({ panel }) => {
+ return panel.openConsoleAndEvaluate(inputString);
+ };
+}
+
+export function openElementInInspectorCommand(grip) {
+ return async ({ panel }) => {
+ return panel.openElementInInspector(grip);
+ };
+}
+
+export function openInspector(grip) {
+ return async ({ panel }) => {
+ return panel.openInspector();
+ };
+}
+
+export function highlightDomElement(grip) {
+ return async ({ panel }) => {
+ return panel.highlightDomElement(grip);
+ };
+}
+
+export function unHighlightDomElement(grip) {
+ return async ({ panel }) => {
+ return panel.unHighlightDomElement(grip);
+ };
+}
diff --git a/devtools/client/debugger/src/actions/tracing.js b/devtools/client/debugger/src/actions/tracing.js
new file mode 100644
index 0000000000..1a90dcfa0a
--- /dev/null
+++ b/devtools/client/debugger/src/actions/tracing.js
@@ -0,0 +1,44 @@
+/* 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 {
+ getIsJavascriptTracingEnabled,
+ getJavascriptTracingLogMethod,
+} from "../selectors/index";
+import { PROMISE } from "./utils/middleware/promise";
+
+/**
+ * Toggle ON/OFF Javascript tracing for all targets.
+ */
+export function toggleTracing() {
+ return async ({ dispatch, getState, client, panel }) => {
+ // For now, the UI can only toggle all the targets all at once.
+ const isTracingEnabled = getIsJavascriptTracingEnabled(getState());
+ const logMethod = getJavascriptTracingLogMethod(getState());
+
+ // Automatically open the split console when enabling tracing to the console
+ if (!isTracingEnabled && logMethod == "console") {
+ await panel.toolbox.openSplitConsole({ focusConsoleInput: false });
+ }
+
+ return dispatch({
+ type: "TOGGLE_TRACING",
+ [PROMISE]: client.toggleTracing(),
+ enabled: !isTracingEnabled,
+ });
+ };
+}
+
+/**
+ * Called when tracing is toggled ON/OFF on a particular thread.
+ */
+export function tracingToggled(thread, enabled) {
+ return ({ dispatch }) => {
+ dispatch({
+ type: "TRACING_TOGGLED",
+ thread,
+ enabled,
+ });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/ui.js b/devtools/client/debugger/src/actions/ui.js
new file mode 100644
index 0000000000..2424c658b8
--- /dev/null
+++ b/devtools/client/debugger/src/actions/ui.js
@@ -0,0 +1,282 @@
+/* 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 {
+ getActiveSearch,
+ getPaneCollapse,
+ getQuickOpenEnabled,
+ getSource,
+ getSourceTextContent,
+ getIgnoreListSourceUrls,
+ getSourceByURL,
+ getBreakpointsForSource,
+} from "../selectors/index";
+import { selectSource } from "../actions/sources/select";
+import {
+ getEditor,
+ getLocationsInViewport,
+ updateDocuments,
+} from "../utils/editor/index";
+import { blackboxSourceActorsForSource } from "./sources/blackbox";
+import { toggleBreakpoints } from "./breakpoints/index";
+import { copyToTheClipboard } from "../utils/clipboard";
+import { isFulfilled } from "../utils/async-value";
+import { primaryPaneTabs } from "../constants";
+
+export function setPrimaryPaneTab(tabName) {
+ return { type: "SET_PRIMARY_PANE_TAB", tabName };
+}
+
+export function closeActiveSearch() {
+ return {
+ type: "TOGGLE_ACTIVE_SEARCH",
+ value: null,
+ };
+}
+
+export function setActiveSearch(activeSearch) {
+ return ({ dispatch, getState }) => {
+ const activeSearchState = getActiveSearch(getState());
+ if (activeSearchState === activeSearch) {
+ return;
+ }
+
+ if (getQuickOpenEnabled(getState())) {
+ dispatch({ type: "CLOSE_QUICK_OPEN" });
+ }
+
+ // Open start panel if it was collapsed so the project search UI is visible
+ if (
+ activeSearch === primaryPaneTabs.PROJECT_SEARCH &&
+ getPaneCollapse(getState(), "start")
+ ) {
+ dispatch({
+ type: "TOGGLE_PANE",
+ position: "start",
+ paneCollapsed: false,
+ });
+ }
+
+ dispatch({
+ type: "TOGGLE_ACTIVE_SEARCH",
+ value: activeSearch,
+ });
+ };
+}
+
+export function toggleFrameworkGrouping(toggleValue) {
+ return ({ dispatch, getState }) => {
+ dispatch({
+ type: "TOGGLE_FRAMEWORK_GROUPING",
+ value: toggleValue,
+ });
+ };
+}
+
+export function toggleInlinePreview(toggleValue) {
+ return ({ dispatch, getState }) => {
+ dispatch({
+ type: "TOGGLE_INLINE_PREVIEW",
+ value: toggleValue,
+ });
+ };
+}
+
+export function toggleEditorWrapping(toggleValue) {
+ return ({ dispatch, getState }) => {
+ updateDocuments(doc => doc.cm.setOption("lineWrapping", toggleValue));
+
+ dispatch({
+ type: "TOGGLE_EDITOR_WRAPPING",
+ value: toggleValue,
+ });
+ };
+}
+
+export function toggleSourceMapsEnabled(toggleValue) {
+ return ({ dispatch, getState }) => {
+ dispatch({
+ type: "TOGGLE_SOURCE_MAPS_ENABLED",
+ value: toggleValue,
+ });
+ };
+}
+
+export function showSource(sourceId) {
+ return ({ dispatch, getState }) => {
+ const source = getSource(getState(), sourceId);
+ if (!source) {
+ return;
+ }
+
+ if (getPaneCollapse(getState(), "start")) {
+ dispatch({
+ type: "TOGGLE_PANE",
+ position: "start",
+ paneCollapsed: false,
+ });
+ }
+
+ dispatch(setPrimaryPaneTab("sources"));
+
+ dispatch(selectSource(source));
+ };
+}
+
+export function togglePaneCollapse(position, paneCollapsed) {
+ return ({ dispatch, getState }) => {
+ const prevPaneCollapse = getPaneCollapse(getState(), position);
+ if (prevPaneCollapse === paneCollapsed) {
+ return;
+ }
+
+ // Set active search to null when closing start panel if project search was active
+ if (
+ position === "start" &&
+ paneCollapsed &&
+ getActiveSearch(getState()) === primaryPaneTabs.PROJECT_SEARCH
+ ) {
+ dispatch(closeActiveSearch());
+ }
+
+ dispatch({
+ type: "TOGGLE_PANE",
+ position,
+ paneCollapsed,
+ });
+ };
+}
+
+/**
+ * Highlight one or many lines in CodeMirror for a given source.
+ *
+ * @param {Object} location
+ * @param {String} location.sourceId
+ * The precise source to highlight.
+ * @param {Number} location.start
+ * The 1-based index of first line to highlight.
+ * @param {Number} location.end
+ * The 1-based index of last line to highlight.
+ */
+export function highlightLineRange(location) {
+ return {
+ type: "HIGHLIGHT_LINES",
+ location,
+ };
+}
+
+export function flashLineRange(location) {
+ return ({ dispatch }) => {
+ dispatch(highlightLineRange(location));
+ setTimeout(() => dispatch(clearHighlightLineRange()), 200);
+ };
+}
+
+export function clearHighlightLineRange() {
+ return {
+ type: "CLEAR_HIGHLIGHT_LINES",
+ };
+}
+
+export function openConditionalPanel(location, log = false) {
+ if (!location) {
+ return null;
+ }
+
+ return {
+ type: "OPEN_CONDITIONAL_PANEL",
+ location,
+ log,
+ };
+}
+
+export function closeConditionalPanel() {
+ return {
+ type: "CLOSE_CONDITIONAL_PANEL",
+ };
+}
+
+export function updateViewport() {
+ return {
+ type: "SET_VIEWPORT",
+ viewport: getLocationsInViewport(getEditor()),
+ };
+}
+
+export function updateCursorPosition(cursorPosition) {
+ return { type: "SET_CURSOR_POSITION", cursorPosition };
+}
+
+export function setOrientation(orientation) {
+ return { type: "SET_ORIENTATION", orientation };
+}
+
+export function setSearchOptions(searchKey, searchOptions) {
+ return { type: "SET_SEARCH_OPTIONS", searchKey, searchOptions };
+}
+
+export function copyToClipboard(location) {
+ return ({ dispatch, getState }) => {
+ const content = getSourceTextContent(getState(), location);
+ if (content && isFulfilled(content) && content.value.type === "text") {
+ copyToTheClipboard(content.value.value);
+ }
+ };
+}
+
+export function setJavascriptTracingLogMethod(value) {
+ return {
+ type: "SET_JAVASCRIPT_TRACING_LOG_METHOD",
+ value,
+ };
+}
+
+export function toggleJavascriptTracingValues() {
+ return {
+ type: "TOGGLE_JAVASCRIPT_TRACING_VALUES",
+ };
+}
+
+export function toggleJavascriptTracingOnNextInteraction() {
+ return {
+ type: "TOGGLE_JAVASCRIPT_TRACING_ON_NEXT_INTERACTION",
+ };
+}
+
+export function toggleJavascriptTracingFunctionReturn() {
+ return {
+ type: "TOGGLE_JAVASCRIPT_TRACING_FUNCTION_RETURN",
+ };
+}
+
+export function toggleJavascriptTracingOnNextLoad() {
+ return {
+ type: "TOGGLE_JAVASCRIPT_TRACING_ON_NEXT_LOAD",
+ };
+}
+
+export function setHideOrShowIgnoredSources(shouldHide) {
+ return ({ dispatch, getState }) => {
+ dispatch({ type: "HIDE_IGNORED_SOURCES", shouldHide });
+ };
+}
+
+export function toggleSourceMapIgnoreList(shouldEnable) {
+ return async thunkArgs => {
+ const { dispatch, getState } = thunkArgs;
+ const ignoreListSourceUrls = getIgnoreListSourceUrls(getState());
+ // Blackbox the source actors on the server
+ for (const url of ignoreListSourceUrls) {
+ const source = getSourceByURL(getState(), url);
+ await blackboxSourceActorsForSource(thunkArgs, source, shouldEnable);
+ // Disable breakpoints in sources on the ignore list
+ const breakpoints = getBreakpointsForSource(getState(), source);
+ await dispatch(toggleBreakpoints(shouldEnable, breakpoints));
+ }
+ await dispatch({
+ type: "ENABLE_SOURCEMAP_IGNORELIST",
+ shouldEnable,
+ });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/utils/create-store.js b/devtools/client/debugger/src/actions/utils/create-store.js
new file mode 100644
index 0000000000..5de4fa74d0
--- /dev/null
+++ b/devtools/client/debugger/src/actions/utils/create-store.js
@@ -0,0 +1,75 @@
+/* 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/>. */
+
+/* global window */
+
+/**
+ * Redux store utils
+ * @module utils/create-store
+ */
+
+import {
+ createStore,
+ applyMiddleware,
+} from "devtools/client/shared/vendor/redux";
+import { waitUntilService } from "./middleware/wait-service";
+import { log } from "./middleware/log";
+import { promise } from "./middleware/promise";
+import { thunk } from "./middleware/thunk";
+import { timing } from "./middleware/timing";
+import { context } from "./middleware/context";
+
+/**
+ * @memberof utils/create-store
+ * @static
+ */
+
+/**
+ * This creates a dispatcher with all the standard middleware in place
+ * that all code requires. It can also be optionally configured in
+ * various ways, such as logging and recording.
+ *
+ * @param {object} opts:
+ * - log: log all dispatched actions to console
+ * - history: an array to store every action in. Should only be
+ * used in tests.
+ * - middleware: array of middleware to be included in the redux store
+ * @memberof utils/create-store
+ * @static
+ */
+const configureStore = (opts = {}) => {
+ const middleware = [
+ thunk(opts.makeThunkArgs),
+ context,
+ promise,
+
+ // Order is important: services must go last as they always
+ // operate on "already transformed" actions. Actions going through
+ // them shouldn't have any special fields like promises, they
+ // should just be normal JSON objects.
+ waitUntilService,
+ ];
+
+ if (opts.middleware) {
+ opts.middleware.forEach(fn => middleware.push(fn));
+ }
+
+ if (opts.log) {
+ middleware.push(log);
+ }
+
+ if (opts.timing) {
+ middleware.push(timing);
+ }
+
+ // Hook in the redux devtools browser extension if it exists
+ const devtoolsExt =
+ typeof window === "object" && window.devToolsExtension
+ ? window.devToolsExtension()
+ : f => f;
+
+ return applyMiddleware(...middleware)(devtoolsExt(createStore));
+};
+
+export default configureStore;
diff --git a/devtools/client/debugger/src/actions/utils/middleware/context.js b/devtools/client/debugger/src/actions/utils/middleware/context.js
new file mode 100644
index 0000000000..00711a8c3f
--- /dev/null
+++ b/devtools/client/debugger/src/actions/utils/middleware/context.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 {
+ validateNavigateContext,
+ validateContext,
+ validateSelectedFrame,
+ validateBreakpoint,
+ validateSource,
+ validateSourceActor,
+ validateThreadFrames,
+ validateFrame,
+} from "../../../utils/context";
+
+function validateActionContext(getState, action) {
+ if (action.type == "COMMAND" && action.status == "done") {
+ // The thread will have resumed execution since the action was initiated,
+ // so just make sure we haven't navigated.
+ validateNavigateContext(getState(), action.cx);
+ return;
+ }
+
+ // Validate using all available information in the context.
+ validateContext(getState(), action.cx);
+}
+
+// Middleware which looks for actions that have a cx property and ignores
+// them if the context is no longer valid.
+function context({ dispatch, getState }) {
+ return next => action => {
+ if ("cx" in action) {
+ validateActionContext(getState, action);
+ }
+
+ // Validate actions specific to a Source object.
+ // This will throw if the source has been removed,
+ // i.e. when the source has been removed from all the threads where it existed.
+ if ("source" in action) {
+ validateSource(getState(), action.source);
+ }
+
+ // Validate actions specific to a Source Actor object.
+ // This will throw if the source actor has been removed,
+ // i.e. when the source actor's thread has been removed.
+ if ("sourceActor" in action) {
+ validateSourceActor(getState(), action.sourceActor);
+ }
+
+ // Similar to sourceActor assertion, but with a distinct attribute name
+ if ("generatedSourceActor" in action) {
+ validateSourceActor(getState(), action.generatedSourceActor);
+ }
+
+ // Validate actions specific to a given breakpoint.
+ // This will throw if the breakpoint's location is obsolete.
+ // i.e. when the related source has been removed.
+ if ("breakpoint" in action) {
+ validateBreakpoint(getState(), action.breakpoint);
+ }
+
+ // Validate actions specific to the currently selected paused frame.
+ // It will throw if we resumed or moved to another frame in the call stack.
+ //
+ // Ignore falsy selectedFrame as sometimes it can be null
+ // for expression actions.
+ if (action.selectedFrame) {
+ validateSelectedFrame(getState(), action.selectedFrame);
+ }
+
+ // Validate actions specific to a given pause location.
+ // This will throw if we resumed or paused in another location.
+ // Compared to selected frame, this would not throw if we moved to another frame in the call stack.
+ if ("thread" in action && "frames" in action) {
+ validateThreadFrames(getState(), action.thread, action.frames);
+ }
+
+ // Validate actions specific to a given frame while being paused.
+ // This will throw if we resumed or paused in another location.
+ // But compared to selected frame, this would not throw if we moved to another frame in the call stack.
+ // This ends up being similar to "pause location" case, but with different arguments.
+ if ("frame" in action) {
+ validateFrame(getState(), action.frame);
+ }
+
+ return next(action);
+ };
+}
+
+export { context };
diff --git a/devtools/client/debugger/src/actions/utils/middleware/log.js b/devtools/client/debugger/src/actions/utils/middleware/log.js
new file mode 100644
index 0000000000..b9592ce22c
--- /dev/null
+++ b/devtools/client/debugger/src/actions/utils/middleware/log.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/>. */
+
+import flags from "devtools/shared/flags";
+import { prefs } from "../../../utils/prefs";
+
+const ignoreList = [
+ "ADD_BREAKPOINT_POSITIONS",
+ "SET_SYMBOLS",
+ "OUT_OF_SCOPE_LOCATIONS",
+ "MAP_SCOPES",
+ "MAP_FRAMES",
+ "ADD_SCOPES",
+ "IN_SCOPE_LINES",
+ "REMOVE_BREAKPOINT",
+ "NODE_PROPERTIES_LOADED",
+ "SET_FOCUSED_SOURCE_ITEM",
+ "NODE_EXPAND",
+ "IN_SCOPE_LINES",
+ "SET_PREVIEW",
+];
+
+function cloneAction(action) {
+ action = action || {};
+ action = { ...action };
+
+ // ADD_TAB, ...
+ if (action.source?.text) {
+ const source = { ...action.source, text: "" };
+ action.source = source;
+ }
+
+ if (action.sources) {
+ const sources = action.sources.slice(0, 20).map(source => {
+ const url = !source.url || source.url.includes("data:") ? "" : source.url;
+ return { ...source, url };
+ });
+ action.sources = sources;
+ }
+
+ // LOAD_SOURCE_TEXT
+ if (action.text) {
+ action.text = "";
+ }
+
+ if (action.value?.text) {
+ const value = { ...action.value, text: "" };
+ action.value = value;
+ }
+
+ return action;
+}
+
+function formatPause(pause) {
+ return {
+ ...pause,
+ pauseInfo: { why: pause.why },
+ scopes: [],
+ loadedObjects: [],
+ };
+}
+
+function serializeAction(action) {
+ try {
+ action = cloneAction(action);
+ if (ignoreList.includes(action.type)) {
+ action = {};
+ }
+
+ if (action.type === "PAUSED") {
+ action = formatPause(action);
+ }
+
+ const serializer = function (key, value) {
+ // Serialize Object/LongString fronts
+ if (value?.getGrip) {
+ return value.getGrip();
+ }
+ return value;
+ };
+
+ // dump(`> ${action.type}...\n ${JSON.stringify(action, serializer)}\n`);
+ return JSON.stringify(action, serializer);
+ } catch (e) {
+ console.error(e);
+ return "";
+ }
+}
+
+/**
+ * A middleware that logs all actions coming through the system
+ * to the console.
+ */
+export function log({ dispatch, getState }) {
+ return next => action => {
+ const asyncMsg = !action.status ? "" : `[${action.status}]`;
+
+ if (prefs.logActions) {
+ if (flags.testing) {
+ dump(
+ `[ACTION] ${action.type} ${asyncMsg} - ${serializeAction(action)}\n`
+ );
+ } else {
+ console.log(action, asyncMsg);
+ }
+ }
+
+ next(action);
+ };
+}
diff --git a/devtools/client/debugger/src/actions/utils/middleware/moz.build b/devtools/client/debugger/src/actions/utils/middleware/moz.build
new file mode 100644
index 0000000000..f46a0bb725
--- /dev/null
+++ b/devtools/client/debugger/src/actions/utils/middleware/moz.build
@@ -0,0 +1,15 @@
+# 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(
+ "context.js",
+ "log.js",
+ "promise.js",
+ "thunk.js",
+ "timing.js",
+ "wait-service.js",
+)
diff --git a/devtools/client/debugger/src/actions/utils/middleware/promise.js b/devtools/client/debugger/src/actions/utils/middleware/promise.js
new file mode 100644
index 0000000000..52054a1fcc
--- /dev/null
+++ b/devtools/client/debugger/src/actions/utils/middleware/promise.js
@@ -0,0 +1,61 @@
+/* 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 { executeSoon } from "../../../utils/DevToolsUtils";
+
+import { pending, rejected, fulfilled } from "../../../utils/async-value";
+export function asyncActionAsValue(action) {
+ if (action.status === "start") {
+ return pending();
+ }
+ if (action.status === "error") {
+ return rejected(action.error);
+ }
+ return fulfilled(action.value);
+}
+
+let seqIdVal = 1;
+
+function seqIdGen() {
+ return seqIdVal++;
+}
+
+function promiseMiddleware({ dispatch, getState }) {
+ return next => action => {
+ if (!(PROMISE in action)) {
+ return next(action);
+ }
+
+ const seqId = seqIdGen().toString();
+ const { [PROMISE]: promiseInst, ...originalActionProperties } = action;
+
+ // Create a new action that doesn't have the promise field and has
+ // the `seqId` field that represents the sequence id
+ action = { ...originalActionProperties, seqId };
+
+ dispatch({ ...action, status: "start" });
+
+ // Return the promise so action creators can still compose if they
+ // want to.
+ return Promise.resolve(promiseInst)
+ .finally(() => new Promise(resolve => executeSoon(resolve)))
+ .then(
+ value => {
+ dispatch({ ...action, status: "done", value: value });
+ return value;
+ },
+ error => {
+ dispatch({
+ ...action,
+ status: "error",
+ error: error.message || error,
+ });
+ return Promise.reject(error);
+ }
+ );
+ };
+}
+
+export const PROMISE = "@@dispatch/promise";
+export { promiseMiddleware as promise };
diff --git a/devtools/client/debugger/src/actions/utils/middleware/thunk.js b/devtools/client/debugger/src/actions/utils/middleware/thunk.js
new file mode 100644
index 0000000000..fba17d516c
--- /dev/null
+++ b/devtools/client/debugger/src/actions/utils/middleware/thunk.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/>. */
+
+/**
+ * A middleware that allows thunks (functions) to be dispatched. If
+ * it's a thunk, it is called with an argument that contains
+ * `dispatch`, `getState`, and any additional args passed in via the
+ * middleware constructure. This allows the action to create multiple
+ * actions (most likely asynchronously).
+ */
+export function thunk(makeArgs) {
+ return ({ dispatch, getState }) => {
+ const args = { dispatch, getState };
+
+ return next => action => {
+ return typeof action === "function"
+ ? action(makeArgs ? makeArgs(args, getState()) : args)
+ : next(action);
+ };
+ };
+}
diff --git a/devtools/client/debugger/src/actions/utils/middleware/timing.js b/devtools/client/debugger/src/actions/utils/middleware/timing.js
new file mode 100644
index 0000000000..d0bfa05977
--- /dev/null
+++ b/devtools/client/debugger/src/actions/utils/middleware/timing.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/>. */
+
+/**
+ * Redux middleware that sets performance markers for all actions such that they
+ * will appear in performance tooling under the User Timing API
+ */
+
+const mark = window.performance?.mark
+ ? window.performance.mark.bind(window.performance)
+ : a => {};
+
+const measure = window.performance?.measure
+ ? window.performance.measure.bind(window.performance)
+ : (a, b, c) => {};
+
+export function timing(store) {
+ return next => action => {
+ mark(`${action.type}_start`);
+ const result = next(action);
+ mark(`${action.type}_end`);
+ measure(`${action.type}`, `${action.type}_start`, `${action.type}_end`);
+ return result;
+ };
+}
diff --git a/devtools/client/debugger/src/actions/utils/middleware/wait-service.js b/devtools/client/debugger/src/actions/utils/middleware/wait-service.js
new file mode 100644
index 0000000000..337df7e336
--- /dev/null
+++ b/devtools/client/debugger/src/actions/utils/middleware/wait-service.js
@@ -0,0 +1,62 @@
+/* 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/>. */
+
+/**
+ * A middleware which acts like a service, because it is stateful
+ * and "long-running" in the background. It provides the ability
+ * for actions to install a function to be run once when a specific
+ * condition is met by an action coming through the system. Think of
+ * it as a thunk that blocks until the condition is met. Example:
+ *
+ * ```js
+ * const services = { WAIT_UNTIL: require('wait-service').NAME };
+ *
+ * { type: services.WAIT_UNTIL,
+ * predicate: action => action.type === "ADD_ITEM",
+ * run: (dispatch, getState, action) => {
+ * // Do anything here. You only need to accept the arguments
+ * // if you need them. `action` is the action that satisfied
+ * // the predicate.
+ * }
+ * }
+ * ```
+ */
+export const NAME = "@@service/waitUntil";
+
+export function waitUntilService({ dispatch, getState }) {
+ let pending = [];
+
+ function checkPending(action) {
+ const readyRequests = [];
+ const stillPending = [];
+
+ // Find the pending requests whose predicates are satisfied with
+ // this action. Wait to run the requests until after we update the
+ // pending queue because the request handler may synchronously
+ // dispatch again and run this service (that use case is
+ // completely valid).
+ for (const request of pending) {
+ if (request.predicate(action)) {
+ readyRequests.push(request);
+ } else {
+ stillPending.push(request);
+ }
+ }
+
+ pending = stillPending;
+ for (const request of readyRequests) {
+ request.run(dispatch, getState, action);
+ }
+ }
+
+ return next => action => {
+ if (action.type === NAME) {
+ pending.push(action);
+ return null;
+ }
+ const result = next(action);
+ checkPending(action);
+ return result;
+ };
+}
diff --git a/devtools/client/debugger/src/actions/utils/moz.build b/devtools/client/debugger/src/actions/utils/moz.build
new file mode 100644
index 0000000000..08a43a218c
--- /dev/null
+++ b/devtools/client/debugger/src/actions/utils/moz.build
@@ -0,0 +1,12 @@
+# 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 += [
+ "middleware",
+]
+
+CompiledModules(
+ "create-store.js",
+)
diff --git a/devtools/client/debugger/src/client/README.md b/devtools/client/debugger/src/client/README.md
new file mode 100644
index 0000000000..4681a4e15e
--- /dev/null
+++ b/devtools/client/debugger/src/client/README.md
@@ -0,0 +1,47 @@
+# DevTools Client
+
+The DevTools client is responsible for managing the communication between the
+client application and JS server.
+
+- When the server sends a notification to the client, the client receives an
+ "event" and notifies the application via redux actions.
+- When the application, wants to send a command to the server, it invokes
+ "commands" in the client.
+
+The Debugger supports a Firefox and a Chrome client, which lets it attach and
+debug Firefox, Chrome, and Node contexts. The clients are defined in
+`src/client` and have an `onConnect` function, and a `commands` and `events`
+module.
+
+Both clients implement client adapters for translating commands and events into
+JSON packets. The chrome client debugger adapter is defined in
+[chrome-remote-interface][chrome-remote-interface]. The Firefox client is maintained in
+[devtools-client.js][devtools-client.js].
+
+## Firefox
+
+### Remote Debugger Protocol
+
+The [Remote Debugger Protocol][protocol] specifies the client / server API.
+
+### Interrupt
+
+When the client wants to add a breakpoint, it avoids race conditions by doing
+temporary pauses called interrupts.
+
+We want to do these interrupts transparently, so we've decided that the client
+should not notify the application that the thread has been paused or resumed.
+
+[protocol]: https://searchfox.org/mozilla-central/source/devtools/docs/backend/protocol.md
+[devtools-client.js]: https://searchfox.org/mozilla-central/source/devtools/client/devtools-client.js
+
+## Chrome
+
+### Chrome Debugger Protocol
+
+The chrome debugger protocol is available [here][devtools-protocol-viewer]. And
+is maintained in the devtools-protocol [repo][devtools-protocol-gh].
+
+[chrome-remote-interface]: https://github.com/cyrus-and/chrome-remote-interface
+[devtools-protocol-viewer]: https://chromedevtools.github.io/devtools-protocol/
+[devtools-protocol-gh]: https://github.com/ChromeDevTools/devtools-protocol
diff --git a/devtools/client/debugger/src/client/firefox.js b/devtools/client/debugger/src/client/firefox.js
new file mode 100644
index 0000000000..6b76d6d175
--- /dev/null
+++ b/devtools/client/debugger/src/client/firefox.js
@@ -0,0 +1,206 @@
+/* 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 { setupCommands, clientCommands } from "./firefox/commands";
+import { setupCreate, createPause } from "./firefox/create";
+import { features } from "../utils/prefs";
+
+import { recordEvent } from "../utils/telemetry";
+import sourceQueue from "../utils/source-queue";
+
+let actions;
+let commands;
+let targetCommand;
+let resourceCommand;
+
+export async function onConnect(_commands, _resourceCommand, _actions, store) {
+ actions = _actions;
+ commands = _commands;
+ targetCommand = _commands.targetCommand;
+ resourceCommand = _resourceCommand;
+
+ setupCommands(commands);
+ setupCreate({ store });
+
+ sourceQueue.initialize(actions);
+
+ const { descriptorFront } = commands;
+
+ // For tab, browser and webextension toolboxes, we want to enable watching for
+ // worker targets as soon as the debugger is opened.
+ // And also for service workers, if the related experimental feature is enabled
+ if (
+ descriptorFront.isTabDescriptor ||
+ descriptorFront.isWebExtensionDescriptor ||
+ descriptorFront.isBrowserProcessDescriptor
+ ) {
+ targetCommand.listenForWorkers = true;
+ if (descriptorFront.isLocalTab && features.windowlessServiceWorkers) {
+ targetCommand.listenForServiceWorkers = true;
+ }
+ await targetCommand.startListening();
+ }
+
+ const options = {
+ // `pauseWorkersUntilAttach` is one option set when the debugger panel is opened rather that from the toolbox.
+ // The reason is to support early breakpoints in workers, which will force the workers to pause
+ // and later on (when TargetMixin.attachThread is called) resume worker execution, after passing the breakpoints.
+ // We only observe workers when the debugger panel is opened (see the few lines before and listenForWorkers = true).
+ // So if we were passing `pauseWorkersUntilAttach=true` from the toolbox code, workers would freeze as we would not watch
+ // for their targets and not resume them.
+ pauseWorkersUntilAttach: true,
+
+ // Bug 1719615 - Immediately turn on WASM debugging when the debugger opens.
+ // We avoid enabling that as soon as DevTools open as WASM generates different kind of machine code
+ // with debugging instruction which significantly increase the memory usage.
+ observeWasm: true,
+ };
+ await commands.threadConfigurationCommand.updateConfiguration(options);
+
+ await targetCommand.watchTargets({
+ types: targetCommand.ALL_TYPES,
+ onAvailable: onTargetAvailable,
+ onDestroyed: onTargetDestroyed,
+ });
+
+ // Use independant listeners for SOURCE and THREAD_STATE in order to ease
+ // doing batching and notify about a set of SOURCE's in one redux action.
+ await resourceCommand.watchResources([resourceCommand.TYPES.SOURCE], {
+ onAvailable: onSourceAvailable,
+ });
+ await resourceCommand.watchResources([resourceCommand.TYPES.THREAD_STATE], {
+ onAvailable: onThreadStateAvailable,
+ });
+ await resourceCommand.watchResources([resourceCommand.TYPES.JSTRACER_STATE], {
+ onAvailable: onTracingStateAvailable,
+ });
+
+ await resourceCommand.watchResources([resourceCommand.TYPES.ERROR_MESSAGE], {
+ onAvailable: actions.addExceptionFromResources,
+ });
+ await resourceCommand.watchResources([resourceCommand.TYPES.DOCUMENT_EVENT], {
+ onAvailable: onDocumentEventAvailable,
+ // we only care about future events for DOCUMENT_EVENT
+ ignoreExistingResources: true,
+ });
+}
+
+export function onDisconnect() {
+ targetCommand.unwatchTargets({
+ types: targetCommand.ALL_TYPES,
+ onAvailable: onTargetAvailable,
+ onDestroyed: onTargetDestroyed,
+ });
+ resourceCommand.unwatchResources([resourceCommand.TYPES.SOURCE], {
+ onAvailable: onSourceAvailable,
+ });
+ resourceCommand.unwatchResources([resourceCommand.TYPES.THREAD_STATE], {
+ onAvailable: onThreadStateAvailable,
+ });
+ resourceCommand.unwatchResources([resourceCommand.TYPES.JSTRACER_STATE], {
+ onAvailable: onTracingStateAvailable,
+ });
+ resourceCommand.unwatchResources([resourceCommand.TYPES.ERROR_MESSAGE], {
+ onAvailable: actions.addExceptionFromResources,
+ });
+ resourceCommand.unwatchResources([resourceCommand.TYPES.DOCUMENT_EVENT], {
+ onAvailable: onDocumentEventAvailable,
+ });
+ sourceQueue.clear();
+}
+
+async function onTargetAvailable({ targetFront, isTargetSwitching }) {
+ const isBrowserToolbox = commands.descriptorFront.isBrowserProcessDescriptor;
+ const isNonTopLevelFrameTarget =
+ !targetFront.isTopLevel &&
+ targetFront.targetType === targetCommand.TYPES.FRAME;
+
+ if (isBrowserToolbox && isNonTopLevelFrameTarget) {
+ // In the BrowserToolbox, non-top-level frame targets are already
+ // debugged via content-process targets.
+ // Do not attach the thread here, as it was already done by the
+ // corresponding content-process target.
+ return;
+ }
+
+ if (!targetFront.isTopLevel) {
+ await actions.addTarget(targetFront);
+ return;
+ }
+
+ // At this point, we expect the target and its thread to be attached.
+ const { threadFront } = targetFront;
+ if (!threadFront) {
+ console.error("The thread for", targetFront, "isn't attached.");
+ return;
+ }
+
+ // Retrieve possible event listener breakpoints
+ actions.getEventListenerBreakpointTypes().catch(e => console.error(e));
+
+ // Initialize the event breakpoints on the thread up front so that
+ // they are active once attached.
+ actions.addEventListenerBreakpoints([]).catch(e => console.error(e));
+
+ await actions.addTarget(targetFront);
+}
+
+function onTargetDestroyed({ targetFront }) {
+ actions.removeTarget(targetFront);
+}
+
+async function onSourceAvailable(sources) {
+ await actions.newGeneratedSources(sources);
+}
+
+async function onThreadStateAvailable(resources) {
+ for (const resource of resources) {
+ if (resource.targetFront.isDestroyed()) {
+ continue;
+ }
+ const threadFront = await resource.targetFront.getFront("thread");
+ if (resource.state == "paused") {
+ const pause = await createPause(threadFront.actorID, resource);
+ await actions.paused(pause);
+ recordEvent("pause", { reason: resource.why.type });
+ } else if (resource.state == "resumed") {
+ await actions.resumed(threadFront.actorID);
+ }
+ }
+}
+
+async function onTracingStateAvailable(resources) {
+ for (const resource of resources) {
+ if (resource.targetFront.isDestroyed()) {
+ continue;
+ }
+ const threadFront = await resource.targetFront.getFront("thread");
+ await actions.tracingToggled(threadFront.actor, resource.enabled);
+ }
+}
+
+function onDocumentEventAvailable(events) {
+ for (const event of events) {
+ // Only consider top level document, and ignore remote iframes top document
+ if (!event.targetFront.isTopLevel) continue;
+ // The browser toolbox debugger doesn't support the iframe dropdown.
+ // you will always see all the sources of all targets of your debugging context.
+ //
+ // But still allow it to clear the debugger when reloading the addon, or when
+ // switching between fallback document and other addon document.
+ if (
+ event.isFrameSwitching &&
+ !commands.descriptorFront.isWebExtensionDescriptor
+ ) {
+ continue;
+ }
+ if (event.name == "will-navigate") {
+ actions.willNavigate({ url: event.newURI });
+ } else if (event.name == "dom-complete") {
+ actions.navigated();
+ }
+ }
+}
+
+export { clientCommands };
diff --git a/devtools/client/debugger/src/client/firefox/commands.js b/devtools/client/debugger/src/client/firefox/commands.js
new file mode 100644
index 0000000000..5ef3ba27d0
--- /dev/null
+++ b/devtools/client/debugger/src/client/firefox/commands.js
@@ -0,0 +1,515 @@
+/* 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 { createFrame } from "./create";
+import { makeBreakpointServerLocationId } from "../../utils/breakpoint/index";
+
+import Reps from "devtools/client/shared/components/reps/index";
+
+let commands;
+let breakpoints;
+
+// The maximal number of stackframes to retrieve when pausing
+const CALL_STACK_PAGE_SIZE = 1000;
+
+function setupCommands(innerCommands) {
+ commands = innerCommands;
+ breakpoints = {};
+}
+
+function currentTarget() {
+ return commands.targetCommand.targetFront;
+}
+
+function currentThreadFront() {
+ return currentTarget().threadFront;
+}
+
+/**
+ * Create an object front for the passed grip
+ *
+ * @param {Object} grip
+ * @param {Object} frame: An optional frame that will manage the created object front.
+ * if not passed, the current thread front will manage the object.
+ * @returns {ObjectFront}
+ */
+function createObjectFront(grip, frame) {
+ if (!grip.actor) {
+ throw new Error("Actor is missing");
+ }
+ const threadFront = frame?.thread
+ ? lookupThreadFront(frame.thread)
+ : currentThreadFront();
+ const frameFront = frame ? threadFront.getActorByID(frame.id) : null;
+ return commands.client.createObjectFront(grip, threadFront, frameFront);
+}
+
+async function loadObjectProperties(root, threadActorID) {
+ const { utils } = Reps.objectInspector;
+ const properties = await utils.loadProperties.loadItemProperties(
+ root,
+ commands.client,
+ undefined,
+ threadActorID
+ );
+ return utils.node.getChildren({
+ item: root,
+ loadedProperties: new Map([[root.path, properties]]),
+ });
+}
+
+function releaseActor(actor) {
+ if (!actor) {
+ return Promise.resolve();
+ }
+ const objFront = commands.client.getFrontByID(actor);
+
+ if (!objFront) {
+ return Promise.resolve();
+ }
+
+ return objFront.release().catch(() => {});
+}
+
+function lookupTarget(thread) {
+ if (thread == currentThreadFront().actor) {
+ return currentTarget();
+ }
+
+ const targets = commands.targetCommand.getAllTargets(
+ commands.targetCommand.ALL_TYPES
+ );
+ return targets.find(target => target.targetForm.threadActor == thread);
+}
+
+function lookupThreadFront(thread) {
+ const target = lookupTarget(thread);
+ return target.threadFront;
+}
+
+function listThreadFronts() {
+ const targets = commands.targetCommand.getAllTargets(
+ commands.targetCommand.ALL_TYPES
+ );
+ return targets.map(target => target.threadFront).filter(front => !!front);
+}
+
+function forEachThread(iteratee) {
+ // We have to be careful here to atomically initiate the operation on every
+ // thread, with no intervening await. Otherwise, other code could run and
+ // trigger additional thread operations. Requests on server threads will
+ // resolve in FIFO order, and this could result in client and server state
+ // going out of sync.
+
+ const promises = listThreadFronts().map(
+ // If a thread shuts down while sending the message then it will
+ // throw. Ignore these exceptions.
+ t => iteratee(t).catch(e => console.log(e))
+ );
+
+ return Promise.all(promises);
+}
+
+async function toggleTracing() {
+ return commands.tracerCommand.toggle();
+}
+
+function resume(thread, frameId) {
+ return lookupThreadFront(thread).resume();
+}
+
+function stepIn(thread, frameId) {
+ return lookupThreadFront(thread).stepIn(frameId);
+}
+
+function stepOver(thread, frameId) {
+ return lookupThreadFront(thread).stepOver(frameId);
+}
+
+function stepOut(thread, frameId) {
+ return lookupThreadFront(thread).stepOut(frameId);
+}
+
+function restart(thread, frameId) {
+ return lookupThreadFront(thread).restart(frameId);
+}
+
+function breakOnNext(thread) {
+ return lookupThreadFront(thread).breakOnNext();
+}
+
+async function sourceContents({ actor, thread }) {
+ const sourceThreadFront = lookupThreadFront(thread);
+ const sourceFront = sourceThreadFront.source({ actor });
+ const { source, contentType } = await sourceFront.source();
+ return { source, contentType };
+}
+
+async function setXHRBreakpoint(path, method) {
+ const hasWatcherSupport = commands.targetCommand.hasTargetWatcherSupport();
+ if (!hasWatcherSupport) {
+ // Without watcher support, forward setXHRBreakpoint to all threads.
+ await forEachThread(thread => thread.setXHRBreakpoint(path, method));
+ return;
+ }
+ const breakpointsFront =
+ await commands.targetCommand.watcherFront.getBreakpointListActor();
+ await breakpointsFront.setXHRBreakpoint(path, method);
+}
+
+async function removeXHRBreakpoint(path, method) {
+ const hasWatcherSupport = commands.targetCommand.hasTargetWatcherSupport();
+ if (!hasWatcherSupport) {
+ // Without watcher support, forward removeXHRBreakpoint to all threads.
+ await forEachThread(thread => thread.removeXHRBreakpoint(path, method));
+ return;
+ }
+ const breakpointsFront =
+ await commands.targetCommand.watcherFront.getBreakpointListActor();
+ await breakpointsFront.removeXHRBreakpoint(path, method);
+}
+
+export function toggleJavaScriptEnabled(enabled) {
+ return commands.targetConfigurationCommand.updateConfiguration({
+ javascriptEnabled: enabled,
+ });
+}
+
+async function addWatchpoint(object, property, label, watchpointType) {
+ if (!currentTarget().getTrait("watchpoints")) {
+ return;
+ }
+ const objectFront = createObjectFront(object);
+ await objectFront.addWatchpoint(property, label, watchpointType);
+}
+
+async function removeWatchpoint(object, property) {
+ if (!currentTarget().getTrait("watchpoints")) {
+ return;
+ }
+ const objectFront = createObjectFront(object);
+ await objectFront.removeWatchpoint(property);
+}
+
+function hasBreakpoint(location) {
+ return !!breakpoints[makeBreakpointServerLocationId(location)];
+}
+
+function getServerBreakpointsList() {
+ return Object.values(breakpoints);
+}
+
+async function setBreakpoint(location, options) {
+ const breakpoint = breakpoints[makeBreakpointServerLocationId(location)];
+ if (
+ breakpoint &&
+ JSON.stringify(breakpoint.options) == JSON.stringify(options)
+ ) {
+ return null;
+ }
+ breakpoints[makeBreakpointServerLocationId(location)] = { location, options };
+
+ // Map frontend options to a more restricted subset of what
+ // the server supports. For example frontend uses `hidden` attribute
+ // which isn't meant to be passed to the server.
+ // (note that protocol.js specification isn't enough to filter attributes,
+ // all primitive attributes will be passed as-is)
+ const serverOptions = {
+ condition: options.condition,
+ logValue: options.logValue,
+ };
+ const hasWatcherSupport = commands.targetCommand.hasTargetWatcherSupport();
+ if (!hasWatcherSupport) {
+ // Without watcher support, unconditionally forward setBreakpoint to all threads.
+ return forEachThread(async thread =>
+ thread.setBreakpoint(location, serverOptions)
+ );
+ }
+ const breakpointsFront =
+ await commands.targetCommand.watcherFront.getBreakpointListActor();
+ await breakpointsFront.setBreakpoint(location, serverOptions);
+
+ // Call setBreakpoint for threads linked to targets
+ // not managed by the watcher.
+ return forEachThread(async thread => {
+ if (
+ !commands.targetCommand.hasTargetWatcherSupport(
+ thread.targetFront.targetType
+ )
+ ) {
+ return thread.setBreakpoint(location, serverOptions);
+ }
+
+ return Promise.resolve();
+ });
+}
+
+async function removeBreakpoint(location) {
+ delete breakpoints[makeBreakpointServerLocationId(location)];
+
+ const hasWatcherSupport = commands.targetCommand.hasTargetWatcherSupport();
+ if (!hasWatcherSupport) {
+ // Without watcher support, unconditionally forward removeBreakpoint to all threads.
+ return forEachThread(async thread => thread.removeBreakpoint(location));
+ }
+ const breakpointsFront =
+ await commands.targetCommand.watcherFront.getBreakpointListActor();
+ await breakpointsFront.removeBreakpoint(location);
+
+ // Call removeBreakpoint for threads linked to targets
+ // not managed by the watcher.
+ return forEachThread(async thread => {
+ if (
+ !commands.targetCommand.hasTargetWatcherSupport(
+ thread.targetFront.targetType
+ )
+ ) {
+ return thread.removeBreakpoint(location);
+ }
+
+ return Promise.resolve();
+ });
+}
+
+async function evaluateExpressions(scripts, options) {
+ return Promise.all(scripts.map(script => evaluate(script, options)));
+}
+
+async function evaluate(script, { frameId, threadId } = {}) {
+ if (!currentTarget() || !script) {
+ return { result: null };
+ }
+
+ const selectedTargetFront = threadId ? lookupTarget(threadId) : null;
+
+ return commands.scriptCommand.execute(script, {
+ frameActor: frameId,
+ selectedTargetFront,
+ disableBreaks: true,
+ });
+}
+
+async function autocomplete(input, cursor, frameId) {
+ if (!currentTarget() || !input) {
+ return {};
+ }
+ const consoleFront = await currentTarget().getFront("console");
+ if (!consoleFront) {
+ return {};
+ }
+
+ return new Promise(resolve => {
+ consoleFront.autocomplete(
+ input,
+ cursor,
+ result => resolve(result),
+ frameId
+ );
+ });
+}
+
+function getProperties(thread, grip) {
+ const objClient = lookupThreadFront(thread).pauseGrip(grip);
+
+ return objClient.getPrototypeAndProperties().then(resp => {
+ const { ownProperties, safeGetterValues } = resp;
+ for (const name in safeGetterValues) {
+ const { enumerable, writable, getterValue } = safeGetterValues[name];
+ ownProperties[name] = { enumerable, writable, value: getterValue };
+ }
+ return resp;
+ });
+}
+
+async function getFrames(thread) {
+ const threadFront = lookupThreadFront(thread);
+ const response = await threadFront.getFrames(0, CALL_STACK_PAGE_SIZE);
+
+ return Promise.all(
+ response.frames.map((frame, i) => createFrame(thread, frame, i))
+ );
+}
+
+async function getFrameScopes(frame) {
+ const frameFront = lookupThreadFront(frame.thread).getActorByID(frame.id);
+ return frameFront.getEnvironment();
+}
+
+async function pauseOnDebuggerStatement(shouldPauseOnDebuggerStatement) {
+ await commands.threadConfigurationCommand.updateConfiguration({
+ shouldPauseOnDebuggerStatement,
+ });
+}
+
+async function pauseOnExceptions(
+ shouldPauseOnExceptions,
+ shouldPauseOnCaughtExceptions
+) {
+ await commands.threadConfigurationCommand.updateConfiguration({
+ pauseOnExceptions: shouldPauseOnExceptions,
+ ignoreCaughtExceptions: !shouldPauseOnCaughtExceptions,
+ });
+}
+
+async function blackBox(sourceActor, shouldBlackBox, ranges) {
+ const hasWatcherSupport = commands.targetCommand.hasTargetWatcherSupport();
+ if (hasWatcherSupport) {
+ const blackboxingFront =
+ await commands.targetCommand.watcherFront.getBlackboxingActor();
+ if (shouldBlackBox) {
+ await blackboxingFront.blackbox(sourceActor.url, ranges);
+ } else {
+ await blackboxingFront.unblackbox(sourceActor.url, ranges);
+ }
+ } else {
+ const sourceFront = currentThreadFront().source({
+ actor: sourceActor.actor,
+ });
+ // If there are no ranges, the whole source is being blackboxed
+ if (!ranges.length) {
+ await toggleBlackBoxSourceFront(sourceFront, shouldBlackBox);
+ return;
+ }
+ // Blackbox the specific ranges
+ for (const range of ranges) {
+ await toggleBlackBoxSourceFront(sourceFront, shouldBlackBox, range);
+ }
+ }
+}
+
+async function toggleBlackBoxSourceFront(sourceFront, shouldBlackBox, range) {
+ if (shouldBlackBox) {
+ await sourceFront.blackBox(range);
+ } else {
+ await sourceFront.unblackBox(range);
+ }
+}
+
+async function setSkipPausing(shouldSkip) {
+ await commands.threadConfigurationCommand.updateConfiguration({
+ skipBreakpoints: shouldSkip,
+ });
+}
+
+async function setEventListenerBreakpoints(ids) {
+ const hasWatcherSupport = commands.targetCommand.hasTargetWatcherSupport();
+ if (!hasWatcherSupport) {
+ await forEachThread(thread => thread.setActiveEventBreakpoints(ids));
+ return;
+ }
+ const breakpointListFront =
+ await commands.targetCommand.watcherFront.getBreakpointListActor();
+ await breakpointListFront.setActiveEventBreakpoints(ids);
+}
+
+async function getEventListenerBreakpointTypes() {
+ return currentThreadFront().getAvailableEventBreakpoints();
+}
+
+function pauseGrip(thread, func) {
+ return lookupThreadFront(thread).pauseGrip(func);
+}
+
+async function toggleEventLogging(logEventBreakpoints) {
+ await commands.threadConfigurationCommand.updateConfiguration({
+ logEventBreakpoints,
+ });
+}
+
+function getMainThread() {
+ return currentThreadFront().actor;
+}
+
+async function getSourceActorBreakpointPositions({ thread, actor }, range) {
+ const sourceThreadFront = lookupThreadFront(thread);
+ const sourceFront = sourceThreadFront.source({ actor });
+ return sourceFront.getBreakpointPositionsCompressed(range);
+}
+
+async function getSourceActorBreakableLines({ thread, actor }) {
+ let actorLines = [];
+ try {
+ const sourceThreadFront = lookupThreadFront(thread);
+ const sourceFront = sourceThreadFront.source({ actor });
+ actorLines = await sourceFront.getBreakableLines();
+ } catch (e) {
+ // Exceptions could be due to the target thread being shut down.
+ console.warn(`getSourceActorBreakableLines failed: ${e}`);
+ }
+
+ return actorLines;
+}
+
+function getFrontByID(actorID) {
+ return commands.client.getFrontByID(actorID);
+}
+
+function fetchAncestorFramePositions(index) {
+ currentThreadFront().fetchAncestorFramePositions(index);
+}
+
+async function setOverride(url, path) {
+ const hasWatcherSupport = commands.targetCommand.hasTargetWatcherSupport();
+ if (hasWatcherSupport) {
+ const networkFront =
+ await commands.targetCommand.watcherFront.getNetworkParentActor();
+ return networkFront.override(url, path);
+ }
+ return null;
+}
+
+async function removeOverride(url) {
+ const hasWatcherSupport = commands.targetCommand.hasTargetWatcherSupport();
+ if (hasWatcherSupport) {
+ const networkFront =
+ await commands.targetCommand.watcherFront.getNetworkParentActor();
+ networkFront.removeOverride(url);
+ }
+}
+
+const clientCommands = {
+ autocomplete,
+ blackBox,
+ createObjectFront,
+ loadObjectProperties,
+ releaseActor,
+ pauseGrip,
+ toggleTracing,
+ resume,
+ stepIn,
+ stepOut,
+ stepOver,
+ restart,
+ breakOnNext,
+ sourceContents,
+ getSourceActorBreakpointPositions,
+ getSourceActorBreakableLines,
+ hasBreakpoint,
+ getServerBreakpointsList,
+ setBreakpoint,
+ setXHRBreakpoint,
+ removeXHRBreakpoint,
+ addWatchpoint,
+ removeWatchpoint,
+ removeBreakpoint,
+ evaluate,
+ evaluateExpressions,
+ getProperties,
+ getFrameScopes,
+ getFrames,
+ pauseOnDebuggerStatement,
+ pauseOnExceptions,
+ toggleEventLogging,
+ getMainThread,
+ setSkipPausing,
+ setEventListenerBreakpoints,
+ getEventListenerBreakpointTypes,
+ getFrontByID,
+ fetchAncestorFramePositions,
+ toggleJavaScriptEnabled,
+ setOverride,
+ removeOverride,
+};
+
+export { setupCommands, clientCommands };
diff --git a/devtools/client/debugger/src/client/firefox/create.js b/devtools/client/debugger/src/client/firefox/create.js
new file mode 100644
index 0000000000..9c610a08dd
--- /dev/null
+++ b/devtools/client/debugger/src/client/firefox/create.js
@@ -0,0 +1,431 @@
+/* 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/>. */
+
+// This module converts Firefox specific types to the generic types
+
+import {
+ hasSource,
+ hasSourceActor,
+ getSourceActor,
+ getSourceCount,
+} from "../../selectors/index";
+import { features } from "../../utils/prefs";
+import { isUrlExtension } from "../../utils/source";
+import { createLocation } from "../../utils/location";
+import { getDisplayURL } from "../../utils/sources-tree/getURL";
+
+let store;
+
+/**
+ * This function is to be called first before any other
+ * and allow having access to any instances of classes that are
+ * useful for this module
+ *
+ * @param {Object} dependencies
+ * @param {Object} dependencies.store
+ * The redux store object of the debugger frontend.
+ */
+export function setupCreate(dependencies) {
+ store = dependencies.store;
+}
+
+export async function createFrame(thread, frame, index = 0) {
+ // Because of throttling, the source related to the top frame may be available a bit late.
+ const sourceActor = await waitForSourceActorToBeRegisteredInStore(
+ frame.where.actor
+ );
+
+ const location = createLocation({
+ source: sourceActor.sourceObject,
+ sourceActor,
+ line: frame.where.line,
+ column: frame.where.column,
+ });
+
+ return {
+ id: frame.actorID,
+ thread,
+ displayName: frame.displayName,
+ location,
+ generatedLocation: location,
+ this: frame.this,
+ index,
+ asyncCause: frame.asyncCause,
+ state: frame.state,
+ type: frame.type,
+ };
+}
+
+/**
+ * Create an original frame object. Similar to the frame objects generated by createFrame.
+ * But with additional information related to SourceMap processing.
+ * For example, location and generatedLocation will be different.
+ *
+ * @param {Function} getState
+ * @param {Object} frame
+ * The frame for the generated location, i.e. WASM binary code.
+ * @param {String} id
+ * The new ID to use for the new frame object.
+ * @param {Object} originalFrame
+ * An object crafted by the SourceMap Worker with additional information
+ * about the original source code. i.e. the Rust, C++, whatever original source code.
+ * See XScope.search() for definition of this object.
+ */
+export function createWasmOriginalFrame(
+ generatedFrame,
+ id,
+ originalFrame,
+ originalFrameLocation
+) {
+ return {
+ id,
+ thread: generatedFrame.thread,
+ displayName: originalFrame.displayName,
+ location: originalFrameLocation,
+ generatedLocation: generatedFrame.generatedLocation,
+ this: generatedFrame.this,
+ index: generatedFrame.index,
+ asyncCause: generatedFrame.asyncCause,
+ state: generatedFrame.state,
+ type: generatedFrame.type,
+
+ // All additional fields only available for WASM original frames
+ isOriginal: true,
+ originalDisplayName: originalFrame.displayName,
+ originalVariables: originalFrame.variables,
+ };
+}
+
+/**
+ * This method wait for the given source actor to be registered in Redux store.
+ *
+ * @param {String} sourceActorId
+ * Actor ID of the source to be waiting for.
+ */
+async function waitForSourceActorToBeRegisteredInStore(sourceActorId) {
+ if (!hasSourceActor(store.getState(), sourceActorId)) {
+ await new Promise(resolve => {
+ const unsubscribe = store.subscribe(check);
+ let currentSize = null;
+ function check() {
+ const previousSize = currentSize;
+ currentSize = store.getState().sourceActors.mutableSourceActors.size;
+ // For perf reason, avoid any extra computation if sources did not change
+ if (previousSize == currentSize) {
+ return;
+ }
+ if (hasSourceActor(store.getState(), sourceActorId)) {
+ unsubscribe();
+ resolve();
+ }
+ }
+ });
+ }
+ return getSourceActor(store.getState(), sourceActorId);
+}
+
+/**
+ * This method wait for the given source to be registered in Redux store.
+ *
+ * @param {String} sourceId
+ * The id of the source to be waiting for.
+ */
+export async function waitForSourceToBeRegisteredInStore(sourceId) {
+ return new Promise(resolve => {
+ if (hasSource(store.getState(), sourceId)) {
+ resolve();
+ return;
+ }
+ const unsubscribe = store.subscribe(check);
+ let currentSize = null;
+ function check() {
+ const previousSize = currentSize;
+ currentSize = getSourceCount(store.getState());
+ // For perf reason, avoid any extra computation if sources did not change
+ if (previousSize == currentSize) {
+ return;
+ }
+ if (hasSource(store.getState(), sourceId)) {
+ unsubscribe();
+ resolve();
+ }
+ }
+ });
+}
+
+// Compute the reducer's source ID for a given source front/resource.
+//
+// We have four kind of "sources":
+// * "sources" in sources.js reducer, which map to 1 or many:
+// * "source actors" in source-actors.js reducer, which map 1 for 1 with:
+// * "SOURCE" resources coming from ResourceCommand API
+// * SourceFront, which are retrieved via `ThreadFront.source(sourceResource)`
+//
+// Note that SOURCE resources are actually the "form" of the SourceActor,
+// with the addition of `resourceType` and `targetFront` attributes.
+//
+// Unfortunately, the debugger frontend interacts with these 4 type of objects.
+// The last three actually try to represent the exact same thing.
+//
+// Here this method received a SOURCE resource (the 3rd bullet point)
+export function makeSourceId(sourceResource) {
+ // Allows Jest to use custom, simplier IDs
+ if ("mockedJestID" in sourceResource) {
+ return sourceResource.mockedJestID;
+ }
+ // By default, within a given target, all sources will be grouped by URL.
+ // You will be having a unique Source object in sources.js reducer,
+ // while you might have many Source Actor objects in source-actors.js reducer.
+ //
+ // There is two distinct usecases here:
+ // * HTML pages, which will have one source object which represents the whole HTML page
+ // and it will relate to many source actors. One for each inline <script> tag.
+ // Each script tag's source actor will actually return the whole content of the html page
+ // and not only this one script tag content.
+ // * Scripts with the same URL injected many times.
+ // For example, two <script src=""> with the same location
+ // Or by using eval("...// # SourceURL=")
+ // All the scripts will be grouped under a unique Source object, while having dedicated
+ // Source Actor objects.
+ // An important point this time is that each actor may have a different source text content.
+ // For now, the debugger arbitrarily picks the first source actor's text content and never
+ // updates it. (See bug 1751063)
+ if (sourceResource.url) {
+ return `source-url-${sourceResource.url}`;
+ }
+
+ // Otherwise, we are processing a source without URL.
+ // This is typically evals, console evaluations, setTimeout/setInterval strings,
+ // DOM event handler strings (i.e. `<div onclick="foo">`), ...
+ // The main way to interact with them is to use a debugger statement from them,
+ // or have other panels ask the debugger to open them (like DOM event handlers from the inspector).
+ // We can register transient breakpoints against them (i.e. they will only apply to the current source actor instance)
+ return `source-actor-${sourceResource.actor}`;
+}
+
+/**
+ * Create the source object for a generated source that is stored in sources.js reducer.
+ * These generated sources relate to JS code which run in the
+ * debugged runtime (as oppose to original sources
+ * which are only available in debugger's environment).
+ *
+ * @param {SOURCE} sourceResource
+ * SOURCE resource coming from the ResourceCommand API.
+ * This represents the `SourceActor` from the server codebase.
+ */
+export function createGeneratedSource(sourceResource) {
+ return createSourceObject({
+ id: makeSourceId(sourceResource),
+ url: sourceResource.url,
+ extensionName: sourceResource.extensionName,
+ isWasm: !!features.wasm && sourceResource.introductionType === "wasm",
+ isExtension:
+ (sourceResource.url && isUrlExtension(sourceResource.url)) || false,
+ isHTML: !!sourceResource.isInlineSource,
+ });
+}
+
+/**
+ * Create the source object that is stored in sources.js reducer.
+ *
+ * This is an internal helper to this module to ensure all sources have the same shape.
+ * Do not use it outside of this module!
+ */
+function createSourceObject({
+ id,
+ url,
+ extensionName = null,
+ isWasm = false,
+ isExtension = false,
+ isPrettyPrinted = false,
+ isOriginal = false,
+ isHTML = false,
+}) {
+ return {
+ // The ID, computed by:
+ // * `makeSourceId` for generated,
+ // * `generatedToOriginalId` for both source map and pretty printed original,
+ id,
+
+ // Absolute URL for the source. This may be a fake URL for pretty printed sources
+ url,
+
+ // A (slightly tweaked) URL object to represent the source URL.
+ // The URL object is augmented of a "group" attribute and some other standard attributes
+ // are modified from their typical value. See getDisplayURL implementation.
+ displayURL: getDisplayURL(url, extensionName),
+
+ // Only set for generated sources that are WebExtension sources.
+ // This is especially useful to display the extension name for content scripts
+ // that executes against the page we are debugging.
+ extensionName,
+
+ // Will be true if the source URL starts with moz-extension://,
+ // which most likely means the source is a content script.
+ // (Note that when debugging an add-on all generated sources will most likely have this flag set to true)
+ isExtension,
+
+ // True if WASM is enabled *and* the generated source is a WASM source
+ isWasm,
+
+ // True if this source is an HTML and relates to many sources actors,
+ // one for each of its inline <script>
+ isHTML,
+
+ // True, if this is an original pretty printed source
+ isPrettyPrinted,
+
+ // True for source map original files, as well as pretty printed sources
+ isOriginal,
+ };
+}
+
+/**
+ * Create the source object for a source mapped original source that is stored in sources.js reducer.
+ * These original sources referred to by source maps.
+ * This isn't code that runs in the runtime, so it isn't associated with anything
+ * on the server side. It is associated with a generated source for the related bundle file
+ * which itself relates to an actual code that runs in the runtime.
+ *
+ * @param {String} id
+ * The ID of the source, computed by source map codebase.
+ * @param {String} url
+ * The URL of the original source file.
+ */
+export function createSourceMapOriginalSource(id, url) {
+ return createSourceObject({
+ id,
+ url,
+ isOriginal: true,
+ });
+}
+
+/**
+ * Create the source object for a pretty printed original source that is stored in sources.js reducer.
+ * These original pretty printed sources aren't code that run in the runtime,
+ * so it isn't associated with anything on the server side.
+ * It is associated with a generated source for the non-pretty-printed file
+ * which itself relates to an actual code that runs in the runtime.
+ *
+ * @param {String} id
+ * The ID of the source, computed by pretty print.
+ * @param {String} url
+ * The URL of the pretty-printed source file.
+ * This URL doesn't work. It is the URL of the non-pretty-printed file with ":formated" suffix.
+ */
+export function createPrettyPrintOriginalSource(id, url) {
+ return createSourceObject({
+ id,
+ url,
+ isOriginal: true,
+ isPrettyPrinted: true,
+ });
+}
+
+/**
+ * Create the "source actor" object that is stored in source-actor.js reducer.
+ * This will represent server's source actor in the reducer universe.
+ *
+ * @param {SOURCE} sourceResource
+ * SOURCE resource coming from the ResourceCommand API.
+ * This represents the `SourceActor` from the server codebase.
+ * @param {Object} sourceObject
+ * Source object stored in redux, i.e. created via createSourceObject.
+ */
+export function createSourceActor(sourceResource, sourceObject) {
+ const actorId = sourceResource.actor;
+
+ return {
+ id: actorId,
+ actor: actorId,
+ // As sourceResource is only SourceActor's form and not the SourceFront,
+ // we have to go through the target to retrieve the related ThreadActor's ID.
+ thread: sourceResource.targetFront.getCachedFront("thread").actorID,
+ // `source` is the reducer source ID
+ source: makeSourceId(sourceResource),
+ sourceObject,
+ sourceMapBaseURL: sourceResource.sourceMapBaseURL,
+ sourceMapURL: sourceResource.sourceMapURL,
+ url: sourceResource.url,
+ introductionType: sourceResource.introductionType,
+ sourceStartLine: sourceResource.sourceStartLine,
+ sourceStartColumn: sourceResource.sourceStartColumn,
+ sourceLength: sourceResource.sourceLength,
+ };
+}
+
+/**
+ * Create a pause info object passed to paused action from
+ * the THREAD_STATE "paused" resource.
+ */
+export async function createPause(threadActorID, pausedThreadState) {
+ const frame = await createFrame(threadActorID, pausedThreadState.frame);
+ return {
+ thread: threadActorID,
+ frame,
+ why: pausedThreadState.why,
+ };
+}
+
+export function createThread(targetFront) {
+ const name = targetFront.isTopLevel
+ ? L10N.getStr("mainThread")
+ : targetFront.name;
+
+ return {
+ actor: targetFront.targetForm.threadActor,
+ url: targetFront.url,
+ isTopLevel: targetFront.isTopLevel,
+ targetType: targetFront.targetType,
+ name,
+ serviceWorkerStatus: targetFront.debuggerServiceWorkerStatus,
+ isWebExtension: targetFront.isWebExtension,
+ processID: targetFront.processID,
+ };
+}
+
+/**
+ * Defines the shape of a breakpoint
+ */
+export function createBreakpoint({
+ id,
+ thread,
+ disabled = false,
+ options = {},
+ location,
+ generatedLocation,
+ text,
+ originalText,
+}) {
+ return {
+ // The unique identifier (string) for the breakpoint, for details on its format and creation See `makeBreakpointId`
+ id,
+
+ // The thread actor id (string) which the source this breakpoint is created in belongs to
+ thread,
+
+ // This (boolean) specifies if the breakpoint is disabled or not
+ disabled,
+
+ // This (object) stores extra information about the breakpoint, which defines the type of the breakpoint (i.e conditional breakpoints, log points)
+ // {
+ // condition: <Boolean>,
+ // logValue: <String>,
+ // hidden: <Boolean>
+ // }
+ options,
+
+ // The location (object) information for the original source, for details on its format and structure See `createLocation`
+ location,
+
+ // The location (object) information for the generated source, for details on its format and structure See `createLocation`
+ generatedLocation,
+
+ // The text (string) on the line which the brekpoint is set in the generated source
+ text,
+
+ // The text (string) on the line which the breakpoint is set in the original source
+ originalText,
+ };
+}
diff --git a/devtools/client/debugger/src/client/firefox/moz.build b/devtools/client/debugger/src/client/firefox/moz.build
new file mode 100644
index 0000000000..9406133e17
--- /dev/null
+++ b/devtools/client/debugger/src/client/firefox/moz.build
@@ -0,0 +1,11 @@
+# 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(
+ "commands.js",
+ "create.js",
+)
diff --git a/devtools/client/debugger/src/client/moz.build b/devtools/client/debugger/src/client/moz.build
new file mode 100644
index 0000000000..cbaaa3a2a0
--- /dev/null
+++ b/devtools/client/debugger/src/client/moz.build
@@ -0,0 +1,12 @@
+# 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 += [
+ "firefox",
+]
+
+CompiledModules(
+ "firefox.js",
+)
diff --git a/devtools/client/debugger/src/components/App.css b/devtools/client/debugger/src/components/App.css
new file mode 100644
index 0000000000..796bf84574
--- /dev/null
+++ b/devtools/client/debugger/src/components/App.css
@@ -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/>. */
+
+* {
+ box-sizing: border-box;
+}
+
+html,
+body {
+ height: 100%;
+ width: 100%;
+ margin: 0;
+ padding: 0;
+}
+
+#mount {
+ height: 100%;
+}
+
+button {
+ background: transparent;
+ border: none;
+ font-family: inherit;
+ font-size: inherit;
+}
+
+button:hover {
+ background-color: var(--theme-toolbar-background-hover);
+}
+
+.theme-dark button:hover {
+ background-color: var(--theme-toolbar-hover);
+}
+
+.debugger {
+ display: flex;
+ flex: 1;
+ height: 100%;
+}
+
+.debugger .tree-indent {
+ width: 16px;
+ margin-inline-start: 0;
+ border-inline-start: 0;
+}
+
+.editor-pane {
+ display: flex;
+ position: relative;
+ flex: 1;
+ background-color: var(--theme-body-background);
+ height: 100%;
+ overflow: hidden;
+}
+
+.editor-container {
+ width: 100%;
+ display: grid;
+ grid-template-areas:
+ "editor-header"
+ "editor "
+ "notification "
+ "editor-footer";
+ grid-template-rows:
+ var(--editor-header-height)
+ 1fr
+ auto
+ var(--editor-footer-height);
+ max-height: 100%;
+ overflow-y: auto;
+}
+
+.editor-notification-footer {
+ background: var(--theme-warning-background);
+ border-top: 1px solid var(--theme-warning-border);
+ color: var(--theme-warning-color);
+ padding: 0.5em;
+ gap: 8px;
+ grid-area: notification;
+ display: flex;
+}
+
+/* Utils */
+.absolute-center {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+}
+
+.d-flex {
+ display: flex;
+}
+
+.align-items-center {
+ align-items: center;
+}
+
+.rounded-circle {
+ border-radius: 50%;
+}
+
+.text-white {
+ color: white;
+}
+
+.text-center {
+ text-align: center;
+}
+
+.min-width-0 {
+ min-width: 0;
+}
+
+/*
+ Prevents horizontal scrollbar from displaying when
+ right pane collapsed (#7505)
+*/
+.split-box > .splitter:last-child {
+ display: none;
+}
+
+/**
+ * In RTL layouts, the Debugger UI overlays the splitters. See Bug 1731233.
+ * Note: we need to the `.debugger` prefix here to beat the specificity of the
+ * general rule defined in SlitBox.css for `.split-box.vert > .splitter`.
+ */
+.debugger .split-box.vert > .splitter {
+ border-left-width: var(--devtools-splitter-inline-start-width);
+ border-right-width: var(--devtools-splitter-inline-end-width);
+
+ margin-left: calc(-1 * var(--devtools-splitter-inline-start-width) - 1px);
+ margin-right: calc(-1 * var(--devtools-splitter-inline-end-width));
+}
+
+::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+ background: transparent;
+}
+
+::-webkit-scrollbar-track {
+ border-radius: 8px;
+ background: transparent;
+}
+
+::-webkit-scrollbar-thumb {
+ border-radius: 8px;
+ background: rgba(113, 113, 113, 0.5);
+}
diff --git a/devtools/client/debugger/src/components/App.js b/devtools/client/debugger/src/components/App.js
new file mode 100644
index 0000000000..40911e5167
--- /dev/null
+++ b/devtools/client/debugger/src/components/App.js
@@ -0,0 +1,396 @@
+/* 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 React, { Component } from "devtools/client/shared/vendor/react";
+import {
+ div,
+ main,
+ span,
+} from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import { prefs } from "../utils/prefs";
+import { primaryPaneTabs } from "../constants";
+import actions from "../actions/index";
+import AccessibleImage from "./shared/AccessibleImage";
+
+import {
+ getSelectedLocation,
+ getPaneCollapse,
+ getActiveSearch,
+ getQuickOpenEnabled,
+ getOrientation,
+ getIsCurrentThreadPaused,
+ isMapScopesEnabled,
+ getSourceMapErrorForSourceActor,
+} from "../selectors/index";
+const KeyShortcuts = require("resource://devtools/client/shared/key-shortcuts.js");
+
+const SplitBox = require("resource://devtools/client/shared/components/splitter/SplitBox.js");
+const AppErrorBoundary = require("resource://devtools/client/shared/components/AppErrorBoundary.js");
+
+const shortcuts = new KeyShortcuts({ window });
+
+const horizontalLayoutBreakpoint = window.matchMedia("(min-width: 800px)");
+const verticalLayoutBreakpoint = window.matchMedia(
+ "(min-width: 10px) and (max-width: 799px)"
+);
+
+import { ShortcutsModal } from "./ShortcutsModal";
+import PrimaryPanes from "./PrimaryPanes/index";
+import Editor from "./Editor/index";
+import SecondaryPanes from "./SecondaryPanes/index";
+import WelcomeBox from "./WelcomeBox";
+import EditorTabs from "./Editor/Tabs";
+import EditorFooter from "./Editor/Footer";
+import QuickOpenModal from "./QuickOpenModal";
+
+class App extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ shortcutsModalEnabled: false,
+ startPanelSize: 0,
+ endPanelSize: 0,
+ };
+ }
+
+ static get propTypes() {
+ return {
+ activeSearch: PropTypes.oneOf(["file", "project"]),
+ closeActiveSearch: PropTypes.func.isRequired,
+ closeQuickOpen: PropTypes.func.isRequired,
+ endPanelCollapsed: PropTypes.bool.isRequired,
+ fluentBundles: PropTypes.array.isRequired,
+ openQuickOpen: PropTypes.func.isRequired,
+ orientation: PropTypes.oneOf(["horizontal", "vertical"]).isRequired,
+ quickOpenEnabled: PropTypes.bool.isRequired,
+ selectedLocation: PropTypes.object,
+ setActiveSearch: PropTypes.func.isRequired,
+ setOrientation: PropTypes.func.isRequired,
+ setPrimaryPaneTab: PropTypes.func.isRequired,
+ startPanelCollapsed: PropTypes.bool.isRequired,
+ toolboxDoc: PropTypes.object.isRequired,
+ showOriginalVariableMappingWarning: PropTypes.bool,
+ };
+ }
+
+ getChildContext() {
+ return {
+ fluentBundles: this.props.fluentBundles,
+ toolboxDoc: this.props.toolboxDoc,
+ shortcuts,
+ l10n: L10N,
+ };
+ }
+
+ componentDidMount() {
+ horizontalLayoutBreakpoint.addListener(this.onLayoutChange);
+ verticalLayoutBreakpoint.addListener(this.onLayoutChange);
+ this.setOrientation();
+
+ shortcuts.on(L10N.getStr("symbolSearch.search.key2"), e =>
+ this.toggleQuickOpenModal(e, "@")
+ );
+
+ [
+ L10N.getStr("sources.search.key2"),
+ L10N.getStr("sources.search.alt.key"),
+ ].forEach(key => shortcuts.on(key, this.toggleQuickOpenModal));
+
+ shortcuts.on(L10N.getStr("gotoLineModal.key3"), e =>
+ this.toggleQuickOpenModal(e, ":")
+ );
+
+ shortcuts.on(
+ L10N.getStr("projectTextSearch.key"),
+ this.jumpToProjectSearch
+ );
+
+ shortcuts.on("Escape", this.onEscape);
+ shortcuts.on("CmdOrCtrl+/", this.onCommandSlash);
+ }
+
+ componentWillUnmount() {
+ horizontalLayoutBreakpoint.removeListener(this.onLayoutChange);
+ verticalLayoutBreakpoint.removeListener(this.onLayoutChange);
+ shortcuts.off(
+ L10N.getStr("symbolSearch.search.key2"),
+ this.toggleQuickOpenModal
+ );
+
+ [
+ L10N.getStr("sources.search.key2"),
+ L10N.getStr("sources.search.alt.key"),
+ ].forEach(key => shortcuts.off(key, this.toggleQuickOpenModal));
+
+ shortcuts.off(L10N.getStr("gotoLineModal.key3"), this.toggleQuickOpenModal);
+
+ shortcuts.off(
+ L10N.getStr("projectTextSearch.key"),
+ this.jumpToProjectSearch
+ );
+
+ shortcuts.off("Escape", this.onEscape);
+ shortcuts.off("CmdOrCtrl+/", this.onCommandSlash);
+ }
+
+ jumpToProjectSearch = e => {
+ e.preventDefault();
+ this.props.setPrimaryPaneTab(primaryPaneTabs.PROJECT_SEARCH);
+ this.props.setActiveSearch(primaryPaneTabs.PROJECT_SEARCH);
+ };
+
+ onEscape = e => {
+ const {
+ activeSearch,
+ closeActiveSearch,
+ closeQuickOpen,
+ quickOpenEnabled,
+ } = this.props;
+ const { shortcutsModalEnabled } = this.state;
+
+ if (activeSearch) {
+ e.preventDefault();
+ closeActiveSearch();
+ }
+
+ if (quickOpenEnabled) {
+ e.preventDefault();
+ closeQuickOpen();
+ }
+
+ if (shortcutsModalEnabled) {
+ e.preventDefault();
+ this.toggleShortcutsModal();
+ }
+ };
+
+ onCommandSlash = () => {
+ this.toggleShortcutsModal();
+ };
+
+ isHorizontal() {
+ return this.props.orientation === "horizontal";
+ }
+
+ toggleQuickOpenModal = (e, query) => {
+ const { quickOpenEnabled, openQuickOpen, closeQuickOpen } = this.props;
+
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (quickOpenEnabled === true) {
+ closeQuickOpen();
+ return;
+ }
+
+ if (query != null) {
+ openQuickOpen(query);
+ return;
+ }
+ openQuickOpen();
+ };
+
+ onLayoutChange = () => {
+ this.setOrientation();
+ };
+
+ setOrientation() {
+ // If the orientation does not match (if it is not visible) it will
+ // not setOrientation, or if it is the same as before, calling
+ // setOrientation will not cause a rerender.
+ if (horizontalLayoutBreakpoint.matches) {
+ this.props.setOrientation("horizontal");
+ } else if (verticalLayoutBreakpoint.matches) {
+ this.props.setOrientation("vertical");
+ }
+ }
+
+ renderEditorNotificationBar() {
+ if (this.props.sourceMapError) {
+ return div(
+ { className: "editor-notification-footer", "aria-role": "status" },
+ span(
+ { className: "info icon" },
+ React.createElement(AccessibleImage, { className: "sourcemap" })
+ ),
+ `Source Map Error: ${this.props.sourceMapError}`
+ );
+ }
+ if (this.props.showOriginalVariableMappingWarning) {
+ return div(
+ { className: "editor-notification-footer", "aria-role": "status" },
+ span(
+ { className: "info icon" },
+ React.createElement(AccessibleImage, { className: "sourcemap" })
+ ),
+ L10N.getFormatStr(
+ "editorNotificationFooter.noOriginalScopes",
+ L10N.getStr("scopes.showOriginalScopes")
+ )
+ );
+ }
+ return null;
+ }
+
+ renderEditorPane = () => {
+ const { startPanelCollapsed, endPanelCollapsed } = this.props;
+ const { endPanelSize, startPanelSize } = this.state;
+ const horizontal = this.isHorizontal();
+ return main(
+ {
+ className: "editor-pane",
+ },
+ div(
+ {
+ className: "editor-container",
+ },
+ React.createElement(EditorTabs, {
+ startPanelCollapsed: startPanelCollapsed,
+ endPanelCollapsed: endPanelCollapsed,
+ horizontal: horizontal,
+ }),
+ React.createElement(Editor, {
+ startPanelSize: startPanelSize,
+ endPanelSize: endPanelSize,
+ }),
+ !this.props.selectedLocation
+ ? React.createElement(WelcomeBox, {
+ horizontal,
+ toggleShortcutsModal: () => this.toggleShortcutsModal(),
+ })
+ : null,
+ this.renderEditorNotificationBar(),
+ React.createElement(EditorFooter, {
+ horizontal,
+ })
+ )
+ );
+ };
+
+ toggleShortcutsModal() {
+ this.setState(prevState => ({
+ shortcutsModalEnabled: !prevState.shortcutsModalEnabled,
+ }));
+ }
+
+ // Important so that the tabs chevron updates appropriately when
+ // the user resizes the left or right columns
+ triggerEditorPaneResize() {
+ const editorPane = window.document.querySelector(".editor-pane");
+ if (editorPane) {
+ editorPane.dispatchEvent(new Event("resizeend"));
+ }
+ }
+
+ renderLayout = () => {
+ const { startPanelCollapsed, endPanelCollapsed } = this.props;
+ const horizontal = this.isHorizontal();
+ return React.createElement(SplitBox, {
+ style: {
+ width: "100vw",
+ },
+ initialSize: prefs.endPanelSize,
+ minSize: 30,
+ maxSize: "70%",
+ splitterSize: 1,
+ vert: horizontal,
+ onResizeEnd: num => {
+ prefs.endPanelSize = num;
+ this.triggerEditorPaneResize();
+ },
+ startPanel: React.createElement(SplitBox, {
+ style: {
+ width: "100vw",
+ },
+ initialSize: prefs.startPanelSize,
+ minSize: 30,
+ maxSize: "85%",
+ splitterSize: 1,
+ onResizeEnd: num => {
+ prefs.startPanelSize = num;
+ this.triggerEditorPaneResize();
+ },
+ startPanelCollapsed: startPanelCollapsed,
+ startPanel: React.createElement(PrimaryPanes, {
+ horizontal,
+ }),
+ endPanel: this.renderEditorPane(),
+ }),
+ endPanelControl: true,
+ endPanel: React.createElement(SecondaryPanes, {
+ horizontal,
+ }),
+ endPanelCollapsed: endPanelCollapsed,
+ });
+ };
+
+ render() {
+ const { quickOpenEnabled } = this.props;
+ return div(
+ {
+ className: "debugger",
+ },
+ React.createElement(
+ AppErrorBoundary,
+ {
+ componentName: "Debugger",
+ panel: L10N.getStr("ToolboxDebugger.label"),
+ },
+ this.renderLayout(),
+ quickOpenEnabled === true &&
+ React.createElement(QuickOpenModal, {
+ shortcutsModalEnabled: this.state.shortcutsModalEnabled,
+ toggleShortcutsModal: () => this.toggleShortcutsModal(),
+ }),
+ React.createElement(ShortcutsModal, {
+ enabled: this.state.shortcutsModalEnabled,
+ handleClose: () => this.toggleShortcutsModal(),
+ })
+ )
+ );
+ }
+}
+
+App.childContextTypes = {
+ toolboxDoc: PropTypes.object,
+ shortcuts: PropTypes.object,
+ l10n: PropTypes.object,
+ fluentBundles: PropTypes.array,
+};
+
+const mapStateToProps = state => {
+ const selectedLocation = getSelectedLocation(state);
+ const mapScopeEnabled = isMapScopesEnabled(state);
+ const isPaused = getIsCurrentThreadPaused(state);
+
+ const showOriginalVariableMappingWarning =
+ isPaused &&
+ selectedLocation?.source.isOriginal &&
+ !selectedLocation?.source.isPrettyPrinted &&
+ !mapScopeEnabled;
+
+ return {
+ showOriginalVariableMappingWarning,
+ selectedLocation,
+ startPanelCollapsed: getPaneCollapse(state, "start"),
+ endPanelCollapsed: getPaneCollapse(state, "end"),
+ activeSearch: getActiveSearch(state),
+ quickOpenEnabled: getQuickOpenEnabled(state),
+ orientation: getOrientation(state),
+ sourceMapError: selectedLocation?.sourceActor
+ ? getSourceMapErrorForSourceActor(state, selectedLocation.sourceActor.id)
+ : null,
+ };
+};
+
+export default connect(mapStateToProps, {
+ setActiveSearch: actions.setActiveSearch,
+ closeActiveSearch: actions.closeActiveSearch,
+ openQuickOpen: actions.openQuickOpen,
+ closeQuickOpen: actions.closeQuickOpen,
+ setOrientation: actions.setOrientation,
+ setPrimaryPaneTab: actions.setPrimaryPaneTab,
+})(App);
diff --git a/devtools/client/debugger/src/components/Editor/BlackboxLines.js b/devtools/client/debugger/src/components/Editor/BlackboxLines.js
new file mode 100644
index 0000000000..1281bc635a
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/BlackboxLines.js
@@ -0,0 +1,139 @@
+/* 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 PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import { Component } from "devtools/client/shared/vendor/react";
+import { toEditorLine, fromEditorLine } from "../../utils/editor/index";
+import { isLineBlackboxed } from "../../utils/source";
+import { isWasm } from "../../utils/wasm";
+
+// This renders blackbox line highlighting in the editor
+class BlackboxLines extends Component {
+ static get propTypes() {
+ return {
+ editor: PropTypes.object.isRequired,
+ selectedSource: PropTypes.object.isRequired,
+ blackboxedRangesForSelectedSource: PropTypes.array,
+ isSourceOnIgnoreList: PropTypes.bool,
+ };
+ }
+
+ componentDidMount() {
+ const { selectedSource, blackboxedRangesForSelectedSource, editor } =
+ this.props;
+
+ if (this.props.isSourceOnIgnoreList) {
+ this.setAllBlackboxLines(editor);
+ return;
+ }
+
+ // When `blackboxedRangesForSelectedSource` is defined and the array is empty,
+ // the whole source was blackboxed.
+ if (!blackboxedRangesForSelectedSource.length) {
+ this.setAllBlackboxLines(editor);
+ } else {
+ editor.codeMirror.operation(() => {
+ blackboxedRangesForSelectedSource.forEach(range => {
+ const start = toEditorLine(selectedSource.id, range.start.line);
+ // CodeMirror.eachLine doesn't include `end` line offset, so bump by one
+ const end = toEditorLine(selectedSource.id, range.end.line) + 1;
+ editor.codeMirror.eachLine(start, end, lineHandle => {
+ this.setBlackboxLine(editor, lineHandle);
+ });
+ });
+ });
+ }
+ }
+
+ componentDidUpdate() {
+ const {
+ selectedSource,
+ blackboxedRangesForSelectedSource,
+ editor,
+ isSourceOnIgnoreList,
+ } = this.props;
+
+ if (this.props.isSourceOnIgnoreList) {
+ this.setAllBlackboxLines(editor);
+ return;
+ }
+
+ // when unblackboxed
+ if (!blackboxedRangesForSelectedSource) {
+ this.clearAllBlackboxLines(editor);
+ return;
+ }
+
+ // When the whole source is blackboxed
+ if (!blackboxedRangesForSelectedSource.length) {
+ this.setAllBlackboxLines(editor);
+ return;
+ }
+
+ const sourceIsWasm = isWasm(selectedSource.id);
+
+ // TODO: Possible perf improvement. Instead of going
+ // over all the lines each time get diffs of what has
+ // changed and update those.
+ editor.codeMirror.operation(() => {
+ editor.codeMirror.eachLine(lineHandle => {
+ const line = fromEditorLine(
+ selectedSource.id,
+ editor.codeMirror.getLineNumber(lineHandle),
+ sourceIsWasm
+ );
+
+ if (
+ isLineBlackboxed(
+ blackboxedRangesForSelectedSource,
+ line,
+ isSourceOnIgnoreList
+ )
+ ) {
+ this.setBlackboxLine(editor, lineHandle);
+ } else {
+ this.clearBlackboxLine(editor, lineHandle);
+ }
+ });
+ });
+ }
+
+ componentWillUnmount() {
+ // Lets make sure we remove everything relating to
+ // blackboxing lines when this component is unmounted.
+ this.clearAllBlackboxLines(this.props.editor);
+ }
+
+ clearAllBlackboxLines(editor) {
+ editor.codeMirror.operation(() => {
+ editor.codeMirror.eachLine(lineHandle => {
+ this.clearBlackboxLine(editor, lineHandle);
+ });
+ });
+ }
+
+ setAllBlackboxLines(editor) {
+ //TODO:We might be able to handle the whole source
+ // than adding the blackboxing line by line
+ editor.codeMirror.operation(() => {
+ editor.codeMirror.eachLine(lineHandle => {
+ this.setBlackboxLine(editor, lineHandle);
+ });
+ });
+ }
+
+ clearBlackboxLine(editor, lineHandle) {
+ editor.codeMirror.removeLineClass(lineHandle, "wrap", "blackboxed-line");
+ }
+
+ setBlackboxLine(editor, lineHandle) {
+ editor.codeMirror.addLineClass(lineHandle, "wrap", "blackboxed-line");
+ }
+
+ render() {
+ return null;
+ }
+}
+
+export default BlackboxLines;
diff --git a/devtools/client/debugger/src/components/Editor/Breakpoint.js b/devtools/client/debugger/src/components/Editor/Breakpoint.js
new file mode 100644
index 0000000000..4559a20289
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/Breakpoint.js
@@ -0,0 +1,157 @@
+/* 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 { PureComponent } from "devtools/client/shared/vendor/react";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+import { getDocument, toEditorLine } from "../../utils/editor/index";
+import { getSelectedLocation } from "../../utils/selected-location";
+import { features } from "../../utils/prefs";
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+const breakpointSvg = document.createElement("div");
+breakpointSvg.innerHTML =
+ '<svg viewBox="0 0 60 15" width="60" height="15"><path d="M53.07.5H1.5c-.54 0-1 .46-1 1v12c0 .54.46 1 1 1h51.57c.58 0 1.15-.26 1.53-.7l4.7-6.3-4.7-6.3c-.38-.44-.95-.7-1.53-.7z"/></svg>';
+
+class Breakpoint extends PureComponent {
+ static get propTypes() {
+ return {
+ breakpoint: PropTypes.object.isRequired,
+ editor: PropTypes.object.isRequired,
+ selectedSource: PropTypes.object,
+ };
+ }
+
+ componentDidMount() {
+ this.addBreakpoint(this.props);
+ }
+
+ componentDidUpdate(prevProps) {
+ this.removeBreakpoint(prevProps);
+ this.addBreakpoint(this.props);
+ }
+
+ componentWillUnmount() {
+ this.removeBreakpoint(this.props);
+ }
+
+ makeMarker() {
+ const { breakpoint } = this.props;
+ const bp = breakpointSvg.cloneNode(true);
+
+ bp.className = classnames("editor new-breakpoint", {
+ "breakpoint-disabled": breakpoint.disabled,
+ "folding-enabled": features.codeFolding,
+ });
+ bp.onmousedown = this.onClick;
+ bp.oncontextmenu = this.onContextMenu;
+
+ return bp;
+ }
+
+ onClick = event => {
+ const {
+ continueToHere,
+ toggleBreakpointsAtLine,
+ removeBreakpointsAtLine,
+ breakpoint,
+ selectedSource,
+ } = this.props;
+
+ // ignore right clicks
+ if ((event.ctrlKey && event.button === 0) || event.button === 2) {
+ return;
+ }
+
+ event.stopPropagation();
+ event.preventDefault();
+
+ const selectedLocation = getSelectedLocation(breakpoint, selectedSource);
+ if (event.metaKey) {
+ continueToHere(selectedLocation);
+ return;
+ }
+
+ if (event.shiftKey) {
+ toggleBreakpointsAtLine(!breakpoint.disabled, selectedLocation.line);
+ return;
+ }
+
+ removeBreakpointsAtLine(selectedLocation.source, selectedLocation.line);
+ };
+
+ onContextMenu = event => {
+ event.stopPropagation();
+ event.preventDefault();
+
+ this.props.showEditorEditBreakpointContextMenu(
+ event,
+ this.props.breakpoint
+ );
+ };
+
+ addBreakpoint(props) {
+ const { breakpoint, editor, selectedSource } = props;
+ // Hidden Breakpoints are never rendered on the client
+ if (breakpoint.options.hidden) {
+ return;
+ }
+
+ if (!selectedSource) {
+ return;
+ }
+
+ const doc = getDocument(selectedSource.id);
+ if (!doc) {
+ return;
+ }
+
+ const selectedLocation = getSelectedLocation(breakpoint, selectedSource);
+ const line = toEditorLine(selectedSource.id, selectedLocation.line);
+
+ doc.setGutterMarker(line, "breakpoints", this.makeMarker());
+
+ editor.codeMirror.addLineClass(line, "wrap", "new-breakpoint");
+ editor.codeMirror.removeLineClass(line, "wrap", "breakpoint-disabled");
+ editor.codeMirror.removeLineClass(line, "wrap", "has-condition");
+ editor.codeMirror.removeLineClass(line, "wrap", "has-log");
+
+ if (breakpoint.disabled) {
+ editor.codeMirror.addLineClass(line, "wrap", "breakpoint-disabled");
+ }
+
+ if (breakpoint.options.logValue) {
+ editor.codeMirror.addLineClass(line, "wrap", "has-log");
+ } else if (breakpoint.options.condition) {
+ editor.codeMirror.addLineClass(line, "wrap", "has-condition");
+ }
+ }
+
+ removeBreakpoint(props) {
+ const { selectedSource, breakpoint } = props;
+ if (!selectedSource) {
+ return;
+ }
+
+ const doc = getDocument(selectedSource.id);
+ if (!doc) {
+ return;
+ }
+
+ const selectedLocation = getSelectedLocation(breakpoint, selectedSource);
+ const line = toEditorLine(selectedSource.id, selectedLocation.line);
+
+ doc.setGutterMarker(line, "breakpoints", null);
+ doc.removeLineClass(line, "wrap", "new-breakpoint");
+ doc.removeLineClass(line, "wrap", "breakpoint-disabled");
+ doc.removeLineClass(line, "wrap", "has-condition");
+ doc.removeLineClass(line, "wrap", "has-log");
+ }
+
+ render() {
+ return null;
+ }
+}
+
+export default Breakpoint;
diff --git a/devtools/client/debugger/src/components/Editor/Breakpoints.css b/devtools/client/debugger/src/components/Editor/Breakpoints.css
new file mode 100644
index 0000000000..1269f73f82
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/Breakpoints.css
@@ -0,0 +1,153 @@
+/* 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/>. */
+
+.theme-light {
+ --gutter-hover-background-color: #dde1e4;
+ --breakpoint-fill: var(--blue-50);
+ --breakpoint-stroke: var(--blue-60);
+}
+
+.theme-dark {
+ --gutter-hover-background-color: #414141;
+ --breakpoint-fill: var(--blue-55);
+ --breakpoint-stroke: var(--blue-40);
+}
+
+.theme-light,
+.theme-dark {
+ --logpoint-fill: var(--theme-graphs-purple);
+ --logpoint-stroke: var(--purple-60);
+ --breakpoint-condition-fill: var(--theme-graphs-yellow);
+ --breakpoint-condition-stroke: var(--theme-graphs-orange);
+ --breakpoint-skipped-opacity: 0.15;
+ --breakpoint-inactive-opacity: 0.3;
+ --breakpoint-disabled-opacity: 0.6;
+}
+
+/* Standard gutter breakpoints */
+.editor-wrapper .breakpoints {
+ position: absolute;
+ top: 0;
+ left: 0;
+}
+
+.new-breakpoint .CodeMirror-linenumber {
+ pointer-events: none;
+}
+
+.editor-wrapper :not(.empty-line, .new-breakpoint)
+ > .CodeMirror-gutter-wrapper
+ > .CodeMirror-linenumber:hover::after {
+ content: "";
+ position: absolute;
+ /* paint below the number */
+ z-index: -1;
+ top: 0;
+ left: 0;
+ right: -4px;
+ bottom: 0;
+ height: 15px;
+ background-color: var(--gutter-hover-background-color);
+ mask: url(chrome://devtools/content/debugger/images/breakpoint.svg)
+ no-repeat;
+ mask-size: auto 15px;
+ mask-position: right;
+}
+
+.editor.new-breakpoint svg {
+ fill: var(--breakpoint-fill);
+ stroke: var(--breakpoint-stroke);
+ width: 60px;
+ height: 15px;
+ position: absolute;
+ top: 0px;
+ right: -4px;
+}
+
+.editor .breakpoint {
+ position: absolute;
+ right: -2px;
+}
+
+.editor.new-breakpoint.folding-enabled svg {
+ right: -16px;
+}
+
+.new-breakpoint.has-condition .CodeMirror-gutter-wrapper svg {
+ fill: var(--breakpoint-condition-fill);
+ stroke: var(--breakpoint-condition-stroke);
+}
+
+.new-breakpoint.has-log .CodeMirror-gutter-wrapper svg {
+ fill: var(--logpoint-fill);
+ stroke: var(--logpoint-stroke);
+}
+
+.editor.new-breakpoint.breakpoint-disabled svg,
+.blackboxed-line .editor.new-breakpoint svg {
+ fill-opacity: var(--breakpoint-disabled-opacity);
+ stroke-opacity: var(--breakpoint-disabled-opacity);
+}
+
+.editor-wrapper.skip-pausing .editor.new-breakpoint svg {
+ fill-opacity: var(--breakpoint-skipped-opacity);
+}
+
+/* Columnn breakpoints */
+.column-breakpoint {
+ display: inline;
+ padding-inline-start: 1px;
+ padding-inline-end: 1px;
+}
+
+.column-breakpoint:hover {
+ background-color: transparent;
+}
+
+.column-breakpoint svg {
+ display: inline-block;
+ cursor: pointer;
+ height: 13px;
+ width: 11px;
+ vertical-align: top;
+ fill: var(--breakpoint-fill);
+ stroke: var(--breakpoint-stroke);
+ fill-opacity: var(--breakpoint-inactive-opacity);
+ stroke-opacity: var(--breakpoint-inactive-opacity);
+}
+
+.column-breakpoint.active svg {
+ fill: var(--breakpoint-fill);
+ stroke: var(--breakpoint-stroke);
+ fill-opacity: 1;
+ stroke-opacity: 1;
+}
+
+.column-breakpoint.disabled svg {
+ fill-opacity: var(--breakpoint-disabled-opacity);
+ stroke-opacity: var(--breakpoint-disabled-opacity);
+}
+
+.column-breakpoint.has-log.disabled svg {
+ fill-opacity: 0.5;
+ stroke-opacity: 0.5;
+}
+
+.column-breakpoint.has-condition svg {
+ fill: var(--breakpoint-condition-fill);
+ stroke: var(--breakpoint-condition-stroke);
+}
+
+.column-breakpoint.has-log svg {
+ fill: var(--logpoint-fill);
+ stroke: var(--logpoint-stroke);
+}
+
+.editor-wrapper.skip-pausing .column-breakpoint svg {
+ fill-opacity: var(--breakpoint-skipped-opacity);
+}
+
+.img.column-marker {
+ background-image: url(chrome://devtools/content/debugger/images/column-marker.svg);
+}
diff --git a/devtools/client/debugger/src/components/Editor/Breakpoints.js b/devtools/client/debugger/src/components/Editor/Breakpoints.js
new file mode 100644
index 0000000000..6d1d088f11
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/Breakpoints.js
@@ -0,0 +1,78 @@
+/* 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 PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import React, { Component } from "devtools/client/shared/vendor/react";
+import { div } from "devtools/client/shared/vendor/react-dom-factories";
+import Breakpoint from "./Breakpoint";
+
+import {
+ getSelectedSource,
+ getFirstVisibleBreakpoints,
+} from "../../selectors/index";
+import { makeBreakpointId } from "../../utils/breakpoint/index";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import actions from "../../actions/index";
+
+class Breakpoints extends Component {
+ static get propTypes() {
+ return {
+ breakpoints: PropTypes.array,
+ editor: PropTypes.object,
+ selectedSource: PropTypes.object,
+ removeBreakpointsAtLine: PropTypes.func,
+ toggleBreakpointsAtLine: PropTypes.func,
+ continueToHere: PropTypes.func,
+ showEditorEditBreakpointContextMenu: PropTypes.func,
+ };
+ }
+ render() {
+ const {
+ breakpoints,
+ selectedSource,
+ editor,
+ showEditorEditBreakpointContextMenu,
+ continueToHere,
+ toggleBreakpointsAtLine,
+ removeBreakpointsAtLine,
+ } = this.props;
+
+ if (!selectedSource || !breakpoints) {
+ return null;
+ }
+ return div(
+ null,
+ breakpoints.map(breakpoint => {
+ return React.createElement(Breakpoint, {
+ key: makeBreakpointId(breakpoint.location),
+ breakpoint,
+ selectedSource,
+ showEditorEditBreakpointContextMenu,
+ continueToHere,
+ toggleBreakpointsAtLine,
+ removeBreakpointsAtLine,
+ editor,
+ });
+ })
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ const selectedSource = getSelectedSource(state);
+ return {
+ // Retrieves only the first breakpoint per line so that the
+ // breakpoint marker represents only the first breakpoint
+ breakpoints: getFirstVisibleBreakpoints(state),
+ selectedSource,
+ };
+};
+
+export default connect(mapStateToProps, {
+ showEditorEditBreakpointContextMenu:
+ actions.showEditorEditBreakpointContextMenu,
+ continueToHere: actions.continueToHere,
+ toggleBreakpointsAtLine: actions.toggleBreakpointsAtLine,
+ removeBreakpointsAtLine: actions.removeBreakpointsAtLine,
+})(Breakpoints);
diff --git a/devtools/client/debugger/src/components/Editor/ColumnBreakpoint.js b/devtools/client/debugger/src/components/Editor/ColumnBreakpoint.js
new file mode 100644
index 0000000000..867f3af4b7
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/ColumnBreakpoint.js
@@ -0,0 +1,128 @@
+/* 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 { PureComponent } from "devtools/client/shared/vendor/react";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+import { getDocument } from "../../utils/editor/index";
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+// eslint-disable-next-line max-len
+
+const breakpointButton = document.createElement("button");
+breakpointButton.innerHTML =
+ '<svg viewBox="0 0 11 13" width="11" height="13"><path d="M5.07.5H1.5c-.54 0-1 .46-1 1v10c0 .54.46 1 1 1h3.57c.58 0 1.15-.26 1.53-.7l3.7-5.3-3.7-5.3C6.22.76 5.65.5 5.07.5z"/></svg>';
+
+function makeBookmark({ breakpoint }, { onClick, onContextMenu }) {
+ const bp = breakpointButton.cloneNode(true);
+
+ const isActive = breakpoint && !breakpoint.disabled;
+ const isDisabled = breakpoint?.disabled;
+ const condition = breakpoint?.options.condition;
+ const logValue = breakpoint?.options.logValue;
+
+ bp.className = classnames("column-breakpoint", {
+ "has-condition": condition,
+ "has-log": logValue,
+ active: isActive,
+ disabled: isDisabled,
+ });
+
+ bp.setAttribute("title", logValue || condition || "");
+ bp.onclick = onClick;
+ bp.oncontextmenu = onContextMenu;
+
+ return bp;
+}
+
+export default class ColumnBreakpoint extends PureComponent {
+ bookmark;
+
+ static get propTypes() {
+ return {
+ columnBreakpoint: PropTypes.object.isRequired,
+ source: PropTypes.object.isRequired,
+ };
+ }
+
+ addColumnBreakpoint = nextProps => {
+ const { columnBreakpoint, source } = nextProps || this.props;
+
+ const sourceId = source.id;
+ const doc = getDocument(sourceId);
+ if (!doc) {
+ return;
+ }
+
+ const { line, column } = columnBreakpoint.location;
+ const widget = makeBookmark(columnBreakpoint, {
+ onClick: this.onClick,
+ onContextMenu: this.onContextMenu,
+ });
+
+ this.bookmark = doc.setBookmark({ line: line - 1, ch: column }, { widget });
+ };
+
+ clearColumnBreakpoint = () => {
+ if (this.bookmark) {
+ this.bookmark.clear();
+ this.bookmark = null;
+ }
+ };
+
+ onClick = event => {
+ event.stopPropagation();
+ event.preventDefault();
+ const {
+ columnBreakpoint,
+ toggleDisabledBreakpoint,
+ removeBreakpoint,
+ addBreakpoint,
+ } = this.props;
+
+ // disable column breakpoint on shift-click.
+ if (event.shiftKey) {
+ toggleDisabledBreakpoint(columnBreakpoint.breakpoint);
+ return;
+ }
+
+ if (columnBreakpoint.breakpoint) {
+ removeBreakpoint(columnBreakpoint.breakpoint);
+ } else {
+ addBreakpoint(columnBreakpoint.location);
+ }
+ };
+
+ onContextMenu = event => {
+ event.stopPropagation();
+ event.preventDefault();
+
+ const {
+ columnBreakpoint: { breakpoint, location },
+ } = this.props;
+
+ if (breakpoint) {
+ this.props.showEditorEditBreakpointContextMenu(event, breakpoint);
+ } else {
+ this.props.showEditorCreateBreakpointContextMenu(event, location);
+ }
+ };
+
+ componentDidMount() {
+ this.addColumnBreakpoint();
+ }
+
+ componentWillUnmount() {
+ this.clearColumnBreakpoint();
+ }
+
+ componentDidUpdate() {
+ this.clearColumnBreakpoint();
+ this.addColumnBreakpoint();
+ }
+
+ render() {
+ return null;
+ }
+}
diff --git a/devtools/client/debugger/src/components/Editor/ColumnBreakpoints.js b/devtools/client/debugger/src/components/Editor/ColumnBreakpoints.js
new file mode 100644
index 0000000000..33ccfad325
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/ColumnBreakpoints.js
@@ -0,0 +1,94 @@
+/* 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 React, { Component } from "devtools/client/shared/vendor/react";
+import { div } from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+import ColumnBreakpoint from "./ColumnBreakpoint";
+
+import {
+ getSelectedSource,
+ visibleColumnBreakpoints,
+ isSourceBlackBoxed,
+} from "../../selectors/index";
+import actions from "../../actions/index";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import { makeBreakpointId } from "../../utils/breakpoint/index";
+
+// eslint-disable-next-line max-len
+
+class ColumnBreakpoints extends Component {
+ static get propTypes() {
+ return {
+ columnBreakpoints: PropTypes.array.isRequired,
+ editor: PropTypes.object.isRequired,
+ selectedSource: PropTypes.object,
+ addBreakpoint: PropTypes.func,
+ removeBreakpoint: PropTypes.func,
+ toggleDisabledBreakpoint: PropTypes.func,
+ showEditorCreateBreakpointContextMenu: PropTypes.func,
+ showEditorEditBreakpointContextMenu: PropTypes.func,
+ };
+ }
+
+ render() {
+ const {
+ editor,
+ columnBreakpoints,
+ selectedSource,
+ showEditorCreateBreakpointContextMenu,
+ showEditorEditBreakpointContextMenu,
+ toggleDisabledBreakpoint,
+ removeBreakpoint,
+ addBreakpoint,
+ } = this.props;
+
+ if (!selectedSource || columnBreakpoints.length === 0) {
+ return null;
+ }
+
+ let breakpoints;
+ editor.codeMirror.operation(() => {
+ breakpoints = columnBreakpoints.map(columnBreakpoint =>
+ React.createElement(ColumnBreakpoint, {
+ key: makeBreakpointId(columnBreakpoint.location),
+ columnBreakpoint,
+ editor,
+ source: selectedSource,
+ showEditorCreateBreakpointContextMenu,
+ showEditorEditBreakpointContextMenu,
+ toggleDisabledBreakpoint,
+ removeBreakpoint,
+ addBreakpoint,
+ })
+ );
+ });
+ return div(null, breakpoints);
+ }
+}
+
+const mapStateToProps = state => {
+ // Avoid rendering this component is there is no selected source,
+ // or if the selected source is blackboxed.
+ // Also avoid computing visible column breakpoint when this happens.
+ const selectedSource = getSelectedSource(state);
+ if (!selectedSource || isSourceBlackBoxed(state, selectedSource)) {
+ return {};
+ }
+ return {
+ selectedSource,
+ columnBreakpoints: visibleColumnBreakpoints(state),
+ };
+};
+
+export default connect(mapStateToProps, {
+ showEditorCreateBreakpointContextMenu:
+ actions.showEditorCreateBreakpointContextMenu,
+ showEditorEditBreakpointContextMenu:
+ actions.showEditorEditBreakpointContextMenu,
+ toggleDisabledBreakpoint: actions.toggleDisabledBreakpoint,
+ removeBreakpoint: actions.removeBreakpoint,
+ addBreakpoint: actions.addBreakpoint,
+})(ColumnBreakpoints);
diff --git a/devtools/client/debugger/src/components/Editor/ConditionalPanel.css b/devtools/client/debugger/src/components/Editor/ConditionalPanel.css
new file mode 100644
index 0000000000..4ce8dbcd8c
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/ConditionalPanel.css
@@ -0,0 +1,39 @@
+/* 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/>. */
+
+.conditional-breakpoint-panel {
+ cursor: initial;
+ margin: 1em 0;
+ position: relative;
+ display: flex;
+ align-items: center;
+ background: var(--theme-toolbar-background);
+ border-top: 1px solid var(--theme-splitter-color);
+ border-bottom: 1px solid var(--theme-splitter-color);
+}
+
+.conditional-breakpoint-panel .prompt {
+ font-size: 1.8em;
+ color: var(--theme-graphs-orange);
+ padding-left: 3px;
+ padding-right: 3px;
+ padding-bottom: 3px;
+ text-align: right;
+ width: 30px;
+ align-self: baseline;
+ margin-top: 3px;
+}
+
+.conditional-breakpoint-panel.log-point .prompt {
+ color: var(--purple-60);
+}
+
+.conditional-breakpoint-panel .CodeMirror {
+ margin: 6px 10px;
+}
+
+.conditional-breakpoint-panel .CodeMirror pre.CodeMirror-placeholder {
+ /* Match the color of the placeholder text to existing inputs in the Debugger */
+ color: var(--theme-text-color-alt);
+}
diff --git a/devtools/client/debugger/src/components/Editor/ConditionalPanel.js b/devtools/client/debugger/src/components/Editor/ConditionalPanel.js
new file mode 100644
index 0000000000..8ff84c287a
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/ConditionalPanel.js
@@ -0,0 +1,280 @@
+/* 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 { PureComponent } from "devtools/client/shared/vendor/react";
+import {
+ div,
+ textarea,
+} from "devtools/client/shared/vendor/react-dom-factories";
+import ReactDOM from "devtools/client/shared/vendor/react-dom";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import { toEditorLine } from "../../utils/editor/index";
+import { prefs } from "../../utils/prefs";
+import actions from "../../actions/index";
+
+import {
+ getClosestBreakpoint,
+ getConditionalPanelLocation,
+ getLogPointStatus,
+} from "../../selectors/index";
+
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+function addNewLine(doc) {
+ const cursor = doc.getCursor();
+ const pos = { line: cursor.line, ch: cursor.ch };
+ doc.replaceRange("\n", pos);
+}
+
+export class ConditionalPanel extends PureComponent {
+ cbPanel;
+ input;
+ codeMirror;
+ panelNode;
+ scrollParent;
+
+ constructor() {
+ super();
+ this.cbPanel = null;
+ }
+
+ static get propTypes() {
+ return {
+ breakpoint: PropTypes.object,
+ closeConditionalPanel: PropTypes.func.isRequired,
+ editor: PropTypes.object.isRequired,
+ location: PropTypes.any.isRequired,
+ log: PropTypes.bool.isRequired,
+ openConditionalPanel: PropTypes.func.isRequired,
+ setBreakpointOptions: PropTypes.func.isRequired,
+ };
+ }
+
+ keepFocusOnInput() {
+ if (this.input) {
+ this.input.focus();
+ }
+ }
+
+ saveAndClose = () => {
+ if (this.input) {
+ this.setBreakpoint(this.input.value.trim());
+ }
+
+ this.props.closeConditionalPanel();
+ };
+
+ onKey = e => {
+ if (e.key === "Enter") {
+ if (this.codeMirror && e.altKey) {
+ addNewLine(this.codeMirror.doc);
+ } else {
+ this.saveAndClose();
+ }
+ } else if (e.key === "Escape") {
+ this.props.closeConditionalPanel();
+ }
+ };
+
+ setBreakpoint(value) {
+ const { log, breakpoint } = this.props;
+ // If breakpoint is `pending`, props will not contain a breakpoint.
+ // If source is a URL without location, breakpoint will contain no generatedLocation.
+ const location =
+ breakpoint && breakpoint.generatedLocation
+ ? breakpoint.generatedLocation
+ : this.props.location;
+ const options = breakpoint ? breakpoint.options : {};
+ const type = log ? "logValue" : "condition";
+ return this.props.setBreakpointOptions(location, {
+ ...options,
+ [type]: value,
+ });
+ }
+
+ clearConditionalPanel() {
+ if (this.cbPanel) {
+ this.cbPanel.clear();
+ this.cbPanel = null;
+ }
+ if (this.scrollParent) {
+ this.scrollParent.removeEventListener("scroll", this.repositionOnScroll);
+ }
+ }
+
+ repositionOnScroll = () => {
+ if (this.panelNode && this.scrollParent) {
+ const { scrollLeft } = this.scrollParent;
+ this.panelNode.style.transform = `translateX(${scrollLeft}px)`;
+ }
+ };
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillMount() {
+ return this.renderToWidget(this.props);
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillUpdate() {
+ return this.clearConditionalPanel();
+ }
+
+ componentDidUpdate(prevProps) {
+ this.keepFocusOnInput();
+ }
+
+ componentWillUnmount() {
+ // This is called if CodeMirror is re-initializing itself before the
+ // user closes the conditional panel. Clear the widget, and re-render it
+ // as soon as this component gets remounted
+ return this.clearConditionalPanel();
+ }
+
+ renderToWidget(props) {
+ if (this.cbPanel) {
+ this.clearConditionalPanel();
+ }
+ const { location, editor } = props;
+
+ const editorLine = toEditorLine(location.source.id, location.line || 0);
+ this.cbPanel = editor.codeMirror.addLineWidget(
+ editorLine,
+ this.renderConditionalPanel(props),
+ {
+ coverGutter: true,
+ noHScroll: true,
+ }
+ );
+
+ if (this.input) {
+ let parent = this.input.parentNode;
+ while (parent) {
+ if (
+ parent instanceof HTMLElement &&
+ parent.classList.contains("CodeMirror-scroll")
+ ) {
+ this.scrollParent = parent;
+ break;
+ }
+ parent = parent.parentNode;
+ }
+
+ if (this.scrollParent) {
+ this.scrollParent.addEventListener("scroll", this.repositionOnScroll);
+ this.repositionOnScroll();
+ }
+ }
+ }
+
+ createEditor = input => {
+ const { log, editor, closeConditionalPanel } = this.props;
+ const codeMirror = editor.CodeMirror.fromTextArea(input, {
+ mode: "javascript",
+ theme: "mozilla",
+ placeholder: L10N.getStr(
+ log
+ ? "editor.conditionalPanel.logPoint.placeholder2"
+ : "editor.conditionalPanel.placeholder2"
+ ),
+ cursorBlinkRate: prefs.cursorBlinkRate,
+ });
+
+ codeMirror.on("keydown", (cm, e) => {
+ if (e.key === "Enter") {
+ e.codemirrorIgnore = true;
+ }
+ });
+
+ codeMirror.on("blur", (cm, e) => {
+ if (
+ e?.relatedTarget &&
+ e.relatedTarget.closest(".conditional-breakpoint-panel")
+ ) {
+ return;
+ }
+
+ closeConditionalPanel();
+ });
+
+ const codeMirrorWrapper = codeMirror.getWrapperElement();
+
+ codeMirrorWrapper.addEventListener("keydown", e => {
+ codeMirror.save();
+ this.onKey(e);
+ });
+
+ this.input = input;
+ this.codeMirror = codeMirror;
+ codeMirror.focus();
+ codeMirror.setCursor(codeMirror.lineCount(), 0);
+ };
+
+ getDefaultValue() {
+ const { breakpoint, log } = this.props;
+ const options = breakpoint?.options || {};
+ return log ? options.logValue : options.condition;
+ }
+
+ renderConditionalPanel(props) {
+ const { log } = props;
+ const defaultValue = this.getDefaultValue();
+
+ const panel = document.createElement("div");
+ ReactDOM.render(
+ div(
+ {
+ className: classnames("conditional-breakpoint-panel", {
+ "log-point": log,
+ }),
+ onClick: () => this.keepFocusOnInput(),
+ ref: node => (this.panelNode = node),
+ },
+ div(
+ {
+ className: "prompt",
+ },
+ "»"
+ ),
+ textarea({
+ defaultValue,
+ ref: input => this.createEditor(input),
+ })
+ ),
+ panel
+ );
+ return panel;
+ }
+
+ render() {
+ return null;
+ }
+}
+
+const mapStateToProps = state => {
+ const location = getConditionalPanelLocation(state);
+
+ if (!location) {
+ throw new Error("Conditional panel location needed.");
+ }
+
+ const breakpoint = getClosestBreakpoint(state, location);
+
+ return {
+ breakpoint,
+ location,
+ log: getLogPointStatus(state),
+ };
+};
+
+const { setBreakpointOptions, openConditionalPanel, closeConditionalPanel } =
+ actions;
+
+const mapDispatchToProps = {
+ setBreakpointOptions,
+ openConditionalPanel,
+ closeConditionalPanel,
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(ConditionalPanel);
diff --git a/devtools/client/debugger/src/components/Editor/DebugLine.js b/devtools/client/debugger/src/components/Editor/DebugLine.js
new file mode 100644
index 0000000000..1b8e59ba64
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/DebugLine.js
@@ -0,0 +1,137 @@
+/* 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 { PureComponent } from "devtools/client/shared/vendor/react";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import {
+ toEditorPosition,
+ getDocument,
+ hasDocument,
+ startOperation,
+ endOperation,
+ getTokenEnd,
+} from "../../utils/editor/index";
+import { isException } from "../../utils/pause/index";
+import { getIndentation } from "../../utils/indentation";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import {
+ getVisibleSelectedFrame,
+ getPauseReason,
+ getSourceTextContent,
+ getCurrentThread,
+} from "../../selectors/index";
+
+export class DebugLine extends PureComponent {
+ debugExpression;
+
+ static get propTypes() {
+ return {
+ location: PropTypes.object,
+ why: PropTypes.object,
+ };
+ }
+
+ componentDidMount() {
+ const { why, location } = this.props;
+ this.setDebugLine(why, location);
+ }
+
+ componentWillUnmount() {
+ const { why, location } = this.props;
+ this.clearDebugLine(why, location);
+ }
+
+ componentDidUpdate(prevProps) {
+ const { why, location } = this.props;
+
+ startOperation();
+ this.clearDebugLine(prevProps.why, prevProps.location);
+ this.setDebugLine(why, location);
+ endOperation();
+ }
+
+ setDebugLine(why, location) {
+ if (!location) {
+ return;
+ }
+ const doc = getDocument(location.source.id);
+
+ let { line, column } = toEditorPosition(location);
+ let { markTextClass, lineClass } = this.getTextClasses(why);
+ doc.addLineClass(line, "wrap", lineClass);
+
+ const lineText = doc.getLine(line);
+ column = Math.max(column, getIndentation(lineText));
+
+ // If component updates because user clicks on
+ // another source tab, codeMirror will be null.
+ const columnEnd = doc.cm ? getTokenEnd(doc.cm, line, column) : null;
+
+ if (columnEnd === null) {
+ markTextClass += " to-line-end";
+ }
+
+ this.debugExpression = doc.markText(
+ { ch: column, line },
+ { ch: columnEnd, line },
+ { className: markTextClass }
+ );
+ }
+
+ clearDebugLine(why, location) {
+ // Avoid clearing the line if we didn't set a debug line before,
+ // or, if the document is no longer available
+ if (!location || !hasDocument(location.source.id)) {
+ return;
+ }
+
+ if (this.debugExpression) {
+ this.debugExpression.clear();
+ }
+
+ const { line } = toEditorPosition(location);
+ const doc = getDocument(location.source.id);
+ const { lineClass } = this.getTextClasses(why);
+ doc.removeLineClass(line, "wrap", lineClass);
+ }
+
+ getTextClasses(why) {
+ if (why && isException(why)) {
+ return {
+ markTextClass: "debug-expression-error",
+ lineClass: "new-debug-line-error",
+ };
+ }
+
+ return { markTextClass: "debug-expression", lineClass: "new-debug-line" };
+ }
+
+ render() {
+ return null;
+ }
+}
+
+function isDocumentReady(location, sourceTextContent) {
+ return location && sourceTextContent && hasDocument(location.source.id);
+}
+
+const mapStateToProps = state => {
+ // Avoid unecessary intermediate updates when there is no location
+ // or the source text content isn't yet fully loaded
+ const frame = getVisibleSelectedFrame(state);
+ const location = frame?.location;
+ if (!location) {
+ return {};
+ }
+ const sourceTextContent = getSourceTextContent(state, location);
+ if (!isDocumentReady(location, sourceTextContent)) {
+ return {};
+ }
+ return {
+ location,
+ why: getPauseReason(state, getCurrentThread(state)),
+ };
+};
+
+export default connect(mapStateToProps)(DebugLine);
diff --git a/devtools/client/debugger/src/components/Editor/Editor.css b/devtools/client/debugger/src/components/Editor/Editor.css
new file mode 100644
index 0000000000..0c48da019e
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/Editor.css
@@ -0,0 +1,216 @@
+/* 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/>. */
+
+.editor-wrapper {
+ --debug-line-border: rgb(145, 188, 219);
+ --debug-expression-background: rgba(202, 227, 255, 0.5);
+ --debug-line-error-border: rgb(255, 0, 0);
+ --debug-expression-error-background: rgba(231, 116, 113, 0.3);
+ --line-exception-background: hsl(344, 73%, 97%);
+ --highlight-line-duration: 5000ms;
+}
+
+.theme-dark .editor-wrapper {
+ --debug-expression-background: rgba(202, 227, 255, 0.3);
+ --debug-line-border: #7786a2;
+ --line-exception-background: hsl(345, 23%, 24%);
+}
+
+.editor-wrapper .CodeMirror-linewidget {
+ margin-right: -7px;
+}
+
+.editor-wrapper {
+ min-width: 0 !important;
+}
+
+.CodeMirror.cm-s-mozilla,
+.CodeMirror-scroll,
+.CodeMirror-sizer {
+ overflow-anchor: none;
+}
+
+/* Prevents inline preview from shifting source height (#1576163) */
+.CodeMirror-linewidget {
+ padding: 0;
+ display: flow-root;
+}
+
+/**
+ * There's a known codemirror flex issue with chrome that this addresses.
+ * BUG https://github.com/firefox-devtools/debugger/issues/63
+ */
+.editor-wrapper {
+ width: calc(100% - 1px);
+ overflow-y: auto;
+ grid-area: editor;
+}
+
+html[dir="rtl"] .editor-mount {
+ direction: ltr;
+}
+
+.function-search {
+ max-height: 300px;
+ overflow: hidden;
+}
+
+.function-search .results {
+ height: auto;
+}
+
+.editor.hit-marker {
+ height: 15px;
+}
+
+.editor-wrapper .highlight-lines {
+ background: var(--theme-selection-background-hover);
+}
+
+.CodeMirror {
+ width: 100%;
+ height: 100%;
+}
+
+.editor-wrapper .editor-mount {
+ width: 100%;
+ background-color: var(--theme-body-background);
+ font-size: var(--theme-code-font-size);
+ line-height: var(--theme-code-line-height);
+}
+
+/* set the linenumber white when there is a breakpoint */
+.editor-wrapper:not(.skip-pausing)
+ .new-breakpoint
+ .CodeMirror-gutter-wrapper
+ .CodeMirror-linenumber {
+ color: white;
+}
+
+/* move the breakpoint below the other gutter elements */
+.new-breakpoint .CodeMirror-gutter-elt:nth-child(2) {
+ z-index: 0;
+}
+
+.theme-dark .editor-wrapper .CodeMirror-line .cm-comment {
+ color: var(--theme-comment);
+}
+
+.debug-expression {
+ background-color: var(--debug-expression-background);
+ border-style: solid;
+ border-color: var(--debug-expression-background);
+ border-width: 1px 0px 1px 0px;
+ position: relative;
+}
+
+.debug-expression::before {
+ content: "";
+ line-height: 1px;
+ border-top: 1px solid var(--blue-50);
+ background: transparent;
+ position: absolute;
+ top: -2px;
+ left: 0px;
+ width: 100%;
+ }
+
+.debug-expression::after {
+ content: "";
+ line-height: 1px;
+ border-bottom: 1px solid var(--blue-50);
+ position: absolute;
+ bottom: -2px;
+ left: 0px;
+ width: 100%;
+ }
+
+.to-line-end ~ .CodeMirror-widget {
+ background-color: var(--debug-expression-background);
+}
+
+.debug-expression-error {
+ background-color: var(--debug-expression-error-background);
+}
+
+.new-debug-line > .CodeMirror-line {
+ background-color: transparent !important;
+ outline: var(--debug-line-border) solid 1px;
+}
+
+/* Don't display the highlight color since the debug line
+ is already highlighted */
+.new-debug-line .CodeMirror-activeline-background {
+ display: none;
+}
+
+.new-debug-line-error > .CodeMirror-line {
+ background-color: var(--debug-expression-error-background) !important;
+ outline: var(--debug-line-error-border) solid 1px;
+}
+
+/* Don't display the highlight color since the debug line
+ is already highlighted */
+.new-debug-line-error .CodeMirror-activeline-background {
+ display: none;
+}
+.highlight-line .CodeMirror-line {
+ animation-name: fade-highlight-out;
+ animation-duration: var(--highlight-line-duration);
+ animation-timing-function: ease-out;
+ animation-fill-mode: forwards;
+}
+
+@keyframes fade-highlight-out {
+ 0%, 30% {
+ /* We want to use a color with some transparency so text selection is visible through it */
+ background-color: var(--theme-contrast-background-alpha);
+ }
+ 100% {
+ background-color: transparent;
+ }
+}
+
+.visible {
+ visibility: visible;
+}
+
+/* Code folding */
+.editor-wrapper .CodeMirror-foldgutter-open {
+ color: var(--grey-40);
+}
+
+.editor-wrapper .CodeMirror-foldgutter-open,
+.editor-wrapper .CodeMirror-foldgutter-folded {
+ fill: var(--grey-40);
+}
+
+.editor-wrapper .CodeMirror-foldgutter-open::before,
+.editor-wrapper .CodeMirror-foldgutter-open::after {
+ border-top: none;
+}
+
+.editor-wrapper .CodeMirror-foldgutter-folded::before,
+.editor-wrapper .CodeMirror-foldgutter-folded::after {
+ border-left: none;
+}
+
+.editor-wrapper .CodeMirror-foldgutter .CodeMirror-guttermarker-subtle {
+ visibility: visible;
+}
+
+.editor-wrapper .CodeMirror-foldgutter .CodeMirror-linenumber {
+ text-align: left;
+ padding: 0 0 0 2px;
+}
+
+/* Exception line */
+.line-exception {
+ background-color: var(--line-exception-background);
+}
+
+.mark-text-exception {
+ text-decoration: var(--red-50) wavy underline;
+ text-decoration-skip-ink: none;
+}
diff --git a/devtools/client/debugger/src/components/Editor/EmptyLines.js b/devtools/client/debugger/src/components/Editor/EmptyLines.js
new file mode 100644
index 0000000000..31513408a8
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/EmptyLines.js
@@ -0,0 +1,91 @@
+/* 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 { connect } from "devtools/client/shared/vendor/react-redux";
+import { Component } from "devtools/client/shared/vendor/react";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import {
+ getSelectedSource,
+ getSelectedBreakableLines,
+} from "../../selectors/index";
+import { fromEditorLine } from "../../utils/editor/index";
+import { isWasm } from "../../utils/wasm";
+
+class EmptyLines extends Component {
+ static get propTypes() {
+ return {
+ breakableLines: PropTypes.object.isRequired,
+ editor: PropTypes.object.isRequired,
+ selectedSource: PropTypes.object.isRequired,
+ };
+ }
+
+ componentDidMount() {
+ this.disableEmptyLines();
+ }
+
+ componentDidUpdate() {
+ this.disableEmptyLines();
+ }
+
+ componentWillUnmount() {
+ const { editor } = this.props;
+
+ editor.codeMirror.operation(() => {
+ editor.codeMirror.eachLine(lineHandle => {
+ editor.codeMirror.removeLineClass(lineHandle, "wrap", "empty-line");
+ });
+ });
+ }
+
+ shouldComponentUpdate(nextProps) {
+ const { breakableLines, selectedSource } = this.props;
+ return (
+ // Breakable lines are something that evolves over time,
+ // but we either have them loaded or not. So only compare the size
+ // as sometimes we always get a blank new empty Set instance.
+ breakableLines.size != nextProps.breakableLines.size ||
+ selectedSource.id != nextProps.selectedSource.id
+ );
+ }
+
+ disableEmptyLines() {
+ const { breakableLines, selectedSource, editor } = this.props;
+
+ const { codeMirror } = editor;
+ const isSourceWasm = isWasm(selectedSource.id);
+
+ codeMirror.operation(() => {
+ const lineCount = codeMirror.lineCount();
+ for (let i = 0; i < lineCount; i++) {
+ const line = fromEditorLine(selectedSource.id, i, isSourceWasm);
+
+ if (breakableLines.has(line)) {
+ codeMirror.removeLineClass(i, "wrap", "empty-line");
+ } else {
+ codeMirror.addLineClass(i, "wrap", "empty-line");
+ }
+ }
+ });
+ }
+
+ render() {
+ return null;
+ }
+}
+
+const mapStateToProps = state => {
+ const selectedSource = getSelectedSource(state);
+ if (!selectedSource) {
+ throw new Error("no selectedSource");
+ }
+ const breakableLines = getSelectedBreakableLines(state);
+
+ return {
+ selectedSource,
+ breakableLines,
+ };
+};
+
+export default connect(mapStateToProps)(EmptyLines);
diff --git a/devtools/client/debugger/src/components/Editor/Exception.js b/devtools/client/debugger/src/components/Editor/Exception.js
new file mode 100644
index 0000000000..b76923e597
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/Exception.js
@@ -0,0 +1,102 @@
+/* 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 { PureComponent } from "devtools/client/shared/vendor/react";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+import {
+ toEditorPosition,
+ getTokenEnd,
+ hasDocument,
+} from "../../utils/editor/index";
+
+import { getIndentation } from "../../utils/indentation";
+import { createLocation } from "../../utils/location";
+
+export default class Exception extends PureComponent {
+ exceptionLine;
+ markText;
+
+ static get propTypes() {
+ return {
+ exception: PropTypes.object.isRequired,
+ doc: PropTypes.object.isRequired,
+ selectedSource: PropTypes.string.isRequired,
+ };
+ }
+
+ componentDidMount() {
+ this.addEditorExceptionLine();
+ }
+
+ componentDidUpdate() {
+ this.clearEditorExceptionLine();
+ this.addEditorExceptionLine();
+ }
+
+ componentWillUnmount() {
+ this.clearEditorExceptionLine();
+ }
+
+ setEditorExceptionLine(doc, line, column, lineText) {
+ doc.addLineClass(line, "wrap", "line-exception");
+
+ column = Math.max(column, getIndentation(lineText));
+ const columnEnd = doc.cm ? getTokenEnd(doc.cm, line, column) : null;
+
+ const markText = doc.markText(
+ { ch: column, line },
+ { ch: columnEnd, line },
+ { className: "mark-text-exception" }
+ );
+
+ this.exceptionLine = line;
+ this.markText = markText;
+ }
+
+ addEditorExceptionLine() {
+ const { exception, doc, selectedSource } = this.props;
+ const { columnNumber, lineNumber } = exception;
+
+ if (!hasDocument(selectedSource.id)) {
+ return;
+ }
+
+ const location = createLocation({
+ source: selectedSource,
+ line: lineNumber,
+ // Exceptions are reported with column being 1-based
+ // while the frontend uses 0-based column.
+ column: columnNumber - 1,
+ });
+
+ const { line, column } = toEditorPosition(location);
+ const lineText = doc.getLine(line);
+
+ this.setEditorExceptionLine(doc, line, column, lineText);
+ }
+
+ clearEditorExceptionLine() {
+ if (this.markText) {
+ const { selectedSource } = this.props;
+
+ this.markText.clear();
+
+ if (hasDocument(selectedSource.id)) {
+ this.props.doc.removeLineClass(
+ this.exceptionLine,
+ "wrap",
+ "line-exception"
+ );
+ }
+ this.exceptionLine = null;
+ this.markText = null;
+ }
+ }
+
+ // This component is only used as a "proxy" to manipulate the editor.
+ render() {
+ return null;
+ }
+}
diff --git a/devtools/client/debugger/src/components/Editor/Exceptions.js b/devtools/client/debugger/src/components/Editor/Exceptions.js
new file mode 100644
index 0000000000..2fb183f135
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/Exceptions.js
@@ -0,0 +1,66 @@
+/* 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 React, { Component } from "devtools/client/shared/vendor/react";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+
+import Exception from "./Exception";
+
+import {
+ getSelectedSource,
+ getSelectedSourceExceptions,
+} from "../../selectors/index";
+import { getDocument } from "../../utils/editor/index";
+
+class Exceptions extends Component {
+ static get propTypes() {
+ return {
+ exceptions: PropTypes.array,
+ selectedSource: PropTypes.object,
+ };
+ }
+
+ render() {
+ const { exceptions, selectedSource } = this.props;
+
+ if (!selectedSource || !exceptions.length) {
+ return null;
+ }
+
+ const doc = getDocument(selectedSource.id);
+ return React.createElement(
+ React.Fragment,
+ null,
+ exceptions.map(exception =>
+ React.createElement(Exception, {
+ exception,
+ doc,
+ key: `${exception.sourceActorId}:${exception.lineNumber}`,
+ selectedSource,
+ })
+ )
+ );
+ }
+}
+
+export default connect(state => {
+ const selectedSource = getSelectedSource(state);
+
+ // Avoid calling getSelectedSourceExceptions when there is no source selected.
+ if (!selectedSource) {
+ return {};
+ }
+
+ // Avoid causing any update until we start having exceptions
+ const exceptions = getSelectedSourceExceptions(state);
+ if (!exceptions.length) {
+ return {};
+ }
+
+ return {
+ exceptions: getSelectedSourceExceptions(state),
+ selectedSource,
+ };
+})(Exceptions);
diff --git a/devtools/client/debugger/src/components/Editor/Footer.css b/devtools/client/debugger/src/components/Editor/Footer.css
new file mode 100644
index 0000000000..4a3272879b
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/Footer.css
@@ -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/>. */
+
+.source-footer {
+ background: var(--theme-body-background);
+ border-top: 1px solid var(--theme-splitter-color);
+ grid-area: editor-footer;
+ display: flex;
+ opacity: 1;
+ width: calc(100% - 1px);
+ user-select: none;
+ height: var(--editor-footer-height);
+ box-sizing: border-box;
+}
+
+.source-footer button {
+ outline-offset: -2px;
+}
+
+.source-footer-start {
+ display: flex;
+ align-items: center;
+ justify-self: start;
+}
+
+.source-footer-end {
+ display: flex;
+ margin-left: auto;
+}
+
+.source-footer .commands * {
+ user-select: none;
+}
+
+.source-footer .commands {
+ display: flex;
+}
+
+.source-footer .commands .action {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ transition: opacity 200ms;
+ border: none;
+ background: transparent;
+ padding: 4px 6px;
+}
+
+.source-footer .commands button.action:hover {
+ background: var(--theme-toolbar-background-hover);
+}
+
+:root.theme-dark .source-footer .commands .action {
+ fill: var(--theme-body-color);
+}
+
+:root.theme-dark .source-footer .commands .action:hover {
+ fill: var(--theme-selection-color);
+}
+
+.source-footer .blackboxed .img.blackBox {
+ background-color: #806414;
+}
+
+.source-footer .commands button.prettyPrint:disabled {
+ opacity: 0.6;
+}
+
+.source-footer .mapped-source,
+.source-footer .cursor-position {
+ color: var(--theme-body-color);
+ padding-right: 2.5px;
+}
+
+.source-footer .mapped-source {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.source-footer .cursor-position {
+ padding: 5px;
+ white-space: nowrap;
+}
diff --git a/devtools/client/debugger/src/components/Editor/Footer.js b/devtools/client/debugger/src/components/Editor/Footer.js
new file mode 100644
index 0000000000..c4ff02caf4
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/Footer.js
@@ -0,0 +1,288 @@
+/* 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 React, { PureComponent } from "devtools/client/shared/vendor/react";
+import {
+ div,
+ button,
+ span,
+} from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import actions from "../../actions/index";
+import {
+ getSelectedSource,
+ getSelectedLocation,
+ getSelectedSourceTextContent,
+ getPrettySource,
+ getPaneCollapse,
+ isSourceBlackBoxed,
+ canPrettyPrintSource,
+ getPrettyPrintMessage,
+ isSourceOnSourceMapIgnoreList,
+ isSourceMapIgnoreListEnabled,
+ getSelectedMappedSource,
+} from "../../selectors/index";
+
+import { isPretty, getFilename, shouldBlackbox } from "../../utils/source";
+
+import { PaneToggleButton } from "../shared/Button/index";
+import AccessibleImage from "../shared/AccessibleImage";
+
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+class SourceFooter extends PureComponent {
+ static get propTypes() {
+ return {
+ canPrettyPrint: PropTypes.bool.isRequired,
+ prettyPrintMessage: PropTypes.string.isRequired,
+ endPanelCollapsed: PropTypes.bool.isRequired,
+ horizontal: PropTypes.bool.isRequired,
+ jumpToMappedLocation: PropTypes.func.isRequired,
+ mappedSource: PropTypes.object,
+ selectedSource: PropTypes.object,
+ selectedLocation: PropTypes.object,
+ isSelectedSourceBlackBoxed: PropTypes.bool.isRequired,
+ sourceLoaded: PropTypes.bool.isRequired,
+ toggleBlackBox: PropTypes.func.isRequired,
+ togglePaneCollapse: PropTypes.func.isRequired,
+ prettyPrintAndSelectSource: PropTypes.func.isRequired,
+ isSourceOnIgnoreList: PropTypes.bool.isRequired,
+ };
+ }
+
+ prettyPrintButton() {
+ const {
+ selectedSource,
+ canPrettyPrint,
+ prettyPrintMessage,
+ prettyPrintAndSelectSource,
+ sourceLoaded,
+ } = this.props;
+
+ if (!selectedSource) {
+ return null;
+ }
+
+ if (!sourceLoaded && selectedSource.isPrettyPrinted) {
+ return div(
+ {
+ className: "action",
+ key: "pretty-loader",
+ },
+ React.createElement(AccessibleImage, {
+ className: "loader spin",
+ })
+ );
+ }
+
+ const type = "prettyPrint";
+ return button(
+ {
+ onClick: () => {
+ if (!canPrettyPrint) {
+ return;
+ }
+ prettyPrintAndSelectSource(selectedSource);
+ },
+ className: classnames("action", type, {
+ active: sourceLoaded && canPrettyPrint,
+ pretty: isPretty(selectedSource),
+ }),
+ key: type,
+ title: prettyPrintMessage,
+ "aria-label": prettyPrintMessage,
+ disabled: !canPrettyPrint,
+ },
+ React.createElement(AccessibleImage, {
+ className: type,
+ })
+ );
+ }
+
+ blackBoxButton() {
+ const {
+ selectedSource,
+ isSelectedSourceBlackBoxed,
+ toggleBlackBox,
+ sourceLoaded,
+ isSourceOnIgnoreList,
+ } = this.props;
+
+ if (!selectedSource || !shouldBlackbox(selectedSource)) {
+ return null;
+ }
+
+ let tooltip = isSelectedSourceBlackBoxed
+ ? L10N.getStr("sourceFooter.unignore")
+ : L10N.getStr("sourceFooter.ignore");
+
+ if (isSourceOnIgnoreList) {
+ tooltip = L10N.getStr("sourceFooter.ignoreList");
+ }
+
+ const type = "black-box";
+ return button(
+ {
+ onClick: () => toggleBlackBox(selectedSource),
+ className: classnames("action", type, {
+ active: sourceLoaded,
+ blackboxed: isSelectedSourceBlackBoxed || isSourceOnIgnoreList,
+ }),
+ key: type,
+ title: tooltip,
+ "aria-label": tooltip,
+ disabled: isSourceOnIgnoreList,
+ },
+ React.createElement(AccessibleImage, {
+ className: "blackBox",
+ })
+ );
+ }
+
+ renderToggleButton() {
+ if (this.props.horizontal) {
+ return null;
+ }
+ return React.createElement(PaneToggleButton, {
+ key: "toggle",
+ collapsed: this.props.endPanelCollapsed,
+ horizontal: this.props.horizontal,
+ handleClick: this.props.togglePaneCollapse,
+ position: "end",
+ });
+ }
+
+ renderCommands() {
+ const commands = [this.blackBoxButton(), this.prettyPrintButton()].filter(
+ Boolean
+ );
+
+ return commands.length
+ ? div(
+ {
+ className: "commands",
+ },
+ commands
+ )
+ : null;
+ }
+
+ renderSourceSummary() {
+ const { mappedSource, jumpToMappedLocation, selectedLocation } = this.props;
+
+ if (!mappedSource) {
+ return null;
+ }
+
+ const tooltip = L10N.getFormatStr(
+ mappedSource.isOriginal
+ ? "sourceFooter.mappedOriginalSource.tooltip"
+ : "sourceFooter.mappedGeneratedSource.tooltip",
+ mappedSource.url
+ );
+ const filename = getFilename(mappedSource);
+ const label = L10N.getFormatStr(
+ mappedSource.isOriginal
+ ? "sourceFooter.mappedOriginalSource.title"
+ : "sourceFooter.mappedGeneratedSource.title",
+ filename
+ );
+ return button(
+ {
+ className: "mapped-source",
+ onClick: () => jumpToMappedLocation(selectedLocation),
+ title: tooltip,
+ },
+ span(null, label)
+ );
+ }
+
+ renderCursorPosition() {
+ // When we open a new source, there is no particular location selected and the line will be set to zero or falsy
+ if (!this.props.selectedLocation || !this.props.selectedLocation.line) {
+ return null;
+ }
+
+ // Note that line is 1-based while column is 0-based.
+ const { line, column } = this.props.selectedLocation;
+
+ const text = L10N.getFormatStr(
+ "sourceFooter.currentCursorPosition",
+ line,
+ column + 1
+ );
+ const title = L10N.getFormatStr(
+ "sourceFooter.currentCursorPosition.tooltip",
+ line,
+ column + 1
+ );
+ return div(
+ {
+ className: "cursor-position",
+ title,
+ },
+ text
+ );
+ }
+
+ render() {
+ return div(
+ {
+ className: "source-footer",
+ },
+ div(
+ {
+ className: "source-footer-start",
+ },
+ this.renderCommands()
+ ),
+ div(
+ {
+ className: "source-footer-end",
+ },
+ this.renderSourceSummary(),
+ this.renderCursorPosition(),
+ this.renderToggleButton()
+ )
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ const selectedSource = getSelectedSource(state);
+ const selectedLocation = getSelectedLocation(state);
+ const sourceTextContent = getSelectedSourceTextContent(state);
+
+ return {
+ selectedSource,
+ selectedLocation,
+ isSelectedSourceBlackBoxed: selectedSource
+ ? isSourceBlackBoxed(state, selectedSource)
+ : null,
+ isSourceOnIgnoreList:
+ isSourceMapIgnoreListEnabled(state) &&
+ isSourceOnSourceMapIgnoreList(state, selectedSource),
+ sourceLoaded: !!sourceTextContent,
+ mappedSource: getSelectedMappedSource(state),
+ prettySource: getPrettySource(
+ state,
+ selectedSource ? selectedSource.id : null
+ ),
+ endPanelCollapsed: getPaneCollapse(state, "end"),
+ canPrettyPrint: selectedLocation
+ ? canPrettyPrintSource(state, selectedLocation)
+ : false,
+ prettyPrintMessage: selectedLocation
+ ? getPrettyPrintMessage(state, selectedLocation)
+ : null,
+ };
+};
+
+export default connect(mapStateToProps, {
+ prettyPrintAndSelectSource: actions.prettyPrintAndSelectSource,
+ toggleBlackBox: actions.toggleBlackBox,
+ jumpToMappedLocation: actions.jumpToMappedLocation,
+ togglePaneCollapse: actions.togglePaneCollapse,
+})(SourceFooter);
diff --git a/devtools/client/debugger/src/components/Editor/HighlightLine.js b/devtools/client/debugger/src/components/Editor/HighlightLine.js
new file mode 100644
index 0000000000..8639128905
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/HighlightLine.js
@@ -0,0 +1,194 @@
+/* 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 { Component } from "devtools/client/shared/vendor/react";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import {
+ toEditorLine,
+ endOperation,
+ startOperation,
+} from "../../utils/editor/index";
+import { getDocument, hasDocument } from "../../utils/editor/source-documents";
+
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import {
+ getVisibleSelectedFrame,
+ getSelectedLocation,
+ getSelectedSourceTextContent,
+ getPauseCommand,
+ getCurrentThread,
+ getShouldHighlightSelectedLocation,
+} from "../../selectors/index";
+
+function isDebugLine(selectedFrame, selectedLocation) {
+ if (!selectedFrame) {
+ return false;
+ }
+
+ return (
+ selectedFrame.location.source.id == selectedLocation.source.id &&
+ selectedFrame.location.line == selectedLocation.line
+ );
+}
+
+function isDocumentReady(selectedLocation, selectedSourceTextContent) {
+ return (
+ selectedLocation &&
+ selectedSourceTextContent &&
+ hasDocument(selectedLocation.source.id)
+ );
+}
+
+export class HighlightLine extends Component {
+ isStepping = false;
+ previousEditorLine = null;
+
+ static get propTypes() {
+ return {
+ pauseCommand: PropTypes.oneOf([
+ "expression",
+ "resume",
+ "stepOver",
+ "stepIn",
+ "stepOut",
+ ]),
+ selectedFrame: PropTypes.object,
+ selectedLocation: PropTypes.object.isRequired,
+ selectedSourceTextContent: PropTypes.object.isRequired,
+ };
+ }
+
+ shouldComponentUpdate(nextProps) {
+ const { selectedLocation, selectedSourceTextContent } = nextProps;
+ return this.shouldSetHighlightLine(
+ selectedLocation,
+ selectedSourceTextContent
+ );
+ }
+
+ componentDidUpdate(prevProps) {
+ this.completeHighlightLine(prevProps);
+ }
+
+ componentDidMount() {
+ this.completeHighlightLine(null);
+ }
+
+ shouldSetHighlightLine(selectedLocation, selectedSourceTextContent) {
+ const { line } = selectedLocation;
+ const editorLine = toEditorLine(selectedLocation.source.id, line);
+
+ if (!isDocumentReady(selectedLocation, selectedSourceTextContent)) {
+ return false;
+ }
+
+ if (this.isStepping && editorLine === this.previousEditorLine) {
+ return false;
+ }
+
+ return true;
+ }
+
+ completeHighlightLine(prevProps) {
+ const {
+ pauseCommand,
+ selectedLocation,
+ selectedFrame,
+ selectedSourceTextContent,
+ shouldHighlightSelectedLocation,
+ } = this.props;
+ if (pauseCommand) {
+ this.isStepping = true;
+ }
+
+ startOperation();
+ if (prevProps) {
+ this.clearHighlightLine(
+ prevProps.selectedLocation,
+ prevProps.selectedSourceTextContent
+ );
+ }
+ if (shouldHighlightSelectedLocation) {
+ this.setHighlightLine(
+ selectedLocation,
+ selectedFrame,
+ selectedSourceTextContent
+ );
+ }
+ endOperation();
+ }
+
+ setHighlightLine(selectedLocation, selectedFrame, selectedSourceTextContent) {
+ const { line } = selectedLocation;
+ if (
+ !this.shouldSetHighlightLine(selectedLocation, selectedSourceTextContent)
+ ) {
+ return;
+ }
+
+ this.isStepping = false;
+ const sourceId = selectedLocation.source.id;
+ const editorLine = toEditorLine(sourceId, line);
+ this.previousEditorLine = editorLine;
+
+ if (!line || isDebugLine(selectedFrame, selectedLocation)) {
+ return;
+ }
+
+ const doc = getDocument(sourceId);
+ doc.addLineClass(editorLine, "wrap", "highlight-line");
+ this.resetHighlightLine(doc, editorLine);
+ }
+
+ resetHighlightLine(doc, editorLine) {
+ const editorWrapper = document.querySelector(".editor-wrapper");
+
+ if (editorWrapper === null) {
+ return;
+ }
+
+ const duration = parseInt(
+ getComputedStyle(editorWrapper).getPropertyValue(
+ "--highlight-line-duration"
+ ),
+ 10
+ );
+
+ setTimeout(
+ () => doc && doc.removeLineClass(editorLine, "wrap", "highlight-line"),
+ duration
+ );
+ }
+
+ clearHighlightLine(selectedLocation, selectedSourceTextContent) {
+ if (!isDocumentReady(selectedLocation, selectedSourceTextContent)) {
+ return;
+ }
+
+ const { line } = selectedLocation;
+ const sourceId = selectedLocation.source.id;
+ const editorLine = toEditorLine(sourceId, line);
+ const doc = getDocument(sourceId);
+ doc.removeLineClass(editorLine, "wrap", "highlight-line");
+ }
+
+ render() {
+ return null;
+ }
+}
+
+export default connect(state => {
+ const selectedLocation = getSelectedLocation(state);
+
+ if (!selectedLocation) {
+ throw new Error("must have selected location");
+ }
+ return {
+ pauseCommand: getPauseCommand(state, getCurrentThread(state)),
+ shouldHighlightSelectedLocation: getShouldHighlightSelectedLocation(state),
+ selectedFrame: getVisibleSelectedFrame(state),
+ selectedLocation,
+ selectedSourceTextContent: getSelectedSourceTextContent(state),
+ };
+})(HighlightLine);
diff --git a/devtools/client/debugger/src/components/Editor/HighlightLines.js b/devtools/client/debugger/src/components/Editor/HighlightLines.js
new file mode 100644
index 0000000000..e34a86aba9
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/HighlightLines.js
@@ -0,0 +1,74 @@
+/* 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 { Component } from "devtools/client/shared/vendor/react";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+class HighlightLines extends Component {
+ static get propTypes() {
+ return {
+ editor: PropTypes.object.isRequired,
+ range: PropTypes.object.isRequired,
+ };
+ }
+
+ componentDidMount() {
+ this.highlightLineRange();
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillUpdate() {
+ this.clearHighlightRange();
+ }
+
+ componentDidUpdate() {
+ this.highlightLineRange();
+ }
+
+ componentWillUnmount() {
+ this.clearHighlightRange();
+ }
+
+ clearHighlightRange() {
+ const { range, editor } = this.props;
+
+ const { codeMirror } = editor;
+
+ if (!range || !codeMirror) {
+ return;
+ }
+
+ const { start, end } = range;
+ codeMirror.operation(() => {
+ for (let line = start - 1; line < end; line++) {
+ codeMirror.removeLineClass(line, "wrap", "highlight-lines");
+ }
+ });
+ }
+
+ highlightLineRange = () => {
+ const { range, editor } = this.props;
+
+ const { codeMirror } = editor;
+
+ if (!range || !codeMirror) {
+ return;
+ }
+
+ const { start, end } = range;
+
+ codeMirror.operation(() => {
+ editor.alignLine(start);
+ for (let line = start - 1; line < end; line++) {
+ codeMirror.addLineClass(line, "wrap", "highlight-lines");
+ }
+ });
+ };
+
+ render() {
+ return null;
+ }
+}
+
+export default HighlightLines;
diff --git a/devtools/client/debugger/src/components/Editor/InlinePreview.css b/devtools/client/debugger/src/components/Editor/InlinePreview.css
new file mode 100644
index 0000000000..13f1b5e23c
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/InlinePreview.css
@@ -0,0 +1,29 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+.inline-preview {
+ display: inline-block;
+ margin-inline-start: 8px;
+ user-select: none;
+}
+
+.inline-preview-outer {
+ background-color: var(--theme-inline-preview-background);
+ border: 1px solid var(--theme-inline-preview-border-color);
+ border-radius: 3px;
+ font-size: 10px;
+ margin-right: 5px;
+ white-space: nowrap;
+}
+
+.inline-preview-label {
+ padding: 0px 2px 0px 4px;
+ border-radius: 2px 0 0 2px;
+ color: var(--theme-inline-preview-label-color);
+ background-color: var(--theme-inline-preview-label-background);
+}
+
+.inline-preview-value {
+ padding: 2px 6px;
+}
diff --git a/devtools/client/debugger/src/components/Editor/InlinePreview.js b/devtools/client/debugger/src/components/Editor/InlinePreview.js
new file mode 100644
index 0000000000..552143dcf2
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/InlinePreview.js
@@ -0,0 +1,74 @@
+/* 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 React, { PureComponent } from "devtools/client/shared/vendor/react";
+import { span } from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import Reps from "devtools/client/shared/components/reps/index";
+
+const {
+ REPS: {
+ Rep,
+ ElementNode: { supportsObject: isElement },
+ },
+ MODE,
+} = Reps;
+
+// Renders single variable preview inside a codemirror line widget
+class InlinePreview extends PureComponent {
+ static get propTypes() {
+ return {
+ highlightDomElement: PropTypes.func.isRequired,
+ openElementInInspector: PropTypes.func.isRequired,
+ unHighlightDomElement: PropTypes.func.isRequired,
+ value: PropTypes.any,
+ variable: PropTypes.string.isRequired,
+ };
+ }
+
+ showInScopes(variable) {
+ // TODO: focus on variable value in the scopes sidepanel
+ // we will need more info from parent comp
+ }
+
+ render() {
+ const {
+ value,
+ variable,
+ openElementInInspector,
+ highlightDomElement,
+ unHighlightDomElement,
+ } = this.props;
+
+ const mode = isElement(value) ? MODE.TINY : MODE.SHORT;
+ return span(
+ {
+ className: "inline-preview-outer",
+ onClick: () => this.showInScopes(variable),
+ },
+ span(
+ {
+ className: "inline-preview-label",
+ },
+ variable,
+ ":"
+ ),
+ span(
+ {
+ className: "inline-preview-value",
+ },
+ React.createElement(Rep, {
+ object: value,
+ mode,
+ onDOMNodeClick: grip => openElementInInspector(grip),
+ onInspectIconClick: grip => openElementInInspector(grip),
+ onDOMNodeMouseOver: grip => highlightDomElement(grip),
+ onDOMNodeMouseOut: grip => unHighlightDomElement(grip),
+ })
+ )
+ );
+ }
+}
+
+export default InlinePreview;
diff --git a/devtools/client/debugger/src/components/Editor/InlinePreviewRow.js b/devtools/client/debugger/src/components/Editor/InlinePreviewRow.js
new file mode 100644
index 0000000000..bc54fc5b4d
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/InlinePreviewRow.js
@@ -0,0 +1,103 @@
+/* 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 React, { PureComponent } from "devtools/client/shared/vendor/react";
+import ReactDOM from "devtools/client/shared/vendor/react-dom";
+
+import actions from "../../actions/index";
+import assert from "../../utils/assert";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import InlinePreview from "./InlinePreview";
+
+// Handles rendering for each line ( row )
+// * Renders single widget for each line in codemirror
+// * Renders InlinePreview for each preview inside the widget
+class InlinePreviewRow extends PureComponent {
+ bookmark;
+ widgetNode;
+
+ componentDidMount() {
+ this.updatePreviewWidget(this.props, null);
+ }
+
+ componentDidUpdate(prevProps) {
+ this.updatePreviewWidget(this.props, prevProps);
+ }
+
+ componentWillUnmount() {
+ this.updatePreviewWidget(null, this.props);
+ }
+
+ updatePreviewWidget(props, prevProps) {
+ if (
+ this.bookmark &&
+ prevProps &&
+ (!props ||
+ prevProps.editor !== props.editor ||
+ prevProps.line !== props.line)
+ ) {
+ this.bookmark.clear();
+ this.bookmark = null;
+ this.widgetNode = null;
+ }
+
+ if (!props) {
+ assert(!this.bookmark, "Inline Preview widget shouldn't be present.");
+ return;
+ }
+
+ const {
+ editor,
+ line,
+ previews,
+ openElementInInspector,
+ highlightDomElement,
+ unHighlightDomElement,
+ } = props;
+
+ if (!this.bookmark) {
+ this.widgetNode = document.createElement("div");
+ this.widgetNode.classList.add("inline-preview");
+ }
+
+ ReactDOM.render(
+ React.createElement(
+ React.Fragment,
+ null,
+ previews.map(preview =>
+ React.createElement(InlinePreview, {
+ line: line,
+ key: `${line}-${preview.name}`,
+ variable: preview.name,
+ value: preview.value,
+ openElementInInspector: openElementInInspector,
+ highlightDomElement: highlightDomElement,
+ unHighlightDomElement: unHighlightDomElement,
+ })
+ )
+ ),
+ this.widgetNode,
+ () => {
+ // Only set the codeMirror bookmark once React rendered the element into this.widgetNode
+ this.bookmark = editor.codeMirror.setBookmark(
+ {
+ line,
+ ch: Infinity,
+ },
+ this.widgetNode
+ );
+ }
+ );
+ }
+
+ render() {
+ return null;
+ }
+}
+
+export default connect(() => ({}), {
+ openElementInInspector: actions.openElementInInspectorCommand,
+ highlightDomElement: actions.highlightDomElement,
+ unHighlightDomElement: actions.unHighlightDomElement,
+})(InlinePreviewRow);
diff --git a/devtools/client/debugger/src/components/Editor/InlinePreviews.js b/devtools/client/debugger/src/components/Editor/InlinePreviews.js
new file mode 100644
index 0000000000..18616ae3ed
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/InlinePreviews.js
@@ -0,0 +1,80 @@
+/* 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 React, { Component } from "devtools/client/shared/vendor/react";
+import { div } from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import InlinePreviewRow from "./InlinePreviewRow";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import {
+ getSelectedFrame,
+ getCurrentThread,
+ getInlinePreviews,
+} from "../../selectors/index";
+
+function hasPreviews(previews) {
+ return !!previews && !!Object.keys(previews).length;
+}
+
+class InlinePreviews extends Component {
+ static get propTypes() {
+ return {
+ editor: PropTypes.object.isRequired,
+ previews: PropTypes.object,
+ selectedFrame: PropTypes.object.isRequired,
+ selectedSource: PropTypes.object.isRequired,
+ };
+ }
+
+ shouldComponentUpdate({ previews }) {
+ return hasPreviews(previews);
+ }
+
+ render() {
+ const { editor, selectedFrame, selectedSource, previews } = this.props;
+
+ // Render only if currently open file is the one where debugger is paused
+ if (
+ !selectedFrame ||
+ selectedFrame.location.source.id !== selectedSource.id ||
+ !hasPreviews(previews)
+ ) {
+ return null;
+ }
+ const previewsObj = previews;
+
+ let inlinePreviewRows;
+ editor.codeMirror.operation(() => {
+ inlinePreviewRows = Object.keys(previewsObj).map(line => {
+ const lineNum = parseInt(line, 10);
+ return React.createElement(InlinePreviewRow, {
+ editor: editor,
+ key: line,
+ line: lineNum,
+ previews: previewsObj[line],
+ });
+ });
+ });
+ return div(null, inlinePreviewRows);
+ }
+}
+
+const mapStateToProps = state => {
+ const thread = getCurrentThread(state);
+ const selectedFrame = getSelectedFrame(state, thread);
+
+ if (!selectedFrame) {
+ return {
+ selectedFrame: null,
+ previews: null,
+ };
+ }
+
+ return {
+ selectedFrame,
+ previews: getInlinePreviews(state, thread, selectedFrame.id),
+ };
+};
+
+export default connect(mapStateToProps)(InlinePreviews);
diff --git a/devtools/client/debugger/src/components/Editor/Preview.css b/devtools/client/debugger/src/components/Editor/Preview.css
new file mode 100644
index 0000000000..7e3d788c68
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/Preview.css
@@ -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/>. */
+
+.popover .preview {
+ background: var(--theme-body-background);
+ width: 350px;
+ border: 1px solid var(--theme-splitter-color);
+ padding: 10px;
+ height: auto;
+ min-height: inherit;
+ max-height: 200px;
+ overflow: auto;
+ box-shadow: 1px 2px 3px var(--popup-shadow-color);
+}
+
+.theme-dark .popover .preview {
+ box-shadow: 1px 2px 3px var(--popup-shadow-color);
+}
+
+.popover .preview .header {
+ width: 100%;
+ line-height: 20px;
+ border-bottom: 1px solid #cccccc;
+ display: flex;
+ flex-direction: column;
+}
+
+.popover .preview .header .link {
+ align-self: flex-end;
+ color: var(--theme-link-color);
+ text-decoration: underline;
+}
+
+.selection,
+.debug-expression.selection {
+ background-color: var(--theme-highlight-yellow);
+}
+
+.theme-dark .selection,
+.theme-dark .debug-expression.selection {
+ background-color: #743884;
+}
+
+.theme-dark .cm-s-mozilla .selection,
+.theme-dark .cm-s-mozilla .debug-expression.selection {
+ color: #e7ebee;
+}
+
+.popover .preview .function-signature {
+ padding-top: 10px;
+}
+
+.theme-dark .popover .preview {
+ border-color: var(--theme-body-color);
+}
+
+.tooltip {
+ position: fixed;
+ z-index: 100;
+}
+
+.tooltip .preview {
+ background: var(--theme-toolbar-background);
+ max-width: inherit;
+ border: 1px solid var(--theme-splitter-color);
+ box-shadow: 1px 2px 4px 1px var(--theme-toolbar-background-alt);
+ padding: 5px;
+ height: auto;
+ min-height: inherit;
+ max-height: 200px;
+ overflow: auto;
+}
+
+.theme-dark .tooltip .preview {
+ border-color: var(--theme-body-color);
+}
+
+.tooltip .gap {
+ height: 4px;
+ padding-top: 4px;
+}
+
+.add-to-expression-bar {
+ border: 1px solid var(--theme-splitter-color);
+ border-top: none;
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ font-size: 14px;
+ line-height: 30px;
+ background: var(--theme-toolbar-background);
+ color: var(--theme-text-color-inactive);
+ padding: 0 4px;
+}
+
+.add-to-expression-bar .prompt {
+ width: 1em;
+}
+
+.add-to-expression-bar .expression-to-save-label {
+ width: calc(100% - 4em);
+}
+
+.add-to-expression-bar .expression-to-save-button {
+ font-size: 14px;
+ color: var(--theme-comment);
+}
diff --git a/devtools/client/debugger/src/components/Editor/Preview/ExceptionPopup.js b/devtools/client/debugger/src/components/Editor/Preview/ExceptionPopup.js
new file mode 100644
index 0000000000..0789b82694
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/Preview/ExceptionPopup.js
@@ -0,0 +1,142 @@
+/* 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 React, { Component } from "devtools/client/shared/vendor/react";
+import { div, span } from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+
+import Reps from "devtools/client/shared/components/reps/index";
+const {
+ REPS: { StringRep },
+} = Reps;
+
+import actions from "../../../actions/index";
+
+import AccessibleImage from "../../shared/AccessibleImage";
+const classnames = require("resource://devtools/client/shared/classnames.js");
+const ANONYMOUS_FN_NAME = "<anonymous>";
+
+// The exception popup works in two modes:
+// a. when the stacktrace is closed the exception popup
+// gets closed when the mouse leaves the popup.
+// b. when the stacktrace is opened the exception popup
+// gets closed only by clicking outside the popup.
+class ExceptionPopup extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ isStacktraceExpanded: true,
+ };
+ }
+
+ static get propTypes() {
+ return {
+ mouseout: PropTypes.func.isRequired,
+ selectSourceURL: PropTypes.func.isRequired,
+ exception: PropTypes.object.isRequired,
+ };
+ }
+
+ onExceptionMessageClick() {
+ const isStacktraceExpanded = this.state.isStacktraceExpanded;
+ this.setState({ isStacktraceExpanded: !isStacktraceExpanded });
+ }
+
+ buildStackFrame(frame) {
+ const { filename, lineNumber } = frame;
+ const functionName = frame.functionName || ANONYMOUS_FN_NAME;
+ return div(
+ {
+ className: "frame",
+ onClick: () =>
+ this.props.selectSourceURL(filename, {
+ line: lineNumber,
+ }),
+ },
+ span(
+ {
+ className: "title",
+ },
+ functionName
+ ),
+ span(
+ {
+ className: "location",
+ },
+ span(
+ {
+ className: "filename",
+ },
+ filename
+ ),
+ ":",
+ span(
+ {
+ className: "line",
+ },
+ lineNumber
+ )
+ )
+ );
+ }
+
+ renderStacktrace(stacktrace) {
+ const isStacktraceExpanded = this.state.isStacktraceExpanded;
+
+ if (stacktrace.length && isStacktraceExpanded) {
+ return div(
+ {
+ className: "exception-stacktrace",
+ },
+ stacktrace.map(frame => this.buildStackFrame(frame))
+ );
+ }
+ return null;
+ }
+
+ renderArrowIcon(stacktrace) {
+ if (stacktrace.length) {
+ return React.createElement(AccessibleImage, {
+ className: classnames("arrow", {
+ expanded: this.state.isStacktraceExpanded,
+ }),
+ });
+ }
+ return null;
+ }
+
+ render() {
+ const {
+ exception: { stacktrace, errorMessage },
+ mouseout,
+ } = this.props;
+ return div(
+ {
+ className: "preview-popup exception-popup",
+ dir: "ltr",
+ onMouseLeave: () => mouseout(true, this.state.isStacktraceExpanded),
+ },
+ div(
+ {
+ className: "exception-message",
+ onClick: () => this.onExceptionMessageClick(),
+ },
+ this.renderArrowIcon(stacktrace),
+ StringRep.rep({
+ object: errorMessage,
+ useQuotes: false,
+ className: "exception-text",
+ })
+ ),
+ this.renderStacktrace(stacktrace)
+ );
+ }
+}
+
+const mapDispatchToProps = {
+ selectSourceURL: actions.selectSourceURL,
+};
+
+export default connect(null, mapDispatchToProps)(ExceptionPopup);
diff --git a/devtools/client/debugger/src/components/Editor/Preview/Popup.css b/devtools/client/debugger/src/components/Editor/Preview/Popup.css
new file mode 100644
index 0000000000..745d272fbb
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/Preview/Popup.css
@@ -0,0 +1,174 @@
+/* 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/>. */
+
+.preview-popup {
+ border: 1px solid var(--theme-splitter-color);
+ height: auto;
+ overflow: auto;
+ background: var(--theme-body-background);
+ box-shadow: 1px 1px 3px var(--popup-shadow-color);
+}
+
+.popover .preview-popup {
+ padding: 5px 10px;
+ max-width: 450px;
+ min-width: 200px;
+}
+
+.tooltip .preview-popup {
+ max-width: inherit;
+ padding: 5px;
+ min-height: inherit;
+ max-height: 200px;
+}
+
+.preview-popup .tree {
+ /* Setting a fixed line height to avoid issues in custom formatters changing
+ * the line height like the CLJS DevTools */
+ line-height: 15px;
+}
+
+.gap svg {
+ pointer-events: none;
+}
+
+.gap polygon {
+ pointer-events: auto;
+}
+
+.popover .preview-popup .object-node {
+ padding-inline-start: 0px;
+}
+
+.preview-token:hover {
+ cursor: default;
+}
+
+.preview-token,
+.debug-expression.preview-token {
+ background-color: var(--theme-highlight-yellow);
+}
+
+.theme-dark .preview-token,
+.theme-dark .debug-expression.preview-token {
+ background-color: #743884;
+}
+
+.theme-dark .cm-s-mozilla .preview-token,
+.theme-dark .cm-s-mozilla .debug-expression.preview-token {
+ color: #e7ebee;
+}
+
+.theme-dark .popover .preview-popup {
+ border-color: var(--theme-body-color);
+}
+
+.tooltip {
+ position: fixed;
+ z-index: 100;
+}
+
+
+
+.theme-dark .tooltip .preview-popup {
+ border-color: var(--theme-body-color);
+}
+
+.tooltip .gap {
+ height: 4px;
+ padding-top: 0px;
+}
+
+.add-to-expression-bar {
+ border: 1px solid var(--theme-splitter-color);
+ border-top: none;
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ font-size: 14px;
+ line-height: 30px;
+ background: var(--theme-toolbar-background);
+ color: var(--theme-text-color-inactive);
+ padding: 0 4px;
+}
+
+.add-to-expression-bar .prompt {
+ width: 1em;
+}
+
+.add-to-expression-bar .expression-to-save-label {
+ width: calc(100% - 4em);
+}
+
+.add-to-expression-bar .expression-to-save-button {
+ font-size: 14px;
+ color: var(--theme-comment);
+}
+
+/* Exception popup */
+.exception-popup .exception-text {
+ color: var(--red-70);
+}
+
+.theme-dark .exception-popup .exception-text {
+ color: var(--red-20);
+}
+
+.exception-popup .exception-message {
+ display: flex;
+ align-items: center;
+}
+
+.exception-message .arrow {
+ margin-inline-end: 4px;
+}
+
+.exception-popup .exception-stacktrace {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ grid-column-gap: 8px;
+ padding-inline: 20px 3px;
+ line-height: var(--theme-code-line-height);
+}
+
+.exception-stacktrace .frame {
+ display: contents;
+ cursor: pointer;
+}
+
+.exception-stacktrace .title {
+ grid-column: 1/2;
+ color: var(--grey-90);
+}
+
+.theme-dark .exception-stacktrace .title {
+ color: white;
+}
+
+.exception-stacktrace .location {
+ grid-column: -1/-2;
+ color: var(--theme-highlight-purple);
+ direction: rtl;
+ text-align: end;
+ white-space: nowrap;
+ /* Force the location to be on one line and crop at start if wider then max-width */
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 350px;
+}
+
+.theme-dark .exception-stacktrace .location {
+ color: var(--blue-40);
+}
+
+.exception-stacktrace .line {
+ color: var(--theme-highlight-blue);
+}
+
+.theme-dark .exception-stacktrace .line {
+ color: hsl(210, 40%, 60%);
+}
diff --git a/devtools/client/debugger/src/components/Editor/Preview/Popup.js b/devtools/client/debugger/src/components/Editor/Preview/Popup.js
new file mode 100644
index 0000000000..a010358dc1
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/Preview/Popup.js
@@ -0,0 +1,277 @@
+/* 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 React, { Component } from "devtools/client/shared/vendor/react";
+import { div } from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+
+import Reps from "devtools/client/shared/components/reps/index";
+const {
+ REPS: { Grip },
+ MODE,
+ objectInspector,
+} = Reps;
+
+const { ObjectInspector, utils } = objectInspector;
+
+const {
+ node: { nodeIsPrimitive },
+} = utils;
+
+import ExceptionPopup from "./ExceptionPopup";
+
+import actions from "../../../actions/index";
+import Popover from "../../shared/Popover";
+
+export class Popup extends Component {
+ constructor(props) {
+ super(props);
+ }
+
+ static get propTypes() {
+ return {
+ clearPreview: PropTypes.func.isRequired,
+ editorRef: PropTypes.object.isRequired,
+ highlightDomElement: PropTypes.func.isRequired,
+ openElementInInspector: PropTypes.func.isRequired,
+ openLink: PropTypes.func.isRequired,
+ preview: PropTypes.object.isRequired,
+ selectSourceURL: PropTypes.func.isRequired,
+ unHighlightDomElement: PropTypes.func.isRequired,
+ };
+ }
+
+ componentDidMount() {
+ this.addHighlightToToken(this.props.preview.target);
+ }
+
+ componentWillUnmount() {
+ this.removeHighlightFromToken(this.props.preview.target);
+ }
+
+ componentDidUpdate(prevProps) {
+ const { target } = this.props.preview;
+ if (prevProps.target == target) {
+ return;
+ }
+
+ this.removeHighlightFromToken(prevProps.preview.target);
+ this.addHighlightToToken(target);
+ }
+
+ addHighlightToToken(target) {
+ if (!target) {
+ return;
+ }
+
+ target.classList.add("preview-token");
+ addHighlightToTargetSiblings(target, this.props);
+ }
+
+ removeHighlightFromToken(target) {
+ if (!target) {
+ return;
+ }
+
+ target.classList.remove("preview-token");
+ removeHighlightForTargetSiblings(target);
+ }
+
+ calculateMaxHeight = () => {
+ const { editorRef } = this.props;
+ if (!editorRef) {
+ return "auto";
+ }
+
+ const { height, top } = editorRef.getBoundingClientRect();
+ const maxHeight = height + top;
+ if (maxHeight < 250) {
+ return maxHeight;
+ }
+
+ return 250;
+ };
+
+ createElement(element) {
+ return document.createElement(element);
+ }
+
+ renderExceptionPreview(exception) {
+ return React.createElement(ExceptionPopup, {
+ exception: exception,
+ clearPreview: this.props.clearPreview,
+ });
+ }
+
+ renderPreview() {
+ const {
+ preview: { root, exception, resultGrip },
+ } = this.props;
+
+ const usesCustomFormatter =
+ root?.contents?.value?.useCustomFormatter ?? false;
+
+ if (exception) {
+ return this.renderExceptionPreview(exception);
+ }
+
+ return div(
+ {
+ className: "preview-popup",
+ style: {
+ maxHeight: this.calculateMaxHeight(),
+ },
+ },
+ React.createElement(ObjectInspector, {
+ roots: [root],
+ autoExpandDepth: 1,
+ autoReleaseObjectActors: false,
+ mode: usesCustomFormatter ? MODE.LONG : MODE.SHORT,
+ disableWrap: true,
+ displayRootNodeAsHeader: true,
+ focusable: false,
+ openLink: this.props.openLink,
+ defaultRep: Grip,
+ createElement: this.createElement,
+ onDOMNodeClick: grip => this.props.openElementInInspector(grip),
+ onInspectIconClick: grip => this.props.openElementInInspector(grip),
+ onDOMNodeMouseOver: grip => this.props.highlightDomElement(grip),
+ onDOMNodeMouseOut: grip => this.props.unHighlightDomElement(grip),
+ mayUseCustomFormatter: true,
+ onViewSourceInDebugger: () => {
+ return (
+ resultGrip.location &&
+ this.props.selectSourceURL(resultGrip.location.url, {
+ line: resultGrip.location.line,
+ column: resultGrip.location.column,
+ })
+ );
+ },
+ })
+ );
+ }
+
+ getPreviewType() {
+ const {
+ preview: { root, exception },
+ } = this.props;
+ if (exception || nodeIsPrimitive(root)) {
+ return "tooltip";
+ }
+
+ return "popover";
+ }
+
+ render() {
+ const {
+ preview: { cursorPos, resultGrip, exception },
+ editorRef,
+ } = this.props;
+
+ if (
+ !exception &&
+ (typeof resultGrip == "undefined" || resultGrip?.optimizedOut)
+ ) {
+ return null;
+ }
+
+ const type = this.getPreviewType();
+ return React.createElement(
+ Popover,
+ {
+ targetPosition: cursorPos,
+ type: type,
+ editorRef: editorRef,
+ target: this.props.preview.target,
+ mouseout: this.props.clearPreview,
+ },
+ this.renderPreview()
+ );
+ }
+}
+
+export function addHighlightToTargetSiblings(target, props) {
+ // This function searches for related tokens that should also be highlighted when previewed.
+ // Here is the process:
+ // It conducts a search on the target's next siblings and then another search for the previous siblings.
+ // If a sibling is not an element node (nodeType === 1), the highlight is not added and the search is short-circuited.
+ // If the element sibling is the same token type as the target, and is also found in the preview expression, the highlight class is added.
+
+ const tokenType = target.classList.item(0);
+ const previewExpression = props.preview.expression;
+
+ if (
+ tokenType &&
+ previewExpression &&
+ target.innerHTML !== previewExpression
+ ) {
+ let nextSibling = target.nextSibling;
+ let nextElementSibling = target.nextElementSibling;
+
+ // Note: Declaring previous/next ELEMENT siblings as well because
+ // properties like innerHTML can't be checked on nextSibling
+ // without creating a flow error even if the node is an element type.
+ while (
+ nextSibling &&
+ nextElementSibling &&
+ nextSibling.nodeType === 1 &&
+ nextElementSibling.className.includes(tokenType) &&
+ previewExpression.includes(nextElementSibling.innerHTML)
+ ) {
+ // All checks passed, add highlight and continue the search.
+ nextElementSibling.classList.add("preview-token");
+
+ nextSibling = nextSibling.nextSibling;
+ nextElementSibling = nextElementSibling.nextElementSibling;
+ }
+
+ let previousSibling = target.previousSibling;
+ let previousElementSibling = target.previousElementSibling;
+
+ while (
+ previousSibling &&
+ previousElementSibling &&
+ previousSibling.nodeType === 1 &&
+ previousElementSibling.className.includes(tokenType) &&
+ previewExpression.includes(previousElementSibling.innerHTML)
+ ) {
+ // All checks passed, add highlight and continue the search.
+ previousElementSibling.classList.add("preview-token");
+
+ previousSibling = previousSibling.previousSibling;
+ previousElementSibling = previousElementSibling.previousElementSibling;
+ }
+ }
+}
+
+export function removeHighlightForTargetSiblings(target) {
+ // Look at target's previous and next token siblings.
+ // If they also have the highlight class 'preview-token',
+ // remove that class.
+ let nextSibling = target.nextElementSibling;
+ while (nextSibling && nextSibling.className.includes("preview-token")) {
+ nextSibling.classList.remove("preview-token");
+ nextSibling = nextSibling.nextElementSibling;
+ }
+ let previousSibling = target.previousElementSibling;
+ while (
+ previousSibling &&
+ previousSibling.className.includes("preview-token")
+ ) {
+ previousSibling.classList.remove("preview-token");
+ previousSibling = previousSibling.previousElementSibling;
+ }
+}
+
+const mapDispatchToProps = {
+ addExpression: actions.addExpression,
+ selectSourceURL: actions.selectSourceURL,
+ openLink: actions.openLink,
+ openElementInInspector: actions.openElementInInspectorCommand,
+ highlightDomElement: actions.highlightDomElement,
+ unHighlightDomElement: actions.unHighlightDomElement,
+};
+
+export default connect(null, mapDispatchToProps)(Popup);
diff --git a/devtools/client/debugger/src/components/Editor/Preview/index.js b/devtools/client/debugger/src/components/Editor/Preview/index.js
new file mode 100644
index 0000000000..218d33007f
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/Preview/index.js
@@ -0,0 +1,128 @@
+/* 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 PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import React, { PureComponent } from "devtools/client/shared/vendor/react";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+
+import Popup from "./Popup";
+
+import { getIsCurrentThreadPaused } from "../../../selectors/index";
+import actions from "../../../actions/index";
+
+const EXCEPTION_MARKER = "mark-text-exception";
+
+class Preview extends PureComponent {
+ target = null;
+ constructor(props) {
+ super(props);
+ this.state = { selecting: false };
+ }
+
+ static get propTypes() {
+ return {
+ editor: PropTypes.object.isRequired,
+ editorRef: PropTypes.object.isRequired,
+ isPaused: PropTypes.bool.isRequired,
+ getExceptionPreview: PropTypes.func.isRequired,
+ getPreview: PropTypes.func,
+ };
+ }
+
+ componentDidMount() {
+ this.updateListeners();
+ }
+
+ componentWillUnmount() {
+ const { codeMirror } = this.props.editor;
+ const codeMirrorWrapper = codeMirror.getWrapperElement();
+
+ codeMirror.off("tokenenter", this.onTokenEnter);
+ codeMirror.off("scroll", this.onScroll);
+ codeMirrorWrapper.removeEventListener("mouseup", this.onMouseUp);
+ codeMirrorWrapper.removeEventListener("mousedown", this.onMouseDown);
+ }
+
+ updateListeners(prevProps) {
+ const { codeMirror } = this.props.editor;
+ const codeMirrorWrapper = codeMirror.getWrapperElement();
+ codeMirror.on("tokenenter", this.onTokenEnter);
+ codeMirror.on("scroll", this.onScroll);
+ codeMirrorWrapper.addEventListener("mouseup", this.onMouseUp);
+ codeMirrorWrapper.addEventListener("mousedown", this.onMouseDown);
+ }
+
+ // Note that these events are emitted by utils/editor/tokens.js
+ onTokenEnter = async ({ target, tokenPos }) => {
+ // Use a temporary object to uniquely identify the asynchronous processing of this user event
+ // and bail out if we started hovering another token.
+ const tokenId = {};
+ this.currentTokenId = tokenId;
+
+ const { editor, getPreview, getExceptionPreview } = this.props;
+ const isTargetException = target.classList.contains(EXCEPTION_MARKER);
+
+ let preview;
+ if (isTargetException) {
+ preview = await getExceptionPreview(target, tokenPos, editor.codeMirror);
+ }
+
+ if (!preview && this.props.isPaused && !this.state.selecting) {
+ preview = await getPreview(target, tokenPos, editor.codeMirror);
+ }
+
+ // Prevent modifying state and showing this preview if we started hovering another token
+ if (!preview || this.currentTokenId !== tokenId) {
+ return;
+ }
+ this.setState({ preview });
+ };
+
+ onMouseUp = () => {
+ if (this.props.isPaused) {
+ this.setState({ selecting: false });
+ }
+ };
+
+ onMouseDown = () => {
+ if (this.props.isPaused) {
+ this.setState({ selecting: true });
+ }
+ };
+
+ onScroll = () => {
+ if (this.props.isPaused) {
+ this.clearPreview();
+ }
+ };
+
+ clearPreview = () => {
+ this.setState({ preview: null });
+ };
+
+ render() {
+ const { preview } = this.state;
+ if (!preview || this.state.selecting) {
+ return null;
+ }
+ return React.createElement(Popup, {
+ preview: preview,
+ editor: this.props.editor,
+ editorRef: this.props.editorRef,
+ clearPreview: this.clearPreview,
+ });
+ }
+}
+
+const mapStateToProps = state => {
+ return {
+ isPaused: getIsCurrentThreadPaused(state),
+ };
+};
+
+export default connect(mapStateToProps, {
+ addExpression: actions.addExpression,
+ getPreview: actions.getPreview,
+ getExceptionPreview: actions.getExceptionPreview,
+})(Preview);
diff --git a/devtools/client/debugger/src/components/Editor/Preview/moz.build b/devtools/client/debugger/src/components/Editor/Preview/moz.build
new file mode 100644
index 0000000000..362faadc42
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/Preview/moz.build
@@ -0,0 +1,12 @@
+# 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(
+ "ExceptionPopup.js",
+ "index.js",
+ "Popup.js",
+)
diff --git a/devtools/client/debugger/src/components/Editor/Preview/tests/Popup.spec.js b/devtools/client/debugger/src/components/Editor/Preview/tests/Popup.spec.js
new file mode 100644
index 0000000000..e504c9f12c
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/Preview/tests/Popup.spec.js
@@ -0,0 +1,99 @@
+/* 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 {
+ addHighlightToTargetSiblings,
+ removeHighlightForTargetSiblings,
+} from "../Popup";
+
+describe("addHighlightToTargetSiblings", () => {
+ it("should add preview highlight class to related target siblings", async () => {
+ const div = document.createElement("div");
+ const divChildren = ["a", "divided", "token"];
+ divChildren.forEach(function (span) {
+ const child = document.createElement("span");
+ const text = document.createTextNode(span);
+ child.appendChild(text);
+ child.classList.add("cm-property");
+ div.appendChild(child);
+ });
+
+ const target = div.children[1];
+ const props = {
+ preview: {
+ expression: "adividedtoken",
+ },
+ };
+
+ addHighlightToTargetSiblings(target, props);
+
+ const previous = target.previousElementSibling;
+ expect(previous.className.includes("preview-token")).toEqual(true);
+
+ const next = target.nextElementSibling;
+ expect(next.className.includes("preview-token")).toEqual(true);
+ });
+
+ it("should not add preview highlight class to target's related siblings after non-element nodes", () => {
+ const div = document.createElement("div");
+
+ const elementBeforePeriod = document.createElement("span");
+ elementBeforePeriod.innerHTML = "object";
+ elementBeforePeriod.classList.add("cm-property");
+ div.appendChild(elementBeforePeriod);
+
+ const period = document.createTextNode(".");
+ div.appendChild(period);
+
+ const target = document.createElement("span");
+ target.innerHTML = "property";
+ target.classList.add("cm-property");
+ div.appendChild(target);
+
+ const anotherPeriod = document.createTextNode(".");
+ div.appendChild(anotherPeriod);
+
+ const elementAfterPeriod = document.createElement("span");
+ elementAfterPeriod.innerHTML = "anotherProperty";
+ elementAfterPeriod.classList.add("cm-property");
+ div.appendChild(elementAfterPeriod);
+
+ const props = {
+ preview: {
+ expression: "object.property.anotherproperty",
+ },
+ };
+ addHighlightToTargetSiblings(target, props);
+
+ expect(elementBeforePeriod.className.includes("preview-token")).toEqual(
+ false
+ );
+ expect(elementAfterPeriod.className.includes("preview-token")).toEqual(
+ false
+ );
+ });
+});
+
+describe("removeHighlightForTargetSiblings", () => {
+ it("should remove preview highlight class from target's related siblings", async () => {
+ const div = document.createElement("div");
+ const divChildren = ["a", "divided", "token"];
+ divChildren.forEach(function (span) {
+ const child = document.createElement("span");
+ const text = document.createTextNode(span);
+ child.appendChild(text);
+ child.classList.add("preview-token");
+ div.appendChild(child);
+ });
+ const target = div.children[1];
+
+ removeHighlightForTargetSiblings(target);
+
+ const previous = target.previousElementSibling;
+ expect(previous.className.includes("preview-token")).toEqual(false);
+
+ const next = target.nextElementSibling;
+ expect(next.className.includes("preview-token")).toEqual(false);
+ });
+});
diff --git a/devtools/client/debugger/src/components/Editor/SearchInFileBar.css b/devtools/client/debugger/src/components/Editor/SearchInFileBar.css
new file mode 100644
index 0000000000..fe90b2a960
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/SearchInFileBar.css
@@ -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/>. */
+
+.search-bar {
+ position: relative;
+ display: flex;
+ border-top: 1px solid var(--theme-splitter-color);
+ height: var(--editor-searchbar-height);
+ transition: outline 150ms ease-out;
+}
+
+/* Display an outline on the container when the child input is focused. If another element
+ is focused (e.g. a button), we only want the outline on that element */
+.search-bar:focus-within:has(input:focus-visible) {
+ outline: var(--theme-focus-outline);
+ outline-offset: -2px;
+}
+
+.search-bar .search-outline {
+ flex-grow: 1;
+ border-width: 0;
+}
+
+/* The outline is set on .search-bar already */
+.search-bar input:focus-visible {
+ outline: none;
+}
+
+.search-bar .result-list {
+ max-height: 230px;
+}
diff --git a/devtools/client/debugger/src/components/Editor/SearchInFileBar.js b/devtools/client/debugger/src/components/Editor/SearchInFileBar.js
new file mode 100644
index 0000000000..a3491a3fef
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/SearchInFileBar.js
@@ -0,0 +1,368 @@
+/* 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 PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import React, { Component } from "devtools/client/shared/vendor/react";
+import { div } from "devtools/client/shared/vendor/react-dom-factories";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import actions from "../../actions/index";
+import {
+ getActiveSearch,
+ getSelectedSource,
+ getSelectedSourceTextContent,
+ getSearchOptions,
+} from "../../selectors/index";
+
+import { searchKeys } from "../../constants";
+import { scrollList } from "../../utils/result-list";
+
+import SearchInput from "../shared/SearchInput";
+
+const { PluralForm } = require("resource://devtools/shared/plural-form.js");
+const { debounce } = require("resource://devtools/shared/debounce.js");
+import { renderWasmText } from "../../utils/wasm";
+import {
+ clearSearch,
+ find,
+ findNext,
+ findPrev,
+ removeOverlay,
+} from "../../utils/editor/index";
+import { isFulfilled } from "../../utils/async-value";
+
+function getSearchShortcut() {
+ return L10N.getStr("sourceSearch.search.key2");
+}
+
+class SearchInFileBar extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ query: "",
+ selectedResultIndex: 0,
+ results: {
+ matches: [],
+ matchIndex: -1,
+ count: 0,
+ index: -1,
+ },
+ inputFocused: false,
+ };
+ }
+
+ static get propTypes() {
+ return {
+ closeFileSearch: PropTypes.func.isRequired,
+ editor: PropTypes.object,
+ modifiers: PropTypes.object.isRequired,
+ searchInFileEnabled: PropTypes.bool.isRequired,
+ selectedSourceTextContent: PropTypes.bool.isRequired,
+ selectedSource: PropTypes.object.isRequired,
+ setActiveSearch: PropTypes.func.isRequired,
+ querySearchWorker: PropTypes.func.isRequired,
+ };
+ }
+
+ componentWillUnmount() {
+ const { shortcuts } = this.context;
+
+ shortcuts.off(getSearchShortcut(), this.toggleSearch);
+ shortcuts.off("Escape", this.onEscape);
+
+ this.doSearch.cancel();
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ const { query } = this.state;
+ // If a new source is selected update the file search results
+ if (
+ this.props.selectedSource &&
+ nextProps.selectedSource !== this.props.selectedSource &&
+ this.props.searchInFileEnabled &&
+ query
+ ) {
+ this.doSearch(query, false);
+ }
+ }
+
+ componentDidMount() {
+ // overwrite this.doSearch with debounced version to
+ // reduce frequency of queries
+ this.doSearch = debounce(this.doSearch, 100);
+ const { shortcuts } = this.context;
+
+ shortcuts.on(getSearchShortcut(), this.toggleSearch);
+ shortcuts.on("Escape", this.onEscape);
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ if (this.refs.resultList && this.refs.resultList.refs) {
+ scrollList(this.refs.resultList.refs, this.state.selectedResultIndex);
+ }
+ }
+
+ onEscape = e => {
+ this.closeSearch(e);
+ };
+
+ clearSearch = () => {
+ const { editor: ed } = this.props;
+ if (ed) {
+ const ctx = { ed, cm: ed.codeMirror };
+ removeOverlay(ctx, this.state.query);
+ }
+ };
+
+ closeSearch = e => {
+ const { closeFileSearch, editor, searchInFileEnabled } = this.props;
+ this.clearSearch();
+ if (editor && searchInFileEnabled) {
+ closeFileSearch();
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ this.setState({ inputFocused: false });
+ };
+
+ toggleSearch = e => {
+ e.stopPropagation();
+ e.preventDefault();
+ const { editor, searchInFileEnabled, setActiveSearch } = this.props;
+
+ // Set inputFocused to false, so that search query is highlighted whenever search shortcut is used, even if the input already has focus.
+ this.setState({ inputFocused: false });
+
+ if (!searchInFileEnabled) {
+ setActiveSearch("file");
+ }
+
+ if (searchInFileEnabled && editor) {
+ const query = editor.codeMirror.getSelection() || this.state.query;
+
+ if (query !== "") {
+ this.setState({ query, inputFocused: true });
+ this.doSearch(query);
+ } else {
+ this.setState({ query: "", inputFocused: true });
+ }
+ }
+ };
+
+ doSearch = async (query, focusFirstResult = true) => {
+ const { editor, modifiers, selectedSourceTextContent } = this.props;
+ if (
+ !editor ||
+ !selectedSourceTextContent ||
+ !isFulfilled(selectedSourceTextContent) ||
+ !modifiers
+ ) {
+ return;
+ }
+ const selectedContent = selectedSourceTextContent.value;
+
+ const ctx = { ed: editor, cm: editor.codeMirror };
+
+ if (!query) {
+ clearSearch(ctx.cm, query);
+ return;
+ }
+
+ let text;
+ if (selectedContent.type === "wasm") {
+ text = renderWasmText(this.props.selectedSource.id, selectedContent).join(
+ "\n"
+ );
+ } else {
+ text = selectedContent.value;
+ }
+
+ const matches = await this.props.querySearchWorker(query, text, modifiers);
+
+ const res = find(ctx, query, true, modifiers, focusFirstResult);
+ if (!res) {
+ return;
+ }
+
+ const { ch, line } = res;
+
+ const matchIndex = matches.findIndex(
+ elm => elm.line === line && elm.ch === ch
+ );
+ this.setState({
+ results: {
+ matches,
+ matchIndex,
+ count: matches.length,
+ index: ch,
+ },
+ });
+ };
+
+ traverseResults = (e, reverse = false) => {
+ e.stopPropagation();
+ e.preventDefault();
+ const { editor } = this.props;
+
+ if (!editor) {
+ return;
+ }
+
+ const ctx = { ed: editor, cm: editor.codeMirror };
+
+ const { modifiers } = this.props;
+ const { query } = this.state;
+ const { matches } = this.state.results;
+
+ if (query === "" && !this.props.searchInFileEnabled) {
+ this.props.setActiveSearch("file");
+ }
+
+ if (modifiers) {
+ const findArgs = [ctx, query, true, modifiers];
+ const results = reverse ? findPrev(...findArgs) : findNext(...findArgs);
+
+ if (!results) {
+ return;
+ }
+ const { ch, line } = results;
+ const matchIndex = matches.findIndex(
+ elm => elm.line === line && elm.ch === ch
+ );
+ this.setState({
+ results: {
+ matches,
+ matchIndex,
+ count: matches.length,
+ index: ch,
+ },
+ });
+ }
+ };
+
+ // Handlers
+
+ onChange = e => {
+ this.setState({ query: e.target.value });
+
+ return this.doSearch(e.target.value);
+ };
+
+ onFocus = e => {
+ this.setState({ inputFocused: true });
+ };
+
+ onBlur = e => {
+ this.setState({ inputFocused: false });
+ };
+
+ onKeyDown = e => {
+ if (e.key !== "Enter" && e.key !== "F3") {
+ return;
+ }
+
+ this.traverseResults(e, e.shiftKey);
+ e.preventDefault();
+ this.doSearch(e.target.value);
+ };
+
+ onHistoryScroll = query => {
+ this.setState({ query });
+ this.doSearch(query);
+ };
+
+ // Renderers
+ buildSummaryMsg() {
+ const {
+ query,
+ results: { matchIndex, count, index },
+ } = this.state;
+
+ if (query.trim() == "") {
+ return "";
+ }
+
+ if (count == 0) {
+ return L10N.getStr("editor.noResultsFound");
+ }
+
+ if (index == -1) {
+ const resultsSummaryString = L10N.getStr("sourceSearch.resultsSummary1");
+ return PluralForm.get(count, resultsSummaryString).replace("#1", count);
+ }
+
+ const searchResultsString = L10N.getStr("editor.searchResults1");
+ return PluralForm.get(count, searchResultsString)
+ .replace("#1", count)
+ .replace("%d", matchIndex + 1);
+ }
+
+ shouldShowErrorEmoji() {
+ const {
+ query,
+ results: { count },
+ } = this.state;
+ return !!query && !count;
+ }
+
+ render() {
+ const { searchInFileEnabled } = this.props;
+ const {
+ results: { count },
+ } = this.state;
+
+ if (!searchInFileEnabled) {
+ return div(null);
+ }
+ return div(
+ {
+ className: "search-bar",
+ },
+ React.createElement(SearchInput, {
+ query: this.state.query,
+ count: count,
+ placeholder: L10N.getStr("sourceSearch.search.placeholder2"),
+ summaryMsg: this.buildSummaryMsg(),
+ isLoading: false,
+ onChange: this.onChange,
+ onFocus: this.onFocus,
+ onBlur: this.onBlur,
+ showErrorEmoji: this.shouldShowErrorEmoji(),
+ onKeyDown: this.onKeyDown,
+ onHistoryScroll: this.onHistoryScroll,
+ handleNext: e => this.traverseResults(e, false),
+ handlePrev: e => this.traverseResults(e, true),
+ shouldFocus: this.state.inputFocused,
+ showClose: true,
+ showExcludePatterns: false,
+ handleClose: this.closeSearch,
+ showSearchModifiers: true,
+ searchKey: searchKeys.FILE_SEARCH,
+ onToggleSearchModifier: () => this.doSearch(this.state.query),
+ })
+ );
+ }
+}
+
+SearchInFileBar.contextTypes = {
+ shortcuts: PropTypes.object,
+};
+
+const mapStateToProps = (state, p) => {
+ const selectedSource = getSelectedSource(state);
+
+ return {
+ searchInFileEnabled: getActiveSearch(state) === "file",
+ selectedSource,
+ selectedSourceTextContent: getSelectedSourceTextContent(state),
+ modifiers: getSearchOptions(state, "file-search"),
+ };
+};
+
+export default connect(mapStateToProps, {
+ setFileSearchQuery: actions.setFileSearchQuery,
+ setActiveSearch: actions.setActiveSearch,
+ closeFileSearch: actions.closeFileSearch,
+ querySearchWorker: actions.querySearchWorker,
+})(SearchInFileBar);
diff --git a/devtools/client/debugger/src/components/Editor/Tab.js b/devtools/client/debugger/src/components/Editor/Tab.js
new file mode 100644
index 0000000000..ba5e1c1934
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/Tab.js
@@ -0,0 +1,148 @@
+/* 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 React, { PureComponent } from "devtools/client/shared/vendor/react";
+import { div, span } from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+
+import SourceIcon from "../shared/SourceIcon";
+import { CloseButton } from "../shared/Button/index";
+
+import actions from "../../actions/index";
+
+import {
+ getDisplayPath,
+ getFileURL,
+ getSourceQueryString,
+ getTruncatedFileName,
+ isPretty,
+} from "../../utils/source";
+import { createLocation } from "../../utils/location";
+
+import {
+ getSelectedLocation,
+ getSourcesForTabs,
+ isSourceBlackBoxed,
+} from "../../selectors/index";
+
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+class Tab extends PureComponent {
+ static get propTypes() {
+ return {
+ closeTab: PropTypes.func.isRequired,
+ onDragEnd: PropTypes.func.isRequired,
+ onDragOver: PropTypes.func.isRequired,
+ onDragStart: PropTypes.func.isRequired,
+ selectSource: PropTypes.func.isRequired,
+ source: PropTypes.object.isRequired,
+ sourceActor: PropTypes.object.isRequired,
+ tabSources: PropTypes.array.isRequired,
+ isBlackBoxed: PropTypes.bool.isRequired,
+ };
+ }
+
+ onContextMenu = event => {
+ event.preventDefault();
+ this.props.showTabContextMenu(event, this.props.source);
+ };
+
+ isSourceSearchEnabled() {
+ return this.props.activeSearch === "source";
+ }
+
+ render() {
+ const {
+ selectSource,
+ closeTab,
+ source,
+ sourceActor,
+ tabSources,
+ onDragOver,
+ onDragStart,
+ onDragEnd,
+ index,
+ isActive,
+ } = this.props;
+ const sourceId = source.id;
+ const isPrettyCode = isPretty(source);
+
+ function onClickClose(e) {
+ e.stopPropagation();
+ closeTab(source);
+ }
+
+ function handleTabClick(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ return selectSource(source, sourceActor);
+ }
+
+ const className = classnames("source-tab", {
+ active: isActive,
+ pretty: isPrettyCode,
+ blackboxed: this.props.isBlackBoxed,
+ });
+
+ const path = getDisplayPath(source, tabSources);
+ const query = getSourceQueryString(source);
+ return div(
+ {
+ draggable: true,
+ onDragOver: onDragOver,
+ onDragStart: onDragStart,
+ onDragEnd: onDragEnd,
+ className: className,
+ "data-index": index,
+ "data-source-id": sourceId,
+ onClick: handleTabClick,
+ // Accommodate middle click to close tab
+ onMouseUp: e => e.button === 1 && closeTab(source),
+ onContextMenu: this.onContextMenu,
+ title: getFileURL(source, false),
+ },
+ React.createElement(SourceIcon, {
+ location: createLocation({
+ source,
+ sourceActor,
+ }),
+ forTab: true,
+ modifier: icon => (["file", "javascript"].includes(icon) ? null : icon),
+ }),
+ div(
+ {
+ className: "filename",
+ },
+ getTruncatedFileName(source, query),
+ path && span(null, `../${path}/..`)
+ ),
+ React.createElement(CloseButton, {
+ handleClick: onClickClose,
+ tooltip: L10N.getStr("sourceTabs.closeTabButtonTooltip"),
+ })
+ );
+ }
+}
+
+const mapStateToProps = (state, { source }) => {
+ return {
+ tabSources: getSourcesForTabs(state),
+ isBlackBoxed: isSourceBlackBoxed(state, source),
+ isActive: source.id === getSelectedLocation(state)?.source.id,
+ };
+};
+
+export default connect(
+ mapStateToProps,
+ {
+ selectSource: actions.selectSource,
+ closeTab: actions.closeTab,
+ showTabContextMenu: actions.showTabContextMenu,
+ },
+ null,
+ {
+ withRef: true,
+ }
+)(Tab);
diff --git a/devtools/client/debugger/src/components/Editor/Tabs.css b/devtools/client/debugger/src/components/Editor/Tabs.css
new file mode 100644
index 0000000000..ab70876d5e
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/Tabs.css
@@ -0,0 +1,127 @@
+/* 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/>. */
+
+.source-header {
+ grid-area: editor-header;
+ display: flex;
+ width: 100%;
+ height: var(--editor-header-height);
+ border-bottom: 1px solid var(--theme-splitter-color);
+ background-color: var(--theme-toolbar-background);
+}
+
+.source-header * {
+ user-select: none;
+}
+
+.source-header .command-bar {
+ flex: initial;
+ flex-shrink: 0;
+ border-bottom: 0;
+ border-inline-start: 1px solid var(--theme-splitter-color);
+}
+
+.source-tabs {
+ flex: auto;
+ align-self: flex-start;
+ /* Reserve space for the overflow button (even if not visible) */
+ padding-inline-end: 28px;
+ /* Make sure that overflowing tabs don't show through other elements (see Bug 1855458) */
+ max-height: 100%;
+ overflow: hidden;
+}
+
+.source-tab {
+ display: inline-flex;
+ align-items: center;
+ position: relative;
+ min-width: 40px;
+ max-width: 100%;
+ overflow: hidden;
+ padding: 4px 10px;
+ cursor: default;
+ height: calc(var(--editor-header-height) - 1px);
+ font-size: 12px;
+ background-color: transparent;
+ vertical-align: bottom;
+}
+
+.source-tab::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 2px;
+ background-color: var(--tab-line-color, transparent);
+ transition: transform 250ms var(--animation-curve),
+ opacity 250ms var(--animation-curve);
+ opacity: 0;
+ transform: scaleX(0);
+}
+
+.source-tab.active {
+ --tab-line-color: var(--tab-line-selected-color);
+ color: var(--theme-toolbar-selected-color);
+ border-bottom-color: transparent;
+}
+
+.source-tab:not(.active):hover {
+ --tab-line-color: var(--tab-line-hover-color);
+ background-color: var(--theme-toolbar-hover);
+}
+
+.source-tab:hover::before,
+.source-tab.active::before {
+ opacity: 1;
+ transform: scaleX(1);
+}
+
+.source-tab .img:is(.prettyPrint,.blackBox) {
+ mask-size: 14px;
+}
+
+.source-tab .img.prettyPrint {
+ background-color: currentColor;
+}
+
+.source-tab .img.source-icon.blackBox {
+ background-color: #806414;
+}
+
+.source-tab .filename {
+ display: block;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ padding-inline-end: 4px;
+}
+
+.source-tab .filename span {
+ opacity: 0.7;
+ padding-inline-start: 4px;
+}
+
+.source-tab .close-btn {
+ visibility: hidden;
+ margin-inline-end: -6px;
+}
+
+.source-tab.active .close-btn {
+ color: inherit;
+}
+
+.source-tab.active .close-btn,
+.source-tab:hover .close-btn {
+ visibility: visible;
+}
+
+.source-tab.active .source-icon {
+ background-color: currentColor;
+}
+
+.source-tab .close-btn:hover {
+ color: var(--theme-selection-color);
+ background-color: var(--theme-selection-background);
+}
diff --git a/devtools/client/debugger/src/components/Editor/Tabs.js b/devtools/client/debugger/src/components/Editor/Tabs.js
new file mode 100644
index 0000000000..3577a4909c
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/Tabs.js
@@ -0,0 +1,320 @@
+/* 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 React, { PureComponent } from "devtools/client/shared/vendor/react";
+import {
+ div,
+ ul,
+ li,
+ span,
+} from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+
+import {
+ getSourceTabs,
+ getSelectedSource,
+ getSourcesForTabs,
+ getIsPaused,
+ getCurrentThread,
+ getBlackBoxRanges,
+} from "../../selectors/index";
+import { isVisible } from "../../utils/ui";
+
+import { getHiddenTabs } from "../../utils/tabs";
+import { getFilename, isPretty, getFileURL } from "../../utils/source";
+import actions from "../../actions/index";
+
+import Tab from "./Tab";
+import { PaneToggleButton } from "../shared/Button/index";
+import Dropdown from "../shared/Dropdown";
+import AccessibleImage from "../shared/AccessibleImage";
+import CommandBar from "../SecondaryPanes/CommandBar";
+
+const { debounce } = require("resource://devtools/shared/debounce.js");
+
+function haveTabSourcesChanged(tabSources, prevTabSources) {
+ if (tabSources.length !== prevTabSources.length) {
+ return true;
+ }
+
+ for (let i = 0; i < tabSources.length; ++i) {
+ if (tabSources[i].id !== prevTabSources[i].id) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+class Tabs extends PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ dropdownShown: false,
+ hiddenTabs: [],
+ };
+
+ this.onResize = debounce(() => {
+ this.updateHiddenTabs();
+ });
+ }
+
+ static get propTypes() {
+ return {
+ endPanelCollapsed: PropTypes.bool.isRequired,
+ horizontal: PropTypes.bool.isRequired,
+ isPaused: PropTypes.bool.isRequired,
+ moveTab: PropTypes.func.isRequired,
+ moveTabBySourceId: PropTypes.func.isRequired,
+ selectSource: PropTypes.func.isRequired,
+ selectedSource: PropTypes.object,
+ blackBoxRanges: PropTypes.object.isRequired,
+ startPanelCollapsed: PropTypes.bool.isRequired,
+ tabSources: PropTypes.array.isRequired,
+ tabs: PropTypes.array.isRequired,
+ togglePaneCollapse: PropTypes.func.isRequired,
+ };
+ }
+
+ componentDidUpdate(prevProps) {
+ if (
+ this.props.selectedSource !== prevProps.selectedSource ||
+ haveTabSourcesChanged(this.props.tabSources, prevProps.tabSources)
+ ) {
+ this.updateHiddenTabs();
+ }
+ }
+
+ componentDidMount() {
+ window.requestIdleCallback(this.updateHiddenTabs);
+ window.addEventListener("resize", this.onResize);
+ window.document
+ .querySelector(".editor-pane")
+ .addEventListener("resizeend", this.onResize);
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener("resize", this.onResize);
+ window.document
+ .querySelector(".editor-pane")
+ .removeEventListener("resizeend", this.onResize);
+ }
+
+ /*
+ * Updates the hiddenSourceTabs state, by
+ * finding the source tabs which are wrapped and are not on the top row.
+ */
+ updateHiddenTabs = () => {
+ if (!this.refs.sourceTabs) {
+ return;
+ }
+ const { selectedSource, tabSources, moveTab } = this.props;
+ const sourceTabEls = this.refs.sourceTabs.children;
+ const hiddenTabs = getHiddenTabs(tabSources, sourceTabEls);
+
+ if (
+ selectedSource &&
+ isVisible() &&
+ hiddenTabs.find(tab => tab.id == selectedSource.id)
+ ) {
+ moveTab(selectedSource.url, 0);
+ return;
+ }
+
+ this.setState({ hiddenTabs });
+ };
+
+ toggleSourcesDropdown() {
+ this.setState(prevState => ({
+ dropdownShown: !prevState.dropdownShown,
+ }));
+ }
+
+ getIconClass(source) {
+ if (isPretty(source)) {
+ return "prettyPrint";
+ }
+ if (this.props.blackBoxRanges[source.url]) {
+ return "blackBox";
+ }
+ return "file";
+ }
+
+ renderDropdownSource = source => {
+ const { selectSource } = this.props;
+ const filename = getFilename(source);
+
+ const onClick = () => selectSource(source);
+ return li(
+ {
+ key: source.id,
+ onClick: onClick,
+ title: getFileURL(source, false),
+ },
+ React.createElement(AccessibleImage, {
+ className: `dropdown-icon ${this.getIconClass(source)}`,
+ }),
+ span(
+ {
+ className: "dropdown-label",
+ },
+ filename
+ )
+ );
+ };
+
+ // Note that these three listener will be called from Tab component
+ // so that e.target will be Tab's DOM (and not Tabs one).
+ onTabDragStart = e => {
+ this.draggedSourceId = e.target.dataset.sourceId;
+ this.draggedSourceIndex = e.target.dataset.index;
+ };
+
+ onTabDragEnd = () => {
+ this.draggedSourceId = null;
+ this.draggedSourceIndex = -1;
+ };
+
+ onTabDragOver = e => {
+ e.preventDefault();
+
+ const hoveredTabIndex = e.target.dataset.index;
+ const { moveTabBySourceId } = this.props;
+
+ if (hoveredTabIndex === this.draggedSourceIndex) {
+ return;
+ }
+
+ const tabDOMRect = e.target.getBoundingClientRect();
+ const { pageX: mouseCursorX } = e;
+ if (
+ /* Case: the mouse cursor moves into the left half of any target tab */
+ mouseCursorX - tabDOMRect.left <
+ tabDOMRect.width / 2
+ ) {
+ // The current tab goes to the left of the target tab
+ const targetTab =
+ hoveredTabIndex > this.draggedSourceIndex
+ ? hoveredTabIndex - 1
+ : hoveredTabIndex;
+ moveTabBySourceId(this.draggedSourceId, targetTab);
+ this.draggedSourceIndex = targetTab;
+ } else if (
+ /* Case: the mouse cursor moves into the right half of any target tab */
+ mouseCursorX - tabDOMRect.left >=
+ tabDOMRect.width / 2
+ ) {
+ // The current tab goes to the right of the target tab
+ const targetTab =
+ hoveredTabIndex < this.draggedSourceIndex
+ ? hoveredTabIndex + 1
+ : hoveredTabIndex;
+ moveTabBySourceId(this.draggedSourceId, targetTab);
+ this.draggedSourceIndex = targetTab;
+ }
+ };
+
+ renderTabs() {
+ const { tabs } = this.props;
+ if (!tabs) {
+ return null;
+ }
+ return div(
+ {
+ className: "source-tabs",
+ ref: "sourceTabs",
+ },
+ tabs.map(({ source, sourceActor }, index) => {
+ return React.createElement(Tab, {
+ onDragStart: this.onTabDragStart,
+ onDragOver: this.onTabDragOver,
+ onDragEnd: this.onTabDragEnd,
+ key: source.id + sourceActor?.id,
+ index,
+ source,
+ sourceActor,
+ });
+ })
+ );
+ }
+
+ renderDropdown() {
+ const { hiddenTabs } = this.state;
+ if (!hiddenTabs || !hiddenTabs.length) {
+ return null;
+ }
+ const panel = ul(null, hiddenTabs.map(this.renderDropdownSource));
+ const icon = React.createElement(AccessibleImage, {
+ className: "more-tabs",
+ });
+ return React.createElement(Dropdown, {
+ panel,
+ icon,
+ });
+ }
+
+ renderCommandBar() {
+ const { horizontal, endPanelCollapsed, isPaused } = this.props;
+ if (!endPanelCollapsed || !isPaused) {
+ return null;
+ }
+ return React.createElement(CommandBar, {
+ horizontal,
+ });
+ }
+
+ renderStartPanelToggleButton() {
+ return React.createElement(PaneToggleButton, {
+ position: "start",
+ collapsed: this.props.startPanelCollapsed,
+ handleClick: this.props.togglePaneCollapse,
+ });
+ }
+
+ renderEndPanelToggleButton() {
+ const { horizontal, endPanelCollapsed, togglePaneCollapse } = this.props;
+ if (!horizontal) {
+ return null;
+ }
+ return React.createElement(PaneToggleButton, {
+ position: "end",
+ collapsed: endPanelCollapsed,
+ handleClick: togglePaneCollapse,
+ horizontal,
+ });
+ }
+
+ render() {
+ return div(
+ {
+ className: "source-header",
+ },
+ this.renderStartPanelToggleButton(),
+ this.renderTabs(),
+ this.renderDropdown(),
+ this.renderEndPanelToggleButton(),
+ this.renderCommandBar()
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ return {
+ selectedSource: getSelectedSource(state),
+ tabSources: getSourcesForTabs(state),
+ tabs: getSourceTabs(state),
+ blackBoxRanges: getBlackBoxRanges(state),
+ isPaused: getIsPaused(state, getCurrentThread(state)),
+ };
+};
+
+export default connect(mapStateToProps, {
+ selectSource: actions.selectSource,
+ moveTab: actions.moveTab,
+ moveTabBySourceId: actions.moveTabBySourceId,
+ closeTab: actions.closeTab,
+ togglePaneCollapse: actions.togglePaneCollapse,
+ showSource: actions.showSource,
+})(Tabs);
diff --git a/devtools/client/debugger/src/components/Editor/index.js b/devtools/client/debugger/src/components/Editor/index.js
new file mode 100644
index 0000000000..c659de77d2
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/index.js
@@ -0,0 +1,795 @@
+/* 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 PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import React, { PureComponent } from "devtools/client/shared/vendor/react";
+import { div } from "devtools/client/shared/vendor/react-dom-factories";
+import { bindActionCreators } from "devtools/client/shared/vendor/redux";
+import ReactDOM from "devtools/client/shared/vendor/react-dom";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+
+import { getLineText, isLineBlackboxed } from "./../../utils/source";
+import { createLocation } from "./../../utils/location";
+import { getIndentation } from "../../utils/indentation";
+
+import {
+ getActiveSearch,
+ getSelectedLocation,
+ getSelectedSource,
+ getSelectedSourceTextContent,
+ getSelectedBreakableLines,
+ getConditionalPanelLocation,
+ getSymbols,
+ getIsCurrentThreadPaused,
+ getSkipPausing,
+ getInlinePreview,
+ getBlackBoxRanges,
+ isSourceBlackBoxed,
+ getHighlightedLineRangeForSelectedSource,
+ isSourceMapIgnoreListEnabled,
+ isSourceOnSourceMapIgnoreList,
+ isMapScopesEnabled,
+} from "../../selectors/index";
+
+// Redux actions
+import actions from "../../actions/index";
+
+import SearchInFileBar from "./SearchInFileBar";
+import HighlightLines from "./HighlightLines";
+import Preview from "./Preview/index";
+import Breakpoints from "./Breakpoints";
+import ColumnBreakpoints from "./ColumnBreakpoints";
+import DebugLine from "./DebugLine";
+import HighlightLine from "./HighlightLine";
+import EmptyLines from "./EmptyLines";
+import ConditionalPanel from "./ConditionalPanel";
+import InlinePreviews from "./InlinePreviews";
+import Exceptions from "./Exceptions";
+import BlackboxLines from "./BlackboxLines";
+
+import {
+ showSourceText,
+ showLoading,
+ showErrorMessage,
+ getEditor,
+ clearEditor,
+ getCursorLine,
+ getCursorColumn,
+ lineAtHeight,
+ toSourceLine,
+ getDocument,
+ scrollToPosition,
+ toEditorPosition,
+ getSourceLocationFromMouseEvent,
+ hasDocument,
+ onMouseOver,
+ startOperation,
+ endOperation,
+} from "../../utils/editor/index";
+
+import { resizeToggleButton, resizeBreakpointGutter } from "../../utils/ui";
+
+const { debounce } = require("resource://devtools/shared/debounce.js");
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+const { appinfo } = Services;
+const isMacOS = appinfo.OS === "Darwin";
+
+function isSecondary(ev) {
+ return isMacOS && ev.ctrlKey && ev.button === 0;
+}
+
+function isCmd(ev) {
+ return isMacOS ? ev.metaKey : ev.ctrlKey;
+}
+
+const cssVars = {
+ searchbarHeight: "var(--editor-searchbar-height)",
+};
+
+class Editor extends PureComponent {
+ static get propTypes() {
+ return {
+ selectedSource: PropTypes.object,
+ selectedSourceTextContent: PropTypes.object,
+ selectedSourceIsBlackBoxed: PropTypes.bool,
+ closeTab: PropTypes.func.isRequired,
+ toggleBreakpointAtLine: PropTypes.func.isRequired,
+ conditionalPanelLocation: PropTypes.object,
+ closeConditionalPanel: PropTypes.func.isRequired,
+ openConditionalPanel: PropTypes.func.isRequired,
+ updateViewport: PropTypes.func.isRequired,
+ isPaused: PropTypes.bool.isRequired,
+ addBreakpointAtLine: PropTypes.func.isRequired,
+ continueToHere: PropTypes.func.isRequired,
+ updateCursorPosition: PropTypes.func.isRequired,
+ jumpToMappedLocation: PropTypes.func.isRequired,
+ selectedLocation: PropTypes.object,
+ symbols: PropTypes.object,
+ startPanelSize: PropTypes.number.isRequired,
+ endPanelSize: PropTypes.number.isRequired,
+ searchInFileEnabled: PropTypes.bool.isRequired,
+ inlinePreviewEnabled: PropTypes.bool.isRequired,
+ skipPausing: PropTypes.bool.isRequired,
+ blackboxedRanges: PropTypes.object.isRequired,
+ breakableLines: PropTypes.object.isRequired,
+ highlightedLineRange: PropTypes.object,
+ isSourceOnIgnoreList: PropTypes.bool,
+ mapScopesEnabled: PropTypes.bool,
+ };
+ }
+
+ $editorWrapper;
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ editor: null,
+ };
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ let { editor } = this.state;
+
+ if (!editor && nextProps.selectedSource) {
+ editor = this.setupEditor();
+ }
+
+ const shouldUpdateText =
+ nextProps.selectedSource !== this.props.selectedSource ||
+ nextProps.selectedSourceTextContent?.value !==
+ this.props.selectedSourceTextContent?.value ||
+ nextProps.symbols !== this.props.symbols;
+
+ const shouldUpdateSize =
+ nextProps.startPanelSize !== this.props.startPanelSize ||
+ nextProps.endPanelSize !== this.props.endPanelSize;
+
+ const shouldScroll =
+ nextProps.selectedLocation &&
+ this.shouldScrollToLocation(nextProps, editor);
+
+ if (shouldUpdateText || shouldUpdateSize || shouldScroll) {
+ startOperation();
+ if (shouldUpdateText) {
+ this.setText(nextProps, editor);
+ }
+ if (shouldUpdateSize) {
+ editor.codeMirror.setSize();
+ }
+ if (shouldScroll) {
+ this.scrollToLocation(nextProps, editor);
+ }
+ endOperation();
+ }
+
+ if (this.props.selectedSource != nextProps.selectedSource) {
+ this.props.updateViewport();
+ resizeBreakpointGutter(editor.codeMirror);
+ resizeToggleButton(editor.codeMirror);
+ }
+ }
+
+ setupEditor() {
+ const editor = getEditor();
+
+ // disables the default search shortcuts
+ editor._initShortcuts = () => {};
+
+ const node = ReactDOM.findDOMNode(this);
+ if (node instanceof HTMLElement) {
+ editor.appendToLocalElement(node.querySelector(".editor-mount"));
+ }
+
+ const { codeMirror } = editor;
+
+ this.abortController = new window.AbortController();
+
+ // CodeMirror refreshes its internal state on window resize, but we need to also
+ // refresh it when the side panels are resized.
+ // We could have a ResizeObserver instead, but we wouldn't be able to differentiate
+ // between window resize and side panel resize and as a result, might refresh
+ // codeMirror twice, which is wasteful.
+ window.document
+ .querySelector(".editor-pane")
+ .addEventListener("resizeend", () => codeMirror.refresh(), {
+ signal: this.abortController.signal,
+ });
+
+ codeMirror.on("gutterClick", this.onGutterClick);
+ codeMirror.on("cursorActivity", this.onCursorChange);
+
+ const codeMirrorWrapper = codeMirror.getWrapperElement();
+ // Set code editor wrapper to be focusable
+ codeMirrorWrapper.tabIndex = 0;
+ codeMirrorWrapper.addEventListener("keydown", e => this.onKeyDown(e));
+ codeMirrorWrapper.addEventListener("click", e => this.onClick(e));
+ codeMirrorWrapper.addEventListener("mouseover", onMouseOver(codeMirror));
+
+ const toggleFoldMarkerVisibility = e => {
+ if (node instanceof HTMLElement) {
+ node
+ .querySelectorAll(".CodeMirror-guttermarker-subtle")
+ .forEach(elem => {
+ elem.classList.toggle("visible");
+ });
+ }
+ };
+
+ const codeMirrorGutter = codeMirror.getGutterElement();
+ codeMirrorGutter.addEventListener("mouseleave", toggleFoldMarkerVisibility);
+ codeMirrorGutter.addEventListener("mouseenter", toggleFoldMarkerVisibility);
+ codeMirrorWrapper.addEventListener("contextmenu", event =>
+ this.openMenu(event)
+ );
+
+ codeMirror.on("scroll", this.onEditorScroll);
+ this.onEditorScroll();
+ this.setState({ editor });
+ return editor;
+ }
+
+ componentDidMount() {
+ const { shortcuts } = this.context;
+
+ shortcuts.on(L10N.getStr("toggleBreakpoint.key"), this.onToggleBreakpoint);
+ shortcuts.on(
+ L10N.getStr("toggleCondPanel.breakpoint.key"),
+ this.onToggleConditionalPanel
+ );
+ shortcuts.on(
+ L10N.getStr("toggleCondPanel.logPoint.key"),
+ this.onToggleConditionalPanel
+ );
+ shortcuts.on(
+ L10N.getStr("sourceTabs.closeTab.key"),
+ this.onCloseShortcutPress
+ );
+ shortcuts.on("Esc", this.onEscape);
+ }
+
+ onCloseShortcutPress = e => {
+ const { selectedSource } = this.props;
+ if (selectedSource) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.props.closeTab(selectedSource, "shortcut");
+ }
+ };
+
+ componentWillUnmount() {
+ const { editor } = this.state;
+ if (editor) {
+ editor.destroy();
+ editor.codeMirror.off("scroll", this.onEditorScroll);
+ this.setState({ editor: null });
+ }
+
+ const { shortcuts } = this.context;
+ shortcuts.off(L10N.getStr("sourceTabs.closeTab.key"));
+ shortcuts.off(L10N.getStr("toggleBreakpoint.key"));
+ shortcuts.off(L10N.getStr("toggleCondPanel.breakpoint.key"));
+ shortcuts.off(L10N.getStr("toggleCondPanel.logPoint.key"));
+
+ if (this.abortController) {
+ this.abortController.abort();
+ this.abortController = null;
+ }
+ }
+
+ getCurrentLine() {
+ const { codeMirror } = this.state.editor;
+ const { selectedSource } = this.props;
+ if (!selectedSource) {
+ return null;
+ }
+
+ const line = getCursorLine(codeMirror);
+ return toSourceLine(selectedSource.id, line);
+ }
+
+ onToggleBreakpoint = e => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const line = this.getCurrentLine();
+ if (typeof line !== "number") {
+ return;
+ }
+
+ this.props.toggleBreakpointAtLine(line);
+ };
+
+ onToggleConditionalPanel = e => {
+ e.stopPropagation();
+ e.preventDefault();
+
+ const {
+ conditionalPanelLocation,
+ closeConditionalPanel,
+ openConditionalPanel,
+ selectedSource,
+ } = this.props;
+
+ const line = this.getCurrentLine();
+
+ const { codeMirror } = this.state.editor;
+ // add one to column for correct position in editor.
+ const column = getCursorColumn(codeMirror) + 1;
+
+ if (conditionalPanelLocation) {
+ return closeConditionalPanel();
+ }
+
+ if (!selectedSource || typeof line !== "number") {
+ return null;
+ }
+
+ return openConditionalPanel(
+ createLocation({
+ line,
+ column,
+ source: selectedSource,
+ }),
+ false
+ );
+ };
+
+ onEditorScroll = debounce(this.props.updateViewport, 75);
+
+ onKeyDown(e) {
+ const { codeMirror } = this.state.editor;
+ const { key, target } = e;
+ const codeWrapper = codeMirror.getWrapperElement();
+ const textArea = codeWrapper.querySelector("textArea");
+
+ if (key === "Escape" && target == textArea) {
+ e.stopPropagation();
+ e.preventDefault();
+ codeWrapper.focus();
+ } else if (key === "Enter" && target == codeWrapper) {
+ e.preventDefault();
+ // Focus into editor's text area
+ textArea.focus();
+ }
+ }
+
+ /*
+ * The default Esc command is overridden in the CodeMirror keymap to allow
+ * the Esc keypress event to be catched by the toolbox and trigger the
+ * split console. Restore it here, but preventDefault if and only if there
+ * is a multiselection.
+ */
+ onEscape = e => {
+ if (!this.state.editor) {
+ return;
+ }
+
+ const { codeMirror } = this.state.editor;
+ if (codeMirror.listSelections().length > 1) {
+ codeMirror.execCommand("singleSelection");
+ e.preventDefault();
+ }
+ };
+
+ openMenu(event) {
+ event.stopPropagation();
+ event.preventDefault();
+
+ const {
+ selectedSource,
+ selectedSourceTextContent,
+ conditionalPanelLocation,
+ closeConditionalPanel,
+ } = this.props;
+
+ const { editor } = this.state;
+
+ if (!selectedSource || !editor) {
+ return;
+ }
+
+ // only allow one conditionalPanel location.
+ if (conditionalPanelLocation) {
+ closeConditionalPanel();
+ }
+
+ const target = event.target;
+ const { id: sourceId } = selectedSource;
+ const line = lineAtHeight(editor, sourceId, event);
+
+ if (typeof line != "number") {
+ return;
+ }
+
+ if (target.classList.contains("CodeMirror-linenumber")) {
+ const location = createLocation({
+ line,
+ column: undefined,
+ source: selectedSource,
+ });
+
+ const lineText = getLineText(
+ sourceId,
+ selectedSourceTextContent,
+ line
+ ).trim();
+
+ this.props.showEditorGutterContextMenu(event, editor, location, lineText);
+ return;
+ }
+
+ if (target.getAttribute("id") === "columnmarker") {
+ return;
+ }
+
+ const location = getSourceLocationFromMouseEvent(
+ editor,
+ selectedSource,
+ event
+ );
+
+ this.props.showEditorContextMenu(event, editor, location);
+ }
+
+ /**
+ * CodeMirror event handler, called whenever the cursor moves
+ * for user-driven or programatic reasons.
+ */
+ onCursorChange = event => {
+ const { line, ch } = event.doc.getCursor();
+ this.props.selectLocation(
+ createLocation({
+ source: this.props.selectedSource,
+ // CodeMirror cursor location is all 0-based.
+ // Whereast in DevTools frontend and backend,
+ // only colunm is 0-based, the line is 1 based.
+ line: line + 1,
+ column: ch,
+ }),
+ {
+ // Reset the context, so that we don't switch to original
+ // while moving the cursor within a bundle
+ keepContext: false,
+
+ // Avoid highlighting the selected line
+ highlight: false,
+ }
+ );
+ };
+
+ onGutterClick = (cm, line, gutter, ev) => {
+ const {
+ selectedSource,
+ conditionalPanelLocation,
+ closeConditionalPanel,
+ addBreakpointAtLine,
+ continueToHere,
+ breakableLines,
+ blackboxedRanges,
+ isSourceOnIgnoreList,
+ } = this.props;
+
+ // ignore right clicks in the gutter
+ if (isSecondary(ev) || ev.button === 2 || !selectedSource) {
+ return;
+ }
+
+ if (conditionalPanelLocation) {
+ closeConditionalPanel();
+ return;
+ }
+
+ if (gutter === "CodeMirror-foldgutter") {
+ return;
+ }
+
+ const sourceLine = toSourceLine(selectedSource.id, line);
+ if (typeof sourceLine !== "number") {
+ return;
+ }
+
+ // ignore clicks on a non-breakable line
+ if (!breakableLines.has(sourceLine)) {
+ return;
+ }
+
+ if (isCmd(ev)) {
+ continueToHere(
+ createLocation({
+ line: sourceLine,
+ column: undefined,
+ source: selectedSource,
+ })
+ );
+ return;
+ }
+
+ addBreakpointAtLine(
+ sourceLine,
+ ev.altKey,
+ ev.shiftKey ||
+ isLineBlackboxed(
+ blackboxedRanges[selectedSource.url],
+ sourceLine,
+ isSourceOnIgnoreList
+ )
+ );
+ };
+
+ onGutterContextMenu = event => {
+ this.openMenu(event);
+ };
+
+ onClick(e) {
+ const { selectedSource, updateCursorPosition, jumpToMappedLocation } =
+ this.props;
+
+ if (selectedSource) {
+ const sourceLocation = getSourceLocationFromMouseEvent(
+ this.state.editor,
+ selectedSource,
+ e
+ );
+
+ if (e.metaKey && e.altKey) {
+ jumpToMappedLocation(sourceLocation);
+ }
+
+ updateCursorPosition(sourceLocation);
+ }
+ }
+
+ shouldScrollToLocation(nextProps, editor) {
+ if (
+ !nextProps.selectedLocation?.line ||
+ !nextProps.selectedSourceTextContent
+ ) {
+ return false;
+ }
+
+ const { selectedLocation, selectedSourceTextContent } = this.props;
+ const contentChanged =
+ !selectedSourceTextContent?.value &&
+ nextProps.selectedSourceTextContent?.value;
+ const locationChanged = selectedLocation !== nextProps.selectedLocation;
+ const symbolsChanged = nextProps.symbols != this.props.symbols;
+
+ return contentChanged || locationChanged || symbolsChanged;
+ }
+
+ scrollToLocation(nextProps, editor) {
+ const { selectedLocation, selectedSource } = nextProps;
+
+ let { line, column } = toEditorPosition(selectedLocation);
+
+ if (selectedSource && hasDocument(selectedSource.id)) {
+ const doc = getDocument(selectedSource.id);
+ const lineText = doc.getLine(line);
+ column = Math.max(column, getIndentation(lineText));
+ }
+
+ scrollToPosition(editor.codeMirror, line, column);
+ }
+
+ setText(props, editor) {
+ const { selectedSource, selectedSourceTextContent, symbols } = props;
+
+ if (!editor) {
+ return;
+ }
+
+ // check if we previously had a selected source
+ if (!selectedSource) {
+ this.clearEditor();
+ return;
+ }
+
+ if (!selectedSourceTextContent?.value) {
+ showLoading(editor);
+ return;
+ }
+
+ if (selectedSourceTextContent.state === "rejected") {
+ let { value } = selectedSourceTextContent;
+ if (typeof value !== "string") {
+ value = "Unexpected source error";
+ }
+
+ this.showErrorMessage(value);
+ return;
+ }
+
+ showSourceText(editor, selectedSource, selectedSourceTextContent, symbols);
+ }
+
+ clearEditor() {
+ const { editor } = this.state;
+ if (!editor) {
+ return;
+ }
+
+ clearEditor(editor);
+ }
+
+ showErrorMessage(msg) {
+ const { editor } = this.state;
+ if (!editor) {
+ return;
+ }
+
+ showErrorMessage(editor, msg);
+ }
+
+ getInlineEditorStyles() {
+ const { searchInFileEnabled } = this.props;
+
+ if (searchInFileEnabled) {
+ return {
+ height: `calc(100% - ${cssVars.searchbarHeight})`,
+ };
+ }
+
+ return {
+ height: "100%",
+ };
+ }
+
+ renderItems() {
+ const {
+ selectedSource,
+ conditionalPanelLocation,
+ isPaused,
+ inlinePreviewEnabled,
+ highlightedLineRange,
+ blackboxedRanges,
+ isSourceOnIgnoreList,
+ selectedSourceIsBlackBoxed,
+ mapScopesEnabled,
+ } = this.props;
+ const { editor } = this.state;
+
+ if (!selectedSource || !editor || !getDocument(selectedSource.id)) {
+ return null;
+ }
+ return div(
+ null,
+ React.createElement(DebugLine, null),
+ React.createElement(HighlightLine, null),
+ React.createElement(EmptyLines, {
+ editor,
+ }),
+ React.createElement(Breakpoints, {
+ editor,
+ }),
+ isPaused &&
+ selectedSource.isOriginal &&
+ !selectedSource.isPrettyPrinted &&
+ !mapScopesEnabled
+ ? null
+ : React.createElement(Preview, {
+ editor,
+ editorRef: this.$editorWrapper,
+ }),
+ highlightedLineRange
+ ? React.createElement(HighlightLines, {
+ editor,
+ range: highlightedLineRange,
+ })
+ : null,
+ isSourceOnIgnoreList || selectedSourceIsBlackBoxed
+ ? React.createElement(BlackboxLines, {
+ editor,
+ selectedSource,
+ isSourceOnIgnoreList,
+ blackboxedRangesForSelectedSource:
+ blackboxedRanges[selectedSource.url],
+ })
+ : null,
+ React.createElement(Exceptions, null),
+ conditionalPanelLocation
+ ? React.createElement(ConditionalPanel, {
+ editor,
+ })
+ : null,
+ React.createElement(ColumnBreakpoints, {
+ editor,
+ }),
+ isPaused &&
+ inlinePreviewEnabled &&
+ (!selectedSource.isOriginal ||
+ (selectedSource.isOriginal && selectedSource.isPrettyPrinted) ||
+ (selectedSource.isOriginal && mapScopesEnabled))
+ ? React.createElement(InlinePreviews, {
+ editor,
+ selectedSource,
+ })
+ : null
+ );
+ }
+
+ renderSearchInFileBar() {
+ if (!this.props.selectedSource) {
+ return null;
+ }
+ return React.createElement(SearchInFileBar, {
+ editor: this.state.editor,
+ });
+ }
+
+ render() {
+ const { selectedSourceIsBlackBoxed, skipPausing } = this.props;
+ return div(
+ {
+ className: classnames("editor-wrapper", {
+ blackboxed: selectedSourceIsBlackBoxed,
+ "skip-pausing": skipPausing,
+ }),
+ ref: c => (this.$editorWrapper = c),
+ },
+ div({
+ className: "editor-mount devtools-monospace",
+ style: this.getInlineEditorStyles(),
+ }),
+ this.renderSearchInFileBar(),
+ this.renderItems()
+ );
+ }
+}
+
+Editor.contextTypes = {
+ shortcuts: PropTypes.object,
+};
+
+const mapStateToProps = state => {
+ const selectedSource = getSelectedSource(state);
+ const selectedLocation = getSelectedLocation(state);
+
+ return {
+ selectedLocation,
+ selectedSource,
+ selectedSourceTextContent: getSelectedSourceTextContent(state),
+ selectedSourceIsBlackBoxed: selectedSource
+ ? isSourceBlackBoxed(state, selectedSource)
+ : null,
+ isSourceOnIgnoreList:
+ isSourceMapIgnoreListEnabled(state) &&
+ isSourceOnSourceMapIgnoreList(state, selectedSource),
+ searchInFileEnabled: getActiveSearch(state) === "file",
+ conditionalPanelLocation: getConditionalPanelLocation(state),
+ symbols: getSymbols(state, selectedLocation),
+ isPaused: getIsCurrentThreadPaused(state),
+ skipPausing: getSkipPausing(state),
+ inlinePreviewEnabled: getInlinePreview(state),
+ blackboxedRanges: getBlackBoxRanges(state),
+ breakableLines: getSelectedBreakableLines(state),
+ highlightedLineRange: getHighlightedLineRangeForSelectedSource(state),
+ mapScopesEnabled: selectedSource?.isOriginal
+ ? isMapScopesEnabled(state)
+ : null,
+ };
+};
+
+const mapDispatchToProps = dispatch => ({
+ ...bindActionCreators(
+ {
+ openConditionalPanel: actions.openConditionalPanel,
+ closeConditionalPanel: actions.closeConditionalPanel,
+ continueToHere: actions.continueToHere,
+ toggleBreakpointAtLine: actions.toggleBreakpointAtLine,
+ addBreakpointAtLine: actions.addBreakpointAtLine,
+ jumpToMappedLocation: actions.jumpToMappedLocation,
+ updateViewport: actions.updateViewport,
+ updateCursorPosition: actions.updateCursorPosition,
+ closeTab: actions.closeTab,
+ showEditorContextMenu: actions.showEditorContextMenu,
+ showEditorGutterContextMenu: actions.showEditorGutterContextMenu,
+ selectLocation: actions.selectLocation,
+ },
+ dispatch
+ ),
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(Editor);
diff --git a/devtools/client/debugger/src/components/Editor/moz.build b/devtools/client/debugger/src/components/Editor/moz.build
new file mode 100644
index 0000000000..909e57d4eb
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/moz.build
@@ -0,0 +1,31 @@
+# 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 += [
+ "Preview",
+]
+
+CompiledModules(
+ "BlackboxLines.js",
+ "Breakpoint.js",
+ "Breakpoints.js",
+ "ColumnBreakpoint.js",
+ "ColumnBreakpoints.js",
+ "ConditionalPanel.js",
+ "DebugLine.js",
+ "EmptyLines.js",
+ "Exception.js",
+ "Exceptions.js",
+ "Footer.js",
+ "HighlightLine.js",
+ "HighlightLines.js",
+ "index.js",
+ "InlinePreview.js",
+ "InlinePreviewRow.js",
+ "InlinePreviews.js",
+ "SearchInFileBar.js",
+ "Tab.js",
+ "Tabs.js",
+)
diff --git a/devtools/client/debugger/src/components/Editor/tests/Breakpoints.spec.js b/devtools/client/debugger/src/components/Editor/tests/Breakpoints.spec.js
new file mode 100644
index 0000000000..41024081ca
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/tests/Breakpoints.spec.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 React from "devtools/client/shared/vendor/react";
+import { shallow } from "enzyme";
+import Breakpoints from "../Breakpoints";
+
+const BreakpointsComponent = Breakpoints.WrappedComponent;
+
+function generateDefaults(overrides) {
+ const sourceId = "server1.conn1.child1/source1";
+ const matchingBreakpoints = [{ location: { source: { id: sourceId } } }];
+
+ return {
+ selectedSource: { sourceId, get: () => false },
+ editor: {
+ codeMirror: {
+ setGutterMarker: jest.fn(),
+ },
+ },
+ blackboxedRanges: {},
+ breakpointActions: {},
+ editorActions: {},
+ breakpoints: matchingBreakpoints,
+ ...overrides,
+ };
+}
+
+function render(overrides = {}) {
+ const props = generateDefaults(overrides);
+ const component = shallow(React.createElement(BreakpointsComponent, props));
+ return { component, props };
+}
+
+describe("Breakpoints Component", () => {
+ it("should render breakpoints without columns", async () => {
+ const sourceId = "server1.conn1.child1/source1";
+ const breakpoints = [{ location: { source: { id: sourceId } } }];
+
+ const { component, props } = render({ breakpoints });
+ expect(component.find("Breakpoint")).toHaveLength(props.breakpoints.length);
+ });
+
+ it("should render breakpoints with columns", async () => {
+ const sourceId = "server1.conn1.child1/source1";
+ const breakpoints = [{ location: { column: 2, source: { id: sourceId } } }];
+
+ const { component, props } = render({ breakpoints });
+ expect(component.find("Breakpoint")).toHaveLength(props.breakpoints.length);
+ expect(component).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/debugger/src/components/Editor/tests/ConditionalPanel.spec.js b/devtools/client/debugger/src/components/Editor/tests/ConditionalPanel.spec.js
new file mode 100644
index 0000000000..f0adb096c4
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/tests/ConditionalPanel.spec.js
@@ -0,0 +1,77 @@
+/* eslint max-nested-callbacks: ["error", 7] */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+import React from "devtools/client/shared/vendor/react";
+import { mount } from "enzyme";
+import { ConditionalPanel } from "../ConditionalPanel";
+import * as mocks from "../../../utils/test-mockup";
+
+const source = mocks.makeMockSource();
+
+function generateDefaults(overrides, log, line, column, condition, logValue) {
+ const breakpoint = mocks.makeMockBreakpoint(source, line, column);
+ breakpoint.options.condition = condition;
+ breakpoint.options.logValue = logValue;
+
+ return {
+ editor: {
+ CodeMirror: {
+ fromTextArea: jest.fn(() => {
+ return {
+ on: jest.fn(),
+ getWrapperElement: jest.fn(() => {
+ return {
+ addEventListener: jest.fn(),
+ };
+ }),
+ focus: jest.fn(),
+ setCursor: jest.fn(),
+ lineCount: jest.fn(),
+ };
+ }),
+ },
+ codeMirror: {
+ addLineWidget: jest.fn(),
+ },
+ },
+ location: breakpoint.location,
+ source,
+ breakpoint,
+ log,
+ getDefaultValue: jest.fn(),
+ openConditionalPanel: jest.fn(),
+ closeConditionalPanel: jest.fn(),
+ ...overrides,
+ };
+}
+
+function render(log, line, column, condition, logValue, overrides = {}) {
+ const defaults = generateDefaults(
+ overrides,
+ log,
+ line,
+ column,
+ condition,
+ logValue
+ );
+ const props = { ...defaults, ...overrides };
+ const wrapper = mount(React.createElement(ConditionalPanel, props));
+ return { wrapper, props };
+}
+
+describe("ConditionalPanel", () => {
+ it("should render at location of selected breakpoint", () => {
+ const { wrapper } = render(false, 2, 2);
+ expect(wrapper).toMatchSnapshot();
+ });
+ it("should render with condition at selected breakpoint location", () => {
+ const { wrapper } = render(false, 3, 3, "I'm a condition", "not a log");
+ expect(wrapper).toMatchSnapshot();
+ });
+ it("should render with logpoint at selected breakpoint location", () => {
+ const { wrapper } = render(true, 4, 4, "not a condition", "I'm a log");
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/debugger/src/components/Editor/tests/DebugLine.spec.js b/devtools/client/debugger/src/components/Editor/tests/DebugLine.spec.js
new file mode 100644
index 0000000000..767dde9e6d
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/tests/DebugLine.spec.js
@@ -0,0 +1,88 @@
+/* eslint max-nested-callbacks: ["error", 7] */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+import React from "devtools/client/shared/vendor/react";
+import { shallow } from "enzyme";
+
+import DebugLine from "../DebugLine";
+
+import { setDocument } from "../../../utils/editor";
+
+function createMockDocument(clear) {
+ const doc = {
+ addLineClass: jest.fn(),
+ removeLineClass: jest.fn(),
+ markText: jest.fn(() => ({ clear })),
+ getLine: line => "",
+ };
+
+ return doc;
+}
+
+function generateDefaults(editor, overrides) {
+ return {
+ editor,
+ pauseInfo: {
+ why: { type: "breakpoint" },
+ },
+ frame: null,
+ sourceTextContent: null,
+ ...overrides,
+ };
+}
+
+function createLocation(line) {
+ return {
+ source: {
+ id: "foo",
+ },
+ sourceId: "foo",
+ line,
+ column: 2,
+ };
+}
+
+function render(overrides = {}) {
+ const clear = jest.fn();
+ const editor = { codeMirror: {} };
+ const props = generateDefaults(editor, overrides);
+
+ const doc = createMockDocument(clear);
+ setDocument("foo", doc);
+
+ const component = shallow(
+ React.createElement(DebugLine.WrappedComponent, props),
+ {
+ lifecycleExperimental: true,
+ }
+ );
+ return { component, props, clear, editor, doc };
+}
+
+describe("DebugLine Component", () => {
+ describe("pausing at the first location", () => {
+ describe("when there is no selected frame", () => {
+ it("should not set the debug line", () => {
+ const { component, props, doc } = render({ frame: null });
+ const line = 2;
+ const location = createLocation(line);
+
+ component.setProps({ ...props, location });
+ expect(doc.removeLineClass).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("when there is a different source", () => {
+ it("should not set the debug line", async () => {
+ const { component, doc } = render();
+ const newSelectedFrame = { location: { sourceId: "bar" } };
+ expect(doc.removeLineClass).not.toHaveBeenCalled();
+
+ component.setProps({ frame: newSelectedFrame });
+ expect(doc.removeLineClass).not.toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/components/Editor/tests/Footer.spec.js b/devtools/client/debugger/src/components/Editor/tests/Footer.spec.js
new file mode 100644
index 0000000000..c132a28aa3
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/tests/Footer.spec.js
@@ -0,0 +1,70 @@
+/* eslint max-nested-callbacks: ["error", 7] */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+import React from "devtools/client/shared/vendor/react";
+import { shallow } from "enzyme";
+
+import SourceFooter from "../Footer";
+import { createSourceObject } from "../../../utils/test-head";
+import { setDocument } from "../../../utils/editor";
+
+function createMockDocument(clear, position) {
+ const doc = {
+ getCursor: jest.fn(() => position),
+ };
+ return doc;
+}
+
+function generateDefaults(overrides) {
+ return {
+ editor: {
+ codeMirror: {
+ doc: {},
+ cursorActivity: jest.fn(),
+ on: jest.fn(),
+ },
+ },
+ endPanelCollapsed: false,
+ selectedSource: {
+ ...createSourceObject("foo"),
+ content: null,
+ },
+ ...overrides,
+ };
+}
+
+function render(overrides = {}, position = { line: 0, column: 0 }) {
+ const clear = jest.fn();
+ const props = generateDefaults(overrides);
+
+ const doc = createMockDocument(clear, position);
+ setDocument(props.selectedSource.id, doc);
+
+ const component = shallow(
+ React.createElement(SourceFooter.WrappedComponent, props),
+ {
+ lifecycleExperimental: true,
+ }
+ );
+ return { component, props, clear, doc };
+}
+
+describe("SourceFooter Component", () => {
+ describe("default case", () => {
+ it("should render", () => {
+ const { component } = render();
+ expect(component).toMatchSnapshot();
+ });
+ });
+
+ describe("move cursor", () => {
+ it("should render new cursor position", () => {
+ const { component } = render();
+ component.setState({ cursorPosition: { line: 5, column: 10 } });
+
+ expect(component).toMatchSnapshot();
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/components/Editor/tests/__snapshots__/Breakpoints.spec.js.snap b/devtools/client/debugger/src/components/Editor/tests/__snapshots__/Breakpoints.spec.js.snap
new file mode 100644
index 0000000000..a2e47aae58
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/tests/__snapshots__/Breakpoints.spec.js.snap
@@ -0,0 +1,32 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Breakpoints Component should render breakpoints with columns 1`] = `
+<div>
+ <Breakpoint
+ breakpoint={
+ Object {
+ "location": Object {
+ "column": 2,
+ "source": Object {
+ "id": "server1.conn1.child1/source1",
+ },
+ },
+ }
+ }
+ editor={
+ Object {
+ "codeMirror": Object {
+ "setGutterMarker": [MockFunction],
+ },
+ }
+ }
+ key="server1.conn1.child1/source1:undefined:2"
+ selectedSource={
+ Object {
+ "get": [Function],
+ "sourceId": "server1.conn1.child1/source1",
+ }
+ }
+ />
+</div>
+`;
diff --git a/devtools/client/debugger/src/components/Editor/tests/__snapshots__/ConditionalPanel.spec.js.snap b/devtools/client/debugger/src/components/Editor/tests/__snapshots__/ConditionalPanel.spec.js.snap
new file mode 100644
index 0000000000..58e86f5009
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/tests/__snapshots__/ConditionalPanel.spec.js.snap
@@ -0,0 +1,747 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ConditionalPanel should render at location of selected breakpoint 1`] = `
+<ConditionalPanel
+ breakpoint={
+ Object {
+ "disabled": false,
+ "generatedLocation": Object {
+ "column": 2,
+ "line": 2,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "id": "breakpoint",
+ "location": Object {
+ "column": 2,
+ "line": 2,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "options": Object {
+ "condition": undefined,
+ "logValue": undefined,
+ },
+ "originalText": "text",
+ "text": "text",
+ }
+ }
+ closeConditionalPanel={[MockFunction]}
+ editor={
+ Object {
+ "CodeMirror": Object {
+ "fromTextArea": [MockFunction] {
+ "calls": Array [
+ Array [
+ <textarea />,
+ Object {
+ "cursorBlinkRate": 530,
+ "mode": "javascript",
+ "placeholder": "Breakpoint condition, e.g. items.length > 0",
+ "theme": "mozilla",
+ },
+ ],
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": Object {
+ "focus": [MockFunction] {
+ "calls": Array [
+ Array [],
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": undefined,
+ },
+ ],
+ },
+ "getWrapperElement": [MockFunction] {
+ "calls": Array [
+ Array [],
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": Object {
+ "addEventListener": [MockFunction] {
+ "calls": Array [
+ Array [
+ "keydown",
+ [Function],
+ ],
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": undefined,
+ },
+ ],
+ },
+ },
+ },
+ ],
+ },
+ "lineCount": [MockFunction] {
+ "calls": Array [
+ Array [],
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": undefined,
+ },
+ ],
+ },
+ "on": [MockFunction] {
+ "calls": Array [
+ Array [
+ "keydown",
+ [Function],
+ ],
+ Array [
+ "blur",
+ [Function],
+ ],
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": undefined,
+ },
+ Object {
+ "type": "return",
+ "value": undefined,
+ },
+ ],
+ },
+ "setCursor": [MockFunction] {
+ "calls": Array [
+ Array [
+ undefined,
+ 0,
+ ],
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": undefined,
+ },
+ ],
+ },
+ },
+ },
+ ],
+ },
+ },
+ "codeMirror": Object {
+ "addLineWidget": [MockFunction] {
+ "calls": Array [
+ Array [
+ 1,
+ <div>
+ <div
+ class="conditional-breakpoint-panel"
+ >
+ <div
+ class="prompt"
+ >
+ »
+ </div>
+ <textarea />
+ </div>
+ </div>,
+ Object {
+ "coverGutter": true,
+ "noHScroll": true,
+ },
+ ],
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": undefined,
+ },
+ ],
+ },
+ },
+ }
+ }
+ getDefaultValue={[MockFunction]}
+ location={
+ Object {
+ "column": 2,
+ "line": 2,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ }
+ }
+ log={false}
+ openConditionalPanel={[MockFunction]}
+ source={
+ Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ }
+ }
+/>
+`;
+
+exports[`ConditionalPanel should render with condition at selected breakpoint location 1`] = `
+<ConditionalPanel
+ breakpoint={
+ Object {
+ "disabled": false,
+ "generatedLocation": Object {
+ "column": 3,
+ "line": 3,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "id": "breakpoint",
+ "location": Object {
+ "column": 3,
+ "line": 3,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "options": Object {
+ "condition": "I'm a condition",
+ "logValue": "not a log",
+ },
+ "originalText": "text",
+ "text": "text",
+ }
+ }
+ closeConditionalPanel={[MockFunction]}
+ editor={
+ Object {
+ "CodeMirror": Object {
+ "fromTextArea": [MockFunction] {
+ "calls": Array [
+ Array [
+ <textarea>
+ I'm a condition
+ </textarea>,
+ Object {
+ "cursorBlinkRate": 530,
+ "mode": "javascript",
+ "placeholder": "Breakpoint condition, e.g. items.length > 0",
+ "theme": "mozilla",
+ },
+ ],
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": Object {
+ "focus": [MockFunction] {
+ "calls": Array [
+ Array [],
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": undefined,
+ },
+ ],
+ },
+ "getWrapperElement": [MockFunction] {
+ "calls": Array [
+ Array [],
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": Object {
+ "addEventListener": [MockFunction] {
+ "calls": Array [
+ Array [
+ "keydown",
+ [Function],
+ ],
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": undefined,
+ },
+ ],
+ },
+ },
+ },
+ ],
+ },
+ "lineCount": [MockFunction] {
+ "calls": Array [
+ Array [],
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": undefined,
+ },
+ ],
+ },
+ "on": [MockFunction] {
+ "calls": Array [
+ Array [
+ "keydown",
+ [Function],
+ ],
+ Array [
+ "blur",
+ [Function],
+ ],
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": undefined,
+ },
+ Object {
+ "type": "return",
+ "value": undefined,
+ },
+ ],
+ },
+ "setCursor": [MockFunction] {
+ "calls": Array [
+ Array [
+ undefined,
+ 0,
+ ],
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": undefined,
+ },
+ ],
+ },
+ },
+ },
+ ],
+ },
+ },
+ "codeMirror": Object {
+ "addLineWidget": [MockFunction] {
+ "calls": Array [
+ Array [
+ 2,
+ <div>
+ <div
+ class="conditional-breakpoint-panel"
+ >
+ <div
+ class="prompt"
+ >
+ »
+ </div>
+ <textarea>
+ I'm a condition
+ </textarea>
+ </div>
+ </div>,
+ Object {
+ "coverGutter": true,
+ "noHScroll": true,
+ },
+ ],
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": undefined,
+ },
+ ],
+ },
+ },
+ }
+ }
+ getDefaultValue={[MockFunction]}
+ location={
+ Object {
+ "column": 3,
+ "line": 3,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ }
+ }
+ log={false}
+ openConditionalPanel={[MockFunction]}
+ source={
+ Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ }
+ }
+/>
+`;
+
+exports[`ConditionalPanel should render with logpoint at selected breakpoint location 1`] = `
+<ConditionalPanel
+ breakpoint={
+ Object {
+ "disabled": false,
+ "generatedLocation": Object {
+ "column": 4,
+ "line": 4,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "id": "breakpoint",
+ "location": Object {
+ "column": 4,
+ "line": 4,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "options": Object {
+ "condition": "not a condition",
+ "logValue": "I'm a log",
+ },
+ "originalText": "text",
+ "text": "text",
+ }
+ }
+ closeConditionalPanel={[MockFunction]}
+ editor={
+ Object {
+ "CodeMirror": Object {
+ "fromTextArea": [MockFunction] {
+ "calls": Array [
+ Array [
+ <textarea>
+ I'm a log
+ </textarea>,
+ Object {
+ "cursorBlinkRate": 530,
+ "mode": "javascript",
+ "placeholder": "Log message, e.g. displayName",
+ "theme": "mozilla",
+ },
+ ],
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": Object {
+ "focus": [MockFunction] {
+ "calls": Array [
+ Array [],
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": undefined,
+ },
+ ],
+ },
+ "getWrapperElement": [MockFunction] {
+ "calls": Array [
+ Array [],
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": Object {
+ "addEventListener": [MockFunction] {
+ "calls": Array [
+ Array [
+ "keydown",
+ [Function],
+ ],
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": undefined,
+ },
+ ],
+ },
+ },
+ },
+ ],
+ },
+ "lineCount": [MockFunction] {
+ "calls": Array [
+ Array [],
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": undefined,
+ },
+ ],
+ },
+ "on": [MockFunction] {
+ "calls": Array [
+ Array [
+ "keydown",
+ [Function],
+ ],
+ Array [
+ "blur",
+ [Function],
+ ],
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": undefined,
+ },
+ Object {
+ "type": "return",
+ "value": undefined,
+ },
+ ],
+ },
+ "setCursor": [MockFunction] {
+ "calls": Array [
+ Array [
+ undefined,
+ 0,
+ ],
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": undefined,
+ },
+ ],
+ },
+ },
+ },
+ ],
+ },
+ },
+ "codeMirror": Object {
+ "addLineWidget": [MockFunction] {
+ "calls": Array [
+ Array [
+ 3,
+ <div>
+ <div
+ class="conditional-breakpoint-panel log-point"
+ >
+ <div
+ class="prompt"
+ >
+ »
+ </div>
+ <textarea>
+ I'm a log
+ </textarea>
+ </div>
+ </div>,
+ Object {
+ "coverGutter": true,
+ "noHScroll": true,
+ },
+ ],
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": undefined,
+ },
+ ],
+ },
+ },
+ }
+ }
+ getDefaultValue={[MockFunction]}
+ location={
+ Object {
+ "column": 4,
+ "line": 4,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ }
+ }
+ log={true}
+ openConditionalPanel={[MockFunction]}
+ source={
+ Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ }
+ }
+/>
+`;
diff --git a/devtools/client/debugger/src/components/Editor/tests/__snapshots__/Footer.spec.js.snap b/devtools/client/debugger/src/components/Editor/tests/__snapshots__/Footer.spec.js.snap
new file mode 100644
index 0000000000..a453b034ff
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/tests/__snapshots__/Footer.spec.js.snap
@@ -0,0 +1,93 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SourceFooter Component default case should render 1`] = `
+<div
+ className="source-footer"
+>
+ <div
+ className="source-footer-start"
+ >
+ <div
+ className="commands"
+ >
+ <button
+ aria-label="Ignore source"
+ className="action black-box"
+ key="black-box"
+ onClick={[Function]}
+ title="Ignore source"
+ >
+ <AccessibleImage
+ className="blackBox"
+ />
+ </button>
+ <button
+ className="action prettyPrint"
+ disabled={true}
+ key="prettyPrint"
+ onClick={[Function]}
+ >
+ <AccessibleImage
+ className="prettyPrint"
+ />
+ </button>
+ </div>
+ </div>
+ <div
+ className="source-footer-end"
+ >
+ <PaneToggleButton
+ collapsed={false}
+ horizontal={false}
+ key="toggle"
+ position="end"
+ />
+ </div>
+</div>
+`;
+
+exports[`SourceFooter Component move cursor should render new cursor position 1`] = `
+<div
+ className="source-footer"
+>
+ <div
+ className="source-footer-start"
+ >
+ <div
+ className="commands"
+ >
+ <button
+ aria-label="Ignore source"
+ className="action black-box"
+ key="black-box"
+ onClick={[Function]}
+ title="Ignore source"
+ >
+ <AccessibleImage
+ className="blackBox"
+ />
+ </button>
+ <button
+ className="action prettyPrint"
+ disabled={true}
+ key="prettyPrint"
+ onClick={[Function]}
+ >
+ <AccessibleImage
+ className="prettyPrint"
+ />
+ </button>
+ </div>
+ </div>
+ <div
+ className="source-footer-end"
+ >
+ <PaneToggleButton
+ collapsed={false}
+ horizontal={false}
+ key="toggle"
+ position="end"
+ />
+ </div>
+</div>
+`;
diff --git a/devtools/client/debugger/src/components/PrimaryPanes/Outline.css b/devtools/client/debugger/src/components/PrimaryPanes/Outline.css
new file mode 100644
index 0000000000..6eb890f2d8
--- /dev/null
+++ b/devtools/client/debugger/src/components/PrimaryPanes/Outline.css
@@ -0,0 +1,158 @@
+/* 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/>. */
+
+
+.sources-panel .outline {
+ display: flex;
+ height: 100%;
+}
+
+.source-outline-panel {
+ flex: 1;
+ overflow: auto;
+}
+
+.outline {
+ overflow-y: hidden;
+}
+
+.outline > div {
+ width: 100%;
+ position: relative;
+}
+
+.outline-pane-info {
+ padding: 0.5em;
+ width: 100%;
+ font-style: italic;
+ text-align: center;
+ user-select: none;
+ font-size: 12px;
+ overflow: hidden;
+}
+
+.outline-list {
+ margin: 0;
+ padding: 4px 0;
+ position: absolute;
+ top: 25px;
+ bottom: 25px;
+ left: 0;
+ right: 0;
+ list-style-type: none;
+ overflow: auto;
+}
+
+.outline-list__class-list {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.outline-list__class-list > .outline-list__element {
+ padding-inline-start: 2rem;
+}
+
+.outline-list__class-list .function-signature .function-name {
+ color: var(--theme-highlight-green);
+}
+
+.outline-list .function-signature .paren {
+ color: inherit;
+}
+
+.outline-list__class h2 {
+ font-weight: normal;
+ font-size: 1em;
+ padding: 3px 0;
+ padding-inline-start: 10px;
+ color: var(--blue-55);
+ margin: 0;
+}
+
+.outline-list__class:not(:first-child) h2 {
+ margin-top: 12px;
+}
+
+.outline-list h2:hover {
+ background: var(--theme-toolbar-background-hover);
+}
+
+.theme-dark .outline-list h2 {
+ color: var(--theme-highlight-blue);
+}
+
+.outline-list h2 .keyword {
+ color: var(--theme-highlight-red);
+}
+
+.outline-list__class h2.focused {
+ background: var(--theme-selection-background);
+}
+
+.outline-list__class h2.focused,
+.outline-list__class h2.focused .keyword {
+ color: var(--theme-selection-color);
+}
+
+.outline-list__element {
+ padding: 3px 10px 3px 10px;
+ cursor: default;
+ white-space: nowrap;
+}
+
+.outline-list > .outline-list__element {
+ padding-inline-start: 1rem;
+}
+
+.outline-list__element-icon {
+ padding-inline-end: 0.4rem;
+}
+
+.outline-list__element:hover {
+ background: var(--theme-toolbar-background-hover);
+}
+
+.outline-list__element.focused {
+ background: var(--theme-selection-background);
+}
+
+.outline-list__element.focused .outline-list__element-icon,
+.outline-list__element.focused .function-signature * {
+ color: var(--theme-selection-color);
+}
+
+.outline-footer {
+ display: flex;
+ box-sizing: border-box;
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 25px;
+ background: var(--theme-body-background);
+ border-top: 1px solid var(--theme-splitter-color);
+ opacity: 1;
+ z-index: 1;
+ user-select: none;
+}
+
+.outline-footer button {
+ color: var(--theme-body-color);
+
+ /* Since the buttons are on the bottom left edge, we need to adjust the outline so
+ it's not off-screen */
+ outline-offset: -2px;
+
+ &.active {
+ background: var(--theme-selection-background);
+ color: var(--theme-selection-color);
+
+ &:focus-visible {
+ /* When the button is active, it has a similar background color than the outline color
+ so we put the focus box-shadow inside the element to make the focus indicator visible */
+ box-shadow: inset var(--theme-outline-box-shadow);
+ }
+ }
+}
diff --git a/devtools/client/debugger/src/components/PrimaryPanes/Outline.js b/devtools/client/debugger/src/components/PrimaryPanes/Outline.js
new file mode 100644
index 0000000000..79ebf7a38e
--- /dev/null
+++ b/devtools/client/debugger/src/components/PrimaryPanes/Outline.js
@@ -0,0 +1,388 @@
+/* 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 React, { Component } from "devtools/client/shared/vendor/react";
+import {
+ div,
+ ul,
+ li,
+ span,
+ h2,
+ button,
+} from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+
+import { containsPosition, positionAfter } from "../../utils/ast";
+import { createLocation } from "../../utils/location";
+
+import actions from "../../actions/index";
+import {
+ getSelectedLocation,
+ getCursorPosition,
+ getSelectedSourceTextContent,
+} from "../../selectors/index";
+
+import OutlineFilter from "./OutlineFilter";
+import PreviewFunction from "../shared/PreviewFunction";
+
+import { isFulfilled } from "../../utils/async-value";
+
+const classnames = require("resource://devtools/client/shared/classnames.js");
+const {
+ score: fuzzaldrinScore,
+} = require("resource://devtools/client/shared/vendor/fuzzaldrin-plus.js");
+
+// Set higher to make the fuzzaldrin filter more specific
+const FUZZALDRIN_FILTER_THRESHOLD = 15000;
+
+/**
+ * Check whether the name argument matches the fuzzy filter argument
+ */
+const filterOutlineItem = (name, filter) => {
+ if (!filter) {
+ return true;
+ }
+
+ if (filter.length === 1) {
+ // when filter is a single char just check if it starts with the char
+ return filter.toLowerCase() === name.toLowerCase()[0];
+ }
+ return fuzzaldrinScore(name, filter) > FUZZALDRIN_FILTER_THRESHOLD;
+};
+
+// Checks if an element is visible inside its parent element
+function isVisible(element, parent) {
+ const parentRect = parent.getBoundingClientRect();
+ const elementRect = element.getBoundingClientRect();
+
+ const parentTop = parentRect.top;
+ const parentBottom = parentRect.bottom;
+ const elTop = elementRect.top;
+ const elBottom = elementRect.bottom;
+
+ return parentTop < elTop && parentBottom > elBottom;
+}
+
+export class Outline extends Component {
+ constructor(props) {
+ super(props);
+ this.focusedElRef = null;
+ this.state = { filter: "", focusedItem: null, symbols: null };
+ }
+
+ static get propTypes() {
+ return {
+ alphabetizeOutline: PropTypes.bool.isRequired,
+ cursorPosition: PropTypes.object,
+ flashLineRange: PropTypes.func.isRequired,
+ onAlphabetizeClick: PropTypes.func.isRequired,
+ selectLocation: PropTypes.func.isRequired,
+ selectedLocation: PropTypes.object.isRequired,
+ getFunctionSymbols: PropTypes.func.isRequired,
+ getClassSymbols: PropTypes.func.isRequired,
+ canFetchSymbols: PropTypes.bool,
+ };
+ }
+
+ componentDidMount() {
+ if (!this.props.canFetchSymbols) {
+ return;
+ }
+ this.getClassAndFunctionSymbols();
+ }
+
+ componentDidUpdate(prevProps) {
+ const { cursorPosition, selectedLocation, canFetchSymbols } = this.props;
+ if (cursorPosition && cursorPosition !== prevProps.cursorPosition) {
+ this.setFocus(cursorPosition);
+ }
+
+ if (
+ this.focusedElRef &&
+ !isVisible(this.focusedElRef, this.refs.outlineList)
+ ) {
+ this.focusedElRef.scrollIntoView({ block: "center" });
+ }
+
+ // Lets make sure the source text has been loaded and is different
+ if (canFetchSymbols && prevProps.selectedLocation !== selectedLocation) {
+ this.getClassAndFunctionSymbols();
+ }
+ }
+
+ async getClassAndFunctionSymbols() {
+ const { selectedLocation, getFunctionSymbols, getClassSymbols } =
+ this.props;
+
+ const functions = await getFunctionSymbols(selectedLocation);
+ const classes = await getClassSymbols(selectedLocation);
+
+ this.setState({ symbols: { functions, classes } });
+ }
+
+ async setFocus(cursorPosition) {
+ const { symbols } = this.state;
+
+ let classes = [];
+ let functions = [];
+
+ if (symbols) {
+ ({ classes, functions } = symbols);
+ }
+
+ // Find items that enclose the selected location
+ const enclosedItems = [...classes, ...functions].filter(
+ ({ name, location }) => containsPosition(location, cursorPosition)
+ );
+
+ if (!enclosedItems.length) {
+ this.setState({ focusedItem: null });
+ return;
+ }
+
+ // Find the closest item to the selected location to focus
+ const closestItem = enclosedItems.reduce((item, closest) =>
+ positionAfter(item.location, closest.location) ? item : closest
+ );
+
+ this.setState({ focusedItem: closestItem });
+ }
+
+ selectItem(selectedItem) {
+ const { selectedLocation, selectLocation } = this.props;
+ if (!selectedLocation || !selectedItem) {
+ return;
+ }
+
+ selectLocation(
+ createLocation({
+ source: selectedLocation.source,
+ line: selectedItem.location.start.line,
+ column: selectedItem.location.start.column,
+ })
+ );
+
+ this.setState({ focusedItem: selectedItem });
+ }
+
+ onContextMenu(event, func) {
+ event.stopPropagation();
+ event.preventDefault();
+
+ const { symbols } = this.state;
+ this.props.showOutlineContextMenu(event, func, symbols);
+ }
+
+ updateFilter = filter => {
+ this.setState({ filter: filter.trim() });
+ };
+
+ renderPlaceholder() {
+ const placeholderMessage = this.props.selectedLocation
+ ? L10N.getStr("outline.noFunctions")
+ : L10N.getStr("outline.noFileSelected");
+ return div(
+ {
+ className: "outline-pane-info",
+ },
+ placeholderMessage
+ );
+ }
+
+ renderLoading() {
+ return div(
+ {
+ className: "outline-pane-info",
+ },
+ L10N.getStr("loadingText")
+ );
+ }
+
+ renderFunction(func) {
+ const { focusedItem } = this.state;
+ const { name, location, parameterNames } = func;
+ const isFocused = focusedItem === func;
+ return li(
+ {
+ key: `${name}:${location.start.line}:${location.start.column}`,
+ className: classnames("outline-list__element", {
+ focused: isFocused,
+ }),
+ ref: el => {
+ if (isFocused) {
+ this.focusedElRef = el;
+ }
+ },
+ onClick: () => this.selectItem(func),
+ onContextMenu: e => this.onContextMenu(e, func),
+ },
+ span(
+ {
+ className: "outline-list__element-icon",
+ },
+ "λ"
+ ),
+ React.createElement(PreviewFunction, {
+ func: {
+ name,
+ parameterNames,
+ },
+ })
+ );
+ }
+
+ renderClassHeader(klass) {
+ return div(
+ null,
+ span(
+ {
+ className: "keyword",
+ },
+ "class"
+ ),
+ " ",
+ klass
+ );
+ }
+
+ renderClassFunctions(klass, functions) {
+ const { symbols } = this.state;
+
+ if (!symbols || klass == null || !functions.length) {
+ return null;
+ }
+
+ const { focusedItem } = this.state;
+ const classFunc = functions.find(func => func.name === klass);
+ const classFunctions = functions.filter(func => func.klass === klass);
+ const classInfo = symbols.classes.find(c => c.name === klass);
+
+ const item = classFunc || classInfo;
+ const isFocused = focusedItem === item;
+
+ return li(
+ {
+ className: "outline-list__class",
+ ref: el => {
+ if (isFocused) {
+ this.focusedElRef = el;
+ }
+ },
+ key: klass,
+ },
+ h2(
+ {
+ className: classnames({
+ focused: isFocused,
+ }),
+ onClick: () => this.selectItem(item),
+ },
+ classFunc
+ ? this.renderFunction(classFunc)
+ : this.renderClassHeader(klass)
+ ),
+ ul(
+ {
+ className: "outline-list__class-list",
+ },
+ classFunctions.map(func => this.renderFunction(func))
+ )
+ );
+ }
+
+ renderFunctions(functions) {
+ const { filter } = this.state;
+ let classes = [...new Set(functions.map(({ klass }) => klass))];
+ const namedFunctions = functions.filter(
+ ({ name, klass }) =>
+ filterOutlineItem(name, filter) && !klass && !classes.includes(name)
+ );
+ const classFunctions = functions.filter(
+ ({ name, klass }) => filterOutlineItem(name, filter) && !!klass
+ );
+
+ if (this.props.alphabetizeOutline) {
+ const sortByName = (a, b) => (a.name < b.name ? -1 : 1);
+ namedFunctions.sort(sortByName);
+ classes = classes.sort();
+ classFunctions.sort(sortByName);
+ }
+ return ul(
+ {
+ ref: "outlineList",
+ className: "outline-list devtools-monospace",
+ dir: "ltr",
+ },
+ namedFunctions.map(func => this.renderFunction(func)),
+ classes.map(klass => this.renderClassFunctions(klass, classFunctions))
+ );
+ }
+
+ renderFooter() {
+ return div(
+ {
+ className: "outline-footer",
+ },
+ button(
+ {
+ onClick: this.props.onAlphabetizeClick,
+ className: this.props.alphabetizeOutline ? "active" : "",
+ },
+ L10N.getStr("outline.sortLabel")
+ )
+ );
+ }
+
+ render() {
+ const { selectedLocation } = this.props;
+ const { filter, symbols } = this.state;
+
+ if (!selectedLocation) {
+ return this.renderPlaceholder();
+ }
+
+ if (!symbols) {
+ return this.renderLoading();
+ }
+
+ const { functions } = symbols;
+
+ if (functions.length === 0) {
+ return this.renderPlaceholder();
+ }
+
+ return div(
+ {
+ className: "outline",
+ },
+ div(
+ null,
+ React.createElement(OutlineFilter, {
+ filter: filter,
+ updateFilter: this.updateFilter,
+ }),
+ this.renderFunctions(functions),
+ this.renderFooter()
+ )
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ const selectedSourceTextContent = getSelectedSourceTextContent(state);
+ return {
+ selectedLocation: getSelectedLocation(state),
+ canFetchSymbols:
+ selectedSourceTextContent && isFulfilled(selectedSourceTextContent),
+ cursorPosition: getCursorPosition(state),
+ };
+};
+
+export default connect(mapStateToProps, {
+ selectLocation: actions.selectLocation,
+ showOutlineContextMenu: actions.showOutlineContextMenu,
+ getFunctionSymbols: actions.getFunctionSymbols,
+ getClassSymbols: actions.getClassSymbols,
+})(Outline);
diff --git a/devtools/client/debugger/src/components/PrimaryPanes/OutlineFilter.css b/devtools/client/debugger/src/components/PrimaryPanes/OutlineFilter.css
new file mode 100644
index 0000000000..787e527490
--- /dev/null
+++ b/devtools/client/debugger/src/components/PrimaryPanes/OutlineFilter.css
@@ -0,0 +1,23 @@
+/* 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/. */
+
+.outline-filter {
+ border: 1px solid var(--theme-splitter-color);
+ border-top: 0px;
+}
+
+.outline-filter-input {
+ height: 24px;
+ width: 100%;
+ background-color: var(--theme-sidebar-background);
+ color: var(--theme-body-color);
+ font-size: inherit;
+ user-select: text;
+ outline-offset: -2px;
+}
+
+.outline-filter-input::placeholder {
+ color: var(--theme-text-color-alt);
+ opacity: 1;
+}
diff --git a/devtools/client/debugger/src/components/PrimaryPanes/OutlineFilter.js b/devtools/client/debugger/src/components/PrimaryPanes/OutlineFilter.js
new file mode 100644
index 0000000000..12f6fed2b7
--- /dev/null
+++ b/devtools/client/debugger/src/components/PrimaryPanes/OutlineFilter.js
@@ -0,0 +1,68 @@
+/* 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 { Component } from "devtools/client/shared/vendor/react";
+import {
+ form,
+ div,
+ input,
+} from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+export default class OutlineFilter extends Component {
+ state = { focused: false };
+
+ static get propTypes() {
+ return {
+ filter: PropTypes.string.isRequired,
+ updateFilter: PropTypes.func.isRequired,
+ };
+ }
+
+ setFocus = shouldFocus => {
+ this.setState({ focused: shouldFocus });
+ };
+
+ onChange = e => {
+ this.props.updateFilter(e.target.value);
+ };
+
+ onKeyDown = e => {
+ if (e.key === "Escape" && this.props.filter !== "") {
+ // use preventDefault to override toggling the split-console which is
+ // also bound to the ESC key
+ e.preventDefault();
+ this.props.updateFilter("");
+ } else if (e.key === "Enter") {
+ // We must prevent the form submission from taking any action
+ // https://github.com/firefox-devtools/debugger/pull/7308
+ e.preventDefault();
+ }
+ };
+
+ render() {
+ const { focused } = this.state;
+ return div(
+ {
+ className: "outline-filter",
+ },
+ form(
+ null,
+ input({
+ className: classnames("outline-filter-input devtools-filterinput", {
+ focused,
+ }),
+ onFocus: () => this.setFocus(true),
+ onBlur: () => this.setFocus(false),
+ placeholder: L10N.getStr("outline.placeholder"),
+ value: this.props.filter,
+ type: "text",
+ onChange: this.onChange,
+ onKeyDown: this.onKeyDown,
+ })
+ )
+ );
+ }
+}
diff --git a/devtools/client/debugger/src/components/PrimaryPanes/ProjectSearch.css b/devtools/client/debugger/src/components/PrimaryPanes/ProjectSearch.css
new file mode 100644
index 0000000000..eb11149a79
--- /dev/null
+++ b/devtools/client/debugger/src/components/PrimaryPanes/ProjectSearch.css
@@ -0,0 +1,227 @@
+/* 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/>. */
+
+.search-container {
+ position: absolute;
+ top: var(--editor-header-height);
+ left: 0;
+ width: calc(100% - 1px);
+ height: calc(100% - var(--editor-header-height));
+ display: flex;
+ flex-direction: column;
+ z-index: 20;
+ overflow-y: hidden;
+
+ /* Using the same colors as the Netmonitor's --table-selection-background-hover */
+ --search-result-background-hover: rgba(209, 232, 255, 0.8);
+}
+
+.theme-dark .search-container {
+ --search-result-background-hover: rgba(53, 59, 72, 1);
+}
+
+.project-text-search {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ overflow-y: hidden;
+ height: 100%;
+}
+
+.project-text-search .result {
+ display: contents;
+ cursor: default;
+ line-height: 16px;
+ font-size: 11px;
+ font-family: var(--monospace-font-family);
+}
+
+.project-text-search .result:hover > * {
+ background-color: var(--search-result-background-hover);
+}
+
+.project-text-search .result .line-number {
+ grid-column: 1;
+ padding-block: 1px;
+ padding-inline-start: 4px;
+ padding-inline-end: 6px;
+ text-align: end;
+ color: var(--theme-text-color-alt);
+}
+
+.unavailable-source {
+ white-space: pre;
+
+ .tooltip-panel {
+ padding: 1em;
+ }
+}
+
+.project-text-search .result .line-value {
+ grid-column: 2;
+ padding-block: 1px;
+ padding-inline-end: 4px;
+ text-overflow: ellipsis;
+ overflow-x: hidden;
+ outline-offset: -2px;
+}
+
+.project-text-search .result .query-match {
+ border-bottom: 1px solid var(--theme-contrast-border);
+ color: var(--theme-contrast-color);
+ background-color: var(--theme-contrast-background);
+}
+
+.project-text-search .result.focused .query-match {
+ border-bottom: none;
+ color: var(--theme-selection-background);
+ background-color: var(--theme-selection-color);
+}
+
+.project-text-search .tree-indent {
+ display: none;
+}
+
+.project-search-results-toolbar {
+ display: grid;
+ grid-template-columns: 1fr auto;
+ background-color: var(--theme-accordion-header-background);
+ border-bottom: 1px solid var(--theme-splitter-color);
+ padding: 2px 8px;
+ align-items: center;
+ gap: 4px;
+}
+
+
+.project-text-search .refresh-btn {
+ background-color: transparent;
+ padding: 2px;
+ display: grid;
+ --size: 16px;
+ --highlight-size: 5px;
+ --remain-size: calc(var(--size) - var(--highlight-size));
+ width: var(--size);
+ aspect-ratio: 1;
+ box-sizing: content-box;
+ grid-template-rows: var(--highlight-size) var(--remain-size);
+ grid-template-columns: var(--remain-size) var(--highlight-size);
+
+ &.devtools-button:focus-visible {
+ outline: var(--theme-focus-outline);
+ }
+
+ &.highlight::after {
+ content: "";
+ display: block;
+ grid-row: 1 / 2;
+ grid-column: 2 / 3;
+ height: 5px;
+ width: 5px;
+ background-color: var(--blue-40);
+ border-radius: 100%;
+ outline: 1px solid var(--theme-sidebar-background);
+ z-index: 1;
+ }
+
+ .img {
+ grid-row: 1 / -1;
+ grid-column: 1 / -1;
+ transition: rotate 0.2s;
+ width: 14px; height: 14px;
+
+ .highlight & {
+ rotate: 0.75turn;
+ }
+ }
+}
+
+.project-text-search .no-result-msg {
+ color: var(--theme-text-color-inactive);
+ font-size: 24px;
+ padding: 4px 15px;
+ max-width: 100%;
+ overflow-wrap: break-word;
+ hyphens: auto;
+}
+
+.project-text-search .file-result {
+ grid-column: 1/3;
+ display: flex;
+ align-items: center;
+ width: 100%;
+ min-height: 24px;
+ padding: 2px 4px;
+ font-weight: bold;
+ font-size: 12px;
+ line-height: 16px;
+ cursor: default;
+}
+
+.project-text-search .file-result .img {
+ margin-inline: 2px;
+}
+
+.project-text-search .file-result .img.file {
+ margin-inline-end: 4px;
+}
+
+.project-text-search .file-path {
+ flex: 0 1 auto;
+ padding-inline-end: 4px;
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.project-text-search .file-path:empty {
+ display: none;
+}
+
+.project-text-search .search-field {
+ display: flex;
+ align-self: stretch;
+ flex-grow: 1;
+ width: 100%;
+ border-bottom: none;
+}
+
+.project-text-search .tree {
+ overflow-x: hidden;
+ overflow-y: auto;
+ height: 100%;
+ display: grid;
+ min-width: 100%;
+ white-space: nowrap;
+ user-select: none;
+ align-content: start;
+ /* Align the second column to the search input's text value */
+ grid-template-columns: minmax(40px, auto) 1fr;
+ padding-top: 4px;
+}
+
+/* Fake padding-bottom using a pseudo-element because Gecko doesn't render the
+ padding-bottom in a scroll container */
+.project-text-search .tree::after {
+ content: "";
+ display: block;
+ height: 4px;
+}
+
+.project-text-search .tree .tree-node {
+ display: contents;
+}
+
+/* Focus values */
+
+.project-text-search .file-result.focused,
+.project-text-search .result.focused .line-value,
+.project-text-search .result.focused .line-number {
+ color: var(--theme-selection-color);
+ background-color: var(--theme-selection-background);
+}
+
+.project-text-search .file-result.focused .img {
+ background-color: currentColor;
+}
diff --git a/devtools/client/debugger/src/components/PrimaryPanes/ProjectSearch.js b/devtools/client/debugger/src/components/PrimaryPanes/ProjectSearch.js
new file mode 100644
index 0000000000..68b08aed2b
--- /dev/null
+++ b/devtools/client/debugger/src/components/PrimaryPanes/ProjectSearch.js
@@ -0,0 +1,480 @@
+/* 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 React, { Component } from "devtools/client/shared/vendor/react";
+import {
+ button,
+ div,
+ span,
+} from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import actions from "../../actions/index";
+
+import { getEditor } from "../../utils/editor/index";
+import { searchKeys } from "../../constants";
+
+import { getRelativePath } from "../../utils/sources-tree/utils";
+import { getFormattedSourceId } from "../../utils/source";
+import {
+ getProjectSearchQuery,
+ getNavigateCounter,
+} from "../../selectors/index";
+
+import SearchInput from "../shared/SearchInput";
+import AccessibleImage from "../shared/AccessibleImage";
+
+const { PluralForm } = require("resource://devtools/shared/plural-form.js");
+const classnames = require("resource://devtools/client/shared/classnames.js");
+const Tree = require("resource://devtools/client/shared/components/Tree.js");
+const { debounce } = require("resource://devtools/shared/debounce.js");
+const { throttle } = require("resource://devtools/shared/throttle.js");
+
+const {
+ HTMLTooltip,
+} = require("resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js");
+
+export const statusType = {
+ initial: "INITIAL",
+ fetching: "FETCHING",
+ cancelled: "CANCELLED",
+ done: "DONE",
+ error: "ERROR",
+};
+
+function getFilePath(item, index) {
+ return item.type === "RESULT"
+ ? `${item.location.source.id}-${index || "$"}`
+ : `${item.location.source.id}-${item.location.line}-${
+ item.location.column
+ }-${index || "$"}`;
+}
+
+export class ProjectSearch extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ // We may restore a previous state when changing tabs in the primary panes,
+ // or when restoring primary panes from collapse.
+ query: this.props.query || "",
+
+ inputFocused: false,
+ focusedItem: null,
+ expanded: new Set(),
+ results: [],
+ navigateCounter: null,
+ status: statusType.done,
+ };
+ // Use throttle for updating results in order to prevent delaying showing result until the end of the search
+ this.onUpdatedResults = throttle(this.onUpdatedResults.bind(this), 100);
+ // Use debounce for input processing in order to wait for the end of user input edition before triggerring the search
+ this.doSearch = debounce(this.doSearch.bind(this), 100);
+ this.doSearch();
+ }
+
+ static get propTypes() {
+ return {
+ doSearchForHighlight: PropTypes.func.isRequired,
+ query: PropTypes.string.isRequired,
+ results: PropTypes.array.isRequired,
+ searchSources: PropTypes.func.isRequired,
+ selectSpecificLocationOrSameUrl: PropTypes.func.isRequired,
+ status: PropTypes.oneOf([
+ "INITIAL",
+ "FETCHING",
+ "CANCELED",
+ "DONE",
+ "ERROR",
+ ]).isRequired,
+ modifiers: PropTypes.object,
+ toggleProjectSearchModifier: PropTypes.func,
+ };
+ }
+
+ async doSearch() {
+ // Cancel any previous async ongoing search
+ if (this.searchAbortController) {
+ this.searchAbortController.abort();
+ }
+
+ if (!this.state.query) {
+ this.setState({ status: statusType.done });
+ return;
+ }
+
+ this.setState({
+ status: statusType.fetching,
+ results: [],
+ navigateCounter: this.props.navigateCounter,
+ });
+
+ // Setup an AbortController whose main goal is to be able to cancel the asynchronous
+ // operation done by the `searchSources` action.
+ // This allows allows the React Component to receive partial updates
+ // to render results as they are available.
+ this.searchAbortController = new AbortController();
+
+ await this.props.searchSources(
+ this.state.query,
+ this.onUpdatedResults,
+ this.searchAbortController.signal
+ );
+ }
+
+ onUpdatedResults(results, done, signal) {
+ // debounce may delay the execution after this search has been cancelled
+ if (signal.aborted) {
+ return;
+ }
+
+ this.setState({
+ results,
+ status: done ? statusType.done : statusType.fetching,
+ });
+ }
+
+ selectMatchItem = async matchItem => {
+ const foundMatchingSource =
+ await this.props.selectSpecificLocationOrSameUrl(matchItem.location);
+ // When we reload, or if the source's target has been destroyed,
+ // we may no longer have the source available in the reducer.
+ // In such case `selectSpecificLocationOrSameUrl` will return false.
+ if (!foundMatchingSource) {
+ // When going over results via the key arrows and Enter, we may display many tooltips at once.
+ if (this.tooltip) {
+ this.tooltip.hide();
+ }
+ // Go down to line-number otherwise HTMLTooltip's call to getBoundingClientRect would return (0, 0) position for the tooltip
+ const element = document.querySelector(
+ ".project-text-search .tree-node.focused .result .line-number"
+ );
+ const tooltip = new HTMLTooltip(element.ownerDocument, {
+ className: "unavailable-source",
+ type: "arrow",
+ });
+ tooltip.panel.textContent = L10N.getStr(
+ "projectTextSearch.sourceNoLongerAvailable"
+ );
+ tooltip.setContentSize({ height: "auto" });
+ tooltip.show(element);
+ this.tooltip = tooltip;
+ return;
+ }
+ this.props.doSearchForHighlight(
+ this.state.query,
+ getEditor(),
+ matchItem.location.line,
+ matchItem.location.column
+ );
+ };
+
+ highlightMatches = lineMatch => {
+ const { value, matchIndex, match } = lineMatch;
+ const len = match.length;
+ return span(
+ {
+ className: "line-value",
+ },
+ span(
+ {
+ className: "line-match",
+ key: 0,
+ },
+ value.slice(0, matchIndex)
+ ),
+ span(
+ {
+ className: "query-match",
+ key: 1,
+ },
+ value.substr(matchIndex, len)
+ ),
+ span(
+ {
+ className: "line-match",
+ key: 2,
+ },
+ value.slice(matchIndex + len, value.length)
+ )
+ );
+ };
+
+ getResultCount = () =>
+ this.state.results.reduce((count, file) => count + file.matches.length, 0);
+
+ onKeyDown = e => {
+ if (e.key === "Escape") {
+ return;
+ }
+
+ e.stopPropagation();
+
+ this.setState({ focusedItem: null });
+ this.doSearch();
+ };
+
+ onHistoryScroll = query => {
+ this.setState({ query });
+ this.doSearch();
+ };
+
+ // This can be called by Tree when manually selecting node via arrow keys and Enter.
+ onActivate = item => {
+ if (item && item.type === "MATCH") {
+ this.selectMatchItem(item);
+ }
+ };
+
+ onFocus = item => {
+ if (this.state.focusedItem !== item) {
+ this.setState({
+ focusedItem: item,
+ });
+ }
+ };
+
+ inputOnChange = e => {
+ const inputValue = e.target.value;
+ this.setState({ query: inputValue });
+ this.doSearch();
+ };
+
+ renderFile = (file, focused, expanded) => {
+ const matchesLength = file.matches.length;
+ const matches = ` (${matchesLength} match${matchesLength > 1 ? "es" : ""})`;
+ return div(
+ {
+ className: classnames("file-result", {
+ focused,
+ }),
+ key: file.location.source.id,
+ },
+ React.createElement(AccessibleImage, {
+ className: classnames("arrow", {
+ expanded,
+ }),
+ }),
+ React.createElement(AccessibleImage, {
+ className: "file",
+ }),
+ span(
+ {
+ className: "file-path",
+ },
+ file.location.source.url
+ ? getRelativePath(file.location.source.url)
+ : getFormattedSourceId(file.location.source.id)
+ ),
+ span(
+ {
+ className: "matches-summary",
+ },
+ matches
+ )
+ );
+ };
+
+ renderMatch = (match, focused) => {
+ return div(
+ {
+ className: classnames("result", {
+ focused,
+ }),
+ onClick: () => this.selectMatchItem(match),
+ },
+ span(
+ {
+ className: "line-number",
+ key: match.location.line,
+ },
+ match.location.line
+ ),
+ this.highlightMatches(match)
+ );
+ };
+
+ renderItem = (item, depth, focused, _, expanded) => {
+ if (item.type === "RESULT") {
+ return this.renderFile(item, focused, expanded);
+ }
+ return this.renderMatch(item, focused);
+ };
+
+ renderRefreshButton() {
+ if (!this.state.query) {
+ return null;
+ }
+
+ // Highlight the refresh button when the current search results
+ // are based on the previous document. doSearch will save the "navigate counter"
+ // into state, while props will report the current "navigate counter".
+ // The "navigate counter" is incremented each time we navigate to a new page.
+ const highlight =
+ this.state.navigateCounter != null &&
+ this.state.navigateCounter != this.props.navigateCounter;
+ return button(
+ {
+ className: classnames("refresh-btn devtools-button", {
+ highlight,
+ }),
+ title: highlight
+ ? L10N.getStr("projectTextSearch.refreshButtonTooltipOnNavigation")
+ : L10N.getStr("projectTextSearch.refreshButtonTooltip"),
+ onClick: this.doSearch,
+ },
+ React.createElement(AccessibleImage, {
+ className: "refresh",
+ })
+ );
+ }
+
+ renderResultsToolbar() {
+ if (!this.state.query) {
+ return null;
+ }
+ return div(
+ { className: "project-search-results-toolbar" },
+ span({ className: "results-count" }, this.renderSummary()),
+ this.renderRefreshButton()
+ );
+ }
+
+ renderResults() {
+ const { status, results } = this.state;
+ if (!this.state.query) {
+ return null;
+ }
+ if (results.length) {
+ return React.createElement(Tree, {
+ getRoots: () => results,
+ getChildren: file => file.matches || [],
+ autoExpandAll: true,
+ autoExpandDepth: 1,
+ autoExpandNodeChildrenLimit: 100,
+ getParent: item => null,
+ getPath: getFilePath,
+ renderItem: this.renderItem,
+ focused: this.state.focusedItem,
+ onFocus: this.onFocus,
+ onActivate: this.onActivate,
+ isExpanded: item => {
+ return this.state.expanded.has(item);
+ },
+ onExpand: item => {
+ const { expanded } = this.state;
+ expanded.add(item);
+ this.setState({
+ expanded,
+ });
+ },
+ onCollapse: item => {
+ const { expanded } = this.state;
+ expanded.delete(item);
+ this.setState({
+ expanded,
+ });
+ },
+ getKey: getFilePath,
+ });
+ }
+ const msg =
+ status === statusType.fetching
+ ? L10N.getStr("loadingText")
+ : L10N.getStr("projectTextSearch.noResults");
+ return div(
+ {
+ className: "no-result-msg absolute-center",
+ },
+ msg
+ );
+ }
+
+ renderSummary = () => {
+ if (this.state.query === "") {
+ return "";
+ }
+ const resultsSummaryString = L10N.getStr("sourceSearch.resultsSummary2");
+ const count = this.getResultCount();
+ if (count === 0) {
+ return "";
+ }
+ return PluralForm.get(count, resultsSummaryString).replace("#1", count);
+ };
+
+ shouldShowErrorEmoji() {
+ return !this.getResultCount() && this.state.status === statusType.done;
+ }
+
+ renderInput() {
+ const { status } = this.state;
+ return React.createElement(SearchInput, {
+ query: this.state.query,
+ count: this.getResultCount(),
+ placeholder: L10N.getStr("projectTextSearch.placeholder"),
+ size: "small",
+ showErrorEmoji: this.shouldShowErrorEmoji(),
+ isLoading: status === statusType.fetching,
+ onChange: this.inputOnChange,
+ onFocus: () =>
+ this.setState({
+ inputFocused: true,
+ }),
+ onBlur: () =>
+ this.setState({
+ inputFocused: false,
+ }),
+ onKeyDown: this.onKeyDown,
+ onHistoryScroll: this.onHistoryScroll,
+ showClose: false,
+ showExcludePatterns: true,
+ excludePatternsLabel: L10N.getStr(
+ "projectTextSearch.excludePatterns.label"
+ ),
+ excludePatternsPlaceholder: L10N.getStr(
+ "projectTextSearch.excludePatterns.placeholder"
+ ),
+ ref: "searchInput",
+ showSearchModifiers: true,
+ searchKey: searchKeys.PROJECT_SEARCH,
+ onToggleSearchModifier: this.doSearch,
+ });
+ }
+
+ render() {
+ return div(
+ {
+ className: "search-container",
+ },
+ div(
+ {
+ className: "project-text-search",
+ },
+ div(
+ {
+ className: "header",
+ },
+ this.renderInput()
+ ),
+ this.renderResultsToolbar(),
+ this.renderResults()
+ )
+ );
+ }
+}
+
+ProjectSearch.contextTypes = {
+ shortcuts: PropTypes.object,
+};
+
+const mapStateToProps = state => ({
+ query: getProjectSearchQuery(state),
+ navigateCounter: getNavigateCounter(state),
+});
+
+export default connect(mapStateToProps, {
+ searchSources: actions.searchSources,
+ selectSpecificLocationOrSameUrl: actions.selectSpecificLocationOrSameUrl,
+ doSearchForHighlight: actions.doSearchForHighlight,
+})(ProjectSearch);
diff --git a/devtools/client/debugger/src/components/PrimaryPanes/Sources.css b/devtools/client/debugger/src/components/PrimaryPanes/Sources.css
new file mode 100644
index 0000000000..68c28655be
--- /dev/null
+++ b/devtools/client/debugger/src/components/PrimaryPanes/Sources.css
@@ -0,0 +1,244 @@
+/* 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/>. */
+
+.sources-panel {
+ background-color: var(--theme-sidebar-background);
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ overflow: hidden;
+ position: relative;
+
+ & * {
+ user-select: none;
+ }
+
+ /* Tabs header */
+ & .tabs-navigation {
+ height: var(--editor-header-height) !important;
+
+ & .tabs-menu {
+ /* override margin set by the Tabs component */
+ margin: 0 !important;
+ }
+
+ & .tab {
+ flex: 1;
+ overflow: hidden;
+ display: inline-flex;
+ align-items: center;
+ }
+
+ & [role="tab"] {
+ padding: 4px 8px;
+ flex: 1;
+ }
+ }
+}
+
+
+
+/***********************/
+/* Souces Panel layout */
+/***********************/
+
+.sources-list {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ overflow: hidden;
+}
+
+.sources-list .sources-clear-root-container {
+ grid-area: custom-root;
+}
+
+.sources-list :is(.tree, .no-sources-message) {
+ grid-area: sources-tree-or-empty-message;
+}
+
+/****************/
+/* Custom root */
+/****************/
+
+.sources-clear-root {
+ padding: 4px 8px;
+ width: 100%;
+ text-align: start;
+ white-space: nowrap;
+ color: inherit;
+ display: flex;
+ border-bottom: 1px solid var(--theme-splitter-color);
+}
+
+.sources-clear-root .home {
+ background-color: var(--theme-icon-dimmed-color);
+}
+
+.sources-clear-root .breadcrumb {
+ width: 5px;
+ margin: 0 2px 0 6px;
+ vertical-align: bottom;
+ background: var(--theme-text-color-alt);
+}
+
+.sources-clear-root-label {
+ margin-left: 5px;
+ line-height: 16px;
+}
+
+/*****************/
+/* Sources tree */
+/*****************/
+
+.sources-list .tree {
+ flex-grow: 1;
+ padding: 4px 0;
+ user-select: none;
+
+ white-space: nowrap;
+ overflow: auto;
+ min-width: 100%;
+
+ display: grid;
+ grid-template-columns: 1fr;
+ align-content: start;
+
+ line-height: 1.4em;
+}
+
+.sources-list .tree .node {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ padding-block: 8px;
+ padding-inline: 6px 8px;
+}
+
+.sources-list .tree .tree-node:not(.focused):hover {
+ background: var(--theme-toolbar-background-hover);
+}
+
+.sources-list .tree button {
+ display: block;
+}
+
+.sources-list .tree .node {
+ padding: 2px 3px;
+ position: relative;
+}
+
+.sources-list .tree .node.focused {
+ color: var(--theme-selection-color);
+ background-color: var(--theme-selection-background);
+}
+
+html:not([dir="rtl"]) .sources-list .tree .node > div {
+ margin-left: 10px;
+}
+
+html[dir="rtl"] .sources-list .tree .node > div {
+ margin-right: 10px;
+}
+
+.sources-list .tree-node button {
+ position: fixed;
+}
+
+.sources-list .img {
+ margin-inline-end: 4px;
+}
+
+.sources-list .tree .focused .img {
+ --icon-color: #ffffff;
+ background-color: var(--icon-color);
+ fill: var(--icon-color);
+}
+
+/* Use the same width as .img.arrow */
+.sources-list .tree .img.no-arrow {
+ width: 10px;
+ visibility: hidden;
+}
+
+.sources-list .tree .label .suffix {
+ font-style: italic;
+ font-size: 0.9em;
+ color: var(--theme-comment);
+}
+
+.sources-list .tree .focused .label .suffix {
+ color: inherit;
+}
+
+.theme-dark .source-list .node.focused {
+ background-color: var(--theme-tab-toolbar-background);
+}
+
+.sources-list .tree .blackboxed {
+ color: #806414;
+}
+
+.sources-list .img.blackBox {
+ mask-size: 13px;
+ background-color: #806414;
+}
+
+.sources-list .tree .label {
+ display: inline-block;
+ line-height: 16px;
+}
+
+.source-list-footer {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 5px;
+ justify-content: center;
+ text-align: center;
+ min-height: var(--editor-footer-height);
+ flex-shrink: 0;
+ border-block-start: 1px solid var(--theme-warning-border);
+ user-select: none;
+ padding: 3px 10px;
+ color: var(--theme-warning-color);
+ background-color: var(--theme-warning-background);
+}
+
+.source-list-footer .devtools-togglebutton {
+ background-color: var(--theme-toolbar-hover);
+}
+
+.source-list-footer .devtools-togglebutton:hover {
+ background-color: var(--theme-toolbar-hover);
+ cursor: pointer;
+}
+
+
+/* Removes start margin when a custom root is used */
+.sources-list-custom-root
+ .tree
+ > .tree-node[data-expandable="false"][aria-level="0"] {
+ padding-inline-start: 4px;
+}
+
+.sources-list .tree-node[data-expandable="false"] .tree-indent:last-of-type {
+ margin-inline-end: 0;
+}
+
+
+/*****************/
+/* No Sources */
+/*****************/
+
+.no-sources-message {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-style: italic;
+ text-align: center;
+ padding: 0.5em;
+ font-size: 12px;
+ user-select: none;
+}
diff --git a/devtools/client/debugger/src/components/PrimaryPanes/SourcesTree.js b/devtools/client/debugger/src/components/PrimaryPanes/SourcesTree.js
new file mode 100644
index 0000000000..286e673706
--- /dev/null
+++ b/devtools/client/debugger/src/components/PrimaryPanes/SourcesTree.js
@@ -0,0 +1,352 @@
+/* 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/>. */
+
+// Dependencies
+import React, {
+ Component,
+ Fragment,
+} from "devtools/client/shared/vendor/react";
+import {
+ div,
+ button,
+ span,
+ footer,
+} from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+
+// Selectors
+import {
+ getMainThreadHost,
+ getExpandedState,
+ getProjectDirectoryRoot,
+ getProjectDirectoryRootName,
+ getSourcesTreeSources,
+ getFocusedSourceItem,
+ getHideIgnoredSources,
+} from "../../selectors/index";
+
+// Actions
+import actions from "../../actions/index";
+
+// Components
+import SourcesTreeItem from "./SourcesTreeItem";
+import AccessibleImage from "../shared/AccessibleImage";
+
+const classnames = require("resource://devtools/client/shared/classnames.js");
+const Tree = require("resource://devtools/client/shared/components/Tree.js");
+
+function shouldAutoExpand(item, mainThreadHost) {
+ // There is only one case where we want to force auto expand,
+ // when we are on the group of the page's domain.
+ return item.type == "group" && item.groupName === mainThreadHost;
+}
+
+class SourcesTree extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {};
+ }
+
+ static get propTypes() {
+ return {
+ mainThreadHost: PropTypes.string.isRequired,
+ expanded: PropTypes.object.isRequired,
+ focusItem: PropTypes.func.isRequired,
+ focused: PropTypes.object,
+ projectRoot: PropTypes.string.isRequired,
+ selectSource: PropTypes.func.isRequired,
+ setExpandedState: PropTypes.func.isRequired,
+ rootItems: PropTypes.object.isRequired,
+ clearProjectDirectoryRoot: PropTypes.func.isRequired,
+ projectRootName: PropTypes.string.isRequired,
+ setHideOrShowIgnoredSources: PropTypes.func.isRequired,
+ hideIgnoredSources: PropTypes.bool.isRequired,
+ };
+ }
+
+ selectSourceItem = item => {
+ this.props.selectSource(item.source, item.sourceActor);
+ };
+
+ onFocus = item => {
+ this.props.focusItem(item);
+ };
+
+ onActivate = item => {
+ if (item.type == "source") {
+ this.selectSourceItem(item);
+ }
+ };
+
+ onExpand = (item, shouldIncludeChildren) => {
+ this.setExpanded(item, true, shouldIncludeChildren);
+ };
+
+ onCollapse = (item, shouldIncludeChildren) => {
+ this.setExpanded(item, false, shouldIncludeChildren);
+ };
+
+ setExpanded = (item, isExpanded, shouldIncludeChildren) => {
+ // Note that setExpandedState relies on us to clone this Set
+ // which is going to be store as-is in the reducer.
+ const expanded = new Set(this.props.expanded);
+
+ let changed = false;
+ const expandItem = i => {
+ const key = this.getKey(i);
+ if (isExpanded) {
+ changed |= !expanded.has(key);
+ expanded.add(key);
+ } else {
+ changed |= expanded.has(key);
+ expanded.delete(key);
+ }
+ };
+ expandItem(item);
+
+ if (shouldIncludeChildren) {
+ let parents = [item];
+ while (parents.length) {
+ const children = [];
+ for (const parent of parents) {
+ for (const child of this.getChildren(parent)) {
+ expandItem(child);
+ children.push(child);
+ }
+ }
+ parents = children;
+ }
+ }
+ if (changed) {
+ this.props.setExpandedState(expanded);
+ }
+ };
+
+ isEmpty() {
+ return !this.getRoots().length;
+ }
+
+ renderEmptyElement(message) {
+ return div(
+ {
+ key: "empty",
+ className: "no-sources-message",
+ },
+ message
+ );
+ }
+
+ getRoots = () => {
+ return this.props.rootItems;
+ };
+
+ getKey = item => {
+ // As this is used as React key in Tree component,
+ // we need to update the key when switching to a new project root
+ // otherwise these items won't be updated and will have a buggy padding start.
+ const { projectRoot } = this.props;
+ if (projectRoot) {
+ return projectRoot + item.uniquePath;
+ }
+ return item.uniquePath;
+ };
+
+ getChildren = item => {
+ // This is the precial magic that coalesce "empty" folders,
+ // i.e folders which have only one sub-folder as children.
+ function skipEmptyDirectories(directory) {
+ if (directory.type != "directory") {
+ return directory;
+ }
+ if (
+ directory.children.length == 1 &&
+ directory.children[0].type == "directory"
+ ) {
+ return skipEmptyDirectories(directory.children[0]);
+ }
+ return directory;
+ }
+ if (item.type == "thread") {
+ return item.children;
+ } else if (item.type == "group" || item.type == "directory") {
+ return item.children.map(skipEmptyDirectories);
+ }
+ return [];
+ };
+
+ getParent = item => {
+ if (item.type == "thread") {
+ return null;
+ }
+ const { rootItems } = this.props;
+ // This is the second magic which skip empty folders
+ // (See getChildren comment)
+ function skipEmptyDirectories(directory) {
+ if (
+ directory.type == "group" ||
+ directory.type == "thread" ||
+ rootItems.includes(directory)
+ ) {
+ return directory;
+ }
+ if (
+ directory.children.length == 1 &&
+ directory.children[0].type == "directory"
+ ) {
+ return skipEmptyDirectories(directory.parent);
+ }
+ return directory;
+ }
+ return skipEmptyDirectories(item.parent);
+ };
+
+ renderProjectRootHeader() {
+ const { projectRootName } = this.props;
+
+ if (!projectRootName) {
+ return null;
+ }
+ return div(
+ {
+ key: "root",
+ className: "sources-clear-root-container",
+ },
+ button(
+ {
+ className: "sources-clear-root",
+ onClick: () => this.props.clearProjectDirectoryRoot(),
+ title: L10N.getStr("removeDirectoryRoot.label"),
+ },
+ React.createElement(AccessibleImage, {
+ className: "home",
+ }),
+ React.createElement(AccessibleImage, {
+ className: "breadcrumb",
+ }),
+ span(
+ {
+ className: "sources-clear-root-label",
+ },
+ projectRootName
+ )
+ )
+ );
+ }
+
+ renderItem = (item, depth, focused, _, expanded) => {
+ const { mainThreadHost } = this.props;
+ return React.createElement(SourcesTreeItem, {
+ item,
+ depth,
+ focused,
+ autoExpand: shouldAutoExpand(item, mainThreadHost),
+ expanded,
+ focusItem: this.onFocus,
+ selectSourceItem: this.selectSourceItem,
+ setExpanded: this.setExpanded,
+ getParent: this.getParent,
+ });
+ };
+
+ renderTree() {
+ const { expanded, focused } = this.props;
+
+ const treeProps = {
+ autoExpandAll: false,
+ autoExpandDepth: 1,
+ expanded,
+ focused,
+ getChildren: this.getChildren,
+ getParent: this.getParent,
+ getKey: this.getKey,
+ getRoots: this.getRoots,
+ onCollapse: this.onCollapse,
+ onExpand: this.onExpand,
+ onFocus: this.onFocus,
+ isExpanded: item => {
+ return this.props.expanded.has(this.getKey(item));
+ },
+ onActivate: this.onActivate,
+ renderItem: this.renderItem,
+ preventBlur: true,
+ };
+ return React.createElement(Tree, treeProps);
+ }
+
+ renderPane(child) {
+ const { projectRoot } = this.props;
+ return div(
+ {
+ key: "pane",
+ className: classnames("sources-pane", {
+ "sources-list-custom-root": !!projectRoot,
+ }),
+ },
+ child
+ );
+ }
+
+ renderFooter() {
+ if (this.props.hideIgnoredSources) {
+ return footer(
+ {
+ className: "source-list-footer",
+ },
+ L10N.getStr("ignoredSourcesHidden"),
+ button(
+ {
+ className: "devtools-togglebutton",
+ onClick: () => this.props.setHideOrShowIgnoredSources(false),
+ title: L10N.getStr("showIgnoredSources.tooltip.label"),
+ },
+ L10N.getStr("showIgnoredSources")
+ )
+ );
+ }
+ return null;
+ }
+
+ render() {
+ const { projectRoot } = this.props;
+ return div(
+ {
+ key: "pane",
+ className: classnames("sources-list", {
+ "sources-list-custom-root": !!projectRoot,
+ }),
+ },
+ this.isEmpty()
+ ? this.renderEmptyElement(L10N.getStr("noSourcesText"))
+ : React.createElement(
+ Fragment,
+ null,
+ this.renderProjectRootHeader(),
+ this.renderTree(),
+ this.renderFooter()
+ )
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ return {
+ mainThreadHost: getMainThreadHost(state),
+ expanded: getExpandedState(state),
+ focused: getFocusedSourceItem(state),
+ projectRoot: getProjectDirectoryRoot(state),
+ rootItems: getSourcesTreeSources(state),
+ projectRootName: getProjectDirectoryRootName(state),
+ hideIgnoredSources: getHideIgnoredSources(state),
+ };
+};
+
+export default connect(mapStateToProps, {
+ selectSource: actions.selectSource,
+ setExpandedState: actions.setExpandedState,
+ focusItem: actions.focusItem,
+ clearProjectDirectoryRoot: actions.clearProjectDirectoryRoot,
+ setHideOrShowIgnoredSources: actions.setHideOrShowIgnoredSources,
+})(SourcesTree);
diff --git a/devtools/client/debugger/src/components/PrimaryPanes/SourcesTreeItem.js b/devtools/client/debugger/src/components/PrimaryPanes/SourcesTreeItem.js
new file mode 100644
index 0000000000..fd5ceca46d
--- /dev/null
+++ b/devtools/client/debugger/src/components/PrimaryPanes/SourcesTreeItem.js
@@ -0,0 +1,249 @@
+/* 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 React, { Component } from "devtools/client/shared/vendor/react";
+import { div, span } from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+
+import SourceIcon from "../shared/SourceIcon";
+import AccessibleImage from "../shared/AccessibleImage";
+
+import {
+ getGeneratedSourceByURL,
+ isSourceOverridden,
+ getHideIgnoredSources,
+} from "../../selectors/index";
+import actions from "../../actions/index";
+
+import { sourceTypes } from "../../utils/source";
+import { createLocation } from "../../utils/location";
+import { safeDecodeItemName } from "../../utils/sources-tree/utils";
+
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+class SourceTreeItem extends Component {
+ static get propTypes() {
+ return {
+ autoExpand: PropTypes.bool.isRequired,
+ depth: PropTypes.bool.isRequired,
+ expanded: PropTypes.bool.isRequired,
+ focusItem: PropTypes.func.isRequired,
+ focused: PropTypes.bool.isRequired,
+ hasMatchingGeneratedSource: PropTypes.bool.isRequired,
+ item: PropTypes.object.isRequired,
+ selectSourceItem: PropTypes.func.isRequired,
+ setExpanded: PropTypes.func.isRequired,
+ getParent: PropTypes.func.isRequired,
+ isOverridden: PropTypes.bool,
+ hideIgnoredSources: PropTypes.bool,
+ };
+ }
+
+ componentDidMount() {
+ const { autoExpand, item } = this.props;
+ if (autoExpand) {
+ this.props.setExpanded(item, true, false);
+ }
+ }
+
+ onClick = e => {
+ const { item, focusItem, selectSourceItem } = this.props;
+
+ focusItem(item);
+ if (item.type == "source") {
+ selectSourceItem(item);
+ }
+ };
+
+ onContextMenu = event => {
+ event.stopPropagation();
+ event.preventDefault();
+ this.props.showSourceTreeItemContextMenu(
+ event,
+ this.props.item,
+ this.props.depth,
+ this.props.setExpanded,
+ this.renderItemName()
+ );
+ };
+
+ renderItemArrow() {
+ const { item, expanded } = this.props;
+ return item.type != "source"
+ ? React.createElement(AccessibleImage, {
+ className: classnames("arrow", {
+ expanded,
+ }),
+ })
+ : span({
+ className: "img no-arrow",
+ });
+ }
+
+ renderIcon(item) {
+ if (item.type == "thread") {
+ const icon = item.thread.targetType.includes("worker")
+ ? "worker"
+ : "window";
+ return React.createElement(AccessibleImage, {
+ className: classnames(icon),
+ });
+ }
+ if (item.type == "group") {
+ if (item.groupName === "Webpack") {
+ return React.createElement(AccessibleImage, {
+ className: "webpack",
+ });
+ } else if (item.groupName === "Angular") {
+ return React.createElement(AccessibleImage, {
+ className: "angular",
+ });
+ }
+ // Check if the group relates to an extension.
+ // This happens when a webextension injects a content script.
+ if (item.isForExtensionSource) {
+ return React.createElement(AccessibleImage, {
+ className: "extension",
+ });
+ }
+ return React.createElement(AccessibleImage, {
+ className: "globe-small",
+ });
+ }
+ if (item.type == "directory") {
+ return React.createElement(AccessibleImage, {
+ className: "folder",
+ });
+ }
+ if (item.type == "source") {
+ const { source, sourceActor } = item;
+ return React.createElement(SourceIcon, {
+ location: createLocation({
+ source,
+ sourceActor,
+ }),
+ modifier: icon => {
+ // In the SourceTree, extension files should use the file-extension based icon,
+ // whereas we use the extension icon in other Components (eg. source tabs and breakpoints pane).
+ if (icon === "extension") {
+ return sourceTypes[source.displayURL.fileExtension] || "javascript";
+ }
+ return icon + (this.props.isOverridden ? " override" : "");
+ },
+ });
+ }
+ return null;
+ }
+ renderItemName() {
+ const { item } = this.props;
+
+ if (item.type == "thread") {
+ const { thread } = item;
+ return (
+ thread.name +
+ (thread.serviceWorkerStatus ? ` (${thread.serviceWorkerStatus})` : "")
+ );
+ }
+ if (item.type == "group") {
+ return safeDecodeItemName(item.groupName);
+ }
+ if (item.type == "directory") {
+ const parentItem = this.props.getParent(item);
+ return safeDecodeItemName(
+ item.path.replace(parentItem.path, "").replace(/^\//, "")
+ );
+ }
+ if (item.type == "source") {
+ const { displayURL } = item.source;
+ const name =
+ displayURL.filename + (displayURL.search ? displayURL.search : "");
+ return safeDecodeItemName(name);
+ }
+
+ return null;
+ }
+
+ renderItemTooltip() {
+ const { item } = this.props;
+
+ if (item.type == "thread") {
+ return item.thread.name;
+ }
+ if (item.type == "group") {
+ return item.groupName;
+ }
+ if (item.type == "directory") {
+ return item.path;
+ }
+ if (item.type == "source") {
+ return item.source.url;
+ }
+
+ return null;
+ }
+
+ render() {
+ const { item, focused, hasMatchingGeneratedSource, hideIgnoredSources } =
+ this.props;
+
+ if (hideIgnoredSources && item.isBlackBoxed) {
+ return null;
+ }
+ const suffix = hasMatchingGeneratedSource
+ ? span(
+ {
+ className: "suffix",
+ },
+ L10N.getStr("sourceFooter.mappedSuffix")
+ )
+ : null;
+ return div(
+ {
+ className: classnames("node", {
+ focused,
+ blackboxed: item.type == "source" && item.isBlackBoxed,
+ }),
+ key: item.path,
+ onClick: this.onClick,
+ onContextMenu: this.onContextMenu,
+ title: this.renderItemTooltip(),
+ },
+ this.renderItemArrow(),
+ this.renderIcon(item),
+ span(
+ {
+ className: "label",
+ },
+ this.renderItemName(),
+ suffix
+ )
+ );
+ }
+}
+
+function getHasMatchingGeneratedSource(state, source) {
+ if (!source || !source.isOriginal) {
+ return false;
+ }
+
+ return !!getGeneratedSourceByURL(state, source.url);
+}
+
+const mapStateToProps = (state, props) => {
+ const { item } = props;
+ if (item.type == "source") {
+ const { source } = item;
+ return {
+ hasMatchingGeneratedSource: getHasMatchingGeneratedSource(state, source),
+ isOverridden: isSourceOverridden(state, source),
+ hideIgnoredSources: getHideIgnoredSources(state),
+ };
+ }
+ return {};
+};
+
+export default connect(mapStateToProps, {
+ showSourceTreeItemContextMenu: actions.showSourceTreeItemContextMenu,
+})(SourceTreeItem);
diff --git a/devtools/client/debugger/src/components/PrimaryPanes/index.js b/devtools/client/debugger/src/components/PrimaryPanes/index.js
new file mode 100644
index 0000000000..a8f6bc9a33
--- /dev/null
+++ b/devtools/client/debugger/src/components/PrimaryPanes/index.js
@@ -0,0 +1,133 @@
+/* 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 React, { Component } from "devtools/client/shared/vendor/react";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+import actions from "../../actions/index";
+import { getSelectedPrimaryPaneTab } from "../../selectors/index";
+import { prefs } from "../../utils/prefs";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import { primaryPaneTabs } from "../../constants";
+
+import Outline from "./Outline";
+import SourcesTree from "./SourcesTree";
+import ProjectSearch from "./ProjectSearch";
+
+const {
+ TabPanel,
+ Tabs,
+} = require("resource://devtools/client/shared/components/tabs/Tabs.js");
+
+const tabs = [
+ primaryPaneTabs.SOURCES,
+ primaryPaneTabs.OUTLINE,
+ primaryPaneTabs.PROJECT_SEARCH,
+];
+
+class PrimaryPanes extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ alphabetizeOutline: prefs.alphabetizeOutline,
+ };
+ }
+
+ static get propTypes() {
+ return {
+ projectRootName: PropTypes.string.isRequired,
+ selectedTab: PropTypes.oneOf(tabs).isRequired,
+ setPrimaryPaneTab: PropTypes.func.isRequired,
+ setActiveSearch: PropTypes.func.isRequired,
+ closeActiveSearch: PropTypes.func.isRequired,
+ };
+ }
+
+ onAlphabetizeClick = () => {
+ const alphabetizeOutline = !prefs.alphabetizeOutline;
+ prefs.alphabetizeOutline = alphabetizeOutline;
+ this.setState({ alphabetizeOutline });
+ };
+
+ onActivateTab = index => {
+ const tab = tabs.at(index);
+ this.props.setPrimaryPaneTab(tab);
+ if (tab == primaryPaneTabs.PROJECT_SEARCH) {
+ this.props.setActiveSearch(tab);
+ } else {
+ this.props.closeActiveSearch();
+ }
+ };
+
+ render() {
+ const { selectedTab } = this.props;
+ return React.createElement(
+ "aside",
+ {
+ className: "tab-panel sources-panel",
+ },
+ React.createElement(
+ Tabs,
+ {
+ activeTab: tabs.indexOf(selectedTab),
+ onAfterChange: this.onActivateTab,
+ },
+ React.createElement(
+ TabPanel,
+ {
+ id: "sources-tab",
+ key: `sources-tab${
+ selectedTab === primaryPaneTabs.SOURCES ? "-selected" : ""
+ }`,
+ className: "tab sources-tab",
+ title: L10N.getStr("sources.header"),
+ },
+ React.createElement(SourcesTree, null)
+ ),
+ React.createElement(
+ TabPanel,
+ {
+ id: "outline-tab",
+ key: `outline-tab${
+ selectedTab === primaryPaneTabs.OUTLINE ? "-selected" : ""
+ }`,
+ className: "tab outline-tab",
+ title: L10N.getStr("outline.header"),
+ },
+ React.createElement(Outline, {
+ alphabetizeOutline: this.state.alphabetizeOutline,
+ onAlphabetizeClick: this.onAlphabetizeClick,
+ })
+ ),
+ React.createElement(
+ TabPanel,
+ {
+ id: "search-tab",
+ key: `search-tab${
+ selectedTab === primaryPaneTabs.PROJECT_SEARCH ? "-selected" : ""
+ }`,
+ className: "tab search-tab",
+ title: L10N.getStr("search.header"),
+ },
+ React.createElement(ProjectSearch, null)
+ )
+ )
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ return {
+ selectedTab: getSelectedPrimaryPaneTab(state),
+ };
+};
+
+const connector = connect(mapStateToProps, {
+ setPrimaryPaneTab: actions.setPrimaryPaneTab,
+ setActiveSearch: actions.setActiveSearch,
+ closeActiveSearch: actions.closeActiveSearch,
+});
+
+export default connector(PrimaryPanes);
diff --git a/devtools/client/debugger/src/components/PrimaryPanes/moz.build b/devtools/client/debugger/src/components/PrimaryPanes/moz.build
new file mode 100644
index 0000000000..fc73b7bee7
--- /dev/null
+++ b/devtools/client/debugger/src/components/PrimaryPanes/moz.build
@@ -0,0 +1,15 @@
+# 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(
+ "index.js",
+ "Outline.js",
+ "OutlineFilter.js",
+ "ProjectSearch.js",
+ "SourcesTree.js",
+ "SourcesTreeItem.js",
+)
diff --git a/devtools/client/debugger/src/components/QuickOpenModal.css b/devtools/client/debugger/src/components/QuickOpenModal.css
new file mode 100644
index 0000000000..5a2627b99f
--- /dev/null
+++ b/devtools/client/debugger/src/components/QuickOpenModal.css
@@ -0,0 +1,28 @@
+/* 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/>. */
+
+.result-item .title .highlight {
+ font-weight: bold;
+ background-color: transparent;
+}
+
+.selected .highlight {
+ color: white;
+}
+
+.result-item .subtitle .highlight {
+ color: var(--grey-90);
+ font-weight: 500;
+ background-color: transparent;
+}
+
+.theme-dark .result-item .title .highlight,
+.theme-dark .result-item .subtitle .highlight {
+ color: white;
+}
+
+.loading-indicator {
+ padding: 5px 0 5px 0;
+ text-align: center;
+}
diff --git a/devtools/client/debugger/src/components/QuickOpenModal.js b/devtools/client/debugger/src/components/QuickOpenModal.js
new file mode 100644
index 0000000000..aa3d4f73b6
--- /dev/null
+++ b/devtools/client/debugger/src/components/QuickOpenModal.js
@@ -0,0 +1,508 @@
+/* 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 React, { Component } from "devtools/client/shared/vendor/react";
+import { div } from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import { basename } from "../utils/path";
+import { createLocation } from "../utils/location";
+
+const fuzzyAldrin = require("resource://devtools/client/shared/vendor/fuzzaldrin-plus.js");
+const { throttle } = require("resource://devtools/shared/throttle.js");
+
+import actions from "../actions/index";
+import {
+ getDisplayedSourcesList,
+ getQuickOpenQuery,
+ getQuickOpenType,
+ getSelectedLocation,
+ getSettledSourceTextContent,
+ getSourceTabs,
+ getBlackBoxRanges,
+ getProjectDirectoryRoot,
+} from "../selectors/index";
+import { memoizeLast } from "../utils/memoizeLast";
+import { searchKeys } from "../constants";
+import {
+ formatSymbol,
+ parseLineColumn,
+ formatShortcutResults,
+ formatSourceForList,
+} from "../utils/quick-open";
+import Modal from "./shared/Modal";
+import SearchInput from "./shared/SearchInput";
+import ResultList from "./shared/ResultList";
+
+const maxResults = 100;
+
+const SIZE_BIG = { size: "big" };
+const SIZE_DEFAULT = {};
+
+function filter(values, query, key = "value") {
+ const preparedQuery = fuzzyAldrin.prepareQuery(query);
+
+ return fuzzyAldrin.filter(values, query, {
+ key,
+ maxResults,
+ preparedQuery,
+ });
+}
+
+export class QuickOpenModal extends Component {
+ // Put it on the class so it can be retrieved in tests
+ static UPDATE_RESULTS_THROTTLE = 100;
+
+ constructor(props) {
+ super(props);
+ this.state = { results: null, selectedIndex: 0 };
+ }
+
+ static get propTypes() {
+ return {
+ closeQuickOpen: PropTypes.func.isRequired,
+ displayedSources: PropTypes.array.isRequired,
+ blackBoxRanges: PropTypes.object.isRequired,
+ highlightLineRange: PropTypes.func.isRequired,
+ clearHighlightLineRange: PropTypes.func.isRequired,
+ query: PropTypes.string.isRequired,
+ searchType: PropTypes.oneOf([
+ "functions",
+ "goto",
+ "gotoSource",
+ "other",
+ "shortcuts",
+ "sources",
+ "variables",
+ ]).isRequired,
+ selectSpecificLocation: PropTypes.func.isRequired,
+ selectedContentLoaded: PropTypes.bool,
+ selectedLocation: PropTypes.object,
+ setQuickOpenQuery: PropTypes.func.isRequired,
+ openedTabUrls: PropTypes.array.isRequired,
+ toggleShortcutsModal: PropTypes.func.isRequired,
+ projectDirectoryRoot: PropTypes.string,
+ getFunctionSymbols: PropTypes.func.isRequired,
+ };
+ }
+
+ setResults(results) {
+ if (results) {
+ results = results.slice(0, maxResults);
+ }
+ this.setState({ results });
+ }
+
+ componentDidMount() {
+ const { query, shortcutsModalEnabled, toggleShortcutsModal } = this.props;
+
+ this.updateResults(query);
+
+ if (shortcutsModalEnabled) {
+ toggleShortcutsModal();
+ }
+ }
+
+ componentDidUpdate(prevProps) {
+ const queryChanged = prevProps.query !== this.props.query;
+
+ if (queryChanged) {
+ this.updateResults(this.props.query);
+ }
+ }
+
+ closeModal = () => {
+ this.props.closeQuickOpen();
+ };
+
+ dropGoto = query => {
+ const index = query.indexOf(":");
+ return index !== -1 ? query.slice(0, index) : query;
+ };
+
+ formatSources = memoizeLast(
+ (displayedSources, openedTabUrls, blackBoxRanges, projectDirectoryRoot) => {
+ // Note that we should format all displayed sources,
+ // the actual filtering will only be done late from `searchSources()`
+ return displayedSources.map(source => {
+ const isBlackBoxed = !!blackBoxRanges[source.url];
+ const hasTabOpened = openedTabUrls.includes(source.url);
+ return formatSourceForList(
+ source,
+ hasTabOpened,
+ isBlackBoxed,
+ projectDirectoryRoot
+ );
+ });
+ }
+ );
+
+ searchSources = query => {
+ const {
+ displayedSources,
+ openedTabUrls,
+ blackBoxRanges,
+ projectDirectoryRoot,
+ } = this.props;
+
+ const sources = this.formatSources(
+ displayedSources,
+ openedTabUrls,
+ blackBoxRanges,
+ projectDirectoryRoot
+ );
+ const results =
+ query == "" ? sources : filter(sources, this.dropGoto(query));
+ return this.setResults(results);
+ };
+
+ searchSymbols = async query => {
+ const { getFunctionSymbols, selectedLocation } = this.props;
+ if (!selectedLocation) {
+ return this.setResults([]);
+ }
+ let results = await getFunctionSymbols(selectedLocation, maxResults);
+
+ if (query === "@" || query === "#") {
+ results = results.map(formatSymbol);
+ return this.setResults(results);
+ }
+ results = filter(results, query.slice(1), "name");
+ results = results.map(formatSymbol);
+ return this.setResults(results);
+ };
+
+ searchShortcuts = query => {
+ const results = formatShortcutResults();
+ if (query == "?") {
+ this.setResults(results);
+ } else {
+ this.setResults(filter(results, query.slice(1)));
+ }
+ };
+
+ /**
+ * This method is called when we just opened the modal and the query input is empty
+ */
+ showTopSources = () => {
+ const { openedTabUrls, blackBoxRanges, projectDirectoryRoot } = this.props;
+ let { displayedSources } = this.props;
+
+ // If there is some tabs opened, only show tab's sources.
+ // Otherwise, we display all visible sources (per SourceTree definition),
+ // setResults will restrict the number of results to a maximum limit.
+ if (openedTabUrls.length) {
+ displayedSources = displayedSources.filter(
+ source => !!source.url && openedTabUrls.includes(source.url)
+ );
+ }
+
+ this.setResults(
+ this.formatSources(
+ displayedSources,
+ openedTabUrls,
+ blackBoxRanges,
+ projectDirectoryRoot
+ )
+ );
+ };
+
+ updateResults = throttle(query => {
+ if (this.isGotoQuery()) {
+ return;
+ }
+
+ if (query == "" && !this.isShortcutQuery()) {
+ this.showTopSources();
+ return;
+ }
+
+ if (this.isSymbolSearch()) {
+ this.searchSymbols(query);
+ return;
+ }
+
+ if (this.isShortcutQuery()) {
+ this.searchShortcuts(query);
+ return;
+ }
+
+ this.searchSources(query);
+ }, QuickOpenModal.UPDATE_RESULTS_THROTTLE);
+
+ setModifier = item => {
+ if (["@", "#", ":"].includes(item.id)) {
+ this.props.setQuickOpenQuery(item.id);
+ }
+ };
+
+ selectResultItem = (e, item) => {
+ if (item == null) {
+ return;
+ }
+
+ if (this.isShortcutQuery()) {
+ this.setModifier(item);
+ return;
+ }
+
+ if (this.isGotoSourceQuery()) {
+ const location = parseLineColumn(this.props.query);
+ this.gotoLocation({ ...location, source: item.source });
+ return;
+ }
+
+ if (this.isSymbolSearch()) {
+ this.gotoLocation({
+ line:
+ item.location && item.location.start ? item.location.start.line : 0,
+ });
+ return;
+ }
+
+ this.gotoLocation({ source: item.source, line: 0 });
+ };
+
+ onSelectResultItem = item => {
+ const { selectedLocation, highlightLineRange, clearHighlightLineRange } =
+ this.props;
+ if (
+ selectedLocation == null ||
+ !this.isSymbolSearch() ||
+ !this.isFunctionQuery()
+ ) {
+ return;
+ }
+
+ if (item.location) {
+ highlightLineRange({
+ start: item.location.start.line,
+ end: item.location.end.line,
+ sourceId: selectedLocation.source.id,
+ });
+ } else {
+ clearHighlightLineRange();
+ }
+ };
+
+ traverseResults = e => {
+ const direction = e.key === "ArrowUp" ? -1 : 1;
+ const { selectedIndex, results } = this.state;
+ const resultCount = this.getResultCount();
+ const index = selectedIndex + direction;
+ const nextIndex = (index + resultCount) % resultCount || 0;
+
+ this.setState({ selectedIndex: nextIndex });
+
+ if (results != null) {
+ this.onSelectResultItem(results[nextIndex]);
+ }
+ };
+
+ gotoLocation = location => {
+ const { selectSpecificLocation, selectedLocation } = this.props;
+
+ if (location != null) {
+ selectSpecificLocation(
+ createLocation({
+ source: location.source || selectedLocation?.source,
+ line: location.line,
+ column: location.column,
+ })
+ );
+ this.closeModal();
+ }
+ };
+
+ onChange = e => {
+ const { selectedLocation, selectedContentLoaded, setQuickOpenQuery } =
+ this.props;
+ setQuickOpenQuery(e.target.value);
+ const noSource = !selectedLocation || !selectedContentLoaded;
+ if ((noSource && this.isSymbolSearch()) || this.isGotoQuery()) {
+ return;
+ }
+
+ // Wait for the next tick so that reducer updates are complete.
+ const targetValue = e.target.value;
+ setTimeout(() => this.updateResults(targetValue), 0);
+ };
+
+ onKeyDown = e => {
+ const { query } = this.props;
+ const { results, selectedIndex } = this.state;
+ const isGoToQuery = this.isGotoQuery();
+
+ if (!results && !isGoToQuery) {
+ return;
+ }
+
+ if (e.key === "Enter") {
+ if (isGoToQuery) {
+ const location = parseLineColumn(query);
+ this.gotoLocation(location);
+ return;
+ }
+
+ if (results) {
+ this.selectResultItem(e, results[selectedIndex]);
+ return;
+ }
+ }
+
+ if (e.key === "Tab") {
+ this.closeModal();
+ return;
+ }
+
+ if (["ArrowUp", "ArrowDown"].includes(e.key)) {
+ e.preventDefault();
+ this.traverseResults(e);
+ }
+ };
+
+ getResultCount = () => {
+ const { results } = this.state;
+ return results && results.length ? results.length : 0;
+ };
+
+ // Query helpers
+ isFunctionQuery = () => this.props.searchType === "functions";
+ isSymbolSearch = () => this.isFunctionQuery();
+ isGotoQuery = () => this.props.searchType === "goto";
+ isGotoSourceQuery = () => this.props.searchType === "gotoSource";
+ isShortcutQuery = () => this.props.searchType === "shortcuts";
+ isSourcesQuery = () => this.props.searchType === "sources";
+ isSourceSearch = () => this.isSourcesQuery() || this.isGotoSourceQuery();
+
+ /* eslint-disable react/no-danger */
+ renderHighlight(candidateString, query, name) {
+ const options = {
+ wrap: {
+ tagOpen: '<mark class="highlight">',
+ tagClose: "</mark>",
+ },
+ };
+ const html = fuzzyAldrin.wrap(candidateString, query, options);
+ return div({
+ dangerouslySetInnerHTML: {
+ __html: html,
+ },
+ });
+ }
+
+ highlightMatching = (query, results) => {
+ let newQuery = query;
+ if (newQuery === "") {
+ return results;
+ }
+ newQuery = query.replace(/[@:#?]/gi, " ");
+
+ return results.map(result => {
+ if (typeof result.title == "string") {
+ return {
+ ...result,
+ title: this.renderHighlight(
+ result.title,
+ basename(newQuery),
+ "title"
+ ),
+ };
+ }
+ return result;
+ });
+ };
+
+ shouldShowErrorEmoji() {
+ const { query } = this.props;
+ if (this.isGotoQuery()) {
+ return !/^:\d*$/.test(query);
+ }
+ return !!query && !this.getResultCount();
+ }
+
+ getSummaryMessage() {
+ let summaryMsg = "";
+ if (this.isGotoQuery()) {
+ summaryMsg = L10N.getStr("shortcuts.gotoLine");
+ } else if (this.isFunctionQuery() && !this.state.results) {
+ summaryMsg = L10N.getStr("loadingText");
+ }
+ return summaryMsg;
+ }
+
+ render() {
+ const { query } = this.props;
+ const { selectedIndex, results } = this.state;
+
+ const items = this.highlightMatching(query, results || []);
+ const expanded = !!items && !!items.length;
+ return React.createElement(
+ Modal,
+ {
+ handleClose: this.closeModal,
+ },
+ React.createElement(SearchInput, {
+ query: query,
+ hasPrefix: true,
+ count: this.getResultCount(),
+ placeholder: L10N.getStr("sourceSearch.search2"),
+ summaryMsg: this.getSummaryMessage(),
+ showErrorEmoji: this.shouldShowErrorEmoji(),
+ isLoading: false,
+ onChange: this.onChange,
+ onKeyDown: this.onKeyDown,
+ handleClose: this.closeModal,
+ expanded: expanded,
+ showClose: false,
+ searchKey: searchKeys.QUICKOPEN_SEARCH,
+ showExcludePatterns: false,
+ showSearchModifiers: false,
+ selectedItemId:
+ expanded && items[selectedIndex] ? items[selectedIndex].id : "",
+ ...(this.isSourceSearch() ? SIZE_BIG : SIZE_DEFAULT),
+ }),
+ results &&
+ React.createElement(ResultList, {
+ key: "results",
+ items: items,
+ selected: selectedIndex,
+ selectItem: this.selectResultItem,
+ ref: "resultList",
+ expanded: expanded,
+ ...(this.isSourceSearch() ? SIZE_BIG : SIZE_DEFAULT),
+ })
+ );
+ }
+}
+
+/* istanbul ignore next: ignoring testing of redux connection stuff */
+function mapStateToProps(state) {
+ const selectedLocation = getSelectedLocation(state);
+ const displayedSources = getDisplayedSourcesList(state);
+ const tabs = getSourceTabs(state);
+ const openedTabUrls = [...new Set(tabs.map(tab => tab.url))];
+
+ return {
+ displayedSources,
+ blackBoxRanges: getBlackBoxRanges(state),
+ projectDirectoryRoot: getProjectDirectoryRoot(state),
+ selectedLocation,
+ selectedContentLoaded: selectedLocation
+ ? !!getSettledSourceTextContent(state, selectedLocation)
+ : undefined,
+ query: getQuickOpenQuery(state),
+ searchType: getQuickOpenType(state),
+ openedTabUrls,
+ };
+}
+
+export default connect(mapStateToProps, {
+ selectSpecificLocation: actions.selectSpecificLocation,
+ setQuickOpenQuery: actions.setQuickOpenQuery,
+ highlightLineRange: actions.highlightLineRange,
+ clearHighlightLineRange: actions.clearHighlightLineRange,
+ closeQuickOpen: actions.closeQuickOpen,
+ getFunctionSymbols: actions.getFunctionSymbols,
+})(QuickOpenModal);
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/Breakpoint.js b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/Breakpoint.js
new file mode 100644
index 0000000000..c55088e411
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/Breakpoint.js
@@ -0,0 +1,235 @@
+/* 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 React, { PureComponent } from "devtools/client/shared/vendor/react";
+import {
+ div,
+ input,
+ span,
+} from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import { createSelector } from "devtools/client/shared/vendor/reselect";
+import actions from "../../../actions/index";
+
+import { CloseButton } from "../../shared/Button/index";
+
+import {
+ getSelectedText,
+ makeBreakpointId,
+} from "../../../utils/breakpoint/index";
+import { getSelectedLocation } from "../../../utils/selected-location";
+import { isLineBlackboxed } from "../../../utils/source";
+
+import {
+ getSelectedFrame,
+ getSelectedSource,
+ getCurrentThread,
+ isSourceMapIgnoreListEnabled,
+ isSourceOnSourceMapIgnoreList,
+ getBlackBoxRanges,
+} from "../../../selectors/index";
+
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+class Breakpoint extends PureComponent {
+ static get propTypes() {
+ return {
+ breakpoint: PropTypes.object.isRequired,
+ disableBreakpoint: PropTypes.func.isRequired,
+ editor: PropTypes.object.isRequired,
+ enableBreakpoint: PropTypes.func.isRequired,
+ frame: PropTypes.object,
+ openConditionalPanel: PropTypes.func.isRequired,
+ removeBreakpoint: PropTypes.func.isRequired,
+ selectSpecificLocation: PropTypes.func.isRequired,
+ selectedSource: PropTypes.object,
+ source: PropTypes.object.isRequired,
+ blackboxedRangesForSource: PropTypes.array.isRequired,
+ checkSourceOnIgnoreList: PropTypes.func.isRequired,
+ isBreakpointLineBlackboxed: PropTypes.bool,
+ showBreakpointContextMenu: PropTypes.func.isRequired,
+ };
+ }
+
+ onContextMenu = event => {
+ event.preventDefault();
+
+ this.props.showBreakpointContextMenu(
+ event,
+ this.props.breakpoint,
+ this.props.source
+ );
+ };
+
+ get selectedLocation() {
+ const { breakpoint, selectedSource } = this.props;
+ return getSelectedLocation(breakpoint, selectedSource);
+ }
+
+ stopClicks = event => event.stopPropagation();
+
+ onDoubleClick = () => {
+ const { breakpoint, openConditionalPanel } = this.props;
+ if (breakpoint.options.condition) {
+ openConditionalPanel(this.selectedLocation);
+ } else if (breakpoint.options.logValue) {
+ openConditionalPanel(this.selectedLocation, true);
+ }
+ };
+
+ selectBreakpoint = event => {
+ event.preventDefault();
+ const { selectSpecificLocation } = this.props;
+ selectSpecificLocation(this.selectedLocation);
+ };
+
+ removeBreakpoint = event => {
+ const { removeBreakpoint, breakpoint } = this.props;
+ event.stopPropagation();
+ removeBreakpoint(breakpoint);
+ };
+
+ handleBreakpointCheckbox = () => {
+ const { breakpoint, enableBreakpoint, disableBreakpoint } = this.props;
+ if (breakpoint.disabled) {
+ enableBreakpoint(breakpoint);
+ } else {
+ disableBreakpoint(breakpoint);
+ }
+ };
+
+ isCurrentlyPausedAtBreakpoint() {
+ const { frame } = this.props;
+ if (!frame) {
+ return false;
+ }
+
+ const bpId = makeBreakpointId(this.selectedLocation);
+ const frameId = makeBreakpointId(frame.selectedLocation);
+ return bpId == frameId;
+ }
+
+ getBreakpointLocation() {
+ const { source } = this.props;
+ const { column, line } = this.selectedLocation;
+
+ const isWasm = source?.isWasm;
+ // column is 0-based everywhere, but we want to display 1-based to the user.
+ const columnVal = column ? `:${column + 1}` : "";
+ const bpLocation = isWasm
+ ? `0x${line.toString(16).toUpperCase()}`
+ : `${line}${columnVal}`;
+
+ return bpLocation;
+ }
+
+ getBreakpointText() {
+ const { breakpoint, selectedSource } = this.props;
+ const { condition, logValue } = breakpoint.options;
+ return logValue || condition || getSelectedText(breakpoint, selectedSource);
+ }
+
+ highlightText(text = "", editor) {
+ const node = document.createElement("div");
+ editor.CodeMirror.runMode(text, "application/javascript", node);
+ return { __html: node.innerHTML };
+ }
+
+ render() {
+ const { breakpoint, editor, isBreakpointLineBlackboxed } = this.props;
+ const text = this.getBreakpointText();
+ const labelId = `${breakpoint.id}-label`;
+ return div(
+ {
+ className: classnames({
+ breakpoint,
+ paused: this.isCurrentlyPausedAtBreakpoint(),
+ disabled: breakpoint.disabled,
+ "is-conditional": !!breakpoint.options.condition,
+ "is-log": !!breakpoint.options.logValue,
+ }),
+ onClick: this.selectBreakpoint,
+ onDoubleClick: this.onDoubleClick,
+ onContextMenu: this.onContextMenu,
+ },
+ input({
+ id: breakpoint.id,
+ type: "checkbox",
+ className: "breakpoint-checkbox",
+ checked: !breakpoint.disabled,
+ disabled: isBreakpointLineBlackboxed,
+ onChange: this.handleBreakpointCheckbox,
+ onClick: this.stopClicks,
+ "aria-labelledby": labelId,
+ }),
+ span(
+ {
+ id: labelId,
+ className: "breakpoint-label cm-s-mozilla devtools-monospace",
+ onClick: this.selectBreakpoint,
+ title: text,
+ },
+ span({
+ dangerouslySetInnerHTML: this.highlightText(text, editor),
+ })
+ ),
+ div(
+ {
+ className: "breakpoint-line-close",
+ },
+ div(
+ {
+ className: "breakpoint-line devtools-monospace",
+ },
+ this.getBreakpointLocation()
+ ),
+ React.createElement(CloseButton, {
+ handleClick: this.removeBreakpoint,
+ tooltip: L10N.getStr("breakpoints.removeBreakpointTooltip"),
+ })
+ )
+ );
+ }
+}
+
+const getFormattedFrame = createSelector(
+ getSelectedSource,
+ getSelectedFrame,
+ (selectedSource, frame) => {
+ if (!frame) {
+ return null;
+ }
+
+ return {
+ ...frame,
+ selectedLocation: getSelectedLocation(frame, selectedSource),
+ };
+ }
+);
+
+const mapStateToProps = (state, props) => {
+ const blackboxedRangesForSource = getBlackBoxRanges(state)[props.source.url];
+ const isSourceOnIgnoreList =
+ isSourceMapIgnoreListEnabled(state) &&
+ isSourceOnSourceMapIgnoreList(state, props.source);
+ return {
+ selectedSource: getSelectedSource(state),
+ isBreakpointLineBlackboxed: isLineBlackboxed(
+ blackboxedRangesForSource,
+ props.breakpoint.location.line,
+ isSourceOnIgnoreList
+ ),
+ frame: getFormattedFrame(state, getCurrentThread(state)),
+ };
+};
+
+export default connect(mapStateToProps, {
+ enableBreakpoint: actions.enableBreakpoint,
+ removeBreakpoint: actions.removeBreakpoint,
+ disableBreakpoint: actions.disableBreakpoint,
+ selectSpecificLocation: actions.selectSpecificLocation,
+ openConditionalPanel: actions.openConditionalPanel,
+ showBreakpointContextMenu: actions.showBreakpointContextMenu,
+})(Breakpoint);
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/BreakpointHeading.js b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/BreakpointHeading.js
new file mode 100644
index 0000000000..78cc530cff
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/BreakpointHeading.js
@@ -0,0 +1,84 @@
+/* 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 React, { PureComponent } from "devtools/client/shared/vendor/react";
+import { div, span } from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import actions from "../../../actions/index";
+
+import {
+ getTruncatedFileName,
+ getDisplayPath,
+ getSourceQueryString,
+ getFileURL,
+} from "../../../utils/source";
+import { createLocation } from "../../../utils/location";
+import { getFirstSourceActorForGeneratedSource } from "../../../selectors/index";
+
+import SourceIcon from "../../shared/SourceIcon";
+
+class BreakpointHeading extends PureComponent {
+ static get propTypes() {
+ return {
+ sources: PropTypes.array.isRequired,
+ source: PropTypes.object.isRequired,
+ firstSourceActor: PropTypes.object,
+ selectSource: PropTypes.func.isRequired,
+ showBreakpointHeadingContextMenu: PropTypes.func.isRequired,
+ };
+ }
+ onContextMenu = event => {
+ event.preventDefault();
+
+ this.props.showBreakpointHeadingContextMenu(event, this.props.source);
+ };
+
+ render() {
+ const { sources, source, selectSource } = this.props;
+
+ const path = getDisplayPath(source, sources);
+ const query = getSourceQueryString(source);
+ return div(
+ {
+ className: "breakpoint-heading",
+ title: getFileURL(source, false),
+ onClick: () => selectSource(source),
+ onContextMenu: this.onContextMenu,
+ },
+ React.createElement(
+ SourceIcon,
+ // Breakpoints are displayed per source and may relate to many source actors.
+ // Arbitrarily pick the first source actor to compute the matching source icon
+ // The source actor is used to pick one specific source text content and guess
+ // the related framework icon.
+ {
+ location: createLocation({
+ source,
+ sourceActor: this.props.firstSourceActor,
+ }),
+ modifier: icon =>
+ ["file", "javascript"].includes(icon) ? null : icon,
+ }
+ ),
+ div(
+ {
+ className: "filename",
+ },
+ getTruncatedFileName(source, query),
+ path && span(null, `../${path}/..`)
+ )
+ );
+ }
+}
+
+const mapStateToProps = (state, { source }) => ({
+ firstSourceActor: getFirstSourceActorForGeneratedSource(state, source.id),
+});
+
+export default connect(mapStateToProps, {
+ selectSource: actions.selectSource,
+ showBreakpointHeadingContextMenu: actions.showBreakpointHeadingContextMenu,
+})(BreakpointHeading);
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/Breakpoints.css b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/Breakpoints.css
new file mode 100644
index 0000000000..c05dd0b53f
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/Breakpoints.css
@@ -0,0 +1,235 @@
+/* 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/>. */
+
+.breakpoints-pane > ._content {
+ overflow-x: auto;
+}
+
+.breakpoints-options *,
+.breakpoints-list * {
+ user-select: none;
+}
+
+.breakpoints-list {
+ padding: 4px 0;
+}
+
+.breakpoints-list .breakpoint-heading {
+ text-overflow: ellipsis;
+ width: 100%;
+ font-size: 12px;
+ line-height: 16px;
+}
+
+.breakpoint-heading:not(:first-child) {
+ margin-top: 2px;
+}
+
+.breakpoints-list .breakpoint-heading .filename {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.breakpoints-list .breakpoint-heading .filename span {
+ opacity: 0.7;
+ padding-left: 4px;
+}
+
+.breakpoints-list .breakpoint-heading,
+.breakpoints-list .breakpoint {
+ color: var(--theme-text-color-strong);
+ position: relative;
+ cursor: pointer;
+}
+
+.breakpoints-list .breakpoint-heading,
+.breakpoints-list .breakpoint,
+.breakpoints-options > * {
+ display: flex;
+ align-items: center;
+ overflow: hidden;
+ padding-top: 2px;
+ padding-bottom: 2px;
+ padding-inline-start: 16px;
+ padding-inline-end: 12px;
+}
+
+.breakpoints-exceptions-caught {
+ padding-bottom: 3px;
+ padding-top: 3px;
+ padding-inline-start: 36px;
+}
+
+.breakpoints-options {
+ padding-top: 4px;
+ padding-bottom: 4px;
+}
+
+.xhr-breakpoints-pane .breakpoints-options {
+ border-bottom: 1px solid var(--theme-splitter-color);
+}
+
+.breakpoints-options:not(.empty) {
+ border-bottom: 1px solid var(--theme-splitter-color);
+}
+
+.breakpoints-options input {
+ padding-inline-start: 2px;
+ margin-top: 0px;
+ margin-bottom: 0px;
+ margin-inline-start: 0;
+ margin-inline-end: 2px;
+ vertical-align: text-bottom;
+}
+
+.breakpoint-exceptions-label {
+ line-height: 14px;
+ padding-inline-end: 8px;
+ cursor: default;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.breakpoints-list .breakpoint,
+.breakpoints-list .breakpoint-heading,
+.breakpoints-options {
+ border-inline-start: 4px solid transparent
+}
+
+html .breakpoints-list .breakpoint.is-conditional {
+ border-inline-start-color: var(--theme-graphs-yellow);
+}
+
+html .breakpoints-list .breakpoint.is-log {
+ border-inline-start-color: var(--theme-graphs-purple);
+}
+
+html .breakpoints-list .breakpoint.paused {
+ background-color: var(--theme-toolbar-background-alt);
+ border-color: var(--breakpoint-active-color);
+}
+
+.breakpoints-list .breakpoint:hover {
+ background-color: var(--search-overlays-semitransparent);
+}
+
+.breakpoint-line-close {
+ margin-inline-start: 4px;
+}
+
+.breakpoints-list .breakpoint .breakpoint-line {
+ font-size: 11px;
+ color: var(--theme-comment);
+ min-width: 16px;
+ text-align: end;
+ padding-top: 1px;
+ padding-bottom: 1px;
+}
+
+.breakpoints-list .breakpoint:hover .breakpoint-line,
+.breakpoints-list .breakpoint-line-close:focus-within .breakpoint-line {
+ color: transparent;
+}
+
+.breakpoints-list .breakpoint.paused:hover {
+ border-color: var(--breakpoint-active-color-hover);
+}
+
+.breakpoints-list .breakpoint-label {
+ display: inline-block;
+ cursor: pointer;
+ flex-grow: 1;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ font-size: 11px;
+}
+
+.breakpoints-list .breakpoint-label span,
+.breakpoint-line-close {
+ display: inline;
+ line-height: 14px;
+}
+
+.breakpoint-checkbox {
+ margin-inline-start: 0px;
+ margin-top: 0px;
+ margin-bottom: 0px;
+ vertical-align: text-bottom;
+}
+
+.breakpoint-label .location {
+ width: 100%;
+ display: inline-block;
+ overflow-x: hidden;
+ text-overflow: ellipsis;
+ padding: 1px 0;
+ vertical-align: bottom;
+}
+
+.breakpoints-list .pause-indicator {
+ flex: 0 1 content;
+ order: 3;
+}
+
+.breakpoint .close-btn {
+ position: absolute;
+ /* hide button outside of row until hovered or focused */
+ top: -100px;
+}
+
+[dir="ltr"] .breakpoint .close-btn {
+ right: 12px;
+}
+
+[dir="rtl"] .breakpoint .close-btn {
+ left: 12px;
+}
+
+/* Reveal the remove button on hover/focus */
+.breakpoint:hover .close-btn,
+.breakpoint .close-btn:focus {
+ top: calc(50% - 8px);
+}
+
+/* Hide the line number when revealing the remove button (since they're overlayed) */
+.breakpoint-line-close:focus-within .breakpoint-line,
+.breakpoint:hover .breakpoint-line {
+ visibility: hidden;
+}
+
+.CodeMirror.cm-s-mozilla-breakpoint {
+ cursor: pointer;
+}
+
+.CodeMirror.cm-s-mozilla-breakpoint .CodeMirror-lines {
+ padding: 0;
+}
+
+.CodeMirror.cm-s-mozilla-breakpoint .CodeMirror-sizer {
+ min-width: initial !important;
+}
+
+.breakpoints-list .breakpoint .CodeMirror.cm-s-mozilla-breakpoint {
+ transition: opacity 0.15s linear;
+}
+
+.breakpoints-list .breakpoint.disabled .CodeMirror.cm-s-mozilla-breakpoint {
+ opacity: 0.5;
+}
+
+.CodeMirror.cm-s-mozilla-breakpoint .CodeMirror-line span[role="presentation"] {
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: inline-block;
+}
+
+.CodeMirror.cm-s-mozilla-breakpoint .CodeMirror-code,
+.CodeMirror.cm-s-mozilla-breakpoint .CodeMirror-scroll {
+ pointer-events: none;
+}
+
+.CodeMirror.cm-s-mozilla-breakpoint {
+ padding-top: 1px;
+}
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/ExceptionOption.js b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/ExceptionOption.js
new file mode 100644
index 0000000000..31ff3f44a3
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/ExceptionOption.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 {
+ div,
+ input,
+ label,
+} from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+export default function ExceptionOption({
+ className,
+ isChecked = false,
+ label: inputLabel,
+ onChange,
+}) {
+ return label(
+ {
+ className,
+ },
+ input({
+ type: "checkbox",
+ checked: isChecked,
+ onChange: onChange,
+ }),
+ div(
+ {
+ className: "breakpoint-exceptions-label",
+ },
+ inputLabel
+ )
+ );
+}
+
+ExceptionOption.propTypes = {
+ className: PropTypes.string.isRequired,
+ isChecked: PropTypes.bool.isRequired,
+ label: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
+};
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/index.js b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/index.js
new file mode 100644
index 0000000000..0f5d6f7ae3
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/index.js
@@ -0,0 +1,172 @@
+/* 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 React, { Component } from "devtools/client/shared/vendor/react";
+import { div } from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+
+import ExceptionOption from "./ExceptionOption";
+
+import Breakpoint from "./Breakpoint";
+import BreakpointHeading from "./BreakpointHeading";
+
+import actions from "../../../actions/index";
+import { getSelectedLocation } from "../../../utils/selected-location";
+import { createHeadlessEditor } from "../../../utils/editor/create-editor";
+
+import { makeBreakpointId } from "../../../utils/breakpoint/index";
+
+import {
+ getSelectedSource,
+ getBreakpointSources,
+ getShouldPauseOnDebuggerStatement,
+ getShouldPauseOnExceptions,
+ getShouldPauseOnCaughtExceptions,
+} from "../../../selectors/index";
+
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+class Breakpoints extends Component {
+ static get propTypes() {
+ return {
+ breakpointSources: PropTypes.array.isRequired,
+ pauseOnExceptions: PropTypes.func.isRequired,
+ selectedSource: PropTypes.object,
+ shouldPauseOnDebuggerStatement: PropTypes.bool.isRequired,
+ shouldPauseOnCaughtExceptions: PropTypes.bool.isRequired,
+ shouldPauseOnExceptions: PropTypes.bool.isRequired,
+ };
+ }
+
+ componentWillUnmount() {
+ this.removeEditor();
+ }
+
+ getEditor() {
+ if (!this.headlessEditor) {
+ this.headlessEditor = createHeadlessEditor();
+ }
+ return this.headlessEditor;
+ }
+
+ removeEditor() {
+ if (!this.headlessEditor) {
+ return;
+ }
+ this.headlessEditor.destroy();
+ this.headlessEditor = null;
+ }
+
+ togglePauseOnDebuggerStatement = () => {
+ this.props.pauseOnDebuggerStatement(
+ !this.props.shouldPauseOnDebuggerStatement
+ );
+ };
+
+ togglePauseOnException = () => {
+ this.props.pauseOnExceptions(!this.props.shouldPauseOnExceptions, false);
+ };
+
+ togglePauseOnCaughtException = () => {
+ this.props.pauseOnExceptions(
+ true,
+ !this.props.shouldPauseOnCaughtExceptions
+ );
+ };
+
+ renderExceptionsOptions() {
+ const {
+ breakpointSources,
+ shouldPauseOnDebuggerStatement,
+ shouldPauseOnExceptions,
+ shouldPauseOnCaughtExceptions,
+ } = this.props;
+
+ const isEmpty = !breakpointSources.length;
+ return div(
+ {
+ className: classnames("breakpoints-options", {
+ empty: isEmpty,
+ }),
+ },
+ React.createElement(ExceptionOption, {
+ className: "breakpoints-debugger-statement",
+ label: L10N.getStr("pauseOnDebuggerStatement"),
+ isChecked: shouldPauseOnDebuggerStatement,
+ onChange: this.togglePauseOnDebuggerStatement,
+ }),
+ React.createElement(ExceptionOption, {
+ className: "breakpoints-exceptions",
+ label: L10N.getStr("pauseOnExceptionsItem2"),
+ isChecked: shouldPauseOnExceptions,
+ onChange: this.togglePauseOnException,
+ }),
+ shouldPauseOnExceptions &&
+ React.createElement(ExceptionOption, {
+ className: "breakpoints-exceptions-caught",
+ label: L10N.getStr("pauseOnCaughtExceptionsItem"),
+ isChecked: shouldPauseOnCaughtExceptions,
+ onChange: this.togglePauseOnCaughtException,
+ })
+ );
+ }
+
+ renderBreakpoints() {
+ const { breakpointSources, selectedSource } = this.props;
+ if (!breakpointSources.length) {
+ return null;
+ }
+
+ const editor = this.getEditor();
+ const sources = breakpointSources.map(({ source }) => source);
+ return div(
+ {
+ className: "pane breakpoints-list",
+ },
+ breakpointSources.map(({ source, breakpoints }) => {
+ return [
+ React.createElement(BreakpointHeading, {
+ key: source.id,
+ source,
+ sources,
+ }),
+ breakpoints.map(breakpoint =>
+ React.createElement(Breakpoint, {
+ breakpoint,
+ source,
+ editor,
+ key: makeBreakpointId(
+ getSelectedLocation(breakpoint, selectedSource)
+ ),
+ })
+ ),
+ ];
+ })
+ );
+ }
+
+ render() {
+ return div(
+ {
+ className: "pane",
+ },
+ this.renderExceptionsOptions(),
+ this.renderBreakpoints()
+ );
+ }
+}
+
+const mapStateToProps = state => ({
+ breakpointSources: getBreakpointSources(state),
+ selectedSource: getSelectedSource(state),
+ shouldPauseOnDebuggerStatement: getShouldPauseOnDebuggerStatement(state),
+ shouldPauseOnExceptions: getShouldPauseOnExceptions(state),
+ shouldPauseOnCaughtExceptions: getShouldPauseOnCaughtExceptions(state),
+});
+
+export default connect(mapStateToProps, {
+ pauseOnDebuggerStatement: actions.pauseOnDebuggerStatement,
+ pauseOnExceptions: actions.pauseOnExceptions,
+})(Breakpoints);
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/moz.build b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/moz.build
new file mode 100644
index 0000000000..85716d122c
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/moz.build
@@ -0,0 +1,13 @@
+# 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(
+ "Breakpoint.js",
+ "BreakpointHeading.js",
+ "ExceptionOption.js",
+ "index.js",
+)
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/tests/ExceptionOption.spec.js b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/tests/ExceptionOption.spec.js
new file mode 100644
index 0000000000..51f0b1e948
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/tests/ExceptionOption.spec.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 React from "devtools/client/shared/vendor/react";
+import { shallow } from "enzyme";
+
+import ExceptionOption from "../ExceptionOption";
+
+describe("ExceptionOption renders", () => {
+ it("with values", () => {
+ const component = shallow(
+ React.createElement(ExceptionOption, {
+ label: "testLabel",
+ isChecked: true,
+ onChange: () => null,
+ className: "testClassName",
+ })
+ );
+ expect(component).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/tests/__snapshots__/ExceptionOption.spec.js.snap b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/tests/__snapshots__/ExceptionOption.spec.js.snap
new file mode 100644
index 0000000000..3ed80783b6
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Breakpoints/tests/__snapshots__/ExceptionOption.spec.js.snap
@@ -0,0 +1,18 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ExceptionOption renders with values 1`] = `
+<label
+ className="testClassName"
+>
+ <input
+ checked={true}
+ onChange={[Function]}
+ type="checkbox"
+ />
+ <div
+ className="breakpoint-exceptions-label"
+ >
+ testLabel
+ </div>
+</label>
+`;
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/CommandBar.css b/devtools/client/debugger/src/components/SecondaryPanes/CommandBar.css
new file mode 100644
index 0000000000..68bd0bfcdd
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/CommandBar.css
@@ -0,0 +1,33 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+.command-bar {
+ flex: 0 0 29px;
+ border-bottom: 1px solid var(--theme-splitter-color);
+ display: flex;
+ overflow: hidden;
+ z-index: 1;
+ background-color: var(--theme-toolbar-background);
+}
+
+html[dir="rtl"] .command-bar {
+ border-right: 1px solid var(--theme-splitter-color);
+}
+
+.command-bar .filler {
+ flex-grow: 1;
+}
+
+.command-bar .step-position {
+ color: var(--theme-text-color-inactive);
+ padding-top: 8px;
+ margin-inline-end: 4px;
+}
+
+.command-bar .divider {
+ width: 1px;
+ background: var(--theme-splitter-color);
+ height: 10px;
+ margin: 11px 6px 0 6px;
+}
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/CommandBar.js b/devtools/client/debugger/src/components/SecondaryPanes/CommandBar.js
new file mode 100644
index 0000000000..deae156a40
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/CommandBar.js
@@ -0,0 +1,502 @@
+/* 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 React, { Component } from "devtools/client/shared/vendor/react";
+import { div, button } from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import { features, prefs } from "../../utils/prefs";
+import {
+ getIsWaitingOnBreak,
+ getSkipPausing,
+ getCurrentThread,
+ isTopFrameSelected,
+ getIsCurrentThreadPaused,
+ getIsJavascriptTracingEnabled,
+ getIsThreadCurrentlyTracing,
+ getJavascriptTracingLogMethod,
+ getJavascriptTracingValues,
+ getJavascriptTracingOnNextInteraction,
+ getJavascriptTracingOnNextLoad,
+ getJavascriptTracingFunctionReturn,
+} from "../../selectors/index";
+import { formatKeyShortcut } from "../../utils/text";
+import actions from "../../actions/index";
+import { debugBtn } from "../shared/Button/CommandBarButton";
+import AccessibleImage from "../shared/AccessibleImage";
+import { showMenu } from "../../context-menu/menu";
+
+const classnames = require("resource://devtools/client/shared/classnames.js");
+const MenuButton = require("resource://devtools/client/shared/components/menu/MenuButton.js");
+const MenuItem = require("resource://devtools/client/shared/components/menu/MenuItem.js");
+const MenuList = require("resource://devtools/client/shared/components/menu/MenuList.js");
+
+const isMacOS = Services.appinfo.OS === "Darwin";
+
+// NOTE: the "resume" command will call either the resume or breakOnNext action
+// depending on whether or not the debugger is paused or running
+const COMMANDS = ["resume", "stepOver", "stepIn", "stepOut"];
+
+const KEYS = {
+ WINNT: {
+ resume: "F8",
+ stepOver: "F10",
+ stepIn: "F11",
+ stepOut: "Shift+F11",
+ trace: "Ctrl+Shift+5",
+ },
+ Darwin: {
+ resume: "Cmd+\\",
+ stepOver: "Cmd+'",
+ stepIn: "Cmd+;",
+ stepOut: "Cmd+Shift+:",
+ stepOutDisplay: "Cmd+Shift+;",
+ trace: "Ctrl+Shift+5",
+ },
+ Linux: {
+ resume: "F8",
+ stepOver: "F10",
+ stepIn: "F11",
+ stepOut: "Shift+F11",
+ trace: "Ctrl+Shift+5",
+ },
+};
+
+const LOG_METHODS = {
+ CONSOLE: "console",
+ STDOUT: "stdout",
+};
+
+function getKey(action) {
+ return getKeyForOS(Services.appinfo.OS, action);
+}
+
+function getKeyForOS(os, action) {
+ const osActions = KEYS[os] || KEYS.Linux;
+ return osActions[action];
+}
+
+function formatKey(action) {
+ const key = getKey(`${action}Display`) || getKey(action);
+
+ // On MacOS, we bind both Windows and MacOS/Darwin key shortcuts
+ // Display them both, but only when they are different
+ if (isMacOS) {
+ const winKey =
+ getKeyForOS("WINNT", `${action}Display`) || getKeyForOS("WINNT", action);
+ if (key != winKey) {
+ return formatKeyShortcut([key, winKey].join(" "));
+ }
+ }
+ return formatKeyShortcut(key);
+}
+
+class CommandBar extends Component {
+ constructor() {
+ super();
+
+ this.state = {};
+ }
+ static get propTypes() {
+ return {
+ breakOnNext: PropTypes.func.isRequired,
+ horizontal: PropTypes.bool.isRequired,
+ isPaused: PropTypes.bool.isRequired,
+ isTracingEnabled: PropTypes.bool.isRequired,
+ isWaitingOnBreak: PropTypes.bool.isRequired,
+ javascriptEnabled: PropTypes.bool.isRequired,
+ trace: PropTypes.func.isRequired,
+ resume: PropTypes.func.isRequired,
+ skipPausing: PropTypes.bool.isRequired,
+ stepIn: PropTypes.func.isRequired,
+ stepOut: PropTypes.func.isRequired,
+ stepOver: PropTypes.func.isRequired,
+ toggleEditorWrapping: PropTypes.func.isRequired,
+ toggleInlinePreview: PropTypes.func.isRequired,
+ toggleJavaScriptEnabled: PropTypes.func.isRequired,
+ toggleSkipPausing: PropTypes.any.isRequired,
+ toggleSourceMapsEnabled: PropTypes.func.isRequired,
+ topFrameSelected: PropTypes.bool.isRequired,
+ toggleTracing: PropTypes.func.isRequired,
+ logMethod: PropTypes.string.isRequired,
+ logValues: PropTypes.bool.isRequired,
+ traceOnNextInteraction: PropTypes.bool.isRequired,
+ setJavascriptTracingLogMethod: PropTypes.func.isRequired,
+ setHideOrShowIgnoredSources: PropTypes.func.isRequired,
+ toggleSourceMapIgnoreList: PropTypes.func.isRequired,
+ };
+ }
+
+ componentWillUnmount() {
+ const { shortcuts } = this.context;
+
+ COMMANDS.forEach(action => shortcuts.off(getKey(action)));
+
+ if (isMacOS) {
+ COMMANDS.forEach(action => shortcuts.off(getKeyForOS("WINNT", action)));
+ }
+ }
+
+ componentDidMount() {
+ const { shortcuts } = this.context;
+
+ COMMANDS.forEach(action =>
+ shortcuts.on(getKey(action), e => this.handleEvent(e, action))
+ );
+
+ if (isMacOS) {
+ // The Mac supports both the Windows Function keys
+ // as well as the Mac non-Function keys
+ COMMANDS.forEach(action =>
+ shortcuts.on(getKeyForOS("WINNT", action), e =>
+ this.handleEvent(e, action)
+ )
+ );
+ }
+ }
+
+ handleEvent(e, action) {
+ e.preventDefault();
+ e.stopPropagation();
+ if (action === "resume") {
+ this.props.isPaused ? this.props.resume() : this.props.breakOnNext();
+ } else {
+ this.props[action]();
+ }
+ }
+
+ renderStepButtons() {
+ const { isPaused, topFrameSelected } = this.props;
+ const className = isPaused ? "active" : "disabled";
+ const isDisabled = !isPaused;
+
+ return [
+ this.renderPauseButton(),
+ debugBtn(
+ () => this.props.stepOver(),
+ "stepOver",
+ className,
+ L10N.getFormatStr("stepOverTooltip", formatKey("stepOver")),
+ isDisabled
+ ),
+ debugBtn(
+ () => this.props.stepIn(),
+ "stepIn",
+ className,
+ L10N.getFormatStr("stepInTooltip", formatKey("stepIn")),
+ isDisabled || !topFrameSelected
+ ),
+ debugBtn(
+ () => this.props.stepOut(),
+ "stepOut",
+ className,
+ L10N.getFormatStr("stepOutTooltip", formatKey("stepOut")),
+ isDisabled
+ ),
+ ];
+ }
+
+ resume() {
+ this.props.resume();
+ }
+
+ renderTraceButton() {
+ if (!features.javascriptTracing) {
+ return null;
+ }
+
+ // The button is highlighted in blue as soon as the user requested to start the trace
+ const isActive = this.props.isTracingEnabled;
+ // But it will only be active once the tracer actually started.
+ // This may come later when using "on next user interaction" feature.
+ const isPending = isActive && !this.props.isTracingActive;
+
+ let className = "";
+ if (isPending) {
+ className = "pending";
+ } else if (isActive) {
+ className = "active";
+ }
+ // Display a button which:
+ // - on left click, would toggle on/off javascript tracing
+ // - on right click, would display a context menu to configure the tracer settings
+ return button({
+ className: `devtools-button command-bar-button debugger-trace-menu-button ${className}`,
+ title: this.props.isTracingEnabled
+ ? L10N.getFormatStr("stopTraceButtonTooltip2", formatKey("trace"))
+ : L10N.getFormatStr(
+ "startTraceButtonTooltip2",
+ formatKey("trace"),
+ this.props.logMethod
+ ),
+ onClick: event => {
+ this.props.toggleTracing();
+ },
+ onContextMenu: event => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ // Avoid showing the menu to avoid having to support changing tracing config "live"
+ if (this.props.isTracingEnabled) {
+ return;
+ }
+ const items = [
+ {
+ id: "debugger-trace-menu-item-console",
+ label: L10N.getStr("traceInWebConsole"),
+ checked: this.props.logMethod == LOG_METHODS.CONSOLE,
+ type: "radio",
+ click: () => {
+ this.props.setJavascriptTracingLogMethod(LOG_METHODS.CONSOLE);
+ },
+ },
+ {
+ id: "debugger-trace-menu-item-stdout",
+ label: L10N.getStr("traceInStdout"),
+ type: "radio",
+ checked: this.props.logMethod == LOG_METHODS.STDOUT,
+ click: () => {
+ this.props.setJavascriptTracingLogMethod(LOG_METHODS.STDOUT);
+ },
+ },
+ { type: "separator" },
+ {
+ id: "debugger-trace-menu-item-next-interaction",
+ label: L10N.getStr("traceOnNextInteraction"),
+ type: "checkbox",
+ checked: this.props.traceOnNextInteraction,
+ click: () => {
+ this.props.toggleJavascriptTracingOnNextInteraction();
+ },
+ },
+ {
+ id: "debugger-trace-menu-item-next-load",
+ label: L10N.getStr("traceOnNextLoad"),
+ type: "checkbox",
+ checked: this.props.traceOnNextLoad,
+ click: () => {
+ this.props.toggleJavascriptTracingOnNextLoad();
+ },
+ },
+ { type: "separator" },
+ {
+ id: "debugger-trace-menu-item-log-values",
+ label: L10N.getStr("traceValues"),
+ type: "checkbox",
+ checked: this.props.logValues,
+ click: () => {
+ this.props.toggleJavascriptTracingValues();
+ },
+ },
+ {
+ id: "debugger-trace-menu-item-function-return",
+ label: L10N.getStr("traceFunctionReturn"),
+ type: "checkbox",
+ checked: this.props.traceFunctionReturn,
+ click: () => {
+ this.props.toggleJavascriptTracingFunctionReturn();
+ },
+ },
+ ];
+ showMenu(event, items);
+ },
+ });
+ }
+ renderPauseButton() {
+ const { breakOnNext, isWaitingOnBreak } = this.props;
+
+ if (this.props.isPaused) {
+ return debugBtn(
+ () => this.resume(),
+ "resume",
+ "active",
+ L10N.getFormatStr("resumeButtonTooltip", formatKey("resume"))
+ );
+ }
+
+ if (isWaitingOnBreak) {
+ return debugBtn(
+ null,
+ "pause",
+ "disabled",
+ L10N.getStr("pausePendingButtonTooltip"),
+ true
+ );
+ }
+
+ return debugBtn(
+ () => breakOnNext(),
+ "pause",
+ "active",
+ L10N.getFormatStr("pauseButtonTooltip", formatKey("resume"))
+ );
+ }
+
+ renderSkipPausingButton() {
+ const { skipPausing, toggleSkipPausing } = this.props;
+ return button(
+ {
+ className: classnames(
+ "command-bar-button",
+ "command-bar-skip-pausing",
+ {
+ active: skipPausing,
+ }
+ ),
+ title: skipPausing
+ ? L10N.getStr("undoSkipPausingTooltip.label")
+ : L10N.getStr("skipPausingTooltip.label"),
+ onClick: toggleSkipPausing,
+ },
+ React.createElement(AccessibleImage, {
+ className: skipPausing ? "enable-pausing" : "disable-pausing",
+ })
+ );
+ }
+
+ renderSettingsButton() {
+ const { toolboxDoc } = this.context;
+ return React.createElement(
+ MenuButton,
+ {
+ menuId: "debugger-settings-menu-button",
+ toolboxDoc: toolboxDoc,
+ className:
+ "devtools-button command-bar-button debugger-settings-menu-button",
+ title: L10N.getStr("settings.button.label"),
+ },
+ () => this.renderSettingsMenuItems()
+ );
+ }
+
+ renderSettingsMenuItems() {
+ return React.createElement(
+ MenuList,
+ {
+ id: "debugger-settings-menu-list",
+ },
+ React.createElement(MenuItem, {
+ key: "debugger-settings-menu-item-disable-javascript",
+ className: "menu-item debugger-settings-menu-item-disable-javascript",
+ checked: !this.props.javascriptEnabled,
+ label: L10N.getStr("settings.disableJavaScript.label"),
+ tooltip: L10N.getStr("settings.disableJavaScript.tooltip"),
+ onClick: () => {
+ this.props.toggleJavaScriptEnabled(!this.props.javascriptEnabled);
+ },
+ }),
+ React.createElement(MenuItem, {
+ key: "debugger-settings-menu-item-disable-inline-previews",
+ checked: features.inlinePreview,
+ label: L10N.getStr("inlinePreview.toggle.label"),
+ tooltip: L10N.getStr("inlinePreview.toggle.tooltip"),
+ onClick: () => this.props.toggleInlinePreview(!features.inlinePreview),
+ }),
+ React.createElement(MenuItem, {
+ key: "debugger-settings-menu-item-disable-wrap-lines",
+ checked: prefs.editorWrapping,
+ label: L10N.getStr("editorWrapping.toggle.label"),
+ tooltip: L10N.getStr("editorWrapping.toggle.tooltip"),
+ onClick: () => this.props.toggleEditorWrapping(!prefs.editorWrapping),
+ }),
+ React.createElement(MenuItem, {
+ key: "debugger-settings-menu-item-disable-sourcemaps",
+ checked: prefs.clientSourceMapsEnabled,
+ label: L10N.getStr("settings.toggleSourceMaps.label"),
+ tooltip: L10N.getStr("settings.toggleSourceMaps.tooltip"),
+ onClick: () =>
+ this.props.toggleSourceMapsEnabled(!prefs.clientSourceMapsEnabled),
+ }),
+ React.createElement(MenuItem, {
+ key: "debugger-settings-menu-item-hide-ignored-sources",
+ className: "menu-item debugger-settings-menu-item-hide-ignored-sources",
+ checked: prefs.hideIgnoredSources,
+ label: L10N.getStr("settings.hideIgnoredSources.label"),
+ tooltip: L10N.getStr("settings.hideIgnoredSources.tooltip"),
+ onClick: () =>
+ this.props.setHideOrShowIgnoredSources(!prefs.hideIgnoredSources),
+ }),
+ React.createElement(MenuItem, {
+ key: "debugger-settings-menu-item-enable-sourcemap-ignore-list",
+ className:
+ "menu-item debugger-settings-menu-item-enable-sourcemap-ignore-list",
+ checked: prefs.sourceMapIgnoreListEnabled,
+ label: L10N.getStr("settings.enableSourceMapIgnoreList.label"),
+ tooltip: L10N.getStr("settings.enableSourceMapIgnoreList.tooltip"),
+ onClick: () =>
+ this.props.toggleSourceMapIgnoreList(
+ !prefs.sourceMapIgnoreListEnabled
+ ),
+ })
+ );
+ }
+
+ render() {
+ return div(
+ {
+ className: classnames("command-bar", {
+ vertical: !this.props.horizontal,
+ }),
+ },
+ this.renderStepButtons(),
+ div({
+ className: "filler",
+ }),
+ this.renderTraceButton(),
+ this.renderSkipPausingButton(),
+ div({
+ className: "devtools-separator",
+ }),
+ this.renderSettingsButton()
+ );
+ }
+}
+
+CommandBar.contextTypes = {
+ shortcuts: PropTypes.object,
+ toolboxDoc: PropTypes.object,
+};
+
+const mapStateToProps = state => ({
+ isWaitingOnBreak: getIsWaitingOnBreak(state, getCurrentThread(state)),
+ skipPausing: getSkipPausing(state),
+ topFrameSelected: isTopFrameSelected(state, getCurrentThread(state)),
+ javascriptEnabled: state.ui.javascriptEnabled,
+ isPaused: getIsCurrentThreadPaused(state),
+ isTracingEnabled: getIsJavascriptTracingEnabled(
+ state,
+ getCurrentThread(state)
+ ),
+ isTracingActive: getIsThreadCurrentlyTracing(state, getCurrentThread(state)),
+ logMethod: getJavascriptTracingLogMethod(state),
+ logValues: getJavascriptTracingValues(state),
+ traceOnNextInteraction: getJavascriptTracingOnNextInteraction(state),
+ traceOnNextLoad: getJavascriptTracingOnNextLoad(state),
+ traceFunctionReturn: getJavascriptTracingFunctionReturn(state),
+});
+
+export default connect(mapStateToProps, {
+ toggleTracing: actions.toggleTracing,
+ setJavascriptTracingLogMethod: actions.setJavascriptTracingLogMethod,
+ toggleJavascriptTracingValues: actions.toggleJavascriptTracingValues,
+ toggleJavascriptTracingOnNextInteraction:
+ actions.toggleJavascriptTracingOnNextInteraction,
+ toggleJavascriptTracingOnNextLoad: actions.toggleJavascriptTracingOnNextLoad,
+ toggleJavascriptTracingFunctionReturn:
+ actions.toggleJavascriptTracingFunctionReturn,
+ resume: actions.resume,
+ stepIn: actions.stepIn,
+ stepOut: actions.stepOut,
+ stepOver: actions.stepOver,
+ breakOnNext: actions.breakOnNext,
+ pauseOnExceptions: actions.pauseOnExceptions,
+ toggleSkipPausing: actions.toggleSkipPausing,
+ toggleInlinePreview: actions.toggleInlinePreview,
+ toggleEditorWrapping: actions.toggleEditorWrapping,
+ toggleSourceMapsEnabled: actions.toggleSourceMapsEnabled,
+ toggleJavaScriptEnabled: actions.toggleJavaScriptEnabled,
+ setHideOrShowIgnoredSources: actions.setHideOrShowIgnoredSources,
+ toggleSourceMapIgnoreList: actions.toggleSourceMapIgnoreList,
+})(CommandBar);
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/DOMMutationBreakpoints.css b/devtools/client/debugger/src/components/SecondaryPanes/DOMMutationBreakpoints.css
new file mode 100644
index 0000000000..b525783984
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/DOMMutationBreakpoints.css
@@ -0,0 +1,76 @@
+/* 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/>. */
+
+ .dom-mutation-empty {
+ padding: 6px 20px;
+ text-align: center;
+ font-style: italic;
+ color: var(--theme-body-color);
+ white-space: normal;
+ }
+
+ .dom-mutation-empty a {
+ text-decoration: underline;
+ color: var(--theme-toolbar-selected-color);
+ cursor: pointer;
+ }
+
+.dom-mutation-list * {
+ user-select: none;
+}
+
+.dom-mutation-list {
+ padding: 4px 0;
+ list-style-type: none;
+}
+
+.dom-mutation-list li {
+ position: relative;
+
+ display: flex;
+ align-items: start;
+ overflow: hidden;
+ padding-top: 2px;
+ padding-bottom: 2px;
+ padding-inline-start: 20px;
+ padding-inline-end: 12px;
+}
+
+.dom-mutation-list input {
+ margin: 2px 3px;
+
+ padding-inline-start: 2px;
+ margin-top: 0px;
+ margin-bottom: 0px;
+ margin-inline-start: 0;
+ margin-inline-end: 2px;
+ vertical-align: text-bottom;
+}
+
+.dom-mutation-info {
+ flex-grow: 1;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ margin-inline-end: 20px;
+}
+
+.dom-mutation-list .close-btn {
+ position: absolute;
+ /* hide button outside of row until hovered or focused */
+ top: -100px;
+}
+
+/* Reveal the remove button on hover/focus */
+.dom-mutation-list li:hover .close-btn,
+.dom-mutation-list li .close-btn:focus {
+ top: calc(50% - 8px);
+}
+
+[dir="ltr"] .dom-mutation-list .close-btn {
+ right: 12px;
+}
+
+[dir="rtl"] .dom-mutation-list .close-btn {
+ left: 12px;
+}
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/DOMMutationBreakpoints.js b/devtools/client/debugger/src/components/SecondaryPanes/DOMMutationBreakpoints.js
new file mode 100644
index 0000000000..642ba21505
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/DOMMutationBreakpoints.js
@@ -0,0 +1,189 @@
+/* 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 React, { Component } from "devtools/client/shared/vendor/react";
+import {
+ div,
+ input,
+ li,
+ ul,
+} from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+import Reps from "devtools/client/shared/components/reps/index";
+const {
+ REPS: { Rep },
+ MODE,
+} = Reps;
+import { translateNodeFrontToGrip } from "devtools/client/inspector/shared/utils";
+
+import {
+ deleteDOMMutationBreakpoint,
+ toggleDOMMutationBreakpointState,
+} from "devtools/client/framework/actions/index";
+
+import actions from "../../actions/index";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+
+import { CloseButton } from "../shared/Button/index";
+
+const localizationTerms = {
+ subtree: L10N.getStr("domMutationTypes.subtree"),
+ attribute: L10N.getStr("domMutationTypes.attribute"),
+ removal: L10N.getStr("domMutationTypes.removal"),
+};
+
+class DOMMutationBreakpointsContents extends Component {
+ static get propTypes() {
+ return {
+ breakpoints: PropTypes.array.isRequired,
+ deleteBreakpoint: PropTypes.func.isRequired,
+ highlightDomElement: PropTypes.func.isRequired,
+ openElementInInspector: PropTypes.func.isRequired,
+ openInspector: PropTypes.func.isRequired,
+ setSkipPausing: PropTypes.func.isRequired,
+ toggleBreakpoint: PropTypes.func.isRequired,
+ unHighlightDomElement: PropTypes.func.isRequired,
+ };
+ }
+
+ handleBreakpoint(breakpointId, shouldEnable) {
+ const { toggleBreakpoint, setSkipPausing } = this.props;
+
+ // The user has enabled a mutation breakpoint so we should no
+ // longer skip pausing
+ if (shouldEnable) {
+ setSkipPausing(false);
+ }
+ toggleBreakpoint(breakpointId, shouldEnable);
+ }
+
+ renderItem(breakpoint) {
+ const {
+ openElementInInspector,
+ highlightDomElement,
+ unHighlightDomElement,
+ deleteBreakpoint,
+ } = this.props;
+ const { enabled, id: breakpointId, nodeFront, mutationType } = breakpoint;
+
+ return li(
+ {
+ key: breakpoint.id,
+ },
+ input({
+ type: "checkbox",
+ checked: enabled,
+ onChange: () => this.handleBreakpoint(breakpointId, !enabled),
+ }),
+ div(
+ {
+ className: "dom-mutation-info",
+ },
+ div(
+ {
+ className: "dom-mutation-label",
+ },
+ Rep({
+ object: translateNodeFrontToGrip(nodeFront),
+ mode: MODE.TINY,
+ onDOMNodeClick: () => openElementInInspector(nodeFront),
+ onInspectIconClick: () => openElementInInspector(nodeFront),
+ onDOMNodeMouseOver: () => highlightDomElement(nodeFront),
+ onDOMNodeMouseOut: () => unHighlightDomElement(),
+ })
+ ),
+ div(
+ {
+ className: "dom-mutation-type",
+ },
+ localizationTerms[mutationType] || mutationType
+ )
+ ),
+ React.createElement(CloseButton, {
+ handleClick: () => deleteBreakpoint(nodeFront, mutationType),
+ })
+ );
+ }
+
+ /* eslint-disable react/no-danger */
+ renderEmpty() {
+ const { openInspector } = this.props;
+ const text = L10N.getFormatStr(
+ "noDomMutationBreakpoints",
+ `<a>${L10N.getStr("inspectorTool")}</a>`
+ );
+ return div(
+ {
+ className: "dom-mutation-empty",
+ },
+ div({
+ onClick: () => openInspector(),
+ dangerouslySetInnerHTML: {
+ __html: text,
+ },
+ })
+ );
+ }
+
+ render() {
+ const { breakpoints } = this.props;
+
+ if (breakpoints.length === 0) {
+ return this.renderEmpty();
+ }
+ return ul(
+ {
+ className: "dom-mutation-list",
+ },
+ breakpoints.map(breakpoint => this.renderItem(breakpoint))
+ );
+ }
+}
+
+const mapStateToProps = state => ({
+ breakpoints: state.domMutationBreakpoints.breakpoints,
+});
+
+const DOMMutationBreakpointsPanel = connect(
+ mapStateToProps,
+ {
+ deleteBreakpoint: deleteDOMMutationBreakpoint,
+ toggleBreakpoint: toggleDOMMutationBreakpointState,
+ },
+ undefined,
+ { storeKey: "toolbox-store" }
+)(DOMMutationBreakpointsContents);
+
+class DomMutationBreakpoints extends Component {
+ static get propTypes() {
+ return {
+ highlightDomElement: PropTypes.func.isRequired,
+ openElementInInspector: PropTypes.func.isRequired,
+ openInspector: PropTypes.func.isRequired,
+ setSkipPausing: PropTypes.func.isRequired,
+ unHighlightDomElement: PropTypes.func.isRequired,
+ };
+ }
+
+ render() {
+ return React.createElement(DOMMutationBreakpointsPanel, {
+ openElementInInspector: this.props.openElementInInspector,
+ highlightDomElement: this.props.highlightDomElement,
+ unHighlightDomElement: this.props.unHighlightDomElement,
+ setSkipPausing: this.props.setSkipPausing,
+ openInspector: this.props.openInspector,
+ });
+ }
+}
+
+export default connect(undefined, {
+ // the debugger-specific action bound to the debugger store
+ // since there is no `storeKey`
+ openElementInInspector: actions.openElementInInspectorCommand,
+ highlightDomElement: actions.highlightDomElement,
+ unHighlightDomElement: actions.unHighlightDomElement,
+ setSkipPausing: actions.setSkipPausing,
+ openInspector: actions.openInspector,
+})(DomMutationBreakpoints);
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/EventListeners.css b/devtools/client/debugger/src/components/SecondaryPanes/EventListeners.css
new file mode 100644
index 0000000000..eadc3a917b
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/EventListeners.css
@@ -0,0 +1,155 @@
+/* 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/>. */
+
+.event-listeners-content {
+ padding-block: 4px;
+}
+
+.event-listeners-content ul {
+ padding: 0;
+ list-style-type: none;
+}
+
+.event-listeners-content button:hover,
+.event-listeners-content button:focus {
+ background: none;
+}
+
+.event-listener-group {
+ user-select: none;
+}
+
+.event-listener-header {
+ display: flex;
+ align-items: center;
+}
+
+.event-listener-expand {
+ border: none;
+ background: none;
+ padding: 4px 5px;
+ line-height: 12px;
+ outline-offset: -2px;
+}
+
+.event-listener-expand:hover {
+ background: transparent;
+}
+
+.event-listener-group input[type="checkbox"] {
+ margin: 0;
+ margin-inline-end: 4px;
+}
+
+.event-listener-label {
+ display: flex;
+ align-items: center;
+ padding-inline-end: 10px;
+}
+
+.event-listener-category {
+ padding: 3px 0;
+ line-height: 14px;
+}
+
+.event-listeners-content .arrow {
+ margin-inline-end: 0;
+}
+
+.event-listeners-content .arrow.expanded {
+ transform: rotate(0deg);
+}
+
+.event-listeners-content .arrow.expanded:dir(rtl) {
+ transform: rotate(90deg);
+}
+
+.event-listeners-list {
+ border-block-start: 1px;
+ padding-inline: 18px 20px;
+}
+
+.event-listener-event {
+ display: flex;
+ align-items: center;
+}
+
+.event-listeners-list .event-listener-event {
+ margin-inline-start: 40px;
+}
+
+.event-search-results-list .event-listener-event {
+ padding-inline: 20px;
+}
+
+.event-listener-name {
+ line-height: 14px;
+ padding: 3px 0;
+}
+
+.event-listener-event input {
+ margin-inline: 0 4px;
+ margin-block: 0;
+}
+
+.event-search-container {
+ display: flex;
+ border: 1px solid transparent;
+ border-block-end: 1px solid var(--theme-splitter-color);
+}
+
+.event-search-form {
+ display: flex;
+ flex-grow: 1;
+}
+
+.event-search-input {
+ flex-grow: 1;
+ margin: 0;
+ font-size: inherit;
+ background-color: var(--theme-sidebar-background);
+ border: 0;
+ height: 24px;
+ color: var(--theme-body-color);
+ background-image: url("chrome://devtools/skin/images/filter-small.svg");
+ background-position-x: 4px;
+ background-position-y: 50%;
+ background-repeat: no-repeat;
+ background-size: 12px;
+ -moz-context-properties: fill;
+ fill: var(--theme-icon-dimmed-color);
+ text-align: match-parent;
+ outline-offset: -1px;
+}
+
+:root:dir(ltr) .event-search-input {
+ /* Be explicit about left/right direction to prevent the text/placeholder
+ * from overlapping the background image when the user changes the text
+ * direction manually (e.g. via Ctrl+Shift). */
+ padding-left: 19px;
+ padding-right: 12px;
+}
+
+:root:dir(rtl) .event-search-input {
+ background-position-x: right 4px;
+ padding-right: 19px;
+ padding-left: 12px;
+}
+
+.category-label {
+ color: var(--theme-comment);
+}
+
+.event-search-input::placeholder {
+ color: var(--theme-text-color-alt);
+ opacity: 1;
+}
+
+.event-search-container:focus-within {
+ border: 1px solid var(--theme-highlight-blue);
+}
+
+.devtools-searchinput-clear {
+ margin-inline-end: 8px;
+}
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/EventListeners.js b/devtools/client/debugger/src/components/SecondaryPanes/EventListeners.js
new file mode 100644
index 0000000000..ce7eabf89d
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/EventListeners.js
@@ -0,0 +1,339 @@
+/* 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 React, { Component } from "devtools/client/shared/vendor/react";
+import {
+ div,
+ input,
+ li,
+ ul,
+ span,
+ button,
+ form,
+ label,
+} from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import actions from "../../actions/index";
+import {
+ getActiveEventListeners,
+ getEventListenerBreakpointTypes,
+ getEventListenerExpanded,
+} from "../../selectors/index";
+
+import AccessibleImage from "../shared/AccessibleImage";
+
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+class EventListeners extends Component {
+ state = {
+ searchText: "",
+ focused: false,
+ };
+
+ static get propTypes() {
+ return {
+ activeEventListeners: PropTypes.array.isRequired,
+ addEventListenerExpanded: PropTypes.func.isRequired,
+ addEventListeners: PropTypes.func.isRequired,
+ categories: PropTypes.array.isRequired,
+ expandedCategories: PropTypes.array.isRequired,
+ removeEventListenerExpanded: PropTypes.func.isRequired,
+ removeEventListeners: PropTypes.func.isRequired,
+ };
+ }
+
+ hasMatch(eventOrCategoryName, searchText) {
+ const lowercaseEventOrCategoryName = eventOrCategoryName.toLowerCase();
+ const lowercaseSearchText = searchText.toLowerCase();
+
+ return lowercaseEventOrCategoryName.includes(lowercaseSearchText);
+ }
+
+ getSearchResults() {
+ const { searchText } = this.state;
+ const { categories } = this.props;
+ const searchResults = categories.reduce((results, cat, index) => {
+ const category = categories[index];
+
+ if (this.hasMatch(category.name, searchText)) {
+ results[category.name] = category.events;
+ } else {
+ results[category.name] = category.events.filter(event =>
+ this.hasMatch(event.name, searchText)
+ );
+ }
+
+ return results;
+ }, {});
+
+ return searchResults;
+ }
+
+ onCategoryToggle(category) {
+ const {
+ expandedCategories,
+ removeEventListenerExpanded,
+ addEventListenerExpanded,
+ } = this.props;
+
+ if (expandedCategories.includes(category)) {
+ removeEventListenerExpanded(category);
+ } else {
+ addEventListenerExpanded(category);
+ }
+ }
+
+ onCategoryClick(category, isChecked) {
+ const { addEventListeners, removeEventListeners } = this.props;
+ const eventsIds = category.events.map(event => event.id);
+
+ if (isChecked) {
+ addEventListeners(eventsIds);
+ } else {
+ removeEventListeners(eventsIds);
+ }
+ }
+
+ onEventTypeClick(eventId, isChecked) {
+ const { addEventListeners, removeEventListeners } = this.props;
+ if (isChecked) {
+ addEventListeners([eventId]);
+ } else {
+ removeEventListeners([eventId]);
+ }
+ }
+
+ onInputChange = event => {
+ this.setState({ searchText: event.currentTarget.value });
+ };
+
+ onKeyDown = event => {
+ if (event.key === "Escape") {
+ this.setState({ searchText: "" });
+ }
+ };
+
+ onFocus = event => {
+ this.setState({ focused: true });
+ };
+
+ onBlur = event => {
+ this.setState({ focused: false });
+ };
+
+ renderSearchInput() {
+ const { focused, searchText } = this.state;
+ const placeholder = L10N.getStr("eventListenersHeader1.placeholder");
+ return form(
+ {
+ className: "event-search-form",
+ onSubmit: e => e.preventDefault(),
+ },
+ input({
+ className: classnames("event-search-input", {
+ focused,
+ }),
+ placeholder: placeholder,
+ value: searchText,
+ onChange: this.onInputChange,
+ onKeyDown: this.onKeyDown,
+ onFocus: this.onFocus,
+ onBlur: this.onBlur,
+ })
+ );
+ }
+
+ renderClearSearchButton() {
+ const { searchText } = this.state;
+
+ if (!searchText) {
+ return null;
+ }
+ return button({
+ onClick: () =>
+ this.setState({
+ searchText: "",
+ }),
+ className: "devtools-searchinput-clear",
+ });
+ }
+
+ renderCategoriesList() {
+ const { categories } = this.props;
+ return ul(
+ {
+ className: "event-listeners-list",
+ },
+ categories.map((category, index) => {
+ return li(
+ {
+ className: "event-listener-group",
+ key: index,
+ },
+ this.renderCategoryHeading(category),
+ this.renderCategoryListing(category)
+ );
+ })
+ );
+ }
+
+ renderSearchResultsList() {
+ const searchResults = this.getSearchResults();
+ return ul(
+ {
+ className: "event-search-results-list",
+ },
+ Object.keys(searchResults).map(category => {
+ return searchResults[category].map(event => {
+ return this.renderListenerEvent(event, category);
+ });
+ })
+ );
+ }
+
+ renderCategoryHeading(category) {
+ const { activeEventListeners, expandedCategories } = this.props;
+ const { events } = category;
+
+ const expanded = expandedCategories.includes(category.name);
+ const checked = events.every(({ id }) => activeEventListeners.includes(id));
+ const indeterminate =
+ !checked && events.some(({ id }) => activeEventListeners.includes(id));
+
+ return div(
+ {
+ className: "event-listener-header",
+ },
+ button(
+ {
+ className: "event-listener-expand",
+ onClick: () => this.onCategoryToggle(category.name),
+ },
+ React.createElement(AccessibleImage, {
+ className: classnames("arrow", {
+ expanded,
+ }),
+ })
+ ),
+ label(
+ {
+ className: "event-listener-label",
+ },
+ input({
+ type: "checkbox",
+ value: category.name,
+ onChange: e => {
+ this.onCategoryClick(
+ category,
+ // Clicking an indeterminate checkbox should always have the
+ // effect of disabling any selected items.
+ indeterminate ? false : e.target.checked
+ );
+ },
+ checked: checked,
+ ref: el => el && (el.indeterminate = indeterminate),
+ }),
+ span(
+ {
+ className: "event-listener-category",
+ },
+ category.name
+ )
+ )
+ );
+ }
+
+ renderCategoryListing(category) {
+ const { expandedCategories } = this.props;
+
+ const expanded = expandedCategories.includes(category.name);
+ if (!expanded) {
+ return null;
+ }
+ return ul(
+ null,
+ category.events.map(event => {
+ return this.renderListenerEvent(event, category.name);
+ })
+ );
+ }
+
+ renderCategory(category) {
+ return span(
+ {
+ className: "category-label",
+ },
+ category,
+ " \u25B8 "
+ );
+ }
+
+ renderListenerEvent(event, category) {
+ const { activeEventListeners } = this.props;
+ const { searchText } = this.state;
+ return li(
+ {
+ className: "event-listener-event",
+ key: event.id,
+ },
+ label(
+ {
+ className: "event-listener-label",
+ },
+ input({
+ type: "checkbox",
+ value: event.id,
+ onChange: e => this.onEventTypeClick(event.id, e.target.checked),
+ checked: activeEventListeners.includes(event.id),
+ }),
+ span(
+ {
+ className: "event-listener-name",
+ },
+ searchText ? this.renderCategory(category) : null,
+ event.name
+ )
+ )
+ );
+ }
+
+ render() {
+ const { searchText } = this.state;
+ return div(
+ {
+ className: "event-listeners",
+ },
+ div(
+ {
+ className: "event-search-container",
+ },
+ this.renderSearchInput(),
+ this.renderClearSearchButton()
+ ),
+ div(
+ {
+ className: "event-listeners-content",
+ },
+ searchText
+ ? this.renderSearchResultsList()
+ : this.renderCategoriesList()
+ )
+ );
+ }
+}
+
+const mapStateToProps = state => ({
+ activeEventListeners: getActiveEventListeners(state),
+ categories: getEventListenerBreakpointTypes(state),
+ expandedCategories: getEventListenerExpanded(state),
+});
+
+export default connect(mapStateToProps, {
+ addEventListeners: actions.addEventListenerBreakpoints,
+ removeEventListeners: actions.removeEventListenerBreakpoints,
+ addEventListenerExpanded: actions.addEventListenerExpanded,
+ removeEventListenerExpanded: actions.removeEventListenerExpanded,
+})(EventListeners);
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Expressions.css b/devtools/client/debugger/src/components/SecondaryPanes/Expressions.css
new file mode 100644
index 0000000000..5dcf2622b5
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Expressions.css
@@ -0,0 +1,171 @@
+/* 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/>. */
+
+.expression-input-form {
+ width: 100%;
+}
+
+.input-expression {
+ width: 100%;
+ margin: 0;
+ font-size: inherit;
+ border: 1px;
+ background-color: var(--theme-sidebar-background);
+ height: 24px;
+ padding-inline-start: 19px;
+ padding-inline-end: 12px;
+ color: var(--theme-body-color);
+ outline: 0;
+}
+
+@keyframes shake {
+ 0%,
+ 100% {
+ transform: translateX(0);
+ }
+ 20%,
+ 60% {
+ transform: translateX(-10px);
+ }
+ 40%,
+ 80% {
+ transform: translateX(10px);
+ }
+}
+
+.input-expression::placeholder {
+ color: var(--theme-text-color-alt);
+ opacity: 1;
+}
+
+.input-expression:focus {
+ cursor: text;
+}
+
+.expressions-list .expression-input-container {
+ height: var(--expression-item-height);
+}
+
+.expressions-list .input-expression {
+ /* Prevent vertical bounce when editing an existing Watch Expression */
+ height: 100%;
+}
+
+.expressions-list {
+ /* TODO: add normalize */
+ margin: 0;
+ padding: 4px 0px;
+ overflow-x: auto;
+}
+
+.expression-input-container {
+ display: flex;
+ border: 1px solid transparent;
+}
+
+.expression-input-container.focused {
+ border: 1px solid var(--theme-highlight-blue);
+}
+
+:root.theme-dark .expression-input-container.focused {
+ border: 1px solid var(--blue-50);
+}
+
+.expression-container {
+ padding-top: 3px;
+ padding-bottom: 3px;
+ padding-inline-start: 20px;
+ padding-inline-end: 12px;
+ width: 100%;
+ color: var(--theme-body-color);
+ background-color: var(--theme-body-background);
+ display: block;
+ position: relative;
+ overflow: hidden;
+}
+
+.expression-container > .tree {
+ width: 100%;
+ overflow: hidden;
+}
+
+.expression-container .tree .tree-node[aria-level="1"] {
+ padding-top: 0px;
+ /* keep line-height at 14px to prevent row from shifting upon expansion */
+ line-height: 14px;
+}
+
+.expression-container .tree-node[aria-level="1"] .object-label {
+ font-family: var(--monospace-font-family);
+}
+
+:root.theme-light .expression-container:hover {
+ background-color: var(--search-overlays-semitransparent);
+}
+
+:root.theme-dark .expression-container:hover {
+ background-color: var(--search-overlays-semitransparent);
+}
+
+.tree .tree-node:not(.focused):hover {
+ background-color: transparent;
+}
+
+.expression-container__close-btn {
+ position: absolute;
+ /* hiding button outside of row until hovered or focused */
+ top: -100px;
+}
+
+.expression-container:hover .expression-container__close-btn,
+.expression-container:focus-within .expression-container__close-btn,
+.expression-container__close-btn:focus-within {
+ top: 0;
+}
+
+.expression-content .object-node {
+ padding-inline-start: 0px;
+ cursor: default;
+}
+
+.expressions-list .tree.object-inspector .node.object-node {
+ max-width: calc(100% - 20px);
+ min-width: 0;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+
+.expression-container__close-btn {
+ max-height: 16px;
+ padding-inline-start: 4px;
+}
+
+[dir="ltr"] .expression-container__close-btn {
+ right: 0;
+}
+
+[dir="rtl"] .expression-container__close-btn {
+ left: 0;
+}
+
+.expression-content {
+ display: flex;
+ align-items: center;
+ flex-grow: 1;
+ position: relative;
+}
+
+.expression-content .tree {
+ overflow: hidden;
+ flex-grow: 1;
+ line-height: 15px;
+}
+
+.expression-content .tree-node[data-expandable="false"][aria-level="1"] {
+ padding-inline-start: 0px;
+}
+
+.input-expression:not(:placeholder-shown) {
+ font-family: var(--monospace-font-family);
+}
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Expressions.js b/devtools/client/debugger/src/components/SecondaryPanes/Expressions.js
new file mode 100644
index 0000000000..be05c7327c
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Expressions.js
@@ -0,0 +1,486 @@
+/* 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 React, { Component } from "devtools/client/shared/vendor/react";
+import {
+ div,
+ input,
+ li,
+ ul,
+ form,
+ datalist,
+ option,
+ span,
+} from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import { features } from "../../utils/prefs";
+import AccessibleImage from "../shared/AccessibleImage";
+
+import { objectInspector } from "devtools/client/shared/components/reps/index";
+
+import actions from "../../actions/index";
+import {
+ getExpressions,
+ getAutocompleteMatchset,
+ getSelectedSource,
+ isMapScopesEnabled,
+ getIsCurrentThreadPaused,
+ getSelectedFrame,
+ getOriginalFrameScope,
+ getCurrentThread,
+} from "../../selectors/index";
+import { getExpressionResultGripAndFront } from "../../utils/expressions";
+
+import { CloseButton } from "../shared/Button/index";
+
+const { debounce } = require("resource://devtools/shared/debounce.js");
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+const { ObjectInspector } = objectInspector;
+
+class Expressions extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ editing: false,
+ editIndex: -1,
+ inputValue: "",
+ focused: false,
+ };
+ }
+
+ static get propTypes() {
+ return {
+ addExpression: PropTypes.func.isRequired,
+ autocomplete: PropTypes.func.isRequired,
+ autocompleteMatches: PropTypes.array,
+ clearAutocomplete: PropTypes.func.isRequired,
+ deleteExpression: PropTypes.func.isRequired,
+ expressions: PropTypes.array.isRequired,
+ highlightDomElement: PropTypes.func.isRequired,
+ onExpressionAdded: PropTypes.func.isRequired,
+ openElementInInspector: PropTypes.func.isRequired,
+ openLink: PropTypes.any.isRequired,
+ showInput: PropTypes.bool.isRequired,
+ unHighlightDomElement: PropTypes.func.isRequired,
+ updateExpression: PropTypes.func.isRequired,
+ isOriginalVariableMappingDisabled: PropTypes.bool,
+ isLoadingOriginalVariables: PropTypes.bool,
+ };
+ }
+
+ componentDidMount() {
+ const { showInput } = this.props;
+
+ // Ensures that the input is focused when the "+"
+ // is clicked while the panel is collapsed
+ if (showInput && this._input) {
+ this._input.focus();
+ }
+ }
+
+ clear = () => {
+ this.setState(() => ({
+ editing: false,
+ editIndex: -1,
+ inputValue: "",
+ focused: false,
+ }));
+ };
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ if (this.state.editing) {
+ this.clear();
+ }
+
+ // Ensures that the add watch expression input
+ // is no longer visible when the new watch expression is rendered
+ if (this.props.expressions.length < nextProps.expressions.length) {
+ this.hideInput();
+ }
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ const { editing, inputValue, focused } = this.state;
+ const {
+ expressions,
+ showInput,
+ autocompleteMatches,
+ isLoadingOriginalVariables,
+ isOriginalVariableMappingDisabled,
+ } = this.props;
+
+ return (
+ autocompleteMatches !== nextProps.autocompleteMatches ||
+ expressions !== nextProps.expressions ||
+ isLoadingOriginalVariables !== nextProps.isLoadingOriginalVariables ||
+ isOriginalVariableMappingDisabled !==
+ nextProps.isOriginalVariableMappingDisabled ||
+ editing !== nextState.editing ||
+ inputValue !== nextState.inputValue ||
+ nextProps.showInput !== showInput ||
+ focused !== nextState.focused
+ );
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ const _input = this._input;
+
+ if (!_input) {
+ return;
+ }
+
+ if (!prevState.editing && this.state.editing) {
+ _input.setSelectionRange(0, _input.value.length);
+ _input.focus();
+ } else if (this.props.showInput && !this.state.focused) {
+ _input.focus();
+ }
+ }
+
+ editExpression(expression, index) {
+ this.setState({
+ inputValue: expression.input,
+ editing: true,
+ editIndex: index,
+ });
+ }
+
+ deleteExpression(e, expression) {
+ e.stopPropagation();
+ const { deleteExpression } = this.props;
+ deleteExpression(expression);
+ }
+
+ handleChange = e => {
+ const { target } = e;
+ if (features.autocompleteExpression) {
+ this.findAutocompleteMatches(target.value, target.selectionStart);
+ }
+ this.setState({ inputValue: target.value });
+ };
+
+ findAutocompleteMatches = debounce((value, selectionStart) => {
+ const { autocomplete } = this.props;
+ autocomplete(value, selectionStart);
+ }, 250);
+
+ handleKeyDown = e => {
+ if (e.key === "Escape") {
+ this.clear();
+ }
+ };
+
+ hideInput = () => {
+ this.setState({ focused: false });
+ this.props.onExpressionAdded();
+ };
+
+ createElement = element => {
+ return document.createElement(element);
+ };
+
+ onFocus = () => {
+ this.setState({ focused: true });
+ };
+
+ onBlur() {
+ this.clear();
+ this.hideInput();
+ }
+
+ handleExistingSubmit = async (e, expression) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.props.updateExpression(this.state.inputValue, expression);
+ };
+
+ handleNewSubmit = async e => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ await this.props.addExpression(this.state.inputValue);
+ this.setState({
+ editing: false,
+ editIndex: -1,
+ inputValue: "",
+ });
+
+ this.props.clearAutocomplete();
+ };
+
+ renderExpressionsNotification() {
+ const { isOriginalVariableMappingDisabled, isLoadingOriginalVariables } =
+ this.props;
+
+ if (isOriginalVariableMappingDisabled) {
+ return div(
+ {
+ className: "pane-info no-original-scopes-info",
+ "aria-role": "status",
+ },
+ span(
+ { className: "info icon" },
+ React.createElement(AccessibleImage, { className: "sourcemap" })
+ ),
+ span(
+ { className: "message" },
+ L10N.getStr("expressions.noOriginalScopes")
+ )
+ );
+ }
+
+ if (isLoadingOriginalVariables) {
+ return div(
+ { className: "pane-info" },
+ span(
+ { className: "info icon" },
+ React.createElement(AccessibleImage, { className: "loader" })
+ ),
+ span(
+ { className: "message" },
+ L10N.getStr("scopes.loadingOriginalScopes")
+ )
+ );
+ }
+ return null;
+ }
+
+ renderExpression = (expression, index) => {
+ const {
+ openLink,
+ openElementInInspector,
+ highlightDomElement,
+ unHighlightDomElement,
+ } = this.props;
+
+ const { editing, editIndex } = this.state;
+ const { input: _input, updating } = expression;
+ const isEditingExpr = editing && editIndex === index;
+ if (isEditingExpr) {
+ return this.renderExpressionEditInput(expression);
+ }
+
+ if (updating) {
+ return null;
+ }
+
+ const { expressionResultGrip, expressionResultFront } =
+ getExpressionResultGripAndFront(expression);
+
+ const root = {
+ name: expression.input,
+ path: _input,
+ contents: {
+ value: expressionResultGrip,
+ front: expressionResultFront,
+ },
+ };
+
+ return li(
+ {
+ className: "expression-container",
+ key: _input,
+ title: expression.input,
+ },
+ div(
+ {
+ className: "expression-content",
+ },
+ React.createElement(ObjectInspector, {
+ roots: [root],
+ autoExpandDepth: 0,
+ disableWrap: true,
+ openLink: openLink,
+ createElement: this.createElement,
+ onDoubleClick: (items, { depth }) => {
+ if (depth === 0) {
+ this.editExpression(expression, index);
+ }
+ },
+ onDOMNodeClick: grip => openElementInInspector(grip),
+ onInspectIconClick: grip => openElementInInspector(grip),
+ onDOMNodeMouseOver: grip => highlightDomElement(grip),
+ onDOMNodeMouseOut: grip => unHighlightDomElement(grip),
+ shouldRenderTooltip: true,
+ mayUseCustomFormatter: true,
+ }),
+ div(
+ {
+ className: "expression-container__close-btn",
+ },
+ React.createElement(CloseButton, {
+ handleClick: e => this.deleteExpression(e, expression),
+ tooltip: L10N.getStr("expressions.remove.tooltip"),
+ })
+ )
+ )
+ );
+ };
+
+ renderExpressions() {
+ const { expressions, showInput } = this.props;
+ return React.createElement(
+ React.Fragment,
+ null,
+ ul(
+ {
+ className: "pane expressions-list",
+ },
+ expressions.map(this.renderExpression)
+ ),
+ showInput && this.renderNewExpressionInput()
+ );
+ }
+
+ renderAutoCompleteMatches() {
+ if (!features.autocompleteExpression) {
+ return null;
+ }
+ const { autocompleteMatches } = this.props;
+ if (autocompleteMatches) {
+ return datalist(
+ {
+ id: "autocomplete-matches",
+ },
+ autocompleteMatches.map((match, index) => {
+ return option({
+ key: index,
+ value: match,
+ });
+ })
+ );
+ }
+ return datalist({
+ id: "autocomplete-matches",
+ });
+ }
+
+ renderNewExpressionInput() {
+ const { editing, inputValue, focused } = this.state;
+ return form(
+ {
+ className: classnames(
+ "expression-input-container expression-input-form",
+ { focused }
+ ),
+ onSubmit: this.handleNewSubmit,
+ },
+ input({
+ className: "input-expression",
+ type: "text",
+ placeholder: L10N.getStr("expressions.placeholder"),
+ onChange: this.handleChange,
+ onBlur: this.hideInput,
+ onKeyDown: this.handleKeyDown,
+ onFocus: this.onFocus,
+ value: !editing ? inputValue : "",
+ ref: c => (this._input = c),
+ ...(features.autocompleteExpression && {
+ list: "autocomplete-matches",
+ }),
+ }),
+ this.renderAutoCompleteMatches(),
+ input({
+ type: "submit",
+ style: {
+ display: "none",
+ },
+ })
+ );
+ }
+
+ renderExpressionEditInput(expression) {
+ const { inputValue, editing, focused } = this.state;
+ return form(
+ {
+ key: expression.input,
+ className: classnames(
+ "expression-input-container expression-input-form",
+ {
+ focused,
+ }
+ ),
+ onSubmit: e => this.handleExistingSubmit(e, expression),
+ },
+ input({
+ className: "input-expression",
+ type: "text",
+ onChange: this.handleChange,
+ onBlur: this.clear,
+ onKeyDown: this.handleKeyDown,
+ onFocus: this.onFocus,
+ value: editing ? inputValue : expression.input,
+ ref: c => (this._input = c),
+ ...(features.autocompleteExpression && {
+ list: "autocomplete-matches",
+ }),
+ }),
+ this.renderAutoCompleteMatches(),
+ input({
+ type: "submit",
+ style: {
+ display: "none",
+ },
+ })
+ );
+ }
+
+ render() {
+ const { expressions } = this.props;
+
+ return div(
+ { className: "pane" },
+ this.renderExpressionsNotification(),
+ expressions.length === 0
+ ? this.renderNewExpressionInput()
+ : this.renderExpressions()
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ const selectedFrame = getSelectedFrame(state, getCurrentThread(state));
+ const selectedSource = getSelectedSource(state);
+ const isPaused = getIsCurrentThreadPaused(state);
+ const mapScopesEnabled = isMapScopesEnabled(state);
+ const expressions = getExpressions(state);
+
+ const selectedSourceIsNonPrettyPrintedOriginal =
+ selectedSource?.isOriginal && !selectedSource?.isPrettyPrinted;
+
+ let isOriginalVariableMappingDisabled, isLoadingOriginalVariables;
+
+ if (selectedSourceIsNonPrettyPrintedOriginal) {
+ isOriginalVariableMappingDisabled = isPaused && !mapScopesEnabled;
+ isLoadingOriginalVariables =
+ isPaused &&
+ mapScopesEnabled &&
+ !expressions.length &&
+ !getOriginalFrameScope(state, selectedFrame)?.scope;
+ }
+
+ return {
+ isOriginalVariableMappingDisabled,
+ isLoadingOriginalVariables,
+ autocompleteMatches: getAutocompleteMatchset(state),
+ expressions,
+ };
+};
+
+export default connect(mapStateToProps, {
+ autocomplete: actions.autocomplete,
+ clearAutocomplete: actions.clearAutocomplete,
+ addExpression: actions.addExpression,
+ updateExpression: actions.updateExpression,
+ deleteExpression: actions.deleteExpression,
+ openLink: actions.openLink,
+ openElementInInspector: actions.openElementInInspectorCommand,
+ highlightDomElement: actions.highlightDomElement,
+ unHighlightDomElement: actions.unHighlightDomElement,
+})(Expressions);
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Frames/Frame.js b/devtools/client/debugger/src/components/SecondaryPanes/Frames/Frame.js
new file mode 100644
index 0000000000..0c81d8afb4
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/Frame.js
@@ -0,0 +1,232 @@
+/* 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 React, { Component, memo } from "devtools/client/shared/vendor/react";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+import AccessibleImage from "../../shared/AccessibleImage";
+import { formatDisplayName } from "../../../utils/pause/frames/index";
+import { getFilename, getFileURL } from "../../../utils/source";
+import FrameIndent from "./FrameIndent";
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+function FrameTitle({ frame, options = {}, l10n }) {
+ const displayName = formatDisplayName(frame, options, l10n);
+ return React.createElement(
+ "span",
+ {
+ className: "title",
+ },
+ displayName
+ );
+}
+
+FrameTitle.propTypes = {
+ frame: PropTypes.object.isRequired,
+ options: PropTypes.object.isRequired,
+ l10n: PropTypes.object.isRequired,
+ showFrameContextMenu: PropTypes.func.isRequired,
+};
+
+function getFrameLocation(frame, shouldDisplayOriginalLocation) {
+ if (shouldDisplayOriginalLocation) {
+ return frame.location;
+ }
+ return frame.generatedLocation || frame.location;
+}
+const FrameLocation = memo(
+ ({ frame, displayFullUrl = false, shouldDisplayOriginalLocation }) => {
+ if (frame.library) {
+ return React.createElement(
+ "span",
+ {
+ className: "location",
+ },
+ frame.library,
+ React.createElement(AccessibleImage, {
+ className: `annotation-logo ${frame.library.toLowerCase()}`,
+ })
+ );
+ }
+ const location = getFrameLocation(frame, shouldDisplayOriginalLocation);
+ const filename = displayFullUrl
+ ? getFileURL(location.source, false)
+ : getFilename(location.source);
+ return React.createElement(
+ "span",
+ {
+ className: "location",
+ title: location.source.url,
+ },
+ React.createElement(
+ "span",
+ {
+ className: "filename",
+ },
+ filename
+ ),
+ ":",
+ React.createElement(
+ "span",
+ {
+ className: "line",
+ },
+ location.line
+ )
+ );
+ }
+);
+FrameLocation.displayName = "FrameLocation";
+
+FrameLocation.propTypes = {
+ frame: PropTypes.object.isRequired,
+ displayFullUrl: PropTypes.bool.isRequired,
+};
+
+export default class FrameComponent extends Component {
+ static defaultProps = {
+ hideLocation: false,
+ shouldMapDisplayName: true,
+ disableContextMenu: false,
+ };
+
+ static get propTypes() {
+ return {
+ disableContextMenu: PropTypes.bool.isRequired,
+ displayFullUrl: PropTypes.bool.isRequired,
+ frame: PropTypes.object.isRequired,
+ getFrameTitle: PropTypes.func,
+ hideLocation: PropTypes.bool.isRequired,
+ isInGroup: PropTypes.bool,
+ panel: PropTypes.oneOf(["debugger", "webconsole"]).isRequired,
+ selectFrame: PropTypes.func.isRequired,
+ selectedFrame: PropTypes.object,
+ shouldMapDisplayName: PropTypes.bool.isRequired,
+ shouldDisplayOriginalLocation: PropTypes.bool.isRequired,
+ showFrameContextMenu: PropTypes.func.isRequired,
+ };
+ }
+
+ get isSelectable() {
+ return this.props.panel == "webconsole";
+ }
+
+ get isDebugger() {
+ return this.props.panel == "debugger";
+ }
+
+ onContextMenu(event) {
+ event.stopPropagation();
+ event.preventDefault();
+
+ const { frame } = this.props;
+ this.props.showFrameContextMenu(event, frame);
+ }
+
+ onMouseDown(e, frame, selectedFrame) {
+ if (e.button !== 0) {
+ return;
+ }
+
+ this.props.selectFrame(frame);
+ }
+
+ onKeyUp(event, frame, selectedFrame) {
+ if (event.key != "Enter") {
+ return;
+ }
+
+ this.props.selectFrame(frame);
+ }
+
+ render() {
+ const {
+ frame,
+ selectedFrame,
+ hideLocation,
+ shouldMapDisplayName,
+ displayFullUrl,
+ getFrameTitle,
+ disableContextMenu,
+ shouldDisplayOriginalLocation,
+ isInGroup,
+ } = this.props;
+ const { l10n } = this.context;
+
+ const className = classnames("frame", {
+ selected: selectedFrame && selectedFrame.id === frame.id,
+ });
+
+ const location = getFrameLocation(frame, shouldDisplayOriginalLocation);
+ const title = getFrameTitle
+ ? getFrameTitle(`${getFileURL(location.source, false)}:${location.line}`)
+ : undefined;
+ return React.createElement(
+ "div",
+ {
+ role: "listitem",
+ key: frame.id,
+ className: className,
+ onMouseDown: e => this.onMouseDown(e, frame, selectedFrame),
+ onKeyUp: e => this.onKeyUp(e, frame, selectedFrame),
+ onContextMenu: disableContextMenu ? null : e => this.onContextMenu(e),
+ tabIndex: 0,
+ title: title,
+ },
+ frame.asyncCause &&
+ React.createElement(
+ "span",
+ {
+ className: "location-async-cause",
+ },
+ this.isSelectable && React.createElement(FrameIndent, null),
+ this.isDebugger
+ ? React.createElement(
+ "span",
+ {
+ className: "async-label",
+ },
+ frame.asyncCause
+ )
+ : l10n.getFormatStr("stacktrace.asyncStack", frame.asyncCause),
+ this.isSelectable &&
+ React.createElement("br", {
+ className: "clipboard-only",
+ })
+ ),
+ this.isSelectable &&
+ React.createElement(FrameIndent, {
+ indentLevel: isInGroup ? 2 : 1,
+ }),
+ React.createElement(FrameTitle, {
+ frame,
+ options: {
+ shouldMapDisplayName,
+ },
+ l10n,
+ }),
+ !hideLocation &&
+ React.createElement(
+ "span",
+ {
+ className: "clipboard-only",
+ },
+ " "
+ ),
+ !hideLocation &&
+ React.createElement(FrameLocation, {
+ frame,
+ displayFullUrl,
+ shouldDisplayOriginalLocation,
+ }),
+ this.isSelectable &&
+ React.createElement("br", {
+ className: "clipboard-only",
+ })
+ );
+ }
+}
+
+FrameComponent.displayName = "Frame";
+FrameComponent.contextTypes = { l10n: PropTypes.object };
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Frames/FrameIndent.js b/devtools/client/debugger/src/components/SecondaryPanes/Frames/FrameIndent.js
new file mode 100644
index 0000000000..7eee92ffd1
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/FrameIndent.js
@@ -0,0 +1,18 @@
+/* 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 React from "devtools/client/shared/vendor/react";
+
+export default function FrameIndent({ indentLevel = 1 } = {}) {
+ // \xA0 represents the non breakable space &nbsp;
+ const indentWidth = 4 * indentLevel;
+ const nonBreakableSpaces = "\xA0".repeat(indentWidth);
+ return React.createElement(
+ "span",
+ {
+ className: "frame-indent clipboard-only",
+ },
+ nonBreakableSpaces
+ );
+}
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Frames/Frames.css b/devtools/client/debugger/src/components/SecondaryPanes/Frames/Frames.css
new file mode 100644
index 0000000000..5f57f97e51
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/Frames.css
@@ -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/>. */
+
+.frames [role="list"] {
+ list-style: none;
+ margin: 0;
+ padding: 4px 0;
+}
+
+.frames [role="list"] [role="listitem"] {
+ padding-bottom: 2px;
+ overflow: hidden;
+ display: flex;
+ justify-content: space-between;
+ column-gap: 0.5em;
+ flex-direction: row;
+ align-items: center;
+ margin: 0;
+ max-width: 100%;
+ flex-wrap: wrap;
+}
+
+.frames [role="list"] [role="listitem"] * {
+ user-select: none;
+}
+
+.frames .badge {
+ flex-shrink: 0;
+ margin-inline-end: 10px;
+}
+
+.frames .location {
+ font-weight: normal;
+ margin: 0;
+ flex-grow: 1;
+ max-width: 100%;
+ overflow: hidden;
+ white-space: nowrap;
+ /* Trick to get the ellipsis at the start of the string */
+ text-overflow: ellipsis;
+ direction: rtl;
+}
+
+.call-stack-pane:dir(ltr) .frames .location {
+ padding-right: 10px;
+ text-align: right;
+}
+
+.call-stack-pane:dir(rtl) .frames .location {
+ padding-left: 10px;
+ text-align: left;
+}
+
+.call-stack-pane .location-async-cause {
+ color: var(--theme-comment);
+}
+
+.theme-light .frames .location {
+ color: var(--theme-comment);
+}
+
+:root.theme-dark .frames .location {
+ color: var(--theme-body-color);
+ opacity: 0.6;
+}
+
+.frames .title {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ padding-inline-start: 10px;
+}
+
+.frames-group .title {
+ padding-inline-start: 40px;
+}
+
+.frames [role="list"] [role="listitem"]:hover,
+.frames [role="list"] [role="listitem"]:focus {
+ background-color: var(--theme-toolbar-background-alt);
+}
+
+.frames [role="list"] [role="listitem"]:hover .location-async-cause,
+.frames [role="list"] [role="listitem"]:focus .location-async-cause,
+.frames [role="list"] [role="listitem"]:hover .async-label,
+.frames [role="list"] [role="listitem"]:focus .async-label {
+ background-color: var(--theme-body-background);
+}
+
+.theme-dark .frames [role="list"] [role="listitem"]:focus,
+.theme-dark .frames [role="list"] [role="listitem"]:focus .async-label,
+.theme-dark .frames [role="list"] [role="listitem"]:focus .async-label {
+ background-color: var(--theme-tab-toolbar-background);
+}
+
+.frames [role="list"] [role="listitem"].selected,
+.frames [role="list"] [role="listitem"].selected .async-label {
+ background-color: var(--theme-selection-background);
+ color: white;
+}
+
+.frames [role="list"] [role="listitem"].selected i.annotation-logo svg path {
+ fill: white;
+}
+
+:root.theme-light .frames [role="list"] [role="listitem"].selected .location,
+:root.theme-dark .frames [role="list"] [role="listitem"].selected .location {
+ color: white;
+}
+
+.frames .show-more-container {
+ display: flex;
+ min-height: 24px;
+ padding: 4px 0;
+}
+
+.frames .show-more {
+ text-align: center;
+ padding: 8px 0px;
+ margin: 7px 10px 7px 7px;
+ border: 1px solid var(--theme-splitter-color);
+ background-color: var(--theme-tab-toolbar-background);
+ width: 100%;
+ font-size: inherit;
+ color: inherit;
+}
+
+.frames .show-more:hover {
+ background-color: var(--theme-toolbar-background-hover);
+}
+
+.frames .img.annotation-logo {
+ margin-inline-end: 4px;
+ background-color: currentColor;
+}
+
+/*
+ * We also show the library icon in locations, which are forced to RTL.
+ */
+.frames .location .img.annotation-logo {
+ margin-inline-start: 4px;
+}
+
+/* Some elements are added to the DOM only to be printed into the clipboard
+ when the user copy some elements. We don't want those elements to mess with
+ the layout so we put them outside of the screen
+*/
+.frames .clipboard-only {
+ position: absolute;
+ left: -9999px;
+}
+
+.call-stack-pane [role="listitem"] .location-async-cause {
+ height: 20px;
+ line-height: 20px;
+ color: var(--theme-icon-dimmed-color);
+ display: block;
+ z-index: 4;
+ position: relative;
+ padding-inline-start: 17px;
+ width: 100%;
+ pointer-events: none;
+}
+
+.frames-group .location-async-cause {
+ padding-inline-start: 47px;
+}
+
+.call-stack-pane [role="listitem"] .location-async-cause::after {
+ content: " ";
+ position: absolute;
+ left: 0;
+ z-index: -1;
+ height: 30px;
+ top: 50%;
+ width: 100%;
+ border-top: 1px solid var(--theme-tab-toolbar-background);;
+}
+
+.call-stack-pane .async-label {
+ z-index: 1;
+ background-color: var(--theme-sidebar-background);
+ padding: 0 3px;
+ display: inline-block;
+}
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Frames/Group.css b/devtools/client/debugger/src/components/SecondaryPanes/Frames/Group.css
new file mode 100644
index 0000000000..14dbea9954
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/Group.css
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+.frames-group .group,
+.frames-group .group .location {
+ font-weight: 500;
+ cursor: default;
+ /*
+ * direction:rtl is set in Frames.css to overflow the location text from the
+ * start. Here we need to reset it in order to display the framework icon
+ * after the framework name.
+ */
+ direction: ltr;
+}
+
+.frames-group.expanded .group,
+.frames-group.expanded .group .location {
+ color: var(--theme-highlight-blue);
+}
+
+.frames-group .frames-list {
+ border-top: 1px solid var(--theme-splitter-color);
+ border-bottom: 1px solid var(--theme-splitter-color);
+}
+
+.frames-group.expanded .badge {
+ color: var(--theme-highlight-blue);
+}
+
+.frames-group .img.arrow {
+ margin-inline-start: -1px;
+ margin-inline-end: 4px;
+}
+
+.frames-group .group-description {
+ padding-inline-start: 6px;
+}
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Frames/Group.js b/devtools/client/debugger/src/components/SecondaryPanes/Frames/Group.js
new file mode 100644
index 0000000000..ab9f7073a7
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/Group.js
@@ -0,0 +1,191 @@
+/* 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 React, { Component } from "devtools/client/shared/vendor/react";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+import { getLibraryFromUrl } from "../../../utils/pause/frames/index";
+
+import AccessibleImage from "../../shared/AccessibleImage";
+import FrameComponent from "./Frame";
+import Badge from "../../shared/Badge";
+import FrameIndent from "./FrameIndent";
+
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+function FrameLocation({ frame, expanded }) {
+ const library = frame.library || getLibraryFromUrl(frame);
+ if (!library) {
+ return null;
+ }
+ const arrowClassName = classnames("arrow", {
+ expanded,
+ });
+ return React.createElement(
+ "span",
+ {
+ className: "group-description",
+ },
+ React.createElement(AccessibleImage, {
+ className: arrowClassName,
+ }),
+ React.createElement(AccessibleImage, {
+ className: `annotation-logo ${library.toLowerCase()}`,
+ }),
+ React.createElement(
+ "span",
+ {
+ className: "group-description-name",
+ },
+ library
+ )
+ );
+}
+
+FrameLocation.propTypes = {
+ expanded: PropTypes.any.isRequired,
+ frame: PropTypes.object.isRequired,
+};
+
+FrameLocation.displayName = "FrameLocation";
+
+export default class Group extends Component {
+ constructor(...args) {
+ super(...args);
+ this.state = { expanded: false };
+ }
+
+ static get propTypes() {
+ return {
+ disableContextMenu: PropTypes.bool.isRequired,
+ displayFullUrl: PropTypes.bool.isRequired,
+ getFrameTitle: PropTypes.func,
+ group: PropTypes.array.isRequired,
+ panel: PropTypes.oneOf(["debugger", "webconsole"]).isRequired,
+ selectFrame: PropTypes.func.isRequired,
+ selectLocation: PropTypes.func,
+ selectedFrame: PropTypes.object,
+ showFrameContextMenu: PropTypes.func.isRequired,
+ };
+ }
+
+ get isSelectable() {
+ return this.props.panel == "webconsole";
+ }
+
+ onContextMenu(event) {
+ const { group } = this.props;
+ const frame = group[0];
+ this.props.showFrameContextMenu(event, frame, true);
+ }
+
+ toggleFrames = event => {
+ event.stopPropagation();
+ this.setState(prevState => ({ expanded: !prevState.expanded }));
+ };
+
+ renderFrames() {
+ const {
+ group,
+ selectFrame,
+ selectLocation,
+ selectedFrame,
+ displayFullUrl,
+ getFrameTitle,
+ disableContextMenu,
+ panel,
+ showFrameContextMenu,
+ } = this.props;
+
+ const { expanded } = this.state;
+ if (!expanded) {
+ return null;
+ }
+
+ return React.createElement(
+ "div",
+ {
+ className: "frames-list",
+ },
+ group.map(frame =>
+ React.createElement(FrameComponent, {
+ frame: frame,
+ showFrameContextMenu: showFrameContextMenu,
+ hideLocation: true,
+ key: frame.id,
+ selectedFrame: selectedFrame,
+ selectFrame: selectFrame,
+ selectLocation: selectLocation,
+ shouldMapDisplayName: false,
+ displayFullUrl: displayFullUrl,
+ getFrameTitle: getFrameTitle,
+ disableContextMenu: disableContextMenu,
+ panel: panel,
+ isInGroup: true,
+ })
+ )
+ );
+ }
+
+ renderDescription() {
+ const { l10n } = this.context;
+ const { group } = this.props;
+ const { expanded } = this.state;
+
+ const frame = group[0];
+ const l10NEntry = expanded
+ ? "callStack.group.collapseTooltip"
+ : "callStack.group.expandTooltip";
+ const title = l10n.getFormatStr(l10NEntry, frame.library);
+
+ return React.createElement(
+ "div",
+ {
+ role: "listitem",
+ key: frame.id,
+ className: "group",
+ onClick: this.toggleFrames,
+ tabIndex: 0,
+ title,
+ },
+ this.isSelectable && React.createElement(FrameIndent, null),
+ React.createElement(FrameLocation, {
+ frame,
+ expanded,
+ }),
+ this.isSelectable &&
+ React.createElement(
+ "span",
+ {
+ className: "clipboard-only",
+ },
+ " "
+ ),
+ React.createElement(Badge, { badgeText: this.props.group.length }),
+ this.isSelectable &&
+ React.createElement("br", {
+ className: "clipboard-only",
+ })
+ );
+ }
+
+ render() {
+ const { expanded } = this.state;
+ const { disableContextMenu } = this.props;
+ return React.createElement(
+ "div",
+ {
+ className: classnames("frames-group", {
+ expanded,
+ }),
+ onContextMenu: disableContextMenu ? null : e => this.onContextMenu(e),
+ },
+ this.renderDescription(),
+ this.renderFrames()
+ );
+ }
+}
+
+Group.displayName = "Group";
+Group.contextTypes = { l10n: PropTypes.object };
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Frames/index.js b/devtools/client/debugger/src/components/SecondaryPanes/Frames/index.js
new file mode 100644
index 0000000000..d83b413a01
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/index.js
@@ -0,0 +1,220 @@
+/* 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 React, { Component } from "devtools/client/shared/vendor/react";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+import FrameComponent from "./Frame";
+import Group from "./Group";
+
+import actions from "../../../actions/index";
+import { collapseFrames } from "../../../utils/pause/frames/index";
+
+import {
+ getFrameworkGroupingState,
+ getSelectedFrame,
+ getCurrentThreadFrames,
+ getCurrentThread,
+ getShouldSelectOriginalLocation,
+} from "../../../selectors/index";
+
+const NUM_FRAMES_SHOWN = 7;
+
+class Frames extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ showAllFrames: !!props.disableFrameTruncate,
+ };
+ }
+
+ static get propTypes() {
+ return {
+ disableContextMenu: PropTypes.bool.isRequired,
+ disableFrameTruncate: PropTypes.bool.isRequired,
+ displayFullUrl: PropTypes.bool.isRequired,
+ frames: PropTypes.array.isRequired,
+ frameworkGroupingOn: PropTypes.bool.isRequired,
+ getFrameTitle: PropTypes.func,
+ panel: PropTypes.oneOf(["debugger", "webconsole"]).isRequired,
+ selectFrame: PropTypes.func.isRequired,
+ selectLocation: PropTypes.func,
+ selectedFrame: PropTypes.object,
+ showFrameContextMenu: PropTypes.func,
+ shouldDisplayOriginalLocation: PropTypes.bool,
+ };
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ const {
+ frames,
+ selectedFrame,
+ frameworkGroupingOn,
+ shouldDisplayOriginalLocation,
+ } = this.props;
+ const { showAllFrames } = this.state;
+ return (
+ frames !== nextProps.frames ||
+ selectedFrame !== nextProps.selectedFrame ||
+ showAllFrames !== nextState.showAllFrames ||
+ frameworkGroupingOn !== nextProps.frameworkGroupingOn ||
+ shouldDisplayOriginalLocation !== nextProps.shouldDisplayOriginalLocation
+ );
+ }
+
+ toggleFramesDisplay = () => {
+ this.setState(prevState => ({
+ showAllFrames: !prevState.showAllFrames,
+ }));
+ };
+
+ collapseFrames(frames) {
+ const { frameworkGroupingOn } = this.props;
+ if (!frameworkGroupingOn) {
+ return frames;
+ }
+
+ return collapseFrames(frames);
+ }
+
+ truncateFrames(frames) {
+ const numFramesToShow = this.state.showAllFrames
+ ? frames.length
+ : NUM_FRAMES_SHOWN;
+
+ return frames.slice(0, numFramesToShow);
+ }
+
+ renderFrames(frames) {
+ const {
+ selectFrame,
+ selectLocation,
+ selectedFrame,
+ displayFullUrl,
+ getFrameTitle,
+ disableContextMenu,
+ panel,
+ shouldDisplayOriginalLocation,
+ showFrameContextMenu,
+ } = this.props;
+
+ const framesOrGroups = this.truncateFrames(this.collapseFrames(frames));
+
+ // We're not using a <ul> because it adds new lines before and after when
+ // the user copies the trace. Needed for the console which has several
+ // places where we don't want to have those new lines.
+ return React.createElement(
+ "div",
+ {
+ role: "list",
+ },
+ framesOrGroups.map(frameOrGroup =>
+ frameOrGroup.id
+ ? React.createElement(FrameComponent, {
+ frame: frameOrGroup,
+ showFrameContextMenu: showFrameContextMenu,
+ selectFrame: selectFrame,
+ selectLocation: selectLocation,
+ selectedFrame: selectedFrame,
+ shouldDisplayOriginalLocation: shouldDisplayOriginalLocation,
+ key: String(frameOrGroup.id),
+ displayFullUrl: displayFullUrl,
+ getFrameTitle: getFrameTitle,
+ disableContextMenu: disableContextMenu,
+ panel: panel,
+ })
+ : React.createElement(Group, {
+ group: frameOrGroup,
+ showFrameContextMenu: showFrameContextMenu,
+ selectFrame: selectFrame,
+ selectLocation: selectLocation,
+ selectedFrame: selectedFrame,
+ key: frameOrGroup[0].id,
+ displayFullUrl: displayFullUrl,
+ getFrameTitle: getFrameTitle,
+ disableContextMenu: disableContextMenu,
+ panel: panel,
+ })
+ )
+ );
+ }
+
+ renderToggleButton(frames) {
+ const { l10n } = this.context;
+ const buttonMessage = this.state.showAllFrames
+ ? l10n.getStr("callStack.collapse")
+ : l10n.getStr("callStack.expand");
+
+ frames = this.collapseFrames(frames);
+ if (frames.length <= NUM_FRAMES_SHOWN) {
+ return null;
+ }
+ return React.createElement(
+ "div",
+ {
+ className: "show-more-container",
+ },
+ React.createElement(
+ "button",
+ {
+ className: "show-more",
+ onClick: this.toggleFramesDisplay,
+ },
+ buttonMessage
+ )
+ );
+ }
+
+ render() {
+ const { frames, disableFrameTruncate } = this.props;
+
+ if (!frames) {
+ return React.createElement(
+ "div",
+ {
+ className: "pane frames",
+ },
+ React.createElement(
+ "div",
+ {
+ className: "pane-info empty",
+ },
+ L10N.getStr("callStack.notPaused")
+ )
+ );
+ }
+ return React.createElement(
+ "div",
+ {
+ className: "pane frames",
+ },
+ this.renderFrames(frames),
+ disableFrameTruncate ? null : this.renderToggleButton(frames)
+ );
+ }
+}
+
+Frames.contextTypes = { l10n: PropTypes.object };
+
+const mapStateToProps = state => ({
+ frames: getCurrentThreadFrames(state),
+ frameworkGroupingOn: getFrameworkGroupingState(state),
+ selectedFrame: getSelectedFrame(state, getCurrentThread(state)),
+ shouldDisplayOriginalLocation: getShouldSelectOriginalLocation(state),
+ disableFrameTruncate: false,
+ disableContextMenu: false,
+ displayFullUrl: false,
+});
+
+export default connect(mapStateToProps, {
+ selectFrame: actions.selectFrame,
+ selectLocation: actions.selectLocation,
+ showFrameContextMenu: actions.showFrameContextMenu,
+})(Frames);
+
+// Export the non-connected component in order to use it outside of the debugger
+// panel (e.g. console, netmonitor, …).
+export { Frames };
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Frames/moz.build b/devtools/client/debugger/src/components/SecondaryPanes/Frames/moz.build
new file mode 100644
index 0000000000..54c188ed98
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/moz.build
@@ -0,0 +1,13 @@
+# 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(
+ "Frame.js",
+ "FrameIndent.js",
+ "Group.js",
+ "index.js",
+)
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Frame.spec.js b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Frame.spec.js
new file mode 100644
index 0000000000..e0dfc58a98
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Frame.spec.js
@@ -0,0 +1,110 @@
+/* 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 React from "devtools/client/shared/vendor/react";
+import { shallow, mount } from "enzyme";
+import Frame from "../Frame.js";
+import { makeMockFrame, makeMockSource } from "../../../../utils/test-mockup";
+
+function frameProperties(frame, selectedFrame, overrides = {}) {
+ return {
+ frame,
+ selectedFrame,
+ copyStackTrace: jest.fn(),
+ contextTypes: {},
+ selectFrame: jest.fn(),
+ selectLocation: jest.fn(),
+ toggleBlackBox: jest.fn(),
+ displayFullUrl: false,
+ frameworkGroupingOn: false,
+ panel: "webconsole",
+ toggleFrameworkGrouping: null,
+ restart: jest.fn(),
+ ...overrides,
+ };
+}
+
+function render(frameToSelect = {}, overrides = {}, propsOverrides = {}) {
+ const source = makeMockSource("foo-view.js");
+ const defaultFrame = makeMockFrame("1", source, undefined, 10, "renderFoo");
+
+ const frame = { ...defaultFrame, ...overrides };
+ const selectedFrame = { ...frame, ...frameToSelect };
+
+ const props = frameProperties(frame, selectedFrame, propsOverrides);
+ const component = shallow(React.createElement(Frame, props));
+ return { component, props };
+}
+
+describe("Frame", () => {
+ it("user frame", () => {
+ const { component } = render();
+ expect(component).toMatchSnapshot();
+ });
+
+ it("user frame (not selected)", () => {
+ const { component } = render({ id: "2" });
+ expect(component).toMatchSnapshot();
+ });
+
+ it("library frame", () => {
+ const source = makeMockSource("backbone.js");
+ const backboneFrame = {
+ ...makeMockFrame("3", source, undefined, 12, "updateEvents"),
+ library: "backbone",
+ };
+
+ const { component } = render({ id: "3" }, backboneFrame);
+ expect(component).toMatchSnapshot();
+ });
+
+ it("filename only", () => {
+ const source = makeMockSource(
+ "https://firefox.com/assets/src/js/foo-view.js"
+ );
+ const frame = makeMockFrame("1", source, undefined, 10, "renderFoo");
+
+ const props = frameProperties(frame, null);
+ const component = mount(React.createElement(Frame, props));
+ expect(component.text()).toBe("    renderFoo foo-view.js:10");
+ });
+
+ it("full URL", () => {
+ const url = `https://${"a".repeat(100)}.com/assets/src/js/foo-view.js`;
+ const source = makeMockSource(url);
+ const frame = makeMockFrame("1", source, undefined, 10, "renderFoo");
+
+ const props = frameProperties(frame, null, { displayFullUrl: true });
+ const component = mount(React.createElement(Frame, props));
+ expect(component.text()).toBe(`    renderFoo ${url}:10`);
+ });
+
+ it("renders asyncCause", () => {
+ const url = `https://example.com/async.js`;
+ const source = makeMockSource(url);
+ const frame = makeMockFrame("1", source, undefined, 10, "timeoutFn");
+ frame.asyncCause = "setTimeout handler";
+
+ const props = frameProperties(frame);
+ const component = mount(React.createElement(Frame, props), {
+ context: { l10n: L10N },
+ });
+ expect(component.find(".location-async-cause").text()).toBe(
+ `    (Async: setTimeout handler)`
+ );
+ });
+
+ it("getFrameTitle", () => {
+ const url = `https://${"a".repeat(100)}.com/assets/src/js/foo-view.js`;
+ const source = makeMockSource(url);
+ const frame = makeMockFrame("1", source, undefined, 10, "renderFoo");
+
+ const props = frameProperties(frame, null, {
+ getFrameTitle: x => `Jump to ${x}`,
+ });
+ const component = shallow(React.createElement(Frame, props));
+ expect(component.prop("title")).toBe(`Jump to ${url}:10`);
+ expect(component).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Frames.spec.js b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Frames.spec.js
new file mode 100644
index 0000000000..240e455f75
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Frames.spec.js
@@ -0,0 +1,270 @@
+/* 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 React from "devtools/client/shared/vendor/react";
+import { mount, shallow } from "enzyme";
+import Frames from "../index.js";
+
+function render(overrides = {}) {
+ const defaultProps = {
+ frames: null,
+ selectedFrame: null,
+ frameworkGroupingOn: false,
+ toggleFrameworkGrouping: jest.fn(),
+ contextTypes: {},
+ selectFrame: jest.fn(),
+ toggleBlackBox: jest.fn(),
+ };
+
+ const props = { ...defaultProps, ...overrides };
+ const component = shallow(
+ React.createElement(Frames.WrappedComponent, props),
+ {
+ context: {
+ l10n: L10N,
+ },
+ }
+ );
+
+ return component;
+}
+
+describe("Frames", () => {
+ describe("Supports different number of frames", () => {
+ it("empty frames", () => {
+ const component = render();
+ expect(component).toMatchSnapshot();
+ expect(component.find(".show-more").exists()).toBeFalsy();
+ });
+
+ it("one frame", () => {
+ const frames = [{ id: 1 }];
+ const selectedFrame = frames[0];
+ const component = render({ frames, selectedFrame });
+
+ expect(component.find(".show-more").exists()).toBeFalsy();
+ expect(component).toMatchSnapshot();
+ });
+
+ it("toggling the show more button", () => {
+ const frames = [
+ { id: 1 },
+ { id: 2 },
+ { id: 3 },
+ { id: 4 },
+ { id: 5 },
+ { id: 6 },
+ { id: 7 },
+ { id: 8 },
+ { id: 9 },
+ { id: 10 },
+ ];
+
+ const selectedFrame = frames[0];
+ const component = render({ selectedFrame, frames });
+
+ const getToggleBtn = () => component.find(".show-more");
+ const getFrames = () => component.find("Frame");
+
+ expect(getToggleBtn().text()).toEqual("Expand rows");
+ expect(getFrames()).toHaveLength(7);
+
+ getToggleBtn().simulate("click");
+ expect(getToggleBtn().text()).toEqual("Collapse rows");
+ expect(getFrames()).toHaveLength(10);
+ expect(component).toMatchSnapshot();
+ });
+
+ it("disable frame truncation", () => {
+ const framesNumber = 20;
+ const frames = Array.from({ length: framesNumber }, (_, i) => ({
+ id: i + 1,
+ }));
+
+ const component = render({
+ frames,
+ disableFrameTruncate: true,
+ });
+
+ const getToggleBtn = () => component.find(".show-more");
+ const getFrames = () => component.find("Frame");
+
+ expect(getToggleBtn().exists()).toBeFalsy();
+ expect(getFrames()).toHaveLength(framesNumber);
+
+ expect(component).toMatchSnapshot();
+ });
+
+ it("shows the full URL", () => {
+ const frames = [
+ {
+ id: 1,
+ displayName: "renderFoo",
+ location: {
+ source: {
+ url: "http://myfile.com/mahscripts.js",
+ },
+ line: 55,
+ },
+ },
+ ];
+
+ const component = mount(
+ <Frames.WrappedComponent
+ frames={frames}
+ disableFrameTruncate={true}
+ displayFullUrl={true}
+ />
+ );
+ expect(component.text()).toBe(
+ "renderFoo http://myfile.com/mahscripts.js:55"
+ );
+ });
+
+ it("passes the getFrameTitle prop to the Frame component", () => {
+ const frames = [
+ {
+ id: 1,
+ displayName: "renderFoo",
+ location: {
+ source: {
+ url: "http://myfile.com/mahscripts.js",
+ },
+ line: 55,
+ },
+ },
+ ];
+ const getFrameTitle = () => {};
+ const component = render({ frames, getFrameTitle });
+
+ expect(component.find("Frame").prop("getFrameTitle")).toBe(getFrameTitle);
+ expect(component).toMatchSnapshot();
+ });
+
+ it("passes the getFrameTitle prop to the Group component", () => {
+ const frames = [
+ {
+ id: 1,
+ displayName: "renderFoo",
+ location: {
+ source: {
+ url: "http://myfile.com/mahscripts.js",
+ },
+ line: 55,
+ },
+ },
+ {
+ id: 2,
+ library: "back",
+ displayName: "a",
+ location: {
+ source: {
+ url: "http://myfile.com/back.js",
+ },
+ line: 55,
+ },
+ },
+ {
+ id: 3,
+ library: "back",
+ displayName: "b",
+ location: {
+ source: {
+ url: "http://myfile.com/back.js",
+ },
+ line: 55,
+ },
+ },
+ ];
+ const getFrameTitle = () => {};
+ const component = render({
+ frames,
+ getFrameTitle,
+ frameworkGroupingOn: true,
+ });
+
+ expect(component.find("Group").prop("getFrameTitle")).toBe(getFrameTitle);
+ });
+ });
+
+ describe("Library Frames", () => {
+ it("toggling framework frames", () => {
+ const frames = [
+ { id: 1, location: { source: {} } },
+ { id: 2, library: "back", location: { source: {} } },
+ { id: 3, library: "back", location: { source: {} } },
+ { id: 8, location: { source: {} } },
+ ];
+
+ const selectedFrame = frames[0];
+ const frameworkGroupingOn = false;
+ const component = render({ frames, frameworkGroupingOn, selectedFrame });
+
+ expect(component.find("Frame")).toHaveLength(4);
+ expect(component).toMatchSnapshot();
+
+ component.setProps({ frameworkGroupingOn: true });
+
+ expect(component.find("Frame")).toHaveLength(2);
+ expect(component).toMatchSnapshot();
+ });
+
+ it("groups all the Webpack-related frames", () => {
+ const frames = [
+ { id: "1-appFrame", location: { source: {} } },
+ {
+ id: "2-webpackBootstrapFrame",
+ location: {
+ source: {
+ url: "webpack:///webpack/bootstrap 01d88449ca6e9335a66f",
+ },
+ },
+ },
+ {
+ id: "3-webpackBundleFrame",
+ location: { source: { url: "https://foo.com/bundle.js" } },
+ },
+ {
+ id: "4-webpackBootstrapFrame",
+ location: {
+ source: {
+ url: "webpack:///webpack/bootstrap 01d88449ca6e9335a66f",
+ },
+ },
+ },
+ {
+ id: "5-webpackBundleFrame",
+ location: { source: { url: "https://foo.com/bundle.js" } },
+ },
+ ];
+ const selectedFrame = frames[0];
+ const frameworkGroupingOn = true;
+ const component = render({ frames, frameworkGroupingOn, selectedFrame });
+
+ expect(component).toMatchSnapshot();
+ });
+
+ it("selectable framework frames", () => {
+ const frames = [
+ { id: 1, location: { source: {} } },
+ { id: 2, library: "back", location: { source: {} } },
+ { id: 3, library: "back", location: { source: {} } },
+ { id: 8, location: { source: {} } },
+ ];
+
+ const selectedFrame = frames[0];
+
+ const component = render({
+ frames,
+ frameworkGroupingOn: false,
+ selectedFrame,
+ selectable: true,
+ });
+ expect(component).toMatchSnapshot();
+
+ component.setProps({ frameworkGroupingOn: true });
+ expect(component).toMatchSnapshot();
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Group.spec.js b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Group.spec.js
new file mode 100644
index 0000000000..8d08fa0aed
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Group.spec.js
@@ -0,0 +1,93 @@
+/* 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 React from "devtools/client/shared/vendor/react";
+import { shallow } from "enzyme";
+import Group from "../Group.js";
+import { makeMockFrame, makeMockSource } from "../../../../utils/test-mockup";
+
+function render(overrides = {}) {
+ const frame = { ...makeMockFrame(), displayName: "foo", library: "Back" };
+ const defaultProps = {
+ group: [frame],
+ selectedFrame: frame,
+ frameworkGroupingOn: true,
+ toggleFrameworkGrouping: jest.fn(),
+ selectFrame: jest.fn(),
+ selectLocation: jest.fn(),
+ copyStackTrace: jest.fn(),
+ toggleBlackBox: jest.fn(),
+ disableContextMenu: false,
+ displayFullUrl: false,
+ panel: "webconsole",
+ restart: jest.fn(),
+ };
+
+ const props = { ...defaultProps, ...overrides };
+ const component = shallow(React.createElement(Group, props), {
+ context: { l10n: L10N },
+ });
+ return { component, props };
+}
+
+describe("Group", () => {
+ it("displays a group", () => {
+ const { component } = render();
+ expect(component).toMatchSnapshot();
+ });
+
+ it("passes the getFrameTitle prop to the Frame components", () => {
+ const mahscripts = makeMockSource("http://myfile.com/mahscripts.js");
+ const back = makeMockSource("http://myfile.com/back.js");
+ const group = [
+ {
+ ...makeMockFrame("1", mahscripts, undefined, 55, "renderFoo"),
+ library: "Back",
+ },
+ {
+ ...makeMockFrame("2", back, undefined, 55, "a"),
+ library: "Back",
+ },
+ {
+ ...makeMockFrame("3", back, undefined, 55, "b"),
+ library: "Back",
+ },
+ ];
+ const getFrameTitle = () => {};
+ const { component } = render({ group, getFrameTitle });
+
+ component.setState({ expanded: true });
+
+ const frameComponents = component.find("Frame");
+ expect(frameComponents).toHaveLength(3);
+ frameComponents.forEach(node => {
+ expect(node.prop("getFrameTitle")).toBe(getFrameTitle);
+ });
+ expect(component).toMatchSnapshot();
+ });
+
+ it("renders group with anonymous functions", () => {
+ const mahscripts = makeMockSource("http://myfile.com/mahscripts.js");
+ const back = makeMockSource("http://myfile.com/back.js");
+ const group = [
+ {
+ ...makeMockFrame("1", mahscripts, undefined, 55),
+ library: "Back",
+ },
+ {
+ ...makeMockFrame("2", back, undefined, 55),
+ library: "Back",
+ },
+ {
+ ...makeMockFrame("3", back, undefined, 55),
+ library: "Back",
+ },
+ ];
+
+ const { component } = render({ group });
+ expect(component).toMatchSnapshot();
+ component.setState({ expanded: true });
+ expect(component).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/__snapshots__/Frame.spec.js.snap b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/__snapshots__/Frame.spec.js.snap
new file mode 100644
index 0000000000..90a5b1f906
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/__snapshots__/Frame.spec.js.snap
@@ -0,0 +1,1172 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Frame getFrameTitle 1`] = `
+<div
+ className="frame"
+ key="1"
+ onContextMenu={[Function]}
+ onKeyUp={[Function]}
+ onMouseDown={[Function]}
+ role="listitem"
+ tabIndex={0}
+ title="Jump to https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com/assets/src/js/foo-view.js:10"
+>
+ <FrameIndent
+ indentLevel={1}
+ />
+ <FrameTitle
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "renderFoo",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 10,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com",
+ "path": "/assets/src/js/foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com/assets/src/js/foo-view.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com",
+ "path": "/assets/src/js/foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com/assets/src/js/foo-view.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "1",
+ "index": 0,
+ "location": Object {
+ "column": undefined,
+ "line": 10,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com",
+ "path": "/assets/src/js/foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com/assets/src/js/foo-view.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com",
+ "path": "/assets/src/js/foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com/assets/src/js/foo-view.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com",
+ "path": "/assets/src/js/foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com/assets/src/js/foo-view.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ options={
+ Object {
+ "shouldMapDisplayName": true,
+ }
+ }
+ />
+ <span
+ className="clipboard-only"
+ >
+
+ </span>
+ <FrameLocation
+ displayFullUrl={false}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "renderFoo",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 10,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com",
+ "path": "/assets/src/js/foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com/assets/src/js/foo-view.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com",
+ "path": "/assets/src/js/foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com/assets/src/js/foo-view.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "1",
+ "index": 0,
+ "location": Object {
+ "column": undefined,
+ "line": 10,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com",
+ "path": "/assets/src/js/foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com/assets/src/js/foo-view.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com",
+ "path": "/assets/src/js/foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com/assets/src/js/foo-view.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com",
+ "path": "/assets/src/js/foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com/assets/src/js/foo-view.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ />
+ <br
+ className="clipboard-only"
+ />
+</div>
+`;
+
+exports[`Frame library frame 1`] = `
+<div
+ className="frame selected"
+ key="3"
+ onContextMenu={[Function]}
+ onKeyUp={[Function]}
+ onMouseDown={[Function]}
+ role="listitem"
+ tabIndex={0}
+>
+ <FrameIndent
+ indentLevel={1}
+ />
+ <FrameTitle
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "updateEvents",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 12,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "backbone.js",
+ "group": "",
+ "path": "backbone.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "backbone.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "backbone.js",
+ "group": "",
+ "path": "backbone.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "backbone.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "3",
+ "index": 0,
+ "library": "backbone",
+ "location": Object {
+ "column": undefined,
+ "line": 12,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "backbone.js",
+ "group": "",
+ "path": "backbone.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "backbone.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "backbone.js",
+ "group": "",
+ "path": "backbone.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "backbone.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "backbone.js",
+ "group": "",
+ "path": "backbone.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "backbone.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ options={
+ Object {
+ "shouldMapDisplayName": true,
+ }
+ }
+ />
+ <span
+ className="clipboard-only"
+ >
+
+ </span>
+ <FrameLocation
+ displayFullUrl={false}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "updateEvents",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 12,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "backbone.js",
+ "group": "",
+ "path": "backbone.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "backbone.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "backbone.js",
+ "group": "",
+ "path": "backbone.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "backbone.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "3",
+ "index": 0,
+ "library": "backbone",
+ "location": Object {
+ "column": undefined,
+ "line": 12,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "backbone.js",
+ "group": "",
+ "path": "backbone.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "backbone.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "backbone.js",
+ "group": "",
+ "path": "backbone.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "backbone.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "backbone.js",
+ "group": "",
+ "path": "backbone.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "backbone.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ />
+ <br
+ className="clipboard-only"
+ />
+</div>
+`;
+
+exports[`Frame user frame (not selected) 1`] = `
+<div
+ className="frame"
+ key="1"
+ onContextMenu={[Function]}
+ onKeyUp={[Function]}
+ onMouseDown={[Function]}
+ role="listitem"
+ tabIndex={0}
+>
+ <FrameIndent
+ indentLevel={1}
+ />
+ <FrameTitle
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "renderFoo",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 10,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "1",
+ "index": 0,
+ "location": Object {
+ "column": undefined,
+ "line": 10,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ options={
+ Object {
+ "shouldMapDisplayName": true,
+ }
+ }
+ />
+ <span
+ className="clipboard-only"
+ >
+
+ </span>
+ <FrameLocation
+ displayFullUrl={false}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "renderFoo",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 10,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "1",
+ "index": 0,
+ "location": Object {
+ "column": undefined,
+ "line": 10,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ />
+ <br
+ className="clipboard-only"
+ />
+</div>
+`;
+
+exports[`Frame user frame 1`] = `
+<div
+ className="frame selected"
+ key="1"
+ onContextMenu={[Function]}
+ onKeyUp={[Function]}
+ onMouseDown={[Function]}
+ role="listitem"
+ tabIndex={0}
+>
+ <FrameIndent
+ indentLevel={1}
+ />
+ <FrameTitle
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "renderFoo",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 10,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "1",
+ "index": 0,
+ "location": Object {
+ "column": undefined,
+ "line": 10,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ options={
+ Object {
+ "shouldMapDisplayName": true,
+ }
+ }
+ />
+ <span
+ className="clipboard-only"
+ >
+
+ </span>
+ <FrameLocation
+ displayFullUrl={false}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "renderFoo",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 10,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "1",
+ "index": 0,
+ "location": Object {
+ "column": undefined,
+ "line": 10,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "foo-view.js",
+ "group": "",
+ "path": "foo-view.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "foo-view.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ />
+ <br
+ className="clipboard-only"
+ />
+</div>
+`;
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/__snapshots__/Frames.spec.js.snap b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/__snapshots__/Frames.spec.js.snap
new file mode 100644
index 0000000000..d1068b1aa0
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/__snapshots__/Frames.spec.js.snap
@@ -0,0 +1,1001 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Frames Library Frames groups all the Webpack-related frames 1`] = `
+<div
+ className="pane frames"
+>
+ <div
+ role="list"
+ >
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": "1-appFrame",
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ hideLocation={false}
+ key="1-appFrame"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": "1-appFrame",
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Group
+ group={
+ Array [
+ Object {
+ "id": "2-webpackBootstrapFrame",
+ "location": Object {
+ "source": Object {
+ "url": "webpack:///webpack/bootstrap 01d88449ca6e9335a66f",
+ },
+ },
+ },
+ Object {
+ "id": "3-webpackBundleFrame",
+ "location": Object {
+ "source": Object {
+ "url": "https://foo.com/bundle.js",
+ },
+ },
+ },
+ Object {
+ "id": "4-webpackBootstrapFrame",
+ "location": Object {
+ "source": Object {
+ "url": "webpack:///webpack/bootstrap 01d88449ca6e9335a66f",
+ },
+ },
+ },
+ Object {
+ "id": "5-webpackBundleFrame",
+ "location": Object {
+ "source": Object {
+ "url": "https://foo.com/bundle.js",
+ },
+ },
+ },
+ ]
+ }
+ key="2-webpackBootstrapFrame"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": "1-appFrame",
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ />
+ </div>
+</div>
+`;
+
+exports[`Frames Library Frames selectable framework frames 1`] = `
+<div
+ className="pane frames"
+>
+ <div
+ role="list"
+ >
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ hideLocation={false}
+ key="1"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 2,
+ "library": "back",
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ hideLocation={false}
+ key="2"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 3,
+ "library": "back",
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ hideLocation={false}
+ key="3"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 8,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ hideLocation={false}
+ key="8"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ </div>
+</div>
+`;
+
+exports[`Frames Library Frames selectable framework frames 2`] = `
+<div
+ className="pane frames"
+>
+ <div
+ role="list"
+ >
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ hideLocation={false}
+ key="1"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Group
+ group={
+ Array [
+ Object {
+ "id": 2,
+ "library": "back",
+ "location": Object {
+ "source": Object {},
+ },
+ },
+ Object {
+ "id": 3,
+ "library": "back",
+ "location": Object {
+ "source": Object {},
+ },
+ },
+ ]
+ }
+ key="2"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 8,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ hideLocation={false}
+ key="8"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ </div>
+</div>
+`;
+
+exports[`Frames Library Frames toggling framework frames 1`] = `
+<div
+ className="pane frames"
+>
+ <div
+ role="list"
+ >
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ hideLocation={false}
+ key="1"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 2,
+ "library": "back",
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ hideLocation={false}
+ key="2"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 3,
+ "library": "back",
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ hideLocation={false}
+ key="3"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 8,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ hideLocation={false}
+ key="8"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ </div>
+</div>
+`;
+
+exports[`Frames Library Frames toggling framework frames 2`] = `
+<div
+ className="pane frames"
+>
+ <div
+ role="list"
+ >
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ hideLocation={false}
+ key="1"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Group
+ group={
+ Array [
+ Object {
+ "id": 2,
+ "library": "back",
+ "location": Object {
+ "source": Object {},
+ },
+ },
+ Object {
+ "id": 3,
+ "library": "back",
+ "location": Object {
+ "source": Object {},
+ },
+ },
+ ]
+ }
+ key="2"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 8,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ hideLocation={false}
+ key="8"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ "location": Object {
+ "source": Object {},
+ },
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ </div>
+</div>
+`;
+
+exports[`Frames Supports different number of frames disable frame truncation 1`] = `
+<div
+ className="pane frames"
+>
+ <div
+ role="list"
+ >
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 1,
+ }
+ }
+ hideLocation={false}
+ key="1"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 2,
+ }
+ }
+ hideLocation={false}
+ key="2"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 3,
+ }
+ }
+ hideLocation={false}
+ key="3"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 4,
+ }
+ }
+ hideLocation={false}
+ key="4"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 5,
+ }
+ }
+ hideLocation={false}
+ key="5"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 6,
+ }
+ }
+ hideLocation={false}
+ key="6"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 7,
+ }
+ }
+ hideLocation={false}
+ key="7"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 8,
+ }
+ }
+ hideLocation={false}
+ key="8"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 9,
+ }
+ }
+ hideLocation={false}
+ key="9"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 10,
+ }
+ }
+ hideLocation={false}
+ key="10"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 11,
+ }
+ }
+ hideLocation={false}
+ key="11"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 12,
+ }
+ }
+ hideLocation={false}
+ key="12"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 13,
+ }
+ }
+ hideLocation={false}
+ key="13"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 14,
+ }
+ }
+ hideLocation={false}
+ key="14"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 15,
+ }
+ }
+ hideLocation={false}
+ key="15"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 16,
+ }
+ }
+ hideLocation={false}
+ key="16"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 17,
+ }
+ }
+ hideLocation={false}
+ key="17"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 18,
+ }
+ }
+ hideLocation={false}
+ key="18"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 19,
+ }
+ }
+ hideLocation={false}
+ key="19"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 20,
+ }
+ }
+ hideLocation={false}
+ key="20"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ </div>
+</div>
+`;
+
+exports[`Frames Supports different number of frames empty frames 1`] = `
+<div
+ className="pane frames"
+>
+ <div
+ className="pane-info empty"
+ >
+ Not paused
+ </div>
+</div>
+`;
+
+exports[`Frames Supports different number of frames one frame 1`] = `
+<div
+ className="pane frames"
+>
+ <div
+ role="list"
+ >
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 1,
+ }
+ }
+ hideLocation={false}
+ key="1"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ </div>
+</div>
+`;
+
+exports[`Frames Supports different number of frames passes the getFrameTitle prop to the Frame component 1`] = `
+<div
+ className="pane frames"
+>
+ <div
+ role="list"
+ >
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "displayName": "renderFoo",
+ "id": 1,
+ "location": Object {
+ "line": 55,
+ "source": Object {
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ },
+ }
+ }
+ getFrameTitle={[Function]}
+ hideLocation={false}
+ key="1"
+ selectFrame={[MockFunction]}
+ selectedFrame={null}
+ shouldMapDisplayName={true}
+ />
+ </div>
+</div>
+`;
+
+exports[`Frames Supports different number of frames toggling the show more button 1`] = `
+<div
+ className="pane frames"
+>
+ <div
+ role="list"
+ >
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 1,
+ }
+ }
+ hideLocation={false}
+ key="1"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 2,
+ }
+ }
+ hideLocation={false}
+ key="2"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 3,
+ }
+ }
+ hideLocation={false}
+ key="3"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 4,
+ }
+ }
+ hideLocation={false}
+ key="4"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 5,
+ }
+ }
+ hideLocation={false}
+ key="5"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 6,
+ }
+ }
+ hideLocation={false}
+ key="6"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 7,
+ }
+ }
+ hideLocation={false}
+ key="7"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 8,
+ }
+ }
+ hideLocation={false}
+ key="8"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 9,
+ }
+ }
+ hideLocation={false}
+ key="9"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ <Frame
+ disableContextMenu={false}
+ frame={
+ Object {
+ "id": 10,
+ }
+ }
+ hideLocation={false}
+ key="10"
+ selectFrame={[MockFunction]}
+ selectedFrame={
+ Object {
+ "id": 1,
+ }
+ }
+ shouldMapDisplayName={true}
+ />
+ </div>
+ <div
+ className="show-more-container"
+ >
+ <button
+ className="show-more"
+ onClick={[Function]}
+ >
+ Collapse rows
+ </button>
+ </div>
+</div>
+`;
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/__snapshots__/Group.spec.js.snap b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/__snapshots__/Group.spec.js.snap
new file mode 100644
index 0000000000..97c4c2ad1a
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/__snapshots__/Group.spec.js.snap
@@ -0,0 +1,2286 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Group displays a group 1`] = `
+<div
+ className="frames-group"
+ onContextMenu={[Function]}
+>
+ <div
+ className="group"
+ key="frame"
+ onClick={[Function]}
+ role="listitem"
+ tabIndex={0}
+ title="Show Back frames"
+ >
+ <FrameIndent />
+ <FrameLocation
+ expanded={false}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "foo",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 4,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "frame",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "column": undefined,
+ "line": 4,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ />
+ <span
+ className="clipboard-only"
+ >
+
+ </span>
+ <Badge
+ badgeText={1}
+ />
+ <br
+ className="clipboard-only"
+ />
+ </div>
+</div>
+`;
+
+exports[`Group passes the getFrameTitle prop to the Frame components 1`] = `
+<div
+ className="frames-group expanded"
+ onContextMenu={[Function]}
+>
+ <div
+ className="group"
+ key="1"
+ onClick={[Function]}
+ role="listitem"
+ tabIndex={0}
+ title="Collapse Back frames"
+ >
+ <FrameIndent />
+ <FrameLocation
+ expanded={true}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "renderFoo",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "1",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ />
+ <span
+ className="clipboard-only"
+ >
+
+ </span>
+ <Badge
+ badgeText={3}
+ />
+ <br
+ className="clipboard-only"
+ />
+ </div>
+ <div
+ className="frames-list"
+ >
+ <Frame
+ disableContextMenu={false}
+ displayFullUrl={false}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "renderFoo",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "1",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ getFrameTitle={[Function]}
+ hideLocation={true}
+ isInGroup={true}
+ key="1"
+ panel="webconsole"
+ selectFrame={[MockFunction]}
+ selectLocation={[MockFunction]}
+ selectedFrame={
+ Object {
+ "asyncCause": null,
+ "displayName": "foo",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 4,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "frame",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "column": undefined,
+ "line": 4,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ shouldMapDisplayName={false}
+ />
+ <Frame
+ disableContextMenu={false}
+ displayFullUrl={false}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "a",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "2",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ getFrameTitle={[Function]}
+ hideLocation={true}
+ isInGroup={true}
+ key="2"
+ panel="webconsole"
+ selectFrame={[MockFunction]}
+ selectLocation={[MockFunction]}
+ selectedFrame={
+ Object {
+ "asyncCause": null,
+ "displayName": "foo",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 4,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "frame",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "column": undefined,
+ "line": 4,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ shouldMapDisplayName={false}
+ />
+ <Frame
+ disableContextMenu={false}
+ displayFullUrl={false}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "b",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "3",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ getFrameTitle={[Function]}
+ hideLocation={true}
+ isInGroup={true}
+ key="3"
+ panel="webconsole"
+ selectFrame={[MockFunction]}
+ selectLocation={[MockFunction]}
+ selectedFrame={
+ Object {
+ "asyncCause": null,
+ "displayName": "foo",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 4,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "frame",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "column": undefined,
+ "line": 4,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ shouldMapDisplayName={false}
+ />
+ </div>
+</div>
+`;
+
+exports[`Group renders group with anonymous functions 1`] = `
+<div
+ className="frames-group"
+ onContextMenu={[Function]}
+>
+ <div
+ className="group"
+ key="1"
+ onClick={[Function]}
+ role="listitem"
+ tabIndex={0}
+ title="Show Back frames"
+ >
+ <FrameIndent />
+ <FrameLocation
+ expanded={false}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "display-1",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "1",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ />
+ <span
+ className="clipboard-only"
+ >
+
+ </span>
+ <Badge
+ badgeText={3}
+ />
+ <br
+ className="clipboard-only"
+ />
+ </div>
+</div>
+`;
+
+exports[`Group renders group with anonymous functions 2`] = `
+<div
+ className="frames-group expanded"
+ onContextMenu={[Function]}
+>
+ <div
+ className="group"
+ key="1"
+ onClick={[Function]}
+ role="listitem"
+ tabIndex={0}
+ title="Collapse Back frames"
+ >
+ <FrameIndent />
+ <FrameLocation
+ expanded={true}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "display-1",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "1",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ />
+ <span
+ className="clipboard-only"
+ >
+
+ </span>
+ <Badge
+ badgeText={3}
+ />
+ <br
+ className="clipboard-only"
+ />
+ </div>
+ <div
+ className="frames-list"
+ >
+ <Frame
+ disableContextMenu={false}
+ displayFullUrl={false}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "display-1",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "1",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "mahscripts.js",
+ "group": "myfile.com",
+ "path": "/mahscripts.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/mahscripts.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ hideLocation={true}
+ isInGroup={true}
+ key="1"
+ panel="webconsole"
+ selectFrame={[MockFunction]}
+ selectLocation={[MockFunction]}
+ selectedFrame={
+ Object {
+ "asyncCause": null,
+ "displayName": "foo",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 4,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "frame",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "column": undefined,
+ "line": 4,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ shouldMapDisplayName={false}
+ />
+ <Frame
+ disableContextMenu={false}
+ displayFullUrl={false}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "display-2",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "2",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ hideLocation={true}
+ isInGroup={true}
+ key="2"
+ panel="webconsole"
+ selectFrame={[MockFunction]}
+ selectLocation={[MockFunction]}
+ selectedFrame={
+ Object {
+ "asyncCause": null,
+ "displayName": "foo",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 4,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "frame",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "column": undefined,
+ "line": 4,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ shouldMapDisplayName={false}
+ />
+ <Frame
+ disableContextMenu={false}
+ displayFullUrl={false}
+ frame={
+ Object {
+ "asyncCause": null,
+ "displayName": "display-3",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "3",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "column": undefined,
+ "line": 55,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "js",
+ "filename": "back.js",
+ "group": "myfile.com",
+ "path": "/back.js",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "http://myfile.com/back.js",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ hideLocation={true}
+ isInGroup={true}
+ key="3"
+ panel="webconsole"
+ selectFrame={[MockFunction]}
+ selectLocation={[MockFunction]}
+ selectedFrame={
+ Object {
+ "asyncCause": null,
+ "displayName": "foo",
+ "generatedLocation": Object {
+ "column": undefined,
+ "line": 4,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "id": "frame",
+ "index": 0,
+ "library": "Back",
+ "location": Object {
+ "column": undefined,
+ "line": 4,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "sourceActor": Object {
+ "actor": "source-actor",
+ "id": "source-actor",
+ "source": "source",
+ "sourceObject": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "sourceActorId": "source-actor",
+ },
+ "scope": Object {
+ "actor": "scope-actor",
+ "bindings": Object {
+ "arguments": Array [],
+ "variables": Object {},
+ },
+ "function": null,
+ "object": null,
+ "parent": null,
+ "scopeKind": "",
+ "type": "block",
+ },
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "source",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ "state": "on-stack",
+ "this": Object {},
+ "thread": "FakeThread",
+ "type": "call",
+ }
+ }
+ shouldMapDisplayName={false}
+ />
+ </div>
+</div>
+`;
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Scopes.css b/devtools/client/debugger/src/components/SecondaryPanes/Scopes.css
new file mode 100644
index 0000000000..07ebd44048
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Scopes.css
@@ -0,0 +1,100 @@
+/* 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/>. */
+
+.secondary-panes .map-scopes-header {
+ padding-inline-end: 3px;
+}
+
+.secondary-panes .header-buttons .img.shortcuts {
+ width: 14px;
+ height: 14px;
+ /* Better vertical centering of the icon */
+ margin-top: -2px;
+}
+
+.scopes-content .node.object-node {
+ padding-inline-start: 7px;
+}
+
+.scopes-content .toggle-map-scopes a.mdn {
+ padding-inline-start: 3px;
+}
+
+.scopes-content .toggle-map-scopes .img.shortcuts {
+ background: var(--theme-comment);
+}
+
+.scopes-content .object-node.default-property {
+ opacity: 0.6;
+}
+
+.scopes-content .object-node {
+ padding-inline-start: 20px;
+}
+
+html[dir="rtl"] .scopes-content .object-node {
+ padding-right: 4px;
+}
+
+.scopes-content .object-label {
+ color: var(--theme-highlight-blue);
+}
+
+.objectBox-object,
+.objectBox-text,
+.objectBox-table,
+.objectLink-textNode,
+.objectLink-event,
+.objectLink-eventLog,
+.objectLink-regexp,
+.objectLink-object,
+.objectLink-Date,
+.theme-dark .objectBox-object,
+.theme-light .objectBox-object {
+ white-space: nowrap;
+}
+
+.scopes-pane ._content {
+ overflow: auto;
+}
+
+.scopes-list {
+ padding: 4px 0px;
+}
+
+.scopes-list .function-signature {
+ display: inline-block;
+}
+
+.scopes-list .scope-type-toggle {
+ text-align: center;
+ padding-top: 10px;
+ padding-bottom: 10px;
+}
+
+.scopes-list .scope-type-toggle button {
+ /* Override color so that the link doesn't turn purple */
+ color: var(--theme-body-color);
+ font-size: inherit;
+ text-decoration: underline;
+ cursor: pointer;
+}
+
+.scopes-list .scope-type-toggle button:hover {
+ background: transparent;
+}
+
+.scopes-list .tree.object-inspector .node.object-node {
+ display: flex;
+ align-items: center;
+}
+
+.scopes-list .tree.object-inspector .tree-node button.arrow,
+.scopes-list button.invoke-getter {
+ margin-top: 2px;
+}
+
+.scopes-list .tree {
+ line-height: 15px;
+}
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Scopes.js b/devtools/client/debugger/src/components/SecondaryPanes/Scopes.js
new file mode 100644
index 0000000000..135decd254
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Scopes.js
@@ -0,0 +1,413 @@
+/* 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 React, { PureComponent } from "devtools/client/shared/vendor/react";
+import {
+ div,
+ button,
+ span,
+} from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import AccessibleImage from "../shared/AccessibleImage";
+import { showMenu } from "../../context-menu/menu";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import actions from "../../actions/index";
+
+import {
+ getSelectedFrame,
+ getCurrentThread,
+ getSelectedSource,
+ getGeneratedFrameScope,
+ getOriginalFrameScope,
+ getPauseReason,
+ isMapScopesEnabled,
+ getLastExpandedScopes,
+ getIsCurrentThreadPaused,
+} from "../../selectors/index";
+import {
+ getScopesItemsForSelectedFrame,
+ getScopeItemPath,
+} from "../../utils/pause/scopes";
+import { clientCommands } from "../../client/firefox";
+
+import { objectInspector } from "devtools/client/shared/components/reps/index";
+const { ObjectInspector } = objectInspector;
+
+class Scopes extends PureComponent {
+ constructor(props) {
+ const { why, selectedFrame, originalFrameScopes, generatedFrameScopes } =
+ props;
+
+ super(props);
+
+ this.state = {
+ originalScopes: getScopesItemsForSelectedFrame(
+ why,
+ selectedFrame,
+ originalFrameScopes
+ ),
+ generatedScopes: getScopesItemsForSelectedFrame(
+ why,
+ selectedFrame,
+ generatedFrameScopes
+ ),
+ };
+ }
+
+ static get propTypes() {
+ return {
+ addWatchpoint: PropTypes.func.isRequired,
+ expandedScopes: PropTypes.array.isRequired,
+ generatedFrameScopes: PropTypes.object,
+ highlightDomElement: PropTypes.func.isRequired,
+ isLoading: PropTypes.bool.isRequired,
+ isPaused: PropTypes.bool.isRequired,
+ mapScopesEnabled: PropTypes.bool.isRequired,
+ openElementInInspector: PropTypes.func.isRequired,
+ openLink: PropTypes.func.isRequired,
+ originalFrameScopes: PropTypes.object,
+ removeWatchpoint: PropTypes.func.isRequired,
+ setExpandedScope: PropTypes.func.isRequired,
+ unHighlightDomElement: PropTypes.func.isRequired,
+ why: PropTypes.object.isRequired,
+ selectedFrame: PropTypes.object,
+ toggleMapScopes: PropTypes.func.isRequired,
+ selectedSource: PropTypes.object.isRequired,
+ };
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ const {
+ selectedFrame,
+ originalFrameScopes,
+ generatedFrameScopes,
+ isPaused,
+ selectedSource,
+ } = this.props;
+ const isPausedChanged = isPaused !== nextProps.isPaused;
+ const selectedFrameChanged = selectedFrame !== nextProps.selectedFrame;
+ const originalFrameScopesChanged =
+ originalFrameScopes !== nextProps.originalFrameScopes;
+ const generatedFrameScopesChanged =
+ generatedFrameScopes !== nextProps.generatedFrameScopes;
+ const selectedSourceChanged = selectedSource !== nextProps.selectedSource;
+
+ if (
+ isPausedChanged ||
+ selectedFrameChanged ||
+ originalFrameScopesChanged ||
+ generatedFrameScopesChanged ||
+ selectedSourceChanged
+ ) {
+ this.setState({
+ originalScopes: getScopesItemsForSelectedFrame(
+ nextProps.why,
+ nextProps.selectedFrame,
+ nextProps.originalFrameScopes
+ ),
+ generatedScopes: getScopesItemsForSelectedFrame(
+ nextProps.why,
+ nextProps.selectedFrame,
+ nextProps.generatedFrameScopes
+ ),
+ });
+ }
+ }
+
+ onContextMenu = (event, item) => {
+ const { addWatchpoint, removeWatchpoint } = this.props;
+
+ if (!item.parent || !item.contents.configurable) {
+ return;
+ }
+
+ if (!item.contents || item.contents.watchpoint) {
+ const removeWatchpointLabel = L10N.getStr("watchpoints.removeWatchpoint");
+
+ const removeWatchpointItem = {
+ id: "node-menu-remove-watchpoint",
+ label: removeWatchpointLabel,
+ disabled: false,
+ click: () => removeWatchpoint(item),
+ };
+
+ const menuItems = [removeWatchpointItem];
+ showMenu(event, menuItems);
+ return;
+ }
+
+ const addSetWatchpointLabel = L10N.getStr("watchpoints.setWatchpoint");
+ const addGetWatchpointLabel = L10N.getStr("watchpoints.getWatchpoint");
+ const addGetOrSetWatchpointLabel = L10N.getStr(
+ "watchpoints.getOrSetWatchpoint"
+ );
+ const watchpointsSubmenuLabel = L10N.getStr("watchpoints.submenu");
+
+ const addSetWatchpointItem = {
+ id: "node-menu-add-set-watchpoint",
+ label: addSetWatchpointLabel,
+ disabled: false,
+ click: () => addWatchpoint(item, "set"),
+ };
+
+ const addGetWatchpointItem = {
+ id: "node-menu-add-get-watchpoint",
+ label: addGetWatchpointLabel,
+ disabled: false,
+ click: () => addWatchpoint(item, "get"),
+ };
+
+ const addGetOrSetWatchpointItem = {
+ id: "node-menu-add-get-watchpoint",
+ label: addGetOrSetWatchpointLabel,
+ disabled: false,
+ click: () => addWatchpoint(item, "getorset"),
+ };
+
+ const watchpointsSubmenuItem = {
+ id: "node-menu-watchpoints",
+ label: watchpointsSubmenuLabel,
+ disabled: false,
+ click: () => addWatchpoint(item, "set"),
+ submenu: [
+ addSetWatchpointItem,
+ addGetWatchpointItem,
+ addGetOrSetWatchpointItem,
+ ],
+ };
+
+ const menuItems = [watchpointsSubmenuItem];
+ showMenu(event, menuItems);
+ };
+
+ renderWatchpointButton = item => {
+ const { removeWatchpoint } = this.props;
+
+ if (
+ !item ||
+ !item.contents ||
+ !item.contents.watchpoint ||
+ typeof L10N === "undefined"
+ ) {
+ return null;
+ }
+
+ const { watchpoint } = item.contents;
+ return button({
+ className: `remove-watchpoint-${watchpoint}`,
+ title: L10N.getStr("watchpoints.removeWatchpointTooltip"),
+ onClick: e => {
+ e.stopPropagation();
+ removeWatchpoint(item);
+ },
+ });
+ };
+
+ renderScopesList() {
+ const {
+ isLoading,
+ openLink,
+ openElementInInspector,
+ highlightDomElement,
+ unHighlightDomElement,
+ mapScopesEnabled,
+ selectedFrame,
+ setExpandedScope,
+ expandedScopes,
+ selectedSource,
+ } = this.props;
+
+ if (!selectedSource) {
+ return div(
+ { className: "pane scopes-list" },
+ div({ className: "pane-info" }, L10N.getStr("scopes.notAvailable"))
+ );
+ }
+
+ const { originalScopes, generatedScopes } = this.state;
+ let scopes = null;
+
+ if (
+ selectedSource.isOriginal &&
+ !selectedSource.isPrettyPrinted &&
+ !selectedFrame.generatedLocation?.source.isWasm
+ ) {
+ if (!mapScopesEnabled) {
+ return div(
+ { className: "pane scopes-list" },
+ div(
+ {
+ className: "pane-info no-original-scopes-info",
+ "aria-role": "status",
+ },
+ span(
+ { className: "info icon" },
+ React.createElement(AccessibleImage, { className: "sourcemap" })
+ ),
+ L10N.getFormatStr(
+ "scopes.noOriginalScopes",
+ L10N.getStr("scopes.showOriginalScopes")
+ )
+ )
+ );
+ }
+ if (isLoading) {
+ return div(
+ {
+ className: "pane scopes-list",
+ },
+ div(
+ { className: "pane-info" },
+ span(
+ { className: "info icon" },
+ React.createElement(AccessibleImage, { className: "loader" })
+ ),
+ L10N.getStr("scopes.loadingOriginalScopes")
+ )
+ );
+ }
+ scopes = originalScopes;
+ } else {
+ if (isLoading) {
+ return div(
+ {
+ className: "pane scopes-list",
+ },
+ div(
+ { className: "pane-info" },
+ span(
+ { className: "info icon" },
+ React.createElement(AccessibleImage, { className: "loader" })
+ ),
+ L10N.getStr("loadingText")
+ )
+ );
+ }
+ scopes = generatedScopes;
+ }
+
+ function initiallyExpanded(item) {
+ return expandedScopes.some(path => path == getScopeItemPath(item));
+ }
+
+ if (scopes && !!scopes.length) {
+ return div(
+ {
+ className: "pane scopes-list",
+ },
+ React.createElement(ObjectInspector, {
+ roots: scopes,
+ autoExpandAll: false,
+ autoExpandDepth: 1,
+ client: clientCommands,
+ createElement: tagName => document.createElement(tagName),
+ disableWrap: true,
+ dimTopLevelWindow: true,
+ frame: selectedFrame,
+ mayUseCustomFormatter: true,
+ openLink: openLink,
+ onDOMNodeClick: grip => openElementInInspector(grip),
+ onInspectIconClick: grip => openElementInInspector(grip),
+ onDOMNodeMouseOver: grip => highlightDomElement(grip),
+ onDOMNodeMouseOut: grip => unHighlightDomElement(grip),
+ onContextMenu: this.onContextMenu,
+ setExpanded: (path, expand) =>
+ setExpandedScope(selectedFrame, path, expand),
+ initiallyExpanded: initiallyExpanded,
+ renderItemActions: this.renderWatchpointButton,
+ shouldRenderTooltip: true,
+ })
+ );
+ }
+
+ return div(
+ {
+ className: "pane scopes-list",
+ },
+ div(
+ {
+ className: "pane-info",
+ },
+ L10N.getStr("scopes.notAvailable")
+ )
+ );
+ }
+
+ render() {
+ return div(
+ {
+ className: "scopes-content",
+ },
+ this.renderScopesList()
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ // This component doesn't need any prop when we are not paused
+ const selectedFrame = getSelectedFrame(state, getCurrentThread(state));
+ if (!selectedFrame) {
+ return {};
+ }
+ const why = getPauseReason(state, selectedFrame.thread);
+ const expandedScopes = getLastExpandedScopes(state, selectedFrame.thread);
+ const isPaused = getIsCurrentThreadPaused(state);
+ const selectedSource = getSelectedSource(state);
+
+ let originalFrameScopes;
+ let generatedFrameScopes;
+ let isLoading;
+ let mapScopesEnabled;
+
+ if (
+ selectedSource?.isOriginal &&
+ !selectedSource?.isPrettyPrinted &&
+ !selectedFrame.generatedLocation?.source.isWasm
+ ) {
+ const { scope, pending: originalPending } = getOriginalFrameScope(
+ state,
+ selectedFrame
+ ) || {
+ scope: null,
+ pending: false,
+ };
+ originalFrameScopes = scope;
+ isLoading = originalPending;
+ mapScopesEnabled = isMapScopesEnabled(state);
+ } else {
+ const { scope, pending: generatedPending } = getGeneratedFrameScope(
+ state,
+ selectedFrame
+ ) || {
+ scope: null,
+ pending: false,
+ };
+ generatedFrameScopes = scope;
+ isLoading = generatedPending;
+ }
+
+ return {
+ originalFrameScopes,
+ generatedFrameScopes,
+ mapScopesEnabled,
+ selectedFrame,
+ isLoading,
+ why,
+ expandedScopes,
+ isPaused,
+ selectedSource,
+ };
+};
+
+export default connect(mapStateToProps, {
+ openLink: actions.openLink,
+ openElementInInspector: actions.openElementInInspectorCommand,
+ highlightDomElement: actions.highlightDomElement,
+ unHighlightDomElement: actions.unHighlightDomElement,
+ setExpandedScope: actions.setExpandedScope,
+ addWatchpoint: actions.addWatchpoint,
+ removeWatchpoint: actions.removeWatchpoint,
+ toggleMapScopes: actions.toggleMapScopes,
+})(Scopes);
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/SecondaryPanes.css b/devtools/client/debugger/src/components/SecondaryPanes/SecondaryPanes.css
new file mode 100644
index 0000000000..6432e9fe75
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/SecondaryPanes.css
@@ -0,0 +1,98 @@
+/* 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/>. */
+
+.secondary-panes {
+ overflow-x: hidden;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ white-space: nowrap;
+ background-color: var(--theme-sidebar-background);
+ --breakpoint-expression-right-clear-space: 36px;
+ --paused-background-color: hsl(54, 100%, 92%);
+ --paused-color: var(--theme-body-color);
+
+ .theme-dark & {
+ --paused-background-color: hsl(42, 37%, 19%);
+ --paused-color: hsl(43, 94%, 81%);
+ }
+}
+
+.secondary-panes .controlled > div {
+ max-width: 100%;
+}
+
+/*
+ We apply overflow to the container with the commandbar.
+ This allows the commandbar to remain fixed when scrolling
+ until the content completely ends. Not just the height of
+ the wrapper.
+ Ref: https://github.com/firefox-devtools/debugger/issues/3426
+*/
+
+.secondary-panes-wrapper {
+ height: 100%;
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
+.secondary-panes .accordion {
+ flex: 1 0 auto;
+ margin-bottom: 0;
+}
+
+.secondary-panes-wrapper .accordion li:last-child ._content {
+ border-bottom: 0;
+}
+
+.pane {
+ color: var(--theme-body-color);
+}
+
+.pane .pane-info {
+ text-align: start;
+ padding: 0.5em;
+ gap: 8px;
+ display: flex;
+ white-space: normal;
+}
+
+.pane .pane-info.no-original-scopes-info {
+ background-color: var(--theme-warning-background);
+ color: var(--theme-warning-color);
+}
+
+.secondary-panes .breakpoints-buttons {
+ display: flex;
+}
+
+.dropdown {
+ width: 20em;
+ overflow: auto;
+}
+
+.secondary-panes input[type="checkbox"] {
+ margin: 0;
+ margin-inline-end: 4px;
+ vertical-align: text-top;
+}
+
+.secondary-panes-wrapper .command-bar.bottom {
+ background-color: var(--theme-body-background);
+}
+
+/**
+ * Skip Pausing style
+ * Add a gray background and lower content opacity
+ */
+.skip-pausing .xhr-breakpoints-pane ._content,
+.skip-pausing .breakpoints-pane ._content,
+.skip-pausing .event-listeners-pane ._content,
+.skip-pausing .dom-mutations-pane ._content {
+ background-color: var(--skip-pausing-background-color);
+ opacity: var(--skip-pausing-opacity);
+ color: var(--skip-pausing-color);
+}
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Thread.js b/devtools/client/debugger/src/components/SecondaryPanes/Thread.js
new file mode 100644
index 0000000000..bfa73f1346
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Thread.js
@@ -0,0 +1,81 @@
+/* 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 React, { Component } from "devtools/client/shared/vendor/react";
+import { div, span } from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+
+import actions from "../../actions/index";
+import { getCurrentThread, getIsPaused } from "../../selectors/index";
+import AccessibleImage from "../shared/AccessibleImage";
+
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+export class Thread extends Component {
+ static get propTypes() {
+ return {
+ currentThread: PropTypes.string.isRequired,
+ isPaused: PropTypes.bool.isRequired,
+ selectThread: PropTypes.func.isRequired,
+ thread: PropTypes.object.isRequired,
+ };
+ }
+
+ onSelectThread = () => {
+ this.props.selectThread(this.props.thread.actor);
+ };
+
+ render() {
+ const { currentThread, isPaused, thread } = this.props;
+
+ const isWorker = thread.targetType.includes("worker");
+ let label = thread.name;
+ if (thread.serviceWorkerStatus) {
+ label += ` (${thread.serviceWorkerStatus})`;
+ }
+ return div(
+ {
+ className: classnames("thread", {
+ selected: thread.actor == currentThread,
+ paused: isPaused,
+ }),
+ key: thread.actor,
+ onClick: this.onSelectThread,
+ },
+ div(
+ {
+ className: "icon",
+ },
+ React.createElement(AccessibleImage, {
+ className: isWorker ? "worker" : "window",
+ })
+ ),
+ div(
+ {
+ className: "label",
+ },
+ label
+ ),
+ isPaused
+ ? span(
+ {
+ className: "pause-badge",
+ role: "status",
+ },
+ L10N.getStr("pausedThread")
+ )
+ : null
+ );
+ }
+}
+
+const mapStateToProps = (state, props) => ({
+ currentThread: getCurrentThread(state),
+ isPaused: getIsPaused(state, props.thread.actor),
+});
+
+export default connect(mapStateToProps, {
+ selectThread: actions.selectThread,
+})(Thread);
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Threads.css b/devtools/client/debugger/src/components/SecondaryPanes/Threads.css
new file mode 100644
index 0000000000..603aa3ad8a
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Threads.css
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+.threads-list {
+ padding: 4px 0;
+}
+
+.threads-list * {
+ user-select: none;
+}
+
+.threads-list > .thread {
+ font-size: inherit;
+ color: var(--theme-text-color-strong);
+ padding: 2px 6px;
+ padding-inline-start: 20px;
+ line-height: 16px;
+ position: relative;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+
+ &:hover {
+ background-color: var(--search-overlays-semitransparent);
+ }
+
+ &.selected {
+ background: var(--theme-selection-background);
+ color: var(--theme-selection-color);
+
+ & .img {
+ background-color: currentColor;
+ }
+ }
+
+ &.paused:not(.selected) {
+ background-color: var(--paused-background-color);
+ color: var(--paused-color);
+ }
+}
+
+.threads-list .icon {
+ flex: none;
+}
+
+.threads-list .img {
+ display: block;
+}
+
+.threads-list .label {
+ display: inline-block;
+ flex-grow: 1;
+ flex-shrink: 1;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.threads-list .pause-badge {
+ flex: none;
+ font-weight: bold;
+}
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Threads.js b/devtools/client/debugger/src/components/SecondaryPanes/Threads.js
new file mode 100644
index 0000000000..2d21a1dcc5
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Threads.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 React, { Component } from "devtools/client/shared/vendor/react";
+import { div } from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+
+import { getAllThreads } from "../../selectors/index";
+import Thread from "./Thread";
+
+export class Threads extends Component {
+ static get propTypes() {
+ return {
+ threads: PropTypes.array.isRequired,
+ };
+ }
+
+ render() {
+ const { threads } = this.props;
+ return div(
+ {
+ className: "pane threads-list",
+ },
+ threads.map(thread =>
+ React.createElement(Thread, {
+ thread: thread,
+ key: thread.actor,
+ })
+ )
+ );
+ }
+}
+
+const mapStateToProps = state => ({
+ threads: getAllThreads(state),
+});
+
+export default connect(mapStateToProps)(Threads);
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/WhyPaused.css b/devtools/client/debugger/src/components/SecondaryPanes/WhyPaused.css
new file mode 100644
index 0000000000..6fa5bc42d7
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/WhyPaused.css
@@ -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/>. */
+
+.why-paused {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ border-bottom: 1px solid var(--theme-splitter-color);
+ background-color: var(--paused-background-color);
+ color: var(--paused-color);
+ font-size: 12px;
+ cursor: default;
+ min-height: 44px;
+ padding: 6px;
+ white-space: normal;
+ font-weight: bold;
+}
+
+.why-paused > div {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+
+.why-paused .info.icon {
+ align-self: center;
+ padding-right: 4px;
+ margin-inline-start: 14px;
+ margin-inline-end: 3px;
+}
+
+.why-paused .pause.reason {
+ display: flex;
+ flex-direction: column;
+ padding-right: 4px;
+}
+
+.why-paused .message {
+ font-style: italic;
+ font-weight: 100;
+}
+
+.why-paused .mutationNode {
+ font-weight: normal;
+}
+
+.why-paused .message.warning {
+ color: var(--theme-graphs-full-red);
+ font-family: var(--monospace-font-family);
+ font-size: 10px;
+ font-style: normal;
+}
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/WhyPaused.js b/devtools/client/debugger/src/components/SecondaryPanes/WhyPaused.js
new file mode 100644
index 0000000000..fabd75a7ce
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/WhyPaused.js
@@ -0,0 +1,218 @@
+/* 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/>. */
+
+const {
+ LocalizationProvider,
+ Localized,
+} = require("resource://devtools/client/shared/vendor/fluent-react.js");
+
+import React, { PureComponent } from "devtools/client/shared/vendor/react";
+import { div, span } from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import AccessibleImage from "../shared/AccessibleImage";
+import actions from "../../actions/index";
+
+import Reps from "devtools/client/shared/components/reps/index";
+const {
+ REPS: { Rep },
+ MODE,
+} = Reps;
+
+import { getPauseReason } from "../../utils/pause/index";
+import {
+ getCurrentThread,
+ getPaneCollapse,
+ getPauseReason as getWhy,
+} from "../../selectors/index";
+
+class WhyPaused extends PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = { hideWhyPaused: "" };
+ }
+
+ static get propTypes() {
+ return {
+ delay: PropTypes.number.isRequired,
+ endPanelCollapsed: PropTypes.bool.isRequired,
+ highlightDomElement: PropTypes.func.isRequired,
+ openElementInInspector: PropTypes.func.isRequired,
+ unHighlightDomElement: PropTypes.func.isRequired,
+ why: PropTypes.object,
+ };
+ }
+
+ componentDidUpdate() {
+ const { delay } = this.props;
+
+ if (delay) {
+ setTimeout(() => {
+ this.setState({ hideWhyPaused: "" });
+ }, delay);
+ } else {
+ this.setState({ hideWhyPaused: "pane why-paused" });
+ }
+ }
+
+ renderExceptionSummary(exception) {
+ if (typeof exception === "string") {
+ return exception;
+ }
+
+ const { preview } = exception;
+ if (!preview || !preview.name || !preview.message) {
+ return null;
+ }
+
+ return `${preview.name}: ${preview.message}`;
+ }
+
+ renderMessage(why) {
+ const { type, exception, message } = why;
+
+ if (type == "exception" && exception) {
+ // Our types for 'Why' are too general because 'type' can be 'string'.
+ // $FlowFixMe - We should have a proper discriminating union of reasons.
+ const summary = this.renderExceptionSummary(exception);
+ return div(
+ {
+ className: "message warning",
+ },
+ summary
+ );
+ }
+
+ if (type === "mutationBreakpoint" && why.nodeGrip) {
+ const { nodeGrip, ancestorGrip, action } = why;
+ const {
+ openElementInInspector,
+ highlightDomElement,
+ unHighlightDomElement,
+ } = this.props;
+
+ const targetRep = Rep({
+ object: nodeGrip,
+ mode: MODE.TINY,
+ onDOMNodeClick: () => openElementInInspector(nodeGrip),
+ onInspectIconClick: () => openElementInInspector(nodeGrip),
+ onDOMNodeMouseOver: () => highlightDomElement(nodeGrip),
+ onDOMNodeMouseOut: () => unHighlightDomElement(),
+ });
+
+ const ancestorRep = ancestorGrip
+ ? Rep({
+ object: ancestorGrip,
+ mode: MODE.TINY,
+ onDOMNodeClick: () => openElementInInspector(ancestorGrip),
+ onInspectIconClick: () => openElementInInspector(ancestorGrip),
+ onDOMNodeMouseOver: () => highlightDomElement(ancestorGrip),
+ onDOMNodeMouseOut: () => unHighlightDomElement(),
+ })
+ : null;
+ return div(
+ null,
+ div(
+ {
+ className: "message",
+ },
+ why.message
+ ),
+ div(
+ {
+ className: "mutationNode",
+ },
+ ancestorRep,
+ ancestorGrip
+ ? span(
+ {
+ className: "why-paused-ancestor",
+ },
+ React.createElement(Localized, {
+ id:
+ action === "remove"
+ ? "whypaused-mutation-breakpoint-removed"
+ : "whypaused-mutation-breakpoint-added",
+ }),
+ targetRep
+ )
+ : targetRep
+ )
+ );
+ }
+
+ if (typeof message == "string") {
+ return div(
+ {
+ className: "message",
+ },
+ message
+ );
+ }
+
+ return null;
+ }
+
+ render() {
+ const { endPanelCollapsed, why } = this.props;
+ const { fluentBundles } = this.context;
+ const reason = getPauseReason(why);
+
+ if (!why || !reason || endPanelCollapsed) {
+ return div({
+ className: this.state.hideWhyPaused,
+ });
+ }
+ return (
+ // We're rendering the LocalizationProvider component from here and not in an upper
+ // component because it does set a new context, overriding the context that we set
+ // in the first place in <App>, which breaks some components.
+ // This should be fixed in Bug 1743155.
+ React.createElement(
+ LocalizationProvider,
+ {
+ bundles: fluentBundles || [],
+ },
+ div(
+ {
+ className: "pane why-paused",
+ },
+ div(
+ null,
+ div(
+ {
+ className: "info icon",
+ },
+ React.createElement(AccessibleImage, {
+ className: "info",
+ })
+ ),
+ div(
+ {
+ className: "pause reason",
+ },
+ React.createElement(Localized, {
+ id: reason,
+ }),
+ this.renderMessage(why)
+ )
+ )
+ )
+ )
+ );
+ }
+}
+
+WhyPaused.contextTypes = { fluentBundles: PropTypes.array };
+
+const mapStateToProps = state => ({
+ endPanelCollapsed: getPaneCollapse(state, "end"),
+ why: getWhy(state, getCurrentThread(state)),
+});
+
+export default connect(mapStateToProps, {
+ openElementInInspector: actions.openElementInInspectorCommand,
+ highlightDomElement: actions.highlightDomElement,
+ unHighlightDomElement: actions.unHighlightDomElement,
+})(WhyPaused);
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.css b/devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.css
new file mode 100644
index 0000000000..50b6240337
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.css
@@ -0,0 +1,108 @@
+/* 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/>. */
+
+.xhr-breakpoints-pane ._content {
+ overflow-x: auto;
+}
+
+.xhr-input-container {
+ display: flex;
+}
+
+.xhr-input-form {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+ width: 100%;
+ /* helps to display a nice outline on focused elements */
+ padding-block: 2px;
+ padding-inline-start: 20px;
+ padding-inline-end: 12px;
+ /* Stop select height from increasing as input height increases */
+ align-items: center;
+}
+
+.xhr-checkbox {
+ margin-inline-start: 0;
+ margin-inline-end: 4px;
+}
+
+.xhr-input-url {
+ border: 1px;
+ flex: 1 1 100px;
+ min-width: min(100%, 100px);
+ height: 24px;
+ background-color: var(--theme-sidebar-background);
+ font-size: inherit;
+ color: var(--theme-body-color);
+}
+
+.xhr-input-url::placeholder {
+ color: var(--theme-text-color-alt);
+ opacity: 1;
+}
+
+.expressions-list .xhr-input-url {
+ /* Prevent vertical bounce when editing an existing XHR Breakpoint */
+ height: 100%;
+}
+
+.xhr-input-method {
+ flex: 0 1 100px;
+ min-width: min(100%, 100px);
+}
+
+.xhr-container {
+ border-left: 4px solid transparent;
+ width: 100%;
+ color: var(--theme-body-color);
+ padding-inline-start: 16px;
+ padding-inline-end: 6px;
+ display: flex;
+ align-items: center;
+ position: relative;
+ height: var(--expression-item-height);
+}
+
+:root.theme-light .xhr-container:hover {
+ background-color: var(--search-overlays-semitransparent);
+}
+
+:root.theme-dark .xhr-container:hover {
+ background-color: var(--search-overlays-semitransparent);
+}
+
+.xhr-label-method {
+ line-height: 14px;
+ display: inline-block;
+ margin-inline-end: 2px;
+}
+
+.xhr-input-container:not(.focused) .xhr-input-method {
+ display: none;
+}
+
+.xhr-label-url {
+ max-width: calc(100% - var(--breakpoint-expression-right-clear-space));
+ color: var(--theme-comment);
+ display: inline-block;
+ cursor: text;
+ flex-grow: 1;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ padding: 0px 2px 0px 2px;
+ line-height: 14px;
+}
+
+.xhr-container label {
+ flex-grow: 1;
+ display: flex;
+ align-items: center;
+ max-width: 100%;
+}
+
+.xhr-container__close-btn {
+ display: flex;
+ padding: 2px;
+}
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.js b/devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.js
new file mode 100644
index 0000000000..9774255dcd
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.js
@@ -0,0 +1,383 @@
+/* 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 React, { Component } from "devtools/client/shared/vendor/react";
+import {
+ div,
+ form,
+ input,
+ li,
+ label,
+ ul,
+ option,
+ select,
+} from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import actions from "../../actions/index";
+
+import { CloseButton } from "../shared/Button/index";
+
+import { getXHRBreakpoints, shouldPauseOnAnyXHR } from "../../selectors/index";
+import ExceptionOption from "./Breakpoints/ExceptionOption";
+
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+// At present, the "Pause on any URL" checkbox creates an xhrBreakpoint
+// of "ANY" with no path, so we can remove that before creating the list
+function getExplicitXHRBreakpoints(xhrBreakpoints) {
+ return xhrBreakpoints.filter(bp => bp.path !== "");
+}
+
+const xhrMethods = [
+ "ANY",
+ "GET",
+ "POST",
+ "PUT",
+ "HEAD",
+ "DELETE",
+ "PATCH",
+ "OPTIONS",
+];
+
+class XHRBreakpoints extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ editing: false,
+ inputValue: "",
+ inputMethod: "ANY",
+ focused: false,
+ editIndex: -1,
+ clickedOnFormElement: false,
+ };
+ }
+
+ static get propTypes() {
+ return {
+ disableXHRBreakpoint: PropTypes.func.isRequired,
+ enableXHRBreakpoint: PropTypes.func.isRequired,
+ onXHRAdded: PropTypes.func.isRequired,
+ removeXHRBreakpoint: PropTypes.func.isRequired,
+ setXHRBreakpoint: PropTypes.func.isRequired,
+ shouldPauseOnAny: PropTypes.bool.isRequired,
+ showInput: PropTypes.bool.isRequired,
+ togglePauseOnAny: PropTypes.func.isRequired,
+ updateXHRBreakpoint: PropTypes.func.isRequired,
+ xhrBreakpoints: PropTypes.array.isRequired,
+ };
+ }
+
+ componentDidMount() {
+ const { showInput } = this.props;
+
+ // Ensures that the input is focused when the "+"
+ // is clicked while the panel is collapsed
+ if (this._input && showInput) {
+ this._input.focus();
+ }
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ const _input = this._input;
+
+ if (!_input) {
+ return;
+ }
+
+ if (!prevState.editing && this.state.editing) {
+ _input.setSelectionRange(0, _input.value.length);
+ _input.focus();
+ } else if (this.props.showInput && !this.state.focused) {
+ _input.focus();
+ }
+ }
+
+ handleNewSubmit = e => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const setXHRBreakpoint = function () {
+ this.props.setXHRBreakpoint(
+ this.state.inputValue,
+ this.state.inputMethod
+ );
+ this.hideInput();
+ };
+
+ // force update inputMethod in state for mochitest purposes
+ // before setting XHR breakpoint
+ this.setState(
+ { inputMethod: e.target.children[1].value },
+ setXHRBreakpoint
+ );
+ };
+
+ handleExistingSubmit = e => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const { editIndex, inputValue, inputMethod } = this.state;
+ const { xhrBreakpoints } = this.props;
+ const { path, method } = xhrBreakpoints[editIndex];
+
+ if (path !== inputValue || method != inputMethod) {
+ this.props.updateXHRBreakpoint(editIndex, inputValue, inputMethod);
+ }
+
+ this.hideInput();
+ };
+
+ handleChange = e => {
+ this.setState({ inputValue: e.target.value });
+ };
+
+ handleMethodChange = e => {
+ this.setState({
+ focused: true,
+ editing: true,
+ inputMethod: e.target.value,
+ });
+ };
+
+ hideInput = () => {
+ if (this.state.clickedOnFormElement) {
+ this.setState({
+ focused: true,
+ clickedOnFormElement: false,
+ });
+ } else {
+ this.setState({
+ focused: false,
+ editing: false,
+ editIndex: -1,
+ inputValue: "",
+ inputMethod: "ANY",
+ });
+ this.props.onXHRAdded();
+ }
+ };
+
+ onFocus = () => {
+ this.setState({ focused: true, editing: true });
+ };
+
+ onMouseDown = e => {
+ this.setState({ editing: false, clickedOnFormElement: true });
+ };
+
+ handleTab = e => {
+ if (e.key !== "Tab") {
+ return;
+ }
+
+ if (e.currentTarget.nodeName === "INPUT") {
+ this.setState({
+ clickedOnFormElement: true,
+ editing: false,
+ });
+ } else if (e.currentTarget.nodeName === "SELECT" && !e.shiftKey) {
+ // The user has tabbed off the select and we should
+ // cancel the edit
+ this.hideInput();
+ }
+ };
+
+ editExpression = index => {
+ const { xhrBreakpoints } = this.props;
+ const { path, method } = xhrBreakpoints[index];
+ this.setState({
+ inputValue: path,
+ inputMethod: method,
+ editing: true,
+ editIndex: index,
+ });
+ };
+
+ renderXHRInput(onSubmit) {
+ const { focused, inputValue } = this.state;
+ const placeholder = L10N.getStr("xhrBreakpoints.placeholder");
+ return form(
+ {
+ key: "xhr-input-container",
+ className: classnames("xhr-input-container xhr-input-form", {
+ focused,
+ }),
+ onSubmit: onSubmit,
+ },
+ input({
+ className: "xhr-input-url",
+ type: "text",
+ placeholder: placeholder,
+ onChange: this.handleChange,
+ onBlur: this.hideInput,
+ onFocus: this.onFocus,
+ value: inputValue,
+ onKeyDown: this.handleTab,
+ ref: c => (this._input = c),
+ }),
+ this.renderMethodSelectElement(),
+ input({
+ type: "submit",
+ style: {
+ display: "none",
+ },
+ })
+ );
+ }
+
+ handleCheckbox = index => {
+ const { xhrBreakpoints, enableXHRBreakpoint, disableXHRBreakpoint } =
+ this.props;
+ const breakpoint = xhrBreakpoints[index];
+ if (breakpoint.disabled) {
+ enableXHRBreakpoint(index);
+ } else {
+ disableXHRBreakpoint(index);
+ }
+ };
+
+ renderBreakpoint = breakpoint => {
+ const { path, disabled, method } = breakpoint;
+ const { editIndex } = this.state;
+ const { removeXHRBreakpoint, xhrBreakpoints } = this.props;
+
+ // The "pause on any" checkbox
+ if (!path) {
+ return null;
+ }
+
+ // Finds the xhrbreakpoint so as to not make assumptions about position
+ const index = xhrBreakpoints.findIndex(
+ bp => bp.path === path && bp.method === method
+ );
+
+ if (index === editIndex) {
+ return this.renderXHRInput(this.handleExistingSubmit);
+ }
+ return li(
+ {
+ className: "xhr-container",
+ key: `${path}-${method}`,
+ title: path,
+ onDoubleClick: (items, options) => this.editExpression(index),
+ },
+ label(
+ null,
+ React.createElement("input", {
+ type: "checkbox",
+ className: "xhr-checkbox",
+ checked: !disabled,
+ onChange: () => this.handleCheckbox(index),
+ onClick: ev => ev.stopPropagation(),
+ }),
+ div(
+ {
+ className: "xhr-label-method",
+ },
+ method
+ ),
+ div(
+ {
+ className: "xhr-label-url",
+ },
+ path
+ ),
+ div(
+ {
+ className: "xhr-container__close-btn",
+ },
+ React.createElement(CloseButton, {
+ handleClick: e => removeXHRBreakpoint(index),
+ })
+ )
+ )
+ );
+ };
+
+ renderBreakpoints = explicitXhrBreakpoints => {
+ const { showInput } = this.props;
+ return React.createElement(
+ React.Fragment,
+ null,
+ ul(
+ {
+ className: "pane expressions-list",
+ },
+ explicitXhrBreakpoints.map(this.renderBreakpoint)
+ ),
+ showInput && this.renderXHRInput(this.handleNewSubmit)
+ );
+ };
+
+ renderCheckbox = explicitXhrBreakpoints => {
+ const { shouldPauseOnAny, togglePauseOnAny } = this.props;
+ return div(
+ {
+ className: classnames("breakpoints-options", {
+ empty: explicitXhrBreakpoints.length === 0,
+ }),
+ },
+ React.createElement(ExceptionOption, {
+ className: "breakpoints-exceptions",
+ label: L10N.getStr("pauseOnAnyXHR"),
+ isChecked: shouldPauseOnAny,
+ onChange: () => togglePauseOnAny(),
+ })
+ );
+ };
+ renderMethodOption = method => {
+ return option(
+ {
+ key: method,
+ value: method,
+ // e.stopPropagation() required here since otherwise Firefox triggers 2x
+ // onMouseDown events on <select> upon clicking on an <option>
+ onMouseDown: e => e.stopPropagation(),
+ },
+ method
+ );
+ };
+
+ renderMethodSelectElement = () => {
+ return select(
+ {
+ value: this.state.inputMethod,
+ className: "xhr-input-method",
+ onChange: this.handleMethodChange,
+ onMouseDown: this.onMouseDown,
+ onKeyDown: this.handleTab,
+ },
+ xhrMethods.map(this.renderMethodOption)
+ );
+ };
+
+ render() {
+ const { xhrBreakpoints } = this.props;
+ const explicitXhrBreakpoints = getExplicitXHRBreakpoints(xhrBreakpoints);
+ return React.createElement(
+ React.Fragment,
+ null,
+ this.renderCheckbox(explicitXhrBreakpoints),
+ explicitXhrBreakpoints.length === 0
+ ? this.renderXHRInput(this.handleNewSubmit)
+ : this.renderBreakpoints(explicitXhrBreakpoints)
+ );
+ }
+}
+
+const mapStateToProps = state => ({
+ xhrBreakpoints: getXHRBreakpoints(state),
+ shouldPauseOnAny: shouldPauseOnAnyXHR(state),
+});
+
+export default connect(mapStateToProps, {
+ setXHRBreakpoint: actions.setXHRBreakpoint,
+ removeXHRBreakpoint: actions.removeXHRBreakpoint,
+ enableXHRBreakpoint: actions.enableXHRBreakpoint,
+ disableXHRBreakpoint: actions.disableXHRBreakpoint,
+ updateXHRBreakpoint: actions.updateXHRBreakpoint,
+ togglePauseOnAny: actions.togglePauseOnAny,
+})(XHRBreakpoints);
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/index.js b/devtools/client/debugger/src/components/SecondaryPanes/index.js
new file mode 100644
index 0000000000..20830afc12
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/index.js
@@ -0,0 +1,548 @@
+/* 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/>. */
+
+const SplitBox = require("resource://devtools/client/shared/components/splitter/SplitBox.js");
+
+import React, { Component } from "devtools/client/shared/vendor/react";
+import {
+ div,
+ input,
+ label,
+ button,
+ a,
+} from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+
+import actions from "../../actions/index";
+import {
+ getTopFrame,
+ getExpressions,
+ getPauseCommand,
+ isMapScopesEnabled,
+ getSelectedFrame,
+ getSelectedSource,
+ getThreads,
+ getCurrentThread,
+ getPauseReason,
+ getShouldBreakpointsPaneOpenOnPause,
+ getSkipPausing,
+ shouldLogEventBreakpoints,
+} from "../../selectors/index";
+
+import AccessibleImage from "../shared/AccessibleImage";
+import { prefs } from "../../utils/prefs";
+
+import Breakpoints from "./Breakpoints/index";
+import Expressions from "./Expressions";
+import Frames from "./Frames/index";
+import Threads from "./Threads";
+import Accordion from "../shared/Accordion";
+import CommandBar from "./CommandBar";
+import XHRBreakpoints from "./XHRBreakpoints";
+import EventListeners from "./EventListeners";
+import DOMMutationBreakpoints from "./DOMMutationBreakpoints";
+import WhyPaused from "./WhyPaused";
+
+import Scopes from "./Scopes";
+
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+function debugBtn(onClick, type, className, tooltip) {
+ return button(
+ {
+ onClick: onClick,
+ className: `${type} ${className}`,
+ key: type,
+ title: tooltip,
+ },
+ React.createElement(AccessibleImage, {
+ className: type,
+ title: tooltip,
+ "aria-label": tooltip,
+ })
+ );
+}
+
+const mdnLink =
+ "https://firefox-source-docs.mozilla.org/devtools-user/debugger/using_the_debugger_map_scopes_feature/";
+
+class SecondaryPanes extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ showExpressionsInput: false,
+ showXHRInput: false,
+ };
+ }
+
+ static get propTypes() {
+ return {
+ evaluateExpressionsForCurrentContext: PropTypes.func.isRequired,
+ expressions: PropTypes.array.isRequired,
+ hasFrames: PropTypes.bool.isRequired,
+ horizontal: PropTypes.bool.isRequired,
+ logEventBreakpoints: PropTypes.bool.isRequired,
+ mapScopesEnabled: PropTypes.bool.isRequired,
+ pauseReason: PropTypes.string.isRequired,
+ shouldBreakpointsPaneOpenOnPause: PropTypes.bool.isRequired,
+ thread: PropTypes.string.isRequired,
+ renderWhyPauseDelay: PropTypes.number.isRequired,
+ selectedFrame: PropTypes.object,
+ skipPausing: PropTypes.bool.isRequired,
+ source: PropTypes.object,
+ toggleEventLogging: PropTypes.func.isRequired,
+ resetBreakpointsPaneState: PropTypes.func.isRequired,
+ toggleMapScopes: PropTypes.func.isRequired,
+ threads: PropTypes.array.isRequired,
+ removeAllBreakpoints: PropTypes.func.isRequired,
+ removeAllXHRBreakpoints: PropTypes.func.isRequired,
+ };
+ }
+
+ onExpressionAdded = () => {
+ this.setState({ showExpressionsInput: false });
+ };
+
+ onXHRAdded = () => {
+ this.setState({ showXHRInput: false });
+ };
+
+ watchExpressionHeaderButtons() {
+ const { expressions } = this.props;
+ const buttons = [];
+
+ if (expressions.length) {
+ buttons.push(
+ debugBtn(
+ () => {
+ this.props.evaluateExpressionsForCurrentContext();
+ },
+ "refresh",
+ "active",
+ L10N.getStr("watchExpressions.refreshButton")
+ )
+ );
+ }
+ buttons.push(
+ debugBtn(
+ () => {
+ if (!prefs.expressionsVisible) {
+ this.onWatchExpressionPaneToggle(true);
+ }
+ this.setState({ showExpressionsInput: true });
+ },
+ "plus",
+ "active",
+ L10N.getStr("expressions.placeholder")
+ )
+ );
+ return buttons;
+ }
+
+ xhrBreakpointsHeaderButtons() {
+ return [
+ debugBtn(
+ () => {
+ if (!prefs.xhrBreakpointsVisible) {
+ this.onXHRPaneToggle(true);
+ }
+ this.setState({ showXHRInput: true });
+ },
+ "plus",
+ "active",
+ L10N.getStr("xhrBreakpoints.label")
+ ),
+
+ debugBtn(
+ () => {
+ this.props.removeAllXHRBreakpoints();
+ },
+ "removeAll",
+ "active",
+ L10N.getStr("xhrBreakpoints.removeAll.tooltip")
+ ),
+ ];
+ }
+
+ breakpointsHeaderButtons() {
+ return [
+ debugBtn(
+ () => {
+ this.props.removeAllBreakpoints();
+ },
+ "removeAll",
+ "active",
+ L10N.getStr("breakpointMenuItem.deleteAll")
+ ),
+ ];
+ }
+
+ getScopeItem() {
+ return {
+ header: L10N.getStr("scopes.header"),
+ className: "scopes-pane",
+ component: React.createElement(Scopes, null),
+ opened: prefs.scopesVisible,
+ buttons: this.getScopesButtons(),
+ onToggle: opened => {
+ prefs.scopesVisible = opened;
+ },
+ };
+ }
+
+ getScopesButtons() {
+ const { selectedFrame, mapScopesEnabled, source } = this.props;
+
+ if (!selectedFrame || !source?.isOriginal || source?.isPrettyPrinted) {
+ return null;
+ }
+
+ return [
+ div(
+ {
+ key: "scopes-buttons",
+ },
+ label(
+ {
+ className: "map-scopes-header",
+ title: L10N.getStr("scopes.showOriginalScopesTooltip"),
+ onClick: e => e.stopPropagation(),
+ },
+ input({
+ type: "checkbox",
+ checked: mapScopesEnabled ? "checked" : "",
+ onChange: e => this.props.toggleMapScopes(),
+ }),
+ L10N.getStr("scopes.showOriginalScopes")
+ ),
+ a(
+ {
+ className: "mdn",
+ target: "_blank",
+ href: mdnLink,
+ onClick: e => e.stopPropagation(),
+ title: L10N.getStr("scopes.showOriginalScopesHelpTooltip"),
+ },
+ React.createElement(AccessibleImage, {
+ className: "shortcuts",
+ })
+ )
+ ),
+ ];
+ }
+
+ getEventButtons() {
+ const { logEventBreakpoints } = this.props;
+ return [
+ div(
+ {
+ key: "events-buttons",
+ },
+ label(
+ {
+ className: "events-header",
+ title: L10N.getStr("eventlisteners.log.label"),
+ },
+ input({
+ type: "checkbox",
+ checked: logEventBreakpoints ? "checked" : "",
+ onChange: e => this.props.toggleEventLogging(),
+ }),
+ L10N.getStr("eventlisteners.log")
+ )
+ ),
+ ];
+ }
+
+ onWatchExpressionPaneToggle(opened) {
+ prefs.expressionsVisible = opened;
+ }
+
+ getWatchItem() {
+ return {
+ header: L10N.getStr("watchExpressions.header"),
+ id: "watch-expressions-pane",
+ className: "watch-expressions-pane",
+ buttons: this.watchExpressionHeaderButtons(),
+ component: React.createElement(Expressions, {
+ showInput: this.state.showExpressionsInput,
+ onExpressionAdded: this.onExpressionAdded,
+ }),
+ opened: prefs.expressionsVisible,
+ onToggle: this.onWatchExpressionPaneToggle,
+ };
+ }
+
+ onXHRPaneToggle(opened) {
+ prefs.xhrBreakpointsVisible = opened;
+ }
+
+ getXHRItem() {
+ const { pauseReason } = this.props;
+
+ return {
+ header: L10N.getStr("xhrBreakpoints.header"),
+ id: "xhr-breakpoints-pane",
+ className: "xhr-breakpoints-pane",
+ buttons: this.xhrBreakpointsHeaderButtons(),
+ component: React.createElement(XHRBreakpoints, {
+ showInput: this.state.showXHRInput,
+ onXHRAdded: this.onXHRAdded,
+ }),
+ opened: prefs.xhrBreakpointsVisible || pauseReason === "XHR",
+ onToggle: this.onXHRPaneToggle,
+ };
+ }
+
+ getCallStackItem() {
+ return {
+ header: L10N.getStr("callStack.header"),
+ id: "call-stack-pane",
+ className: "call-stack-pane",
+ component: React.createElement(Frames, {
+ panel: "debugger",
+ }),
+ opened: prefs.callStackVisible,
+ onToggle: opened => {
+ prefs.callStackVisible = opened;
+ },
+ };
+ }
+
+ getThreadsItem() {
+ return {
+ header: L10N.getStr("threadsHeader"),
+ id: "threads-pane",
+ className: "threads-pane",
+ component: React.createElement(Threads, null),
+ opened: prefs.threadsVisible,
+ onToggle: opened => {
+ prefs.threadsVisible = opened;
+ },
+ };
+ }
+
+ getBreakpointsItem() {
+ const { pauseReason, shouldBreakpointsPaneOpenOnPause, thread } =
+ this.props;
+
+ return {
+ header: L10N.getStr("breakpoints.header"),
+ id: "breakpoints-pane",
+ className: "breakpoints-pane",
+ buttons: this.breakpointsHeaderButtons(),
+ component: React.createElement(Breakpoints),
+ opened:
+ prefs.breakpointsVisible ||
+ (pauseReason === "breakpoint" && shouldBreakpointsPaneOpenOnPause),
+ onToggle: opened => {
+ prefs.breakpointsVisible = opened;
+ // one-shot flag used to force open the Breakpoints Pane only
+ // when hitting a breakpoint, but not when selecting frames etc...
+ if (shouldBreakpointsPaneOpenOnPause) {
+ this.props.resetBreakpointsPaneState(thread);
+ }
+ },
+ };
+ }
+
+ getEventListenersItem() {
+ const { pauseReason } = this.props;
+
+ return {
+ header: L10N.getStr("eventListenersHeader1"),
+ id: "event-listeners-pane",
+ className: "event-listeners-pane",
+ buttons: this.getEventButtons(),
+ component: React.createElement(EventListeners, null),
+ opened: prefs.eventListenersVisible || pauseReason === "eventBreakpoint",
+ onToggle: opened => {
+ prefs.eventListenersVisible = opened;
+ },
+ };
+ }
+
+ getDOMMutationsItem() {
+ const { pauseReason } = this.props;
+
+ return {
+ header: L10N.getStr("domMutationHeader"),
+ id: "dom-mutations-pane",
+ className: "dom-mutations-pane",
+ buttons: [],
+ component: React.createElement(DOMMutationBreakpoints, null),
+ opened:
+ prefs.domMutationBreakpointsVisible ||
+ pauseReason === "mutationBreakpoint",
+ onToggle: opened => {
+ prefs.domMutationBreakpointsVisible = opened;
+ },
+ };
+ }
+
+ getStartItems() {
+ const items = [];
+ const { horizontal, hasFrames } = this.props;
+
+ if (horizontal) {
+ if (this.props.threads.length) {
+ items.push(this.getThreadsItem());
+ }
+
+ items.push(this.getWatchItem());
+ }
+
+ items.push(this.getBreakpointsItem());
+
+ if (hasFrames) {
+ items.push(this.getCallStackItem());
+ if (horizontal) {
+ items.push(this.getScopeItem());
+ }
+ }
+
+ items.push(this.getXHRItem());
+
+ items.push(this.getEventListenersItem());
+
+ items.push(this.getDOMMutationsItem());
+
+ return items;
+ }
+
+ getEndItems() {
+ if (this.props.horizontal) {
+ return [];
+ }
+
+ const items = [];
+ if (this.props.threads.length) {
+ items.push(this.getThreadsItem());
+ }
+
+ items.push(this.getWatchItem());
+
+ if (this.props.hasFrames) {
+ items.push(this.getScopeItem());
+ }
+
+ return items;
+ }
+
+ getItems() {
+ return [...this.getStartItems(), ...this.getEndItems()];
+ }
+
+ renderHorizontalLayout() {
+ const { renderWhyPauseDelay } = this.props;
+ return div(
+ null,
+ React.createElement(WhyPaused, {
+ delay: renderWhyPauseDelay,
+ }),
+ React.createElement(Accordion, {
+ items: this.getItems(),
+ })
+ );
+ }
+
+ renderVerticalLayout() {
+ return React.createElement(SplitBox, {
+ initialSize: "300px",
+ minSize: 10,
+ maxSize: "50%",
+ splitterSize: 1,
+ startPanel: div(
+ {
+ style: {
+ width: "inherit",
+ },
+ },
+ React.createElement(WhyPaused, {
+ delay: this.props.renderWhyPauseDelay,
+ }),
+ React.createElement(Accordion, {
+ items: this.getStartItems(),
+ })
+ ),
+ endPanel: React.createElement(Accordion, {
+ items: this.getEndItems(),
+ }),
+ });
+ }
+
+ render() {
+ const { skipPausing } = this.props;
+ return div(
+ {
+ className: "secondary-panes-wrapper",
+ },
+ React.createElement(CommandBar, {
+ horizontal: this.props.horizontal,
+ }),
+ React.createElement(
+ "div",
+ {
+ className: classnames(
+ "secondary-panes",
+ skipPausing && "skip-pausing"
+ ),
+ },
+ this.props.horizontal
+ ? this.renderHorizontalLayout()
+ : this.renderVerticalLayout()
+ )
+ );
+ }
+}
+
+// Checks if user is in debugging mode and adds a delay preventing
+// excessive vertical 'jumpiness'
+function getRenderWhyPauseDelay(state, thread) {
+ const inPauseCommand = !!getPauseCommand(state, thread);
+
+ if (!inPauseCommand) {
+ return 100;
+ }
+
+ return 0;
+}
+
+const mapStateToProps = state => {
+ const thread = getCurrentThread(state);
+ const selectedFrame = getSelectedFrame(state, thread);
+ const pauseReason = getPauseReason(state, thread);
+ const shouldBreakpointsPaneOpenOnPause = getShouldBreakpointsPaneOpenOnPause(
+ state,
+ thread
+ );
+
+ return {
+ expressions: getExpressions(state),
+ hasFrames: !!getTopFrame(state, thread),
+ renderWhyPauseDelay: getRenderWhyPauseDelay(state, thread),
+ selectedFrame,
+ mapScopesEnabled: isMapScopesEnabled(state),
+ threads: getThreads(state),
+ skipPausing: getSkipPausing(state),
+ logEventBreakpoints: shouldLogEventBreakpoints(state),
+ source: getSelectedSource(state),
+ pauseReason: pauseReason?.type ?? "",
+ shouldBreakpointsPaneOpenOnPause,
+ thread,
+ };
+};
+
+export default connect(mapStateToProps, {
+ evaluateExpressionsForCurrentContext:
+ actions.evaluateExpressionsForCurrentContext,
+ toggleMapScopes: actions.toggleMapScopes,
+ breakOnNext: actions.breakOnNext,
+ toggleEventLogging: actions.toggleEventLogging,
+ removeAllBreakpoints: actions.removeAllBreakpoints,
+ removeAllXHRBreakpoints: actions.removeAllXHRBreakpoints,
+ resetBreakpointsPaneState: actions.resetBreakpointsPaneState,
+})(SecondaryPanes);
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/moz.build b/devtools/client/debugger/src/components/SecondaryPanes/moz.build
new file mode 100644
index 0000000000..33cfa2e316
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/moz.build
@@ -0,0 +1,22 @@
+# 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 += [
+ "Breakpoints",
+ "Frames",
+]
+
+CompiledModules(
+ "CommandBar.js",
+ "DOMMutationBreakpoints.js",
+ "EventListeners.js",
+ "Expressions.js",
+ "index.js",
+ "Scopes.js",
+ "Thread.js",
+ "Threads.js",
+ "WhyPaused.js",
+ "XHRBreakpoints.js",
+)
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/tests/CommandBar.spec.js b/devtools/client/debugger/src/components/SecondaryPanes/tests/CommandBar.spec.js
new file mode 100644
index 0000000000..b82997eb9a
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/tests/CommandBar.spec.js
@@ -0,0 +1,78 @@
+/* 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 React from "devtools/client/shared/vendor/react";
+import { shallow } from "enzyme";
+import CommandBar from "../CommandBar";
+
+describe("CommandBar", () => {
+ it("f8 key command calls props.breakOnNext when not in paused state", () => {
+ const props = {
+ breakOnNext: jest.fn(),
+ resume: jest.fn(),
+ isPaused: false,
+ };
+ const mockEvent = {
+ preventDefault: jest.fn(),
+ stopPropagation: jest.fn(),
+ };
+
+ // The "on" spy will see all the keyboard listeners being registered by
+ // the shortcuts.on function
+ const context = { shortcuts: { on: jest.fn() } };
+
+ shallow(React.createElement(CommandBar.WrappedComponent, props), {
+ context,
+ });
+
+ // get the keyboard event listeners recorded from the "on" spy.
+ // this will be an array where each item is itself a two item array
+ // containing the key code and the corresponding handler for that key code
+ const keyEventHandlers = context.shortcuts.on.mock.calls;
+
+ // simulate pressing the F8 key by calling the F8 handlers
+ keyEventHandlers
+ .filter(i => i[0] === "F8")
+ .forEach(([_, handler]) => {
+ handler(mockEvent);
+ });
+
+ expect(props.breakOnNext).toHaveBeenCalled();
+ expect(props.resume).not.toHaveBeenCalled();
+ });
+
+ it("f8 key command calls props.resume when in paused state", () => {
+ const props = {
+ breakOnNext: jest.fn(),
+ resume: jest.fn(),
+ isPaused: true,
+ };
+ const mockEvent = {
+ preventDefault: jest.fn(),
+ stopPropagation: jest.fn(),
+ };
+
+ // The "on" spy will see all the keyboard listeners being registered by
+ // the shortcuts.on function
+ const context = { shortcuts: { on: jest.fn() } };
+
+ shallow(React.createElement(CommandBar.WrappedComponent, props), {
+ context,
+ });
+
+ // get the keyboard event listeners recorded from the "on" spy.
+ // this will be an array where each item is itself a two item array
+ // containing the key code and the corresponding handler for that key code
+ const keyEventHandlers = context.shortcuts.on.mock.calls;
+
+ // simulate pressing the F8 key by calling the F8 handlers
+ keyEventHandlers
+ .filter(i => i[0] === "F8")
+ .forEach(([_, handler]) => {
+ handler(mockEvent);
+ });
+ expect(props.resume).toHaveBeenCalled();
+ expect(props.breakOnNext).not.toHaveBeenCalled();
+ });
+});
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/tests/EventListeners.spec.js b/devtools/client/debugger/src/components/SecondaryPanes/tests/EventListeners.spec.js
new file mode 100644
index 0000000000..58d7776e5a
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/tests/EventListeners.spec.js
@@ -0,0 +1,136 @@
+/* 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 React from "devtools/client/shared/vendor/react";
+import { shallow } from "enzyme";
+import EventListeners from "../EventListeners";
+
+function getCategories() {
+ return [
+ {
+ name: "Category 1",
+ events: [
+ { name: "Subcategory 1", id: "category1.subcategory1" },
+ { name: "Subcategory 2", id: "category1.subcategory2" },
+ ],
+ },
+ {
+ name: "Category 2",
+ events: [
+ { name: "Subcategory 3", id: "category2.subcategory1" },
+ { name: "Subcategory 4", id: "category2.subcategory2" },
+ ],
+ },
+ ];
+}
+
+function generateDefaults(overrides = {}) {
+ const defaults = {
+ activeEventListeners: [],
+ expandedCategories: [],
+ categories: [],
+ };
+
+ return { ...defaults, ...overrides };
+}
+
+function render(overrides = {}) {
+ const props = generateDefaults(overrides);
+ const component = shallow(
+ React.createElement(EventListeners.WrappedComponent, props)
+ );
+ return { component, props };
+}
+
+describe("EventListeners", () => {
+ it("should render", async () => {
+ const { component } = render();
+ expect(component).toMatchSnapshot();
+ });
+
+ it("should render categories appropriately", async () => {
+ const props = {
+ ...generateDefaults(),
+ categories: getCategories(),
+ };
+ const { component } = render(props);
+ expect(component).toMatchSnapshot();
+ });
+
+ it("should render expanded categories appropriately", async () => {
+ const props = {
+ ...generateDefaults(),
+ categories: getCategories(),
+ expandedCategories: ["Category 2"],
+ };
+ const { component } = render(props);
+ expect(component).toMatchSnapshot();
+ });
+
+ it("should render checked subcategories appropriately", async () => {
+ const props = {
+ ...generateDefaults(),
+ categories: getCategories(),
+ activeEventListeners: ["category1.subcategory2"],
+ expandedCategories: ["Category 1"],
+ };
+ const { component } = render(props);
+ expect(component).toMatchSnapshot();
+ });
+
+ it("should filter the event listeners based on the event name", async () => {
+ const props = {
+ ...generateDefaults(),
+ categories: getCategories(),
+ };
+ const { component } = render(props);
+ component.find(".event-search-input").simulate("focus");
+
+ const searchInput = component.find(".event-search-input");
+ // Simulate a search query of "Subcategory 3" to display just one event which
+ // will be the Subcategory 3 event
+ searchInput.simulate("change", {
+ currentTarget: { value: "Subcategory 3" },
+ });
+
+ const displayedEvents = component.find(".event-listener-event");
+ expect(displayedEvents).toHaveLength(1);
+ });
+
+ it("should filter the event listeners based on the category name", async () => {
+ const props = {
+ ...generateDefaults(),
+ categories: getCategories(),
+ };
+ const { component } = render(props);
+ component.find(".event-search-input").simulate("focus");
+
+ const searchInput = component.find(".event-search-input");
+ // Simulate a search query of "Category 1" to display two events which will be
+ // the Subcategory 1 event and the Subcategory 2 event
+ searchInput.simulate("change", { currentTarget: { value: "Category 1" } });
+
+ const displayedEvents = component.find(".event-listener-event");
+ expect(displayedEvents).toHaveLength(2);
+ });
+
+ it("should be case insensitive when filtering events and categories", async () => {
+ const props = {
+ ...generateDefaults(),
+ categories: getCategories(),
+ };
+ const { component } = render(props);
+ component.find(".event-search-input").simulate("focus");
+
+ const searchInput = component.find(".event-search-input");
+ // Simulate a search query of "Subcategory 3" to display just one event which
+ // will be the Subcategory 3 event
+ searchInput.simulate("change", {
+ currentTarget: { value: "sUbCaTeGoRy 3" },
+ });
+
+ const displayedEvents = component.find(".event-listener-event");
+ expect(displayedEvents).toHaveLength(1);
+ });
+});
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/tests/Expressions.spec.js b/devtools/client/debugger/src/components/SecondaryPanes/tests/Expressions.spec.js
new file mode 100644
index 0000000000..685c636f05
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/tests/Expressions.spec.js
@@ -0,0 +1,77 @@
+/* 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 React from "devtools/client/shared/vendor/react";
+import { shallow } from "enzyme";
+import Expressions from "../Expressions";
+
+function generateDefaults(overrides) {
+ return {
+ evaluateExpressions: async () => {},
+ expressions: [
+ {
+ input: "expression1",
+ value: {
+ result: {
+ value: "foo",
+ class: "",
+ },
+ },
+ },
+ {
+ input: "expression2",
+ value: {
+ result: {
+ value: "bar",
+ class: "",
+ },
+ },
+ },
+ ],
+ ...overrides,
+ };
+}
+
+function render(overrides = {}) {
+ const props = generateDefaults(overrides);
+ const component = shallow(
+ React.createElement(Expressions.WrappedComponent, props)
+ );
+ return { component, props };
+}
+
+describe("Expressions", () => {
+ it("should render", async () => {
+ const { component } = render();
+ expect(component).toMatchSnapshot();
+ });
+
+ it("should always have unique keys", async () => {
+ const overrides = {
+ expressions: [
+ {
+ input: "expression1",
+ value: {
+ result: {
+ value: undefined,
+ class: "",
+ },
+ },
+ },
+ {
+ input: "expression2",
+ value: {
+ result: {
+ value: undefined,
+ class: "",
+ },
+ },
+ },
+ ],
+ };
+
+ const { component } = render(overrides);
+ expect(component).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/tests/XHRBreakpoints.spec.js b/devtools/client/debugger/src/components/SecondaryPanes/tests/XHRBreakpoints.spec.js
new file mode 100644
index 0000000000..532c95e4ad
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/tests/XHRBreakpoints.spec.js
@@ -0,0 +1,341 @@
+/* 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 React from "devtools/client/shared/vendor/react";
+import { mount } from "enzyme";
+import XHRBreakpoints from "../XHRBreakpoints";
+
+const xhrMethods = [
+ "ANY",
+ "GET",
+ "POST",
+ "PUT",
+ "HEAD",
+ "DELETE",
+ "PATCH",
+ "OPTIONS",
+];
+
+// default state includes xhrBreakpoints[0] which is the checkbox that
+// enables breaking on any url during an XMLHTTPRequest
+function generateDefaultState(propsOverride) {
+ return {
+ xhrBreakpoints: [
+ {
+ path: "",
+ method: "ANY",
+ disabled: false,
+ loading: false,
+ text: 'URL contains ""',
+ },
+ ],
+ enableXHRBreakpoint: () => {},
+ disableXHRBreakpoint: () => {},
+ updateXHRBreakpoint: () => {},
+ removeXHRBreakpoint: () => {},
+ setXHRBreakpoint: () => {},
+ togglePauseOnAny: () => {},
+ showInput: false,
+ shouldPauseOnAny: false,
+ onXHRAdded: () => {},
+ ...propsOverride,
+ };
+}
+
+function renderXHRBreakpointsComponent(propsOverride) {
+ const props = generateDefaultState(propsOverride);
+ const xhrBreakpointsComponent = mount(
+ React.createElement(XHRBreakpoints.WrappedComponent, props)
+ );
+ return xhrBreakpointsComponent;
+}
+
+describe("XHR Breakpoints", function () {
+ it("should render with 0 expressions passed from props", function () {
+ const xhrBreakpointsComponent = renderXHRBreakpointsComponent();
+ expect(xhrBreakpointsComponent).toMatchSnapshot();
+ });
+
+ it("should render with 8 expressions passed from props", function () {
+ const allXHRBreakpointMethods = {
+ xhrBreakpoints: [
+ {
+ path: "",
+ method: "ANY",
+ disabled: false,
+ loading: false,
+ text: 'URL contains ""',
+ },
+ {
+ path: "this is any",
+ method: "ANY",
+ disabled: false,
+ loading: false,
+ text: 'URL contains "this is any"',
+ },
+ {
+ path: "this is get",
+ method: "GET",
+ disabled: false,
+ loading: false,
+ text: 'URL contains "this is get"',
+ },
+ {
+ path: "this is post",
+ method: "POST",
+ disabled: false,
+ loading: false,
+ text: 'URL contains "this is post"',
+ },
+ {
+ path: "this is put",
+ method: "PUT",
+ disabled: false,
+ loading: false,
+ text: 'URL contains "this is put"',
+ },
+ {
+ path: "this is head",
+ method: "HEAD",
+ disabled: false,
+ loading: false,
+ text: 'URL contains "this is head"',
+ },
+ {
+ path: "this is delete",
+ method: "DELETE",
+ disabled: false,
+ loading: false,
+ text: 'URL contains "this is delete"',
+ },
+ {
+ path: "this is patch",
+ method: "PATCH",
+ disabled: false,
+ loading: false,
+ text: 'URL contains "this is patch"',
+ },
+ {
+ path: "this is options",
+ method: "OPTIONS",
+ disabled: false,
+ loading: false,
+ text: 'URL contains "this is options"',
+ },
+ ],
+ };
+
+ const xhrBreakpointsComponent = renderXHRBreakpointsComponent(
+ allXHRBreakpointMethods
+ );
+ expect(xhrBreakpointsComponent).toMatchSnapshot();
+ });
+
+ it("should display xhr-input-method on click", function () {
+ const xhrBreakpointsComponent = renderXHRBreakpointsComponent();
+ xhrBreakpointsComponent.find(".xhr-input-url").simulate("focus");
+
+ const xhrInputContainer = xhrBreakpointsComponent.find(
+ ".xhr-input-container"
+ );
+ expect(xhrInputContainer.hasClass("focused")).toBeTruthy();
+ });
+
+ it("should have focused and editing default to false", function () {
+ const xhrBreakpointsComponent = renderXHRBreakpointsComponent();
+ expect(xhrBreakpointsComponent.state("focused")).toBe(false);
+ expect(xhrBreakpointsComponent.state("editing")).toBe(false);
+ });
+
+ it("should have state {..focused: true, editing: true} on focus", function () {
+ const xhrBreakpointsComponent = renderXHRBreakpointsComponent();
+ xhrBreakpointsComponent.find(".xhr-input-url").simulate("focus");
+ expect(xhrBreakpointsComponent.state("focused")).toBe(true);
+ expect(xhrBreakpointsComponent.state("editing")).toBe(true);
+ });
+
+ // shifting focus from .xhr-input to any other element apart from
+ // .xhr-input-method should unrender .xhr-input-method
+ it("shifting focus should unrender XHR methods", function () {
+ const propsOverride = {
+ onXHRAdded: jest.fn,
+ togglePauseOnAny: jest.fn,
+ };
+ const xhrBreakpointsComponent =
+ renderXHRBreakpointsComponent(propsOverride);
+ xhrBreakpointsComponent.find(".xhr-input-url").simulate("focus");
+ let xhrInputContainer = xhrBreakpointsComponent.find(
+ ".xhr-input-container"
+ );
+ expect(xhrInputContainer.hasClass("focused")).toBeTruthy();
+
+ xhrBreakpointsComponent.find(".breakpoints-options").simulate("mousedown");
+ expect(xhrBreakpointsComponent.state("focused")).toBe(true);
+ expect(xhrBreakpointsComponent.state("editing")).toBe(true);
+ expect(xhrBreakpointsComponent.state("clickedOnFormElement")).toBe(false);
+
+ xhrBreakpointsComponent.find(".xhr-input-url").simulate("blur");
+ expect(xhrBreakpointsComponent.state("focused")).toBe(false);
+ expect(xhrBreakpointsComponent.state("editing")).toBe(false);
+ expect(xhrBreakpointsComponent.state("clickedOnFormElement")).toBe(false);
+
+ xhrBreakpointsComponent.find(".breakpoints-options").simulate("click");
+
+ xhrInputContainer = xhrBreakpointsComponent.find(".xhr-input-container");
+ expect(xhrInputContainer.hasClass("focused")).not.toBeTruthy();
+ });
+
+ // shifting focus from .xhr-input to .xhr-input-method
+ // should not unrender .xhr-input-method
+ it("shifting focus to XHR methods should not unrender", function () {
+ const xhrBreakpointsComponent = renderXHRBreakpointsComponent();
+ xhrBreakpointsComponent.find(".xhr-input-url").simulate("focus");
+
+ xhrBreakpointsComponent.find(".xhr-input-method").simulate("mousedown");
+ expect(xhrBreakpointsComponent.state("focused")).toBe(true);
+ expect(xhrBreakpointsComponent.state("editing")).toBe(false);
+ expect(xhrBreakpointsComponent.state("clickedOnFormElement")).toBe(true);
+
+ xhrBreakpointsComponent.find(".xhr-input-url").simulate("blur");
+ expect(xhrBreakpointsComponent.state("focused")).toBe(true);
+ expect(xhrBreakpointsComponent.state("editing")).toBe(false);
+ expect(xhrBreakpointsComponent.state("clickedOnFormElement")).toBe(false);
+
+ xhrBreakpointsComponent.find(".xhr-input-method").simulate("click");
+ const xhrInputContainer = xhrBreakpointsComponent.find(
+ ".xhr-input-container"
+ );
+ expect(xhrInputContainer.hasClass("focused")).toBeTruthy();
+ });
+
+ it("should have all 8 methods available as options", function () {
+ const xhrBreakpointsComponent = renderXHRBreakpointsComponent();
+ xhrBreakpointsComponent.find(".xhr-input-url").simulate("focus");
+
+ const xhrInputMethod = xhrBreakpointsComponent.find(".xhr-input-method");
+ expect(xhrInputMethod.children()).toHaveLength(8);
+
+ const actualXHRMethods = [];
+ const expectedXHRMethods = xhrMethods;
+
+ // fill the actualXHRMethods array with actual methods displayed in DOM
+ for (let i = 0; i < xhrInputMethod.children().length; i++) {
+ actualXHRMethods.push(xhrInputMethod.childAt(i).key());
+ }
+
+ // check each expected XHR Method to see if they match the actual methods
+ expectedXHRMethods.forEach((expectedMethod, i) => {
+ function compareMethods(actualMethod) {
+ return expectedMethod === actualMethod;
+ }
+ expect(actualXHRMethods.find(compareMethods)).toBeTruthy();
+ });
+ });
+
+ it("should return focus to input box after selecting a method", function () {
+ const xhrBreakpointsComponent = renderXHRBreakpointsComponent();
+
+ // focus starts off at .xhr-input
+ xhrBreakpointsComponent.find(".xhr-input-url").simulate("focus");
+
+ // click on method options and select GET
+ const methodEvent = { target: { value: "GET" } };
+ xhrBreakpointsComponent.find(".xhr-input-method").simulate("mousedown");
+ expect(xhrBreakpointsComponent.state("inputMethod")).toBe("ANY");
+ expect(xhrBreakpointsComponent.state("editing")).toBe(false);
+ xhrBreakpointsComponent
+ .find(".xhr-input-method")
+ .simulate("change", methodEvent);
+
+ // if state.editing changes from false to true, infer that
+ // this._input.focus() is called, which shifts focus back to input box
+ expect(xhrBreakpointsComponent.state("inputMethod")).toBe("GET");
+ expect(xhrBreakpointsComponent.state("editing")).toBe(true);
+ });
+
+ it("should submit the URL and method when adding a breakpoint", function () {
+ const setXHRBreakpointCallback = jest.fn();
+ const propsOverride = {
+ setXHRBreakpoint: setXHRBreakpointCallback,
+ onXHRAdded: jest.fn(),
+ };
+ const mockEvent = {
+ preventDefault: jest.fn(),
+ stopPropagation: jest.fn(),
+ };
+ const availableXHRMethods = xhrMethods;
+ expect(!!availableXHRMethods.length).toBeTruthy();
+
+ // check each of the available methods to see whether
+ // adding them as a method to a new breakpoint works as expected
+ availableXHRMethods.forEach(function (method) {
+ const xhrBreakpointsComponent =
+ renderXHRBreakpointsComponent(propsOverride);
+ xhrBreakpointsComponent.find(".xhr-input-url").simulate("focus");
+ const urlValue = `${method.toLowerCase()}URLValue`;
+
+ // simulate DOM event adding urlValue to .xhr-input
+ const xhrInput = xhrBreakpointsComponent.find(".xhr-input-url");
+ xhrInput.simulate("change", { target: { value: urlValue } });
+
+ // simulate DOM event adding the input method to .xhr-input-method
+ const xhrInputMethod = xhrBreakpointsComponent.find(".xhr-input-method");
+ xhrInputMethod.simulate("change", { target: { value: method } });
+
+ xhrBreakpointsComponent.find("form").simulate("submit", mockEvent);
+ expect(setXHRBreakpointCallback).toHaveBeenCalledWith(urlValue, method);
+ });
+ });
+
+ it("should submit the URL and method when editing a breakpoint", function () {
+ const setXHRBreakpointCallback = jest.fn();
+ const mockEvent = {
+ preventDefault: jest.fn(),
+ stopPropagation: jest.fn(),
+ };
+ const propsOverride = {
+ updateXHRBreakpoint: setXHRBreakpointCallback,
+ onXHRAdded: jest.fn(),
+ xhrBreakpoints: [
+ {
+ path: "",
+ method: "ANY",
+ disabled: false,
+ loading: false,
+ text: 'URL contains ""',
+ },
+ {
+ path: "this is GET",
+ method: "GET",
+ disabled: false,
+ loading: false,
+ text: 'URL contains "this is get"',
+ },
+ ],
+ };
+ const xhrBreakpointsComponent =
+ renderXHRBreakpointsComponent(propsOverride);
+
+ // load xhrBreakpoints pane with one existing xhrBreakpoint
+ const existingXHRbreakpoint =
+ xhrBreakpointsComponent.find(".xhr-container");
+ expect(existingXHRbreakpoint).toHaveLength(1);
+
+ // double click on existing breakpoint
+ existingXHRbreakpoint.simulate("doubleclick");
+ const xhrInput = xhrBreakpointsComponent.find(".xhr-input-url");
+ xhrInput.simulate("focus");
+
+ // change inputs and submit form
+ const xhrInputMethod = xhrBreakpointsComponent.find(".xhr-input-method");
+ xhrInput.simulate("change", { target: { value: "POSTURLValue" } });
+ xhrInputMethod.simulate("change", { target: { value: "POST" } });
+ xhrBreakpointsComponent.find("form").simulate("submit", mockEvent);
+ expect(setXHRBreakpointCallback).toHaveBeenCalledWith(
+ 1,
+ "POSTURLValue",
+ "POST"
+ );
+ });
+});
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/tests/__snapshots__/EventListeners.spec.js.snap b/devtools/client/debugger/src/components/SecondaryPanes/tests/__snapshots__/EventListeners.spec.js.snap
new file mode 100644
index 0000000000..cc2ddf09f6
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/tests/__snapshots__/EventListeners.spec.js.snap
@@ -0,0 +1,408 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EventListeners should render 1`] = `
+<div
+ className="event-listeners"
+>
+ <div
+ className="event-search-container"
+ >
+ <form
+ className="event-search-form"
+ onSubmit={[Function]}
+ >
+ <input
+ className="event-search-input"
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Filter by event type"
+ value=""
+ />
+ </form>
+ </div>
+ <div
+ className="event-listeners-content"
+ >
+ <ul
+ className="event-listeners-list"
+ />
+ </div>
+</div>
+`;
+
+exports[`EventListeners should render categories appropriately 1`] = `
+<div
+ className="event-listeners"
+>
+ <div
+ className="event-search-container"
+ >
+ <form
+ className="event-search-form"
+ onSubmit={[Function]}
+ >
+ <input
+ className="event-search-input"
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Filter by event type"
+ value=""
+ />
+ </form>
+ </div>
+ <div
+ className="event-listeners-content"
+ >
+ <ul
+ className="event-listeners-list"
+ >
+ <li
+ className="event-listener-group"
+ key="0"
+ >
+ <div
+ className="event-listener-header"
+ >
+ <button
+ className="event-listener-expand"
+ onClick={[Function]}
+ >
+ <AccessibleImage
+ className="arrow"
+ />
+ </button>
+ <label
+ className="event-listener-label"
+ >
+ <input
+ checked={false}
+ onChange={[Function]}
+ type="checkbox"
+ value="Category 1"
+ />
+ <span
+ className="event-listener-category"
+ >
+ Category 1
+ </span>
+ </label>
+ </div>
+ </li>
+ <li
+ className="event-listener-group"
+ key="1"
+ >
+ <div
+ className="event-listener-header"
+ >
+ <button
+ className="event-listener-expand"
+ onClick={[Function]}
+ >
+ <AccessibleImage
+ className="arrow"
+ />
+ </button>
+ <label
+ className="event-listener-label"
+ >
+ <input
+ checked={false}
+ onChange={[Function]}
+ type="checkbox"
+ value="Category 2"
+ />
+ <span
+ className="event-listener-category"
+ >
+ Category 2
+ </span>
+ </label>
+ </div>
+ </li>
+ </ul>
+ </div>
+</div>
+`;
+
+exports[`EventListeners should render checked subcategories appropriately 1`] = `
+<div
+ className="event-listeners"
+>
+ <div
+ className="event-search-container"
+ >
+ <form
+ className="event-search-form"
+ onSubmit={[Function]}
+ >
+ <input
+ className="event-search-input"
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Filter by event type"
+ value=""
+ />
+ </form>
+ </div>
+ <div
+ className="event-listeners-content"
+ >
+ <ul
+ className="event-listeners-list"
+ >
+ <li
+ className="event-listener-group"
+ key="0"
+ >
+ <div
+ className="event-listener-header"
+ >
+ <button
+ className="event-listener-expand"
+ onClick={[Function]}
+ >
+ <AccessibleImage
+ className="arrow expanded"
+ />
+ </button>
+ <label
+ className="event-listener-label"
+ >
+ <input
+ checked={false}
+ onChange={[Function]}
+ type="checkbox"
+ value="Category 1"
+ />
+ <span
+ className="event-listener-category"
+ >
+ Category 1
+ </span>
+ </label>
+ </div>
+ <ul>
+ <li
+ className="event-listener-event"
+ key="category1.subcategory1"
+ >
+ <label
+ className="event-listener-label"
+ >
+ <input
+ checked={false}
+ onChange={[Function]}
+ type="checkbox"
+ value="category1.subcategory1"
+ />
+ <span
+ className="event-listener-name"
+ >
+ Subcategory 1
+ </span>
+ </label>
+ </li>
+ <li
+ className="event-listener-event"
+ key="category1.subcategory2"
+ >
+ <label
+ className="event-listener-label"
+ >
+ <input
+ checked={true}
+ onChange={[Function]}
+ type="checkbox"
+ value="category1.subcategory2"
+ />
+ <span
+ className="event-listener-name"
+ >
+ Subcategory 2
+ </span>
+ </label>
+ </li>
+ </ul>
+ </li>
+ <li
+ className="event-listener-group"
+ key="1"
+ >
+ <div
+ className="event-listener-header"
+ >
+ <button
+ className="event-listener-expand"
+ onClick={[Function]}
+ >
+ <AccessibleImage
+ className="arrow"
+ />
+ </button>
+ <label
+ className="event-listener-label"
+ >
+ <input
+ checked={false}
+ onChange={[Function]}
+ type="checkbox"
+ value="Category 2"
+ />
+ <span
+ className="event-listener-category"
+ >
+ Category 2
+ </span>
+ </label>
+ </div>
+ </li>
+ </ul>
+ </div>
+</div>
+`;
+
+exports[`EventListeners should render expanded categories appropriately 1`] = `
+<div
+ className="event-listeners"
+>
+ <div
+ className="event-search-container"
+ >
+ <form
+ className="event-search-form"
+ onSubmit={[Function]}
+ >
+ <input
+ className="event-search-input"
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Filter by event type"
+ value=""
+ />
+ </form>
+ </div>
+ <div
+ className="event-listeners-content"
+ >
+ <ul
+ className="event-listeners-list"
+ >
+ <li
+ className="event-listener-group"
+ key="0"
+ >
+ <div
+ className="event-listener-header"
+ >
+ <button
+ className="event-listener-expand"
+ onClick={[Function]}
+ >
+ <AccessibleImage
+ className="arrow"
+ />
+ </button>
+ <label
+ className="event-listener-label"
+ >
+ <input
+ checked={false}
+ onChange={[Function]}
+ type="checkbox"
+ value="Category 1"
+ />
+ <span
+ className="event-listener-category"
+ >
+ Category 1
+ </span>
+ </label>
+ </div>
+ </li>
+ <li
+ className="event-listener-group"
+ key="1"
+ >
+ <div
+ className="event-listener-header"
+ >
+ <button
+ className="event-listener-expand"
+ onClick={[Function]}
+ >
+ <AccessibleImage
+ className="arrow expanded"
+ />
+ </button>
+ <label
+ className="event-listener-label"
+ >
+ <input
+ checked={false}
+ onChange={[Function]}
+ type="checkbox"
+ value="Category 2"
+ />
+ <span
+ className="event-listener-category"
+ >
+ Category 2
+ </span>
+ </label>
+ </div>
+ <ul>
+ <li
+ className="event-listener-event"
+ key="category2.subcategory1"
+ >
+ <label
+ className="event-listener-label"
+ >
+ <input
+ checked={false}
+ onChange={[Function]}
+ type="checkbox"
+ value="category2.subcategory1"
+ />
+ <span
+ className="event-listener-name"
+ >
+ Subcategory 3
+ </span>
+ </label>
+ </li>
+ <li
+ className="event-listener-event"
+ key="category2.subcategory2"
+ >
+ <label
+ className="event-listener-label"
+ >
+ <input
+ checked={false}
+ onChange={[Function]}
+ type="checkbox"
+ value="category2.subcategory2"
+ />
+ <span
+ className="event-listener-name"
+ >
+ Subcategory 4
+ </span>
+ </label>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+</div>
+`;
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/tests/__snapshots__/Expressions.spec.js.snap b/devtools/client/debugger/src/components/SecondaryPanes/tests/__snapshots__/Expressions.spec.js.snap
new file mode 100644
index 0000000000..6ecf6c2c2e
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/tests/__snapshots__/Expressions.spec.js.snap
@@ -0,0 +1,203 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Expressions should always have unique keys 1`] = `
+<div
+ className="pane"
+>
+ <ul
+ className="pane expressions-list"
+ >
+ <li
+ className="expression-container"
+ key="expression1"
+ title="expression1"
+ >
+ <div
+ className="expression-content"
+ >
+ <Component
+ autoExpandDepth={0}
+ createElement={[Function]}
+ disableWrap={true}
+ mayUseCustomFormatter={true}
+ onDOMNodeClick={[Function]}
+ onDOMNodeMouseOut={[Function]}
+ onDOMNodeMouseOver={[Function]}
+ onDoubleClick={[Function]}
+ onInspectIconClick={[Function]}
+ roots={
+ Array [
+ Object {
+ "contents": Object {
+ "front": null,
+ "value": Object {
+ "class": "",
+ "value": undefined,
+ },
+ },
+ "name": "expression1",
+ "path": "expression1",
+ },
+ ]
+ }
+ shouldRenderTooltip={true}
+ />
+ <div
+ className="expression-container__close-btn"
+ >
+ <CloseButton
+ handleClick={[Function]}
+ tooltip="Remove watch expression"
+ />
+ </div>
+ </div>
+ </li>
+ <li
+ className="expression-container"
+ key="expression2"
+ title="expression2"
+ >
+ <div
+ className="expression-content"
+ >
+ <Component
+ autoExpandDepth={0}
+ createElement={[Function]}
+ disableWrap={true}
+ mayUseCustomFormatter={true}
+ onDOMNodeClick={[Function]}
+ onDOMNodeMouseOut={[Function]}
+ onDOMNodeMouseOver={[Function]}
+ onDoubleClick={[Function]}
+ onInspectIconClick={[Function]}
+ roots={
+ Array [
+ Object {
+ "contents": Object {
+ "front": null,
+ "value": Object {
+ "class": "",
+ "value": undefined,
+ },
+ },
+ "name": "expression2",
+ "path": "expression2",
+ },
+ ]
+ }
+ shouldRenderTooltip={true}
+ />
+ <div
+ className="expression-container__close-btn"
+ >
+ <CloseButton
+ handleClick={[Function]}
+ tooltip="Remove watch expression"
+ />
+ </div>
+ </div>
+ </li>
+ </ul>
+</div>
+`;
+
+exports[`Expressions should render 1`] = `
+<div
+ className="pane"
+>
+ <ul
+ className="pane expressions-list"
+ >
+ <li
+ className="expression-container"
+ key="expression1"
+ title="expression1"
+ >
+ <div
+ className="expression-content"
+ >
+ <Component
+ autoExpandDepth={0}
+ createElement={[Function]}
+ disableWrap={true}
+ mayUseCustomFormatter={true}
+ onDOMNodeClick={[Function]}
+ onDOMNodeMouseOut={[Function]}
+ onDOMNodeMouseOver={[Function]}
+ onDoubleClick={[Function]}
+ onInspectIconClick={[Function]}
+ roots={
+ Array [
+ Object {
+ "contents": Object {
+ "front": null,
+ "value": Object {
+ "class": "",
+ "value": "foo",
+ },
+ },
+ "name": "expression1",
+ "path": "expression1",
+ },
+ ]
+ }
+ shouldRenderTooltip={true}
+ />
+ <div
+ className="expression-container__close-btn"
+ >
+ <CloseButton
+ handleClick={[Function]}
+ tooltip="Remove watch expression"
+ />
+ </div>
+ </div>
+ </li>
+ <li
+ className="expression-container"
+ key="expression2"
+ title="expression2"
+ >
+ <div
+ className="expression-content"
+ >
+ <Component
+ autoExpandDepth={0}
+ createElement={[Function]}
+ disableWrap={true}
+ mayUseCustomFormatter={true}
+ onDOMNodeClick={[Function]}
+ onDOMNodeMouseOut={[Function]}
+ onDOMNodeMouseOver={[Function]}
+ onDoubleClick={[Function]}
+ onInspectIconClick={[Function]}
+ roots={
+ Array [
+ Object {
+ "contents": Object {
+ "front": null,
+ "value": Object {
+ "class": "",
+ "value": "bar",
+ },
+ },
+ "name": "expression2",
+ "path": "expression2",
+ },
+ ]
+ }
+ shouldRenderTooltip={true}
+ />
+ <div
+ className="expression-container__close-btn"
+ >
+ <CloseButton
+ handleClick={[Function]}
+ tooltip="Remove watch expression"
+ />
+ </div>
+ </div>
+ </li>
+ </ul>
+</div>
+`;
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/tests/__snapshots__/XHRBreakpoints.spec.js.snap b/devtools/client/debugger/src/components/SecondaryPanes/tests/__snapshots__/XHRBreakpoints.spec.js.snap
new file mode 100644
index 0000000000..e7c4527621
--- /dev/null
+++ b/devtools/client/debugger/src/components/SecondaryPanes/tests/__snapshots__/XHRBreakpoints.spec.js.snap
@@ -0,0 +1,619 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`XHR Breakpoints should render with 0 expressions passed from props 1`] = `
+<XHRBreakpoints
+ disableXHRBreakpoint={[Function]}
+ enableXHRBreakpoint={[Function]}
+ onXHRAdded={[Function]}
+ removeXHRBreakpoint={[Function]}
+ setXHRBreakpoint={[Function]}
+ shouldPauseOnAny={false}
+ showInput={false}
+ togglePauseOnAny={[Function]}
+ updateXHRBreakpoint={[Function]}
+ xhrBreakpoints={
+ Array [
+ Object {
+ "disabled": false,
+ "loading": false,
+ "method": "ANY",
+ "path": "",
+ "text": "URL contains \\"\\"",
+ },
+ ]
+ }
+>
+ <div
+ className="breakpoints-options empty"
+ >
+ <ExceptionOption
+ className="breakpoints-exceptions"
+ isChecked={false}
+ label="Pause on any URL"
+ onChange={[Function]}
+ >
+ <label
+ className="breakpoints-exceptions"
+ >
+ <input
+ checked={false}
+ onChange={[Function]}
+ type="checkbox"
+ />
+ <div
+ className="breakpoint-exceptions-label"
+ >
+ Pause on any URL
+ </div>
+ </label>
+ </ExceptionOption>
+ </div>
+ <form
+ className="xhr-input-container xhr-input-form"
+ key="xhr-input-container"
+ onSubmit={[Function]}
+ >
+ <input
+ className="xhr-input-url"
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Break when URL contains"
+ type="text"
+ value=""
+ />
+ <select
+ className="xhr-input-method"
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ onMouseDown={[Function]}
+ value="ANY"
+ >
+ <option
+ key="ANY"
+ onMouseDown={[Function]}
+ value="ANY"
+ >
+ ANY
+ </option>
+ <option
+ key="GET"
+ onMouseDown={[Function]}
+ value="GET"
+ >
+ GET
+ </option>
+ <option
+ key="POST"
+ onMouseDown={[Function]}
+ value="POST"
+ >
+ POST
+ </option>
+ <option
+ key="PUT"
+ onMouseDown={[Function]}
+ value="PUT"
+ >
+ PUT
+ </option>
+ <option
+ key="HEAD"
+ onMouseDown={[Function]}
+ value="HEAD"
+ >
+ HEAD
+ </option>
+ <option
+ key="DELETE"
+ onMouseDown={[Function]}
+ value="DELETE"
+ >
+ DELETE
+ </option>
+ <option
+ key="PATCH"
+ onMouseDown={[Function]}
+ value="PATCH"
+ >
+ PATCH
+ </option>
+ <option
+ key="OPTIONS"
+ onMouseDown={[Function]}
+ value="OPTIONS"
+ >
+ OPTIONS
+ </option>
+ </select>
+ <input
+ style={
+ Object {
+ "display": "none",
+ }
+ }
+ type="submit"
+ />
+ </form>
+</XHRBreakpoints>
+`;
+
+exports[`XHR Breakpoints should render with 8 expressions passed from props 1`] = `
+<XHRBreakpoints
+ disableXHRBreakpoint={[Function]}
+ enableXHRBreakpoint={[Function]}
+ onXHRAdded={[Function]}
+ removeXHRBreakpoint={[Function]}
+ setXHRBreakpoint={[Function]}
+ shouldPauseOnAny={false}
+ showInput={false}
+ togglePauseOnAny={[Function]}
+ updateXHRBreakpoint={[Function]}
+ xhrBreakpoints={
+ Array [
+ Object {
+ "disabled": false,
+ "loading": false,
+ "method": "ANY",
+ "path": "",
+ "text": "URL contains \\"\\"",
+ },
+ Object {
+ "disabled": false,
+ "loading": false,
+ "method": "ANY",
+ "path": "this is any",
+ "text": "URL contains \\"this is any\\"",
+ },
+ Object {
+ "disabled": false,
+ "loading": false,
+ "method": "GET",
+ "path": "this is get",
+ "text": "URL contains \\"this is get\\"",
+ },
+ Object {
+ "disabled": false,
+ "loading": false,
+ "method": "POST",
+ "path": "this is post",
+ "text": "URL contains \\"this is post\\"",
+ },
+ Object {
+ "disabled": false,
+ "loading": false,
+ "method": "PUT",
+ "path": "this is put",
+ "text": "URL contains \\"this is put\\"",
+ },
+ Object {
+ "disabled": false,
+ "loading": false,
+ "method": "HEAD",
+ "path": "this is head",
+ "text": "URL contains \\"this is head\\"",
+ },
+ Object {
+ "disabled": false,
+ "loading": false,
+ "method": "DELETE",
+ "path": "this is delete",
+ "text": "URL contains \\"this is delete\\"",
+ },
+ Object {
+ "disabled": false,
+ "loading": false,
+ "method": "PATCH",
+ "path": "this is patch",
+ "text": "URL contains \\"this is patch\\"",
+ },
+ Object {
+ "disabled": false,
+ "loading": false,
+ "method": "OPTIONS",
+ "path": "this is options",
+ "text": "URL contains \\"this is options\\"",
+ },
+ ]
+ }
+>
+ <div
+ className="breakpoints-options"
+ >
+ <ExceptionOption
+ className="breakpoints-exceptions"
+ isChecked={false}
+ label="Pause on any URL"
+ onChange={[Function]}
+ >
+ <label
+ className="breakpoints-exceptions"
+ >
+ <input
+ checked={false}
+ onChange={[Function]}
+ type="checkbox"
+ />
+ <div
+ className="breakpoint-exceptions-label"
+ >
+ Pause on any URL
+ </div>
+ </label>
+ </ExceptionOption>
+ </div>
+ <ul
+ className="pane expressions-list"
+ >
+ <li
+ className="xhr-container"
+ key="this is any-ANY"
+ onDoubleClick={[Function]}
+ title="this is any"
+ >
+ <label>
+ <input
+ checked={true}
+ className="xhr-checkbox"
+ onChange={[Function]}
+ onClick={[Function]}
+ type="checkbox"
+ />
+ <div
+ className="xhr-label-method"
+ >
+ ANY
+ </div>
+ <div
+ className="xhr-label-url"
+ >
+ this is any
+ </div>
+ <div
+ className="xhr-container__close-btn"
+ >
+ <CloseButton
+ handleClick={[Function]}
+ >
+ <button
+ className="close-btn"
+ onClick={[Function]}
+ >
+ <AccessibleImage
+ className="close"
+ >
+ <span
+ className="img close"
+ />
+ </AccessibleImage>
+ </button>
+ </CloseButton>
+ </div>
+ </label>
+ </li>
+ <li
+ className="xhr-container"
+ key="this is get-GET"
+ onDoubleClick={[Function]}
+ title="this is get"
+ >
+ <label>
+ <input
+ checked={true}
+ className="xhr-checkbox"
+ onChange={[Function]}
+ onClick={[Function]}
+ type="checkbox"
+ />
+ <div
+ className="xhr-label-method"
+ >
+ GET
+ </div>
+ <div
+ className="xhr-label-url"
+ >
+ this is get
+ </div>
+ <div
+ className="xhr-container__close-btn"
+ >
+ <CloseButton
+ handleClick={[Function]}
+ >
+ <button
+ className="close-btn"
+ onClick={[Function]}
+ >
+ <AccessibleImage
+ className="close"
+ >
+ <span
+ className="img close"
+ />
+ </AccessibleImage>
+ </button>
+ </CloseButton>
+ </div>
+ </label>
+ </li>
+ <li
+ className="xhr-container"
+ key="this is post-POST"
+ onDoubleClick={[Function]}
+ title="this is post"
+ >
+ <label>
+ <input
+ checked={true}
+ className="xhr-checkbox"
+ onChange={[Function]}
+ onClick={[Function]}
+ type="checkbox"
+ />
+ <div
+ className="xhr-label-method"
+ >
+ POST
+ </div>
+ <div
+ className="xhr-label-url"
+ >
+ this is post
+ </div>
+ <div
+ className="xhr-container__close-btn"
+ >
+ <CloseButton
+ handleClick={[Function]}
+ >
+ <button
+ className="close-btn"
+ onClick={[Function]}
+ >
+ <AccessibleImage
+ className="close"
+ >
+ <span
+ className="img close"
+ />
+ </AccessibleImage>
+ </button>
+ </CloseButton>
+ </div>
+ </label>
+ </li>
+ <li
+ className="xhr-container"
+ key="this is put-PUT"
+ onDoubleClick={[Function]}
+ title="this is put"
+ >
+ <label>
+ <input
+ checked={true}
+ className="xhr-checkbox"
+ onChange={[Function]}
+ onClick={[Function]}
+ type="checkbox"
+ />
+ <div
+ className="xhr-label-method"
+ >
+ PUT
+ </div>
+ <div
+ className="xhr-label-url"
+ >
+ this is put
+ </div>
+ <div
+ className="xhr-container__close-btn"
+ >
+ <CloseButton
+ handleClick={[Function]}
+ >
+ <button
+ className="close-btn"
+ onClick={[Function]}
+ >
+ <AccessibleImage
+ className="close"
+ >
+ <span
+ className="img close"
+ />
+ </AccessibleImage>
+ </button>
+ </CloseButton>
+ </div>
+ </label>
+ </li>
+ <li
+ className="xhr-container"
+ key="this is head-HEAD"
+ onDoubleClick={[Function]}
+ title="this is head"
+ >
+ <label>
+ <input
+ checked={true}
+ className="xhr-checkbox"
+ onChange={[Function]}
+ onClick={[Function]}
+ type="checkbox"
+ />
+ <div
+ className="xhr-label-method"
+ >
+ HEAD
+ </div>
+ <div
+ className="xhr-label-url"
+ >
+ this is head
+ </div>
+ <div
+ className="xhr-container__close-btn"
+ >
+ <CloseButton
+ handleClick={[Function]}
+ >
+ <button
+ className="close-btn"
+ onClick={[Function]}
+ >
+ <AccessibleImage
+ className="close"
+ >
+ <span
+ className="img close"
+ />
+ </AccessibleImage>
+ </button>
+ </CloseButton>
+ </div>
+ </label>
+ </li>
+ <li
+ className="xhr-container"
+ key="this is delete-DELETE"
+ onDoubleClick={[Function]}
+ title="this is delete"
+ >
+ <label>
+ <input
+ checked={true}
+ className="xhr-checkbox"
+ onChange={[Function]}
+ onClick={[Function]}
+ type="checkbox"
+ />
+ <div
+ className="xhr-label-method"
+ >
+ DELETE
+ </div>
+ <div
+ className="xhr-label-url"
+ >
+ this is delete
+ </div>
+ <div
+ className="xhr-container__close-btn"
+ >
+ <CloseButton
+ handleClick={[Function]}
+ >
+ <button
+ className="close-btn"
+ onClick={[Function]}
+ >
+ <AccessibleImage
+ className="close"
+ >
+ <span
+ className="img close"
+ />
+ </AccessibleImage>
+ </button>
+ </CloseButton>
+ </div>
+ </label>
+ </li>
+ <li
+ className="xhr-container"
+ key="this is patch-PATCH"
+ onDoubleClick={[Function]}
+ title="this is patch"
+ >
+ <label>
+ <input
+ checked={true}
+ className="xhr-checkbox"
+ onChange={[Function]}
+ onClick={[Function]}
+ type="checkbox"
+ />
+ <div
+ className="xhr-label-method"
+ >
+ PATCH
+ </div>
+ <div
+ className="xhr-label-url"
+ >
+ this is patch
+ </div>
+ <div
+ className="xhr-container__close-btn"
+ >
+ <CloseButton
+ handleClick={[Function]}
+ >
+ <button
+ className="close-btn"
+ onClick={[Function]}
+ >
+ <AccessibleImage
+ className="close"
+ >
+ <span
+ className="img close"
+ />
+ </AccessibleImage>
+ </button>
+ </CloseButton>
+ </div>
+ </label>
+ </li>
+ <li
+ className="xhr-container"
+ key="this is options-OPTIONS"
+ onDoubleClick={[Function]}
+ title="this is options"
+ >
+ <label>
+ <input
+ checked={true}
+ className="xhr-checkbox"
+ onChange={[Function]}
+ onClick={[Function]}
+ type="checkbox"
+ />
+ <div
+ className="xhr-label-method"
+ >
+ OPTIONS
+ </div>
+ <div
+ className="xhr-label-url"
+ >
+ this is options
+ </div>
+ <div
+ className="xhr-container__close-btn"
+ >
+ <CloseButton
+ handleClick={[Function]}
+ >
+ <button
+ className="close-btn"
+ onClick={[Function]}
+ >
+ <AccessibleImage
+ className="close"
+ >
+ <span
+ className="img close"
+ />
+ </AccessibleImage>
+ </button>
+ </CloseButton>
+ </div>
+ </label>
+ </li>
+ </ul>
+</XHRBreakpoints>
+`;
diff --git a/devtools/client/debugger/src/components/ShortcutsModal.css b/devtools/client/debugger/src/components/ShortcutsModal.css
new file mode 100644
index 0000000000..84024f9677
--- /dev/null
+++ b/devtools/client/debugger/src/components/ShortcutsModal.css
@@ -0,0 +1,47 @@
+/* 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/>. */
+
+.shortcuts-content {
+ padding: 15px;
+ column-width: 250px;
+ cursor: default;
+ user-select: none;
+}
+
+.shortcuts-content h2 {
+ margin-top: 2px;
+ margin-bottom: 2px;
+ color: var(--theme-text-color-strong);
+}
+
+.shortcuts-section {
+ display: inline-block;
+ margin: 5px;
+ margin-bottom: 15px;
+ width: 250px;
+}
+
+.shortcuts-list {
+ list-style: none;
+ margin: 0px;
+ padding: 0px;
+ overflow: auto;
+ width: calc(100% - 1px); /* 1px fixes the hidden right border */
+}
+
+.shortcuts-list li {
+ font-size: 12px;
+ color: var(--theme-body-color);
+ padding-top: 5px;
+ display: flex;
+ justify-content: space-between;
+ border: 1px solid transparent;
+ white-space: pre;
+}
+
+@media (max-width: 640px) {
+ .shortcuts-section {
+ width: 100%;
+ }
+}
diff --git a/devtools/client/debugger/src/components/ShortcutsModal.js b/devtools/client/debugger/src/components/ShortcutsModal.js
new file mode 100644
index 0000000000..2e72c1b50f
--- /dev/null
+++ b/devtools/client/debugger/src/components/ShortcutsModal.js
@@ -0,0 +1,155 @@
+/* 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 React, { Component } from "devtools/client/shared/vendor/react";
+import {
+ div,
+ ul,
+ h2,
+ span,
+ li,
+} from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import Modal from "./shared/Modal";
+import { formatKeyShortcut } from "../utils/text";
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+const isMacOS = Services.appinfo.OS === "Darwin";
+
+export class ShortcutsModal extends Component {
+ static get propTypes() {
+ return {
+ enabled: PropTypes.bool.isRequired,
+ handleClose: PropTypes.func.isRequired,
+ };
+ }
+
+ renderPrettyCombos(combo) {
+ return combo
+ .split(" ")
+ .map(c =>
+ span(
+ {
+ key: c,
+ className: "keystroke",
+ },
+ c
+ )
+ )
+ .reduce((prev, curr) => [prev, " + ", curr]);
+ }
+
+ renderShorcutItem(title, combo) {
+ return li(
+ null,
+ span(null, title),
+ span(null, this.renderPrettyCombos(combo))
+ );
+ }
+
+ renderEditorShortcuts() {
+ return ul(
+ {
+ className: "shortcuts-list",
+ },
+ this.renderShorcutItem(
+ L10N.getStr("shortcuts.toggleBreakpoint"),
+ formatKeyShortcut(L10N.getStr("toggleBreakpoint.key"))
+ ),
+ this.renderShorcutItem(
+ L10N.getStr("shortcuts.toggleCondPanel.breakpoint"),
+ formatKeyShortcut(L10N.getStr("toggleCondPanel.breakpoint.key"))
+ ),
+ this.renderShorcutItem(
+ L10N.getStr("shortcuts.toggleCondPanel.logPoint"),
+ formatKeyShortcut(L10N.getStr("toggleCondPanel.logPoint.key"))
+ )
+ );
+ }
+
+ renderSteppingShortcuts() {
+ return ul(
+ {
+ className: "shortcuts-list",
+ },
+ this.renderShorcutItem(L10N.getStr("shortcuts.pauseOrResume"), "F8"),
+ this.renderShorcutItem(L10N.getStr("shortcuts.stepOver"), "F10"),
+ this.renderShorcutItem(L10N.getStr("shortcuts.stepIn"), "F11"),
+ this.renderShorcutItem(
+ L10N.getStr("shortcuts.stepOut"),
+ formatKeyShortcut(L10N.getStr("stepOut.key"))
+ )
+ );
+ }
+
+ renderSearchShortcuts() {
+ return ul(
+ {
+ className: "shortcuts-list",
+ },
+ this.renderShorcutItem(
+ L10N.getStr("shortcuts.fileSearch2"),
+ formatKeyShortcut(L10N.getStr("sources.search.key2"))
+ ),
+ this.renderShorcutItem(
+ L10N.getStr("shortcuts.projectSearch2"),
+ formatKeyShortcut(L10N.getStr("projectTextSearch.key"))
+ ),
+ this.renderShorcutItem(
+ L10N.getStr("shortcuts.functionSearch2"),
+ formatKeyShortcut(L10N.getStr("functionSearch.key"))
+ ),
+ this.renderShorcutItem(
+ L10N.getStr("shortcuts.gotoLine"),
+ formatKeyShortcut(L10N.getStr("gotoLineModal.key3"))
+ )
+ );
+ }
+
+ renderShortcutsContent() {
+ return div(
+ {
+ className: classnames("shortcuts-content", isMacOS ? "mac" : ""),
+ },
+ div(
+ {
+ className: "shortcuts-section",
+ },
+ h2(null, L10N.getStr("shortcuts.header.editor")),
+ this.renderEditorShortcuts()
+ ),
+ div(
+ {
+ className: "shortcuts-section",
+ },
+ h2(null, L10N.getStr("shortcuts.header.stepping")),
+ this.renderSteppingShortcuts()
+ ),
+ div(
+ {
+ className: "shortcuts-section",
+ },
+ h2(null, L10N.getStr("shortcuts.header.search")),
+ this.renderSearchShortcuts()
+ )
+ );
+ }
+
+ render() {
+ const { enabled } = this.props;
+
+ if (!enabled) {
+ return null;
+ }
+
+ return React.createElement(
+ Modal,
+ {
+ additionalClass: "shortcuts-modal",
+ handleClose: this.props.handleClose,
+ },
+ this.renderShortcutsContent()
+ );
+ }
+}
diff --git a/devtools/client/debugger/src/components/WelcomeBox.css b/devtools/client/debugger/src/components/WelcomeBox.css
new file mode 100644
index 0000000000..a0932625ae
--- /dev/null
+++ b/devtools/client/debugger/src/components/WelcomeBox.css
@@ -0,0 +1,83 @@
+/* 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/>. */
+
+.welcomebox {
+ position: absolute;
+ top: var(--editor-header-height);
+ left: 0;
+ bottom: var(--editor-footer-height);
+ width: calc(100% - 1px);
+ padding: 10vh 0;
+ background-color: var(--theme-toolbar-background);
+ overflow: hidden;
+ font-weight: 300;
+ z-index: 10;
+ user-select: none;
+}
+
+.theme-dark .welcomebox {
+ background-color: var(--theme-body-background);
+}
+
+.alignlabel {
+ display: flex;
+ white-space: nowrap;
+ font-size: 1.25em;
+}
+
+.shortcutKey,
+.shortcutLabel {
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ cursor: pointer;
+}
+
+.welcomebox__searchSources:hover,
+.welcomebox__searchProject:hover,
+.welcomebox__allShortcuts:hover {
+ color: var(--theme-body-color);
+}
+
+.shortcutKey {
+ direction: ltr;
+ text-align: right;
+ padding-right: 10px;
+ font-family: var(--monospace-font-family);
+ font-size: 14px;
+ line-height: 18px;
+ color: var(--theme-body-color);
+}
+
+.shortcutKey:dir(rtl) {
+ text-align: left;
+}
+
+:root[platform="mac"] .welcomebox .shortcutKey {
+ font-family: system-ui, -apple-system, sans-serif;
+ font-weight: 500;
+}
+
+.shortcutLabel {
+ text-align: start;
+ padding-left: 10px;
+ font-size: 14px;
+ line-height: 18px;
+}
+
+.shortcutFunction {
+ margin: 0 auto;
+ color: var(--theme-comment);
+ display: table;
+}
+
+.shortcutFunction p {
+ display: table-row;
+}
+
+.shortcutFunction .shortcutKey,
+.shortcutFunction .shortcutLabel {
+ padding: 10px 5px;
+ display: table-cell;
+}
diff --git a/devtools/client/debugger/src/components/WelcomeBox.js b/devtools/client/debugger/src/components/WelcomeBox.js
new file mode 100644
index 0000000000..cdb1d6c23e
--- /dev/null
+++ b/devtools/client/debugger/src/components/WelcomeBox.js
@@ -0,0 +1,137 @@
+/* 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 { Component } from "devtools/client/shared/vendor/react";
+import {
+ div,
+ p,
+ span,
+} from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import { primaryPaneTabs } from "../constants";
+
+import actions from "../actions/index";
+import { getPaneCollapse } from "../selectors/index";
+import { formatKeyShortcut } from "../utils/text";
+
+export class WelcomeBox extends Component {
+ static get propTypes() {
+ return {
+ openQuickOpen: PropTypes.func.isRequired,
+ setActiveSearch: PropTypes.func.isRequired,
+ toggleShortcutsModal: PropTypes.func.isRequired,
+ setPrimaryPaneTab: PropTypes.func.isRequired,
+ };
+ }
+
+ render() {
+ const searchSourcesShortcut = formatKeyShortcut(
+ L10N.getStr("sources.search.key2")
+ );
+
+ const searchProjectShortcut = formatKeyShortcut(
+ L10N.getStr("projectTextSearch.key")
+ );
+
+ const allShortcutsShortcut = formatKeyShortcut(
+ L10N.getStr("allShortcut.key")
+ );
+
+ const allShortcutsLabel = L10N.getStr("welcome.allShortcuts");
+ const searchSourcesLabel = L10N.getStr("welcome.search2").substring(2);
+ const searchProjectLabel = L10N.getStr("welcome.findInFiles2").substring(2);
+
+ return div(
+ {
+ className: "welcomebox",
+ },
+ div(
+ {
+ className: "alignlabel",
+ },
+ div(
+ {
+ className: "shortcutFunction",
+ },
+ p(
+ {
+ className: "welcomebox__searchSources",
+ role: "button",
+ tabIndex: "0",
+ onClick: () => this.props.openQuickOpen(),
+ },
+ span(
+ {
+ className: "shortcutKey",
+ },
+ searchSourcesShortcut
+ ),
+ span(
+ {
+ className: "shortcutLabel",
+ },
+ searchSourcesLabel
+ )
+ ),
+ p(
+ {
+ className: "welcomebox__searchProject",
+ role: "button",
+ tabIndex: "0",
+ onClick: () => {
+ this.props.setActiveSearch(primaryPaneTabs.PROJECT_SEARCH);
+ this.props.setPrimaryPaneTab(primaryPaneTabs.PROJECT_SEARCH);
+ },
+ },
+ span(
+ {
+ className: "shortcutKey",
+ },
+ searchProjectShortcut
+ ),
+ span(
+ {
+ className: "shortcutLabel",
+ },
+ searchProjectLabel
+ )
+ ),
+ p(
+ {
+ className: "welcomebox__allShortcuts",
+ role: "button",
+ tabIndex: "0",
+ onClick: () => this.props.toggleShortcutsModal(),
+ },
+ span(
+ {
+ className: "shortcutKey",
+ },
+ allShortcutsShortcut
+ ),
+ span(
+ {
+ className: "shortcutLabel",
+ },
+ allShortcutsLabel
+ )
+ )
+ )
+ )
+ );
+ }
+}
+
+const mapStateToProps = state => ({
+ endPanelCollapsed: getPaneCollapse(state, "end"),
+});
+
+export default connect(mapStateToProps, {
+ togglePaneCollapse: actions.togglePaneCollapse,
+ setActiveSearch: actions.setActiveSearch,
+ openQuickOpen: actions.openQuickOpen,
+ setPrimaryPaneTab: actions.setPrimaryPaneTab,
+})(WelcomeBox);
diff --git a/devtools/client/debugger/src/components/moz.build b/devtools/client/debugger/src/components/moz.build
new file mode 100644
index 0000000000..6cd81e653b
--- /dev/null
+++ b/devtools/client/debugger/src/components/moz.build
@@ -0,0 +1,18 @@
+# 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 += [
+ "Editor",
+ "PrimaryPanes",
+ "SecondaryPanes",
+ "shared",
+]
+
+CompiledModules(
+ "App.js",
+ "QuickOpenModal.js",
+ "ShortcutsModal.js",
+ "WelcomeBox.js",
+)
diff --git a/devtools/client/debugger/src/components/shared/AccessibleImage.css b/devtools/client/debugger/src/components/shared/AccessibleImage.css
new file mode 100644
index 0000000000..4ba5f1326a
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/AccessibleImage.css
@@ -0,0 +1,201 @@
+/* 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/>. */
+
+.img {
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ vertical-align: middle;
+ /* use background-color for the icon color, and mask-image for its shape */
+ background-color: var(--theme-icon-color);
+ mask-size: contain;
+ mask-repeat: no-repeat;
+ mask-position: center;
+ /* multicolor icons use background-image */
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: contain;
+ /* do not let images shrink when used as flex children */
+ flex-shrink: 0;
+}
+
+/* Expand arrow icon */
+.img.arrow {
+ width: 10px;
+ height: 10px;
+ mask-image: url(chrome://devtools/content/debugger/images/arrow.svg);
+ /* we may override the width/height in specific contexts to make the
+ clickable area bigger, but we should always keep the mask size 10x10 */
+ mask-size: 10px 10px;
+ background-color: var(--theme-icon-dimmed-color);
+ transform: rotate(-90deg);
+ transition: transform 180ms var(--animation-curve);
+}
+
+.img.arrow:dir(rtl) {
+ transform: rotate(90deg);
+}
+
+.img.arrow.expanded {
+ /* icon should always point to the bottom (default) when expanded,
+ regardless of the text direction */
+ transform: none !important;
+}
+
+.img.arrow-down {
+ mask-image: url(chrome://devtools/content/debugger/images/arrow-down.svg);
+}
+
+.img.arrow-up {
+ mask-image: url(chrome://devtools/content/debugger/images/arrow-up.svg);
+}
+
+.img.blackBox {
+ mask-image: url(chrome://devtools/content/debugger/images/blackBox.svg);
+}
+
+.img.breadcrumb {
+ mask-image: url(chrome://devtools/content/debugger/images/breadcrumbs-divider.svg);
+}
+
+.img.close {
+ mask-image: url(chrome://devtools/skin/images/close.svg);
+}
+
+.img.disable-pausing {
+ mask-image: url(chrome://devtools/content/debugger/images/disable-pausing.svg);
+}
+
+.img.enable-pausing {
+ mask-image: url(chrome://devtools/content/debugger/images/enable-pausing.svg);
+ background-color: var(--theme-icon-checked-color);
+}
+
+.img.globe {
+ mask-image: url(chrome://devtools/content/debugger/images/globe.svg);
+}
+
+.img.globe-small {
+ mask-image: url(chrome://devtools/content/debugger/images/globe-small.svg);
+ mask-size: 12px 12px;
+}
+
+.img.window {
+ mask-image: url(chrome://devtools/content/debugger/images/window.svg);
+}
+
+.img.file {
+ mask-image: url(chrome://devtools/content/debugger/images/file-small.svg);
+ mask-size: 12px 12px;
+}
+
+.img.folder {
+ mask-image: url(chrome://devtools/content/debugger/images/folder.svg);
+}
+
+.img.home {
+ mask-image: url(chrome://devtools/content/debugger/images/home.svg);
+}
+
+.img.info {
+ mask-image: url(chrome://devtools/skin/images/info.svg);
+}
+
+.img.loader {
+ background-image: url(chrome://devtools/content/debugger/images/loader.svg);
+ -moz-context-properties: fill;
+ fill: var(--theme-icon-color);
+ background-color: unset;
+}
+
+.img.more-tabs {
+ mask-image: url(chrome://devtools/content/debugger/images/command-chevron.svg);
+}
+
+html[dir="rtl"] .img.more-tabs {
+ transform: scaleX(-1);
+}
+
+.img.sourcemap {
+ background-image: url(chrome://devtools/content/debugger/images/sourcemap.svg);
+ -moz-context-properties: fill;
+ fill: var(--theme-icon-warning-color);
+ background-color: unset;
+}
+
+.img.next {
+ mask-image: url(chrome://devtools/content/debugger/images/next.svg);
+}
+
+.img.next-circle {
+ mask-image: url(chrome://devtools/content/debugger/images/next-circle.svg);
+}
+
+.img.pane-collapse {
+ mask-image: url(chrome://devtools/content/debugger/images/pane-collapse.svg);
+}
+
+.img.pane-expand {
+ mask-image: url(chrome://devtools/content/debugger/images/pane-expand.svg);
+}
+
+.img.pause {
+ mask-image: url(chrome://devtools/content/debugger/images/pause.svg);
+}
+
+.img.plus {
+ mask-image: url(chrome://devtools/skin/images/add.svg);
+}
+
+.img.prettyPrint {
+ background-image: url(chrome://devtools/content/debugger/images/prettyPrint.svg);
+ background-size: 14px 14px;
+ background-color: transparent !important;
+ fill: var(--theme-icon-color);
+ -moz-context-properties: fill;
+}
+
+.img.removeAll {
+ mask-image: url(chrome://devtools/skin/images/clear.svg)
+}
+
+.img.refresh {
+ mask-image: url(chrome://devtools/skin/images/reload.svg);
+}
+
+.img.resume {
+ mask-image: url(resource://devtools-shared-images/resume.svg);
+}
+
+.img.search {
+ mask-image: url(chrome://devtools/content/debugger/images/search.svg);
+}
+
+.img.shortcuts {
+ mask-image: url(chrome://devtools/content/debugger/images/help.svg);
+}
+
+.img.spin {
+ animation: spin 0.5s linear infinite;
+}
+
+.img.stepIn {
+ mask-image: url(chrome://devtools/content/debugger/images/stepIn.svg);
+}
+
+.img.stepOut {
+ mask-image: url(chrome://devtools/content/debugger/images/stepOut.svg);
+}
+
+.img.stepOver {
+ mask-image: url(resource://devtools-shared-images/stepOver.svg);
+}
+
+.img.tab {
+ mask-image: url(chrome://devtools/content/debugger/images/tab.svg);
+}
+
+.img.worker {
+ mask-image: url(chrome://devtools/content/debugger/images/worker.svg);
+}
diff --git a/devtools/client/debugger/src/components/shared/AccessibleImage.js b/devtools/client/debugger/src/components/shared/AccessibleImage.js
new file mode 100644
index 0000000000..e3a59573ea
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/AccessibleImage.js
@@ -0,0 +1,21 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+import React from "devtools/client/shared/vendor/react";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+const AccessibleImage = props => {
+ return React.createElement("span", {
+ ...props,
+ className: classnames("img", props.className),
+ });
+};
+
+AccessibleImage.propTypes = {
+ className: PropTypes.string.isRequired,
+};
+
+export default AccessibleImage;
diff --git a/devtools/client/debugger/src/components/shared/Accordion.css b/devtools/client/debugger/src/components/shared/Accordion.css
new file mode 100644
index 0000000000..d970527014
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Accordion.css
@@ -0,0 +1,107 @@
+/* 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/>. */
+
+.accordion {
+ background-color: var(--theme-sidebar-background);
+ width: 100%;
+ list-style-type: none;
+ padding: 0px;
+ margin-top: 0px;
+}
+
+.accordion ._header {
+ background-color: var(--theme-accordion-header-background);
+ border-bottom: 1px solid var(--theme-splitter-color);
+ display: flex;
+ column-gap: 8px;
+ font-size: 12px;
+ line-height: calc(16 / 12);
+ padding: 4px 6px;
+ width: 100%;
+ align-items: center;
+ margin: 0px;
+ font-weight: normal;
+ cursor: default;
+ user-select: none;
+}
+
+.accordion ._header:hover {
+ background-color: var(--theme-accordion-header-hover);
+}
+
+
+.accordion ._header .header-label {
+ flex-grow: 1;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ color: var(--theme-toolbar-color);
+ background: transparent;
+ padding: 0;
+
+ /* align expand arrow and button text */
+ display: flex;
+ align-items: center;
+ gap: 4px;
+
+ &:hover {
+ background: transparent;
+ }
+
+ /* The expand arrow needs to be displayed inside the button to be accessible */
+ &::before {
+ content: "";
+ display: inline-block;
+ width: 10px;
+ height: 10px;
+ background-image: url(chrome://devtools/content/debugger/images/arrow.svg);
+ background-size: contain;
+ -moz-context-properties: fill;
+ fill: var(--theme-icon-dimmed-color);
+ rotate: -90deg;
+ transition: rotate 180ms var(--animation-curve);
+
+ &:dir(rtl) {
+ rotate: 90deg;
+ }
+ }
+
+ &[aria-expanded="true"]::before {
+ /* icon should always point to the bottom (default) when expanded,
+ regardless of the text direction */
+ rotate: 0deg !important;
+ }
+}
+
+.accordion ._header .header-buttons {
+ display: flex;
+ margin-inline-start: auto;
+}
+
+.accordion ._header .header-buttons button {
+ color: var(--theme-body-color);
+ border: none;
+ background: none;
+ padding: 0;
+ margin: 0 2px;
+ width: 16px;
+ height: 16px;
+}
+
+.accordion ._header .header-buttons button::-moz-focus-inner {
+ border: none;
+}
+
+.accordion ._header .header-buttons button .img {
+ display: block;
+}
+
+.accordion ._content {
+ border-bottom: 1px solid var(--theme-splitter-color);
+ font-size: var(--theme-body-font-size);
+}
+
+.accordion div:last-child ._content {
+ border-bottom: none;
+}
diff --git a/devtools/client/debugger/src/components/shared/Accordion.js b/devtools/client/debugger/src/components/shared/Accordion.js
new file mode 100644
index 0000000000..3b5d5ae516
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Accordion.js
@@ -0,0 +1,89 @@
+/* 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 { cloneElement, Component } from "devtools/client/shared/vendor/react";
+import {
+ aside,
+ button,
+ div,
+ h2,
+} from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+class Accordion extends Component {
+ static get propTypes() {
+ return {
+ items: PropTypes.array.isRequired,
+ };
+ }
+
+ handleHeaderClick(i) {
+ const item = this.props.items[i];
+ const opened = !item.opened;
+ item.opened = opened;
+
+ if (item.onToggle) {
+ item.onToggle(opened);
+ }
+
+ // We force an update because otherwise the accordion
+ // would not re-render
+ this.forceUpdate();
+ }
+
+ renderContainer = (item, i) => {
+ const { opened } = item;
+ const contentElementId = `${item.id}-content`;
+
+ return aside(
+ {
+ className: item.className,
+ key: item.id,
+ "aria-labelledby": item.id,
+ role: item.role,
+ },
+ h2(
+ {
+ className: "_header",
+ },
+ button(
+ {
+ id: item.id,
+ className: "header-label",
+ "aria-expanded": `${opened ? "true" : "false"}`,
+ "aria-controls": opened ? contentElementId : undefined,
+ onClick: () => this.handleHeaderClick(i),
+ },
+ item.header
+ ),
+ item.buttons
+ ? div(
+ {
+ className: "header-buttons",
+ },
+ item.buttons
+ )
+ : null
+ ),
+ opened &&
+ div(
+ {
+ className: "_content",
+ id: contentElementId,
+ },
+ cloneElement(item.component, item.componentProps || {})
+ )
+ );
+ };
+ render() {
+ return div(
+ {
+ className: "accordion",
+ },
+ this.props.items.map(this.renderContainer)
+ );
+ }
+}
+
+export default Accordion;
diff --git a/devtools/client/debugger/src/components/shared/Badge.css b/devtools/client/debugger/src/components/shared/Badge.css
new file mode 100644
index 0000000000..f52d32edf4
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Badge.css
@@ -0,0 +1,16 @@
+/* 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/>. */
+
+.badge {
+ --size: 17px;
+ --radius: calc(var(--size) / 2);
+ height: var(--size);
+ min-width: var(--size);
+ line-height: var(--size);
+ background: var(--theme-toolbar-background-hover);
+ color: var(--theme-body-color);
+ border-radius: var(--radius);
+ padding: 0 4px;
+ font-size: 0.9em;
+}
diff --git a/devtools/client/debugger/src/components/shared/Badge.js b/devtools/client/debugger/src/components/shared/Badge.js
new file mode 100644
index 0000000000..72571c0f58
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Badge.js
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+import React from "devtools/client/shared/vendor/react";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+class Badge extends React.Component {
+ constructor(props) {
+ super(props);
+ }
+
+ static get propTypes() {
+ return {
+ badgeText: PropTypes.node.isRequired,
+ };
+ }
+
+ render() {
+ return React.createElement(
+ "span",
+ {
+ className: "badge text-white text-center",
+ },
+ this.props.badgeText
+ );
+ }
+}
+
+export default Badge;
diff --git a/devtools/client/debugger/src/components/shared/BracketArrow.css b/devtools/client/debugger/src/components/shared/BracketArrow.css
new file mode 100644
index 0000000000..afca888371
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/BracketArrow.css
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+.bracket-arrow {
+ position: absolute;
+ pointer-events: none;
+}
+
+.bracket-arrow::before,
+.bracket-arrow::after {
+ content: "";
+ height: 0;
+ width: 0;
+ position: absolute;
+ border: 7px solid transparent;
+}
+
+.bracket-arrow.up::before {
+ border-bottom-color: var(--theme-splitter-color);
+ top: -1px;
+}
+
+.theme-dark .bracket-arrow.up::before {
+ border-bottom-color: var(--theme-body-color);
+}
+
+.bracket-arrow.up::after {
+ border-bottom-color: var(--theme-body-background);
+ top: 0px;
+}
+
+.bracket-arrow.down::before {
+ border-bottom-color: transparent;
+ border-top-color: var(--theme-splitter-color);
+ top: 0px;
+}
+
+.theme-dark .bracket-arrow.down::before {
+ border-top-color: var(--theme-body-color);
+}
+
+.bracket-arrow.down::after {
+ border-bottom-color: transparent;
+ border-top-color: var(--theme-body-background);
+ top: -1px;
+}
+
+.bracket-arrow.left::before {
+ border-left-color: transparent;
+ border-right-color: var(--theme-splitter-color);
+ top: 0px;
+}
+
+.theme-dark .bracket-arrow.left::before {
+ border-right-color: var(--theme-body-color);
+}
+
+.bracket-arrow.left::after {
+ border-left-color: transparent;
+ border-right-color: var(--theme-body-background);
+ top: 0px;
+ left: 1px;
+}
diff --git a/devtools/client/debugger/src/components/shared/BracketArrow.js b/devtools/client/debugger/src/components/shared/BracketArrow.js
new file mode 100644
index 0000000000..40e2cda6c4
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/BracketArrow.js
@@ -0,0 +1,28 @@
+/* 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 { div } from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+const BracketArrow = ({ orientation, left, top, bottom }) => {
+ return div({
+ className: classnames("bracket-arrow", orientation || "up"),
+ style: {
+ left,
+ top,
+ bottom,
+ },
+ });
+};
+
+BracketArrow.propTypes = {
+ bottom: PropTypes.number,
+ left: PropTypes.number,
+ orientation: PropTypes.string.isRequired,
+ top: PropTypes.number,
+};
+
+export default BracketArrow;
diff --git a/devtools/client/debugger/src/components/shared/Button/CloseButton.js b/devtools/client/debugger/src/components/shared/Button/CloseButton.js
new file mode 100644
index 0000000000..a8f66de60d
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/CloseButton.js
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+import React from "devtools/client/shared/vendor/react";
+import { button } from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+import AccessibleImage from "../AccessibleImage";
+
+function CloseButton({ handleClick, buttonClass, tooltip }) {
+ return button(
+ {
+ className: buttonClass ? `close-btn ${buttonClass}` : "close-btn",
+ onClick: handleClick,
+ title: tooltip,
+ },
+ React.createElement(AccessibleImage, {
+ className: "close",
+ })
+ );
+}
+
+CloseButton.propTypes = {
+ buttonClass: PropTypes.string,
+ handleClick: PropTypes.func.isRequired,
+ tooltip: PropTypes.string,
+};
+
+export default CloseButton;
diff --git a/devtools/client/debugger/src/components/shared/Button/CommandBarButton.js b/devtools/client/debugger/src/components/shared/Button/CommandBarButton.js
new file mode 100644
index 0000000000..4b0b52e186
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/CommandBarButton.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 React from "devtools/client/shared/vendor/react";
+import { button } from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+import AccessibleImage from "../AccessibleImage";
+
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+export function debugBtn(
+ onClick,
+ type,
+ className,
+ tooltip,
+ disabled = false,
+ ariaPressed = false
+) {
+ return React.createElement(
+ CommandBarButton,
+ {
+ className: classnames(type, className),
+ disabled: disabled,
+ key: type,
+ onClick: onClick,
+ pressed: ariaPressed,
+ title: tooltip,
+ },
+ React.createElement(AccessibleImage, {
+ className: type,
+ })
+ );
+}
+const CommandBarButton = props => {
+ const { children, className, pressed = false, ...rest } = props;
+
+ return button(
+ {
+ "aria-pressed": pressed,
+ className: classnames("command-bar-button", className),
+ ...rest,
+ },
+ children
+ );
+};
+
+CommandBarButton.propTypes = {
+ children: PropTypes.node.isRequired,
+ className: PropTypes.string.isRequired,
+ pressed: PropTypes.bool,
+};
+
+export default CommandBarButton;
diff --git a/devtools/client/debugger/src/components/shared/Button/PaneToggleButton.js b/devtools/client/debugger/src/components/shared/Button/PaneToggleButton.js
new file mode 100644
index 0000000000..ad003552ad
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/PaneToggleButton.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 React, { PureComponent } from "devtools/client/shared/vendor/react";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import AccessibleImage from "../AccessibleImage";
+import { CommandBarButton } from "./index";
+
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+class PaneToggleButton extends PureComponent {
+ static defaultProps = {
+ horizontal: false,
+ position: "start",
+ };
+
+ static get propTypes() {
+ return {
+ collapsed: PropTypes.bool.isRequired,
+ handleClick: PropTypes.func.isRequired,
+ horizontal: PropTypes.bool.isRequired,
+ position: PropTypes.oneOf(["start", "end"]).isRequired,
+ };
+ }
+
+ label(position, collapsed) {
+ switch (position) {
+ case "start":
+ return L10N.getStr(collapsed ? "expandSources" : "collapseSources");
+ case "end":
+ return L10N.getStr(
+ collapsed ? "expandBreakpoints" : "collapseBreakpoints"
+ );
+ }
+ return null;
+ }
+
+ render() {
+ const { position, collapsed, horizontal, handleClick } = this.props;
+ return React.createElement(
+ CommandBarButton,
+ {
+ className: classnames("toggle-button", position, {
+ collapsed,
+ vertical: !horizontal,
+ }),
+ onClick: () => handleClick(position, !collapsed),
+ title: this.label(position, collapsed),
+ },
+ React.createElement(AccessibleImage, {
+ className: collapsed ? "pane-expand" : "pane-collapse",
+ })
+ );
+ }
+}
+
+export default PaneToggleButton;
diff --git a/devtools/client/debugger/src/components/shared/Button/index.js b/devtools/client/debugger/src/components/shared/Button/index.js
new file mode 100644
index 0000000000..df7976ba90
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/index.js
@@ -0,0 +1,9 @@
+/* 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 CloseButton from "./CloseButton";
+import CommandBarButton, { debugBtn } from "./CommandBarButton";
+import PaneToggleButton from "./PaneToggleButton";
+
+export { CloseButton, CommandBarButton, debugBtn, PaneToggleButton };
diff --git a/devtools/client/debugger/src/components/shared/Button/moz.build b/devtools/client/debugger/src/components/shared/Button/moz.build
new file mode 100644
index 0000000000..c6e652d5dc
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/moz.build
@@ -0,0 +1,15 @@
+# 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 += [
+ "styles",
+]
+
+CompiledModules(
+ "CloseButton.js",
+ "CommandBarButton.js",
+ "index.js",
+ "PaneToggleButton.js",
+)
diff --git a/devtools/client/debugger/src/components/shared/Button/styles/CloseButton.css b/devtools/client/debugger/src/components/shared/Button/styles/CloseButton.css
new file mode 100644
index 0000000000..c2d8df6d38
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/styles/CloseButton.css
@@ -0,0 +1,35 @@
+/* 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/>. */
+
+.close-btn {
+ width: 16px;
+ height: 16px;
+ border: 1px solid transparent;
+ border-radius: 2px;
+ padding: 1px;
+ color: var(--theme-icon-color);
+}
+
+.close-btn:hover {
+ color: var(--theme-selection-color);
+ background-color: var(--theme-selection-background);
+}
+
+.close-btn .img {
+ display: block;
+ width: 12px;
+ height: 12px;
+ /* inherit the button's text color for the icon's color */
+ background-color: currentColor;
+}
+
+.close-btn.big {
+ width: 20px;
+ height: 20px;
+}
+
+.close-btn.big .img {
+ width: 16px;
+ height: 16px;
+}
diff --git a/devtools/client/debugger/src/components/shared/Button/styles/CommandBarButton.css b/devtools/client/debugger/src/components/shared/Button/styles/CommandBarButton.css
new file mode 100644
index 0000000000..12e53e6fc5
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/styles/CommandBarButton.css
@@ -0,0 +1,73 @@
+/* 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/>. */
+
+.command-bar-button {
+ appearance: none;
+ background: transparent;
+ border: none;
+ display: inline-block;
+ text-align: center;
+ position: relative;
+ padding: 0px 5px;
+ fill: currentColor;
+ min-width: 30px;
+ /* Adjust outline so it's not clipped */
+ outline-offset: -3px;
+}
+
+.command-bar-button:disabled {
+ opacity: 0.6;
+ cursor: default;
+}
+
+.command-bar-button:not(.disabled):hover,
+.devtools-button.debugger-settings-menu-button:empty:enabled:not([aria-expanded="true"]):hover {
+ background: var(--theme-toolbar-background-hover);
+}
+
+.theme-dark .command-bar-button:not(.disabled):hover,
+.devtools-button.debugger-settings-menu-button:empty:enabled:not([aria-expanded="true"]):hover {
+ background: var(--theme-toolbar-hover);
+}
+
+:root.theme-dark .command-bar-button {
+ color: var(--theme-body-color);
+}
+
+.command-bar-button > * {
+ width: 16px;
+ height: 16px;
+ display: inline-block;
+ vertical-align: middle;
+}
+
+/**
+ * Settings icon and menu
+ */
+.devtools-button.debugger-settings-menu-button {
+ border-radius: 0;
+ margin: 0;
+ padding: 0;
+}
+
+.devtools-button.debugger-settings-menu-button::before {
+ background-image: url("chrome://devtools/skin/images/settings.svg");
+}
+
+.devtools-button.debugger-trace-menu-button::before {
+ background-image: url(chrome://devtools/content/debugger/images/trace.svg);
+}
+.devtools-button.debugger-trace-menu-button:is(.active, .pending)::before {
+ fill: var(--theme-icon-checked-color);
+}
+.devtools-button.debugger-trace-menu-button.pending::after
+{
+ content: url("chrome://global/skin/icons/badge-blue.svg");
+ width: 14px;
+ height: 14px;
+ display: block;
+ position: absolute;
+ bottom: -2px;
+ right: 0;
+}
diff --git a/devtools/client/debugger/src/components/shared/Button/styles/PaneToggleButton.css b/devtools/client/debugger/src/components/shared/Button/styles/PaneToggleButton.css
new file mode 100644
index 0000000000..d8a2495408
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/styles/PaneToggleButton.css
@@ -0,0 +1,29 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+.toggle-button {
+ padding: 4px 6px;
+}
+
+.toggle-button .img {
+ vertical-align: middle;
+}
+
+.toggle-button.end {
+ margin-inline-end: 0px;
+ margin-inline-start: auto;
+}
+
+.toggle-button.start {
+ margin-inline-start: 0px;
+}
+
+html[dir="rtl"] .toggle-button.start .img,
+html[dir="ltr"] .toggle-button.end:not(.vertical) .img {
+ transform: scaleX(-1);
+}
+
+.toggle-button.end.vertical .img {
+ transform: rotate(-90deg);
+}
diff --git a/devtools/client/debugger/src/components/shared/Button/styles/moz.build b/devtools/client/debugger/src/components/shared/Button/styles/moz.build
new file mode 100644
index 0000000000..7d80140dbe
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/styles/moz.build
@@ -0,0 +1,8 @@
+# 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()
diff --git a/devtools/client/debugger/src/components/shared/Button/tests/CloseButton.spec.js b/devtools/client/debugger/src/components/shared/Button/tests/CloseButton.spec.js
new file mode 100644
index 0000000000..5e448881d9
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/tests/CloseButton.spec.js
@@ -0,0 +1,31 @@
+/* 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 React from "devtools/client/shared/vendor/react";
+import { shallow } from "enzyme";
+import { CloseButton } from "../";
+
+describe("CloseButton", () => {
+ it("renders with tooltip", () => {
+ const tooltip = "testTooltip";
+ const wrapper = shallow(
+ React.createElement(CloseButton, {
+ tooltip: tooltip,
+ handleClick: () => {},
+ })
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("handles click event", () => {
+ const handleClickSpy = jest.fn();
+ const wrapper = shallow(
+ React.createElement(CloseButton, {
+ handleClick: handleClickSpy,
+ })
+ );
+ wrapper.simulate("click");
+ expect(handleClickSpy).toHaveBeenCalled();
+ });
+});
diff --git a/devtools/client/debugger/src/components/shared/Button/tests/CommandBarButton.spec.js b/devtools/client/debugger/src/components/shared/Button/tests/CommandBarButton.spec.js
new file mode 100644
index 0000000000..41537cf8e4
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/tests/CommandBarButton.spec.js
@@ -0,0 +1,44 @@
+/* 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 React from "devtools/client/shared/vendor/react";
+import { shallow } from "enzyme";
+import { CommandBarButton, debugBtn } from "../";
+
+describe("CommandBarButton", () => {
+ it("renders", () => {
+ const wrapper = shallow(
+ React.createElement(CommandBarButton, {
+ children: [],
+ className: "",
+ })
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("renders children", () => {
+ const children = [1, 2, 3, 4];
+ const wrapper = shallow(
+ React.createElement(CommandBarButton, {
+ children: children,
+ className: "",
+ })
+ );
+ expect(wrapper.find("button").children()).toHaveLength(4);
+ });
+});
+
+describe("debugBtn", () => {
+ it("renders", () => {
+ const wrapper = shallow(debugBtn());
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("handles onClick", () => {
+ const onClickSpy = jest.fn();
+ const wrapper = shallow(debugBtn(onClickSpy));
+ wrapper.simulate("click");
+ expect(onClickSpy).toHaveBeenCalled();
+ });
+});
diff --git a/devtools/client/debugger/src/components/shared/Button/tests/PaneToggleButton.spec.js b/devtools/client/debugger/src/components/shared/Button/tests/PaneToggleButton.spec.js
new file mode 100644
index 0000000000..89b548379d
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/tests/PaneToggleButton.spec.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/>. */
+
+import React from "devtools/client/shared/vendor/react";
+import { shallow } from "enzyme";
+import { PaneToggleButton } from "../";
+
+describe("PaneToggleButton", () => {
+ const handleClickSpy = jest.fn();
+ const wrapper = shallow(
+ React.createElement(PaneToggleButton, {
+ handleClick: handleClickSpy,
+ collapsed: false,
+ position: "start",
+ })
+ );
+
+ it("renders default", () => {
+ expect(wrapper.hasClass("vertical")).toBe(true);
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("toggles horizontal class", () => {
+ wrapper.setProps({ horizontal: true });
+ expect(wrapper.hasClass("vertical")).toBe(false);
+ });
+
+ it("toggles collapsed class", () => {
+ wrapper.setProps({ collapsed: true });
+ expect(wrapper.hasClass("collapsed")).toBe(true);
+ });
+
+ it("toggles start position", () => {
+ wrapper.setProps({ position: "start" });
+ expect(wrapper.hasClass("start")).toBe(true);
+ });
+
+ it("toggles end position", () => {
+ wrapper.setProps({ position: "end" });
+ expect(wrapper.hasClass("end")).toBe(true);
+ });
+
+ it("handleClick is called", () => {
+ const position = "end";
+ const collapsed = false;
+ wrapper.setProps({ position, collapsed });
+ wrapper.simulate("click");
+ expect(handleClickSpy).toHaveBeenCalledWith(position, true);
+ });
+});
diff --git a/devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/CloseButton.spec.js.snap b/devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/CloseButton.spec.js.snap
new file mode 100644
index 0000000000..d0a0cb9967
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/CloseButton.spec.js.snap
@@ -0,0 +1,13 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`CloseButton renders with tooltip 1`] = `
+<button
+ className="close-btn"
+ onClick={[Function]}
+ title="testTooltip"
+>
+ <AccessibleImage
+ className="close"
+ />
+</button>
+`;
diff --git a/devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/CommandBarButton.spec.js.snap b/devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/CommandBarButton.spec.js.snap
new file mode 100644
index 0000000000..cebcb5892c
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/CommandBarButton.spec.js.snap
@@ -0,0 +1,18 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`CommandBarButton renders 1`] = `
+<button
+ aria-pressed={false}
+ className="command-bar-button"
+/>
+`;
+
+exports[`debugBtn renders 1`] = `
+<button
+ aria-pressed={false}
+ className="command-bar-button"
+ disabled={false}
+>
+ <AccessibleImage />
+</button>
+`;
diff --git a/devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/PaneToggleButton.spec.js.snap b/devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/PaneToggleButton.spec.js.snap
new file mode 100644
index 0000000000..86067066a6
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Button/tests/__snapshots__/PaneToggleButton.spec.js.snap
@@ -0,0 +1,13 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`PaneToggleButton renders default 1`] = `
+<CommandBarButton
+ className="toggle-button start vertical"
+ onClick={[Function]}
+ title="Collapse Sources and Outline panes"
+>
+ <AccessibleImage
+ className="pane-collapse"
+ />
+</CommandBarButton>
+`;
diff --git a/devtools/client/debugger/src/components/shared/Dropdown.css b/devtools/client/debugger/src/components/shared/Dropdown.css
new file mode 100644
index 0000000000..bb9295b296
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Dropdown.css
@@ -0,0 +1,97 @@
+/* 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/>. */
+
+.dropdown {
+ background: var(--theme-body-background);
+ border: 1px solid var(--theme-splitter-color);
+ border-radius: 4px;
+ box-shadow: 0 4px 4px 0 var(--search-overlays-semitransparent);
+ max-height: 300px;
+ position: absolute;
+ top: 24px;
+ width: 150px;
+ z-index: 1000;
+ overflow: auto;
+}
+
+[dir="ltr"] .dropdown {
+ right: 2px;
+}
+
+[dir="rtl"] .dropdown {
+ left: 2px;
+}
+
+.dropdown-block {
+ position: relative;
+ align-self: center;
+ height: 100%;
+}
+
+/* cover the reserved space at the end of .source-tabs */
+.source-tabs + .dropdown-block {
+ margin-inline-start: -28px;
+}
+
+.dropdown-button {
+ color: var(--theme-comment);
+ background: none;
+ border: none;
+ padding: 4px 6px;
+ font-weight: 100;
+ font-size: 14px;
+ height: 100%;
+ width: 28px;
+ outline-offset: -2px;
+}
+
+.dropdown-button .img {
+ display: block;
+}
+
+.dropdown ul {
+ margin: 0;
+ padding: 4px 0;
+ list-style: none;
+}
+
+.dropdown li {
+ display: flex;
+ align-items: center;
+ padding: 6px 8px;
+ font-size: 12px;
+ line-height: calc(16 / 12);
+ transition: all 0.25s ease;
+}
+
+.dropdown li:hover {
+ background-color: var(--search-overlays-semitransparent);
+}
+
+.dropdown-icon {
+ margin-inline-end: 4px;
+ mask-size: 13px 13px;
+}
+
+.dropdown-label {
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.dropdown-icon.prettyPrint,
+.dropdown-icon.blackBox {
+ background-color: var(--theme-highlight-blue);
+}
+
+.dropdown-mask {
+ position: fixed;
+ width: 100%;
+ height: 100%;
+ background: transparent;
+ z-index: 999;
+ left: 0;
+ top: 0;
+}
diff --git a/devtools/client/debugger/src/components/shared/Dropdown.js b/devtools/client/debugger/src/components/shared/Dropdown.js
new file mode 100644
index 0000000000..a47eef9534
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Dropdown.js
@@ -0,0 +1,74 @@
+/* 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 { Component } from "devtools/client/shared/vendor/react";
+import { button, div } from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+export class Dropdown extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ dropdownShown: false,
+ };
+ }
+
+ static get propTypes() {
+ return {
+ icon: PropTypes.node.isRequired,
+ panel: PropTypes.node.isRequired,
+ };
+ }
+
+ toggleDropdown = e => {
+ this.setState(prevState => ({
+ dropdownShown: !prevState.dropdownShown,
+ }));
+ };
+
+ renderPanel() {
+ return div(
+ {
+ className: "dropdown",
+ onClick: this.toggleDropdown,
+ style: {
+ display: this.state.dropdownShown ? "block" : "none",
+ },
+ },
+ this.props.panel
+ );
+ }
+
+ renderButton() {
+ return button(
+ {
+ className: "dropdown-button",
+ onClick: this.toggleDropdown,
+ },
+ this.props.icon
+ );
+ }
+
+ renderMask() {
+ return div({
+ className: "dropdown-mask",
+ onClick: this.toggleDropdown,
+ style: {
+ display: this.state.dropdownShown ? "block" : "none",
+ },
+ });
+ }
+ render() {
+ return div(
+ {
+ className: "dropdown-block",
+ },
+ this.renderPanel(),
+ this.renderButton(),
+ this.renderMask()
+ );
+ }
+}
+
+export default Dropdown;
diff --git a/devtools/client/debugger/src/components/shared/Modal.css b/devtools/client/debugger/src/components/shared/Modal.css
new file mode 100644
index 0000000000..2c8f429285
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Modal.css
@@ -0,0 +1,47 @@
+/* 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/>. */
+
+.modal-wrapper {
+ position: fixed;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ z-index: 100;
+}
+
+.modal {
+ display: flex;
+ flex-direction: column;
+ /* Place the modal below the sources tab strip */
+ margin-block-start: var(--editor-header-height);
+ width: 80%;
+ max-height: 80vh;
+ overflow-y: auto;
+ background-color: var(--theme-toolbar-background);
+ box-shadow: 1px 1px 6px 1px var(--popup-shadow-color);
+
+ @media not (prefers-reduced-motion) {
+ animation: 150ms cubic-bezier(0.07, 0.95, 0, 1) slidein forwards;
+ }
+}
+
+@keyframes slidein {
+ from {
+ transform: translateY(-101%);
+ }
+ to {
+ transform: translateY(0);
+ }
+}
+
+/* This rule is active when the screen is not narrow */
+@media (min-width: 580px) {
+ .modal {
+ width: 50%;
+ }
+}
diff --git a/devtools/client/debugger/src/components/shared/Modal.js b/devtools/client/debugger/src/components/shared/Modal.js
new file mode 100644
index 0000000000..c14732f302
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Modal.js
@@ -0,0 +1,45 @@
+/* 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 PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import React from "devtools/client/shared/vendor/react";
+import { div } from "devtools/client/shared/vendor/react-dom-factories";
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+class Modal extends React.Component {
+ static get propTypes() {
+ return {
+ additionalClass: PropTypes.string,
+ children: PropTypes.node.isRequired,
+ handleClose: PropTypes.func.isRequired,
+ };
+ }
+
+ onClick = e => {
+ e.stopPropagation();
+ };
+
+ render() {
+ const { additionalClass, children, handleClose } = this.props;
+ return div(
+ {
+ className: "modal-wrapper",
+ onClick: handleClose,
+ },
+ div(
+ {
+ className: classnames("modal", additionalClass),
+ onClick: this.onClick,
+ },
+ children
+ )
+ );
+ }
+}
+
+Modal.contextTypes = {
+ shortcuts: PropTypes.object,
+};
+
+export default Modal;
diff --git a/devtools/client/debugger/src/components/shared/Popover.css b/devtools/client/debugger/src/components/shared/Popover.css
new file mode 100644
index 0000000000..5da8ea4b63
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Popover.css
@@ -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/>. */
+
+.popover {
+ position: fixed;
+ z-index: 100;
+ --gap-size: 10px;
+ --left-offset: -55px;
+}
+
+.popover.orientation-right {
+ display: flex;
+ flex-direction: row;
+}
+
+.popover.orientation-right .gap {
+ width: var(--gap-size);
+}
+
+.popover:not(.orientation-right) .gap {
+ height: var(--gap-size);
+ margin-left: var(--left-offset);
+}
+
+.popover:not(.orientation-right) .preview-popup {
+ margin-left: var(--left-offset);
+}
+
+.popover .add-to-expression-bar {
+ margin-left: var(--left-offset);
+}
diff --git a/devtools/client/debugger/src/components/shared/Popover.js b/devtools/client/debugger/src/components/shared/Popover.js
new file mode 100644
index 0000000000..8748e36418
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/Popover.js
@@ -0,0 +1,324 @@
+/* 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 React, { Component } from "devtools/client/shared/vendor/react";
+import { div } from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import BracketArrow from "./BracketArrow";
+import SmartGap from "./SmartGap";
+
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+class Popover extends Component {
+ state = {
+ coords: {
+ left: 0,
+ top: 0,
+ orientation: "down",
+ targetMid: { x: 0, y: 0 },
+ },
+ };
+ firstRender = true;
+
+ static defaultProps = {
+ type: "popover",
+ };
+
+ static get propTypes() {
+ return {
+ children: PropTypes.node.isRequired,
+ editorRef: PropTypes.object.isRequired,
+ mouseout: PropTypes.func.isRequired,
+ target: PropTypes.object.isRequired,
+ targetPosition: PropTypes.object.isRequired,
+ type: PropTypes.string.isRequired,
+ };
+ }
+
+ componentDidMount() {
+ const { type } = this.props;
+ this.gapHeight = this.$gap.getBoundingClientRect().height;
+ const coords =
+ type == "popover" ? this.getPopoverCoords() : this.getTooltipCoords();
+
+ if (coords) {
+ this.setState({ coords });
+ }
+
+ this.firstRender = false;
+ this.startTimer();
+ }
+
+ componentDidUpdate(prevProps) {
+ // We have to update `coords` when the Popover type changes
+ if (
+ prevProps.type != this.props.type ||
+ prevProps.target !== this.props.target
+ ) {
+ const coords =
+ this.props.type == "popover"
+ ? this.getPopoverCoords()
+ : this.getTooltipCoords();
+
+ if (coords) {
+ this.setState({ coords });
+ }
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.timerId) {
+ clearTimeout(this.timerId);
+ }
+ }
+
+ startTimer() {
+ this.timerId = setTimeout(this.onTimeout, 0);
+ }
+
+ onTimeout = () => {
+ const isHoveredOnGap = this.$gap && this.$gap.matches(":hover");
+ const isHoveredOnPopover = this.$popover && this.$popover.matches(":hover");
+ const isHoveredOnTooltip = this.$tooltip && this.$tooltip.matches(":hover");
+ const isHoveredOnTarget = this.props.target.matches(":hover");
+
+ if (isHoveredOnGap) {
+ if (!this.wasOnGap) {
+ this.wasOnGap = true;
+ this.timerId = setTimeout(this.onTimeout, 200);
+ return;
+ }
+ this.props.mouseout();
+ return;
+ }
+
+ // Don't clear the current preview if mouse is hovered on
+ // the current preview's token (target) or the popup element
+ if (isHoveredOnPopover || isHoveredOnTooltip || isHoveredOnTarget) {
+ this.wasOnGap = false;
+ this.timerId = setTimeout(this.onTimeout, 0);
+ return;
+ }
+
+ this.props.mouseout();
+ };
+
+ calculateLeft(target, editor, popover, orientation) {
+ const estimatedLeft = target.left;
+ const estimatedRight = estimatedLeft + popover.width;
+ const isOverflowingRight = estimatedRight > editor.right;
+ if (orientation === "right") {
+ return target.left + target.width;
+ }
+ if (isOverflowingRight) {
+ const adjustedLeft = editor.right - popover.width - 8;
+ return adjustedLeft;
+ }
+ return estimatedLeft;
+ }
+
+ calculateTopForRightOrientation = (target, editor, popover) => {
+ if (popover.height <= editor.height) {
+ const rightOrientationTop = target.top - popover.height / 2;
+ if (rightOrientationTop < editor.top) {
+ return editor.top - target.height;
+ }
+ const rightOrientationBottom = rightOrientationTop + popover.height;
+ if (rightOrientationBottom > editor.bottom) {
+ return editor.bottom + target.height - popover.height + this.gapHeight;
+ }
+ return rightOrientationTop;
+ }
+ return editor.top - target.height;
+ };
+
+ calculateOrientation(target, editor, popover) {
+ const estimatedBottom = target.bottom + popover.height;
+ if (editor.bottom > estimatedBottom) {
+ return "down";
+ }
+ const upOrientationTop = target.top - popover.height;
+ if (upOrientationTop > editor.top) {
+ return "up";
+ }
+
+ return "right";
+ }
+
+ calculateTop = (target, editor, popover, orientation) => {
+ if (orientation === "down") {
+ return target.bottom;
+ }
+ if (orientation === "up") {
+ return target.top - popover.height;
+ }
+
+ return this.calculateTopForRightOrientation(target, editor, popover);
+ };
+
+ getPopoverCoords() {
+ if (!this.$popover || !this.props.editorRef) {
+ return null;
+ }
+
+ const popover = this.$popover;
+ const editor = this.props.editorRef;
+ const popoverRect = popover.getBoundingClientRect();
+ const editorRect = editor.getBoundingClientRect();
+ const targetRect = this.props.targetPosition;
+ const orientation = this.calculateOrientation(
+ targetRect,
+ editorRect,
+ popoverRect
+ );
+ const top = this.calculateTop(
+ targetRect,
+ editorRect,
+ popoverRect,
+ orientation
+ );
+ const popoverLeft = this.calculateLeft(
+ targetRect,
+ editorRect,
+ popoverRect,
+ orientation
+ );
+ let targetMid;
+ if (orientation === "right") {
+ targetMid = {
+ x: -14,
+ y: targetRect.top - top - 2,
+ };
+ } else {
+ targetMid = {
+ x: targetRect.left - popoverLeft + targetRect.width / 2 - 8,
+ y: 0,
+ };
+ }
+
+ return {
+ left: popoverLeft,
+ top,
+ orientation,
+ targetMid,
+ };
+ }
+
+ getTooltipCoords() {
+ if (!this.$tooltip || !this.props.editorRef) {
+ return null;
+ }
+ const tooltip = this.$tooltip;
+ const editor = this.props.editorRef;
+ const tooltipRect = tooltip.getBoundingClientRect();
+ const editorRect = editor.getBoundingClientRect();
+ const targetRect = this.props.targetPosition;
+ const left = this.calculateLeft(targetRect, editorRect, tooltipRect);
+ const enoughRoomForTooltipAbove =
+ targetRect.top - editorRect.top > tooltipRect.height;
+ const top = enoughRoomForTooltipAbove
+ ? targetRect.top - tooltipRect.height
+ : targetRect.bottom;
+
+ return {
+ left,
+ top,
+ orientation: enoughRoomForTooltipAbove ? "up" : "down",
+ targetMid: { x: 0, y: 0 },
+ };
+ }
+
+ getChildren() {
+ const { children } = this.props;
+ const { coords } = this.state;
+ const gap = this.getGap();
+
+ return coords.orientation === "up" ? [children, gap] : [gap, children];
+ }
+
+ getGap() {
+ if (this.firstRender) {
+ return div({
+ className: "gap",
+ key: "gap",
+ ref: a => (this.$gap = a),
+ });
+ }
+
+ return div(
+ {
+ className: "gap",
+ key: "gap",
+ ref: a => (this.$gap = a),
+ },
+ React.createElement(SmartGap, {
+ token: this.props.target,
+ preview: this.$tooltip || this.$popover,
+ type: this.props.type,
+ gapHeight: this.gapHeight,
+ coords: this.state.coords,
+ offset: this.$gap.getBoundingClientRect().left,
+ })
+ );
+ }
+
+ getPopoverArrow(orientation, left, top) {
+ let arrowProps = {};
+
+ if (orientation === "up") {
+ arrowProps = { orientation: "down", bottom: 10, left };
+ } else if (orientation === "down") {
+ arrowProps = { orientation: "up", top: -2, left };
+ } else {
+ arrowProps = { orientation: "left", top, left: -4 };
+ }
+ return React.createElement(BracketArrow, arrowProps);
+ }
+
+ renderPopover() {
+ const { top, left, orientation, targetMid } = this.state.coords;
+ const arrow = this.getPopoverArrow(orientation, targetMid.x, targetMid.y);
+ return div(
+ {
+ className: classnames("popover", `orientation-${orientation}`, {
+ up: orientation === "up",
+ }),
+ style: {
+ top,
+ left,
+ },
+ ref: c => (this.$popover = c),
+ },
+ arrow,
+ this.getChildren()
+ );
+ }
+
+ renderTooltip() {
+ const { top, left, orientation } = this.state.coords;
+ return div(
+ {
+ className: `tooltip orientation-${orientation}`,
+ style: {
+ top,
+ left,
+ },
+ ref: c => (this.$tooltip = c),
+ },
+ this.getChildren()
+ );
+ }
+
+ render() {
+ const { type } = this.props;
+
+ if (type === "tooltip") {
+ return this.renderTooltip();
+ }
+
+ return this.renderPopover();
+ }
+}
+
+export default Popover;
diff --git a/devtools/client/debugger/src/components/shared/PreviewFunction.css b/devtools/client/debugger/src/components/shared/PreviewFunction.css
new file mode 100644
index 0000000000..bff9ce25a2
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/PreviewFunction.css
@@ -0,0 +1,23 @@
+/* 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/>. */
+
+.function-signature {
+ align-self: center;
+}
+
+.function-signature .function-name {
+ color: var(--theme-highlight-blue);
+}
+
+.function-signature .param {
+ color: var(--theme-highlight-red);
+}
+
+.function-signature .paren {
+ color: var(--object-color);
+}
+
+.function-signature .comma {
+ color: var(--object-color);
+}
diff --git a/devtools/client/debugger/src/components/shared/PreviewFunction.js b/devtools/client/debugger/src/components/shared/PreviewFunction.js
new file mode 100644
index 0000000000..1a6d164cdf
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/PreviewFunction.js
@@ -0,0 +1,108 @@
+/* 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 { Component } from "devtools/client/shared/vendor/react";
+import {
+ span,
+ button,
+} from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+import { formatDisplayName } from "../../utils/pause/frames/index";
+
+const IGNORED_SOURCE_URLS = ["debugger eval code"];
+
+export default class PreviewFunction extends Component {
+ static get propTypes() {
+ return {
+ func: PropTypes.object.isRequired,
+ };
+ }
+
+ renderFunctionName(func) {
+ const { l10n } = this.context;
+ const name = formatDisplayName(func, undefined, l10n);
+ return span(
+ {
+ className: "function-name",
+ },
+ name
+ );
+ }
+
+ renderParams(func) {
+ const { parameterNames = [] } = func;
+
+ return parameterNames
+ .filter(Boolean)
+ .map((param, i, arr) => {
+ const elements = [
+ span(
+ {
+ className: "param",
+ key: param,
+ },
+ param
+ ),
+ ];
+ // if this isn't the last param, add a comma
+ if (i !== arr.length - 1) {
+ elements.push(
+ span(
+ {
+ className: "delimiter",
+ key: i,
+ },
+ ", "
+ )
+ );
+ }
+ return elements;
+ })
+ .flat();
+ }
+
+ jumpToDefinitionButton(func) {
+ const { location } = func;
+
+ if (!location?.url || IGNORED_SOURCE_URLS.includes(location.url)) {
+ return null;
+ }
+
+ const lastIndex = location.url.lastIndexOf("/");
+ return button({
+ className: "jump-definition",
+ draggable: "false",
+ title: `${location.url.slice(lastIndex + 1)}:${location.line}`,
+ });
+ }
+
+ render() {
+ const { func } = this.props;
+ return span(
+ {
+ className: "function-signature",
+ },
+ this.renderFunctionName(func),
+ span(
+ {
+ className: "paren",
+ },
+ "("
+ ),
+ this.renderParams(func),
+ span(
+ {
+ className: "paren",
+ },
+ ")"
+ ),
+ this.jumpToDefinitionButton(func)
+ );
+ }
+}
+
+PreviewFunction.contextTypes = {
+ l10n: PropTypes.object,
+};
diff --git a/devtools/client/debugger/src/components/shared/ResultList.css b/devtools/client/debugger/src/components/shared/ResultList.css
new file mode 100644
index 0000000000..037c3497d3
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/ResultList.css
@@ -0,0 +1,131 @@
+/* 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/>. */
+
+.result-list {
+ list-style: none;
+ margin: 0px;
+ padding: 0px;
+ overflow: auto;
+ width: 100%;
+ background: var(--theme-body-background);
+}
+
+.result-list * {
+ user-select: none;
+}
+
+.result-list li {
+ color: var(--theme-body-color);
+ padding: 4px 8px;
+ display: flex;
+}
+
+.result-list.big li {
+ flex-direction: row;
+ align-items: center;
+ padding: 6px 8px;
+ font-size: 12px;
+ line-height: 16px;
+}
+
+.result-list.small li {
+ justify-content: space-between;
+}
+
+.result-list li:hover {
+ background: var(--theme-tab-toolbar-background);
+}
+
+.theme-dark .result-list li:hover {
+ background: var(--grey-70);
+}
+
+.result-list li.selected {
+ background: var(--theme-accordion-header-background);
+}
+
+.result-list.small li.selected {
+ background-color: var(--theme-selection-background);
+ color: white;
+}
+
+.result-list li .result-item-icon {
+ background-color: var(--theme-icon-dimmed-color);
+}
+
+.result-list li .icon {
+ align-self: center;
+ margin-inline-end: 14px;
+ margin-inline-start: 4px;
+}
+
+.result-list .result-item-icon {
+ display: block;
+}
+
+.result-list .selected .result-item-icon {
+ background-color: var(--theme-selection-color);
+}
+
+.result-list li .title {
+ word-break: break-all;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+
+ /** https://searchfox.org/mozilla-central/source/devtools/client/themes/variables.css **/
+ color: var(--grey-90);
+}
+
+.theme-dark .result-list li .title {
+ /** https://searchfox.org/mozilla-central/source/devtools/client/themes/variables.css **/
+ color: var(--grey-30);
+}
+
+.result-list li.selected .title {
+ color: white;
+}
+
+.result-list.big li.selected {
+ background-color: var(--theme-selection-background);
+ color: white;
+}
+
+.result-list.big li.selected .subtitle {
+ color: white;
+}
+
+.result-list.big li.selected .subtitle .highlight {
+ color: white;
+ font-weight: bold;
+}
+
+.result-list.big li .subtitle {
+ word-break: break-all;
+ /** https://searchfox.org/mozilla-central/source/devtools/client/themes/variables.css **/
+ color: var(--grey-40);
+ margin-left: 15px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.theme-dark .result-list.big li.selected .subtitle {
+ color: white;
+}
+
+.theme-dark .result-list.big li .subtitle {
+ color: var(--theme-text-color-inactive);
+}
+
+.search-bar .result-list li.selected .subtitle {
+ color: white;
+}
+
+.search-bar .result-list {
+ border-bottom: 1px solid var(--theme-splitter-color);
+}
+
+.theme-dark .result-list {
+ background-color: var(--theme-body-background);
+}
diff --git a/devtools/client/debugger/src/components/shared/ResultList.js b/devtools/client/debugger/src/components/shared/ResultList.js
new file mode 100644
index 0000000000..6b29de51f4
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/ResultList.js
@@ -0,0 +1,102 @@
+/* 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 React, { Component } from "devtools/client/shared/vendor/react";
+import { li, div, ul } from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+import AccessibleImage from "./AccessibleImage";
+
+const classnames = require("resource://devtools/client/shared/classnames.js");
+
+import { scrollList } from "../../utils/result-list";
+
+export default class ResultList extends Component {
+ static defaultProps = {
+ size: "small",
+ role: "listbox",
+ };
+
+ static get propTypes() {
+ return {
+ items: PropTypes.array.isRequired,
+ role: PropTypes.oneOf(["listbox"]),
+ selectItem: PropTypes.func.isRequired,
+ selected: PropTypes.number.isRequired,
+ size: PropTypes.oneOf(["big", "small"]),
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.ref = React.createRef();
+ }
+
+ componentDidUpdate() {
+ if (this.ref.current.childNodes) {
+ scrollList(this.ref.current.childNodes, this.props.selected);
+ }
+ }
+
+ renderListItem = (item, index) => {
+ if (item.value === "/" && item.title === "") {
+ item.title = "(index)";
+ }
+
+ const { selectItem, selected } = this.props;
+ const props = {
+ onClick: event => selectItem(event, item, index),
+ key: `${item.id}${item.value}${index}`,
+ title: item.value,
+ "aria-labelledby": `${item.id}-title`,
+ "aria-describedby": `${item.id}-subtitle`,
+ role: "option",
+ className: classnames("result-item", {
+ selected: index === selected,
+ }),
+ };
+
+ return li(
+ props,
+ item.icon &&
+ div(
+ {
+ className: "icon",
+ },
+ React.createElement(AccessibleImage, {
+ className: item.icon,
+ })
+ ),
+ div(
+ {
+ id: `${item.id}-title`,
+ className: "title",
+ },
+ item.title
+ ),
+ item.subtitle != item.title
+ ? div(
+ {
+ id: `${item.id}-subtitle`,
+ className: "subtitle",
+ },
+ item.subtitle
+ )
+ : null
+ );
+ };
+ render() {
+ const { size, items, role } = this.props;
+ return ul(
+ {
+ ref: this.ref,
+ className: classnames("result-list", size),
+ id: "result-list",
+ role: role,
+ "aria-live": "polite",
+ },
+ items.map(this.renderListItem)
+ );
+ }
+}
diff --git a/devtools/client/debugger/src/components/shared/SearchInput.css b/devtools/client/debugger/src/components/shared/SearchInput.css
new file mode 100644
index 0000000000..4a5ee85ed3
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/SearchInput.css
@@ -0,0 +1,223 @@
+/* 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/>. */
+
+.search-outline {
+ border: 1px solid var(--theme-toolbar-background);
+ border-bottom: 1px solid var(--theme-splitter-color);
+ transition: border-color 200ms ease-in-out;
+ display: flex;
+ flex-direction: column;
+}
+
+.search-field {
+ position: relative;
+ display: flex;
+ align-items: center;
+ flex-shrink: 0;
+ min-height: 24px;
+ width: 100%;
+}
+
+.search-field .img.search {
+ --icon-mask-size: 12px;
+ --icon-inset-inline-start: 6px;
+ position: absolute;
+ z-index: 1;
+ top: calc(50% - 8px);
+ mask-size: var(--icon-mask-size);
+ background-color: var(--theme-icon-dimmed-color);
+ pointer-events: none;
+}
+
+.search-field.big .img.search {
+ --icon-mask-size: 16px;
+ --icon-inset-inline-start: 12px;
+}
+
+[dir="ltr"] .search-field .img.search {
+ left: var(--icon-inset-inline-start);
+}
+
+[dir="rtl"] .search-field .img.search {
+ right: var(--icon-inset-inline-start);
+}
+
+.search-field .img.loader {
+ width: 24px;
+ height: 24px;
+ margin-inline-end: 4px;
+}
+
+.search-field input {
+ align-self: stretch;
+ flex-grow: 1;
+ height: 24px;
+ width: 40px;
+ border: none;
+ padding: 4px;
+ padding-inline-start: 28px;
+ line-height: 16px;
+ font-family: inherit;
+ font-size: inherit;
+ color: var(--theme-body-color);
+ background-color: transparent;
+ outline-offset: -1px;
+
+ &:focus-visible {
+ /* Don't show the box-shadow focus indicator, only keep the outline, otherwise the
+ shadow overlap the first item in the result list */
+ box-shadow: none;
+ }
+}
+
+.exclude-patterns-field {
+ position: relative;
+ display: flex;
+ align-items: flex-start;
+ flex-direction: column;
+ flex-shrink: 0;
+ min-height: 24px;
+ width: 100%;
+ border-top: 1px solid var(--theme-splitter-color);
+ margin-top: 1px;
+ outline-offset: -1px;
+}
+
+.exclude-patterns-field label {
+ padding-inline-start: 8px;
+ padding-top: 5px;
+ padding-bottom: 3px;
+ align-self: stretch;
+ background-color: var(--theme-accordion-header-background);
+ font-size: 12px;
+}
+
+.exclude-patterns-field input {
+ align-self: stretch;
+ height: 24px;
+ border: none;
+ padding-top: 14px;
+ padding-bottom: 14px;
+ padding-inline-start: 10px;
+ line-height: 16px;
+ font-family: inherit;
+ font-size: inherit;
+ color: var(--theme-body-color);
+ background-color: transparent;
+ border-top: 1px solid var(--theme-splitter-color);
+ min-height: 24px;
+ outline-offset: -1px;
+}
+
+.exclude-patterns-field input::placeholder {
+ color: var(--theme-text-color-alt);
+ opacity: 1;
+}
+
+.search-field.big input {
+ height: 40px;
+ padding-top: 10px;
+ padding-bottom: 10px;
+ padding-inline-start: 40px;
+ font-size: 14px;
+ line-height: 20px;
+}
+
+.search-field input::placeholder {
+ color: var(--theme-text-color-alt);
+ opacity: 1;
+}
+
+.search-field-summary {
+ align-self: center;
+ padding: 2px 4px;
+ white-space: nowrap;
+ text-align: center;
+ user-select: none;
+ color: var(--theme-text-color-alt);
+ /* Avoid layout jumps when we increment the result count quickly. With tabular
+ numbers, layout will only jump between 9 and 10, 99 and 100, etc. */
+ font-variant-numeric: tabular-nums;
+}
+
+.search-field.big .search-field-summary {
+ margin-inline-end: 4px;
+}
+
+.search-field .search-nav-buttons {
+ display: flex;
+ user-select: none;
+}
+
+.search-field .search-nav-buttons .nav-btn {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100%;
+ padding: 4px;
+ background: transparent;
+ outline-offset: -2px;
+}
+
+.search-field .search-nav-buttons .nav-btn:hover {
+ background-color: var(--theme-toolbar-background-hover);
+}
+
+.search-field .close-btn {
+ margin-inline-end: 4px;
+}
+
+.search-field.big .close-btn {
+ margin-inline-end: 8px;
+}
+
+.search-field .close-btn::-moz-focus-inner {
+ border: none;
+}
+
+.search-buttons-bar .pipe-divider {
+ flex: none;
+ align-self: stretch;
+ width: 1px;
+ vertical-align: middle;
+ margin: 4px;
+ background-color: var(--theme-splitter-color);
+}
+
+.search-buttons-bar * {
+ user-select: none;
+}
+
+.search-buttons-bar {
+ display: flex;
+ flex-shrink: 0;
+ justify-content: flex-end;
+ align-items: center;
+ padding: 0;
+}
+
+.search-buttons-bar .search-type-toggles {
+ display: flex;
+ align-items: center;
+ max-width: 68%;
+}
+
+.search-buttons-bar .search-type-name {
+ margin: 0 4px;
+ border: none;
+ background: transparent;
+ color: var(--theme-comment);
+}
+
+.search-buttons-bar .search-type-toggles .search-type-btn.active {
+ color: var(--theme-selection-background);
+}
+
+.theme-dark .search-buttons-bar .search-type-toggles .search-type-btn.active {
+ color: white;
+}
+
+.search-buttons-bar .close-btn {
+ margin-inline-end: 3px;
+}
diff --git a/devtools/client/debugger/src/components/shared/SearchInput.js b/devtools/client/debugger/src/components/shared/SearchInput.js
new file mode 100644
index 0000000000..18f6ffbebb
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/SearchInput.js
@@ -0,0 +1,362 @@
+/* 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 React, { Component } from "devtools/client/shared/vendor/react";
+import {
+ button,
+ div,
+ label,
+ input,
+ span,
+} from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import { CloseButton } from "./Button/index";
+
+import AccessibleImage from "./AccessibleImage";
+import actions from "../../actions/index";
+import { getSearchOptions } from "../../selectors/index";
+
+const classnames = require("resource://devtools/client/shared/classnames.js");
+const SearchModifiers = require("resource://devtools/client/shared/components/SearchModifiers.js");
+
+const arrowBtn = (onClick, type, className, tooltip) => {
+ const props = {
+ className,
+ key: type,
+ onClick,
+ title: tooltip,
+ type,
+ };
+ return button(
+ props,
+ React.createElement(AccessibleImage, {
+ className: type,
+ })
+ );
+};
+
+export class SearchInput extends Component {
+ static defaultProps = {
+ expanded: false,
+ hasPrefix: false,
+ selectedItemId: "",
+ size: "",
+ showClose: true,
+ };
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ history: [],
+ excludePatterns: props.searchOptions.excludePatterns,
+ };
+ }
+
+ static get propTypes() {
+ return {
+ count: PropTypes.number.isRequired,
+ expanded: PropTypes.bool.isRequired,
+ handleClose: PropTypes.func,
+ handleNext: PropTypes.func,
+ handlePrev: PropTypes.func,
+ hasPrefix: PropTypes.bool.isRequired,
+ isLoading: PropTypes.bool.isRequired,
+ onBlur: PropTypes.func,
+ onChange: PropTypes.func,
+ onFocus: PropTypes.func,
+ onHistoryScroll: PropTypes.func,
+ onKeyDown: PropTypes.func,
+ onKeyUp: PropTypes.func,
+ placeholder: PropTypes.string,
+ query: PropTypes.string,
+ selectedItemId: PropTypes.string,
+ shouldFocus: PropTypes.bool,
+ showClose: PropTypes.bool.isRequired,
+ showExcludePatterns: PropTypes.bool.isRequired,
+ excludePatternsLabel: PropTypes.string,
+ excludePatternsPlaceholder: PropTypes.string,
+ showErrorEmoji: PropTypes.bool.isRequired,
+ size: PropTypes.string,
+ summaryMsg: PropTypes.string,
+ searchKey: PropTypes.string.isRequired,
+ searchOptions: PropTypes.object,
+ setSearchOptions: PropTypes.func,
+ showSearchModifiers: PropTypes.bool.isRequired,
+ onToggleSearchModifier: PropTypes.func,
+ };
+ }
+
+ componentDidMount() {
+ this.setFocus();
+ }
+
+ componentDidUpdate(prevProps) {
+ if (this.props.shouldFocus && !prevProps.shouldFocus) {
+ this.setFocus();
+ }
+ }
+
+ setFocus() {
+ if (this.$input) {
+ const _input = this.$input;
+ _input.focus();
+
+ if (!_input.value) {
+ return;
+ }
+
+ // omit prefix @:# from being selected
+ const selectStartPos = this.props.hasPrefix ? 1 : 0;
+ _input.setSelectionRange(selectStartPos, _input.value.length + 1);
+ }
+ }
+
+ renderArrowButtons() {
+ const { handleNext, handlePrev } = this.props;
+
+ return [
+ arrowBtn(
+ handlePrev,
+ "arrow-up",
+ classnames("nav-btn", "prev"),
+ L10N.getFormatStr("editor.searchResults.prevResult")
+ ),
+ arrowBtn(
+ handleNext,
+ "arrow-down",
+ classnames("nav-btn", "next"),
+ L10N.getFormatStr("editor.searchResults.nextResult")
+ ),
+ ];
+ }
+
+ onFocus = e => {
+ const { onFocus } = this.props;
+
+ if (onFocus) {
+ onFocus(e);
+ }
+ };
+
+ onBlur = e => {
+ const { onBlur } = this.props;
+
+ if (onBlur) {
+ onBlur(e);
+ }
+ };
+
+ onKeyDown = e => {
+ const { onHistoryScroll, onKeyDown } = this.props;
+ if (!onHistoryScroll) {
+ onKeyDown(e);
+ return;
+ }
+
+ const inputValue = e.target.value;
+ const { history } = this.state;
+ const currentHistoryIndex = history.indexOf(inputValue);
+
+ if (e.key === "Enter") {
+ this.saveEnteredTerm(inputValue);
+ onKeyDown(e);
+ return;
+ }
+
+ if (e.key === "ArrowUp") {
+ const previous =
+ currentHistoryIndex > -1 ? currentHistoryIndex - 1 : history.length - 1;
+ const previousInHistory = history[previous];
+ if (previousInHistory) {
+ e.preventDefault();
+ onHistoryScroll(previousInHistory);
+ }
+ return;
+ }
+
+ if (e.key === "ArrowDown") {
+ const next = currentHistoryIndex + 1;
+ const nextInHistory = history[next];
+ if (nextInHistory) {
+ onHistoryScroll(nextInHistory);
+ }
+ }
+ };
+
+ onExcludeKeyDown = e => {
+ if (e.key === "Enter") {
+ this.props.setSearchOptions(this.props.searchKey, {
+ excludePatterns: this.state.excludePatterns,
+ });
+ this.props.onKeyDown(e);
+ }
+ };
+
+ saveEnteredTerm(query) {
+ const { history } = this.state;
+ const previousIndex = history.indexOf(query);
+ if (previousIndex !== -1) {
+ history.splice(previousIndex, 1);
+ }
+ history.push(query);
+ this.setState({ history });
+ }
+
+ renderSummaryMsg() {
+ const { summaryMsg } = this.props;
+
+ if (!summaryMsg) {
+ return null;
+ }
+ return div(
+ {
+ className: "search-field-summary",
+ },
+ summaryMsg
+ );
+ }
+
+ renderSpinner() {
+ const { isLoading } = this.props;
+ if (!isLoading) {
+ return null;
+ }
+ return React.createElement(AccessibleImage, {
+ className: "loader spin",
+ });
+ }
+
+ renderNav() {
+ const { count, handleNext, handlePrev } = this.props;
+ if ((!handleNext && !handlePrev) || !count || count == 1) {
+ return null;
+ }
+ return div(
+ {
+ className: "search-nav-buttons",
+ },
+ this.renderArrowButtons()
+ );
+ }
+
+ renderSearchModifiers() {
+ if (!this.props.showSearchModifiers) {
+ return null;
+ }
+ return React.createElement(SearchModifiers, {
+ modifiers: this.props.searchOptions,
+ onToggleSearchModifier: updatedOptions => {
+ this.props.setSearchOptions(this.props.searchKey, updatedOptions);
+ this.props.onToggleSearchModifier();
+ },
+ });
+ }
+
+ renderExcludePatterns() {
+ if (!this.props.showExcludePatterns) {
+ return null;
+ }
+ return div(
+ {
+ className: classnames("exclude-patterns-field", this.props.size),
+ },
+ label(null, this.props.excludePatternsLabel),
+ input({
+ placeholder: this.props.excludePatternsPlaceholder,
+ value: this.state.excludePatterns,
+ onKeyDown: this.onExcludeKeyDown,
+ onChange: e =>
+ this.setState({
+ excludePatterns: e.target.value,
+ }),
+ })
+ );
+ }
+
+ renderClose() {
+ if (!this.props.showClose) {
+ return null;
+ }
+ return React.createElement(
+ React.Fragment,
+ null,
+ span({
+ className: "pipe-divider",
+ }),
+ React.createElement(CloseButton, {
+ handleClick: this.props.handleClose,
+ buttonClass: this.props.size,
+ })
+ );
+ }
+
+ render() {
+ const {
+ expanded,
+ onChange,
+ onKeyUp,
+ placeholder,
+ query,
+ selectedItemId,
+ showErrorEmoji,
+ size,
+ } = this.props;
+
+ const inputProps = {
+ className: classnames({
+ empty: showErrorEmoji,
+ }),
+ onChange,
+ onKeyDown: e => this.onKeyDown(e),
+ onKeyUp,
+ onFocus: e => this.onFocus(e),
+ onBlur: e => this.onBlur(e),
+ "aria-autocomplete": "list",
+ "aria-controls": "result-list",
+ "aria-activedescendant":
+ expanded && selectedItemId ? `${selectedItemId}-title` : "",
+ placeholder,
+ value: query,
+ spellCheck: false,
+ ref: c => (this.$input = c),
+ };
+ return div(
+ {
+ className: "search-outline",
+ },
+ div(
+ {
+ className: classnames("search-field", size),
+ role: "combobox",
+ "aria-haspopup": "listbox",
+ "aria-owns": "result-list",
+ "aria-expanded": expanded,
+ },
+ React.createElement(AccessibleImage, {
+ className: "search",
+ }),
+ input(inputProps),
+ this.renderSpinner(),
+ this.renderSummaryMsg(),
+ this.renderNav(),
+ div(
+ {
+ className: "search-buttons-bar",
+ },
+ this.renderSearchModifiers(),
+ this.renderClose()
+ )
+ ),
+ this.renderExcludePatterns()
+ );
+ }
+}
+const mapStateToProps = (state, props) => ({
+ searchOptions: getSearchOptions(state, props.searchKey),
+});
+
+export default connect(mapStateToProps, {
+ setSearchOptions: actions.setSearchOptions,
+})(SearchInput);
diff --git a/devtools/client/debugger/src/components/shared/SmartGap.js b/devtools/client/debugger/src/components/shared/SmartGap.js
new file mode 100644
index 0000000000..d76d018987
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/SmartGap.js
@@ -0,0 +1,170 @@
+/* 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 {
+ svg,
+ polygon,
+} from "devtools/client/shared/vendor/react-dom-factories";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+function shorten(coordinates) {
+ // In cases where the token is wider than the preview, the smartGap
+ // gets distorted. This shortens the coordinate array so that the smartGap
+ // is only touching 2 corners of the token (instead of all 4 corners)
+ coordinates.splice(0, 2);
+ coordinates.splice(4, 2);
+ return coordinates;
+}
+
+function getSmartGapCoordinates(
+ preview,
+ token,
+ offset,
+ orientation,
+ gapHeight,
+ coords
+) {
+ if (orientation === "up") {
+ const coordinates = [
+ token.left - coords.left + offset,
+ token.top + token.height - (coords.top + preview.height) + gapHeight,
+ 0,
+ 0,
+ preview.width + offset,
+ 0,
+ token.left + token.width - coords.left + offset,
+ token.top + token.height - (coords.top + preview.height) + gapHeight,
+ token.left + token.width - coords.left + offset,
+ token.top - (coords.top + preview.height) + gapHeight,
+ token.left - coords.left + offset,
+ token.top - (coords.top + preview.height) + gapHeight,
+ ];
+ return preview.width > token.width ? coordinates : shorten(coordinates);
+ }
+ if (orientation === "down") {
+ const coordinates = [
+ token.left + token.width - (coords.left + preview.top) + offset,
+ 0,
+ preview.width + offset,
+ coords.top - token.top + gapHeight,
+ 0,
+ coords.top - token.top + gapHeight,
+ token.left - (coords.left + preview.top) + offset,
+ 0,
+ token.left - (coords.left + preview.top) + offset,
+ token.height,
+ token.left + token.width - (coords.left + preview.top) + offset,
+ token.height,
+ ];
+ return preview.width > token.width ? coordinates : shorten(coordinates);
+ }
+ return [
+ 0,
+ token.top - coords.top,
+ gapHeight + token.width,
+ 0,
+ gapHeight + token.width,
+ preview.height - gapHeight,
+ 0,
+ token.top + token.height - coords.top,
+ token.width,
+ token.top + token.height - coords.top,
+ token.width,
+ token.top - coords.top,
+ ];
+}
+
+function getSmartGapDimensions(
+ previewRect,
+ tokenRect,
+ offset,
+ orientation,
+ gapHeight,
+ coords
+) {
+ if (orientation === "up") {
+ return {
+ height:
+ tokenRect.top +
+ tokenRect.height -
+ coords.top -
+ previewRect.height +
+ gapHeight,
+ width: Math.max(previewRect.width, tokenRect.width) + offset,
+ };
+ }
+ if (orientation === "down") {
+ return {
+ height: coords.top - tokenRect.top + gapHeight,
+ width: Math.max(previewRect.width, tokenRect.width) + offset,
+ };
+ }
+ return {
+ height: previewRect.height - gapHeight,
+ width: coords.left - tokenRect.left + gapHeight,
+ };
+}
+
+export default function SmartGap({
+ token,
+ preview,
+ type,
+ gapHeight,
+ coords,
+ offset,
+}) {
+ const tokenRect = token.getBoundingClientRect();
+ const previewRect = preview.getBoundingClientRect();
+ const { orientation } = coords;
+ let optionalMarginLeft, optionalMarginTop;
+
+ if (orientation === "down") {
+ optionalMarginTop = -tokenRect.height;
+ } else if (orientation === "right") {
+ optionalMarginLeft = -tokenRect.width;
+ }
+
+ const { height, width } = getSmartGapDimensions(
+ previewRect,
+ tokenRect,
+ -offset,
+ orientation,
+ gapHeight,
+ coords
+ );
+ const coordinates = getSmartGapCoordinates(
+ previewRect,
+ tokenRect,
+ -offset,
+ orientation,
+ gapHeight,
+ coords
+ );
+ return svg(
+ {
+ version: "1.1",
+ xmlns: "http://www.w3.org/2000/svg",
+ style: {
+ height,
+ width,
+ position: "absolute",
+ marginLeft: optionalMarginLeft,
+ marginTop: optionalMarginTop,
+ },
+ },
+ polygon({
+ points: coordinates,
+ fill: "transparent",
+ })
+ );
+}
+
+SmartGap.propTypes = {
+ coords: PropTypes.object.isRequired,
+ gapHeight: PropTypes.number.isRequired,
+ offset: PropTypes.number.isRequired,
+ preview: PropTypes.object.isRequired,
+ token: PropTypes.object.isRequired,
+ type: PropTypes.oneOf(["popover", "tooltip"]).isRequired,
+};
diff --git a/devtools/client/debugger/src/components/shared/SourceIcon.css b/devtools/client/debugger/src/components/shared/SourceIcon.css
new file mode 100644
index 0000000000..0b9bf3e79e
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/SourceIcon.css
@@ -0,0 +1,176 @@
+/* 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/>. */
+
+/**
+ * Variant of AccessibleImage used in sources list and tabs.
+ * Define the different source type / framework / library icons here.
+ */
+
+.source-icon {
+ margin-inline-end: 4px;
+}
+
+/* Icons for frameworks and libs */
+
+.img.aframe {
+ background-image: url(chrome://devtools/content/debugger/images/sources/aframe.svg);
+ background-color: transparent !important;
+}
+
+.img.angular {
+ background-image: url(chrome://devtools/content/debugger/images/sources/angular.svg);
+ background-color: transparent !important;
+}
+
+.img.babel {
+ mask-image: url(chrome://devtools/content/debugger/images/sources/babel.svg);
+}
+
+.img.backbone {
+ mask-image: url(chrome://devtools/content/debugger/images/sources/backbone.svg);
+}
+
+.img.choo {
+ background-image: url(chrome://devtools/content/debugger/images/sources/choo.svg);
+ background-color: transparent !important;
+}
+
+.img.coffeescript {
+ background-image: url(chrome://devtools/content/debugger/images/sources/coffeescript.svg);
+ background-color: transparent !important;
+ fill: var(--theme-icon-color);
+ -moz-context-properties: fill;
+}
+
+.img.dojo {
+ background-image: url(chrome://devtools/content/debugger/images/sources/dojo.svg);
+ background-color: transparent !important;
+}
+
+.img.ember {
+ background-image: url(chrome://devtools/content/debugger/images/sources/ember.svg);
+ background-color: transparent !important;
+}
+
+.img.express {
+ mask-image: url(chrome://devtools/content/debugger/images/sources/express.svg);
+}
+
+.img.extension {
+ mask-image: url(chrome://devtools/content/debugger/images/sources/extension.svg);
+}
+
+.img.immutable {
+ mask-image: url(chrome://devtools/content/debugger/images/sources/immutable.svg);
+}
+
+.img.javascript {
+ background-image: url(chrome://devtools/content/debugger/images/sources/javascript.svg);
+ background-size: 14px 14px;
+ background-color: transparent !important;
+ fill: var(--theme-icon-color);
+ -moz-context-properties: fill;
+}
+
+.img.override::after {
+ content: "";
+ display: block;
+ height: 5px;
+ width: 5px;
+ background-color: var(--purple-30);
+ border-radius: 100%;
+ outline: 1px solid var(--theme-sidebar-background);
+ translate: 12px 10px;
+}
+
+.node.focused .img.override::after {
+ outline-color: var(--theme-selection-background);
+}
+
+.img.jquery {
+ mask-image: url(chrome://devtools/content/debugger/images/sources/jquery.svg);
+}
+
+.img.lodash {
+ mask-image: url(chrome://devtools/content/debugger/images/sources/lodash.svg);
+}
+
+.img.marko {
+ background-image: url(chrome://devtools/content/debugger/images/sources/marko.svg);
+ background-color: transparent !important;
+}
+
+.img.mobx {
+ background-image: url(chrome://devtools/content/debugger/images/sources/mobx.svg);
+ background-color: transparent !important;
+}
+
+.img.nextjs {
+ background-image: url(chrome://devtools/content/debugger/images/sources/nextjs.svg);
+ background-color: transparent !important;
+}
+
+.img.node {
+ background-image: url(chrome://devtools/content/debugger/images/sources/node.svg);
+ background-color: transparent !important;
+}
+
+.img.nuxtjs {
+ background-image: url(chrome://devtools/content/debugger/images/sources/nuxtjs.svg);
+ background-color: transparent !important;
+}
+
+.img.preact {
+ background-image: url(chrome://devtools/content/debugger/images/sources/preact.svg);
+ background-color: transparent !important;
+}
+
+.img.pug {
+ background-image: url(chrome://devtools/content/debugger/images/sources/pug.svg);
+ background-color: transparent !important;
+}
+
+.img.react {
+ background-image: url(chrome://devtools/content/debugger/images/sources/react.svg);
+ background-color: transparent !important;
+ fill: var(--theme-highlight-bluegrey);
+ -moz-context-properties: fill;
+}
+
+.img.redux {
+ mask-image: url(chrome://devtools/content/debugger/images/sources/redux.svg);
+}
+
+.img.rxjs {
+ background-image: url(chrome://devtools/content/debugger/images/sources/rxjs.svg);
+ background-color: transparent !important;
+}
+
+.img.sencha-extjs {
+ background-image: url(chrome://devtools/content/debugger/images/sources/sencha-extjs.svg);
+ background-color: transparent !important;
+}
+
+.img.typescript {
+ background-image: url(chrome://devtools/content/debugger/images/sources/typescript.svg);
+ background-color: transparent !important;
+ fill: var(--theme-icon-color);
+ -moz-context-properties: fill;
+}
+
+.img.underscore {
+ mask-image: url(chrome://devtools/content/debugger/images/sources/underscore.svg);
+}
+
+/* We use both 'Vue' and 'VueJS' when identifying frameworks */
+.img.vue,
+.img.vuejs {
+ background-image: url(chrome://devtools/content/debugger/images/sources/vuejs.svg);
+ background-color: transparent !important;
+}
+
+.img.webpack {
+ background-image: url(chrome://devtools/content/debugger/images/sources/webpack.svg);
+ background-color: transparent !important;
+}
diff --git a/devtools/client/debugger/src/components/shared/SourceIcon.js b/devtools/client/debugger/src/components/shared/SourceIcon.js
new file mode 100644
index 0000000000..b2a7486bd6
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/SourceIcon.js
@@ -0,0 +1,71 @@
+/* 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 React, { PureComponent } from "devtools/client/shared/vendor/react";
+import PropTypes from "devtools/client/shared/vendor/react-prop-types";
+
+import { connect } from "devtools/client/shared/vendor/react-redux";
+
+import AccessibleImage from "./AccessibleImage";
+
+import { getSourceClassnames } from "../../utils/source";
+import {
+ getSymbols,
+ isSourceBlackBoxed,
+ hasPrettyTab,
+} from "../../selectors/index";
+
+class SourceIcon extends PureComponent {
+ static get propTypes() {
+ return {
+ modifier: PropTypes.func.isRequired,
+ location: PropTypes.object.isRequired,
+ iconClass: PropTypes.string,
+ forTab: PropTypes.bool,
+ };
+ }
+
+ render() {
+ const { modifier } = this.props;
+ let { iconClass } = this.props;
+
+ if (modifier) {
+ const modified = modifier(iconClass);
+ if (!modified) {
+ return null;
+ }
+ iconClass = modified;
+ }
+ return React.createElement(AccessibleImage, {
+ className: `source-icon ${iconClass}`,
+ });
+ }
+}
+
+export default connect((state, props) => {
+ const { forTab, location } = props;
+ // BreakpointHeading sometimes spawn locations without source actor for generated sources
+ // which disallows fetching symbols. In such race condition return the default icon.
+ // (this reproduces when running browser_dbg-breakpoints-popup.js)
+ if (!location.source.isOriginal && !location.sourceActor) {
+ return "file";
+ }
+ const symbols = getSymbols(state, location);
+ const isBlackBoxed = isSourceBlackBoxed(state, location.source);
+ // For the tab icon, we don't want to show the pretty icon for the non-pretty tab
+ const hasMatchingPrettyTab = !forTab && hasPrettyTab(state, location.source);
+
+ // This is the key function that will compute the icon type,
+ // In addition to the "modifier" implemented by each callsite.
+ const iconClass = getSourceClassnames(
+ location.source,
+ symbols,
+ isBlackBoxed,
+ hasMatchingPrettyTab
+ );
+
+ return {
+ iconClass,
+ };
+})(SourceIcon);
diff --git a/devtools/client/debugger/src/components/shared/menu.css b/devtools/client/debugger/src/components/shared/menu.css
new file mode 100644
index 0000000000..37dfbc2e8f
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/menu.css
@@ -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/>. */
+
+menupopup {
+ position: fixed;
+ z-index: 10000;
+ border: 1px solid #cccccc;
+ padding: 5px 0;
+ background: #f2f2f2;
+ border-radius: 5px;
+ color: #585858;
+ box-shadow: 0 0 4px 0 rgba(190, 190, 190, 0.8);
+ min-width: 130px;
+}
+
+menuitem {
+ display: block;
+ padding: 0 20px;
+ line-height: 20px;
+ font-weight: 500;
+ font-size: 13px;
+ user-select: none;
+}
+
+menuitem:hover {
+ background: #3780fb;
+ color: white;
+}
+
+menuitem[disabled="true"] {
+ color: #cccccc;
+}
+
+menuitem[disabled="true"]:hover {
+ background-color: transparent;
+ cursor: default;
+}
+
+menuseparator {
+ border-bottom: 1px solid #cacdd3;
+ width: 100%;
+ height: 5px;
+ display: block;
+ margin-bottom: 5px;
+}
+
+#contextmenu-mask.show {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 999;
+}
diff --git a/devtools/client/debugger/src/components/shared/moz.build b/devtools/client/debugger/src/components/shared/moz.build
new file mode 100644
index 0000000000..b30ea0ab4f
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/moz.build
@@ -0,0 +1,23 @@
+# 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 += [
+ "Button",
+]
+
+CompiledModules(
+ "AccessibleImage.js",
+ "Accordion.js",
+ "Badge.js",
+ "BracketArrow.js",
+ "Dropdown.js",
+ "Modal.js",
+ "Popover.js",
+ "PreviewFunction.js",
+ "ResultList.js",
+ "SearchInput.js",
+ "SourceIcon.js",
+ "SmartGap.js",
+)
diff --git a/devtools/client/debugger/src/components/shared/tests/Accordion.spec.js b/devtools/client/debugger/src/components/shared/tests/Accordion.spec.js
new file mode 100644
index 0000000000..cbe5ab12bf
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/Accordion.spec.js
@@ -0,0 +1,47 @@
+/* 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 React from "devtools/client/shared/vendor/react";
+import { shallow } from "enzyme";
+
+import Accordion from "../Accordion";
+
+describe("Accordion", () => {
+ const testItems = [
+ {
+ header: "Test Accordion Item 1",
+ id: "accordion-item-1",
+ className: "accordion-item-1",
+ component: React.createElement("div", null),
+ opened: false,
+ onToggle: jest.fn(),
+ },
+ {
+ header: "Test Accordion Item 2",
+ id: "accordion-item-2",
+ className: "accordion-item-2",
+ component: React.createElement("div", null),
+ buttons: React.createElement("button", null),
+ opened: false,
+ onToggle: jest.fn(),
+ },
+ {
+ header: "Test Accordion Item 3",
+ id: "accordion-item-3",
+ className: "accordion-item-3",
+ component: React.createElement("div", null),
+ opened: true,
+ onToggle: jest.fn(),
+ },
+ ];
+ const wrapper = shallow(
+ React.createElement(Accordion, {
+ items: testItems,
+ })
+ );
+ it("basic render", () => expect(wrapper).toMatchSnapshot());
+ wrapper.find(".accordion-item-1 button").simulate("click");
+ it("handleClick and onToggle", () =>
+ expect(testItems[0].onToggle).toHaveBeenCalledWith(true));
+});
diff --git a/devtools/client/debugger/src/components/shared/tests/Badge.spec.js b/devtools/client/debugger/src/components/shared/tests/Badge.spec.js
new file mode 100644
index 0000000000..a19b35a7c2
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/Badge.spec.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/>. */
+
+import React from "devtools/client/shared/vendor/react";
+import { shallow } from "enzyme";
+
+import Badge from "../Badge";
+
+describe("Badge", () => {
+ it("render", () =>
+ expect(
+ shallow(
+ React.createElement(Badge, {
+ badgeText: 3,
+ })
+ )
+ ).toMatchSnapshot());
+});
diff --git a/devtools/client/debugger/src/components/shared/tests/BracketArrow.spec.js b/devtools/client/debugger/src/components/shared/tests/BracketArrow.spec.js
new file mode 100644
index 0000000000..4ce9a5b5ce
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/BracketArrow.spec.js
@@ -0,0 +1,24 @@
+/* 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 React from "devtools/client/shared/vendor/react";
+import { shallow } from "enzyme";
+
+import BracketArrow from "../BracketArrow";
+
+describe("BracketArrow", () => {
+ const wrapper = shallow(
+ React.createElement(BracketArrow, {
+ orientation: "down",
+ left: 10,
+ top: 20,
+ bottom: 50,
+ })
+ );
+ it("render", () => expect(wrapper).toMatchSnapshot());
+ it("render up", () => {
+ wrapper.setProps({ orientation: null });
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/debugger/src/components/shared/tests/Dropdown.spec.js b/devtools/client/debugger/src/components/shared/tests/Dropdown.spec.js
new file mode 100644
index 0000000000..9b001ba9e5
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/Dropdown.spec.js
@@ -0,0 +1,21 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+import React from "devtools/client/shared/vendor/react";
+import { shallow } from "enzyme";
+
+import Dropdown from "../Dropdown";
+
+describe("Dropdown", () => {
+ const wrapper = shallow(
+ React.createElement(Dropdown, {
+ panel: React.createElement("div", null),
+ icon: "✅",
+ })
+ );
+ it("render", () => expect(wrapper).toMatchSnapshot());
+ wrapper.find(".dropdown").simulate("click");
+ it("handle toggleDropdown", () =>
+ expect(wrapper.state().dropdownShown).toEqual(true));
+});
diff --git a/devtools/client/debugger/src/components/shared/tests/Modal.spec.js b/devtools/client/debugger/src/components/shared/tests/Modal.spec.js
new file mode 100644
index 0000000000..58c38502e7
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/Modal.spec.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 React from "devtools/client/shared/vendor/react";
+import { shallow } from "enzyme";
+
+import Modal from "../Modal";
+
+describe("Modal", () => {
+ it("renders", () => {
+ const wrapper = shallow(
+ React.createElement(Modal, {
+ handleClose: () => {},
+ })
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("handles close modal click", () => {
+ const handleCloseSpy = jest.fn();
+ const wrapper = shallow(
+ React.createElement(Modal, {
+ handleClose: handleCloseSpy,
+ })
+ );
+ wrapper.find(".modal-wrapper").simulate("click");
+ expect(handleCloseSpy).toHaveBeenCalled();
+ });
+
+ it("renders children", () => {
+ const wrapper = shallow(
+ React.createElement(
+ Modal,
+ {
+ handleClose: () => {},
+ },
+ React.createElement("div", {
+ className: "aChild",
+ })
+ )
+ );
+ expect(wrapper.find(".aChild")).toHaveLength(1);
+ });
+
+ it("passes additionalClass to child div class", () => {
+ const additionalClass = "testAddon";
+ const wrapper = shallow(
+ React.createElement(Modal, {
+ additionalClass,
+ handleClose: () => {},
+ })
+ );
+ expect(wrapper.find(`.modal-wrapper .${additionalClass}`)).toHaveLength(1);
+ });
+});
diff --git a/devtools/client/debugger/src/components/shared/tests/Popover.spec.js b/devtools/client/debugger/src/components/shared/tests/Popover.spec.js
new file mode 100644
index 0000000000..7150f4afe8
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/Popover.spec.js
@@ -0,0 +1,212 @@
+/* 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 React from "devtools/client/shared/vendor/react";
+import { mount } from "enzyme";
+
+import Popover from "../Popover";
+
+describe("Popover", () => {
+ const onMouseLeave = jest.fn();
+ const onKeyDown = jest.fn();
+ const editorRef = {
+ getBoundingClientRect() {
+ return {
+ x: 0,
+ y: 0,
+ width: 100,
+ height: 100,
+ top: 250,
+ right: 0,
+ bottom: 0,
+ left: 20,
+ };
+ },
+ };
+
+ const targetRef = {
+ getBoundingClientRect() {
+ return {
+ x: 0,
+ y: 0,
+ width: 100,
+ height: 100,
+ top: 250,
+ right: 0,
+ bottom: 0,
+ left: 20,
+ };
+ },
+ };
+ const targetPosition = {
+ x: 100,
+ y: 200,
+ width: 300,
+ height: 300,
+ top: 50,
+ right: 0,
+ bottom: 0,
+ left: 200,
+ };
+ const popover = mount(
+ React.createElement(
+ Popover,
+ {
+ onMouseLeave: onMouseLeave,
+ onKeyDown: onKeyDown,
+ editorRef: editorRef,
+ targetPosition: targetPosition,
+ mouseout: () => {},
+ target: targetRef,
+ },
+ React.createElement("h1", null, "Poppy!")
+ )
+ );
+
+ const tooltip = mount(
+ React.createElement(
+ Popover,
+ {
+ type: "tooltip",
+ onMouseLeave: onMouseLeave,
+ onKeyDown: onKeyDown,
+ editorRef: editorRef,
+ targetPosition: targetPosition,
+ mouseout: () => {},
+ target: targetRef,
+ },
+ React.createElement("h1", null, "Toolie!")
+ )
+ );
+
+ beforeEach(() => {
+ onMouseLeave.mockClear();
+ onKeyDown.mockClear();
+ });
+
+ it("render", () => expect(popover).toMatchSnapshot());
+
+ it("render (tooltip)", () => expect(tooltip).toMatchSnapshot());
+
+ it("mount popover", () => {
+ const mountedPopover = mount(
+ React.createElement(
+ Popover,
+ {
+ onMouseLeave: onMouseLeave,
+ onKeyDown: onKeyDown,
+ editorRef: editorRef,
+ targetPosition: targetPosition,
+ mouseout: () => {},
+ target: targetRef,
+ },
+ React.createElement("h1", null, "Poppy!")
+ )
+ );
+ expect(mountedPopover).toMatchSnapshot();
+ });
+
+ it("mount tooltip", () => {
+ const mountedTooltip = mount(
+ React.createElement(
+ Popover,
+ {
+ type: "tooltip",
+ onMouseLeave: onMouseLeave,
+ onKeyDown: onKeyDown,
+ editorRef: editorRef,
+ targetPosition: targetPosition,
+ mouseout: () => {},
+ target: targetRef,
+ },
+ React.createElement("h1", null, "Toolie!")
+ )
+ );
+ expect(mountedTooltip).toMatchSnapshot();
+ });
+
+ it("tooltip normally displays above the target", () => {
+ const editor = {
+ getBoundingClientRect() {
+ return {
+ width: 500,
+ height: 500,
+ top: 0,
+ bottom: 500,
+ left: 0,
+ right: 500,
+ };
+ },
+ };
+ const target = {
+ width: 30,
+ height: 10,
+ top: 100,
+ bottom: 110,
+ left: 20,
+ right: 50,
+ };
+
+ const mountedTooltip = mount(
+ React.createElement(
+ Popover,
+ {
+ type: "tooltip",
+ onMouseLeave: onMouseLeave,
+ onKeyDown: onKeyDown,
+ editorRef: editor,
+ targetPosition: target,
+ mouseout: () => {},
+ target: targetRef,
+ },
+ React.createElement("h1", null, "Toolie!")
+ )
+ );
+
+ const toolTipTop = parseInt(mountedTooltip.getDOMNode().style.top, 10);
+ expect(toolTipTop).toBeLessThanOrEqual(target.top);
+ });
+
+ it("tooltop won't display above the target when insufficient space", () => {
+ const editor = {
+ getBoundingClientRect() {
+ return {
+ width: 100,
+ height: 100,
+ top: 0,
+ bottom: 100,
+ left: 0,
+ right: 100,
+ };
+ },
+ };
+ const target = {
+ width: 30,
+ height: 10,
+ top: 0,
+ bottom: 10,
+ left: 20,
+ right: 50,
+ };
+
+ const mountedTooltip = mount(
+ React.createElement(
+ Popover,
+ {
+ type: "tooltip",
+ onMouseLeave: onMouseLeave,
+ onKeyDown: onKeyDown,
+ editorRef: editor,
+ targetPosition: target,
+ mouseout: () => {},
+ target: targetRef,
+ },
+ React.createElement("h1", null, "Toolie!")
+ )
+ );
+
+ const toolTipTop = parseInt(mountedTooltip.getDOMNode().style.top, 10);
+ expect(toolTipTop).toBeGreaterThanOrEqual(target.bottom);
+ });
+});
diff --git a/devtools/client/debugger/src/components/shared/tests/PreviewFunction.spec.js b/devtools/client/debugger/src/components/shared/tests/PreviewFunction.spec.js
new file mode 100644
index 0000000000..62f635acc1
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/PreviewFunction.spec.js
@@ -0,0 +1,130 @@
+/* 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 React from "devtools/client/shared/vendor/react";
+import { shallow } from "enzyme";
+import PreviewFunction from "../PreviewFunction";
+
+function render(props) {
+ return shallow(React.createElement(PreviewFunction, props), {
+ context: {
+ l10n: L10N,
+ },
+ });
+}
+
+describe("PreviewFunction", () => {
+ it("should return a span", () => {
+ const item = { name: "" };
+ const returnedSpan = render({ func: item });
+ expect(returnedSpan).toMatchSnapshot();
+ expect(returnedSpan.name()).toEqual("span");
+ });
+
+ it('should return a span with a class of "function-signature"', () => {
+ const item = { name: "" };
+ const returnedSpan = render({ func: item });
+ expect(returnedSpan.hasClass("function-signature")).toBe(true);
+ });
+
+ it("should return a span with 3 children", () => {
+ const item = { name: "" };
+ const returnedSpan = render({ func: item });
+ expect(returnedSpan.children()).toHaveLength(3);
+ });
+
+ describe("function name", () => {
+ it("should be a span", () => {
+ const item = { name: "" };
+ const returnedSpan = render({ func: item });
+ expect(returnedSpan.children().first().name()).toEqual("span");
+ });
+
+ it('should have a "function-name" class', () => {
+ const item = { name: "" };
+ const returnedSpan = render({ func: item });
+ expect(returnedSpan.children().first().hasClass("function-name")).toBe(
+ true
+ );
+ });
+
+ it("should be be set to userDisplayName if defined", () => {
+ const item = {
+ name: "",
+ displayName: "chuck",
+ };
+ const returnedSpan = render({ func: item });
+ expect(returnedSpan.children().first().first().text()).toEqual("chuck");
+ });
+
+ it('should use displayName if defined & no "userDisplayName" exist', () => {
+ const item = {
+ displayName: "norris",
+ name: "last",
+ };
+ const returnedSpan = render({ func: item });
+ expect(returnedSpan.children().first().first().text()).toEqual("norris");
+ });
+
+ it('should use to name if no "userDisplayName"/"displayName" exist', () => {
+ const item = {
+ name: "last",
+ };
+ const returnedSpan = render({ func: item });
+ expect(returnedSpan.children().first().first().text()).toEqual("last");
+ });
+ });
+
+ describe("render parentheses", () => {
+ let leftParen;
+ let rightParen;
+
+ beforeAll(() => {
+ const item = { name: "" };
+ const returnedSpan = render({ func: item });
+ const children = returnedSpan.children();
+ leftParen = returnedSpan.childAt(1);
+ rightParen = returnedSpan.childAt(children.length - 1);
+ });
+
+ it("should be spans", () => {
+ expect(leftParen.name()).toEqual("span");
+ expect(rightParen.name()).toEqual("span");
+ });
+
+ it("should create a left paren", () => {
+ expect(leftParen.text()).toEqual("(");
+ });
+
+ it("should create a right paren", () => {
+ expect(rightParen.text()).toEqual(")");
+ });
+ });
+
+ describe("render parameters", () => {
+ let returnedSpan;
+ let children;
+
+ beforeAll(() => {
+ const item = {
+ name: "",
+ parameterNames: ["one", "two", "three"],
+ };
+ returnedSpan = render({ func: item });
+ children = returnedSpan.children();
+ });
+
+ it("should render spans according to the dynamic params given", () => {
+ expect(children).toHaveLength(8);
+ });
+
+ it("should render the parameters names", () => {
+ expect(returnedSpan.childAt(2).text()).toEqual("one");
+ });
+
+ it("should render the parameters commas", () => {
+ expect(returnedSpan.childAt(3).text()).toEqual(", ");
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/components/shared/tests/ResultList.spec.js b/devtools/client/debugger/src/components/shared/tests/ResultList.spec.js
new file mode 100644
index 0000000000..4cdc85fb23
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/ResultList.spec.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 React from "devtools/client/shared/vendor/react";
+import { shallow } from "enzyme";
+import ResultList from "../ResultList";
+
+const selectItem = jest.fn();
+const selectedIndex = 1;
+const payload = {
+ items: [
+ {
+ id: 0,
+ subtitle: "subtitle",
+ title: "title",
+ value: "value",
+ },
+ {
+ id: 1,
+ subtitle: "subtitle 1",
+ title: "title 1",
+ value: "value 1",
+ },
+ ],
+ selected: selectedIndex,
+ selectItem,
+};
+
+describe("Result list", () => {
+ it("should call onClick function", () => {
+ const wrapper = shallow(React.createElement(ResultList, payload));
+ wrapper.childAt(selectedIndex).simulate("click");
+ expect(selectItem).toHaveBeenCalled();
+ });
+
+ it("should render the component", () => {
+ const wrapper = shallow(React.createElement(ResultList, payload));
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("selected index should have 'selected class'", () => {
+ const wrapper = shallow(React.createElement(ResultList, payload));
+ const childHasClass = wrapper.childAt(selectedIndex).hasClass("selected");
+
+ expect(childHasClass).toEqual(true);
+ });
+});
diff --git a/devtools/client/debugger/src/components/shared/tests/SearchInput.spec.js b/devtools/client/debugger/src/components/shared/tests/SearchInput.spec.js
new file mode 100644
index 0000000000..c4c3990771
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/SearchInput.spec.js
@@ -0,0 +1,126 @@
+/* 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 React from "devtools/client/shared/vendor/react";
+import { shallow } from "enzyme";
+import configureStore from "redux-mock-store";
+
+import SearchInput from "../SearchInput";
+
+describe("SearchInput", () => {
+ // !! wrapper is defined outside test scope
+ // so it will keep values between tests
+ const mockStore = configureStore([]);
+ const store = mockStore({
+ ui: { mutableSearchOptions: { "foo-search": {} } },
+ });
+ const wrapper = shallow(
+ React.createElement(SearchInput, {
+ store: store,
+ query: "",
+ count: 5,
+ placeholder: "A placeholder",
+ summaryMsg: "So many results",
+ showErrorEmoji: false,
+ isLoading: false,
+ onChange: () => {},
+ onKeyDown: () => {},
+ searchKey: "foo-search",
+ showSearchModifiers: false,
+ showExcludePatterns: false,
+ showClose: true,
+ handleClose: jest.fn(),
+ setSearchOptions: jest.fn(),
+ })
+ ).dive();
+
+ it("renders", () => expect(wrapper).toMatchSnapshot());
+
+ it("shows nav buttons", () => {
+ wrapper.setProps({
+ handleNext: jest.fn(),
+ handlePrev: jest.fn(),
+ });
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("shows svg error emoji", () => {
+ wrapper.setProps({ showErrorEmoji: true });
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("shows svg magnifying glass", () => {
+ wrapper.setProps({ showErrorEmoji: false });
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ describe("with optional onHistoryScroll", () => {
+ const searches = ["foo", "bar", "baz"];
+ const createSearch = term => ({
+ target: { value: term },
+ key: "Enter",
+ });
+
+ const scrollUp = currentTerm => ({
+ key: "ArrowUp",
+ target: { value: currentTerm },
+ preventDefault: jest.fn(),
+ });
+ const scrollDown = currentTerm => ({
+ key: "ArrowDown",
+ target: { value: currentTerm },
+ preventDefault: jest.fn(),
+ });
+
+ it("stores entered history in state", () => {
+ wrapper.setProps({
+ onHistoryScroll: jest.fn(),
+ onKeyDown: jest.fn(),
+ });
+ wrapper.find("input").simulate("keyDown", createSearch(searches[0]));
+ expect(wrapper.state().history[0]).toEqual(searches[0]);
+ });
+
+ it("stores scroll history in state", () => {
+ const onHistoryScroll = jest.fn();
+ wrapper.setProps({
+ onHistoryScroll,
+ onKeyDown: jest.fn(),
+ });
+ wrapper.find("input").simulate("keyDown", createSearch(searches[0]));
+ wrapper.find("input").simulate("keyDown", createSearch(searches[1]));
+ expect(wrapper.state().history[0]).toEqual(searches[0]);
+ expect(wrapper.state().history[1]).toEqual(searches[1]);
+ });
+
+ it("scrolls up stored history on arrow up", () => {
+ const onHistoryScroll = jest.fn();
+ wrapper.setProps({
+ onHistoryScroll,
+ onKeyDown: jest.fn(),
+ });
+ wrapper.find("input").simulate("keyDown", createSearch(searches[0]));
+ wrapper.find("input").simulate("keyDown", createSearch(searches[1]));
+ wrapper.find("input").simulate("keyDown", scrollUp(searches[1]));
+ expect(wrapper.state().history[0]).toEqual(searches[0]);
+ expect(wrapper.state().history[1]).toEqual(searches[1]);
+ expect(onHistoryScroll).toHaveBeenCalledWith(searches[0]);
+ });
+
+ it("scrolls down stored history on arrow down", () => {
+ const onHistoryScroll = jest.fn();
+ wrapper.setProps({
+ onHistoryScroll,
+ onKeyDown: jest.fn(),
+ });
+ wrapper.find("input").simulate("keyDown", createSearch(searches[0]));
+ wrapper.find("input").simulate("keyDown", createSearch(searches[1]));
+ wrapper.find("input").simulate("keyDown", createSearch(searches[2]));
+ wrapper.find("input").simulate("keyDown", scrollUp(searches[2]));
+ wrapper.find("input").simulate("keyDown", scrollUp(searches[1]));
+ wrapper.find("input").simulate("keyDown", scrollDown(searches[0]));
+ expect(onHistoryScroll.mock.calls[2][0]).toBe(searches[1]);
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/components/shared/tests/__snapshots__/Accordion.spec.js.snap b/devtools/client/debugger/src/components/shared/tests/__snapshots__/Accordion.spec.js.snap
new file mode 100644
index 0000000000..abd8f10f51
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/__snapshots__/Accordion.spec.js.snap
@@ -0,0 +1,81 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Accordion basic render 1`] = `
+<div
+ className="accordion"
+>
+ <aside
+ aria-labelledby="accordion-item-1"
+ className="accordion-item-1"
+ key="accordion-item-1"
+ >
+ <h2
+ className="_header"
+ >
+ <button
+ aria-controls="accordion-item-1-content"
+ aria-expanded="true"
+ className="header-label"
+ id="accordion-item-1"
+ onClick={[Function]}
+ >
+ Test Accordion Item 1
+ </button>
+ </h2>
+ <div
+ className="_content"
+ id="accordion-item-1-content"
+ >
+ <div />
+ </div>
+ </aside>
+ <aside
+ aria-labelledby="accordion-item-2"
+ className="accordion-item-2"
+ key="accordion-item-2"
+ >
+ <h2
+ className="_header"
+ >
+ <button
+ aria-expanded="false"
+ className="header-label"
+ id="accordion-item-2"
+ onClick={[Function]}
+ >
+ Test Accordion Item 2
+ </button>
+ <div
+ className="header-buttons"
+ >
+ <button />
+ </div>
+ </h2>
+ </aside>
+ <aside
+ aria-labelledby="accordion-item-3"
+ className="accordion-item-3"
+ key="accordion-item-3"
+ >
+ <h2
+ className="_header"
+ >
+ <button
+ aria-controls="accordion-item-3-content"
+ aria-expanded="true"
+ className="header-label"
+ id="accordion-item-3"
+ onClick={[Function]}
+ >
+ Test Accordion Item 3
+ </button>
+ </h2>
+ <div
+ className="_content"
+ id="accordion-item-3-content"
+ >
+ <div />
+ </div>
+ </aside>
+</div>
+`;
diff --git a/devtools/client/debugger/src/components/shared/tests/__snapshots__/Badge.spec.js.snap b/devtools/client/debugger/src/components/shared/tests/__snapshots__/Badge.spec.js.snap
new file mode 100644
index 0000000000..cbeeeaa3f2
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/__snapshots__/Badge.spec.js.snap
@@ -0,0 +1,9 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Badge render 1`] = `
+<span
+ className="badge text-white text-center"
+>
+ 3
+</span>
+`;
diff --git a/devtools/client/debugger/src/components/shared/tests/__snapshots__/BracketArrow.spec.js.snap b/devtools/client/debugger/src/components/shared/tests/__snapshots__/BracketArrow.spec.js.snap
new file mode 100644
index 0000000000..5078cebc9e
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/__snapshots__/BracketArrow.spec.js.snap
@@ -0,0 +1,27 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`BracketArrow render 1`] = `
+<div
+ className="bracket-arrow down"
+ style={
+ Object {
+ "bottom": 50,
+ "left": 10,
+ "top": 20,
+ }
+ }
+/>
+`;
+
+exports[`BracketArrow render up 1`] = `
+<div
+ className="bracket-arrow up"
+ style={
+ Object {
+ "bottom": 50,
+ "left": 10,
+ "top": 20,
+ }
+ }
+/>
+`;
diff --git a/devtools/client/debugger/src/components/shared/tests/__snapshots__/Dropdown.spec.js.snap b/devtools/client/debugger/src/components/shared/tests/__snapshots__/Dropdown.spec.js.snap
new file mode 100644
index 0000000000..fd60784327
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/__snapshots__/Dropdown.spec.js.snap
@@ -0,0 +1,34 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Dropdown render 1`] = `
+<div
+ className="dropdown-block"
+>
+ <div
+ className="dropdown"
+ onClick={[Function]}
+ style={
+ Object {
+ "display": "block",
+ }
+ }
+ >
+ <div />
+ </div>
+ <button
+ className="dropdown-button"
+ onClick={[Function]}
+ >
+ ✅
+ </button>
+ <div
+ className="dropdown-mask"
+ onClick={[Function]}
+ style={
+ Object {
+ "display": "block",
+ }
+ }
+ />
+</div>
+`;
diff --git a/devtools/client/debugger/src/components/shared/tests/__snapshots__/Modal.spec.js.snap b/devtools/client/debugger/src/components/shared/tests/__snapshots__/Modal.spec.js.snap
new file mode 100644
index 0000000000..c8534c4032
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/__snapshots__/Modal.spec.js.snap
@@ -0,0 +1,13 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Modal renders 1`] = `
+<div
+ className="modal-wrapper"
+ onClick={[Function]}
+>
+ <div
+ className="modal"
+ onClick={[Function]}
+ />
+</div>
+`;
diff --git a/devtools/client/debugger/src/components/shared/tests/__snapshots__/Popover.spec.js.snap b/devtools/client/debugger/src/components/shared/tests/__snapshots__/Popover.spec.js.snap
new file mode 100644
index 0000000000..1c3589a6f8
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/__snapshots__/Popover.spec.js.snap
@@ -0,0 +1,549 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Popover mount popover 1`] = `
+<Popover
+ editorRef={
+ Object {
+ "getBoundingClientRect": [Function],
+ }
+ }
+ mouseout={[Function]}
+ onKeyDown={[MockFunction]}
+ onMouseLeave={[MockFunction]}
+ target={
+ Object {
+ "getBoundingClientRect": [Function],
+ }
+ }
+ targetPosition={
+ Object {
+ "bottom": 0,
+ "height": 300,
+ "left": 200,
+ "right": 0,
+ "top": 50,
+ "width": 300,
+ "x": 100,
+ "y": 200,
+ }
+ }
+ type="popover"
+>
+ <div
+ className="popover orientation-right"
+ style={
+ Object {
+ "left": 500,
+ "top": -50,
+ }
+ }
+ >
+ <BracketArrow
+ left={-4}
+ orientation="left"
+ top={98}
+ >
+ <div
+ className="bracket-arrow left"
+ style={
+ Object {
+ "bottom": undefined,
+ "left": -4,
+ "top": 98,
+ }
+ }
+ />
+ </BracketArrow>
+ <div
+ className="gap"
+ key="gap"
+ >
+ <SmartGap
+ coords={
+ Object {
+ "left": 500,
+ "orientation": "right",
+ "targetMid": Object {
+ "x": -14,
+ "y": 98,
+ },
+ "top": -50,
+ }
+ }
+ gapHeight={0}
+ offset={0}
+ preview={
+ <div
+ class="popover orientation-right"
+ style="top: -50px; left: 500px;"
+ >
+ <div
+ class="bracket-arrow left"
+ style="left: -4px; top: 98px;"
+ />
+ <div
+ class="gap"
+ >
+ <svg
+ style="height: 0px; width: 480px; position: absolute; margin-left: -100px;"
+ version="1.1"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <polygon
+ fill="transparent"
+ points="0,300,100,0,100,0,0,400,100,400,100,300"
+ />
+ </svg>
+ </div>
+ <h1>
+ Poppy!
+ </h1>
+ </div>
+ }
+ token={
+ Object {
+ "getBoundingClientRect": [Function],
+ }
+ }
+ type="popover"
+ >
+ <svg
+ style={
+ Object {
+ "height": 0,
+ "marginLeft": -100,
+ "marginTop": undefined,
+ "position": "absolute",
+ "width": 480,
+ }
+ }
+ version="1.1"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <polygon
+ fill="transparent"
+ points={
+ Array [
+ 0,
+ 300,
+ 100,
+ 0,
+ 100,
+ 0,
+ 0,
+ 400,
+ 100,
+ 400,
+ 100,
+ 300,
+ ]
+ }
+ />
+ </svg>
+ </SmartGap>
+ </div>
+ <h1>
+ Poppy!
+ </h1>
+ </div>
+</Popover>
+`;
+
+exports[`Popover mount tooltip 1`] = `
+<Popover
+ editorRef={
+ Object {
+ "getBoundingClientRect": [Function],
+ }
+ }
+ mouseout={[Function]}
+ onKeyDown={[MockFunction]}
+ onMouseLeave={[MockFunction]}
+ target={
+ Object {
+ "getBoundingClientRect": [Function],
+ }
+ }
+ targetPosition={
+ Object {
+ "bottom": 0,
+ "height": 300,
+ "left": 200,
+ "right": 0,
+ "top": 50,
+ "width": 300,
+ "x": 100,
+ "y": 200,
+ }
+ }
+ type="tooltip"
+>
+ <div
+ className="tooltip orientation-down"
+ style={
+ Object {
+ "left": -8,
+ "top": 0,
+ }
+ }
+ >
+ <div
+ className="gap"
+ key="gap"
+ >
+ <SmartGap
+ coords={
+ Object {
+ "left": -8,
+ "orientation": "down",
+ "targetMid": Object {
+ "x": 0,
+ "y": 0,
+ },
+ "top": 0,
+ }
+ }
+ gapHeight={0}
+ offset={0}
+ preview={
+ <div
+ class="tooltip orientation-down"
+ style="top: 0px; left: -8px;"
+ >
+ <div
+ class="gap"
+ >
+ <svg
+ style="height: -250px; width: 100px; position: absolute; margin-top: -100px;"
+ version="1.1"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <polygon
+ fill="transparent"
+ points="0,-250,0,-250,28,100,128,100"
+ />
+ </svg>
+ </div>
+ <h1>
+ Toolie!
+ </h1>
+ </div>
+ }
+ token={
+ Object {
+ "getBoundingClientRect": [Function],
+ }
+ }
+ type="tooltip"
+ >
+ <svg
+ style={
+ Object {
+ "height": -250,
+ "marginLeft": undefined,
+ "marginTop": -100,
+ "position": "absolute",
+ "width": 100,
+ }
+ }
+ version="1.1"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <polygon
+ fill="transparent"
+ points={
+ Array [
+ 0,
+ -250,
+ 0,
+ -250,
+ 28,
+ 100,
+ 128,
+ 100,
+ ]
+ }
+ />
+ </svg>
+ </SmartGap>
+ </div>
+ <h1>
+ Toolie!
+ </h1>
+ </div>
+</Popover>
+`;
+
+exports[`Popover render (tooltip) 1`] = `
+<Popover
+ editorRef={
+ Object {
+ "getBoundingClientRect": [Function],
+ }
+ }
+ mouseout={[Function]}
+ onKeyDown={[MockFunction]}
+ onMouseLeave={[MockFunction]}
+ target={
+ Object {
+ "getBoundingClientRect": [Function],
+ }
+ }
+ targetPosition={
+ Object {
+ "bottom": 0,
+ "height": 300,
+ "left": 200,
+ "right": 0,
+ "top": 50,
+ "width": 300,
+ "x": 100,
+ "y": 200,
+ }
+ }
+ type="tooltip"
+>
+ <div
+ className="tooltip orientation-down"
+ style={
+ Object {
+ "left": -8,
+ "top": 0,
+ }
+ }
+ >
+ <div
+ className="gap"
+ key="gap"
+ >
+ <SmartGap
+ coords={
+ Object {
+ "left": -8,
+ "orientation": "down",
+ "targetMid": Object {
+ "x": 0,
+ "y": 0,
+ },
+ "top": 0,
+ }
+ }
+ gapHeight={0}
+ offset={0}
+ preview={
+ <div
+ class="tooltip orientation-down"
+ style="top: 0px; left: -8px;"
+ >
+ <div
+ class="gap"
+ >
+ <svg
+ style="height: -250px; width: 100px; position: absolute; margin-top: -100px;"
+ version="1.1"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <polygon
+ fill="transparent"
+ points="0,-250,0,-250,28,100,128,100"
+ />
+ </svg>
+ </div>
+ <h1>
+ Toolie!
+ </h1>
+ </div>
+ }
+ token={
+ Object {
+ "getBoundingClientRect": [Function],
+ }
+ }
+ type="tooltip"
+ >
+ <svg
+ style={
+ Object {
+ "height": -250,
+ "marginLeft": undefined,
+ "marginTop": -100,
+ "position": "absolute",
+ "width": 100,
+ }
+ }
+ version="1.1"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <polygon
+ fill="transparent"
+ points={
+ Array [
+ 0,
+ -250,
+ 0,
+ -250,
+ 28,
+ 100,
+ 128,
+ 100,
+ ]
+ }
+ />
+ </svg>
+ </SmartGap>
+ </div>
+ <h1>
+ Toolie!
+ </h1>
+ </div>
+</Popover>
+`;
+
+exports[`Popover render 1`] = `
+<Popover
+ editorRef={
+ Object {
+ "getBoundingClientRect": [Function],
+ }
+ }
+ mouseout={[Function]}
+ onKeyDown={[MockFunction]}
+ onMouseLeave={[MockFunction]}
+ target={
+ Object {
+ "getBoundingClientRect": [Function],
+ }
+ }
+ targetPosition={
+ Object {
+ "bottom": 0,
+ "height": 300,
+ "left": 200,
+ "right": 0,
+ "top": 50,
+ "width": 300,
+ "x": 100,
+ "y": 200,
+ }
+ }
+ type="popover"
+>
+ <div
+ className="popover orientation-right"
+ style={
+ Object {
+ "left": 500,
+ "top": -50,
+ }
+ }
+ >
+ <BracketArrow
+ left={-4}
+ orientation="left"
+ top={98}
+ >
+ <div
+ className="bracket-arrow left"
+ style={
+ Object {
+ "bottom": undefined,
+ "left": -4,
+ "top": 98,
+ }
+ }
+ />
+ </BracketArrow>
+ <div
+ className="gap"
+ key="gap"
+ >
+ <SmartGap
+ coords={
+ Object {
+ "left": 500,
+ "orientation": "right",
+ "targetMid": Object {
+ "x": -14,
+ "y": 98,
+ },
+ "top": -50,
+ }
+ }
+ gapHeight={0}
+ offset={0}
+ preview={
+ <div
+ class="popover orientation-right"
+ style="top: -50px; left: 500px;"
+ >
+ <div
+ class="bracket-arrow left"
+ style="left: -4px; top: 98px;"
+ />
+ <div
+ class="gap"
+ >
+ <svg
+ style="height: 0px; width: 480px; position: absolute; margin-left: -100px;"
+ version="1.1"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <polygon
+ fill="transparent"
+ points="0,300,100,0,100,0,0,400,100,400,100,300"
+ />
+ </svg>
+ </div>
+ <h1>
+ Poppy!
+ </h1>
+ </div>
+ }
+ token={
+ Object {
+ "getBoundingClientRect": [Function],
+ }
+ }
+ type="popover"
+ >
+ <svg
+ style={
+ Object {
+ "height": 0,
+ "marginLeft": -100,
+ "marginTop": undefined,
+ "position": "absolute",
+ "width": 480,
+ }
+ }
+ version="1.1"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <polygon
+ fill="transparent"
+ points={
+ Array [
+ 0,
+ 300,
+ 100,
+ 0,
+ 100,
+ 0,
+ 0,
+ 400,
+ 100,
+ 400,
+ 100,
+ 300,
+ ]
+ }
+ />
+ </svg>
+ </SmartGap>
+ </div>
+ <h1>
+ Poppy!
+ </h1>
+ </div>
+</Popover>
+`;
diff --git a/devtools/client/debugger/src/components/shared/tests/__snapshots__/PreviewFunction.spec.js.snap b/devtools/client/debugger/src/components/shared/tests/__snapshots__/PreviewFunction.spec.js.snap
new file mode 100644
index 0000000000..e766bd45aa
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/__snapshots__/PreviewFunction.spec.js.snap
@@ -0,0 +1,23 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`PreviewFunction should return a span 1`] = `
+<span
+ className="function-signature"
+>
+ <span
+ className="function-name"
+ >
+ &lt;anonymous&gt;
+ </span>
+ <span
+ className="paren"
+ >
+ (
+ </span>
+ <span
+ className="paren"
+ >
+ )
+ </span>
+</span>
+`;
diff --git a/devtools/client/debugger/src/components/shared/tests/__snapshots__/ResultList.spec.js.snap b/devtools/client/debugger/src/components/shared/tests/__snapshots__/ResultList.spec.js.snap
new file mode 100644
index 0000000000..d3d8b27575
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/__snapshots__/ResultList.spec.js.snap
@@ -0,0 +1,55 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Result list should render the component 1`] = `
+<ul
+ aria-live="polite"
+ className="result-list small"
+ id="result-list"
+ role="listbox"
+>
+ <li
+ aria-describedby="0-subtitle"
+ aria-labelledby="0-title"
+ className="result-item"
+ key="0value0"
+ onClick={[Function]}
+ role="option"
+ title="value"
+ >
+ <div
+ className="title"
+ id="0-title"
+ >
+ title
+ </div>
+ <div
+ className="subtitle"
+ id="0-subtitle"
+ >
+ subtitle
+ </div>
+ </li>
+ <li
+ aria-describedby="1-subtitle"
+ aria-labelledby="1-title"
+ className="result-item selected"
+ key="1value 11"
+ onClick={[Function]}
+ role="option"
+ title="value 1"
+ >
+ <div
+ className="title"
+ id="1-title"
+ >
+ title 1
+ </div>
+ <div
+ className="subtitle"
+ id="1-subtitle"
+ >
+ subtitle 1
+ </div>
+ </li>
+</ul>
+`;
diff --git a/devtools/client/debugger/src/components/shared/tests/__snapshots__/SearchInput.spec.js.snap b/devtools/client/debugger/src/components/shared/tests/__snapshots__/SearchInput.spec.js.snap
new file mode 100644
index 0000000000..c56a13dc3b
--- /dev/null
+++ b/devtools/client/debugger/src/components/shared/tests/__snapshots__/SearchInput.spec.js.snap
@@ -0,0 +1,267 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SearchInput renders 1`] = `
+<div
+ className="search-outline"
+>
+ <div
+ aria-expanded={false}
+ aria-haspopup="listbox"
+ aria-owns="result-list"
+ className="search-field"
+ role="combobox"
+ >
+ <AccessibleImage
+ className="search"
+ />
+ <input
+ aria-activedescendant=""
+ aria-autocomplete="list"
+ aria-controls="result-list"
+ className=""
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ placeholder="A placeholder"
+ spellCheck={false}
+ value=""
+ />
+ <div
+ className="search-field-summary"
+ >
+ So many results
+ </div>
+ <div
+ className="search-buttons-bar"
+ >
+ <span
+ className="pipe-divider"
+ />
+ <CloseButton
+ buttonClass=""
+ handleClick={[MockFunction]}
+ />
+ </div>
+ </div>
+</div>
+`;
+
+exports[`SearchInput shows nav buttons 1`] = `
+<div
+ className="search-outline"
+>
+ <div
+ aria-expanded={false}
+ aria-haspopup="listbox"
+ aria-owns="result-list"
+ className="search-field"
+ role="combobox"
+ >
+ <AccessibleImage
+ className="search"
+ />
+ <input
+ aria-activedescendant=""
+ aria-autocomplete="list"
+ aria-controls="result-list"
+ className=""
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ placeholder="A placeholder"
+ spellCheck={false}
+ value=""
+ />
+ <div
+ className="search-field-summary"
+ >
+ So many results
+ </div>
+ <div
+ className="search-nav-buttons"
+ >
+ <button
+ className="nav-btn prev"
+ key="arrow-up"
+ onClick={[MockFunction]}
+ title="Previous result"
+ type="arrow-up"
+ >
+ <AccessibleImage
+ className="arrow-up"
+ />
+ </button>
+ <button
+ className="nav-btn next"
+ key="arrow-down"
+ onClick={[MockFunction]}
+ title="Next result"
+ type="arrow-down"
+ >
+ <AccessibleImage
+ className="arrow-down"
+ />
+ </button>
+ </div>
+ <div
+ className="search-buttons-bar"
+ >
+ <span
+ className="pipe-divider"
+ />
+ <CloseButton
+ buttonClass=""
+ handleClick={[MockFunction]}
+ />
+ </div>
+ </div>
+</div>
+`;
+
+exports[`SearchInput shows svg error emoji 1`] = `
+<div
+ className="search-outline"
+>
+ <div
+ aria-expanded={false}
+ aria-haspopup="listbox"
+ aria-owns="result-list"
+ className="search-field"
+ role="combobox"
+ >
+ <AccessibleImage
+ className="search"
+ />
+ <input
+ aria-activedescendant=""
+ aria-autocomplete="list"
+ aria-controls="result-list"
+ className="empty"
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ placeholder="A placeholder"
+ spellCheck={false}
+ value=""
+ />
+ <div
+ className="search-field-summary"
+ >
+ So many results
+ </div>
+ <div
+ className="search-nav-buttons"
+ >
+ <button
+ className="nav-btn prev"
+ key="arrow-up"
+ onClick={[MockFunction]}
+ title="Previous result"
+ type="arrow-up"
+ >
+ <AccessibleImage
+ className="arrow-up"
+ />
+ </button>
+ <button
+ className="nav-btn next"
+ key="arrow-down"
+ onClick={[MockFunction]}
+ title="Next result"
+ type="arrow-down"
+ >
+ <AccessibleImage
+ className="arrow-down"
+ />
+ </button>
+ </div>
+ <div
+ className="search-buttons-bar"
+ >
+ <span
+ className="pipe-divider"
+ />
+ <CloseButton
+ buttonClass=""
+ handleClick={[MockFunction]}
+ />
+ </div>
+ </div>
+</div>
+`;
+
+exports[`SearchInput shows svg magnifying glass 1`] = `
+<div
+ className="search-outline"
+>
+ <div
+ aria-expanded={false}
+ aria-haspopup="listbox"
+ aria-owns="result-list"
+ className="search-field"
+ role="combobox"
+ >
+ <AccessibleImage
+ className="search"
+ />
+ <input
+ aria-activedescendant=""
+ aria-autocomplete="list"
+ aria-controls="result-list"
+ className=""
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ placeholder="A placeholder"
+ spellCheck={false}
+ value=""
+ />
+ <div
+ className="search-field-summary"
+ >
+ So many results
+ </div>
+ <div
+ className="search-nav-buttons"
+ >
+ <button
+ className="nav-btn prev"
+ key="arrow-up"
+ onClick={[MockFunction]}
+ title="Previous result"
+ type="arrow-up"
+ >
+ <AccessibleImage
+ className="arrow-up"
+ />
+ </button>
+ <button
+ className="nav-btn next"
+ key="arrow-down"
+ onClick={[MockFunction]}
+ title="Next result"
+ type="arrow-down"
+ >
+ <AccessibleImage
+ className="arrow-down"
+ />
+ </button>
+ </div>
+ <div
+ className="search-buttons-bar"
+ >
+ <span
+ className="pipe-divider"
+ />
+ <CloseButton
+ buttonClass=""
+ handleClick={[MockFunction]}
+ />
+ </div>
+ </div>
+</div>
+`;
diff --git a/devtools/client/debugger/src/components/test/QuickOpenModal.spec.js b/devtools/client/debugger/src/components/test/QuickOpenModal.spec.js
new file mode 100644
index 0000000000..74bc7c75bc
--- /dev/null
+++ b/devtools/client/debugger/src/components/test/QuickOpenModal.spec.js
@@ -0,0 +1,803 @@
+/* eslint max-nested-callbacks: ["error", 4] */
+/* 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 React from "devtools/client/shared/vendor/react";
+import { Provider } from "devtools/client/shared/vendor/react-redux";
+import configureStore from "redux-mock-store";
+import { shallow, mount } from "enzyme";
+import { getDisplayURL } from "../../utils/sources-tree/getURL";
+import { searchKeys } from "../../constants";
+
+// it's important to mock the module before importing the QuickOpenModal
+jest.mock("devtools/client/shared/vendor/fuzzaldrin-plus.js", () => {
+ return {
+ filter: jest.fn(() => []),
+ prepareQuery: jest.fn(() => {}),
+ wrap: jest.fn(() => {}),
+ };
+});
+import { QuickOpenModal } from "../QuickOpenModal";
+const { filter } = require("devtools/client/shared/vendor/fuzzaldrin-plus.js");
+
+function generateModal(propOverrides, renderType = "shallow") {
+ const mockStore = configureStore([]);
+ const store = mockStore({
+ ui: {
+ mutableSearchOptions: {
+ [searchKeys.QUICKOPEN_SEARCH]: {
+ regexMatch: false,
+ wholeWord: false,
+ caseSensitive: false,
+ excludePatterns: "",
+ },
+ },
+ },
+ });
+ const props = {
+ enabled: false,
+ query: "",
+ searchType: "sources",
+ displayedSources: [],
+ blackBoxRanges: {},
+ openedTabUrls: [],
+ selectedLocation: { source: { id: "foo" } },
+ selectSpecificLocation: jest.fn(),
+ setQuickOpenQuery: jest.fn(),
+ highlightLineRange: jest.fn(),
+ clearHighlightLineRange: jest.fn(),
+ closeQuickOpen: jest.fn(),
+ getFunctionSymbols: jest.fn(() => []),
+ shortcutsModalEnabled: false,
+ toggleShortcutsModal: jest.fn(),
+ isOriginal: false,
+ thread: "FakeThread",
+ ...propOverrides,
+ };
+ return {
+ wrapper:
+ renderType === "shallow"
+ ? shallow(
+ <Provider store={store}>
+ <QuickOpenModal {...props} />
+ </Provider>
+ ).dive()
+ : mount(
+ <Provider store={store}>
+ <QuickOpenModal {...props} />
+ </Provider>
+ ),
+ props,
+ };
+}
+
+async function waitForUpdateResultsThrottle() {
+ await new Promise(res =>
+ setTimeout(res, QuickOpenModal.UPDATE_RESULTS_THROTTLE)
+ );
+}
+
+describe("QuickOpenModal", () => {
+ beforeEach(() => {
+ filter.mockClear();
+ });
+ test("Doesn't render when disabled", () => {
+ const { wrapper } = generateModal();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test("Renders when enabled", () => {
+ const { wrapper } = generateModal({ enabled: true });
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test("Basic render with mount", () => {
+ const { wrapper } = generateModal({ enabled: true }, "mount");
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test("Basic render with mount & searchType = functions", () => {
+ const { wrapper } = generateModal(
+ {
+ enabled: true,
+ query: "@",
+ searchType: "functions",
+ },
+ "mount"
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test("toggles shortcut modal if enabled", () => {
+ const { props } = generateModal(
+ {
+ enabled: true,
+ query: "test",
+ shortcutsModalEnabled: true,
+ toggleShortcutsModal: jest.fn(),
+ },
+ "shallow"
+ );
+ expect(props.toggleShortcutsModal).toHaveBeenCalled();
+ });
+
+ test("shows top sources", () => {
+ const { wrapper } = generateModal(
+ {
+ enabled: true,
+ query: "",
+ displayedSources: [
+ {
+ url: "mozilla.com",
+ displayURL: getDisplayURL("mozilla.com"),
+ },
+ ],
+ openedTabUrls: ["mozilla.com"],
+ },
+ "shallow"
+ );
+ expect(wrapper.state("results")).toEqual([
+ {
+ id: undefined,
+ icon: "tab result-item-icon",
+ subtitle: "mozilla.com",
+ title: "mozilla.com",
+ url: "mozilla.com",
+ value: "mozilla.com",
+ source: {
+ url: "mozilla.com",
+ displayURL: getDisplayURL("mozilla.com"),
+ },
+ },
+ ]);
+ });
+
+ describe("shows loading", () => {
+ it("loads with function type search", () => {
+ const { wrapper } = generateModal(
+ {
+ enabled: true,
+ query: "",
+ searchType: "functions",
+ },
+ "shallow"
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+ });
+
+ test("Basic render with mount & searchType = variables", () => {
+ const { wrapper } = generateModal(
+ {
+ enabled: true,
+ query: "#",
+ searchType: "variables",
+ },
+ "mount"
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test("Basic render with mount & searchType = shortcuts", () => {
+ const { wrapper } = generateModal(
+ {
+ enabled: true,
+ query: "?",
+ searchType: "shortcuts",
+ },
+ "mount"
+ );
+ expect(wrapper.find("ResultList")).toHaveLength(1);
+ expect(wrapper.find("li")).toHaveLength(3);
+ });
+
+ test("updateResults on enable", () => {
+ const { wrapper } = generateModal({}, "mount");
+ expect(wrapper).toMatchSnapshot();
+ wrapper.setProps({ enabled: true });
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test("basic source search", async () => {
+ const { wrapper } = generateModal(
+ {
+ enabled: true,
+ },
+ "mount"
+ );
+ wrapper.find("input").simulate("change", { target: { value: "somefil" } });
+ await waitForUpdateResultsThrottle();
+ expect(filter).toHaveBeenCalledWith([], "somefil", {
+ key: "value",
+ maxResults: 100,
+ });
+ });
+
+ test("basic gotoSource search", async () => {
+ const { wrapper } = generateModal(
+ {
+ enabled: true,
+ searchType: "gotoSource",
+ },
+ "mount"
+ );
+ wrapper
+ .find("input")
+ .simulate("change", { target: { value: "somefil:33" } });
+
+ await waitForUpdateResultsThrottle();
+
+ expect(filter).toHaveBeenCalledWith([], "somefil", {
+ key: "value",
+ maxResults: 100,
+ });
+ });
+
+ describe("empty symbol search", () => {
+ it("basic symbol search", async () => {
+ const { wrapper } = generateModal(
+ {
+ enabled: true,
+ searchType: "functions",
+ // symbol searching relies on a source being selected.
+ // So we dummy out the source and the API.
+ selectedLocation: { source: { id: "foo", text: "yo" } },
+ selectedContentLoaded: true,
+ },
+ "mount"
+ );
+
+ wrapper
+ .find("input")
+ .simulate("change", { target: { value: "@someFunc" } });
+ await waitForUpdateResultsThrottle();
+ expect(filter).toHaveBeenCalledWith([], "someFunc", {
+ key: "name",
+ maxResults: 100,
+ preparedQuery: undefined,
+ });
+ });
+
+ it("does not do symbol search if no selected source", () => {
+ const { wrapper } = generateModal(
+ {
+ enabled: true,
+ searchType: "functions",
+
+ // symbol searching relies on a source being selected.
+ // So we dummy out the source and the API.
+ selectedLocation: null,
+ selectedContentLoaded: false,
+ },
+ "mount"
+ );
+ wrapper
+ .find("input")
+ .simulate("change", { target: { value: "@someFunc" } });
+ expect(filter).not.toHaveBeenCalled();
+ });
+ });
+
+ test("Simple goto search query = :abc & searchType = goto", () => {
+ const { wrapper } = generateModal(
+ {
+ enabled: true,
+ query: ":abc",
+ searchType: "goto",
+ },
+ "mount"
+ );
+ expect(wrapper.childAt(0)).toMatchSnapshot();
+ expect(wrapper.childAt(0).state().results).toEqual(null);
+ });
+
+ describe("onEnter", () => {
+ it("on Enter go to location", () => {
+ const { wrapper, props } = generateModal(
+ {
+ enabled: true,
+ query: ":34:12",
+ searchType: "goto",
+ selectedLocation: { source: { id: "foo" } },
+ },
+ "shallow"
+ );
+ const event = {
+ key: "Enter",
+ };
+ wrapper.find("Connect(SearchInput)").simulate("keydown", event);
+ expect(props.selectSpecificLocation).toHaveBeenCalledWith({
+ column: 11,
+ line: 34,
+ source: {
+ id: "foo",
+ },
+ sourceActorId: undefined,
+ sourceActor: null,
+ });
+ });
+
+ it("on Enter go to location with sourceId", () => {
+ const sourceId = "source_id";
+ const { wrapper, props } = generateModal(
+ {
+ enabled: true,
+ query: ":34:12",
+ searchType: "goto",
+ selectedLocation: { source: { id: sourceId } },
+ selectedContentLoaded: true,
+ },
+ "shallow"
+ );
+ const event = {
+ key: "Enter",
+ };
+ wrapper.find("Connect(SearchInput)").simulate("keydown", event);
+ expect(props.selectSpecificLocation).toHaveBeenCalledWith({
+ column: 11,
+ line: 34,
+ source: {
+ id: sourceId,
+ },
+ sourceActorId: undefined,
+ sourceActor: null,
+ });
+ });
+
+ it("on Enter with no location, does no action", () => {
+ const { wrapper, props } = generateModal(
+ {
+ enabled: true,
+ query: ":",
+ searchType: "goto",
+ },
+ "shallow"
+ );
+ const event = {
+ key: "Enter",
+ };
+ wrapper.find("Connect(SearchInput)").simulate("keydown", event);
+ expect(props.setQuickOpenQuery).not.toHaveBeenCalled();
+ expect(props.selectSpecificLocation).not.toHaveBeenCalled();
+ expect(props.highlightLineRange).not.toHaveBeenCalled();
+ });
+
+ it("on Enter with empty results, handle no item", () => {
+ const { wrapper, props } = generateModal(
+ {
+ enabled: true,
+ query: "",
+ searchType: "shortcuts",
+ },
+ "shallow"
+ );
+ wrapper.setState(() => ({
+ results: [],
+ selectedIndex: 0,
+ }));
+ const event = {
+ key: "Enter",
+ };
+ wrapper.find("Connect(SearchInput)").simulate("keydown", event);
+ expect(props.setQuickOpenQuery).not.toHaveBeenCalled();
+ expect(props.selectSpecificLocation).not.toHaveBeenCalled();
+ expect(props.highlightLineRange).not.toHaveBeenCalled();
+ });
+
+ it("on Enter with results, handle symbol shortcut", () => {
+ const symbols = [":", "#", "@"];
+ for (const symbol of symbols) {
+ const { wrapper, props } = generateModal(
+ {
+ enabled: true,
+ query: "",
+ searchType: "shortcuts",
+ },
+ "shallow"
+ );
+ wrapper.setState(() => ({
+ results: [{ id: symbol }],
+ selectedIndex: 0,
+ }));
+ const event = {
+ key: "Enter",
+ };
+ wrapper.find("Connect(SearchInput)").simulate("keydown", event);
+ expect(props.setQuickOpenQuery).toHaveBeenCalledWith(symbol);
+ }
+ });
+
+ it("on Enter, returns the result with the selected index", () => {
+ const { wrapper, props } = generateModal(
+ {
+ enabled: true,
+ query: "@test",
+ searchType: "shortcuts",
+ },
+ "shallow"
+ );
+ wrapper.setState(() => ({
+ results: [{ id: "@" }, { id: ":" }, { id: "#" }],
+ selectedIndex: 1,
+ }));
+ const event = {
+ key: "Enter",
+ };
+ wrapper.find("Connect(SearchInput)").simulate("keydown", event);
+ expect(props.setQuickOpenQuery).toHaveBeenCalledWith(":");
+ });
+
+ it("on Enter with results, handle result item", () => {
+ const id = "test_id";
+ const { wrapper, props } = generateModal(
+ {
+ enabled: true,
+ query: "@test",
+ searchType: "other",
+ selectedLocation: { source: { id } },
+ },
+ "shallow"
+ );
+ wrapper.setState(() => ({
+ results: [{}, { id }],
+ selectedIndex: 1,
+ }));
+ const event = {
+ key: "Enter",
+ };
+ wrapper.find("Connect(SearchInput)").simulate("keydown", event);
+ expect(props.selectSpecificLocation).toHaveBeenCalledWith({
+ column: undefined,
+ line: 0,
+ source: { id },
+ sourceActorId: undefined,
+ sourceActor: null,
+ });
+ expect(props.setQuickOpenQuery).not.toHaveBeenCalled();
+ });
+
+ it("on Enter with results, handle functions result item", () => {
+ const id = "test_id";
+ const { wrapper, props } = generateModal(
+ {
+ enabled: true,
+ query: "@test",
+ searchType: "functions",
+ selectedLocation: { source: { id } },
+ },
+ "shallow"
+ );
+ wrapper.setState(() => ({
+ results: [{}, { id }],
+ selectedIndex: 1,
+ }));
+ const event = {
+ key: "Enter",
+ };
+ wrapper.find("Connect(SearchInput)").simulate("keydown", event);
+ expect(props.selectSpecificLocation).toHaveBeenCalledWith({
+ column: undefined,
+ line: 0,
+ source: { id },
+ sourceActorId: undefined,
+ sourceActor: null,
+ });
+ expect(props.setQuickOpenQuery).not.toHaveBeenCalled();
+ });
+
+ it("on Enter with results, handle gotoSource search", () => {
+ const id = "test_id";
+ const { wrapper, props } = generateModal(
+ {
+ enabled: true,
+ query: ":3:4",
+ searchType: "gotoSource",
+ selectedLocation: { source: { id } },
+ },
+ "shallow"
+ );
+ wrapper.setState(() => ({
+ results: [{}, { id }],
+ selectedIndex: 1,
+ }));
+ const event = {
+ key: "Enter",
+ };
+ wrapper.find("Connect(SearchInput)").simulate("keydown", event);
+ expect(props.selectSpecificLocation).toHaveBeenCalledWith({
+ column: 3,
+ line: 3,
+ source: { id },
+ sourceActorId: undefined,
+ sourceActor: null,
+ });
+ expect(props.setQuickOpenQuery).not.toHaveBeenCalled();
+ });
+
+ it("on Enter with results, handle shortcuts search", () => {
+ const { wrapper, props } = generateModal(
+ {
+ enabled: true,
+ query: "@",
+ searchType: "shortcuts",
+ },
+ "shallow"
+ );
+ const id = "#";
+ wrapper.setState(() => ({
+ results: [{}, { id }],
+ selectedIndex: 1,
+ }));
+ const event = {
+ key: "Enter",
+ };
+ wrapper.find("Connect(SearchInput)").simulate("keydown", event);
+ expect(props.selectSpecificLocation).not.toHaveBeenCalled();
+ expect(props.setQuickOpenQuery).toHaveBeenCalledWith(id);
+ });
+ });
+
+ describe("onKeyDown", () => {
+ it("does nothing if search type is not goto", () => {
+ const { wrapper, props } = generateModal(
+ {
+ enabled: true,
+ query: "test",
+ searchType: "other",
+ },
+ "shallow"
+ );
+ wrapper.find("Connect(SearchInput)").simulate("keydown", {});
+ expect(props.selectSpecificLocation).not.toHaveBeenCalled();
+ expect(props.setQuickOpenQuery).not.toHaveBeenCalled();
+ });
+
+ it("on Tab, close modal", () => {
+ const { wrapper, props } = generateModal(
+ {
+ enabled: true,
+ query: ":34:12",
+ searchType: "goto",
+ },
+ "shallow"
+ );
+ const event = {
+ key: "Tab",
+ };
+ wrapper.find("Connect(SearchInput)").simulate("keydown", event);
+ expect(props.closeQuickOpen).toHaveBeenCalled();
+ expect(props.selectSpecificLocation).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("with arrow keys", () => {
+ it("on ArrowUp, traverse results up with functions", () => {
+ const sourceId = "sourceId";
+ const { wrapper, props } = generateModal(
+ {
+ enabled: true,
+ query: "test",
+ searchType: "functions",
+ selectedLocation: { source: { id: sourceId } },
+ selectedContentLoaded: true,
+ },
+ "shallow"
+ );
+ const event = {
+ preventDefault: jest.fn(),
+ key: "ArrowUp",
+ };
+ const location = {
+ sourceId: "sourceId",
+ start: {
+ line: 1,
+ },
+ end: {
+ line: 3,
+ },
+ };
+
+ wrapper.setState(() => ({
+ results: [{ id: "0", location }, { id: "1" }, { id: "2" }],
+ selectedIndex: 1,
+ }));
+ wrapper.find("Connect(SearchInput)").simulate("keydown", event);
+ expect(event.preventDefault).toHaveBeenCalled();
+ expect(wrapper.state().selectedIndex).toEqual(0);
+ expect(props.highlightLineRange).toHaveBeenCalledWith({
+ sourceId: "sourceId",
+ end: 3,
+ start: 1,
+ });
+ });
+
+ it("on ArrowDown, traverse down with no results", () => {
+ const { wrapper, props } = generateModal(
+ {
+ enabled: true,
+ query: "test",
+ searchType: "goto",
+ },
+ "shallow"
+ );
+ const event = {
+ preventDefault: jest.fn(),
+ key: "ArrowDown",
+ };
+ wrapper.setState(() => ({
+ results: null,
+ selectedIndex: 1,
+ }));
+ wrapper.find("Connect(SearchInput)").simulate("keydown", event);
+ expect(event.preventDefault).toHaveBeenCalled();
+ expect(wrapper.state().selectedIndex).toEqual(0);
+ expect(props.selectSpecificLocation).not.toHaveBeenCalledWith();
+ expect(props.highlightLineRange).not.toHaveBeenCalled();
+ });
+
+ it("on ArrowUp, traverse results up to function with no location", () => {
+ const sourceId = "sourceId";
+ const { wrapper, props } = generateModal(
+ {
+ enabled: true,
+ query: "test",
+ searchType: "functions",
+ selectedLocation: { source: { id: sourceId } },
+ selectedContentLoaded: true,
+ },
+ "shallow"
+ );
+ const event = {
+ preventDefault: jest.fn(),
+ key: "ArrowUp",
+ };
+ wrapper.setState(() => ({
+ results: [{ id: "0", location: null }, { id: "1" }, { id: "2" }],
+ selectedIndex: 1,
+ }));
+ wrapper.find("Connect(SearchInput)").simulate("keydown", event);
+ expect(event.preventDefault).toHaveBeenCalled();
+ expect(wrapper.state().selectedIndex).toEqual(0);
+ expect(props.highlightLineRange).not.toHaveBeenCalled();
+ expect(props.clearHighlightLineRange).toHaveBeenCalled();
+ });
+
+ it(
+ "on ArrowDown, traverse down results, without " +
+ "taking action if no selectedSource",
+ () => {
+ const { wrapper, props } = generateModal(
+ {
+ enabled: true,
+ query: "test",
+ searchType: "variables",
+ selectedLocation: null,
+ selectedContentLoaded: true,
+ },
+ "shallow"
+ );
+ const event = {
+ preventDefault: jest.fn(),
+ key: "ArrowDown",
+ };
+ const location = {
+ sourceId: "sourceId",
+ start: {
+ line: 7,
+ },
+ };
+ wrapper.setState(() => ({
+ results: [{ id: "0", location }, { id: "1" }, { id: "2" }],
+ selectedIndex: 1,
+ }));
+ wrapper.find("Connect(SearchInput)").simulate("keydown", event);
+ expect(event.preventDefault).toHaveBeenCalled();
+ expect(wrapper.state().selectedIndex).toEqual(2);
+ expect(props.selectSpecificLocation).not.toHaveBeenCalled();
+ expect(props.highlightLineRange).not.toHaveBeenCalled();
+ }
+ );
+
+ it(
+ "on ArrowUp, traverse up results, without taking action if " +
+ "the query is not for variables or functions",
+ () => {
+ const sourceId = "sourceId";
+ const { wrapper, props } = generateModal(
+ {
+ enabled: true,
+ query: "test",
+ searchType: "other",
+ selectedLocation: { source: { id: sourceId } },
+ selectedContentLoaded: true,
+ },
+ "shallow"
+ );
+ const event = {
+ preventDefault: jest.fn(),
+ key: "ArrowUp",
+ };
+ const location = {
+ sourceId: "sourceId",
+ start: {
+ line: 7,
+ },
+ };
+ wrapper.setState(() => ({
+ results: [{ id: "0", location }, { id: "1" }, { id: "2" }],
+ selectedIndex: 1,
+ }));
+ wrapper.find("Connect(SearchInput)").simulate("keydown", event);
+ expect(event.preventDefault).toHaveBeenCalled();
+ expect(wrapper.state().selectedIndex).toEqual(0);
+ expect(props.selectSpecificLocation).not.toHaveBeenCalled();
+ expect(props.highlightLineRange).not.toHaveBeenCalled();
+ }
+ );
+ });
+
+ describe("showErrorEmoji", () => {
+ it("true when no count + query", () => {
+ const { wrapper } = generateModal(
+ {
+ enabled: true,
+ query: "test",
+ searchType: "other",
+ },
+ "mount"
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("false when count + query", () => {
+ const { wrapper } = generateModal(
+ {
+ enabled: true,
+ query: "dasdasdas",
+ },
+ "mount"
+ );
+ wrapper.setState(() => ({
+ results: [1, 2],
+ }));
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("false when no query", () => {
+ const { wrapper } = generateModal(
+ {
+ enabled: true,
+ query: "",
+ searchType: "other",
+ },
+ "mount"
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("false when goto numeric ':2222'", () => {
+ const { wrapper } = generateModal(
+ {
+ enabled: true,
+ query: ":2222",
+ searchType: "goto",
+ },
+ "mount"
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it("true when goto not numeric ':22k22'", () => {
+ const { wrapper } = generateModal(
+ {
+ enabled: true,
+ query: ":22k22",
+ searchType: "goto",
+ },
+ "mount"
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/components/test/WelcomeBox.spec.js b/devtools/client/debugger/src/components/test/WelcomeBox.spec.js
new file mode 100644
index 0000000000..5599c416fe
--- /dev/null
+++ b/devtools/client/debugger/src/components/test/WelcomeBox.spec.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 React from "devtools/client/shared/vendor/react";
+import { shallow } from "enzyme";
+
+import { WelcomeBox } from "../WelcomeBox";
+
+function render(overrides = {}) {
+ const props = {
+ horizontal: false,
+ togglePaneCollapse: jest.fn(),
+ endPanelCollapsed: false,
+ setActiveSearch: jest.fn(),
+ openQuickOpen: jest.fn(),
+ toggleShortcutsModal: jest.fn(),
+ setPrimaryPaneTab: jest.fn(),
+ ...overrides,
+ };
+ const component = shallow(React.createElement(WelcomeBox, props));
+ return { component, props };
+}
+
+describe("WelomeBox", () => {
+ it("renders with default values", () => {
+ const { component } = render();
+ expect(component).toMatchSnapshot();
+ });
+
+ it("doesn't render toggle button in horizontal mode", () => {
+ const { component } = render({
+ horizontal: true,
+ });
+ expect(component.find("PaneToggleButton")).toHaveLength(0);
+ });
+
+ it("calls correct function on searchSources click", () => {
+ const { component, props } = render();
+
+ component.find(".welcomebox__searchSources").simulate("click");
+ expect(props.openQuickOpen).toHaveBeenCalled();
+ });
+
+ it("calls correct function on searchProject click", () => {
+ const { component, props } = render();
+
+ component.find(".welcomebox__searchProject").simulate("click");
+ expect(props.setActiveSearch).toHaveBeenCalled();
+ });
+
+ it("calls correct function on allShotcuts click", () => {
+ const { component, props } = render();
+
+ component.find(".welcomebox__allShortcuts").simulate("click");
+ expect(props.toggleShortcutsModal).toHaveBeenCalled();
+ });
+});
diff --git a/devtools/client/debugger/src/components/test/WhyPaused.spec.js b/devtools/client/debugger/src/components/test/WhyPaused.spec.js
new file mode 100644
index 0000000000..5466976dfb
--- /dev/null
+++ b/devtools/client/debugger/src/components/test/WhyPaused.spec.js
@@ -0,0 +1,61 @@
+/* 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 React from "devtools/client/shared/vendor/react";
+import { shallow } from "enzyme";
+import WhyPaused from "../SecondaryPanes/WhyPaused.js";
+
+function render(why, delay) {
+ const props = { why, delay };
+ const component = shallow(
+ React.createElement(WhyPaused.WrappedComponent, props)
+ );
+
+ return { component, props };
+}
+
+describe("WhyPaused", () => {
+ it("should pause reason with message", () => {
+ const why = {
+ type: "breakpoint",
+ message: "bla is hit",
+ };
+ const { component } = render(why);
+ expect(component).toMatchSnapshot();
+ });
+
+ it("should show pause reason with exception details", () => {
+ const why = {
+ type: "exception",
+ exception: {
+ class: "ReferenceError",
+ isError: true,
+ preview: {
+ name: "ReferenceError",
+ message: "o is not defined",
+ },
+ },
+ };
+
+ const { component } = render(why);
+ expect(component).toMatchSnapshot();
+ });
+
+ it("should show pause reason with exception string", () => {
+ const why = {
+ type: "exception",
+ exception: "Not Available",
+ };
+
+ const { component } = render(why);
+ expect(component).toMatchSnapshot();
+ });
+
+ it("should show an empty div when there is no pause reason", () => {
+ const why = undefined;
+
+ const { component } = render(why);
+ expect(component).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/debugger/src/components/test/__snapshots__/QuickOpenModal.spec.js.snap b/devtools/client/debugger/src/components/test/__snapshots__/QuickOpenModal.spec.js.snap
new file mode 100644
index 0000000000..d58d2d58a0
--- /dev/null
+++ b/devtools/client/debugger/src/components/test/__snapshots__/QuickOpenModal.spec.js.snap
@@ -0,0 +1,1777 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`QuickOpenModal Basic render with mount & searchType = functions 1`] = `
+<Provider
+ store={
+ Object {
+ "clearActions": [Function],
+ "dispatch": [Function],
+ "getActions": [Function],
+ "getState": [Function],
+ "replaceReducer": [Function],
+ "subscribe": [Function],
+ }
+ }
+>
+ <QuickOpenModal
+ blackBoxRanges={Object {}}
+ clearHighlightLineRange={[MockFunction]}
+ closeQuickOpen={[MockFunction]}
+ displayedSources={Array []}
+ enabled={true}
+ getFunctionSymbols={
+ [MockFunction] {
+ "calls": Array [
+ Array [
+ Object {
+ "source": Object {
+ "id": "foo",
+ },
+ },
+ 100,
+ ],
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": Array [],
+ },
+ ],
+ }
+ }
+ highlightLineRange={[MockFunction]}
+ isOriginal={false}
+ openedTabUrls={Array []}
+ query="@"
+ searchType="functions"
+ selectSpecificLocation={[MockFunction]}
+ selectedLocation={
+ Object {
+ "source": Object {
+ "id": "foo",
+ },
+ }
+ }
+ setQuickOpenQuery={[MockFunction]}
+ shortcutsModalEnabled={false}
+ thread="FakeThread"
+ toggleShortcutsModal={[MockFunction]}
+ >
+ <Modal
+ handleClose={[Function]}
+ >
+ <div
+ className="modal-wrapper"
+ onClick={[Function]}
+ >
+ <div
+ className="modal"
+ onClick={[Function]}
+ >
+ <Connect(SearchInput)
+ count={0}
+ expanded={false}
+ handleClose={[Function]}
+ hasPrefix={true}
+ isLoading={false}
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ query="@"
+ searchKey="quickopen-search"
+ selectedItemId=""
+ showClose={false}
+ showErrorEmoji={true}
+ showExcludePatterns={false}
+ showSearchModifiers={false}
+ summaryMsg="Loading…"
+ >
+ <SearchInput
+ count={0}
+ expanded={false}
+ handleClose={[Function]}
+ hasPrefix={true}
+ isLoading={false}
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ query="@"
+ searchKey="quickopen-search"
+ searchOptions={
+ Object {
+ "caseSensitive": false,
+ "excludePatterns": "",
+ "regexMatch": false,
+ "wholeWord": false,
+ }
+ }
+ selectedItemId=""
+ setSearchOptions={[Function]}
+ showClose={false}
+ showErrorEmoji={true}
+ showExcludePatterns={false}
+ showSearchModifiers={false}
+ size=""
+ summaryMsg="Loading…"
+ >
+ <div
+ className="search-outline"
+ >
+ <div
+ aria-expanded={false}
+ aria-haspopup="listbox"
+ aria-owns="result-list"
+ className="search-field"
+ role="combobox"
+ >
+ <AccessibleImage
+ className="search"
+ >
+ <span
+ className="img search"
+ />
+ </AccessibleImage>
+ <input
+ aria-activedescendant=""
+ aria-autocomplete="list"
+ aria-controls="result-list"
+ className="empty"
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ spellCheck={false}
+ value="@"
+ />
+ <div
+ className="search-field-summary"
+ >
+ Loading…
+ </div>
+ <div
+ className="search-buttons-bar"
+ />
+ </div>
+ </div>
+ </SearchInput>
+ </Connect(SearchInput)>
+ </div>
+ </div>
+ </Modal>
+ </QuickOpenModal>
+</Provider>
+`;
+
+exports[`QuickOpenModal Basic render with mount & searchType = variables 1`] = `
+<Provider
+ store={
+ Object {
+ "clearActions": [Function],
+ "dispatch": [Function],
+ "getActions": [Function],
+ "getState": [Function],
+ "replaceReducer": [Function],
+ "subscribe": [Function],
+ }
+ }
+>
+ <QuickOpenModal
+ blackBoxRanges={Object {}}
+ clearHighlightLineRange={[MockFunction]}
+ closeQuickOpen={[MockFunction]}
+ displayedSources={Array []}
+ enabled={true}
+ getFunctionSymbols={[MockFunction]}
+ highlightLineRange={[MockFunction]}
+ isOriginal={false}
+ openedTabUrls={Array []}
+ query="#"
+ searchType="variables"
+ selectSpecificLocation={[MockFunction]}
+ selectedLocation={
+ Object {
+ "source": Object {
+ "id": "foo",
+ },
+ }
+ }
+ setQuickOpenQuery={[MockFunction]}
+ shortcutsModalEnabled={false}
+ thread="FakeThread"
+ toggleShortcutsModal={[MockFunction]}
+ >
+ <Modal
+ handleClose={[Function]}
+ >
+ <div
+ className="modal-wrapper"
+ onClick={[Function]}
+ >
+ <div
+ className="modal"
+ onClick={[Function]}
+ >
+ <Connect(SearchInput)
+ count={0}
+ expanded={false}
+ handleClose={[Function]}
+ hasPrefix={true}
+ isLoading={false}
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ query="#"
+ searchKey="quickopen-search"
+ selectedItemId=""
+ showClose={false}
+ showErrorEmoji={true}
+ showExcludePatterns={false}
+ showSearchModifiers={false}
+ summaryMsg=""
+ >
+ <SearchInput
+ count={0}
+ expanded={false}
+ handleClose={[Function]}
+ hasPrefix={true}
+ isLoading={false}
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ query="#"
+ searchKey="quickopen-search"
+ searchOptions={
+ Object {
+ "caseSensitive": false,
+ "excludePatterns": "",
+ "regexMatch": false,
+ "wholeWord": false,
+ }
+ }
+ selectedItemId=""
+ setSearchOptions={[Function]}
+ showClose={false}
+ showErrorEmoji={true}
+ showExcludePatterns={false}
+ showSearchModifiers={false}
+ size=""
+ summaryMsg=""
+ >
+ <div
+ className="search-outline"
+ >
+ <div
+ aria-expanded={false}
+ aria-haspopup="listbox"
+ aria-owns="result-list"
+ className="search-field"
+ role="combobox"
+ >
+ <AccessibleImage
+ className="search"
+ >
+ <span
+ className="img search"
+ />
+ </AccessibleImage>
+ <input
+ aria-activedescendant=""
+ aria-autocomplete="list"
+ aria-controls="result-list"
+ className="empty"
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ spellCheck={false}
+ value="#"
+ />
+ <div
+ className="search-buttons-bar"
+ />
+ </div>
+ </div>
+ </SearchInput>
+ </Connect(SearchInput)>
+ <ResultList
+ expanded={false}
+ items={Array []}
+ key="results"
+ role="listbox"
+ selectItem={[Function]}
+ selected={0}
+ size="small"
+ >
+ <ul
+ aria-live="polite"
+ className="result-list small"
+ id="result-list"
+ role="listbox"
+ />
+ </ResultList>
+ </div>
+ </div>
+ </Modal>
+ </QuickOpenModal>
+</Provider>
+`;
+
+exports[`QuickOpenModal Basic render with mount 1`] = `
+<Provider
+ store={
+ Object {
+ "clearActions": [Function],
+ "dispatch": [Function],
+ "getActions": [Function],
+ "getState": [Function],
+ "replaceReducer": [Function],
+ "subscribe": [Function],
+ }
+ }
+>
+ <QuickOpenModal
+ blackBoxRanges={Object {}}
+ clearHighlightLineRange={[MockFunction]}
+ closeQuickOpen={[MockFunction]}
+ displayedSources={Array []}
+ enabled={true}
+ getFunctionSymbols={[MockFunction]}
+ highlightLineRange={[MockFunction]}
+ isOriginal={false}
+ openedTabUrls={Array []}
+ query=""
+ searchType="sources"
+ selectSpecificLocation={[MockFunction]}
+ selectedLocation={
+ Object {
+ "source": Object {
+ "id": "foo",
+ },
+ }
+ }
+ setQuickOpenQuery={[MockFunction]}
+ shortcutsModalEnabled={false}
+ thread="FakeThread"
+ toggleShortcutsModal={[MockFunction]}
+ >
+ <Modal
+ handleClose={[Function]}
+ >
+ <div
+ className="modal-wrapper"
+ onClick={[Function]}
+ >
+ <div
+ className="modal"
+ onClick={[Function]}
+ >
+ <Connect(SearchInput)
+ count={0}
+ expanded={false}
+ handleClose={[Function]}
+ hasPrefix={true}
+ isLoading={false}
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ query=""
+ searchKey="quickopen-search"
+ selectedItemId=""
+ showClose={false}
+ showErrorEmoji={false}
+ showExcludePatterns={false}
+ showSearchModifiers={false}
+ size="big"
+ summaryMsg=""
+ >
+ <SearchInput
+ count={0}
+ expanded={false}
+ handleClose={[Function]}
+ hasPrefix={true}
+ isLoading={false}
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ query=""
+ searchKey="quickopen-search"
+ searchOptions={
+ Object {
+ "caseSensitive": false,
+ "excludePatterns": "",
+ "regexMatch": false,
+ "wholeWord": false,
+ }
+ }
+ selectedItemId=""
+ setSearchOptions={[Function]}
+ showClose={false}
+ showErrorEmoji={false}
+ showExcludePatterns={false}
+ showSearchModifiers={false}
+ size="big"
+ summaryMsg=""
+ >
+ <div
+ className="search-outline"
+ >
+ <div
+ aria-expanded={false}
+ aria-haspopup="listbox"
+ aria-owns="result-list"
+ className="search-field big"
+ role="combobox"
+ >
+ <AccessibleImage
+ className="search"
+ >
+ <span
+ className="img search"
+ />
+ </AccessibleImage>
+ <input
+ aria-activedescendant=""
+ aria-autocomplete="list"
+ aria-controls="result-list"
+ className=""
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ spellCheck={false}
+ value=""
+ />
+ <div
+ className="search-buttons-bar"
+ />
+ </div>
+ </div>
+ </SearchInput>
+ </Connect(SearchInput)>
+ <ResultList
+ expanded={false}
+ items={Array []}
+ key="results"
+ role="listbox"
+ selectItem={[Function]}
+ selected={0}
+ size="big"
+ >
+ <ul
+ aria-live="polite"
+ className="result-list big"
+ id="result-list"
+ role="listbox"
+ />
+ </ResultList>
+ </div>
+ </div>
+ </Modal>
+ </QuickOpenModal>
+</Provider>
+`;
+
+exports[`QuickOpenModal Doesn't render when disabled 1`] = `
+<Modal
+ handleClose={[Function]}
+>
+ <Connect(SearchInput)
+ count={0}
+ expanded={false}
+ handleClose={[Function]}
+ hasPrefix={true}
+ isLoading={false}
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ query=""
+ searchKey="quickopen-search"
+ selectedItemId=""
+ showClose={false}
+ showErrorEmoji={false}
+ showExcludePatterns={false}
+ showSearchModifiers={false}
+ size="big"
+ summaryMsg=""
+ />
+ <ResultList
+ expanded={false}
+ items={Array []}
+ key="results"
+ role="listbox"
+ selectItem={[Function]}
+ selected={0}
+ size="big"
+ />
+</Modal>
+`;
+
+exports[`QuickOpenModal Renders when enabled 1`] = `
+<Modal
+ handleClose={[Function]}
+>
+ <Connect(SearchInput)
+ count={0}
+ expanded={false}
+ handleClose={[Function]}
+ hasPrefix={true}
+ isLoading={false}
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ query=""
+ searchKey="quickopen-search"
+ selectedItemId=""
+ showClose={false}
+ showErrorEmoji={false}
+ showExcludePatterns={false}
+ showSearchModifiers={false}
+ size="big"
+ summaryMsg=""
+ />
+ <ResultList
+ expanded={false}
+ items={Array []}
+ key="results"
+ role="listbox"
+ selectItem={[Function]}
+ selected={0}
+ size="big"
+ />
+</Modal>
+`;
+
+exports[`QuickOpenModal Simple goto search query = :abc & searchType = goto 1`] = `
+<QuickOpenModal
+ blackBoxRanges={Object {}}
+ clearHighlightLineRange={[MockFunction]}
+ closeQuickOpen={[MockFunction]}
+ displayedSources={Array []}
+ enabled={true}
+ getFunctionSymbols={[MockFunction]}
+ highlightLineRange={[MockFunction]}
+ isOriginal={false}
+ openedTabUrls={Array []}
+ query=":abc"
+ searchType="goto"
+ selectSpecificLocation={[MockFunction]}
+ selectedLocation={
+ Object {
+ "source": Object {
+ "id": "foo",
+ },
+ }
+ }
+ setQuickOpenQuery={[MockFunction]}
+ shortcutsModalEnabled={false}
+ thread="FakeThread"
+ toggleShortcutsModal={[MockFunction]}
+>
+ <Modal
+ handleClose={[Function]}
+ >
+ <div
+ className="modal-wrapper"
+ onClick={[Function]}
+ >
+ <div
+ className="modal"
+ onClick={[Function]}
+ >
+ <Connect(SearchInput)
+ count={0}
+ expanded={false}
+ handleClose={[Function]}
+ hasPrefix={true}
+ isLoading={false}
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ query=":abc"
+ searchKey="quickopen-search"
+ selectedItemId=""
+ showClose={false}
+ showErrorEmoji={true}
+ showExcludePatterns={false}
+ showSearchModifiers={false}
+ summaryMsg="Go to line"
+ >
+ <SearchInput
+ count={0}
+ expanded={false}
+ handleClose={[Function]}
+ hasPrefix={true}
+ isLoading={false}
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ query=":abc"
+ searchKey="quickopen-search"
+ searchOptions={
+ Object {
+ "caseSensitive": false,
+ "excludePatterns": "",
+ "regexMatch": false,
+ "wholeWord": false,
+ }
+ }
+ selectedItemId=""
+ setSearchOptions={[Function]}
+ showClose={false}
+ showErrorEmoji={true}
+ showExcludePatterns={false}
+ showSearchModifiers={false}
+ size=""
+ summaryMsg="Go to line"
+ >
+ <div
+ className="search-outline"
+ >
+ <div
+ aria-expanded={false}
+ aria-haspopup="listbox"
+ aria-owns="result-list"
+ className="search-field"
+ role="combobox"
+ >
+ <AccessibleImage
+ className="search"
+ >
+ <span
+ className="img search"
+ />
+ </AccessibleImage>
+ <input
+ aria-activedescendant=""
+ aria-autocomplete="list"
+ aria-controls="result-list"
+ className="empty"
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ spellCheck={false}
+ value=":abc"
+ />
+ <div
+ className="search-field-summary"
+ >
+ Go to line
+ </div>
+ <div
+ className="search-buttons-bar"
+ />
+ </div>
+ </div>
+ </SearchInput>
+ </Connect(SearchInput)>
+ </div>
+ </div>
+ </Modal>
+</QuickOpenModal>
+`;
+
+exports[`QuickOpenModal showErrorEmoji false when count + query 1`] = `
+<Provider
+ store={
+ Object {
+ "clearActions": [Function],
+ "dispatch": [Function],
+ "getActions": [Function],
+ "getState": [Function],
+ "replaceReducer": [Function],
+ "subscribe": [Function],
+ }
+ }
+>
+ <QuickOpenModal
+ blackBoxRanges={Object {}}
+ clearHighlightLineRange={[MockFunction]}
+ closeQuickOpen={[MockFunction]}
+ displayedSources={Array []}
+ enabled={true}
+ getFunctionSymbols={[MockFunction]}
+ highlightLineRange={[MockFunction]}
+ isOriginal={false}
+ openedTabUrls={Array []}
+ query="dasdasdas"
+ searchType="sources"
+ selectSpecificLocation={[MockFunction]}
+ selectedLocation={
+ Object {
+ "source": Object {
+ "id": "foo",
+ },
+ }
+ }
+ setQuickOpenQuery={[MockFunction]}
+ shortcutsModalEnabled={false}
+ thread="FakeThread"
+ toggleShortcutsModal={[MockFunction]}
+ >
+ <Modal
+ handleClose={[Function]}
+ >
+ <div
+ className="modal-wrapper"
+ onClick={[Function]}
+ >
+ <div
+ className="modal"
+ onClick={[Function]}
+ >
+ <Connect(SearchInput)
+ count={0}
+ expanded={false}
+ handleClose={[Function]}
+ hasPrefix={true}
+ isLoading={false}
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ query="dasdasdas"
+ searchKey="quickopen-search"
+ selectedItemId=""
+ showClose={false}
+ showErrorEmoji={true}
+ showExcludePatterns={false}
+ showSearchModifiers={false}
+ size="big"
+ summaryMsg=""
+ >
+ <SearchInput
+ count={0}
+ expanded={false}
+ handleClose={[Function]}
+ hasPrefix={true}
+ isLoading={false}
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ query="dasdasdas"
+ searchKey="quickopen-search"
+ searchOptions={
+ Object {
+ "caseSensitive": false,
+ "excludePatterns": "",
+ "regexMatch": false,
+ "wholeWord": false,
+ }
+ }
+ selectedItemId=""
+ setSearchOptions={[Function]}
+ showClose={false}
+ showErrorEmoji={true}
+ showExcludePatterns={false}
+ showSearchModifiers={false}
+ size="big"
+ summaryMsg=""
+ >
+ <div
+ className="search-outline"
+ >
+ <div
+ aria-expanded={false}
+ aria-haspopup="listbox"
+ aria-owns="result-list"
+ className="search-field big"
+ role="combobox"
+ >
+ <AccessibleImage
+ className="search"
+ >
+ <span
+ className="img search"
+ />
+ </AccessibleImage>
+ <input
+ aria-activedescendant=""
+ aria-autocomplete="list"
+ aria-controls="result-list"
+ className="empty"
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ spellCheck={false}
+ value="dasdasdas"
+ />
+ <div
+ className="search-buttons-bar"
+ />
+ </div>
+ </div>
+ </SearchInput>
+ </Connect(SearchInput)>
+ <ResultList
+ expanded={false}
+ items={Array []}
+ key="results"
+ role="listbox"
+ selectItem={[Function]}
+ selected={0}
+ size="big"
+ >
+ <ul
+ aria-live="polite"
+ className="result-list big"
+ id="result-list"
+ role="listbox"
+ />
+ </ResultList>
+ </div>
+ </div>
+ </Modal>
+ </QuickOpenModal>
+</Provider>
+`;
+
+exports[`QuickOpenModal showErrorEmoji false when goto numeric ':2222' 1`] = `
+<Provider
+ store={
+ Object {
+ "clearActions": [Function],
+ "dispatch": [Function],
+ "getActions": [Function],
+ "getState": [Function],
+ "replaceReducer": [Function],
+ "subscribe": [Function],
+ }
+ }
+>
+ <QuickOpenModal
+ blackBoxRanges={Object {}}
+ clearHighlightLineRange={[MockFunction]}
+ closeQuickOpen={[MockFunction]}
+ displayedSources={Array []}
+ enabled={true}
+ getFunctionSymbols={[MockFunction]}
+ highlightLineRange={[MockFunction]}
+ isOriginal={false}
+ openedTabUrls={Array []}
+ query=":2222"
+ searchType="goto"
+ selectSpecificLocation={[MockFunction]}
+ selectedLocation={
+ Object {
+ "source": Object {
+ "id": "foo",
+ },
+ }
+ }
+ setQuickOpenQuery={[MockFunction]}
+ shortcutsModalEnabled={false}
+ thread="FakeThread"
+ toggleShortcutsModal={[MockFunction]}
+ >
+ <Modal
+ handleClose={[Function]}
+ >
+ <div
+ className="modal-wrapper"
+ onClick={[Function]}
+ >
+ <div
+ className="modal"
+ onClick={[Function]}
+ >
+ <Connect(SearchInput)
+ count={0}
+ expanded={false}
+ handleClose={[Function]}
+ hasPrefix={true}
+ isLoading={false}
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ query=":2222"
+ searchKey="quickopen-search"
+ selectedItemId=""
+ showClose={false}
+ showErrorEmoji={false}
+ showExcludePatterns={false}
+ showSearchModifiers={false}
+ summaryMsg="Go to line"
+ >
+ <SearchInput
+ count={0}
+ expanded={false}
+ handleClose={[Function]}
+ hasPrefix={true}
+ isLoading={false}
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ query=":2222"
+ searchKey="quickopen-search"
+ searchOptions={
+ Object {
+ "caseSensitive": false,
+ "excludePatterns": "",
+ "regexMatch": false,
+ "wholeWord": false,
+ }
+ }
+ selectedItemId=""
+ setSearchOptions={[Function]}
+ showClose={false}
+ showErrorEmoji={false}
+ showExcludePatterns={false}
+ showSearchModifiers={false}
+ size=""
+ summaryMsg="Go to line"
+ >
+ <div
+ className="search-outline"
+ >
+ <div
+ aria-expanded={false}
+ aria-haspopup="listbox"
+ aria-owns="result-list"
+ className="search-field"
+ role="combobox"
+ >
+ <AccessibleImage
+ className="search"
+ >
+ <span
+ className="img search"
+ />
+ </AccessibleImage>
+ <input
+ aria-activedescendant=""
+ aria-autocomplete="list"
+ aria-controls="result-list"
+ className=""
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ spellCheck={false}
+ value=":2222"
+ />
+ <div
+ className="search-field-summary"
+ >
+ Go to line
+ </div>
+ <div
+ className="search-buttons-bar"
+ />
+ </div>
+ </div>
+ </SearchInput>
+ </Connect(SearchInput)>
+ </div>
+ </div>
+ </Modal>
+ </QuickOpenModal>
+</Provider>
+`;
+
+exports[`QuickOpenModal showErrorEmoji false when no query 1`] = `
+<Provider
+ store={
+ Object {
+ "clearActions": [Function],
+ "dispatch": [Function],
+ "getActions": [Function],
+ "getState": [Function],
+ "replaceReducer": [Function],
+ "subscribe": [Function],
+ }
+ }
+>
+ <QuickOpenModal
+ blackBoxRanges={Object {}}
+ clearHighlightLineRange={[MockFunction]}
+ closeQuickOpen={[MockFunction]}
+ displayedSources={Array []}
+ enabled={true}
+ getFunctionSymbols={[MockFunction]}
+ highlightLineRange={[MockFunction]}
+ isOriginal={false}
+ openedTabUrls={Array []}
+ query=""
+ searchType="other"
+ selectSpecificLocation={[MockFunction]}
+ selectedLocation={
+ Object {
+ "source": Object {
+ "id": "foo",
+ },
+ }
+ }
+ setQuickOpenQuery={[MockFunction]}
+ shortcutsModalEnabled={false}
+ thread="FakeThread"
+ toggleShortcutsModal={[MockFunction]}
+ >
+ <Modal
+ handleClose={[Function]}
+ >
+ <div
+ className="modal-wrapper"
+ onClick={[Function]}
+ >
+ <div
+ className="modal"
+ onClick={[Function]}
+ >
+ <Connect(SearchInput)
+ count={0}
+ expanded={false}
+ handleClose={[Function]}
+ hasPrefix={true}
+ isLoading={false}
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ query=""
+ searchKey="quickopen-search"
+ selectedItemId=""
+ showClose={false}
+ showErrorEmoji={false}
+ showExcludePatterns={false}
+ showSearchModifiers={false}
+ summaryMsg=""
+ >
+ <SearchInput
+ count={0}
+ expanded={false}
+ handleClose={[Function]}
+ hasPrefix={true}
+ isLoading={false}
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ query=""
+ searchKey="quickopen-search"
+ searchOptions={
+ Object {
+ "caseSensitive": false,
+ "excludePatterns": "",
+ "regexMatch": false,
+ "wholeWord": false,
+ }
+ }
+ selectedItemId=""
+ setSearchOptions={[Function]}
+ showClose={false}
+ showErrorEmoji={false}
+ showExcludePatterns={false}
+ showSearchModifiers={false}
+ size=""
+ summaryMsg=""
+ >
+ <div
+ className="search-outline"
+ >
+ <div
+ aria-expanded={false}
+ aria-haspopup="listbox"
+ aria-owns="result-list"
+ className="search-field"
+ role="combobox"
+ >
+ <AccessibleImage
+ className="search"
+ >
+ <span
+ className="img search"
+ />
+ </AccessibleImage>
+ <input
+ aria-activedescendant=""
+ aria-autocomplete="list"
+ aria-controls="result-list"
+ className=""
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ spellCheck={false}
+ value=""
+ />
+ <div
+ className="search-buttons-bar"
+ />
+ </div>
+ </div>
+ </SearchInput>
+ </Connect(SearchInput)>
+ <ResultList
+ expanded={false}
+ items={Array []}
+ key="results"
+ role="listbox"
+ selectItem={[Function]}
+ selected={0}
+ size="small"
+ >
+ <ul
+ aria-live="polite"
+ className="result-list small"
+ id="result-list"
+ role="listbox"
+ />
+ </ResultList>
+ </div>
+ </div>
+ </Modal>
+ </QuickOpenModal>
+</Provider>
+`;
+
+exports[`QuickOpenModal showErrorEmoji true when goto not numeric ':22k22' 1`] = `
+<Provider
+ store={
+ Object {
+ "clearActions": [Function],
+ "dispatch": [Function],
+ "getActions": [Function],
+ "getState": [Function],
+ "replaceReducer": [Function],
+ "subscribe": [Function],
+ }
+ }
+>
+ <QuickOpenModal
+ blackBoxRanges={Object {}}
+ clearHighlightLineRange={[MockFunction]}
+ closeQuickOpen={[MockFunction]}
+ displayedSources={Array []}
+ enabled={true}
+ getFunctionSymbols={[MockFunction]}
+ highlightLineRange={[MockFunction]}
+ isOriginal={false}
+ openedTabUrls={Array []}
+ query=":22k22"
+ searchType="goto"
+ selectSpecificLocation={[MockFunction]}
+ selectedLocation={
+ Object {
+ "source": Object {
+ "id": "foo",
+ },
+ }
+ }
+ setQuickOpenQuery={[MockFunction]}
+ shortcutsModalEnabled={false}
+ thread="FakeThread"
+ toggleShortcutsModal={[MockFunction]}
+ >
+ <Modal
+ handleClose={[Function]}
+ >
+ <div
+ className="modal-wrapper"
+ onClick={[Function]}
+ >
+ <div
+ className="modal"
+ onClick={[Function]}
+ >
+ <Connect(SearchInput)
+ count={0}
+ expanded={false}
+ handleClose={[Function]}
+ hasPrefix={true}
+ isLoading={false}
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ query=":22k22"
+ searchKey="quickopen-search"
+ selectedItemId=""
+ showClose={false}
+ showErrorEmoji={true}
+ showExcludePatterns={false}
+ showSearchModifiers={false}
+ summaryMsg="Go to line"
+ >
+ <SearchInput
+ count={0}
+ expanded={false}
+ handleClose={[Function]}
+ hasPrefix={true}
+ isLoading={false}
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ query=":22k22"
+ searchKey="quickopen-search"
+ searchOptions={
+ Object {
+ "caseSensitive": false,
+ "excludePatterns": "",
+ "regexMatch": false,
+ "wholeWord": false,
+ }
+ }
+ selectedItemId=""
+ setSearchOptions={[Function]}
+ showClose={false}
+ showErrorEmoji={true}
+ showExcludePatterns={false}
+ showSearchModifiers={false}
+ size=""
+ summaryMsg="Go to line"
+ >
+ <div
+ className="search-outline"
+ >
+ <div
+ aria-expanded={false}
+ aria-haspopup="listbox"
+ aria-owns="result-list"
+ className="search-field"
+ role="combobox"
+ >
+ <AccessibleImage
+ className="search"
+ >
+ <span
+ className="img search"
+ />
+ </AccessibleImage>
+ <input
+ aria-activedescendant=""
+ aria-autocomplete="list"
+ aria-controls="result-list"
+ className="empty"
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ spellCheck={false}
+ value=":22k22"
+ />
+ <div
+ className="search-field-summary"
+ >
+ Go to line
+ </div>
+ <div
+ className="search-buttons-bar"
+ />
+ </div>
+ </div>
+ </SearchInput>
+ </Connect(SearchInput)>
+ </div>
+ </div>
+ </Modal>
+ </QuickOpenModal>
+</Provider>
+`;
+
+exports[`QuickOpenModal showErrorEmoji true when no count + query 1`] = `
+<Provider
+ store={
+ Object {
+ "clearActions": [Function],
+ "dispatch": [Function],
+ "getActions": [Function],
+ "getState": [Function],
+ "replaceReducer": [Function],
+ "subscribe": [Function],
+ }
+ }
+>
+ <QuickOpenModal
+ blackBoxRanges={Object {}}
+ clearHighlightLineRange={[MockFunction]}
+ closeQuickOpen={[MockFunction]}
+ displayedSources={Array []}
+ enabled={true}
+ getFunctionSymbols={[MockFunction]}
+ highlightLineRange={[MockFunction]}
+ isOriginal={false}
+ openedTabUrls={Array []}
+ query="test"
+ searchType="other"
+ selectSpecificLocation={[MockFunction]}
+ selectedLocation={
+ Object {
+ "source": Object {
+ "id": "foo",
+ },
+ }
+ }
+ setQuickOpenQuery={[MockFunction]}
+ shortcutsModalEnabled={false}
+ thread="FakeThread"
+ toggleShortcutsModal={[MockFunction]}
+ >
+ <Modal
+ handleClose={[Function]}
+ >
+ <div
+ className="modal-wrapper"
+ onClick={[Function]}
+ >
+ <div
+ className="modal"
+ onClick={[Function]}
+ >
+ <Connect(SearchInput)
+ count={0}
+ expanded={false}
+ handleClose={[Function]}
+ hasPrefix={true}
+ isLoading={false}
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ query="test"
+ searchKey="quickopen-search"
+ selectedItemId=""
+ showClose={false}
+ showErrorEmoji={true}
+ showExcludePatterns={false}
+ showSearchModifiers={false}
+ summaryMsg=""
+ >
+ <SearchInput
+ count={0}
+ expanded={false}
+ handleClose={[Function]}
+ hasPrefix={true}
+ isLoading={false}
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ query="test"
+ searchKey="quickopen-search"
+ searchOptions={
+ Object {
+ "caseSensitive": false,
+ "excludePatterns": "",
+ "regexMatch": false,
+ "wholeWord": false,
+ }
+ }
+ selectedItemId=""
+ setSearchOptions={[Function]}
+ showClose={false}
+ showErrorEmoji={true}
+ showExcludePatterns={false}
+ showSearchModifiers={false}
+ size=""
+ summaryMsg=""
+ >
+ <div
+ className="search-outline"
+ >
+ <div
+ aria-expanded={false}
+ aria-haspopup="listbox"
+ aria-owns="result-list"
+ className="search-field"
+ role="combobox"
+ >
+ <AccessibleImage
+ className="search"
+ >
+ <span
+ className="img search"
+ />
+ </AccessibleImage>
+ <input
+ aria-activedescendant=""
+ aria-autocomplete="list"
+ aria-controls="result-list"
+ className="empty"
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ spellCheck={false}
+ value="test"
+ />
+ <div
+ className="search-buttons-bar"
+ />
+ </div>
+ </div>
+ </SearchInput>
+ </Connect(SearchInput)>
+ <ResultList
+ expanded={false}
+ items={Array []}
+ key="results"
+ role="listbox"
+ selectItem={[Function]}
+ selected={0}
+ size="small"
+ >
+ <ul
+ aria-live="polite"
+ className="result-list small"
+ id="result-list"
+ role="listbox"
+ />
+ </ResultList>
+ </div>
+ </div>
+ </Modal>
+ </QuickOpenModal>
+</Provider>
+`;
+
+exports[`QuickOpenModal shows loading loads with function type search 1`] = `
+<Modal
+ handleClose={[Function]}
+>
+ <Connect(SearchInput)
+ count={0}
+ expanded={false}
+ handleClose={[Function]}
+ hasPrefix={true}
+ isLoading={false}
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ query=""
+ searchKey="quickopen-search"
+ selectedItemId=""
+ showClose={false}
+ showErrorEmoji={false}
+ showExcludePatterns={false}
+ showSearchModifiers={false}
+ summaryMsg=""
+ />
+ <ResultList
+ expanded={false}
+ items={Array []}
+ key="results"
+ role="listbox"
+ selectItem={[Function]}
+ selected={0}
+ size="small"
+ />
+</Modal>
+`;
+
+exports[`QuickOpenModal updateResults on enable 1`] = `
+<Provider
+ store={
+ Object {
+ "clearActions": [Function],
+ "dispatch": [Function],
+ "getActions": [Function],
+ "getState": [Function],
+ "replaceReducer": [Function],
+ "subscribe": [Function],
+ }
+ }
+>
+ <QuickOpenModal
+ blackBoxRanges={Object {}}
+ clearHighlightLineRange={[MockFunction]}
+ closeQuickOpen={[MockFunction]}
+ displayedSources={Array []}
+ enabled={false}
+ getFunctionSymbols={[MockFunction]}
+ highlightLineRange={[MockFunction]}
+ isOriginal={false}
+ openedTabUrls={Array []}
+ query=""
+ searchType="sources"
+ selectSpecificLocation={[MockFunction]}
+ selectedLocation={
+ Object {
+ "source": Object {
+ "id": "foo",
+ },
+ }
+ }
+ setQuickOpenQuery={[MockFunction]}
+ shortcutsModalEnabled={false}
+ thread="FakeThread"
+ toggleShortcutsModal={[MockFunction]}
+ >
+ <Modal
+ handleClose={[Function]}
+ >
+ <div
+ className="modal-wrapper"
+ onClick={[Function]}
+ >
+ <div
+ className="modal"
+ onClick={[Function]}
+ >
+ <Connect(SearchInput)
+ count={0}
+ expanded={false}
+ handleClose={[Function]}
+ hasPrefix={true}
+ isLoading={false}
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ query=""
+ searchKey="quickopen-search"
+ selectedItemId=""
+ showClose={false}
+ showErrorEmoji={false}
+ showExcludePatterns={false}
+ showSearchModifiers={false}
+ size="big"
+ summaryMsg=""
+ >
+ <SearchInput
+ count={0}
+ expanded={false}
+ handleClose={[Function]}
+ hasPrefix={true}
+ isLoading={false}
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ query=""
+ searchKey="quickopen-search"
+ searchOptions={
+ Object {
+ "caseSensitive": false,
+ "excludePatterns": "",
+ "regexMatch": false,
+ "wholeWord": false,
+ }
+ }
+ selectedItemId=""
+ setSearchOptions={[Function]}
+ showClose={false}
+ showErrorEmoji={false}
+ showExcludePatterns={false}
+ showSearchModifiers={false}
+ size="big"
+ summaryMsg=""
+ >
+ <div
+ className="search-outline"
+ >
+ <div
+ aria-expanded={false}
+ aria-haspopup="listbox"
+ aria-owns="result-list"
+ className="search-field big"
+ role="combobox"
+ >
+ <AccessibleImage
+ className="search"
+ >
+ <span
+ className="img search"
+ />
+ </AccessibleImage>
+ <input
+ aria-activedescendant=""
+ aria-autocomplete="list"
+ aria-controls="result-list"
+ className=""
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ spellCheck={false}
+ value=""
+ />
+ <div
+ className="search-buttons-bar"
+ />
+ </div>
+ </div>
+ </SearchInput>
+ </Connect(SearchInput)>
+ <ResultList
+ expanded={false}
+ items={Array []}
+ key="results"
+ role="listbox"
+ selectItem={[Function]}
+ selected={0}
+ size="big"
+ >
+ <ul
+ aria-live="polite"
+ className="result-list big"
+ id="result-list"
+ role="listbox"
+ />
+ </ResultList>
+ </div>
+ </div>
+ </Modal>
+ </QuickOpenModal>
+</Provider>
+`;
+
+exports[`QuickOpenModal updateResults on enable 2`] = `
+<Provider
+ enabled={true}
+ store={
+ Object {
+ "clearActions": [Function],
+ "dispatch": [Function],
+ "getActions": [Function],
+ "getState": [Function],
+ "replaceReducer": [Function],
+ "subscribe": [Function],
+ }
+ }
+>
+ <QuickOpenModal
+ blackBoxRanges={Object {}}
+ clearHighlightLineRange={[MockFunction]}
+ closeQuickOpen={[MockFunction]}
+ displayedSources={Array []}
+ enabled={false}
+ getFunctionSymbols={[MockFunction]}
+ highlightLineRange={[MockFunction]}
+ isOriginal={false}
+ openedTabUrls={Array []}
+ query=""
+ searchType="sources"
+ selectSpecificLocation={[MockFunction]}
+ selectedLocation={
+ Object {
+ "source": Object {
+ "id": "foo",
+ },
+ }
+ }
+ setQuickOpenQuery={[MockFunction]}
+ shortcutsModalEnabled={false}
+ thread="FakeThread"
+ toggleShortcutsModal={[MockFunction]}
+ >
+ <Modal
+ handleClose={[Function]}
+ >
+ <div
+ className="modal-wrapper"
+ onClick={[Function]}
+ >
+ <div
+ className="modal"
+ onClick={[Function]}
+ >
+ <Connect(SearchInput)
+ count={0}
+ expanded={false}
+ handleClose={[Function]}
+ hasPrefix={true}
+ isLoading={false}
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ query=""
+ searchKey="quickopen-search"
+ selectedItemId=""
+ showClose={false}
+ showErrorEmoji={false}
+ showExcludePatterns={false}
+ showSearchModifiers={false}
+ size="big"
+ summaryMsg=""
+ >
+ <SearchInput
+ count={0}
+ expanded={false}
+ handleClose={[Function]}
+ hasPrefix={true}
+ isLoading={false}
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ query=""
+ searchKey="quickopen-search"
+ searchOptions={
+ Object {
+ "caseSensitive": false,
+ "excludePatterns": "",
+ "regexMatch": false,
+ "wholeWord": false,
+ }
+ }
+ selectedItemId=""
+ setSearchOptions={[Function]}
+ showClose={false}
+ showErrorEmoji={false}
+ showExcludePatterns={false}
+ showSearchModifiers={false}
+ size="big"
+ summaryMsg=""
+ >
+ <div
+ className="search-outline"
+ >
+ <div
+ aria-expanded={false}
+ aria-haspopup="listbox"
+ aria-owns="result-list"
+ className="search-field big"
+ role="combobox"
+ >
+ <AccessibleImage
+ className="search"
+ >
+ <span
+ className="img search"
+ />
+ </AccessibleImage>
+ <input
+ aria-activedescendant=""
+ aria-autocomplete="list"
+ aria-controls="result-list"
+ className=""
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ onKeyDown={[Function]}
+ placeholder="Go to file…"
+ spellCheck={false}
+ value=""
+ />
+ <div
+ className="search-buttons-bar"
+ />
+ </div>
+ </div>
+ </SearchInput>
+ </Connect(SearchInput)>
+ <ResultList
+ expanded={false}
+ items={Array []}
+ key="results"
+ role="listbox"
+ selectItem={[Function]}
+ selected={0}
+ size="big"
+ >
+ <ul
+ aria-live="polite"
+ className="result-list big"
+ id="result-list"
+ role="listbox"
+ />
+ </ResultList>
+ </div>
+ </div>
+ </Modal>
+ </QuickOpenModal>
+</Provider>
+`;
diff --git a/devtools/client/debugger/src/components/test/__snapshots__/WelcomeBox.spec.js.snap b/devtools/client/debugger/src/components/test/__snapshots__/WelcomeBox.spec.js.snap
new file mode 100644
index 0000000000..9828e88ef4
--- /dev/null
+++ b/devtools/client/debugger/src/components/test/__snapshots__/WelcomeBox.spec.js.snap
@@ -0,0 +1,67 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`WelomeBox renders with default values 1`] = `
+<div
+ className="welcomebox"
+>
+ <div
+ className="alignlabel"
+ >
+ <div
+ className="shortcutFunction"
+ >
+ <p
+ className="welcomebox__searchSources"
+ onClick={[Function]}
+ role="button"
+ tabIndex="0"
+ >
+ <span
+ className="shortcutKey"
+ >
+ Ctrl+P
+ </span>
+ <span
+ className="shortcutLabel"
+ >
+ Go to file
+ </span>
+ </p>
+ <p
+ className="welcomebox__searchProject"
+ onClick={[Function]}
+ role="button"
+ tabIndex="0"
+ >
+ <span
+ className="shortcutKey"
+ >
+ Ctrl+Shift+F
+ </span>
+ <span
+ className="shortcutLabel"
+ >
+ Find in files
+ </span>
+ </p>
+ <p
+ className="welcomebox__allShortcuts"
+ onClick={[Function]}
+ role="button"
+ tabIndex="0"
+ >
+ <span
+ className="shortcutKey"
+ >
+ Ctrl+/
+ </span>
+ <span
+ className="shortcutLabel"
+ >
+ Show all shortcuts
+ </span>
+ </p>
+ </div>
+ </div>
+</div>
+`;
diff --git a/devtools/client/debugger/src/components/test/__snapshots__/WhyPaused.spec.js.snap b/devtools/client/debugger/src/components/test/__snapshots__/WhyPaused.spec.js.snap
new file mode 100644
index 0000000000..0762a0b69d
--- /dev/null
+++ b/devtools/client/debugger/src/components/test/__snapshots__/WhyPaused.spec.js.snap
@@ -0,0 +1,103 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`WhyPaused should pause reason with message 1`] = `
+<LocalizationProvider
+ bundles={Array []}
+>
+ <div
+ className="pane why-paused"
+ >
+ <div>
+ <div
+ className="info icon"
+ >
+ <AccessibleImage
+ className="info"
+ />
+ </div>
+ <div
+ className="pause reason"
+ >
+ <Localized
+ id="whypaused-breakpoint"
+ />
+ <div
+ className="message"
+ >
+ bla is hit
+ </div>
+ </div>
+ </div>
+ </div>
+</LocalizationProvider>
+`;
+
+exports[`WhyPaused should show an empty div when there is no pause reason 1`] = `
+<div
+ className=""
+/>
+`;
+
+exports[`WhyPaused should show pause reason with exception details 1`] = `
+<LocalizationProvider
+ bundles={Array []}
+>
+ <div
+ className="pane why-paused"
+ >
+ <div>
+ <div
+ className="info icon"
+ >
+ <AccessibleImage
+ className="info"
+ />
+ </div>
+ <div
+ className="pause reason"
+ >
+ <Localized
+ id="whypaused-exception"
+ />
+ <div
+ className="message warning"
+ >
+ ReferenceError: o is not defined
+ </div>
+ </div>
+ </div>
+ </div>
+</LocalizationProvider>
+`;
+
+exports[`WhyPaused should show pause reason with exception string 1`] = `
+<LocalizationProvider
+ bundles={Array []}
+>
+ <div
+ className="pane why-paused"
+ >
+ <div>
+ <div
+ className="info icon"
+ >
+ <AccessibleImage
+ className="info"
+ />
+ </div>
+ <div
+ className="pause reason"
+ >
+ <Localized
+ id="whypaused-exception"
+ />
+ <div
+ className="message warning"
+ >
+ Not Available
+ </div>
+ </div>
+ </div>
+ </div>
+</LocalizationProvider>
+`;
diff --git a/devtools/client/debugger/src/components/variables.css b/devtools/client/debugger/src/components/variables.css
new file mode 100644
index 0000000000..628c590714
--- /dev/null
+++ b/devtools/client/debugger/src/components/variables.css
@@ -0,0 +1,45 @@
+/* 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/>. */
+
+:root {
+ /* header height is 28px + 1px for its border */
+ --editor-header-height: 29px;
+ /* footer height is 24px + 1px for its border */
+ --editor-footer-height: 25px;
+ /* searchbar height is 24px + 1px for its top border */
+ --editor-searchbar-height: 25px;
+ /* Remove once https://bugzilla.mozilla.org/show_bug.cgi?id=1520440 lands */
+ --theme-code-line-height: calc(15 / 11);
+ /* Background and text colors and opacity for skipped breakpoint panes */
+ --skip-pausing-background-color: var(--theme-toolbar-hover);
+ --skip-pausing-opacity: 0.6;
+ --skip-pausing-color: var(--theme-body-color);
+}
+
+:root.theme-light,
+:root .theme-light {
+ --search-overlays-semitransparent: rgba(221, 225, 228, 0.66);
+ --popup-shadow-color: #d0d0d0;
+ --theme-inline-preview-background: rgba(192, 105, 255, 0.05);
+ --theme-inline-preview-border-color: #ebd1ff;
+ --theme-inline-preview-label-color: #6300a6;
+ --theme-inline-preview-label-background: rgb(244, 230, 255);
+}
+
+:root.theme-dark,
+:root .theme-dark {
+ --search-overlays-semitransparent: rgba(42, 46, 56, 0.66);
+ --popup-shadow-color: #5c667b;
+ --theme-inline-preview-background: rgba(192, 105, 255, 0.05);
+ --theme-inline-preview-border-color: #47326c;
+ --theme-inline-preview-label-color: #dfccff;
+ --theme-inline-preview-label-background: #3f2e5f;
+}
+
+/* Animations */
+
+@keyframes spin {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
diff --git a/devtools/client/debugger/src/constants.js b/devtools/client/debugger/src/constants.js
new file mode 100644
index 0000000000..67b6ca5362
--- /dev/null
+++ b/devtools/client/debugger/src/constants.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 const searchKeys = {
+ PROJECT_SEARCH: "project-search",
+ FILE_SEARCH: "file-search",
+ QUICKOPEN_SEARCH: "quickopen-search",
+};
+
+export const primaryPaneTabs = {
+ SOURCES: "sources",
+ OUTLINE: "outline",
+ PROJECT_SEARCH: "project",
+};
diff --git a/devtools/client/debugger/src/context-menu/menu.js b/devtools/client/debugger/src/context-menu/menu.js
new file mode 100644
index 0000000000..3478c9071c
--- /dev/null
+++ b/devtools/client/debugger/src/context-menu/menu.js
@@ -0,0 +1,43 @@
+/* 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/>. */
+
+const Menu = require("resource://devtools/client/framework/menu.js");
+const MenuItem = require("resource://devtools/client/framework/menu-item.js");
+
+export function showMenu(evt, items) {
+ if (items.length === 0) {
+ return;
+ }
+
+ const menu = new Menu();
+ items
+ .filter(item => item.visible === undefined || item.visible === true)
+ .forEach(item => {
+ const menuItem = new MenuItem(item);
+ menuItem.submenu = createSubMenu(item.submenu);
+ menu.append(menuItem);
+ });
+
+ menu.popup(evt.screenX, evt.screenY, window.parent.document);
+}
+
+function createSubMenu(subItems) {
+ if (subItems) {
+ const subMenu = new Menu();
+ subItems.forEach(subItem => {
+ subMenu.append(new MenuItem(subItem));
+ });
+ return subMenu;
+ }
+ return null;
+}
+
+export function buildMenu(items) {
+ return items
+ .map(itm => {
+ const hide = typeof itm.hidden === "function" ? itm.hidden() : itm.hidden;
+ return hide ? null : itm.item;
+ })
+ .filter(itm => itm !== null);
+}
diff --git a/devtools/client/debugger/src/context-menu/moz.build b/devtools/client/debugger/src/context-menu/moz.build
new file mode 100644
index 0000000000..48089353f1
--- /dev/null
+++ b/devtools/client/debugger/src/context-menu/moz.build
@@ -0,0 +1,8 @@
+# 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/.
+
+CompiledModules(
+ "menu.js",
+)
diff --git a/devtools/client/debugger/src/debugger.css b/devtools/client/debugger/src/debugger.css
new file mode 100644
index 0000000000..0e7f80d694
--- /dev/null
+++ b/devtools/client/debugger/src/debugger.css
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* Shared imports */
+@import url("chrome://devtools/content/shared/components/AppErrorBoundary.css");
+@import url("chrome://devtools/content/shared/components/SearchModifiers.css");
+
+/* Devtools imports */
+@import url("chrome://devtools/content/debugger/src/components/variables.css");
+@import url("chrome://devtools/content/debugger/src/components/App.css");
+@import url("chrome://devtools/content/debugger/src/components/shared/AccessibleImage.css");
+@import url("chrome://devtools/content/debugger/src/components/shared/Accordion.css");
+@import url("chrome://devtools/content/debugger/src/components/shared/Badge.css");
+@import url("chrome://devtools/content/debugger/src/components/shared/BracketArrow.css");
+@import url("chrome://devtools/content/debugger/src/components/shared/Button/styles/CloseButton.css");
+@import url("chrome://devtools/content/debugger/src/components/shared/Button/styles/CommandBarButton.css");
+@import url("chrome://devtools/content/debugger/src/components/shared/Button/styles/PaneToggleButton.css");
+@import url("chrome://devtools/content/debugger/src/components/shared/Dropdown.css");
+@import url("chrome://devtools/content/debugger/src/components/shared/menu.css");
+@import url("chrome://devtools/content/debugger/src/components/shared/Modal.css");
+@import url("chrome://devtools/content/debugger/src/components/shared/Popover.css");
+@import url("chrome://devtools/content/debugger/src/components/shared/PreviewFunction.css");
+@import url("chrome://devtools/content/debugger/src/components/shared/ResultList.css");
+@import url("chrome://devtools/content/debugger/src/components/shared/SearchInput.css");
+@import url("chrome://devtools/content/debugger/src/components/shared/SourceIcon.css");
+@import url("chrome://devtools/content/debugger/src/components/Editor/Breakpoints.css");
+@import url("chrome://devtools/content/debugger/src/components/Editor/ConditionalPanel.css");
+@import url("chrome://devtools/content/debugger/src/components/Editor/Editor.css");
+@import url("chrome://devtools/content/debugger/src/components/Editor/Footer.css");
+@import url("chrome://devtools/content/debugger/src/components/Editor/InlinePreview.css");
+@import url("chrome://devtools/content/debugger/src/components/Editor/Preview.css");
+@import url("chrome://devtools/content/debugger/src/components/Editor/Preview/Popup.css");
+@import url("chrome://devtools/content/debugger/src/components/Editor/SearchInFileBar.css");
+@import url("chrome://devtools/content/debugger/src/components/Editor/Tabs.css");
+@import url("chrome://devtools/content/debugger/src/components/PrimaryPanes/Outline.css");
+@import url("chrome://devtools/content/debugger/src/components/PrimaryPanes/OutlineFilter.css");
+@import url("chrome://devtools/content/debugger/src/components/PrimaryPanes/ProjectSearch.css");
+@import url("chrome://devtools/content/debugger/src/components/PrimaryPanes/Sources.css");
+@import url("chrome://devtools/content/debugger/src/components/QuickOpenModal.css");
+@import url("chrome://devtools/content/debugger/src/components/SecondaryPanes/Breakpoints/Breakpoints.css");
+@import url("chrome://devtools/content/debugger/src/components/SecondaryPanes/CommandBar.css");
+@import url("chrome://devtools/content/debugger/src/components/SecondaryPanes/EventListeners.css");
+@import url("chrome://devtools/content/debugger/src/components/SecondaryPanes/DOMMutationBreakpoints.css");
+@import url("chrome://devtools/content/debugger/src/components/SecondaryPanes/Expressions.css");
+@import url("chrome://devtools/content/debugger/src/components/SecondaryPanes/Frames/Frames.css");
+@import url("chrome://devtools/content/debugger/src/components/SecondaryPanes/Frames/Group.css");
+@import url("chrome://devtools/content/debugger/src/components/SecondaryPanes/Scopes.css");
+@import url("chrome://devtools/content/debugger/src/components/SecondaryPanes/SecondaryPanes.css");
+@import url("chrome://devtools/content/debugger/src/components/SecondaryPanes/WhyPaused.css");
+@import url("chrome://devtools/content/debugger/src/components/SecondaryPanes/Threads.css");
+@import url("chrome://devtools/content/debugger/src/components/SecondaryPanes/XHRBreakpoints.css");
+@import url("chrome://devtools/content/debugger/src/components/ShortcutsModal.css");
+@import url("chrome://devtools/content/debugger/src/components/WelcomeBox.css");
+@import url("chrome://devtools/content/debugger/src/utils/editor/source-editor.css");
+
+@import url("chrome://devtools/content/shared/components/splitter/SplitBox.css");
diff --git a/devtools/client/debugger/src/main.js b/devtools/client/debugger/src/main.js
new file mode 100644
index 0000000000..83a6c1e6fc
--- /dev/null
+++ b/devtools/client/debugger/src/main.js
@@ -0,0 +1,149 @@
+/* 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 * as firefox from "./client/firefox";
+
+import { asyncStore, verifyPrefSchema, prefs } from "./utils/prefs";
+import { setupHelper } from "./utils/dbg";
+import { setToolboxTelemetry } from "./utils/telemetry";
+
+import {
+ bootstrapApp,
+ bootstrapStore,
+ bootstrapWorkers,
+ unmountRoot,
+ teardownWorkers,
+} from "./utils/bootstrap";
+
+import { initialBreakpointsState } from "./reducers/breakpoints";
+import { initialSourcesState } from "./reducers/sources";
+import { initialUIState } from "./reducers/ui";
+import { initialSourceBlackBoxState } from "./reducers/source-blackbox";
+
+const {
+ sanitizeBreakpoints,
+} = require("resource://devtools/client/shared/thread-utils.js");
+
+async function syncBreakpoints() {
+ const breakpoints = await asyncStore.pendingBreakpoints;
+ const breakpointValues = Object.values(sanitizeBreakpoints(breakpoints));
+ return Promise.all(
+ breakpointValues.map(({ disabled, options, generatedLocation }) => {
+ if (disabled) {
+ return Promise.resolve();
+ }
+ // Set the breakpoint on the server using the generated location as generated
+ // sources are known on server, not original sources.
+ return firefox.clientCommands.setBreakpoint(generatedLocation, options);
+ })
+ );
+}
+
+async function syncXHRBreakpoints() {
+ const breakpoints = await asyncStore.xhrBreakpoints;
+ return Promise.all(
+ breakpoints.map(({ path, method, disabled }) => {
+ if (!disabled) {
+ firefox.clientCommands.setXHRBreakpoint(path, method);
+ }
+ })
+ );
+}
+
+function setPauseOnDebuggerStatement() {
+ const { pauseOnDebuggerStatement } = prefs;
+ return firefox.clientCommands.pauseOnDebuggerStatement(
+ pauseOnDebuggerStatement
+ );
+}
+
+function setPauseOnExceptions() {
+ const { pauseOnExceptions, pauseOnCaughtException } = prefs;
+ return firefox.clientCommands.pauseOnExceptions(
+ pauseOnExceptions,
+ pauseOnCaughtException
+ );
+}
+
+async function loadInitialState(commands, toolbox) {
+ const pendingBreakpoints = sanitizeBreakpoints(
+ await asyncStore.pendingBreakpoints
+ );
+ const tabs = { tabs: await asyncStore.tabs };
+ const xhrBreakpoints = await asyncStore.xhrBreakpoints;
+ const blackboxedRanges = await asyncStore.blackboxedRanges;
+ const eventListenerBreakpoints = await asyncStore.eventListenerBreakpoints;
+ const breakpoints = initialBreakpointsState(xhrBreakpoints);
+ const sourceBlackBox = initialSourceBlackBoxState({ blackboxedRanges });
+ const sources = initialSourcesState();
+ const ui = initialUIState();
+
+ return {
+ pendingBreakpoints,
+ tabs,
+ breakpoints,
+ eventListenerBreakpoints,
+ sources,
+ sourceBlackBox,
+ ui,
+ };
+}
+
+export async function bootstrap({
+ commands,
+ fluentBundles,
+ resourceCommand,
+ workers: panelWorkers,
+ panel,
+}) {
+ verifyPrefSchema();
+
+ // Set telemetry at the very beginning as some actions fired from this function might
+ // record events.
+ setToolboxTelemetry(panel.toolbox.telemetry);
+
+ const initialState = await loadInitialState(commands, panel.toolbox);
+ const workers = bootstrapWorkers(panelWorkers);
+
+ const { store, actions, selectors } = bootstrapStore(
+ firefox.clientCommands,
+ workers,
+ panel,
+ initialState
+ );
+
+ const connected = firefox.onConnect(
+ commands,
+ resourceCommand,
+ actions,
+ store
+ );
+
+ await syncBreakpoints();
+ await syncXHRBreakpoints();
+ await setPauseOnDebuggerStatement();
+ await setPauseOnExceptions();
+
+ setupHelper({
+ store,
+ actions,
+ selectors,
+ workers,
+ targetCommand: commands.targetCommand,
+ client: firefox.clientCommands,
+ });
+
+ bootstrapApp(store, panel.getToolboxStore(), {
+ fluentBundles,
+ toolboxDoc: panel.panelWin.parent.document,
+ });
+ await connected;
+ return { store, actions, selectors, client: firefox.clientCommands };
+}
+
+export async function destroy() {
+ firefox.onDisconnect();
+ unmountRoot();
+ teardownWorkers();
+}
diff --git a/devtools/client/debugger/src/moz.build b/devtools/client/debugger/src/moz.build
new file mode 100644
index 0000000000..44336626f2
--- /dev/null
+++ b/devtools/client/debugger/src/moz.build
@@ -0,0 +1,20 @@
+# 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 += [
+ "actions",
+ "client",
+ "components",
+ "context-menu",
+ "reducers",
+ "selectors",
+ "utils",
+ "workers",
+]
+
+CompiledModules(
+ "constants.js",
+ "main.js",
+)
diff --git a/devtools/client/debugger/src/reducers/ast.js b/devtools/client/debugger/src/reducers/ast.js
new file mode 100644
index 0000000000..22ae3e3d23
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/ast.js
@@ -0,0 +1,92 @@
+/* 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/>. */
+
+/**
+ * Ast reducer
+ * @module reducers/ast
+ */
+
+import { makeBreakpointId } from "../utils/breakpoint/index";
+
+export function initialASTState() {
+ return {
+ // We are using mutable objects as we never return the dictionary as-is from the selectors
+ // but only their values.
+ // Note that all these dictionaries are storing objects as values
+ // which all will have a threadActorId attribute.
+
+ // We have two maps, a first one for original sources.
+ // This is keyed by source id.
+ mutableOriginalSourcesSymbols: {},
+
+ // And another one, for generated sources.
+ // This is keyed by source actor id.
+ mutableSourceActorSymbols: {},
+
+ mutableInScopeLines: {},
+ };
+}
+
+function update(state = initialASTState(), action) {
+ switch (action.type) {
+ case "SET_SYMBOLS": {
+ const { location } = action;
+ if (action.status === "start") {
+ return state;
+ }
+
+ const entry = {
+ value: action.value,
+ threadActorId: location.sourceActor?.thread,
+ };
+ if (location.source.isOriginal) {
+ state.mutableOriginalSourcesSymbols[location.source.id] = entry;
+ } else {
+ if (!location.sourceActor) {
+ throw new Error(
+ "Expects a location with a source actor when adding symbols for non-original sources"
+ );
+ }
+ state.mutableSourceActorSymbols[location.sourceActor.id] = entry;
+ }
+ return {
+ ...state,
+ };
+ }
+
+ case "IN_SCOPE_LINES": {
+ state.mutableInScopeLines[makeBreakpointId(action.location)] = {
+ lines: action.lines,
+ threadActorId: action.location.sourceActor?.thread,
+ };
+ return {
+ ...state,
+ };
+ }
+
+ case "RESUME": {
+ return { ...state, mutableInScopeLines: {} };
+ }
+
+ case "REMOVE_THREAD": {
+ function clearDict(dict, threadId) {
+ for (const key in dict) {
+ if (dict[key].threadActorId == threadId) {
+ delete dict[key];
+ }
+ }
+ }
+ clearDict(state.mutableSourceActorSymbols, action.threadActorID);
+ clearDict(state.mutableOriginalSourcesSymbols, action.threadActorID);
+ clearDict(state.mutableInScopeLines, action.threadActorID);
+ return { ...state };
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
+
+export default update;
diff --git a/devtools/client/debugger/src/reducers/breakpoints.js b/devtools/client/debugger/src/reducers/breakpoints.js
new file mode 100644
index 0000000000..2a35cda9ef
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/breakpoints.js
@@ -0,0 +1,149 @@
+/* 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/>. */
+
+/**
+ * Breakpoints reducer
+ * @module reducers/breakpoints
+ */
+
+import { makeBreakpointId } from "../utils/breakpoint/index";
+
+export function initialBreakpointsState(xhrBreakpoints = []) {
+ return {
+ breakpoints: {},
+ xhrBreakpoints,
+ };
+}
+
+function update(state = initialBreakpointsState(), action) {
+ switch (action.type) {
+ case "SET_BREAKPOINT": {
+ if (action.status === "start") {
+ return setBreakpoint(state, action);
+ }
+ return state;
+ }
+
+ case "REMOVE_BREAKPOINT": {
+ if (action.status === "start") {
+ return removeBreakpoint(state, action);
+ }
+ return state;
+ }
+
+ case "CLEAR_BREAKPOINTS": {
+ return { ...state, breakpoints: {} };
+ }
+
+ case "REMOVE_THREAD": {
+ return removeBreakpointsForSources(state, action.sources);
+ }
+
+ case "SET_XHR_BREAKPOINT": {
+ return addXHRBreakpoint(state, action);
+ }
+
+ case "REMOVE_XHR_BREAKPOINT": {
+ return removeXHRBreakpoint(state, action);
+ }
+
+ case "UPDATE_XHR_BREAKPOINT": {
+ return updateXHRBreakpoint(state, action);
+ }
+
+ case "ENABLE_XHR_BREAKPOINT": {
+ return updateXHRBreakpoint(state, action);
+ }
+
+ case "DISABLE_XHR_BREAKPOINT": {
+ return updateXHRBreakpoint(state, action);
+ }
+ case "CLEAR_XHR_BREAKPOINTS": {
+ if (action.status == "start") {
+ return state;
+ }
+ return { ...state, xhrBreakpoints: [] };
+ }
+ }
+
+ return state;
+}
+
+function addXHRBreakpoint(state, action) {
+ const { xhrBreakpoints } = state;
+ const { breakpoint } = action;
+ const { path, method } = breakpoint;
+
+ const existingBreakpointIndex = state.xhrBreakpoints.findIndex(
+ bp => bp.path === path && bp.method === method
+ );
+
+ if (existingBreakpointIndex === -1) {
+ return {
+ ...state,
+ xhrBreakpoints: [...xhrBreakpoints, breakpoint],
+ };
+ } else if (xhrBreakpoints[existingBreakpointIndex] !== breakpoint) {
+ const newXhrBreakpoints = [...xhrBreakpoints];
+ newXhrBreakpoints[existingBreakpointIndex] = breakpoint;
+ return {
+ ...state,
+ xhrBreakpoints: newXhrBreakpoints,
+ };
+ }
+
+ return state;
+}
+
+function removeXHRBreakpoint(state, action) {
+ const { breakpoint } = action;
+ const { xhrBreakpoints } = state;
+
+ if (action.status === "start") {
+ return state;
+ }
+
+ return {
+ ...state,
+ xhrBreakpoints: xhrBreakpoints.filter(
+ bp => bp.path !== breakpoint.path || bp.method !== breakpoint.method
+ ),
+ };
+}
+
+function updateXHRBreakpoint(state, action) {
+ const { breakpoint, index } = action;
+ const { xhrBreakpoints } = state;
+ const newXhrBreakpoints = [...xhrBreakpoints];
+ newXhrBreakpoints[index] = breakpoint;
+ return {
+ ...state,
+ xhrBreakpoints: newXhrBreakpoints,
+ };
+}
+
+function setBreakpoint(state, { breakpoint }) {
+ const id = makeBreakpointId(breakpoint.location);
+ const breakpoints = { ...state.breakpoints, [id]: breakpoint };
+ return { ...state, breakpoints };
+}
+
+function removeBreakpoint(state, { breakpoint }) {
+ const id = makeBreakpointId(breakpoint.location);
+ const breakpoints = { ...state.breakpoints };
+ delete breakpoints[id];
+ return { ...state, breakpoints };
+}
+
+function removeBreakpointsForSources(state, sources) {
+ const remainingBreakpoints = {};
+ for (const [id, breakpoint] of Object.entries(state.breakpoints)) {
+ if (!sources.includes(breakpoint.location.source)) {
+ remainingBreakpoints[id] = breakpoint;
+ }
+ }
+ return { ...state, breakpoints: remainingBreakpoints };
+}
+
+export default update;
diff --git a/devtools/client/debugger/src/reducers/event-listeners.js b/devtools/client/debugger/src/reducers/event-listeners.js
new file mode 100644
index 0000000000..d0418b9ece
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/event-listeners.js
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+import { prefs } from "../utils/prefs";
+
+export function initialEventListenerState() {
+ return {
+ active: [],
+ categories: [],
+ expanded: [],
+ logEventBreakpoints: prefs.logEventBreakpoints,
+ };
+}
+
+function update(state = initialEventListenerState(), action) {
+ switch (action.type) {
+ case "UPDATE_EVENT_LISTENERS":
+ return { ...state, active: action.active };
+
+ case "RECEIVE_EVENT_LISTENER_TYPES":
+ return { ...state, categories: action.categories };
+
+ case "UPDATE_EVENT_LISTENER_EXPANDED":
+ return { ...state, expanded: action.expanded };
+
+ case "TOGGLE_EVENT_LISTENERS": {
+ const { logEventBreakpoints } = action;
+ prefs.logEventBreakpoints = logEventBreakpoints;
+ return { ...state, logEventBreakpoints };
+ }
+
+ default:
+ return state;
+ }
+}
+
+export default update;
diff --git a/devtools/client/debugger/src/reducers/exceptions.js b/devtools/client/debugger/src/reducers/exceptions.js
new file mode 100644
index 0000000000..a4a26de5df
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/exceptions.js
@@ -0,0 +1,78 @@
+/* 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/>. */
+
+/**
+ * Exceptions reducer
+ * @module reducers/exceptionss
+ */
+
+export function initialExceptionsState() {
+ return {
+ // Store exception objects created by actions/exceptions.js's addExceptionFromResources()
+ // This is keyed by source actor id, and values are arrays of exception objects.
+ mutableExceptionsMap: new Map(),
+ };
+}
+
+function update(state = initialExceptionsState(), action) {
+ switch (action.type) {
+ case "ADD_EXCEPTION":
+ return updateExceptions(state, action);
+ case "REMOVE_THREAD": {
+ return removeExceptionsFromThread(state, action);
+ }
+ }
+ return state;
+}
+
+function updateExceptions(state, action) {
+ const { mutableExceptionsMap } = state;
+ const { exception } = action;
+ const { sourceActorId } = exception;
+
+ let exceptions = mutableExceptionsMap.get(sourceActorId);
+ if (!exceptions) {
+ exceptions = [];
+ mutableExceptionsMap.set(sourceActorId, exceptions);
+ } else if (
+ exceptions.some(({ lineNumber, columnNumber }) => {
+ return (
+ lineNumber == exception.lineNumber &&
+ columnNumber == exception.columnNumber
+ );
+ })
+ ) {
+ // Avoid adding duplicated exceptions for the same line/column
+ return state;
+ }
+
+ // As these arrays are only used by getSelectedSourceExceptions selector method,
+ // which coalesce multiple arrays and always return new array instance,
+ // it isn't important to clone these array in case of modification.
+ exceptions.push(exception);
+
+ return {
+ ...state,
+ };
+}
+
+function removeExceptionsFromThread(state, action) {
+ const { mutableExceptionsMap } = state;
+ const { threadActorID } = action;
+ const sizeBefore = mutableExceptionsMap.size;
+ for (const [sourceActorId, exceptions] of mutableExceptionsMap) {
+ // All exceptions relates to the same source actor, and so, the same thread actor.
+ if (exceptions[0].threadActorId == threadActorID) {
+ mutableExceptionsMap.delete(sourceActorId);
+ }
+ }
+ if (sizeBefore != mutableExceptionsMap.size) {
+ return {
+ ...state,
+ };
+ }
+ return state;
+}
+
+export default update;
diff --git a/devtools/client/debugger/src/reducers/expressions.js b/devtools/client/debugger/src/reducers/expressions.js
new file mode 100644
index 0000000000..9af13523e4
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/expressions.js
@@ -0,0 +1,124 @@
+/* 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/>. */
+
+/**
+ * Expressions reducer
+ * @module reducers/expressions
+ */
+
+import { prefs } from "../utils/prefs";
+
+export const initialExpressionState = () => ({
+ expressions: restoreExpressions(),
+ autocompleteMatches: {},
+ currentAutocompleteInput: null,
+});
+
+function update(state = initialExpressionState(), action) {
+ switch (action.type) {
+ case "ADD_EXPRESSION":
+ return appendExpressionToList(state, {
+ input: action.input,
+ value: null,
+ updating: true,
+ });
+
+ case "UPDATE_EXPRESSION":
+ const key = action.expression.input;
+ return updateExpressionInList(state, key, {
+ input: action.input,
+ value: null,
+ updating: true,
+ });
+
+ case "EVALUATE_EXPRESSION":
+ return updateExpressionInList(state, action.input, {
+ input: action.input,
+ value: action.value,
+ updating: false,
+ });
+
+ case "EVALUATE_EXPRESSIONS":
+ const { inputs, results } = action;
+
+ return inputs.reduce(
+ (_state, input, index) =>
+ updateExpressionInList(_state, input, {
+ input,
+ value: results[index],
+ updating: false,
+ }),
+ state
+ );
+
+ case "DELETE_EXPRESSION":
+ return deleteExpression(state, action.input);
+
+ case "AUTOCOMPLETE":
+ const { matchProp, matches } = action.result;
+
+ return {
+ ...state,
+ currentAutocompleteInput: matchProp,
+ autocompleteMatches: {
+ ...state.autocompleteMatches,
+ [matchProp]: matches,
+ },
+ };
+
+ case "CLEAR_AUTOCOMPLETE":
+ return {
+ ...state,
+ autocompleteMatches: {},
+ currentAutocompleteInput: "",
+ };
+ }
+
+ return state;
+}
+
+function restoreExpressions() {
+ const exprs = prefs.expressions;
+ if (!exprs.length) {
+ return [];
+ }
+
+ return exprs;
+}
+
+function storeExpressions({ expressions }) {
+ // Return the expressions without the `value` property
+ prefs.expressions = expressions.map(({ input, updating }) => ({
+ input,
+ updating,
+ }));
+}
+
+function appendExpressionToList(state, value) {
+ const newState = { ...state, expressions: [...state.expressions, value] };
+
+ storeExpressions(newState);
+ return newState;
+}
+
+function updateExpressionInList(state, key, value) {
+ const list = [...state.expressions];
+ const index = list.findIndex(e => e.input == key);
+ list[index] = value;
+
+ const newState = { ...state, expressions: list };
+ storeExpressions(newState);
+ return newState;
+}
+
+function deleteExpression(state, input) {
+ const list = [...state.expressions];
+ const index = list.findIndex(e => e.input == input);
+ list.splice(index, 1);
+ const newState = { ...state, expressions: list };
+ storeExpressions(newState);
+ return newState;
+}
+
+export default update;
diff --git a/devtools/client/debugger/src/reducers/index.js b/devtools/client/debugger/src/reducers/index.js
new file mode 100644
index 0000000000..b254a5b176
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/index.js
@@ -0,0 +1,76 @@
+/* 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/>. */
+
+/**
+ * Reducer index
+ * @module reducers/index
+ */
+
+import expressions, { initialExpressionState } from "./expressions";
+import sourceActors, { initialSourceActorsState } from "./source-actors";
+import sources, { initialSourcesState } from "./sources";
+import sourceBlackBox, { initialSourceBlackBoxState } from "./source-blackbox";
+import sourcesContent, { initialSourcesContentState } from "./sources-content";
+import tabs, { initialTabState } from "./tabs";
+import breakpoints, { initialBreakpointsState } from "./breakpoints";
+import pendingBreakpoints from "./pending-breakpoints";
+import pause, { initialPauseState } from "./pause";
+import ui, { initialUIState } from "./ui";
+import ast, { initialASTState } from "./ast";
+import quickOpen, { initialQuickOpenState } from "./quick-open";
+import sourcesTree, { initialSourcesTreeState } from "./sources-tree";
+import threads, { initialThreadsState } from "./threads";
+import eventListenerBreakpoints, {
+ initialEventListenerState,
+} from "./event-listeners";
+import exceptions, { initialExceptionsState } from "./exceptions";
+
+import { objectInspector } from "devtools/client/shared/components/reps/index";
+
+/**
+ * Note that this is only used by jest tests.
+ *
+ * Production is using loadInitialState() in main.js
+ */
+export function initialState() {
+ return {
+ sources: initialSourcesState(),
+ sourcesContent: initialSourcesContentState(),
+ expressions: initialExpressionState(),
+ sourceActors: initialSourceActorsState(),
+ sourceBlackBox: initialSourceBlackBoxState(),
+ tabs: initialTabState(),
+ breakpoints: initialBreakpointsState(),
+ pendingBreakpoints: {},
+ pause: initialPauseState(),
+ ui: initialUIState(),
+ ast: initialASTState(),
+ quickOpen: initialQuickOpenState(),
+ sourcesTree: initialSourcesTreeState(),
+ threads: initialThreadsState(),
+ objectInspector: objectInspector.reducer.initialOIState(),
+ eventListenerBreakpoints: initialEventListenerState(),
+ exceptions: initialExceptionsState(),
+ };
+}
+
+export default {
+ expressions,
+ sourceActors,
+ sourceBlackBox,
+ sourcesContent,
+ sources,
+ tabs,
+ breakpoints,
+ pendingBreakpoints,
+ pause,
+ ui,
+ ast,
+ quickOpen,
+ sourcesTree,
+ threads,
+ objectInspector: objectInspector.reducer.default,
+ eventListenerBreakpoints,
+ exceptions,
+};
diff --git a/devtools/client/debugger/src/reducers/moz.build b/devtools/client/debugger/src/reducers/moz.build
new file mode 100644
index 0000000000..4e2d2a1045
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/moz.build
@@ -0,0 +1,26 @@
+# 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",
+ "breakpoints.js",
+ "event-listeners.js",
+ "exceptions.js",
+ "expressions.js",
+ "index.js",
+ "pause.js",
+ "pending-breakpoints.js",
+ "quick-open.js",
+ "source-actors.js",
+ "source-blackbox.js",
+ "sources.js",
+ "sources-content.js",
+ "sources-tree.js",
+ "tabs.js",
+ "threads.js",
+ "ui.js",
+)
diff --git a/devtools/client/debugger/src/reducers/pause.js b/devtools/client/debugger/src/reducers/pause.js
new file mode 100644
index 0000000000..8cb5eefe98
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/pause.js
@@ -0,0 +1,427 @@
+/* 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/>. */
+
+/* eslint complexity: ["error", 36]*/
+
+/**
+ * Pause reducer
+ * @module reducers/pause
+ */
+
+import { prefs } from "../utils/prefs";
+
+// Pause state associated with an individual thread.
+
+// Pause state describing all threads.
+
+export function initialPauseState(thread = "UnknownThread") {
+ return {
+ cx: {
+ navigateCounter: 0,
+ },
+ // This `threadcx` is the `cx` variable we pass around in components and actions.
+ // This is pulled via getThreadContext().
+ // This stores information about the currently selected thread and its paused state.
+ threadcx: {
+ navigateCounter: 0,
+ thread,
+ pauseCounter: 0,
+ },
+ threads: {},
+ skipPausing: prefs.skipPausing,
+ mapScopes: prefs.mapScopes,
+ shouldPauseOnDebuggerStatement: prefs.pauseOnDebuggerStatement,
+ shouldPauseOnExceptions: prefs.pauseOnExceptions,
+ shouldPauseOnCaughtExceptions: prefs.pauseOnCaughtExceptions,
+ };
+}
+
+const resumedPauseState = {
+ isPaused: false,
+ frames: null,
+ framesLoading: false,
+ frameScopes: {
+ generated: {},
+ original: {},
+ mappings: {},
+ },
+ selectedFrameId: null,
+ why: null,
+ inlinePreview: {},
+};
+
+const createInitialPauseState = () => ({
+ ...resumedPauseState,
+ isWaitingOnBreak: false,
+ command: null,
+ previousLocation: null,
+ expandedScopes: new Set(),
+ lastExpandedScopes: [],
+});
+
+export function getThreadPauseState(state, thread) {
+ // Thread state is lazily initialized so that we don't have to keep track of
+ // the current set of worker threads.
+ return state.threads[thread] || createInitialPauseState();
+}
+
+function update(state = initialPauseState(), action) {
+ // All the actions updating pause state must pass an object which designate
+ // the related thread.
+ const getActionThread = () => {
+ const thread =
+ action.thread || action.selectedFrame?.thread || action.frame?.thread;
+ if (!thread) {
+ throw new Error(`Missing thread in action ${action.type}`);
+ }
+ return thread;
+ };
+
+ // `threadState` and `updateThreadState` help easily get and update
+ // the pause state for a given thread.
+ const threadState = () => {
+ return getThreadPauseState(state, getActionThread());
+ };
+ const updateThreadState = newThreadState => {
+ return {
+ ...state,
+ threads: {
+ ...state.threads,
+ [getActionThread()]: { ...threadState(), ...newThreadState },
+ },
+ };
+ };
+
+ switch (action.type) {
+ case "SELECT_THREAD": {
+ // Ignore the action if the related thread doesn't exist.
+ if (!state.threads[action.thread]) {
+ console.warn(
+ `Trying to select a destroyed or non-existent thread '${action.thread}'`
+ );
+ return state;
+ }
+
+ return {
+ ...state,
+ threadcx: {
+ ...state.threadcx,
+ thread: action.thread,
+ pauseCounter: state.threadcx.pauseCounter + 1,
+ },
+ };
+ }
+
+ case "INSERT_THREAD": {
+ // When navigating to a new location,
+ // we receive NAVIGATE early, which clear things
+ // then we have REMOVE_THREAD of the previous thread.
+ // INSERT_THREAD will be the very first event with the new thread actor ID.
+ // Automatically select the new top level thread.
+ if (action.newThread.isTopLevel) {
+ return {
+ ...state,
+ threadcx: {
+ ...state.threadcx,
+ thread: action.newThread.actor,
+ pauseCounter: state.threadcx.pauseCounter + 1,
+ },
+ threads: {
+ ...state.threads,
+ [action.newThread.actor]: createInitialPauseState(),
+ },
+ };
+ }
+
+ return {
+ ...state,
+ threads: {
+ ...state.threads,
+ [action.newThread.actor]: createInitialPauseState(),
+ },
+ };
+ }
+
+ case "REMOVE_THREAD": {
+ if (
+ action.threadActorID in state.threads ||
+ action.threadActorID == state.threadcx.thread
+ ) {
+ // Remove the thread from the cached list
+ const threads = { ...state.threads };
+ delete threads[action.threadActorID];
+ let threadcx = state.threadcx;
+
+ // And also switch to another thread if this was the currently selected one.
+ // As we don't store thread objects in this reducer, and only store thread actor IDs,
+ // we can't try to find the top level thread. So we pick the first available thread,
+ // and hope that's the top level one.
+ if (state.threadcx.thread == action.threadActorID) {
+ threadcx = {
+ ...threadcx,
+ thread: Object.keys(threads)[0],
+ pauseCounter: threadcx.pauseCounter + 1,
+ };
+ }
+ return {
+ ...state,
+ threadcx,
+ threads,
+ };
+ }
+ break;
+ }
+
+ case "PAUSED": {
+ const { thread, topFrame, why } = action;
+ state = {
+ ...state,
+ threadcx: {
+ ...state.threadcx,
+ pauseCounter: state.threadcx.pauseCounter + 1,
+ thread,
+ },
+ };
+
+ return updateThreadState({
+ isWaitingOnBreak: false,
+ selectedFrameId: topFrame.id,
+ isPaused: true,
+ // On pause, we only receive the top frame, all subsequent ones
+ // will be asynchronously populated via `fetchFrames` action
+ frames: [topFrame],
+ framesLoading: true,
+ frameScopes: { ...resumedPauseState.frameScopes },
+ why,
+ shouldBreakpointsPaneOpenOnPause: why.type === "breakpoint",
+ });
+ }
+
+ case "FETCHED_FRAMES": {
+ const { frames } = action;
+
+ // We typically receive a PAUSED action before this one,
+ // with only the first frame. Here, we avoid replacing it
+ // with a copy of it in order to avoid triggerring selectors
+ // uncessarily
+ // (note that in jest, action's frames might be empty)
+ // (and if we resume in between PAUSED and FETCHED_FRAMES
+ // threadState().frames might be null)
+ if (threadState().frames) {
+ const previousFirstFrame = threadState().frames[0];
+ if (previousFirstFrame.id == frames[0]?.id) {
+ frames.splice(0, 1, previousFirstFrame);
+ }
+ }
+ return updateThreadState({ frames, framesLoading: false });
+ }
+
+ case "MAP_FRAMES": {
+ const { selectedFrameId, frames } = action;
+ return updateThreadState({ frames, selectedFrameId });
+ }
+
+ case "ADD_SCOPES": {
+ const { status, value } = action;
+ const selectedFrameId = action.selectedFrame.id;
+
+ const generated = {
+ ...threadState().frameScopes.generated,
+ [selectedFrameId]: {
+ pending: status !== "done",
+ scope: value,
+ },
+ };
+
+ return updateThreadState({
+ frameScopes: {
+ ...threadState().frameScopes,
+ generated,
+ },
+ });
+ }
+
+ case "MAP_SCOPES": {
+ const { status, value } = action;
+ const selectedFrameId = action.selectedFrame.id;
+
+ const original = {
+ ...threadState().frameScopes.original,
+ [selectedFrameId]: {
+ pending: status !== "done",
+ scope: value?.scope,
+ },
+ };
+
+ const mappings = {
+ ...threadState().frameScopes.mappings,
+ [selectedFrameId]: value?.mappings,
+ };
+
+ return updateThreadState({
+ frameScopes: {
+ ...threadState().frameScopes,
+ original,
+ mappings,
+ },
+ });
+ }
+
+ case "BREAK_ON_NEXT":
+ return updateThreadState({ isWaitingOnBreak: true });
+
+ case "SELECT_FRAME":
+ return updateThreadState({ selectedFrameId: action.frame.id });
+
+ case "PAUSE_ON_DEBUGGER_STATEMENT": {
+ const { shouldPauseOnDebuggerStatement } = action;
+
+ prefs.pauseOnDebuggerStatement = shouldPauseOnDebuggerStatement;
+
+ return {
+ ...state,
+ shouldPauseOnDebuggerStatement,
+ };
+ }
+
+ case "PAUSE_ON_EXCEPTIONS": {
+ const { shouldPauseOnExceptions, shouldPauseOnCaughtExceptions } = action;
+
+ prefs.pauseOnExceptions = shouldPauseOnExceptions;
+ prefs.pauseOnCaughtExceptions = shouldPauseOnCaughtExceptions;
+
+ // Preserving for the old debugger
+ prefs.ignoreCaughtExceptions = !shouldPauseOnCaughtExceptions;
+
+ return {
+ ...state,
+ shouldPauseOnExceptions,
+ shouldPauseOnCaughtExceptions,
+ };
+ }
+
+ case "COMMAND":
+ if (action.status === "start") {
+ return updateThreadState({
+ ...resumedPauseState,
+ command: action.command,
+ previousLocation: getPauseLocation(threadState(), action),
+ });
+ }
+ return updateThreadState({ command: null });
+
+ case "RESUME": {
+ if (action.thread == state.threadcx.thread) {
+ state = {
+ ...state,
+ threadcx: {
+ ...state.threadcx,
+ pauseCounter: state.threadcx.pauseCounter + 1,
+ },
+ };
+ }
+
+ return updateThreadState({
+ ...resumedPauseState,
+ expandedScopes: new Set(),
+ lastExpandedScopes: [...threadState().expandedScopes],
+ shouldBreakpointsPaneOpenOnPause: false,
+ });
+ }
+
+ case "EVALUATE_EXPRESSION":
+ return updateThreadState({
+ command: action.status === "start" ? "expression" : null,
+ });
+
+ case "NAVIGATE": {
+ const navigateCounter = state.cx.navigateCounter + 1;
+ return {
+ ...state,
+ cx: {
+ navigateCounter,
+ },
+ threadcx: {
+ navigateCounter,
+ thread: action.mainThread.actor,
+ pauseCounter: 0,
+ },
+ threads: {
+ ...state.threads,
+ [action.mainThread.actor]: {
+ ...getThreadPauseState(state, action.mainThread.actor),
+ ...resumedPauseState,
+ },
+ },
+ };
+ }
+
+ case "TOGGLE_SKIP_PAUSING": {
+ const { skipPausing } = action;
+ prefs.skipPausing = skipPausing;
+
+ return { ...state, skipPausing };
+ }
+
+ case "TOGGLE_MAP_SCOPES": {
+ const { mapScopes } = action;
+ prefs.mapScopes = mapScopes;
+ return { ...state, mapScopes };
+ }
+
+ case "SET_EXPANDED_SCOPE": {
+ const { path, expanded } = action;
+ const expandedScopes = new Set(threadState().expandedScopes);
+ if (expanded) {
+ expandedScopes.add(path);
+ } else {
+ expandedScopes.delete(path);
+ }
+ return updateThreadState({ expandedScopes });
+ }
+
+ case "ADD_INLINE_PREVIEW": {
+ const { selectedFrame, previews } = action;
+ const selectedFrameId = selectedFrame.id;
+
+ return updateThreadState({
+ inlinePreview: {
+ ...threadState().inlinePreview,
+ [selectedFrameId]: previews,
+ },
+ });
+ }
+
+ case "RESET_BREAKPOINTS_PANE_STATE": {
+ return updateThreadState({
+ ...threadState(),
+ shouldBreakpointsPaneOpenOnPause: false,
+ });
+ }
+ }
+
+ return state;
+}
+
+function getPauseLocation(state, action) {
+ const { frames, previousLocation } = state;
+
+ // NOTE: We store the previous location so that we ensure that we
+ // do not stop at the same location twice when we step over.
+ if (action.command !== "stepOver") {
+ return null;
+ }
+
+ const frame = frames?.[0];
+ if (!frame) {
+ return previousLocation;
+ }
+
+ return {
+ location: frame.location,
+ generatedLocation: frame.generatedLocation,
+ };
+}
+
+export default update;
diff --git a/devtools/client/debugger/src/reducers/pending-breakpoints.js b/devtools/client/debugger/src/reducers/pending-breakpoints.js
new file mode 100644
index 0000000000..a0d51e5693
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/pending-breakpoints.js
@@ -0,0 +1,135 @@
+/* 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/>. */
+
+/**
+ * Pending breakpoints reducer.
+ *
+ * Pending breakpoints are a more lightweight version compared to regular breakpoints objects.
+ * They are meant to be persisted across Firefox restarts and stored into async-storage.
+ * This reducer data is saved into asyncStore from bootstrap.js and restored from main.js.
+ *
+ * The main difference with pending breakpoints is that we only save breakpoints
+ * against source with an URL as only them can be restored. (source IDs are different across reloads).
+ * The second difference is that we don't store the whole source object but only the source URL.
+ */
+
+import { isPrettyURL } from "../utils/source";
+import assert from "../utils/assert";
+
+function update(state = {}, action) {
+ switch (action.type) {
+ case "SET_BREAKPOINT":
+ if (action.status === "start") {
+ return setBreakpoint(state, action.breakpoint);
+ }
+ return state;
+
+ case "REMOVE_BREAKPOINT":
+ if (action.status === "start") {
+ return removeBreakpoint(state, action.breakpoint);
+ }
+ return state;
+
+ case "REMOVE_PENDING_BREAKPOINT":
+ return removePendingBreakpoint(state, action.pendingBreakpoint);
+
+ case "CLEAR_BREAKPOINTS": {
+ return {};
+ }
+ }
+
+ return state;
+}
+
+function shouldBreakpointBePersisted(breakpoint) {
+ // We only save breakpoint for source with URL.
+ // Source without URL can only be identified via their source actor ID
+ // which isn't persisted across reloads.
+ return !breakpoint.options.hidden && breakpoint.location.source.url;
+}
+
+function setBreakpoint(state, breakpoint) {
+ if (!shouldBreakpointBePersisted(breakpoint)) {
+ return state;
+ }
+
+ const id = makeIdFromBreakpoint(breakpoint);
+ const pendingBreakpoint = createPendingBreakpoint(breakpoint);
+
+ return { ...state, [id]: pendingBreakpoint };
+}
+
+function removeBreakpoint(state, breakpoint) {
+ if (!shouldBreakpointBePersisted(breakpoint)) {
+ return state;
+ }
+
+ const id = makeIdFromBreakpoint(breakpoint);
+ state = { ...state };
+
+ delete state[id];
+ return state;
+}
+
+function removePendingBreakpoint(state, pendingBreakpoint) {
+ const id = makeIdFromPendingBreakpoint(pendingBreakpoint);
+ state = { ...state };
+
+ delete state[id];
+ return state;
+}
+
+/**
+ * Return a unique identifier for a given breakpoint,
+ * using its original location, or for pretty-printed sources,
+ * its generated location.
+ *
+ * @param {Object} breakpoint
+ */
+function makeIdFromBreakpoint(breakpoint) {
+ const location = isPrettyURL(breakpoint.location.source.url)
+ ? breakpoint.generatedLocation
+ : breakpoint.location;
+
+ const { source, line, column } = location;
+ const sourceUrlString = source.url || "";
+ const columnString = column || "";
+
+ return `${sourceUrlString}:${line}:${columnString}`;
+}
+
+function makeIdFromPendingBreakpoint(pendingBreakpoint) {
+ const { sourceUrl, line, column } = pendingBreakpoint.location;
+ const sourceUrlString = sourceUrl || "";
+ const columnString = column || "";
+
+ return `${sourceUrlString}:${line}:${columnString}`;
+}
+
+/**
+ * Convert typical debugger frontend location (created via location.js:createLocation)
+ * to a more lightweight flavor of it which will be stored in async storage.
+ */
+function createPendingLocation(location) {
+ assert(location.hasOwnProperty("line"), "location must have a line");
+ assert(location.hasOwnProperty("column"), "location must have a column");
+
+ const { source, line, column } = location;
+ assert(source.url !== undefined, "pending location must have a source url");
+ return { sourceUrl: source.url, line, column };
+}
+
+/**
+ * Create a new pending breakpoint, which is a more lightweight version of the regular breakpoint object.
+ */
+function createPendingBreakpoint(bp) {
+ return {
+ options: bp.options,
+ disabled: bp.disabled,
+ location: createPendingLocation(bp.location),
+ generatedLocation: createPendingLocation(bp.generatedLocation),
+ };
+}
+
+export default update;
diff --git a/devtools/client/debugger/src/reducers/quick-open.js b/devtools/client/debugger/src/reducers/quick-open.js
new file mode 100644
index 0000000000..459e530e7b
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/quick-open.js
@@ -0,0 +1,41 @@
+/* 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/>. */
+
+/**
+ * Quick Open reducer
+ * @module reducers/quick-open
+ */
+
+import { parseQuickOpenQuery } from "../utils/quick-open";
+
+export const initialQuickOpenState = () => ({
+ enabled: false,
+ query: "",
+ searchType: "sources",
+});
+
+export default function update(state = initialQuickOpenState(), action) {
+ switch (action.type) {
+ case "OPEN_QUICK_OPEN":
+ if (action.query != null) {
+ return {
+ ...state,
+ enabled: true,
+ query: action.query,
+ searchType: parseQuickOpenQuery(action.query),
+ };
+ }
+ return { ...state, enabled: true };
+ case "CLOSE_QUICK_OPEN":
+ return initialQuickOpenState();
+ case "SET_QUICK_OPEN_QUERY":
+ return {
+ ...state,
+ query: action.query,
+ searchType: parseQuickOpenQuery(action.query),
+ };
+ default:
+ return state;
+ }
+}
diff --git a/devtools/client/debugger/src/reducers/source-actors.js b/devtools/client/debugger/src/reducers/source-actors.js
new file mode 100644
index 0000000000..fd99955742
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/source-actors.js
@@ -0,0 +1,109 @@
+/* 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/>. */
+
+/**
+ * This reducer stores the list of all source actors as well their breakable lines.
+ *
+ * There is a one-one relationship with Source Actors from the server codebase,
+ * as well as SOURCE Resources distributed by the ResourceCommand API.
+ *
+ */
+function initialSourceActorsState() {
+ return {
+ // Map(Source Actor ID: string => SourceActor: object)
+ // See create.js: `createSourceActor` for the shape of the source actor objects.
+ mutableSourceActors: new Map(),
+
+ // Map(Source Actor ID: string => Breakable lines: Array<Number>)
+ // The array is the list of all lines where breakpoints can be set
+ mutableBreakableLines: new Map(),
+
+ // Set(Source Actor ID: string)
+ // List of all IDs of source actor which have a valid related source map / original source.
+ // The SourceActor object may have a sourceMapURL attribute set,
+ // but this may be invalid. The source map URL or source map file content may be invalid.
+ // In these scenarios we will remove the source actor from this set.
+ mutableSourceActorsWithSourceMap: new Set(),
+
+ // Map(Source Actor ID: string => string)
+ // Store the exception message when processing the sourceMapURL field of the source actor.
+ mutableSourceMapErrors: new Map(),
+
+ // Map(Source Actor ID: string => string)
+ // When a bundle has a functional sourcemap, reports the resolved source map URL.
+ mutableResolvedSourceMapURL: new Map(),
+ };
+}
+
+export const initial = initialSourceActorsState();
+
+export default function update(state = initialSourceActorsState(), action) {
+ switch (action.type) {
+ case "INSERT_SOURCE_ACTORS": {
+ for (const sourceActor of action.sourceActors) {
+ state.mutableSourceActors.set(sourceActor.id, sourceActor);
+
+ // If the sourceMapURL attribute is set, consider that it is valid.
+ // But this may be revised later and removed from this Set.
+ if (sourceActor.sourceMapURL) {
+ state.mutableSourceActorsWithSourceMap.add(sourceActor.id);
+ }
+ }
+ return {
+ ...state,
+ };
+ }
+
+ case "REMOVE_THREAD": {
+ for (const sourceActor of state.mutableSourceActors.values()) {
+ if (sourceActor.thread == action.threadActorID) {
+ state.mutableSourceActors.delete(sourceActor.id);
+ state.mutableBreakableLines.delete(sourceActor.id);
+ state.mutableSourceActorsWithSourceMap.delete(sourceActor.id);
+ }
+ }
+ return {
+ ...state,
+ };
+ }
+
+ case "SET_SOURCE_ACTOR_BREAKABLE_LINES":
+ state.mutableBreakableLines.set(
+ action.sourceActor.id,
+ action.breakableLines
+ );
+
+ return {
+ ...state,
+ };
+
+ case "CLEAR_SOURCE_ACTOR_MAP_URL":
+ if (
+ state.mutableSourceActorsWithSourceMap.delete(action.sourceActor.id)
+ ) {
+ return {
+ ...state,
+ };
+ }
+ return state;
+
+ case "SOURCE_MAP_ERROR": {
+ state.mutableSourceMapErrors.set(
+ action.sourceActor.id,
+ action.errorMessage
+ );
+ return { ...state };
+ }
+
+ case "RESOLVED_SOURCEMAP_URL": {
+ state.mutableResolvedSourceMapURL.set(
+ action.sourceActor.id,
+ action.resolvedSourceMapURL
+ );
+ return { ...state };
+ }
+ }
+
+ return state;
+}
diff --git a/devtools/client/debugger/src/reducers/source-blackbox.js b/devtools/client/debugger/src/reducers/source-blackbox.js
new file mode 100644
index 0000000000..3ccca49790
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/source-blackbox.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/>. */
+
+/**
+ * Reducer containing all data about sources being "black boxed".
+ *
+ * i.e. sources which should be ignored by the debugger.
+ * Typically, these sources should be hidden from paused stack frames,
+ * and any debugger statement or breakpoint should be ignored.
+ */
+
+export function initialSourceBlackBoxState(state) {
+ return {
+ /* FORMAT:
+ * blackboxedRanges: {
+ * [source url]: [range, range, ...], -- source lines blackboxed
+ * [source url]: [], -- whole source blackboxed
+ * ...
+ * }
+ */
+ blackboxedRanges: state?.blackboxedRanges ?? {},
+
+ blackboxedSet: state?.blackboxedRanges
+ ? new Set(Object.keys(state.blackboxedRanges))
+ : new Set(),
+
+ sourceMapIgnoreListUrls: [],
+ };
+}
+
+function update(state = initialSourceBlackBoxState(), action) {
+ switch (action.type) {
+ case "BLACKBOX_WHOLE_SOURCES": {
+ const { sources } = action;
+
+ const currentBlackboxedRanges = { ...state.blackboxedRanges };
+ const currentBlackboxedSet = new Set(state.blackboxedSet);
+
+ for (const source of sources) {
+ currentBlackboxedRanges[source.url] = [];
+ currentBlackboxedSet.add(source.url);
+ }
+
+ return {
+ ...state,
+ blackboxedRanges: currentBlackboxedRanges,
+ blackboxedSet: currentBlackboxedSet,
+ };
+ }
+
+ case "BLACKBOX_SOURCE_RANGES": {
+ const { source, ranges } = action;
+
+ const currentBlackboxedRanges = { ...state.blackboxedRanges };
+ const currentBlackboxedSet = new Set(state.blackboxedSet);
+
+ if (!currentBlackboxedRanges[source.url]) {
+ currentBlackboxedRanges[source.url] = [];
+ currentBlackboxedSet.add(source.url);
+ } else {
+ currentBlackboxedRanges[source.url] = [
+ ...state.blackboxedRanges[source.url],
+ ];
+ }
+
+ // Add new blackboxed lines in acsending order
+ for (const newRange of ranges) {
+ const index = currentBlackboxedRanges[source.url].findIndex(
+ range =>
+ range.end.line <= newRange.start.line &&
+ range.end.column <= newRange.start.column
+ );
+ currentBlackboxedRanges[source.url].splice(index + 1, 0, newRange);
+ }
+
+ return {
+ ...state,
+ blackboxedRanges: currentBlackboxedRanges,
+ blackboxedSet: currentBlackboxedSet,
+ };
+ }
+
+ case "UNBLACKBOX_WHOLE_SOURCES": {
+ const { sources } = action;
+
+ const currentBlackboxedRanges = { ...state.blackboxedRanges };
+ const currentBlackboxedSet = new Set(state.blackboxedSet);
+
+ for (const source of sources) {
+ delete currentBlackboxedRanges[source.url];
+ currentBlackboxedSet.delete(source.url);
+ }
+
+ return {
+ ...state,
+ blackboxedRanges: currentBlackboxedRanges,
+ blackboxedSet: currentBlackboxedSet,
+ };
+ }
+
+ case "UNBLACKBOX_SOURCE_RANGES": {
+ const { source, ranges } = action;
+
+ const currentBlackboxedRanges = {
+ ...state.blackboxedRanges,
+ [source.url]: [...state.blackboxedRanges[source.url]],
+ };
+
+ for (const newRange of ranges) {
+ const index = currentBlackboxedRanges[source.url].findIndex(
+ range =>
+ range.start.line === newRange.start.line &&
+ range.end.line === newRange.end.line
+ );
+
+ if (index !== -1) {
+ currentBlackboxedRanges[source.url].splice(index, 1);
+ }
+ }
+
+ return {
+ ...state,
+ blackboxedRanges: currentBlackboxedRanges,
+ };
+ }
+
+ case "ADD_SOURCEMAP_IGNORE_LIST_SOURCES": {
+ return {
+ ...state,
+ sourceMapIgnoreListUrls: [
+ ...state.sourceMapIgnoreListUrls,
+ ...action.ignoreListUrls,
+ ],
+ };
+ }
+
+ case "NAVIGATE":
+ return initialSourceBlackBoxState(state);
+ }
+
+ return state;
+}
+
+export default update;
diff --git a/devtools/client/debugger/src/reducers/sources-content.js b/devtools/client/debugger/src/reducers/sources-content.js
new file mode 100644
index 0000000000..e71330dc5c
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/sources-content.js
@@ -0,0 +1,139 @@
+/* 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/>. */
+
+/**
+ * Sources content reducer.
+ *
+ * This store the textual content for each source.
+ */
+
+import { pending, fulfilled, rejected } from "../utils/async-value";
+
+export function initialSourcesContentState() {
+ return {
+ /**
+ * Text content of all the original sources.
+ * This is large data, so this is only fetched on-demand for a subset of sources.
+ * This state attribute is mutable in order to avoid cloning this possibly large map
+ * on each new source. But selectors are never based on the map. Instead they only
+ * query elements of the map.
+ *
+ * Map(source id => AsyncValue<String>)
+ */
+ mutableOriginalSourceTextContentMapBySourceId: new Map(),
+
+ /**
+ * Text content of all the generated sources.
+ *
+ * Map(source actor is => AsyncValue<String>)
+ */
+ mutableGeneratedSourceTextContentMapBySourceActorId: new Map(),
+
+ /**
+ * Incremental number that is bumped each time we navigate to a new page.
+ *
+ * This is used to better handle async race condition where we mix previous page data
+ * with the new page. As sources are keyed by URL we may easily conflate the two page loads data.
+ */
+ epoch: 1,
+ };
+}
+
+function update(state = initialSourcesContentState(), action) {
+ switch (action.type) {
+ case "LOAD_ORIGINAL_SOURCE_TEXT":
+ if (!action.source) {
+ throw new Error("Missing source");
+ }
+ return updateSourceTextContent(state, action);
+
+ case "LOAD_GENERATED_SOURCE_TEXT":
+ if (!action.sourceActor) {
+ throw new Error("Missing source actor.");
+ }
+ return updateSourceTextContent(state, action);
+
+ case "REMOVE_THREAD":
+ return removeThread(state, action);
+ }
+
+ return state;
+}
+
+/*
+ * Update a source's loaded text content.
+ */
+function updateSourceTextContent(state, action) {
+ // If there was a navigation between the time the action was started and
+ // completed, we don't want to update the store.
+ if (action.epoch !== state.epoch) {
+ return state;
+ }
+
+ let content;
+ if (action.status === "start") {
+ content = pending();
+ } else if (action.status === "error") {
+ content = rejected(action.error);
+ } else if (typeof action.value.text === "string") {
+ content = fulfilled({
+ type: "text",
+ value: action.value.text,
+ contentType: action.value.contentType,
+ });
+ } else {
+ content = fulfilled({
+ type: "wasm",
+ value: action.value.text,
+ });
+ }
+
+ if (action.source && action.sourceActor) {
+ throw new Error(
+ "Both the source and the source actor should not exist at the same time"
+ );
+ }
+
+ if (action.source) {
+ state.mutableOriginalSourceTextContentMapBySourceId.set(
+ action.source.id,
+ content
+ );
+ }
+
+ if (action.sourceActor) {
+ state.mutableGeneratedSourceTextContentMapBySourceActorId.set(
+ action.sourceActor.id,
+ content
+ );
+ }
+
+ return {
+ ...state,
+ };
+}
+
+function removeThread(state, action) {
+ const originalSizeBefore =
+ state.mutableOriginalSourceTextContentMapBySourceId.size;
+ for (const source of action.sources) {
+ state.mutableOriginalSourceTextContentMapBySourceId.delete(source.id);
+ }
+ const generatedSizeBefore =
+ state.mutableGeneratedSourceTextContentMapBySourceActorId.size;
+ for (const actor of action.actors) {
+ state.mutableGeneratedSourceTextContentMapBySourceActorId.delete(actor.id);
+ }
+ if (
+ originalSizeBefore !=
+ state.mutableOriginalSourceTextContentMapBySourceId.size ||
+ generatedSizeBefore !=
+ state.mutableGeneratedSourceTextContentMapBySourceActorId.size
+ ) {
+ return { ...state };
+ }
+ return state;
+}
+
+export default update;
diff --git a/devtools/client/debugger/src/reducers/sources-tree.js b/devtools/client/debugger/src/reducers/sources-tree.js
new file mode 100644
index 0000000000..0f0e8dadb3
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/sources-tree.js
@@ -0,0 +1,704 @@
+/* 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/>. */
+
+/**
+ * Sources tree reducer
+ *
+ * A Source Tree is composed of:
+ *
+ * - Thread Items To designate targets/threads.
+ * These are the roots of the Tree if no project directory is selected.
+ *
+ * - Group Items To designates the different domains used in the website.
+ * These are direct children of threads and may contain directory or source items.
+ *
+ * - Directory Items To designate all the folders.
+ * Note that each every folder has an items. The Source Tree React component is doing the magic to coallesce folders made of only one sub folder.
+ *
+ * - Source Items To designate sources.
+ * They are the leaves of the Tree. (we should not have empty directories.)
+ */
+
+const IGNORED_URLS = ["debugger eval code", "XStringBundle"];
+const IGNORED_EXTENSIONS = ["css", "svg", "png"];
+import { isPretty, getRawSourceURL } from "../utils/source";
+import { prefs } from "../utils/prefs";
+
+export function initialSourcesTreeState() {
+ return {
+ // List of all Thread Tree Items.
+ // All other item types are children of these and aren't store in
+ // the reducer as top level objects.
+ threadItems: [],
+
+ // List of `uniquePath` of Tree Items that are expanded.
+ // This should be all but Source Tree Items.
+ expanded: new Set(),
+
+ // Reference to the currently focused Tree Item.
+ // It can be any type of Tree Item.
+ focusedItem: null,
+
+ // Project root set from the Source Tree.
+ // This focuses the source tree on a subset of sources.
+ // This is a `uniquePath`, where ${thread} is replaced by "top-level"
+ // when we picked an item from the main thread. This allows to preserve
+ // the root selection on page reload.
+ projectDirectoryRoot: prefs.projectDirectoryRoot,
+
+ // The name is displayed in Source Tree header
+ projectDirectoryRootName: prefs.projectDirectoryRootName,
+
+ // Reports if the top level target is a web extension.
+ // If so, we should display all web extension sources.
+ isWebExtension: false,
+
+ /**
+ * Boolean, to be set to true in order to display WebExtension's content scripts
+ * that are applied to the current page we are debugging.
+ *
+ * Covered by: browser_dbg-content-script-sources.js
+ * Bound to: devtools.chrome.enabled
+ *
+ */
+ chromeAndExtensionsEnabled: prefs.chromeAndExtensionsEnabled,
+ };
+}
+
+// eslint-disable-next-line complexity
+export default function update(state = initialSourcesTreeState(), action) {
+ switch (action.type) {
+ case "ADD_ORIGINAL_SOURCES": {
+ const { generatedSourceActor } = action;
+ const validOriginalSources = action.originalSources.filter(source =>
+ isSourceVisibleInSourceTree(
+ source,
+ state.chromeAndExtensionsEnabled,
+ state.isWebExtension
+ )
+ );
+ if (!validOriginalSources.length) {
+ return state;
+ }
+ let changed = false;
+ // Fork the array only once for all the sources
+ const threadItems = [...state.threadItems];
+ for (const source of validOriginalSources) {
+ changed |= addSource(threadItems, source, generatedSourceActor);
+ }
+ if (changed) {
+ return {
+ ...state,
+ threadItems,
+ };
+ }
+ return state;
+ }
+ case "INSERT_SOURCE_ACTORS": {
+ // With this action, we only cover generated sources.
+ // (i.e. we need something else for sourcemapped/original sources)
+ // But we do want to process source actors in order to be able to display
+ // distinct Source Tree Items for sources with the same URL loaded in distinct thread.
+ // (And may be also later be able to highlight the many sources with the same URL loaded in a given thread)
+ const newSourceActors = action.sourceActors.filter(sourceActor =>
+ isSourceVisibleInSourceTree(
+ sourceActor.sourceObject,
+ state.chromeAndExtensionsEnabled,
+ state.isWebExtension
+ )
+ );
+ if (!newSourceActors.length) {
+ return state;
+ }
+ let changed = false;
+ // Fork the array only once for all the sources
+ const threadItems = [...state.threadItems];
+ for (const sourceActor of newSourceActors) {
+ // We mostly wanted to read the thread of the SourceActor,
+ // most of the interesting attributes are on the Source Object.
+ changed |= addSource(
+ threadItems,
+ sourceActor.sourceObject,
+ sourceActor
+ );
+ }
+ if (changed) {
+ return {
+ ...state,
+ threadItems,
+ };
+ }
+ return state;
+ }
+
+ case "INSERT_THREAD":
+ state = { ...state };
+ addThread(state, action.newThread);
+ return state;
+
+ case "REMOVE_THREAD": {
+ const { threadActorID } = action;
+ const index = state.threadItems.findIndex(item => {
+ return item.threadActorID == threadActorID;
+ });
+
+ if (index == -1) {
+ return state;
+ }
+
+ // Also clear focusedItem and expanded items related
+ // to this thread. These fields store uniquePath which starts
+ // with the thread actor ID.
+ let { focusedItem } = state;
+ if (focusedItem && focusedItem.uniquePath.startsWith(threadActorID)) {
+ focusedItem = null;
+ }
+ const expanded = new Set();
+ for (const path of state.expanded) {
+ if (!path.startsWith(threadActorID)) {
+ expanded.add(path);
+ }
+ }
+
+ const threadItems = [...state.threadItems];
+ threadItems.splice(index, 1);
+ return {
+ ...state,
+ threadItems,
+ focusedItem,
+ expanded,
+ };
+ }
+
+ case "SET_EXPANDED_STATE":
+ return updateExpanded(state, action);
+
+ case "SET_FOCUSED_SOURCE_ITEM":
+ return { ...state, focusedItem: action.item };
+
+ case "SET_SELECTED_LOCATION":
+ return updateSelectedLocation(state, action.location);
+
+ case "SET_PROJECT_DIRECTORY_ROOT":
+ const { uniquePath, name } = action;
+ return updateProjectDirectoryRoot(state, uniquePath, name);
+
+ case "BLACKBOX_WHOLE_SOURCES":
+ case "BLACKBOX_SOURCE_RANGES": {
+ const sources = action.sources || [action.source];
+ return updateBlackbox(state, sources, true);
+ }
+
+ case "UNBLACKBOX_WHOLE_SOURCES": {
+ const sources = action.sources || [action.source];
+ return updateBlackbox(state, sources, false);
+ }
+ }
+ return state;
+}
+
+function addThread(state, thread) {
+ const threadActorID = thread.actor;
+ // When processing the top level target,
+ // see if we are debugging an extension.
+ if (thread.isTopLevel) {
+ state.isWebExtension = thread.isWebExtension;
+ }
+ let threadItem = state.threadItems.find(item => {
+ return item.threadActorID == threadActorID;
+ });
+ if (!threadItem) {
+ threadItem = createThreadTreeItem(threadActorID);
+ state.threadItems = [...state.threadItems];
+ threadItem.thread = thread;
+ addSortedItem(state.threadItems, threadItem, sortThreadItems);
+ } else {
+ // We force updating the list to trigger mapStateToProps
+ // as the getSourcesTreeSources selector is awaiting for the `thread` attribute
+ // which we will set here.
+ state.threadItems = [...state.threadItems];
+
+ // Inject the reducer thread object on Thread Tree Items
+ // (this is handy shortcut to have access to from React components)
+ // (this is also used by sortThreadItems to sort the thread as a Tree in the Browser Toolbox)
+ threadItem.thread = thread;
+
+ // We have to re-sort all threads because of the new `thread` attribute on current thread item
+ state.threadItems.sort(sortThreadItems);
+ }
+}
+
+function updateBlackbox(state, sources, shouldBlackBox) {
+ const threadItems = [...state.threadItems];
+
+ for (const source of sources) {
+ for (const threadItem of threadItems) {
+ const sourceTreeItem = findSourceInThreadItem(source, threadItem);
+ if (sourceTreeItem && sourceTreeItem.isBlackBoxed != shouldBlackBox) {
+ // Replace the Item with a clone so that we get the expected React updates
+ const { children } = sourceTreeItem.parent;
+ children.splice(children.indexOf(sourceTreeItem), 1, {
+ ...sourceTreeItem,
+ isBlackBoxed: shouldBlackBox,
+ });
+ }
+ }
+ }
+ return { ...state, threadItems };
+}
+
+function updateExpanded(state, action) {
+ // We receive the full list of all expanded items
+ // (not only the one added/removed)
+ // Also assume that this action is called only if the Set changed.
+ return {
+ ...state,
+ // Consider that the action already cloned the Set
+ expanded: action.expanded,
+ };
+}
+
+/**
+ * Update the project directory root
+ */
+function updateProjectDirectoryRoot(state, uniquePath, name) {
+ // Only persists root within the top level target.
+ // Otherwise the thread actor ID will change on page reload and we won't match anything
+ if (!uniquePath || uniquePath.startsWith("top-level")) {
+ prefs.projectDirectoryRoot = uniquePath;
+ prefs.projectDirectoryRootName = name;
+ }
+
+ return {
+ ...state,
+ projectDirectoryRoot: uniquePath,
+ projectDirectoryRootName: name,
+ };
+}
+
+function isSourceVisibleInSourceTree(
+ source,
+ chromeAndExtensionsEnabled,
+ debuggeeIsWebExtension
+) {
+ return (
+ !!source.url &&
+ !IGNORED_EXTENSIONS.includes(source.displayURL.fileExtension) &&
+ !IGNORED_URLS.includes(source.url) &&
+ !isPretty(source) &&
+ // Only accept web extension sources when the chrome pref is enabled (to allows showing content scripts),
+ // or when we are debugging an extension
+ (!source.isExtension ||
+ chromeAndExtensionsEnabled ||
+ debuggeeIsWebExtension)
+ );
+}
+
+/**
+ * Generic Array helper to add a new value at the right position
+ * given that the array is already sorted.
+ *
+ * @param {Array} array
+ * The already sorted into which a value should be added.
+ * @param {any} newValue
+ * The value to add in the array while keeping the array sorted.
+ * @param {Function} sortFunction
+ * A function to compare two array values and their ordering.
+ * Follow same behavior as Array sorting function.
+ */
+function addSortedItem(array, newValue, sortFunction) {
+ let index = array.findIndex(value => sortFunction(value, newValue) === 1);
+ index = index >= 0 ? index : array.length;
+ array.splice(index, 0, newValue);
+}
+
+function addSource(threadItems, source, sourceActor) {
+ // Ensure creating or fetching the related Thread Item
+ let threadItem = threadItems.find(item => {
+ return item.threadActorID == sourceActor.thread;
+ });
+ if (!threadItem) {
+ threadItem = createThreadTreeItem(sourceActor.thread);
+ // Note that threadItems will be cloned once to force a state update
+ // by the callsite of `addSourceActor`
+ addSortedItem(threadItems, threadItem, sortThreadItems);
+ }
+
+ // Then ensure creating or fetching the related Group Item
+ // About `source` versus `sourceActor`:
+ const { displayURL } = source;
+ const { group } = displayURL;
+
+ let groupItem = threadItem.children.find(item => {
+ return item.groupName == group;
+ });
+
+ if (!groupItem) {
+ groupItem = createGroupTreeItem(group, threadItem, source);
+ // Copy children in order to force updating react in case we picked
+ // this directory as a project root
+ threadItem.children = [...threadItem.children];
+
+ addSortedItem(threadItem.children, groupItem, sortItems);
+ }
+
+ // Then ensure creating or fetching all possibly nested Directory Item(s)
+ const { path } = displayURL;
+ const parentPath = path.substring(0, path.lastIndexOf("/"));
+ const directoryItem = addOrGetParentDirectory(groupItem, parentPath);
+
+ // Check if a previous source actor registered this source.
+ // It happens if we load the same url multiple times, or,
+ // for inline sources (=HTML pages with inline scripts).
+ const existing = directoryItem.children.find(item => {
+ return item.type == "source" && item.source == source;
+ });
+ if (existing) {
+ return false;
+ }
+
+ // Finaly, create the Source Item and register it in its parent Directory Item
+ const sourceItem = createSourceTreeItem(source, sourceActor, directoryItem);
+ // Copy children in order to force updating react in case we picked
+ // this directory as a project root
+ directoryItem.children = [...directoryItem.children];
+ addSortedItem(directoryItem.children, sourceItem, sortItems);
+
+ return true;
+}
+/**
+ * Find all the source items in tree
+ * @param {Object} item - Current item node in the tree
+ * @param {Function} callback
+ */
+function findSourceInThreadItem(source, threadItem) {
+ const { displayURL } = source;
+ const { group, path } = displayURL;
+ const groupItem = threadItem.children.find(item => {
+ return item.groupName == group;
+ });
+ if (!groupItem) return null;
+
+ const parentPath = path.substring(0, path.lastIndexOf("/"));
+ const directoryItem = groupItem._allGroupDirectoryItems.find(item => {
+ return item.type == "directory" && item.path == parentPath;
+ });
+ if (!directoryItem) return null;
+
+ return directoryItem.children.find(item => {
+ return item.type == "source" && item.source == source;
+ });
+}
+
+function sortItems(a, b) {
+ if (a.type == "directory" && b.type == "source") {
+ return -1;
+ } else if (b.type == "directory" && a.type == "source") {
+ return 1;
+ } else if (a.type == "directory" && b.type == "directory") {
+ return a.path.localeCompare(b.path);
+ } else if (a.type == "source" && b.type == "source") {
+ return a.source.displayURL.filename.localeCompare(
+ b.source.displayURL.filename
+ );
+ }
+ return 0;
+}
+
+function sortThreadItems(a, b) {
+ // Jest tests aren't emitting the necessary actions to populate the thread attributes.
+ // Ignore sorting for them.
+ if (!a.thread || !b.thread) {
+ return 0;
+ }
+
+ // Top level target is always listed first
+ if (a.thread.isTopLevel) {
+ return -1;
+ } else if (b.thread.isTopLevel) {
+ return 1;
+ }
+
+ // Process targets should come next and after that frame targets
+ if (a.thread.targetType == "process" && b.thread.targetType == "frame") {
+ return -1;
+ } else if (
+ a.thread.targetType == "frame" &&
+ b.thread.targetType == "process"
+ ) {
+ return 1;
+ }
+
+ // And we display the worker targets last.
+ if (
+ a.thread.targetType.endsWith("worker") &&
+ !b.thread.targetType.endsWith("worker")
+ ) {
+ return 1;
+ } else if (
+ !a.thread.targetType.endsWith("worker") &&
+ b.thread.targetType.endsWith("worker")
+ ) {
+ return -1;
+ }
+
+ // Order the process targets by their process ids
+ if (a.thread.processID > b.thread.processID) {
+ return 1;
+ } else if (a.thread.processID < b.thread.processID) {
+ return 0;
+ }
+
+ // Order the frame targets and the worker targets by their target name
+ if (a.thread.targetType == "frame" && b.thread.targetType == "frame") {
+ return a.thread.name.localeCompare(b.thread.name);
+ } else if (
+ a.thread.targetType.endsWith("worker") &&
+ b.thread.targetType.endsWith("worker")
+ ) {
+ return a.thread.name.localeCompare(b.thread.name);
+ }
+
+ return 0;
+}
+
+/**
+ * For a given URL's path, in the given group (i.e. typically a given scheme+domain),
+ * return the already existing parent directory item, or create it if it doesn't exists.
+ * Note that it will create all ancestors up to the Group Item.
+ *
+ * @param {GroupItem} groupItem
+ * The Group Item for the group where the path should be displayed.
+ * @param {String} path
+ * Path of the directory for which we want a Directory Item.
+ * @return {GroupItem|DirectoryItem}
+ * The parent Item where this path should be inserted.
+ * Note that it may be displayed right under the Group Item if the path is empty.
+ */
+function addOrGetParentDirectory(groupItem, path) {
+ // We reached the top of the Tree, so return the Group Item.
+ if (!path) {
+ return groupItem;
+ }
+ // See if we have this directory already registered by a previous source
+ const existing = groupItem._allGroupDirectoryItems.find(item => {
+ return item.type == "directory" && item.path == path;
+ });
+ if (existing) {
+ return existing;
+ }
+ // It doesn't exists, so we will create a new Directory Item.
+ // But now, lookup recursively for the parent Item for this to-be-create Directory Item
+ const parentPath = path.substring(0, path.lastIndexOf("/"));
+ const parentDirectory = addOrGetParentDirectory(groupItem, parentPath);
+
+ // We can now create the new Directory Item and register it in its parent Item.
+ const directory = createDirectoryTreeItem(path, parentDirectory);
+ // Copy children in order to force updating react in case we picked
+ // this directory as a project root
+ parentDirectory.children = [...parentDirectory.children];
+
+ addSortedItem(parentDirectory.children, directory, sortItems);
+
+ // Also maintain the list of all group items,
+ // Which helps speedup querying for existing items.
+ groupItem._allGroupDirectoryItems.push(directory);
+
+ return directory;
+}
+
+/**
+ * Definition of all Items of a SourceTree
+ */
+// Highlights the attributes that all Source Tree Item should expose
+function createBaseTreeItem({ type, parent, uniquePath, children }) {
+ return {
+ // Can be: thread, group, directory or source
+ type,
+ // Reference to the parent TreeItem
+ parent,
+ // This attribute is used for two things:
+ // * as a string key identified in the React Tree
+ // * for project root in order to find the root in the tree
+ // It is of the form:
+ // `${ThreadActorID}|${GroupName}|${DirectoryPath}|${SourceID}`
+ // Group and path/ID are optional.
+ // `|` is used as separator in order to avoid having this character being used in name/path/IDs.
+ uniquePath,
+ // Array of TreeItem, children of this item.
+ // Will be null for Source Tree Item
+ children,
+ };
+}
+function createThreadTreeItem(thread) {
+ return {
+ ...createBaseTreeItem({
+ type: "thread",
+ // Each thread is considered as an independant root item
+ parent: null,
+ uniquePath: thread,
+ // Children of threads will only be Group Items
+ children: [],
+ }),
+
+ // This will be used to set the reducer's thread object.
+ // This threadActorID attribute isn't meant to be used outside of this selector.
+ // A `thread` attribute will be exposed from INSERT_THREAD action.
+ threadActorID: thread,
+ };
+}
+function createGroupTreeItem(groupName, parent, source) {
+ return {
+ ...createBaseTreeItem({
+ type: "group",
+ parent,
+ uniquePath: `${parent.uniquePath}|${groupName}`,
+ // Children of Group can be Directory and Source items
+ children: [],
+ }),
+
+ groupName,
+
+ // When a content script appear in a web page,
+ // a dedicated group is created for it and should
+ // be having an extension icon.
+ isForExtensionSource: source.isExtension,
+
+ // List of all nested items for this group.
+ // This helps find any nested directory in a given group without having to walk the tree.
+ // This is meant to be used only by the reducer.
+ _allGroupDirectoryItems: [],
+ };
+}
+function createDirectoryTreeItem(path, parent) {
+ // If the parent is a group we want to use '/' as separator
+ const pathSeparator = parent.type == "directory" ? "/" : "|";
+
+ // `path` will be the absolute path from the group/domain,
+ // while we want to append only the directory name in uniquePath.
+ // Also, we need to strip '/' prefix.
+ const relativePath =
+ parent.type == "directory"
+ ? path.replace(parent.path, "").replace(/^\//, "")
+ : path;
+
+ return {
+ ...createBaseTreeItem({
+ type: "directory",
+ parent,
+ uniquePath: `${parent.uniquePath}${pathSeparator}${relativePath}`,
+ // Children can be nested Directory or Source items
+ children: [],
+ }),
+
+ // This is the absolute path from the "group"
+ // i.e. the path from the domain name
+ // For http://mozilla.org/foo/bar folder,
+ // path will be:
+ // foo/bar
+ path,
+ };
+}
+function createSourceTreeItem(source, sourceActor, parent) {
+ return {
+ ...createBaseTreeItem({
+ type: "source",
+ parent,
+ uniquePath: `${parent.uniquePath}|${source.id}`,
+ // Sources items are leaves of the SourceTree
+ children: null,
+ }),
+
+ source,
+ sourceActor,
+ };
+}
+
+/**
+ * Update `expanded` and `focusedItem` so that we show and focus
+ * the new selected source.
+ *
+ * @param {Object} state
+ * @param {Object} selectedLocation
+ * The new location being selected.
+ */
+function updateSelectedLocation(state, selectedLocation) {
+ const sourceItem = getSourceItemForSelectedLocation(state, selectedLocation);
+ if (sourceItem) {
+ // Walk up the tree to expand all ancestor items up to the root of the tree.
+ const expanded = new Set(state.expanded);
+ let parentDirectory = sourceItem;
+ while (parentDirectory) {
+ expanded.add(parentDirectory.uniquePath);
+ parentDirectory = parentDirectory.parent;
+ }
+
+ return {
+ ...state,
+ expanded,
+ focusedItem: sourceItem,
+ };
+ }
+ return state;
+}
+
+/**
+ * Get the SourceItem displayed in the SourceTree for the currently selected location.
+ *
+ * @param {Object} state
+ * @param {Object} selectedLocation
+ * @return {SourceItem}
+ * The directory source item where the given source is displayed.
+ */
+function getSourceItemForSelectedLocation(state, selectedLocation) {
+ const { source, sourceActor } = selectedLocation;
+
+ // Sources without URLs are not visible in the SourceTree
+ if (!source.url) {
+ return null;
+ }
+
+ // In the SourceTree, we never show the pretty printed sources and only
+ // the minified version, so if we are selecting a pretty file, fake selecting
+ // the minified version by looking up for the minified URL instead of the pretty one.
+ const sourceUrl = getRawSourceURL(source.url);
+
+ const { displayURL } = source;
+ function findSourceInItem(item, path) {
+ if (item.type == "source") {
+ if (item.source.url == sourceUrl) {
+ return item;
+ }
+ return null;
+ }
+ // Bail out if the current item doesn't match the source
+ if (item.type == "thread" && item.threadActorID != sourceActor?.thread) {
+ return null;
+ }
+ if (item.type == "group" && displayURL.group != item.groupName) {
+ return null;
+ }
+ if (item.type == "directory" && !path.startsWith(item.path)) {
+ return null;
+ }
+ // Otherwise, walk down the tree if this ancestor item seems to match
+ for (const child of item.children) {
+ const match = findSourceInItem(child, path);
+ if (match) {
+ return match;
+ }
+ }
+
+ return null;
+ }
+ for (const rootItem of state.threadItems) {
+ // Note that when we are setting a project root, rootItem
+ // may no longer be only Thread Item, but also be Group, Directory or Source Items.
+ const item = findSourceInItem(rootItem, displayURL.path);
+ if (item) {
+ return item;
+ }
+ }
+ return null;
+}
diff --git a/devtools/client/debugger/src/reducers/sources.js b/devtools/client/debugger/src/reducers/sources.js
new file mode 100644
index 0000000000..76160b75f2
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/sources.js
@@ -0,0 +1,395 @@
+/* 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/>. */
+
+/**
+ * Sources reducer
+ * @module reducers/sources
+ */
+
+import { originalToGeneratedId } from "devtools/client/shared/source-map-loader/index";
+import { prefs } from "../utils/prefs";
+import { createPendingSelectedLocation } from "../utils/location";
+
+export const UNDEFINED_LOCATION = Symbol("Undefined location");
+export const NO_LOCATION = Symbol("No location");
+
+export function initialSourcesState(state) {
+ /* eslint sort-keys: "error" */
+ return {
+ /**
+ * List of all breakpoint positions for all sources (generated and original).
+ * Map of source id (string) to dictionary object whose keys are line numbers
+ * and values of array of positions.
+ * A position is an object made with two attributes:
+ * location and generatedLocation. Both refering to breakpoint positions
+ * in original and generated sources.
+ * In case of generated source, the two location will be the same.
+ *
+ * Map(source id => Dictionary(int => array<Position>))
+ */
+ mutableBreakpointPositions: new Map(),
+
+ /**
+ * List of all breakable lines for original sources only.
+ *
+ * Map(source id => array<int : breakable line numbers>)
+ */
+ mutableOriginalBreakableLines: new Map(),
+
+ /**
+ * Map of the source id's to one or more related original source id's
+ * Only generated sources which have related original sources will be maintained here.
+ *
+ * Map(source id => array<Original Source ID>)
+ */
+ mutableOriginalSources: new Map(),
+
+ /**
+ * List of override objects whose sources texts have been locally overridden.
+ *
+ * Object { sourceUrl, path }
+ */
+ mutableOverrideSources: state?.mutableOverrideSources || new Map(),
+
+ /**
+ * Mapping of source id's to one or more source-actor's.
+ * Dictionary whose keys are source id's and values are arrays
+ * made of all the related source-actor's.
+ * Note: The source mapped here are only generated sources.
+ *
+ * "source" are the objects stored in this reducer, in the `sources` attribute.
+ * "source-actor" are the objects stored in the "source-actors.js" reducer, in its `sourceActors` attribute.
+ *
+ * Map(source id => array<Source Actor object>)
+ */
+ mutableSourceActors: new Map(),
+
+ /**
+ * All currently available sources.
+ *
+ * See create.js: `createSourceObject` method for the description of stored objects.
+ */
+ mutableSources: new Map(),
+
+ /**
+ * All sources associated with a given URL. When using source maps, multiple
+ * sources can have the same URL.
+ *
+ * Map(url => array<source>)
+ */
+ mutableSourcesPerUrl: new Map(),
+
+ /**
+ * When we want to select a source that isn't available yet, use this.
+ * The location object should have a url attribute instead of a sourceId.
+ *
+ * See `createPendingSelectedLocation` for the definition of this object.
+ */
+ pendingSelectedLocation: prefs.pendingSelectedLocation,
+
+ /**
+ * The actual currently selected location.
+ * Only set if the related source is already registered in the sources reducer.
+ * Otherwise, pendingSelectedLocation should be used. Typically for sources
+ * which are about to be created.
+ *
+ * It also includes line and column information.
+ *
+ * See `createLocation` for the definition of this object.
+ */
+ selectedLocation: undefined,
+
+ /**
+ * When selectedLocation refers to a generated source mapping to an original source
+ * via a source-map, refers to the related original location.
+ *
+ * This is UNDEFINED_LOCATION by default and will switch to NO_LOCATION asynchronously after location
+ * selection if there is no valid original location to map to.
+ */
+ selectedOriginalLocation: UNDEFINED_LOCATION,
+
+ /**
+ * By default, the `selectedLocation` should be highlighted in the editor with a special background.
+ * On demand, this flag can be set to false in order to prevent this.
+ * The location will be shown, but not highlighted.
+ */
+ shouldHighlightSelectedLocation: true,
+
+ /**
+ * By default, if we have a source-mapped source, we would automatically try
+ * to select and show the content of the original source. But, if we explicitly
+ * select a generated source, we remember this choice. That, until we explicitly
+ * select an original source.
+ * Note that selections related to non-source-mapped sources should never
+ * change this setting.
+ */
+ shouldSelectOriginalLocation: true,
+ };
+ /* eslint-disable sort-keys */
+}
+
+function update(state = initialSourcesState(), action) {
+ switch (action.type) {
+ case "ADD_SOURCES":
+ return addSources(state, action.sources);
+
+ case "ADD_ORIGINAL_SOURCES":
+ return addSources(state, action.originalSources);
+
+ case "INSERT_SOURCE_ACTORS":
+ return insertSourceActors(state, action);
+
+ case "SET_SELECTED_LOCATION": {
+ let pendingSelectedLocation = null;
+
+ if (action.location.source.url) {
+ pendingSelectedLocation = createPendingSelectedLocation(
+ action.location
+ );
+ prefs.pendingSelectedLocation = pendingSelectedLocation;
+ }
+
+ return {
+ ...state,
+ selectedLocation: action.location,
+ selectedOriginalLocation: UNDEFINED_LOCATION,
+ pendingSelectedLocation,
+ shouldSelectOriginalLocation: action.shouldSelectOriginalLocation,
+ shouldHighlightSelectedLocation: action.shouldHighlightSelectedLocation,
+ };
+ }
+
+ case "CLEAR_SELECTED_LOCATION": {
+ const pendingSelectedLocation = { url: "" };
+ prefs.pendingSelectedLocation = pendingSelectedLocation;
+
+ return {
+ ...state,
+ selectedLocation: null,
+ selectedOriginalLocation: UNDEFINED_LOCATION,
+ pendingSelectedLocation,
+ };
+ }
+
+ case "SET_ORIGINAL_SELECTED_LOCATION": {
+ if (action.location != state.selectedLocation) {
+ return state;
+ }
+ return {
+ ...state,
+ selectedOriginalLocation: action.originalLocation,
+ };
+ }
+
+ case "SET_PENDING_SELECTED_LOCATION": {
+ const pendingSelectedLocation = {
+ url: action.url,
+ line: action.line,
+ column: action.column,
+ };
+
+ prefs.pendingSelectedLocation = pendingSelectedLocation;
+ return { ...state, pendingSelectedLocation };
+ }
+
+ case "SET_ORIGINAL_BREAKABLE_LINES": {
+ state.mutableOriginalBreakableLines.set(
+ action.source.id,
+ action.breakableLines
+ );
+
+ return {
+ ...state,
+ };
+ }
+
+ case "ADD_BREAKPOINT_POSITIONS": {
+ // Merge existing and new reported position if some where already stored
+ let positions = state.mutableBreakpointPositions.get(action.source.id);
+ if (positions) {
+ positions = { ...positions, ...action.positions };
+ } else {
+ positions = action.positions;
+ }
+
+ state.mutableBreakpointPositions.set(action.source.id, positions);
+
+ return {
+ ...state,
+ };
+ }
+
+ case "REMOVE_THREAD": {
+ return removeSourcesAndActors(state, action);
+ }
+
+ case "SET_OVERRIDE": {
+ state.mutableOverrideSources.set(action.url, action.path);
+ return state;
+ }
+
+ case "REMOVE_OVERRIDE": {
+ if (state.mutableOverrideSources.has(action.url)) {
+ state.mutableOverrideSources.delete(action.url);
+ }
+ return state;
+ }
+ }
+
+ return state;
+}
+
+/*
+ * Add sources to the sources store
+ * - Add the source to the sources store
+ * - Add the source URL to the source url map
+ */
+function addSources(state, sources) {
+ for (const source of sources) {
+ state.mutableSources.set(source.id, source);
+
+ // Update the source url map
+ const existing = state.mutableSourcesPerUrl.get(source.url);
+ if (existing) {
+ // We never return this array from selectors as-is,
+ // we either return the first entry or lookup for a precise entry
+ // so we can mutate it.
+ existing.push(source);
+ } else {
+ state.mutableSourcesPerUrl.set(source.url, [source]);
+ }
+
+ // In case of original source, maintain the mapping of generated source to original sources map.
+ if (source.isOriginal) {
+ const generatedSourceId = originalToGeneratedId(source.id);
+ let originalSourceIds =
+ state.mutableOriginalSources.get(generatedSourceId);
+ if (!originalSourceIds) {
+ originalSourceIds = [];
+ state.mutableOriginalSources.set(generatedSourceId, originalSourceIds);
+ }
+ // We never return this array out of selectors, so mutate the list
+ originalSourceIds.push(source.id);
+ }
+ }
+
+ return { ...state };
+}
+
+function removeSourcesAndActors(state, action) {
+ const {
+ mutableSourcesPerUrl,
+ mutableSources,
+ mutableOriginalSources,
+ mutableSourceActors,
+ mutableOriginalBreakableLines,
+ mutableBreakpointPositions,
+ } = state;
+
+ const newState = { ...state };
+
+ for (const removedSource of action.sources) {
+ const sourceId = removedSource.id;
+
+ // Clear the urls Map
+ const sourceUrl = removedSource.url;
+ if (sourceUrl) {
+ const sourcesForSameUrl = (
+ mutableSourcesPerUrl.get(sourceUrl) || []
+ ).filter(s => s != removedSource);
+ if (!sourcesForSameUrl.length) {
+ // All sources with this URL have been removed
+ mutableSourcesPerUrl.delete(sourceUrl);
+ } else {
+ // There are other sources still alive with the same URL
+ mutableSourcesPerUrl.set(sourceUrl, sourcesForSameUrl);
+ }
+ }
+
+ mutableSources.delete(sourceId);
+
+ // Note that the caller of this method queried the reducer state
+ // to aggregate the related original sources.
+ // So if we were having related original sources, they will be
+ // in `action.sources`.
+ mutableOriginalSources.delete(sourceId);
+
+ // If a source is removed, immediately remove all its related source actors.
+ // It can speed-up the following for loop cleaning actors.
+ mutableSourceActors.delete(sourceId);
+
+ if (removedSource.isOriginal) {
+ mutableOriginalBreakableLines.delete(sourceId);
+ }
+
+ mutableBreakpointPositions.delete(sourceId);
+
+ if (newState.selectedLocation?.source == removedSource) {
+ newState.selectedLocation = null;
+ newState.selectedOriginalLocation = UNDEFINED_LOCATION;
+ }
+ }
+
+ for (const removedActor of action.actors) {
+ const sourceId = removedActor.source;
+ const actorsForSource = mutableSourceActors.get(sourceId);
+ // actors may have already been cleared by the previous for..loop
+ if (!actorsForSource) {
+ continue;
+ }
+ const idx = actorsForSource.indexOf(removedActor);
+ if (idx != -1) {
+ actorsForSource.splice(idx, 1);
+ // While the Map is mutable, we expect new array instance on each new change
+ mutableSourceActors.set(sourceId, [...actorsForSource]);
+ }
+
+ // Remove the entry in the Map if there is no more actors for that source
+ if (!actorsForSource.length) {
+ mutableSourceActors.delete(sourceId);
+ }
+
+ if (newState.selectedLocation?.sourceActor == removedActor) {
+ newState.selectedLocation = null;
+ newState.selectedOriginalLocation = UNDEFINED_LOCATION;
+ }
+ }
+
+ return newState;
+}
+
+function insertSourceActors(state, action) {
+ const { sourceActors } = action;
+
+ const { mutableSourceActors } = state;
+ // The `sourceActor` objects are defined from `newGeneratedSources` action:
+ // https://searchfox.org/mozilla-central/rev/4646b826a25d3825cf209db890862b45fa09ffc3/devtools/client/debugger/src/actions/sources/newSources.js#300-314
+ for (const sourceActor of sourceActors) {
+ const sourceId = sourceActor.source;
+ // We always clone the array of source actors as we return it from selectors.
+ // So the map is mutable, but its values are considered immutable and will change
+ // anytime there is a new actor added per source ID.
+ const existing = mutableSourceActors.get(sourceId);
+ if (existing) {
+ mutableSourceActors.set(sourceId, [...existing, sourceActor]);
+ } else {
+ mutableSourceActors.set(sourceId, [sourceActor]);
+ }
+ }
+
+ const scriptActors = sourceActors.filter(
+ item => item.introductionType === "scriptElement"
+ );
+ if (scriptActors.length) {
+ // If new HTML sources are being added, we need to clear the breakpoint
+ // positions since the new source is a <script> with new breakpoints.
+ for (const { source } of scriptActors) {
+ state.mutableBreakpointPositions.delete(source);
+ }
+ }
+
+ return { ...state };
+}
+
+export default update;
diff --git a/devtools/client/debugger/src/reducers/tabs.js b/devtools/client/debugger/src/reducers/tabs.js
new file mode 100644
index 0000000000..28d1d7b568
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/tabs.js
@@ -0,0 +1,200 @@
+/* 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/>. */
+
+/**
+ * Tabs reducer
+ * @module reducers/tabs
+ */
+
+import { isSimilarTab } from "../utils/tabs";
+
+export function initialTabState() {
+ return { tabs: [] };
+}
+
+function update(state = initialTabState(), action) {
+ switch (action.type) {
+ case "ADD_TAB":
+ return updateTabList(state, action.source, action.sourceActor);
+
+ case "MOVE_TAB":
+ return moveTabInList(state, action);
+
+ case "MOVE_TAB_BY_SOURCE_ID":
+ return moveTabInListBySourceId(state, action);
+
+ case "CLOSE_TABS":
+ return removeSourcesFromTabList(state, action);
+
+ case "ADD_ORIGINAL_SOURCES":
+ return addVisibleTabsForOriginalSources(
+ state,
+ action.originalSources,
+ action.generatedSourceActor
+ );
+
+ case "INSERT_SOURCE_ACTORS":
+ return addVisibleTabsForSourceActors(state, action.sourceActors);
+
+ case "REMOVE_THREAD": {
+ return resetTabsForThread(state, action.threadActorID);
+ }
+
+ default:
+ return state;
+ }
+}
+
+function matchesSource(tab, source) {
+ return tab.source?.id === source.id || matchesUrl(tab, source);
+}
+
+function matchesUrl(tab, source) {
+ return (
+ source.url && tab.url === source.url && tab.isOriginal == source.isOriginal
+ );
+}
+
+function addVisibleTabsForSourceActors(state, sourceActors) {
+ let changed = false;
+ // Lookups for tabs matching any source actor's URL
+ // and reference their source and sourceActor attribute
+ // so that the tab becomes visible.
+ const tabs = state.tabs.map(tab => {
+ const sourceActor = sourceActors.find(actor =>
+ matchesUrl(tab, actor.sourceObject)
+ );
+ if (!sourceActor) {
+ return tab;
+ }
+ changed = true;
+ return {
+ ...tab,
+ source: sourceActor.sourceObject,
+ sourceActor,
+ };
+ });
+
+ return changed ? { tabs } : state;
+}
+
+function addVisibleTabsForOriginalSources(
+ state,
+ sources,
+ generatedSourceActor
+) {
+ let changed = false;
+
+ // Lookups for tabs matching any source's URL
+ // and reference their source and sourceActor attribute
+ // so that the tab becomes visible.
+ const tabs = state.tabs.map(tab => {
+ const source = sources.find(s => matchesUrl(tab, s));
+ if (!source) {
+ return tab;
+ }
+ changed = true;
+ return {
+ ...tab,
+ source,
+ // All currently reported original sources are related to a single source actor
+ sourceActor: generatedSourceActor,
+ };
+ });
+
+ return changed ? { tabs } : state;
+}
+
+function removeSourcesFromTabList(state, { sources }) {
+ const newTabs = sources.reduce(
+ (tabList, source) => tabList.filter(tab => !matchesSource(tab, source)),
+ state.tabs
+ );
+ if (newTabs.length == state.tabs.length) {
+ return state;
+ }
+
+ return { tabs: newTabs };
+}
+
+function resetTabsForThread(state, threadActorID) {
+ let changed = false;
+ // Nullify source and sourceActor attributes of all tabs
+ // related to the given thread so that they become hidden.
+ //
+ // They may later be restored if a source matches their URL again.
+ // This is similar to persistTabs, but specific to a unique thread.
+ const tabs = state.tabs.map(tab => {
+ if (tab.sourceActor?.thread != threadActorID) {
+ return tab;
+ }
+ changed = true;
+ return {
+ ...tab,
+ source: null,
+ sourceActor: null,
+ };
+ });
+
+ return changed ? { tabs } : state;
+}
+
+/**
+ * Adds the new source to the tab list if it is not already there.
+ */
+function updateTabList(state, source, sourceActor) {
+ const { url } = source;
+ const isOriginal = source.isOriginal;
+
+ let { tabs } = state;
+ // Set currentIndex to -1 for URL-less tabs so that they aren't
+ // filtered by isSimilarTab
+ const currentIndex = url
+ ? tabs.findIndex(tab => isSimilarTab(tab, url, isOriginal))
+ : -1;
+
+ if (currentIndex === -1) {
+ const newTab = {
+ url,
+ source,
+ isOriginal,
+ sourceActor,
+ };
+ // New tabs are added first in the list
+ tabs = [newTab, ...tabs];
+ } else {
+ return state;
+ }
+
+ return { ...state, tabs };
+}
+
+function moveTabInList(state, { url, tabIndex: newIndex }) {
+ const currentIndex = state.tabs.findIndex(tab => tab.url == url);
+ return moveTab(state, currentIndex, newIndex);
+}
+
+function moveTabInListBySourceId(state, { sourceId, tabIndex: newIndex }) {
+ const currentIndex = state.tabs.findIndex(tab => tab.source?.id == sourceId);
+ return moveTab(state, currentIndex, newIndex);
+}
+
+function moveTab(state, currentIndex, newIndex) {
+ const { tabs } = state;
+ const item = tabs[currentIndex];
+ // Avoid any state change if we are on the same position or the new is invalid
+ if (currentIndex == newIndex || isNaN(newIndex)) {
+ return state;
+ }
+
+ const newTabs = Array.from(tabs);
+ // Remove the item from its current location
+ newTabs.splice(currentIndex, 1);
+ // And add it to the new one
+ newTabs.splice(newIndex, 0, item);
+
+ return { tabs: newTabs };
+}
+
+export default update;
diff --git a/devtools/client/debugger/src/reducers/tests/breakpoints.spec.js b/devtools/client/debugger/src/reducers/tests/breakpoints.spec.js
new file mode 100644
index 0000000000..043c59dc5f
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/tests/breakpoints.spec.js
@@ -0,0 +1,73 @@
+/* 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 { initialBreakpointsState } from "../breakpoints";
+import { getBreakpointsForSource } from "../../selectors/breakpoints";
+
+import { makeMockBreakpoint, makeMockSource } from "../../utils/test-mockup";
+
+function initializeStateWith(data) {
+ const state = initialBreakpointsState();
+ state.breakpoints = data;
+ return state;
+}
+
+describe("Breakpoints Selectors", () => {
+ it("gets a breakpoint for an original source", () => {
+ const sourceId = "server1.conn1.child1/source1/originalSource";
+ const source = makeMockSource(undefined, sourceId);
+ const matchingBreakpoints = {
+ id1: makeMockBreakpoint(source, 1),
+ };
+
+ const otherBreakpoints = {
+ id2: makeMockBreakpoint(makeMockSource(undefined, "not-this-source"), 1),
+ };
+
+ const data = {
+ ...matchingBreakpoints,
+ ...otherBreakpoints,
+ };
+
+ const breakpoints = initializeStateWith(data);
+ const allBreakpoints = Object.values(matchingBreakpoints);
+ const sourceBreakpoints = getBreakpointsForSource({ breakpoints }, source);
+
+ expect(sourceBreakpoints).toEqual(allBreakpoints);
+ expect(sourceBreakpoints[0] === allBreakpoints[0]).toBe(true);
+ });
+
+ it("gets a breakpoint for a generated source", () => {
+ const generatedSourceId = "random-source";
+ const generatedSource = makeMockSource(undefined, generatedSourceId);
+ const matchingBreakpoints = {
+ id1: {
+ ...makeMockBreakpoint(generatedSource, 1),
+ location: { line: 1, source: { id: "original-source-id-1" } },
+ },
+ };
+
+ const otherBreakpoints = {
+ id2: {
+ ...makeMockBreakpoint(makeMockSource(undefined, "not-this-source"), 1),
+ location: { line: 1, source: { id: "original-source-id-2" } },
+ },
+ };
+
+ const data = {
+ ...matchingBreakpoints,
+ ...otherBreakpoints,
+ };
+
+ const breakpoints = initializeStateWith(data);
+
+ const allBreakpoints = Object.values(matchingBreakpoints);
+ const sourceBreakpoints = getBreakpointsForSource(
+ { breakpoints },
+ generatedSource
+ );
+
+ expect(sourceBreakpoints).toEqual(allBreakpoints);
+ });
+});
diff --git a/devtools/client/debugger/src/reducers/tests/quick-open.spec.js b/devtools/client/debugger/src/reducers/tests/quick-open.spec.js
new file mode 100644
index 0000000000..cd91b040be
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/tests/quick-open.spec.js
@@ -0,0 +1,59 @@
+/* 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 update, { initialQuickOpenState } from "../quick-open";
+import {
+ getQuickOpenEnabled,
+ getQuickOpenQuery,
+ getQuickOpenType,
+} from "../../selectors/quick-open";
+import {
+ setQuickOpenQuery,
+ openQuickOpen,
+ closeQuickOpen,
+} from "../../actions/quick-open";
+
+describe("quickOpen reducer", () => {
+ test("initial state", () => {
+ const state = update(undefined, { type: "FAKE" });
+ expect(getQuickOpenQuery({ quickOpen: state })).toEqual("");
+ expect(getQuickOpenType({ quickOpen: state })).toEqual("sources");
+ });
+
+ test("opens the quickOpen modal", () => {
+ const state = update(initialQuickOpenState(), openQuickOpen());
+ expect(getQuickOpenEnabled({ quickOpen: state })).toEqual(true);
+ });
+
+ test("closes the quickOpen modal", () => {
+ let state = update(initialQuickOpenState(), openQuickOpen());
+ expect(getQuickOpenEnabled({ quickOpen: state })).toEqual(true);
+ state = update(initialQuickOpenState(), closeQuickOpen());
+ expect(getQuickOpenEnabled({ quickOpen: state })).toEqual(false);
+ });
+
+ test("leaves query alone on open if not provided", () => {
+ const state = update(initialQuickOpenState(), openQuickOpen());
+ expect(getQuickOpenQuery({ quickOpen: state })).toEqual("");
+ expect(getQuickOpenType({ quickOpen: state })).toEqual("sources");
+ });
+
+ test("set query on open if provided", () => {
+ const state = update(initialQuickOpenState(), openQuickOpen("@"));
+ expect(getQuickOpenQuery({ quickOpen: state })).toEqual("@");
+ expect(getQuickOpenType({ quickOpen: state })).toEqual("functions");
+ });
+
+ test("clear query on close", () => {
+ const state = update(initialQuickOpenState(), closeQuickOpen());
+ expect(getQuickOpenQuery({ quickOpen: state })).toEqual("");
+ expect(getQuickOpenType({ quickOpen: state })).toEqual("sources");
+ });
+
+ test("sets the query to the provided string", () => {
+ const state = update(initialQuickOpenState(), setQuickOpenQuery("test"));
+ expect(getQuickOpenQuery({ quickOpen: state })).toEqual("test");
+ expect(getQuickOpenType({ quickOpen: state })).toEqual("sources");
+ });
+});
diff --git a/devtools/client/debugger/src/reducers/tests/ui.spec.js b/devtools/client/debugger/src/reducers/tests/ui.spec.js
new file mode 100644
index 0000000000..0be451429f
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/tests/ui.spec.js
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+import { prefs } from "../../utils/prefs";
+import update, { initialUIState } from "../ui";
+
+describe("ui reducer", () => {
+ it("toggle framework grouping to false", () => {
+ const state = initialUIState();
+ const value = false;
+ const updatedState = update(state, {
+ type: "TOGGLE_FRAMEWORK_GROUPING",
+ value,
+ });
+ expect(updatedState.frameworkGroupingOn).toBe(value);
+ expect(prefs.frameworkGroupingOn).toBe(value);
+ });
+
+ it("toggle framework grouping to true", () => {
+ const state = initialUIState();
+ const value = true;
+ const updatedState = update(state, {
+ type: "TOGGLE_FRAMEWORK_GROUPING",
+ value,
+ });
+ expect(updatedState.frameworkGroupingOn).toBe(value);
+ expect(prefs.frameworkGroupingOn).toBe(value);
+ });
+});
diff --git a/devtools/client/debugger/src/reducers/threads.js b/devtools/client/debugger/src/reducers/threads.js
new file mode 100644
index 0000000000..0131c6c7e8
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/threads.js
@@ -0,0 +1,69 @@
+/* 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/>. */
+
+/**
+ * Threads reducer
+ * @module reducers/threads
+ */
+
+export function initialThreadsState() {
+ return {
+ threads: [],
+
+ // List of thread actor IDs which are current tracing.
+ // i.e. where JavaScript tracing is enabled.
+ mutableTracingThreads: new Set(),
+ };
+}
+
+export default function update(state = initialThreadsState(), action) {
+ switch (action.type) {
+ case "INSERT_THREAD":
+ return {
+ ...state,
+ threads: [...state.threads, action.newThread],
+ };
+
+ case "REMOVE_THREAD":
+ return {
+ ...state,
+ threads: state.threads.filter(
+ thread => action.threadActorID != thread.actor
+ ),
+ };
+
+ case "UPDATE_SERVICE_WORKER_STATUS":
+ return {
+ ...state,
+ threads: state.threads.map(t => {
+ if (t.actor == action.thread) {
+ return { ...t, serviceWorkerStatus: action.status };
+ }
+ return t;
+ }),
+ };
+
+ case "TRACING_TOGGLED":
+ const { mutableTracingThreads } = state;
+ const sizeBefore = mutableTracingThreads.size;
+ if (action.enabled) {
+ mutableTracingThreads.add(action.thread);
+ } else {
+ mutableTracingThreads.delete(action.thread);
+ }
+ // We may receive toggle events when we change the logging method
+ // while we are already tracing, but the list of tracing thread stays the same.
+ const changed = mutableTracingThreads.size != sizeBefore;
+ if (changed) {
+ return {
+ ...state,
+ mutableTracingThreads,
+ };
+ }
+ return state;
+
+ default:
+ return state;
+ }
+}
diff --git a/devtools/client/debugger/src/reducers/ui.js b/devtools/client/debugger/src/reducers/ui.js
new file mode 100644
index 0000000000..7f37d2f54f
--- /dev/null
+++ b/devtools/client/debugger/src/reducers/ui.js
@@ -0,0 +1,253 @@
+/* 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/>. */
+
+/* eslint-disable complexity */
+
+/**
+ * UI reducer
+ * @module reducers/ui
+ */
+
+import { prefs, features } from "../utils/prefs";
+import { searchKeys } from "../constants";
+
+export const initialUIState = () => ({
+ selectedPrimaryPaneTab: "sources",
+ activeSearch: null,
+ startPanelCollapsed: prefs.startPanelCollapsed,
+ endPanelCollapsed: prefs.endPanelCollapsed,
+ frameworkGroupingOn: prefs.frameworkGroupingOn,
+
+ // This is used from Outline's copy to clipboard context menu
+ // and QuickOpen to highlight lines temporarily.
+ // If defined, it will be an object with following attributes:
+ // - sourceId, String
+ // - start, Number, start line to highlight, 1-based
+ // - end, Number, end line to highlight, 1-based
+ highlightedLineRange: null,
+
+ conditionalPanelLocation: null,
+ isLogPoint: false,
+ orientation: "horizontal",
+ viewport: null,
+ cursorPosition: null,
+ inlinePreviewEnabled: features.inlinePreview,
+ editorWrappingEnabled: prefs.editorWrapping,
+ javascriptEnabled: true,
+ javascriptTracingEnabled: false,
+ javascriptTracingLogMethod: prefs.javascriptTracingLogMethod,
+ javascriptTracingValues: prefs.javascriptTracingValues,
+ javascriptTracingOnNextInteraction: prefs.javascriptTracingOnNextInteraction,
+ javascriptTracingOnNextLoad: prefs.javascriptTracingOnNextLoad,
+ javascriptTracingFunctionReturn: prefs.javascriptTracingFunctionReturn,
+ mutableSearchOptions: prefs.searchOptions || {
+ [searchKeys.FILE_SEARCH]: {
+ regexMatch: false,
+ wholeWord: false,
+ caseSensitive: false,
+ excludePatterns: "",
+ },
+ [searchKeys.PROJECT_SEARCH]: {
+ regexMatch: false,
+ wholeWord: false,
+ caseSensitive: false,
+ excludePatterns: "",
+ },
+ [searchKeys.QUICKOPEN_SEARCH]: {
+ regexMatch: false,
+ wholeWord: false,
+ caseSensitive: false,
+ excludePatterns: "",
+ },
+ },
+ projectSearchQuery: "",
+ hideIgnoredSources: prefs.hideIgnoredSources,
+ sourceMapIgnoreListEnabled: prefs.sourceMapIgnoreListEnabled,
+});
+
+function update(state = initialUIState(), action) {
+ switch (action.type) {
+ case "TOGGLE_ACTIVE_SEARCH": {
+ return { ...state, activeSearch: action.value };
+ }
+
+ case "TOGGLE_FRAMEWORK_GROUPING": {
+ prefs.frameworkGroupingOn = action.value;
+ return { ...state, frameworkGroupingOn: action.value };
+ }
+
+ case "TOGGLE_INLINE_PREVIEW": {
+ features.inlinePreview = action.value;
+ return { ...state, inlinePreviewEnabled: action.value };
+ }
+
+ case "TOGGLE_EDITOR_WRAPPING": {
+ prefs.editorWrapping = action.value;
+ return { ...state, editorWrappingEnabled: action.value };
+ }
+
+ case "TOGGLE_JAVASCRIPT_ENABLED": {
+ return { ...state, javascriptEnabled: action.value };
+ }
+
+ case "TOGGLE_SOURCE_MAPS_ENABLED": {
+ prefs.clientSourceMapsEnabled = action.value;
+ return { ...state };
+ }
+
+ case "SET_ORIENTATION": {
+ return { ...state, orientation: action.orientation };
+ }
+
+ case "TOGGLE_PANE": {
+ if (action.position == "start") {
+ prefs.startPanelCollapsed = action.paneCollapsed;
+ return { ...state, startPanelCollapsed: action.paneCollapsed };
+ }
+
+ prefs.endPanelCollapsed = action.paneCollapsed;
+ return { ...state, endPanelCollapsed: action.paneCollapsed };
+ }
+
+ case "HIGHLIGHT_LINES": {
+ return { ...state, highlightedLineRange: action.location };
+ }
+
+ case "CLOSE_QUICK_OPEN":
+ case "CLEAR_HIGHLIGHT_LINES":
+ if (!state.highlightedLineRange) {
+ return state;
+ }
+ return { ...state, highlightedLineRange: null };
+
+ case "OPEN_CONDITIONAL_PANEL":
+ return {
+ ...state,
+ conditionalPanelLocation: action.location,
+ isLogPoint: action.log,
+ };
+
+ case "CLOSE_CONDITIONAL_PANEL":
+ return { ...state, conditionalPanelLocation: null };
+
+ case "SET_PRIMARY_PANE_TAB":
+ return { ...state, selectedPrimaryPaneTab: action.tabName };
+
+ case "CLOSE_PROJECT_SEARCH": {
+ if (state.activeSearch === "project") {
+ return { ...state, activeSearch: null };
+ }
+ return state;
+ }
+
+ case "SET_VIEWPORT": {
+ return { ...state, viewport: action.viewport };
+ }
+
+ case "SET_CURSOR_POSITION": {
+ return { ...state, cursorPosition: action.cursorPosition };
+ }
+
+ case "NAVIGATE": {
+ return { ...state, highlightedLineRange: null };
+ }
+
+ case "REMOVE_THREAD": {
+ // Reset the highlighted range if the related source has been removed
+ const sourceId = state.highlightedLineRange?.sourceId;
+ if (sourceId && action.sources.some(s => s.id == sourceId)) {
+ return { ...state, highlightedLineRange: null };
+ }
+ return state;
+ }
+
+ case "TOGGLE_TRACING": {
+ if (action.status === "start") {
+ return { ...state, javascriptTracingEnabled: action.enabled };
+ }
+ return state;
+ }
+
+ case "SET_JAVASCRIPT_TRACING_LOG_METHOD": {
+ prefs.javascriptTracingLogMethod = action.value;
+ return { ...state, javascriptTracingLogMethod: action.value };
+ }
+
+ case "TOGGLE_JAVASCRIPT_TRACING_VALUES": {
+ prefs.javascriptTracingValues = !prefs.javascriptTracingValues;
+ return {
+ ...state,
+ javascriptTracingValues: prefs.javascriptTracingValues,
+ };
+ }
+
+ case "TOGGLE_JAVASCRIPT_TRACING_ON_NEXT_INTERACTION": {
+ prefs.javascriptTracingOnNextInteraction =
+ !prefs.javascriptTracingOnNextInteraction;
+ return {
+ ...state,
+ javascriptTracingOnNextInteraction:
+ prefs.javascriptTracingOnNextInteraction,
+ };
+ }
+
+ case "TOGGLE_JAVASCRIPT_TRACING_ON_NEXT_LOAD": {
+ prefs.javascriptTracingOnNextLoad = !prefs.javascriptTracingOnNextLoad;
+ return {
+ ...state,
+ javascriptTracingOnNextLoad: prefs.javascriptTracingOnNextLoad,
+ };
+ }
+
+ case "TOGGLE_JAVASCRIPT_TRACING_FUNCTION_RETURN": {
+ prefs.javascriptTracingFunctionReturn =
+ !prefs.javascriptTracingFunctionReturn;
+ return {
+ ...state,
+ javascriptTracingFunctionReturn: prefs.javascriptTracingFunctionReturn,
+ };
+ }
+
+ case "SET_SEARCH_OPTIONS": {
+ state.mutableSearchOptions[action.searchKey] = {
+ ...state.mutableSearchOptions[action.searchKey],
+ ...action.searchOptions,
+ };
+ prefs.searchOptions = state.mutableSearchOptions;
+ return { ...state };
+ }
+
+ case "SET_PROJECT_SEARCH_QUERY": {
+ if (action.query != state.projectSearchQuery) {
+ state.projectSearchQuery = action.query;
+ return { ...state };
+ }
+ return state;
+ }
+
+ case "HIDE_IGNORED_SOURCES": {
+ const { shouldHide } = action;
+ if (shouldHide !== state.hideIgnoredSources) {
+ prefs.hideIgnoredSources = shouldHide;
+ return { ...state, hideIgnoredSources: shouldHide };
+ }
+ return state;
+ }
+
+ case "ENABLE_SOURCEMAP_IGNORELIST": {
+ const { shouldEnable } = action;
+ if (shouldEnable !== state.sourceMapIgnoreListEnabled) {
+ prefs.sourceMapIgnoreListEnabled = shouldEnable;
+ return { ...state, sourceMapIgnoreListEnabled: shouldEnable };
+ }
+ return state;
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
+
+export default update;
diff --git a/devtools/client/debugger/src/selectors/ast.js b/devtools/client/debugger/src/selectors/ast.js
new file mode 100644
index 0000000000..4e2995d95c
--- /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/index";
+
+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..a542d86aeb
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/breakpointAtLocation.js
@@ -0,0 +1,120 @@
+/* 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";
+
+function getColumn(column, selectedSource) {
+ if (column) {
+ return column;
+ }
+
+ return !selectedSource.isOriginal ? undefined : 0;
+}
+
+function getLocation(bp, selectedSource) {
+ return !selectedSource.isOriginal
+ ? 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.source.id === 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..a0daedae21
--- /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 "devtools/client/shared/vendor/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..29b8fbdd42
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/breakpoints.js
@@ -0,0 +1,84 @@
+/* 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 "devtools/client/shared/vendor/reselect";
+
+import { makeBreakpointId } from "../utils/breakpoint/index";
+
+// 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} source
+ * @param {Number|Object} lines - line or an object with a start and end range of lines
+ * @returns {Array} breakpoints
+ */
+export function getBreakpointsForSource(state, source, lines) {
+ if (!source) {
+ return [];
+ }
+
+ const breakpoints = getBreakpointsList(state);
+ return breakpoints.filter(bp => {
+ const location = source.isOriginal ? bp.location : bp.generatedLocation;
+
+ if (lines) {
+ const isOnLineOrWithinRange =
+ typeof lines == "number"
+ ? location.line == lines
+ : location.line >= lines.start.line &&
+ location.line <= lines.end.line;
+ return location.source === source && isOnLineOrWithinRange;
+ }
+ return location.source === source;
+ });
+}
+
+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..f5a1a3f074
--- /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 "devtools/client/shared/vendor/reselect";
+import { shallowEqual, arrayShallowEqual } from "../utils/shallow-equal";
+
+import { getSelectedSource, getSourceActorsForSource } from "./index";
+
+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..085aa63033
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/expressions.js
@@ -0,0 +1,29 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+import { createSelector } from "devtools/client/shared/vendor/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];
+}
diff --git a/devtools/client/debugger/src/selectors/index.js b/devtools/client/debugger/src/selectors/index.js
new file mode 100644
index 0000000000..1c045ae6ad
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/index.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/>. */
+
+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 { isLineInScope } from "./isLineInScope";
+export { isSelectedFrameVisible } from "./isSelectedFrameVisible";
+export * from "./pause";
+export * from "./pending-breakpoints";
+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..f4d42ccc1d
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/isSelectedFrameVisible.js
@@ -0,0 +1,37 @@
+/* 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 } from "devtools/client/shared/source-map-loader/index";
+import { getSelectedLocation } from "./sources";
+import { getCurrentThread, getSelectedFrame } from "./pause";
+
+function getGeneratedId(source) {
+ if (source.isOriginal) {
+ return originalToGeneratedId(source.id);
+ }
+
+ return source.id;
+}
+
+/*
+ * 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 (selectedLocation.source.isOriginal) {
+ return selectedLocation.source.id === selectedFrame.location.source.id;
+ }
+
+ return (
+ selectedLocation.source.id === getGeneratedId(selectedFrame.location.source)
+ );
+}
diff --git a/devtools/client/debugger/src/selectors/moz.build b/devtools/client/debugger/src/selectors/moz.build
new file mode 100644
index 0000000000..ac728b1de4
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/moz.build
@@ -0,0 +1,32 @@
+# 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",
+ "index.js",
+ "isLineInScope.js",
+ "isSelectedFrameVisible.js",
+ "pause.js",
+ "pending-breakpoints.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..a1bccda7c4
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/pause.js
@@ -0,0 +1,293 @@
+/* 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 { getSelectedSource, getSelectedLocation } from "./sources";
+import { getBlackBoxRanges } from "./source-blackbox";
+
+// eslint-disable-next-line
+import { getSelectedLocation as _getSelectedLocation } from "../utils/selected-location";
+import { isFrameBlackBoxed } from "../utils/source";
+import { createSelector } from "devtools/client/shared/vendor/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 getNavigateCounter(state) {
+ return state.pause.threadcx.navigateCounter;
+}
+
+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 getShouldPauseOnDebuggerStatement(state) {
+ return state.pause.shouldPauseOnDebuggerStatement;
+}
+
+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 const getCurrentThreadFrames = createSelector(
+ state => {
+ const { frames, framesLoading } = getThreadPauseState(
+ state.pause,
+ getCurrentThread(state)
+ );
+ if (framesLoading) {
+ return [];
+ }
+ return frames;
+ },
+ getBlackBoxRanges,
+ (frames, blackboxedRanges) => {
+ return frames.filter(frame => !isFrameBlackBoxed(frame, blackboxedRanges));
+ }
+);
+
+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, frame) {
+ if (!frame) {
+ return null;
+ }
+ return getFrameScopes(state, frame.thread).generated[
+ getGeneratedFrameId(frame.id)
+ ];
+}
+
+export function getOriginalFrameScope(state, frame) {
+ if (!frame) {
+ return null;
+ }
+ // Only compute original scope if we are currently showing an original source.
+ const source = getSelectedSource(state);
+ if (!source || !source.isOriginal) {
+ return null;
+ }
+
+ const original = getFrameScopes(state, frame.thread).original[
+ getGeneratedFrameId(frame.id)
+ ];
+
+ if (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, frame) {
+ return (
+ getOriginalFrameScope(state, frame) || getGeneratedFrameScope(state, frame)
+ );
+}
+
+// This is only used by tests
+export function getSelectedScope(state, thread) {
+ const frame = getSelectedFrame(state, thread);
+
+ const frameScope = getFrameScope(state, frame);
+ if (!frameScope) {
+ return null;
+ }
+
+ return frameScope.scope || null;
+}
+
+export function getSelectedOriginalScope(state, thread) {
+ const frame = getSelectedFrame(state, thread);
+ return getOriginalFrameScope(state, frame);
+}
+
+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];
+}
+
+// getTopFrame wouldn't return the top frame if the frames are still being fetched
+export function getCurrentlyFetchedTopFrame(state, thread) {
+ const { frames } = getThreadPauseState(state.pause, thread);
+ return frames?.[0];
+}
+
+export function hasFrame(state, frame) {
+ // Don't use getFrames as it returns null when the frames are still loading
+ const { frames } = getThreadPauseState(state.pause, frame.thread);
+ if (!frames) {
+ return false;
+ }
+ // Compare IDs and not frame objects as they get cloned during mapping
+ return frames.some(f => f.id == frame.id);
+}
+
+export function getSkipPausing(state) {
+ return state.pause.skipPausing;
+}
+
+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/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..3c9c08dc1d
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/source-actors.js
@@ -0,0 +1,119 @@
+/* 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);
+}
+
+export function getSourceMapErrorForSourceActor(state, sourceActorId) {
+ return state.sourceActors.mutableSourceMapErrors.get(sourceActorId);
+}
+
+export function getSourceMapResolvedURL(state, sourceActorId) {
+ return state.sourceActors.mutableResolvedSourceMapURL.get(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..8fbf161397
--- /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 "devtools/client/shared/vendor/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..938eae9fe2
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/sources.js
@@ -0,0 +1,410 @@
+/* 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 "devtools/client/shared/vendor/reselect";
+
+import { getPrettySourceURL, 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 { UNDEFINED_LOCATION, NO_LOCATION } from "../reducers/sources";
+
+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 (!source.isOriginal) {
+ 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;
+}
+
+/**
+ * Return the "mapped" location for the currently selected location:
+ * - When selecting a location in an original source, returns
+ * the related location in the bundle source.
+ *
+ * - When selecting a location in a bundle source, returns
+ * the related location in the original source. This may return undefined
+ * while we are still computing this information. (we need to query the asynchronous SourceMap service)
+ *
+ * - Otherwise, when selecting a location in a source unrelated to source map
+ * or a pretty printed source, returns null.
+ */
+export function getSelectedMappedSource(state) {
+ const selectedLocation = getSelectedLocation(state);
+ if (!selectedLocation) {
+ return null;
+ }
+
+ // Don't map pretty printed to its related compressed source
+ if (selectedLocation.source.isPrettyPrinted) {
+ return null;
+ }
+
+ // If we are on a bundle with a functional source-map,
+ // the `selectLocation` action should compute the `selectedOriginalLocation` field.
+ if (
+ !selectedLocation.source.isOriginal &&
+ isSourceActorWithSourceMap(state, selectedLocation.sourceActor.id)
+ ) {
+ const { selectedOriginalLocation } = state.sources;
+ // Return undefined if we are still loading the source map.
+ // `selectedOriginalLocation` will be set to undefined instead of null
+ if (
+ selectedOriginalLocation &&
+ selectedOriginalLocation != UNDEFINED_LOCATION &&
+ selectedOriginalLocation != NO_LOCATION
+ ) {
+ return selectedOriginalLocation.source;
+ }
+ return null;
+ }
+
+ const mappedSource = getGeneratedSource(state, selectedLocation.source);
+ // getGeneratedSource will return the exact same source object on sources
+ // that don't map to any original source. In this case, return null
+ // as that's most likely a regular source, not using source maps.
+ if (mappedSource == selectedLocation.source) {
+ return null;
+ }
+ return mappedSource || null;
+}
+
+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;
+}
+
+export function getShouldHighlightSelectedLocation(state) {
+ return state.sources.shouldHighlightSelectedLocation;
+}
+
+/**
+ * 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.source.id;
+ 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.source.id;
+ 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..822bb7f49a
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/tabs.js
@@ -0,0 +1,89 @@
+/* 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 "devtools/client/shared/vendor/reselect";
+import { getPrettySourceURL } from "../utils/source";
+
+import { getSpecificSourceByURL } from "./sources";
+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, source) {
+ const prettyUrl = getPrettySourceURL(source.url);
+ return getTabs(state).some(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, selectedSource.isOriginal)
+ );
+
+ 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..e4b18538cf
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/test/__snapshots__/visibleColumnBreakpoints.spec.js.snap
@@ -0,0 +1,269 @@
+// 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 {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "foo",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "id": "breakpoint",
+ "location": Object {
+ "column": 1,
+ "line": 1,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "foo",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "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 {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "foo",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "id": "breakpoint",
+ "location": Object {
+ "column": 1,
+ "line": 1,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "foo",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "options": Object {},
+ "originalText": "text",
+ "text": "text",
+ },
+ "location": Object {
+ "column": 1,
+ "line": 1,
+ "sourceId": "foo",
+ },
+ },
+ Object {
+ "breakpoint": undefined,
+ "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 {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "foo",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "id": "breakpoint",
+ "location": Object {
+ "column": 1,
+ "line": 1,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "foo",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "options": Object {},
+ "originalText": "text",
+ "text": "text",
+ },
+ "location": Object {
+ "column": 1,
+ "line": 1,
+ "sourceId": "foo",
+ },
+ },
+ Object {
+ "breakpoint": undefined,
+ "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 {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "foo",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "id": "breakpoint",
+ "location": Object {
+ "column": 1,
+ "line": 1,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "url",
+ "group": "",
+ "path": "url",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "foo",
+ "isExtension": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "thread": "FakeThread",
+ "url": "url",
+ },
+ },
+ "options": Object {},
+ "originalText": "text",
+ "text": "text",
+ },
+ "location": Object {
+ "column": 1,
+ "line": 1,
+ "sourceId": "foo",
+ },
+ },
+ Object {
+ "breakpoint": undefined,
+ "location": Object {
+ "column": 5,
+ "line": 1,
+ "sourceId": "foo",
+ },
+ },
+]
+`;
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..873bf35ae2
--- /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 = { 1: [pp(1, 1), pp(1, 5)], 3: [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 = { 1: [pp(1, 1), pp(1, 3)], 2: [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 = { 1: [pp(1, 1), pp(1, 3)], 20: [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 = { 1: [pp(1, 1), pp(1, 15)], 20: [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..931af555a3
--- /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 "devtools/client/shared/vendor/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..4780b5e051
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/ui.js
@@ -0,0 +1,117 @@
+/* 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 { getIsThreadCurrentlyTracing, getAllThreads } from "./threads";
+
+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 getIsJavascriptTracingEnabled(state) {
+ // Check for the global state which may be set by debugger toggling,
+ // but also on individual thread state which will be set by the `:trace` console command.
+ return (
+ state.ui.javascriptTracingEnabled ||
+ getAllThreads(state).some(thread =>
+ getIsThreadCurrentlyTracing(state, thread.actor)
+ )
+ );
+}
+
+export function getJavascriptTracingLogMethod(state) {
+ return state.ui.javascriptTracingLogMethod;
+}
+
+export function getJavascriptTracingValues(state) {
+ return state.ui.javascriptTracingValues;
+}
+
+export function getJavascriptTracingOnNextInteraction(state) {
+ return state.ui.javascriptTracingOnNextInteraction;
+}
+
+export function getJavascriptTracingOnNextLoad(state) {
+ return state.ui.javascriptTracingOnNextLoad;
+}
+
+export function getJavascriptTracingFunctionReturn(state) {
+ return state.ui.javascriptTracingFunctionReturn;
+}
+
+export function getSearchOptions(state, searchKey) {
+ return state.ui.mutableSearchOptions[searchKey];
+}
+
+export function getProjectSearchQuery(state) {
+ return state.ui.projectSearchQuery;
+}
+
+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..32eadb1ba7
--- /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 "devtools/client/shared/vendor/reselect";
+
+import { getBreakpointsList } from "./breakpoints";
+import { getSelectedSource } from "./sources";
+
+import { sortSelectedBreakpoints } from "../utils/breakpoint/index";
+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)?.source.id === 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..ce463bea14
--- /dev/null
+++ b/devtools/client/debugger/src/selectors/visibleColumnBreakpoints.js
@@ -0,0 +1,149 @@
+/* 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 "devtools/client/shared/vendor/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 convertToList(breakpointPositions) {
+ return [].concat(...Object.values(breakpointPositions));
+}
+
+/**
+ * Retrieve the list of column breakpoints to be displayed.
+ * This ignores lines without any breakpoint, but also lines with a single possible breakpoint.
+ * So that we only return breakpoints where there is at least two possible breakpoint on a given line.
+ * Also, this only consider lines currently visible in CodeMirror editor.
+ *
+ * This method returns an array whose elements are objects having two attributes:
+ * - breakpoint: A breakpoint object refering to a precise column location
+ * - location: The location object in an active source where the breakpoint location matched.
+ * This location may be the generated or original source based on the currently selected source type.
+ *
+ * See `visibleColumnBreakpoints()` for the definition of arguments.
+ */
+export function getColumnBreakpoints(
+ positions,
+ breakpoints,
+ viewport,
+ selectedSource,
+ selectedSourceTextContent
+) {
+ if (!positions || !selectedSource || !breakpoints.length || !viewport) {
+ return [];
+ }
+
+ const breakpointsPerLine = new Map();
+ for (const breakpoint of breakpoints) {
+ if (breakpoint.options.hidden) {
+ continue;
+ }
+ const location = getSelectedLocation(breakpoint, selectedSource);
+ const { line } = location;
+ let breakpointsPerColumn = breakpointsPerLine.get(line);
+ if (!breakpointsPerColumn) {
+ breakpointsPerColumn = new Map();
+ breakpointsPerLine.set(line, breakpointsPerColumn);
+ }
+ breakpointsPerColumn.set(location.column, breakpoint);
+ }
+
+ const columnBreakpoints = [];
+ for (const keyLine in positions) {
+ const positionsPerLine = positions[keyLine];
+ // Only consider positions where there is more than one breakable position per line.
+ // When there is only one breakpoint, this isn't a column breakpoint.
+ if (positionsPerLine.length <= 1) {
+ continue;
+ }
+ for (const breakpointPosition of positionsPerLine) {
+ const location = getSelectedLocation(breakpointPosition, selectedSource);
+ const { line } = location;
+
+ // Ignore any further computation if there is no breakpoint on that line.
+ const breakpointsPerColumn = breakpointsPerLine.get(line);
+ if (!breakpointsPerColumn) {
+ continue;
+ }
+
+ // Only consider positions visible in the current CodeMirror viewport
+ if (!contains(location, viewport)) {
+ continue;
+ }
+
+ // Filters out breakpoints to the right of the line. (bug 1552039)
+ // XXX Not really clear why we get such positions??
+ const { column } = location;
+ if (column) {
+ const lineText = getLineText(
+ selectedSource.id,
+ selectedSourceTextContent,
+ line
+ );
+ if (column > lineText.length) {
+ continue;
+ }
+ }
+
+ // Finally, return the expected format output for this selector.
+ // Location of each column breakpoint + a reference to the breakpoint object (if one is set on that column, it can be null).
+ const breakpoint = breakpointsPerColumn.get(column);
+
+ columnBreakpoints.push({
+ location,
+ breakpoint,
+ });
+ }
+ }
+
+ return columnBreakpoints;
+}
+
+function getVisibleBreakpointPositions(state) {
+ const source = getSelectedSource(state);
+ if (!source) {
+ return null;
+ }
+ return getBreakpointPositionsForSource(state, source.id);
+}
+
+export const visibleColumnBreakpoints = createSelector(
+ getVisibleBreakpointPositions,
+ getVisibleBreakpoints,
+ getViewport,
+ getSelectedSource,
+ getSelectedSourceTextContent,
+ getColumnBreakpoints
+);
+
+export function getFirstBreakpointPosition(state, location) {
+ const positions = getBreakpointPositionsForSource(state, location.source.id);
+ if (!positions) {
+ return null;
+ }
+
+ return sortSelectedLocations(convertToList(positions), location.source).find(
+ position =>
+ getSelectedLocation(position, location.source).line == location.line
+ );
+}
diff --git a/devtools/client/debugger/src/test/__mocks__/request-animation-frame.js b/devtools/client/debugger/src/test/__mocks__/request-animation-frame.js
new file mode 100644
index 0000000000..d4441616ef
--- /dev/null
+++ b/devtools/client/debugger/src/test/__mocks__/request-animation-frame.js
@@ -0,0 +1,8 @@
+/* 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/>. */
+
+global.requestAnimationFrame = function (cb) {
+ cb();
+ return null;
+};
diff --git a/devtools/client/debugger/src/test/fixtures/README.md b/devtools/client/debugger/src/test/fixtures/README.md
new file mode 100644
index 0000000000..ea907643e3
--- /dev/null
+++ b/devtools/client/debugger/src/test/fixtures/README.md
@@ -0,0 +1,3 @@
+## Fixtures
+
+Fixtures used for unit tests
diff --git a/devtools/client/debugger/src/test/fixtures/foobar.json b/devtools/client/debugger/src/test/fixtures/foobar.json
new file mode 100644
index 0000000000..f225bf1f7a
--- /dev/null
+++ b/devtools/client/debugger/src/test/fixtures/foobar.json
@@ -0,0 +1,56 @@
+{
+ "sources": {
+ "sources": {
+ "fooSourceActor": {
+ "id": "fooSourceActor",
+ "url": "http://example.com/foo/foo.js",
+ "filename": "foo.js",
+ "pathname": "foo/foo.js"
+ },
+ "barSourceActor": {
+ "id": "barSourceActor",
+ "url": "http://example.com/bar/bar.js",
+ "filename": "bar.js",
+ "pathname": "bar/bar.js"
+ },
+ "bazzSourceActor": {
+ "id": "bazzSourceActor",
+ "url": "http://example.com/bazz/bazz.js",
+ "filename": "bazz.js",
+ "pathname": "bazz/bazz.js"
+ }
+ },
+ "sourcesText": {
+ "fooSourceActor": {
+ "contentType": "text/javascript",
+ "text": "function() {\n return foo;\n}"
+ },
+ "barSourceActor": {
+ "contentType": "text/javascript",
+ "text": "function() {\n return bar;\n}"
+ },
+ "bazzSourceActor": {
+ "contentType": "text/javascript",
+ "text": "function() {\n return bazz;\n}"
+ }
+ }
+ },
+ "breakpoints": {
+ "breakpoints": {
+ "fooBreakpointActor": {
+ "id": "fooBreakpointActor",
+ "location": {
+ "sourceId": "fooSourceActor",
+ "line": 16
+ }
+ },
+ "barBreakpointActor": {
+ "id": "barBreakpointActor",
+ "location": {
+ "sourceId": "barSourceActor",
+ "line": 18
+ }
+ }
+ }
+ }
+}
diff --git a/devtools/client/debugger/src/test/fixtures/index.js b/devtools/client/debugger/src/test/fixtures/index.js
new file mode 100644
index 0000000000..a0efc568b0
--- /dev/null
+++ b/devtools/client/debugger/src/test/fixtures/index.js
@@ -0,0 +1,3 @@
+import foobarJson from "./foobar.json";
+
+export const foobar = foobarJson;
diff --git a/devtools/client/debugger/src/test/shim.js b/devtools/client/debugger/src/test/shim.js
new file mode 100644
index 0000000000..d1ac2f549f
--- /dev/null
+++ b/devtools/client/debugger/src/test/shim.js
@@ -0,0 +1,31 @@
+/* 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/>. */
+
+const {
+ setMocksInGlobal,
+} = require("devtools/client/shared/test-helpers/shared-node-helpers");
+setMocksInGlobal();
+
+const { LocalizationHelper } = require("devtools/shared/l10n");
+global.L10N = new LocalizationHelper(
+ "devtools/client/locales/debugger.properties"
+);
+
+const { URL } = require("url");
+global.URL = URL;
+
+// JSDOM doesn't seem to have those functions that are used by codeMirror.
+// See https://github.com/jsdom/jsdom/issues/3002
+document.createRange = () => {
+ const range = new Range();
+
+ range.getBoundingClientRect = jest.fn();
+
+ range.getClientRects = jest.fn(() => ({
+ item: () => null,
+ length: 0,
+ }));
+
+ return range;
+};
diff --git a/devtools/client/debugger/src/test/tests-setup.js b/devtools/client/debugger/src/test/tests-setup.js
new file mode 100644
index 0000000000..c376f51d00
--- /dev/null
+++ b/devtools/client/debugger/src/test/tests-setup.js
@@ -0,0 +1,61 @@
+/* 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/>. */
+
+global.Worker = require("workerjs");
+
+import path from "path";
+import Enzyme from "enzyme";
+import Adapter from "enzyme-adapter-react-16";
+import { setupHelper } from "../utils/dbg";
+import { prefs } from "../utils/prefs";
+
+import { PrettyPrintDispatcher } from "../workers/pretty-print/index";
+import { ParserDispatcher } from "../workers/parser";
+import { SearchDispatcher } from "../workers/search/index";
+
+import { clearDocuments } from "../utils/editor/index";
+
+const rootPath = path.join(__dirname, "../../");
+
+Enzyme.configure({ adapter: new Adapter() });
+
+jest.setTimeout(20000);
+
+function formatException(reason, p) {
+ console && console.log("Unhandled Rejection at:", p, "reason:", reason);
+}
+
+export const parserWorker = new ParserDispatcher(
+ path.join(rootPath, "src/workers/parser/worker.js")
+);
+export const prettyPrintWorker = new PrettyPrintDispatcher(
+ path.join(rootPath, "src/workers/pretty-print/worker.js")
+);
+export const searchWorker = new SearchDispatcher(
+ path.join(rootPath, "src/workers/search/worker.js")
+);
+
+beforeAll(() => {
+ process.on("unhandledRejection", formatException);
+});
+
+afterAll(() => {
+ parserWorker.stop();
+ prettyPrintWorker.stop();
+ searchWorker.stop();
+
+ process.removeListener("unhandledRejection", formatException);
+});
+
+afterEach(() => {});
+
+beforeEach(async () => {
+ clearDocuments();
+ prefs.projectDirectoryRoot = "";
+ prefs.projectDirectoryRootName = "";
+ prefs.expressions = [];
+
+ // Ensures window.dbg is there to track telemetry
+ setupHelper({ selectors: {} });
+});
diff --git a/devtools/client/debugger/src/utils/DevToolsUtils.js b/devtools/client/debugger/src/utils/DevToolsUtils.js
new file mode 100644
index 0000000000..b2e01f5502
--- /dev/null
+++ b/devtools/client/debugger/src/utils/DevToolsUtils.js
@@ -0,0 +1,16 @@
+/* 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 assert from "./assert";
+
+export function reportException(who, exception) {
+ const msg = `${who} threw an exception: `;
+ console.error(msg, exception);
+}
+
+export function executeSoon(fn) {
+ setTimeout(fn, 0);
+}
+
+export default assert;
diff --git a/devtools/client/debugger/src/utils/assert.js b/devtools/client/debugger/src/utils/assert.js
new file mode 100644
index 0000000000..2be4f3c7f1
--- /dev/null
+++ b/devtools/client/debugger/src/utils/assert.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 { isNodeTest } from "./environment";
+
+let assert;
+// TODO: try to enable these assertions on mochitest by also enabling it on:
+// import flags from "devtools/shared/flags";
+// if (flags.testing)
+// Unfortunately it throws a lot on mochitests...
+
+if (isNodeTest()) {
+ assert = function (condition, message) {
+ if (!condition) {
+ throw new Error(`Assertion failure: ${message}`);
+ }
+ };
+} else {
+ assert = function () {};
+}
+export default assert;
diff --git a/devtools/client/debugger/src/utils/ast.js b/devtools/client/debugger/src/utils/ast.js
new file mode 100644
index 0000000000..08869df8dd
--- /dev/null
+++ b/devtools/client/debugger/src/utils/ast.js
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// Check whether location A starts after location B
+export function positionAfter(a, b) {
+ return (
+ a.start.line > b.start.line ||
+ (a.start.line === b.start.line && a.start.column > b.start.column)
+ );
+}
+
+export function containsPosition(a, b) {
+ const bColumn = b.column || 0;
+ const startsBefore =
+ a.start.line < b.line ||
+ (a.start.line === b.line && a.start.column <= bColumn);
+ const endsAfter =
+ a.end.line > b.line || (a.end.line === b.line && a.end.column >= bColumn);
+
+ return startsBefore && endsAfter;
+}
+
+function findClosestofSymbol(declarations, location) {
+ if (!declarations) {
+ return null;
+ }
+
+ return declarations.reduce((found, currNode) => {
+ if (
+ currNode.name === "anonymous" ||
+ !containsPosition(currNode.location, {
+ line: location.line,
+ column: location.column || 0,
+ })
+ ) {
+ return found;
+ }
+
+ if (!found) {
+ return currNode;
+ }
+
+ if (found.location.start.line > currNode.location.start.line) {
+ return found;
+ }
+ if (
+ found.location.start.line === currNode.location.start.line &&
+ found.location.start.column > currNode.location.start.column
+ ) {
+ return found;
+ }
+
+ return currNode;
+ }, null);
+}
+
+export function findClosestFunction(symbols, location) {
+ if (!symbols) {
+ return null;
+ }
+
+ return findClosestofSymbol(symbols.functions, location);
+}
diff --git a/devtools/client/debugger/src/utils/async-value.js b/devtools/client/debugger/src/utils/async-value.js
new file mode 100644
index 0000000000..e1467d2401
--- /dev/null
+++ b/devtools/client/debugger/src/utils/async-value.js
@@ -0,0 +1,27 @@
+/* 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 pending() {
+ return { state: "pending" };
+}
+export function fulfilled(value) {
+ return { state: "fulfilled", value };
+}
+export function rejected(value) {
+ return { state: "rejected", value };
+}
+
+export function asSettled(value) {
+ return value && value.state !== "pending" ? value : null;
+}
+
+export function isPending(value) {
+ return value.state === "pending";
+}
+export function isFulfilled(value) {
+ return value.state === "fulfilled";
+}
+export function isRejected(value) {
+ return value.state === "rejected";
+}
diff --git a/devtools/client/debugger/src/utils/bootstrap.js b/devtools/client/debugger/src/utils/bootstrap.js
new file mode 100644
index 0000000000..e8d6de2cf0
--- /dev/null
+++ b/devtools/client/debugger/src/utils/bootstrap.js
@@ -0,0 +1,142 @@
+/* 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 React from "devtools/client/shared/vendor/react";
+import {
+ bindActionCreators,
+ combineReducers,
+} from "devtools/client/shared/vendor/redux";
+import ReactDOM from "devtools/client/shared/vendor/react-dom";
+const {
+ Provider,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+import ToolboxProvider from "devtools/client/framework/store-provider";
+import flags from "devtools/shared/flags";
+const {
+ registerStoreObserver,
+} = require("resource://devtools/client/shared/redux/subscriber.js");
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+import { SearchDispatcher } from "../workers/search/index";
+import { PrettyPrintDispatcher } from "../workers/pretty-print/index";
+
+import configureStore from "../actions/utils/create-store";
+import reducers from "../reducers/index";
+import * as selectors from "../selectors/index";
+import App from "../components/App";
+import { asyncStore, prefs } from "./prefs";
+import { persistTabs } from "../utils/tabs";
+const {
+ sanitizeBreakpoints,
+} = require("resource://devtools/client/shared/thread-utils.js");
+
+let gWorkers;
+
+export function bootstrapStore(client, workers, panel, initialState) {
+ const debugJsModules = AppConstants.DEBUG_JS_MODULES == "1";
+ const createStore = configureStore({
+ log: prefs.logging || flags.testing,
+ timing: debugJsModules,
+ makeThunkArgs: (args, state) => {
+ return { ...args, client, ...workers, panel };
+ },
+ });
+
+ const store = createStore(combineReducers(reducers), initialState);
+ registerStoreObserver(store, updatePrefs);
+
+ const actions = bindActionCreators(
+ require("../actions/index").default,
+ store.dispatch
+ );
+
+ return { store, actions, selectors };
+}
+
+export function bootstrapWorkers(panelWorkers) {
+ // The panel worker will typically be the source map and parser workers.
+ // Both will be managed by the toolbox.
+ gWorkers = {
+ prettyPrintWorker: new PrettyPrintDispatcher(),
+ searchWorker: new SearchDispatcher(),
+ };
+ return { ...panelWorkers, ...gWorkers };
+}
+
+export function teardownWorkers() {
+ gWorkers.prettyPrintWorker.stop();
+ gWorkers.searchWorker.stop();
+}
+
+/**
+ * Create and mount the root App component.
+ *
+ * @param {ReduxStore} store
+ * @param {ReduxStore} toolboxStore
+ * @param {Object} appComponentAttributes
+ * @param {Array} appComponentAttributes.fluentBundles
+ * @param {Document} appComponentAttributes.toolboxDoc
+ */
+export function bootstrapApp(store, toolboxStore, appComponentAttributes = {}) {
+ const mount = getMountElement();
+ if (!mount) {
+ return;
+ }
+
+ ReactDOM.render(
+ React.createElement(
+ Provider,
+ { store },
+ React.createElement(
+ ToolboxProvider,
+ { store: toolboxStore },
+ React.createElement(App, appComponentAttributes)
+ )
+ ),
+ mount
+ );
+}
+
+function getMountElement() {
+ return document.querySelector("#mount");
+}
+
+// This is the opposite of bootstrapApp
+export function unmountRoot() {
+ ReactDOM.unmountComponentAtNode(getMountElement());
+}
+
+function updatePrefs(state, oldState) {
+ const hasChanged = selector =>
+ selector(oldState) && selector(oldState) !== selector(state);
+
+ if (hasChanged(selectors.getPendingBreakpoints)) {
+ asyncStore.pendingBreakpoints = sanitizeBreakpoints(
+ selectors.getPendingBreakpoints(state)
+ );
+ }
+
+ if (
+ oldState.eventListenerBreakpoints &&
+ oldState.eventListenerBreakpoints !== state.eventListenerBreakpoints
+ ) {
+ asyncStore.eventListenerBreakpoints = state.eventListenerBreakpoints;
+ }
+
+ if (hasChanged(selectors.getTabs)) {
+ asyncStore.tabs = persistTabs(selectors.getTabs(state));
+ }
+
+ if (hasChanged(selectors.getXHRBreakpoints)) {
+ asyncStore.xhrBreakpoints = selectors.getXHRBreakpoints(state);
+ }
+
+ if (hasChanged(selectors.getBlackBoxRanges)) {
+ asyncStore.blackboxedRanges = selectors.getBlackBoxRanges(state);
+ }
+}
diff --git a/devtools/client/debugger/src/utils/breakpoint/breakpointPositions.js b/devtools/client/debugger/src/utils/breakpoint/breakpointPositions.js
new file mode 100644
index 0000000000..49b8523284
--- /dev/null
+++ b/devtools/client/debugger/src/utils/breakpoint/breakpointPositions.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/>. */
+
+import { comparePosition } from "../location";
+import { getSelectedLocation } from "../selected-location";
+
+export function findPosition(positions, location) {
+ if (!positions) {
+ return null;
+ }
+
+ const lineBps = positions[location.line];
+ if (!lineBps) {
+ return null;
+ }
+ return lineBps.find(pos =>
+ comparePosition(getSelectedLocation(pos, location), location)
+ );
+}
diff --git a/devtools/client/debugger/src/utils/breakpoint/index.js b/devtools/client/debugger/src/utils/breakpoint/index.js
new file mode 100644
index 0000000000..740d3cd180
--- /dev/null
+++ b/devtools/client/debugger/src/utils/breakpoint/index.js
@@ -0,0 +1,71 @@
+/* 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 { getSourceActorsForSource } from "../../selectors/index";
+import { sortSelectedLocations } from "../location";
+export * from "./breakpointPositions";
+
+// The ID for a Breakpoint is derived from its location in its Source.
+export function makeBreakpointId(location) {
+ const { source, line, column } = location;
+ const columnString = column || "";
+ return `${source.id}:${line}:${columnString}`;
+}
+
+export function makeBreakpointServerLocationId(breakpointServerLocation) {
+ const { sourceUrl, sourceId, line, column } = breakpointServerLocation;
+ const sourceUrlOrId = sourceUrl || sourceId;
+ const columnString = column || "";
+
+ return `${sourceUrlOrId}:${line}:${columnString}`;
+}
+
+/**
+ * Create a location object to set a breakpoint on the server.
+ *
+ * Debugger location objects includes a source and sourceActor attributes
+ * whereas the server don't need them and instead only need either
+ * the source URL -or- a precise source actor ID.
+ */
+export function makeBreakpointServerLocation(state, location) {
+ const source = location.source;
+ if (!source) {
+ throw new Error("Missing 'source' attribute on location object");
+ }
+ const breakpointLocation = {
+ line: location.line,
+ column: location.column,
+ };
+ if (source.url) {
+ breakpointLocation.sourceUrl = source.url;
+ } else {
+ breakpointLocation.sourceId = getSourceActorsForSource(
+ state,
+ source.id
+ )[0].id;
+ }
+ return breakpointLocation;
+}
+
+export function createXHRBreakpoint(path, method, overrides = {}) {
+ const properties = {
+ path,
+ method,
+ disabled: false,
+ loading: false,
+ text: L10N.getFormatStr("xhrBreakpoints.item.label", path),
+ };
+
+ return { ...properties, ...overrides };
+}
+
+export function getSelectedText(breakpoint, selectedSource) {
+ return !!selectedSource && !selectedSource.isOriginal
+ ? breakpoint.text
+ : breakpoint.originalText;
+}
+
+export function sortSelectedBreakpoints(breakpoints, selectedSource) {
+ return sortSelectedLocations(breakpoints, selectedSource);
+}
diff --git a/devtools/client/debugger/src/utils/breakpoint/moz.build b/devtools/client/debugger/src/utils/breakpoint/moz.build
new file mode 100644
index 0000000000..02c5302a6c
--- /dev/null
+++ b/devtools/client/debugger/src/utils/breakpoint/moz.build
@@ -0,0 +1,11 @@
+# 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(
+ "breakpointPositions.js",
+ "index.js",
+)
diff --git a/devtools/client/debugger/src/utils/breakpoint/tests/index.spec.js b/devtools/client/debugger/src/utils/breakpoint/tests/index.spec.js
new file mode 100644
index 0000000000..fcfd155cff
--- /dev/null
+++ b/devtools/client/debugger/src/utils/breakpoint/tests/index.spec.js
@@ -0,0 +1,28 @@
+/* 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 { sortSelectedBreakpoints } from "../index";
+
+import { makeMockBreakpoint, makeMockSource } from "../../test-mockup";
+
+describe("breakpoint sorting", () => {
+ it("sortSelectedBreakpoints should sort by line number and column", () => {
+ const sorted = sortSelectedBreakpoints(
+ [
+ makeMockBreakpoint(undefined, 100, 2),
+ makeMockBreakpoint(undefined, 9, 2),
+ makeMockBreakpoint(undefined, 2),
+ makeMockBreakpoint(undefined, 2, 7),
+ ],
+ makeMockSource()
+ );
+
+ expect(sorted[0].location.line).toBe(2);
+ expect(sorted[0].location.column).toBe(undefined);
+ expect(sorted[1].location.line).toBe(2);
+ expect(sorted[1].location.column).toBe(7);
+ expect(sorted[2].location.line).toBe(9);
+ expect(sorted[3].location.line).toBe(100);
+ });
+});
diff --git a/devtools/client/debugger/src/utils/build-query.js b/devtools/client/debugger/src/utils/build-query.js
new file mode 100644
index 0000000000..f10999aee4
--- /dev/null
+++ b/devtools/client/debugger/src/utils/build-query.js
@@ -0,0 +1,80 @@
+/* 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/>. */
+
+function escapeRegExp(str) {
+ const reRegExpChar = /[\\^$.*+?()[\]{}|]/g;
+ return str.replace(reRegExpChar, "\\$&");
+}
+
+/**
+ * Ignore doing outline matches for less than 3 whitespaces
+ *
+ * @memberof utils/source-search
+ * @static
+ */
+function ignoreWhiteSpace(str) {
+ return /^\s{0,2}$/.test(str) ? "(?!\\s*.*)" : str;
+}
+
+function wholeMatch(query, wholeWord) {
+ if (query === "" || !wholeWord) {
+ return query;
+ }
+
+ return `\\b${query}\\b`;
+}
+
+function buildFlags(caseSensitive, isGlobal) {
+ if (caseSensitive && isGlobal) {
+ return "g";
+ }
+
+ if (!caseSensitive && isGlobal) {
+ return "gi";
+ }
+
+ if (!caseSensitive && !isGlobal) {
+ return "i";
+ }
+
+ return null;
+}
+
+export default function buildQuery(
+ originalQuery,
+ modifiers,
+ { isGlobal = false, ignoreSpaces = false }
+) {
+ const { caseSensitive, regexMatch, wholeWord } = modifiers;
+
+ if (originalQuery === "") {
+ return new RegExp(originalQuery);
+ }
+
+ // Remove the backslashes at the end of the query as it
+ // breaks the RegExp
+ let query = originalQuery.replace(/\\$/, "");
+
+ // If we don't want to do a regexMatch, we need to escape all regex related characters
+ // so they would actually match.
+ if (!regexMatch) {
+ query = escapeRegExp(query);
+ }
+
+ // ignoreWhiteSpace might return a negative lookbehind, and in such case, we want it
+ // to be consumed as a RegExp part by the callsite, so this needs to be called after
+ // the regexp is escaped.
+ if (ignoreSpaces) {
+ query = ignoreWhiteSpace(query);
+ }
+
+ query = wholeMatch(query, wholeWord);
+ const flags = buildFlags(caseSensitive, isGlobal);
+
+ if (flags) {
+ return new RegExp(query, flags);
+ }
+
+ return new RegExp(query);
+}
diff --git a/devtools/client/debugger/src/utils/clipboard.js b/devtools/client/debugger/src/utils/clipboard.js
new file mode 100644
index 0000000000..66c0d297cc
--- /dev/null
+++ b/devtools/client/debugger/src/utils/clipboard.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/>. */
+
+/**
+ * Clipboard function taken from
+ * https://searchfox.org/mozilla-central/source/devtools/shared/platform/clipboard.js
+ */
+
+export function copyToTheClipboard(string) {
+ const doCopy = function (e) {
+ e.clipboardData.setData("text/plain", string);
+ e.preventDefault();
+ };
+
+ document.addEventListener("copy", doCopy);
+ document.execCommand("copy", false, null);
+ document.removeEventListener("copy", doCopy);
+}
diff --git a/devtools/client/debugger/src/utils/context.js b/devtools/client/debugger/src/utils/context.js
new file mode 100644
index 0000000000..8c7311008d
--- /dev/null
+++ b/devtools/client/debugger/src/utils/context.js
@@ -0,0 +1,143 @@
+/* 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 {
+ getThreadContext,
+ getSelectedFrame,
+ getCurrentThread,
+ hasSource,
+ hasSourceActor,
+ getCurrentlyFetchedTopFrame,
+ hasFrame,
+} from "../selectors/index";
+
+// Context encapsulates the main parameters of the current redux state, which
+// impact most other information tracked by the debugger.
+//
+// The main use of Context is to control when asynchronous operations are
+// allowed to make changes to the program state. Such operations might be
+// invalidated as the state changes from the time the operation was originally
+// initiated. For example, operations on pause state might still continue even
+// after the thread unpauses.
+//
+// The methods below can be used to compare an old context with the current one
+// and see if the operation is now invalid and should be abandoned. Actions can
+// also include a 'cx' Context property, which will be checked by the context
+// middleware. If the action fails validateContextAction() then it will not be
+// dispatched.
+//
+// Context can additionally be used as a shortcut to access the main properties
+// of the pause state.
+
+// A normal Context is invalidated if the target navigates.
+
+// A ThreadContext is invalidated if the target navigates, or if the current
+// thread changes, pauses, or resumes.
+
+export class ContextError extends Error {
+ constructor(msg) {
+ // Use a prefix string to help `PromiseTestUtils.allowMatchingRejectionsGlobally`
+ // ignore all these exceptions as this is based on error strings.
+ super(`DebuggerContextError: ${msg}`);
+ }
+}
+
+export function validateNavigateContext(state, cx) {
+ const newcx = getThreadContext(state);
+
+ if (newcx.navigateCounter != cx.navigateCounter) {
+ throw new ContextError("Page has navigated");
+ }
+}
+
+export function validateThreadContext(state, cx) {
+ const newcx = getThreadContext(state);
+
+ if (cx.thread != newcx.thread) {
+ throw new ContextError("Current thread has changed");
+ }
+
+ if (cx.pauseCounter != newcx.pauseCounter) {
+ throw new ContextError("Current thread has paused or resumed");
+ }
+}
+
+export function validateContext(state, cx) {
+ validateNavigateContext(state, cx);
+
+ if ("thread" in cx) {
+ validateThreadContext(state, cx);
+ }
+}
+
+export function validateSelectedFrame(state, selectedFrame) {
+ const newThread = getCurrentThread(state);
+ if (selectedFrame.thread != newThread) {
+ throw new ContextError("Selected thread has changed");
+ }
+
+ const newSelectedFrame = getSelectedFrame(state, newThread);
+ // Compare frame's IDs as frame objects are cloned during mapping
+ if (selectedFrame.id != newSelectedFrame?.id) {
+ throw new ContextError("Selected frame changed");
+ }
+}
+
+export function validateBreakpoint(state, breakpoint) {
+ // XHR breakpoint don't use any location and are always valid
+ if (!breakpoint.location) {
+ return;
+ }
+
+ if (!hasSource(state, breakpoint.location.source.id)) {
+ throw new ContextError(
+ `Breakpoint's location is obsolete (source '${breakpoint.location.source.id}' no longer exists)`
+ );
+ }
+ if (!hasSource(state, breakpoint.generatedLocation.source.id)) {
+ throw new ContextError(
+ `Breakpoint's generated location is obsolete (source '${breakpoint.generatedLocation.source.id}' no longer exists)`
+ );
+ }
+}
+
+export function validateSource(state, source) {
+ if (!hasSource(state, source.id)) {
+ throw new ContextError(
+ `Obsolete source (source '${source.id}' no longer exists)`
+ );
+ }
+}
+
+export function validateSourceActor(state, sourceActor) {
+ if (!hasSourceActor(state, sourceActor.id)) {
+ throw new ContextError(
+ `Obsolete source actor (source '${sourceActor.id}' no longer exists)`
+ );
+ }
+}
+
+export function validateThreadFrames(state, thread, frames) {
+ const newThread = getCurrentThread(state);
+ if (thread != newThread) {
+ throw new ContextError("Selected thread has changed");
+ }
+ const newTopFrame = getCurrentlyFetchedTopFrame(state, newThread);
+ if (newTopFrame?.id != frames[0].id) {
+ throw new ContextError("Thread moved to another location");
+ }
+}
+
+export function validateFrame(state, frame) {
+ if (!hasFrame(state, frame)) {
+ throw new ContextError(
+ `Obsolete frame (frame '${frame.id}' no longer exists)`
+ );
+ }
+}
+
+export function isValidThreadContext(state, cx) {
+ const newcx = getThreadContext(state);
+ return cx.thread == newcx.thread && cx.pauseCounter == newcx.pauseCounter;
+}
diff --git a/devtools/client/debugger/src/utils/dbg.js b/devtools/client/debugger/src/utils/dbg.js
new file mode 100644
index 0000000000..0d7dff72e1
--- /dev/null
+++ b/devtools/client/debugger/src/utils/dbg.js
@@ -0,0 +1,100 @@
+/* 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 { prefs, asyncStore, features } from "./prefs";
+import { getDocument } from "./editor/source-documents";
+import { wasmOffsetToLine } from "./wasm";
+
+function getThreadFront(dbg) {
+ return dbg.targetCommand.targetFront.threadFront;
+}
+
+function findSource(dbg, url) {
+ const sources = dbg.selectors.getSourceList();
+ return sources.find(s => (s.url || "").includes(url));
+}
+
+function findSources(dbg, url) {
+ const sources = dbg.selectors.getSourceList();
+ return sources.filter(s => (s.url || "").includes(url));
+}
+
+function evaluate(dbg, expression) {
+ return dbg.client.evaluate(expression);
+}
+
+function bindSelectors(obj) {
+ return Object.keys(obj.selectors).reduce((bound, selector) => {
+ bound[selector] = (a, b, c) =>
+ obj.selectors[selector](obj.store.getState(), a, b, c);
+ return bound;
+ }, {});
+}
+
+function getCM() {
+ const cm = document.querySelector(".CodeMirror");
+ return cm?.CodeMirror;
+}
+
+function formatMappedLocation(mappedLocation) {
+ const { location, generatedLocation } = mappedLocation;
+ return {
+ original: `(${location.line}, ${location.column})`,
+ generated: `(${generatedLocation.line}, ${generatedLocation.column})`,
+ };
+}
+
+function formatMappedLocations(locations) {
+ return console.table(locations.map(loc => formatMappedLocation(loc)));
+}
+
+function formatSelectedColumnBreakpoints(dbg) {
+ const positions = dbg.selectors.getBreakpointPositionsForSource(
+ dbg.selectors.getSelectedSource().id
+ );
+
+ return formatMappedLocations(positions);
+}
+
+function getDocumentForUrl(dbg, url) {
+ const source = findSource(dbg, url);
+ return getDocument(source.id);
+}
+
+const diff = (a, b) => Object.keys(a).filter(key => !Object.is(a[key], b[key]));
+
+export function setupHelper(obj) {
+ const selectors = bindSelectors(obj);
+ const dbg = {
+ ...obj,
+ selectors,
+ prefs,
+ asyncStore,
+ features,
+ getCM,
+
+ // Expose this to tests as they don't have access to debugger's browser loader require
+ // and so can't load utils/wasm.js
+ wasmOffsetToLine: (sourceId, offset) => wasmOffsetToLine(sourceId, offset),
+
+ helpers: {
+ findSource: url => findSource(dbg, url),
+ findSources: url => findSources(dbg, url),
+ evaluate: expression => evaluate(dbg, expression),
+ dumpThread: () => getThreadFront(dbg).dumpThread(),
+ getDocument: url => getDocumentForUrl(dbg, url),
+ },
+ formatters: {
+ mappedLocations: locations => formatMappedLocations(locations),
+ mappedLocation: location => formatMappedLocation(location),
+ selectedColumnBreakpoints: () => formatSelectedColumnBreakpoints(dbg),
+ },
+ _telemetry: {
+ events: {},
+ },
+ diff,
+ };
+
+ window.dbg = dbg;
+}
diff --git a/devtools/client/debugger/src/utils/editor/create-editor.js b/devtools/client/debugger/src/utils/editor/create-editor.js
new file mode 100644
index 0000000000..6bb280fc4d
--- /dev/null
+++ b/devtools/client/debugger/src/utils/editor/create-editor.js
@@ -0,0 +1,44 @@
+/* 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 SourceEditor from "devtools/client/shared/sourceeditor/editor";
+import { features, prefs } from "../prefs";
+
+export function createEditor() {
+ const gutters = ["breakpoints", "hit-markers", "CodeMirror-linenumbers"];
+
+ if (features.codeFolding) {
+ gutters.push("CodeMirror-foldgutter");
+ }
+
+ return new SourceEditor({
+ mode: "javascript",
+ foldGutter: features.codeFolding,
+ enableCodeFolding: features.codeFolding,
+ readOnly: true,
+ lineNumbers: true,
+ theme: "mozilla",
+ styleActiveLine: false,
+ lineWrapping: prefs.editorWrapping,
+ matchBrackets: true,
+ showAnnotationRuler: true,
+ gutters,
+ value: " ",
+ extraKeys: {
+ // Override code mirror keymap to avoid conflicts with split console.
+ Esc: false,
+ "Cmd-F": false,
+ "Ctrl-F": false,
+ "Cmd-G": false,
+ "Ctrl-G": false,
+ },
+ cursorBlinkRate: prefs.cursorBlinkRate,
+ });
+}
+
+export function createHeadlessEditor() {
+ const editor = createEditor();
+ editor.appendToLocalElement(document.createElement("div"));
+ return editor;
+}
diff --git a/devtools/client/debugger/src/utils/editor/get-expression.js b/devtools/client/debugger/src/utils/editor/get-expression.js
new file mode 100644
index 0000000000..c664f163c3
--- /dev/null
+++ b/devtools/client/debugger/src/utils/editor/get-expression.js
@@ -0,0 +1,54 @@
+/* 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 tokenAtTextPosition(cm, { line, column }) {
+ if (line < 0 || line >= cm.lineCount()) {
+ return null;
+ }
+
+ const token = cm.getTokenAt({ line: line - 1, ch: column });
+ if (!token) {
+ return null;
+ }
+
+ return { startColumn: token.start, endColumn: token.end, type: token.type };
+}
+
+// The strategy of querying codeMirror tokens was borrowed
+// from Chrome's inital implementation in JavaScriptSourceFrame.js#L414
+export function getExpressionFromCoords(cm, coord) {
+ const token = tokenAtTextPosition(cm, coord);
+ if (!token) {
+ return null;
+ }
+
+ let startHighlight = token.startColumn;
+ const endHighlight = token.endColumn;
+ const lineNumber = coord.line;
+ const line = cm.doc.getLine(coord.line - 1);
+ while (startHighlight > 1 && line.charAt(startHighlight - 1) === ".") {
+ const tokenBefore = tokenAtTextPosition(cm, {
+ line: coord.line,
+ column: startHighlight - 2,
+ });
+
+ if (!tokenBefore || !tokenBefore.type) {
+ return null;
+ }
+
+ startHighlight = tokenBefore.startColumn;
+ }
+
+ const expression = line.substring(startHighlight, endHighlight) || "";
+
+ if (!expression) {
+ return null;
+ }
+
+ const location = {
+ start: { line: lineNumber, column: startHighlight },
+ end: { line: lineNumber, column: endHighlight },
+ };
+ return { expression, location };
+}
diff --git a/devtools/client/debugger/src/utils/editor/index.js b/devtools/client/debugger/src/utils/editor/index.js
new file mode 100644
index 0000000000..1adc73b4f8
--- /dev/null
+++ b/devtools/client/debugger/src/utils/editor/index.js
@@ -0,0 +1,222 @@
+/* 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 "./source-documents";
+export * from "./source-search";
+export * from "../ui";
+export * from "./tokens";
+
+import { createEditor } from "./create-editor";
+
+import { isWasm, lineToWasmOffset, wasmOffsetToLine } from "../wasm";
+import { createLocation } from "../location";
+
+let editor;
+
+export function getEditor() {
+ if (editor) {
+ return editor;
+ }
+
+ editor = createEditor();
+ return editor;
+}
+
+export function removeEditor() {
+ editor = null;
+}
+
+function getCodeMirror() {
+ return editor && editor.hasCodeMirror ? editor.codeMirror : null;
+}
+
+export function startOperation() {
+ const codeMirror = getCodeMirror();
+ if (!codeMirror) {
+ return;
+ }
+
+ codeMirror.startOperation();
+}
+
+export function endOperation() {
+ const codeMirror = getCodeMirror();
+ if (!codeMirror) {
+ return;
+ }
+
+ codeMirror.endOperation();
+}
+
+export function toEditorLine(sourceId, lineOrOffset) {
+ if (isWasm(sourceId)) {
+ // TODO ensure offset is always "mappable" to edit line.
+ return wasmOffsetToLine(sourceId, lineOrOffset) || 0;
+ }
+
+ return lineOrOffset ? lineOrOffset - 1 : 1;
+}
+
+export function fromEditorLine(sourceId, line, sourceIsWasm) {
+ if (sourceIsWasm) {
+ return lineToWasmOffset(sourceId, line) || 0;
+ }
+
+ return line + 1;
+}
+
+export function toEditorPosition(location) {
+ // Note that Spidermonkey, Debugger frontend and CodeMirror are all consistant regarding column
+ // and are 0-based. But only CodeMirror consider the line to be 0-based while the two others
+ // consider lines to be 1-based.
+ return {
+ line: toEditorLine(location.source.id, location.line),
+ column:
+ isWasm(location.source.id) || (!location.column ? 0 : location.column),
+ };
+}
+
+export function toSourceLine(sourceId, line) {
+ return isWasm(sourceId) ? lineToWasmOffset(sourceId, line) : line + 1;
+}
+
+export function scrollToPosition(codeMirror, line, column) {
+ // For all cases where these are on the first line and column,
+ // avoid the possibly slow computation of cursor location on large bundles.
+ if (!line && !column) {
+ codeMirror.scrollTo(0, 0);
+ return;
+ }
+
+ const { top, left } = codeMirror.charCoords({ line, ch: column }, "local");
+
+ if (!isVisible(codeMirror, top, left)) {
+ const scroller = codeMirror.getScrollerElement();
+ const centeredX = Math.max(left - scroller.offsetWidth / 2, 0);
+ const centeredY = Math.max(top - scroller.offsetHeight / 2, 0);
+
+ codeMirror.scrollTo(centeredX, centeredY);
+ }
+}
+
+function isVisible(codeMirror, top, left) {
+ function withinBounds(x, min, max) {
+ return x >= min && x <= max;
+ }
+
+ const scrollArea = codeMirror.getScrollInfo();
+ const charWidth = codeMirror.defaultCharWidth();
+ const fontHeight = codeMirror.defaultTextHeight();
+ const { scrollTop, scrollLeft } = codeMirror.doc;
+
+ const inXView = withinBounds(
+ left,
+ scrollLeft,
+ scrollLeft + (scrollArea.clientWidth - 30) - charWidth
+ );
+
+ const inYView = withinBounds(
+ top,
+ scrollTop,
+ scrollTop + scrollArea.clientHeight - fontHeight
+ );
+
+ return inXView && inYView;
+}
+
+export function getLocationsInViewport(
+ { codeMirror },
+ // Offset represents an allowance of characters or lines offscreen to improve
+ // perceived performance of column breakpoint rendering
+ offsetHorizontalCharacters = 100,
+ offsetVerticalLines = 20
+) {
+ // Get scroll position
+ if (!codeMirror) {
+ return {
+ start: { line: 0, column: 0 },
+ end: { line: 0, column: 0 },
+ };
+ }
+ const charWidth = codeMirror.defaultCharWidth();
+ const scrollArea = codeMirror.getScrollInfo();
+ const { scrollLeft } = codeMirror.doc;
+ const rect = codeMirror.getWrapperElement().getBoundingClientRect();
+ const topVisibleLine =
+ codeMirror.lineAtHeight(rect.top, "window") - offsetVerticalLines;
+ const bottomVisibleLine =
+ codeMirror.lineAtHeight(rect.bottom, "window") + offsetVerticalLines;
+
+ const leftColumn = Math.floor(
+ scrollLeft > 0 ? scrollLeft / charWidth - offsetHorizontalCharacters : 0
+ );
+ const rightPosition = scrollLeft + (scrollArea.clientWidth - 30);
+ const rightCharacter =
+ Math.floor(rightPosition / charWidth) + offsetHorizontalCharacters;
+
+ return {
+ start: {
+ line: topVisibleLine || 0,
+ column: leftColumn || 0,
+ },
+ end: {
+ line: bottomVisibleLine || 0,
+ column: rightCharacter,
+ },
+ };
+}
+
+export function markText({ codeMirror }, className, { start, end }) {
+ return codeMirror.markText(
+ { ch: start.column, line: start.line },
+ { ch: end.column, line: end.line },
+ { className }
+ );
+}
+
+export function lineAtHeight({ codeMirror }, sourceId, event) {
+ const _editorLine = codeMirror.lineAtHeight(event.clientY);
+ return toSourceLine(sourceId, _editorLine);
+}
+
+export function getSourceLocationFromMouseEvent({ codeMirror }, source, e) {
+ const { line, ch } = codeMirror.coordsChar({
+ left: e.clientX,
+ top: e.clientY,
+ });
+
+ return createLocation({
+ source,
+ line: fromEditorLine(source.id, line, isWasm(source.id)),
+ column: isWasm(source.id) ? 0 : ch + 1,
+ });
+}
+
+export function forEachLine(codeMirror, iter) {
+ codeMirror.operation(() => {
+ codeMirror.doc.iter(0, codeMirror.lineCount(), iter);
+ });
+}
+
+export function removeLineClass(codeMirror, line, className) {
+ codeMirror.removeLineClass(line, "wrap", className);
+}
+
+export function clearLineClass(codeMirror, className) {
+ forEachLine(codeMirror, line => {
+ removeLineClass(codeMirror, line, className);
+ });
+}
+
+export function getTextForLine(codeMirror, line) {
+ return codeMirror.getLine(line - 1).trim();
+}
+
+export function getCursorLine(codeMirror) {
+ return codeMirror.getCursor().line;
+}
+
+export function getCursorColumn(codeMirror) {
+ return codeMirror.getCursor().ch;
+}
diff --git a/devtools/client/debugger/src/utils/editor/moz.build b/devtools/client/debugger/src/utils/editor/moz.build
new file mode 100644
index 0000000000..568546897d
--- /dev/null
+++ b/devtools/client/debugger/src/utils/editor/moz.build
@@ -0,0 +1,15 @@
+# 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(
+ "create-editor.js",
+ "get-expression.js",
+ "index.js",
+ "source-documents.js",
+ "source-search.js",
+ "tokens.js",
+)
diff --git a/devtools/client/debugger/src/utils/editor/source-documents.js b/devtools/client/debugger/src/utils/editor/source-documents.js
new file mode 100644
index 0000000000..2ddb0b1965
--- /dev/null
+++ b/devtools/client/debugger/src/utils/editor/source-documents.js
@@ -0,0 +1,256 @@
+/* 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 { isWasm, getWasmLineNumberFormatter, renderWasmText } from "../wasm";
+import { isMinified } from "../isMinified";
+import { resizeBreakpointGutter, resizeToggleButton } from "../ui";
+import { javascriptLikeExtensions } from "../source";
+
+const sourceDocs = new Map();
+
+export function getDocument(key) {
+ return sourceDocs.get(key);
+}
+
+export function hasDocument(key) {
+ return sourceDocs.has(key);
+}
+
+export function setDocument(key, doc) {
+ sourceDocs.set(key, doc);
+}
+
+export function removeDocument(key) {
+ sourceDocs.delete(key);
+}
+
+export function clearDocuments() {
+ sourceDocs.clear();
+}
+
+export function clearDocumentsForSources(sources) {
+ for (const source of sources) {
+ sourceDocs.delete(source.id);
+ }
+}
+
+function resetLineNumberFormat(editor) {
+ const cm = editor.codeMirror;
+ cm.setOption("lineNumberFormatter", number => number);
+ resizeBreakpointGutter(cm);
+ resizeToggleButton(cm);
+}
+
+function updateLineNumberFormat(editor, sourceId) {
+ if (!isWasm(sourceId)) {
+ resetLineNumberFormat(editor);
+ return;
+ }
+ const cm = editor.codeMirror;
+ const lineNumberFormatter = getWasmLineNumberFormatter(sourceId);
+ cm.setOption("lineNumberFormatter", lineNumberFormatter);
+ resizeBreakpointGutter(cm);
+ resizeToggleButton(cm);
+}
+
+export function updateDocument(editor, source) {
+ if (!source) {
+ return;
+ }
+
+ const sourceId = source.id;
+ const doc = getDocument(sourceId) || editor.createDocument();
+ editor.replaceDocument(doc);
+
+ updateLineNumberFormat(editor, sourceId);
+}
+
+/* used to apply the context menu wrap line option change to all the docs */
+export function updateDocuments(updater) {
+ for (const doc of sourceDocs.values()) {
+ if (doc.cm == null) {
+ continue;
+ } else {
+ updater(doc);
+ }
+ }
+}
+
+export function clearEditor(editor) {
+ const doc = editor.createDocument("", { name: "text" });
+ editor.replaceDocument(doc);
+ resetLineNumberFormat(editor);
+}
+
+export function showLoading(editor) {
+ // Create the "loading message" document only once
+ let doc = getDocument("loading");
+ if (!doc) {
+ doc = editor.createDocument(L10N.getStr("loadingText"), { name: "text" });
+ setDocument("loading", doc);
+ }
+ // `createDocument` won't be used right away in the editor, we still need to
+ // explicitely update it
+ editor.replaceDocument(doc);
+}
+
+export function showErrorMessage(editor, msg) {
+ let error;
+ if (msg.includes("WebAssembly binary source is not available")) {
+ error = L10N.getStr("wasmIsNotAvailable");
+ } else {
+ error = L10N.getFormatStr("errorLoadingText3", msg);
+ }
+ const doc = editor.createDocument(error, { name: "text" });
+ editor.replaceDocument(doc);
+ resetLineNumberFormat(editor);
+}
+
+const contentTypeModeMap = new Map([
+ ["text/javascript", { name: "javascript" }],
+ ["text/typescript", { name: "javascript", typescript: true }],
+ ["text/coffeescript", { name: "coffeescript" }],
+ [
+ "text/typescript-jsx",
+ {
+ name: "jsx",
+ base: { name: "javascript", typescript: true },
+ },
+ ],
+ ["text/jsx", { name: "jsx" }],
+ ["text/x-elm", { name: "elm" }],
+ ["text/x-clojure", { name: "clojure" }],
+ ["text/x-clojurescript", { name: "clojure" }],
+ ["text/wasm", { name: "text" }],
+ ["text/html", { name: "htmlmixed" }],
+ ["text/plain", { name: "text" }],
+]);
+
+const nonJSLanguageExtensionMap = new Map([
+ ["c", { name: "text/x-csrc" }],
+ ["kt", { name: "text/x-kotlin" }],
+ ["cpp", { name: "text/x-c++src" }],
+ ["m", { name: "text/x-objectivec" }],
+ ["rs", { name: "text/x-rustsrc" }],
+ ["hx", { name: "text/x-haxe" }],
+]);
+
+/**
+ * Returns Code Mirror mode for source content type
+ */
+// eslint-disable-next-line complexity
+export function getMode(source, sourceTextContent, symbols) {
+ const content = sourceTextContent.value;
+ // Disable modes for minified files with 1+ million characters (See Bug 1569829).
+ if (
+ content.type === "text" &&
+ isMinified(source, sourceTextContent) &&
+ content.value.length > 1000000
+ ) {
+ return contentTypeModeMap.get("text/plain");
+ }
+
+ if (content.type !== "text") {
+ return contentTypeModeMap.get("text/plain");
+ }
+
+ const extension = source.displayURL.fileExtension;
+ if (extension === "jsx" || (symbols && symbols.hasJsx)) {
+ if (symbols && symbols.hasTypes) {
+ return contentTypeModeMap.get("text/typescript-jsx");
+ }
+ return contentTypeModeMap.get("text/jsx");
+ }
+
+ if (symbols && symbols.hasTypes) {
+ if (symbols.hasJsx) {
+ return contentTypeModeMap.get("text/typescript-jsx");
+ }
+
+ return contentTypeModeMap.get("text/typescript");
+ }
+
+ // check for C and other non JS languages
+ if (nonJSLanguageExtensionMap.has(extension)) {
+ return nonJSLanguageExtensionMap.get(extension);
+ }
+
+ // if the url ends with a known Javascript-like URL, provide JavaScript mode.
+ if (javascriptLikeExtensions.has(extension)) {
+ return contentTypeModeMap.get("text/javascript");
+ }
+
+ const { contentType, value: text } = content;
+ // Use HTML mode for files in which the first non whitespace
+ // character is `<` regardless of extension.
+ const isHTMLLike = () => text.match(/^\s*</);
+ if (!contentType) {
+ if (isHTMLLike()) {
+ return contentTypeModeMap.get("text/html");
+ }
+ return contentTypeModeMap.get("text/plain");
+ }
+
+ // // @flow or /* @flow */
+ if (text.match(/^\s*(\/\/ @flow|\/\* @flow \*\/)/)) {
+ return contentTypeModeMap.get("text/typescript");
+ }
+
+ if (contentTypeModeMap.has(contentType)) {
+ return contentTypeModeMap.get(contentType);
+ }
+
+ if (isHTMLLike()) {
+ return contentTypeModeMap.get("text/html");
+ }
+
+ return contentTypeModeMap.get("text/plain");
+}
+
+function setMode(editor, source, sourceTextContent, symbols) {
+ const mode = getMode(source, sourceTextContent, symbols);
+ const currentMode = editor.codeMirror.getOption("mode");
+ if (!currentMode || currentMode.name != mode.name) {
+ editor.setMode(mode);
+ }
+}
+
+/**
+ * Handle getting the source document or creating a new
+ * document with the correct mode and text.
+ */
+export function showSourceText(editor, source, sourceTextContent, symbols) {
+ if (hasDocument(source.id)) {
+ const doc = getDocument(source.id);
+ if (editor.codeMirror.doc === doc) {
+ setMode(editor, source, sourceTextContent, symbols);
+ return;
+ }
+
+ editor.replaceDocument(doc);
+ updateLineNumberFormat(editor, source.id);
+ setMode(editor, source, sourceTextContent, symbols);
+ return;
+ }
+
+ const content = sourceTextContent.value;
+
+ const doc = editor.createDocument(
+ // We can set wasm text content directly from the constructor, so we pass an empty string
+ // here, and set the text after replacing the document.
+ content.type !== "wasm" ? content.value : "",
+ getMode(source, sourceTextContent, symbols)
+ );
+
+ setDocument(source.id, doc);
+ editor.replaceDocument(doc);
+
+ if (content.type === "wasm") {
+ const wasmLines = renderWasmText(source.id, content);
+ // cm will try to split into lines anyway, saving memory
+ editor.setText({ split: () => wasmLines, match: () => false });
+ }
+
+ updateLineNumberFormat(editor, source.id);
+}
diff --git a/devtools/client/debugger/src/utils/editor/source-editor.css b/devtools/client/debugger/src/utils/editor/source-editor.css
new file mode 100644
index 0000000000..b2ae305657
--- /dev/null
+++ b/devtools/client/debugger/src/utils/editor/source-editor.css
@@ -0,0 +1,271 @@
+/* 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/. */
+
+:root {
+ --breakpoint-active-color: rgba(44, 187, 15, 0.2);
+ --breakpoint-active-color-hover: rgba(44, 187, 15, 0.5);
+ --debug-line-background: rgba(226, 236, 247, 0.5);
+ --debug-line-border: rgb(145, 188, 219);
+}
+
+.theme-dark:root {
+ --debug-line-background: rgb(73, 82, 103);
+ --debug-line-border: rgb(119, 134, 162);
+ --breakpoint-active-color: rgba(45, 210, 158, 0.5);
+ --breakpoint-active-color-hover: rgba(0, 255, 175, 0.7);
+}
+
+.CodeMirror .errors {
+ width: 16px;
+}
+
+.CodeMirror .error {
+ display: inline-block;
+ margin-left: 5px;
+ width: 12px;
+ height: 12px;
+ opacity: 0.75;
+}
+
+.CodeMirror .hit-counts {
+ width: 6px;
+}
+
+.CodeMirror .hit-count {
+ display: inline-block;
+ height: 12px;
+ border: solid rgba(0, 0, 0, 0.2);
+ border-width: 1px 1px 1px 0;
+ border-radius: 0 3px 3px 0;
+ padding: 0 3px;
+ font-size: 10px;
+ pointer-events: none;
+}
+
+.theme-dark .debug-line .CodeMirror-linenumber {
+ color: #c0c0c0;
+}
+
+.debug-line .CodeMirror-line {
+ background-color: var(--debug-line-background) !important;
+ outline: var(--debug-line-border) solid 1px;
+}
+
+/* Don't display the highlight color since the debug line
+ is already highlighted */
+.debug-line .CodeMirror-activeline-background {
+ display: none;
+}
+
+.CodeMirror {
+ cursor: text;
+ height: 100%;
+}
+
+.CodeMirror-gutters {
+ cursor: default;
+}
+
+/* This is to avoid the fake horizontal scrollbar div of codemirror to go 0
+height when floating scrollbars are active. Make sure that this value is equal
+to the maximum of `min-height` specific to the `scrollbar[orient="horizontal"]`
+selector in floating-scrollbar-light.css across all platforms. */
+.CodeMirror-hscrollbar {
+ min-height: 10px;
+}
+
+/* This is to avoid the fake vertical scrollbar div of codemirror to go 0
+width when floating scrollbars are active. Make sure that this value is equal
+to the maximum of `min-width` specific to the `scrollbar[orient="vertical"]`
+selector in floating-scrollbar-light.css across all platforms. */
+.CodeMirror-vscrollbar {
+ min-width: 10px;
+}
+
+.cm-trailingspace {
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAACCAYAAAB/qH1jAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QUXCToH00Y1UgAAACFJREFUCNdjPMDBUc/AwNDAAAFMTAwMDA0OP34wQgX/AQBYgwYEx4f9lQAAAABJRU5ErkJggg==");
+ opacity: 0.75;
+ background-position: left bottom;
+ background-repeat: repeat-x;
+}
+
+/* CodeMirror dialogs styling */
+
+.CodeMirror-dialog {
+ padding: 4px 3px;
+}
+
+.CodeMirror-dialog,
+.CodeMirror-dialog input {
+ font: message-box;
+}
+
+/* Fold addon */
+
+.CodeMirror-foldmarker {
+ color: blue;
+ text-shadow: #b9f 1px 1px 2px, #b9f -1px -1px 2px, #b9f 1px -1px 2px,
+ #b9f -1px 1px 2px;
+ font-family: sans-serif;
+ line-height: 0.3;
+ cursor: pointer;
+}
+
+.CodeMirror-foldgutter {
+ width: 10px;
+}
+
+.CodeMirror-foldgutter-open,
+.CodeMirror-foldgutter-folded {
+ color: #555;
+ cursor: pointer;
+ line-height: 1;
+ padding: 0 1px;
+}
+
+.CodeMirror-foldgutter-open::after,
+.CodeMirror-foldgutter-open::before,
+.CodeMirror-foldgutter-folded::after,
+.CodeMirror-foldgutter-folded::before {
+ content: "";
+ height: 0;
+ width: 0;
+ position: absolute;
+ border: 4px solid transparent;
+}
+
+.CodeMirror-foldgutter-open::after {
+ border-top-color: var(--theme-codemirror-gutter-background);
+ top: 4px;
+}
+
+.CodeMirror-foldgutter-open::before {
+ border-top-color: var(--theme-body-color);
+ top: 5px;
+}
+
+.new-breakpoint .CodeMirror-foldgutter-open::after {
+ border-top-color: var(--theme-selection-background);
+}
+
+.new-breakpoint .CodeMirror-foldgutter-open::before {
+ border-top-color: white;
+}
+
+.CodeMirror-foldgutter-folded::after {
+ border-left-color: var(--theme-codemirror-gutter-background);
+ left: 3px;
+ top: 3px;
+}
+
+.CodeMirror-foldgutter-folded::before {
+ border-left-color: var(--theme-body-color);
+ left: 4px;
+ top: 3px;
+}
+
+.new-breakpoint .CodeMirror-foldgutter-folded::after {
+ border-left-color: var(--theme-selection-background);
+}
+
+.new-breakpoint .CodeMirror-foldgutter-folded::before {
+ border-left-color: white;
+}
+
+.CodeMirror-hints {
+ position: absolute;
+ z-index: 10;
+ overflow: hidden;
+ list-style: none;
+ margin: 0;
+ padding: 2px;
+ border-radius: 3px;
+ font-size: 90%;
+ max-height: 20em;
+ overflow-y: auto;
+}
+
+.CodeMirror-hint {
+ margin: 0;
+ padding: 0 4px;
+ border-radius: 2px;
+ max-width: 19em;
+ overflow: hidden;
+ white-space: pre;
+ cursor: pointer;
+}
+
+.CodeMirror-Tern-completion {
+ padding-inline-start: 22px;
+ position: relative;
+ line-height: 18px;
+}
+
+.CodeMirror-Tern-completion:before {
+ position: absolute;
+ left: 2px;
+ bottom: 2px;
+ border-radius: 50%;
+ font-size: 12px;
+ font-weight: bold;
+ height: 15px;
+ width: 15px;
+ line-height: 16px;
+ text-align: center;
+ color: #ffffff;
+ box-sizing: border-box;
+}
+
+.CodeMirror-Tern-completion-unknown:before {
+ content: "?";
+}
+
+.CodeMirror-Tern-completion-object:before {
+ content: "O";
+}
+
+.CodeMirror-Tern-completion-fn:before {
+ content: "F";
+}
+
+.CodeMirror-Tern-completion-array:before {
+ content: "A";
+}
+
+.CodeMirror-Tern-completion-number:before {
+ content: "N";
+}
+
+.CodeMirror-Tern-completion-string:before {
+ content: "S";
+}
+
+.CodeMirror-Tern-completion-bool:before {
+ content: "B";
+}
+
+.CodeMirror-Tern-completion-guess {
+ color: #999;
+}
+
+.CodeMirror-Tern-tooltip {
+ border-radius: 3px;
+ padding: 2px 5px;
+ white-space: pre-wrap;
+ max-width: 40em;
+ position: absolute;
+ z-index: 10;
+}
+
+.CodeMirror-Tern-hint-doc {
+ max-width: 25em;
+}
+
+.CodeMirror-Tern-farg-current {
+ text-decoration: underline;
+}
+
+.CodeMirror-Tern-fhint-guess {
+ opacity: 0.7;
+}
diff --git a/devtools/client/debugger/src/utils/editor/source-search.js b/devtools/client/debugger/src/utils/editor/source-search.js
new file mode 100644
index 0000000000..92097377ba
--- /dev/null
+++ b/devtools/client/debugger/src/utils/editor/source-search.js
@@ -0,0 +1,327 @@
+/* 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 buildQuery from "../build-query";
+
+/**
+ * @memberof utils/source-search
+ * @static
+ */
+function getSearchCursor(cm, query, pos, modifiers) {
+ const regexQuery = buildQuery(query, modifiers, { isGlobal: true });
+ return cm.getSearchCursor(regexQuery, pos);
+}
+
+/**
+ * @memberof utils/source-search
+ * @static
+ */
+function SearchState() {
+ this.posFrom = this.posTo = this.query = null;
+ this.overlay = null;
+ this.results = [];
+}
+
+/**
+ * @memberof utils/source-search
+ * @static
+ */
+function getSearchState(cm, query) {
+ const state = cm.state.search || (cm.state.search = new SearchState());
+ return state;
+}
+
+function isWhitespace(query) {
+ return !query.match(/\S/);
+}
+
+/**
+ * This returns a mode object used by CodeMirror's addOverlay function
+ * to parse and style tokens in the file.
+ * The mode object contains a tokenizer function (token) which takes
+ * a character stream as input, advances it a character at a time,
+ * and returns style(s) for that token. For more details see
+ * https://codemirror.net/5/doc/manual.html#modeapi
+ *
+ * @memberof utils/source-search
+ * @static
+ */
+function searchOverlay(query, modifiers) {
+ const regexQuery = buildQuery(query, modifiers, {
+ ignoreSpaces: true,
+ // regex must be global for the overlay
+ isGlobal: true,
+ });
+
+ return {
+ token: function (stream, state) {
+ // set the last index to be the current stream position
+ // this acts as an offset
+ regexQuery.lastIndex = stream.pos;
+ const match = regexQuery.exec(stream.string);
+ if (match && match.index === stream.pos) {
+ // if we have a match at the current stream position
+ // set the class for a match
+ stream.pos += match[0].length || 1;
+ return "highlight highlight-full";
+ }
+
+ if (match) {
+ // if we have a match somewhere in the line, go to that point in the
+ // stream
+ stream.pos = match.index;
+ } else {
+ // if we have no matches in this line, skip to the end of the line
+ stream.skipToEnd();
+ }
+
+ return null;
+ },
+ };
+}
+
+/**
+ * @memberof utils/source-search
+ * @static
+ */
+function updateOverlay(cm, state, query, modifiers) {
+ cm.removeOverlay(state.overlay);
+ state.overlay = searchOverlay(query, modifiers);
+ cm.addOverlay(state.overlay, { opaque: false });
+}
+
+function updateCursor(cm, state, keepSelection) {
+ state.posTo = cm.getCursor("anchor");
+ state.posFrom = cm.getCursor("head");
+
+ if (!keepSelection) {
+ state.posTo = { line: 0, ch: 0 };
+ state.posFrom = { line: 0, ch: 0 };
+ }
+}
+
+export function getMatchIndex(count, currentIndex, rev) {
+ if (!rev) {
+ if (currentIndex == count - 1) {
+ return 0;
+ }
+
+ return currentIndex + 1;
+ }
+
+ if (currentIndex == 0) {
+ return count - 1;
+ }
+
+ return currentIndex - 1;
+}
+
+/**
+ * If there's a saved search, selects the next results.
+ * Otherwise, creates a new search and selects the first
+ * result.
+ *
+ * @memberof utils/source-search
+ * @static
+ */
+function doSearch(
+ ctx,
+ rev,
+ query,
+ keepSelection,
+ modifiers,
+ focusFirstResult = true
+) {
+ const { cm, ed } = ctx;
+ if (!cm) {
+ return null;
+ }
+ const defaultIndex = { line: -1, ch: -1 };
+
+ return cm.operation(function () {
+ if (!query || isWhitespace(query)) {
+ clearSearch(cm, query);
+ return null;
+ }
+
+ const state = getSearchState(cm, query);
+ const isNewQuery = state.query !== query;
+ state.query = query;
+
+ updateOverlay(cm, state, query, modifiers);
+ updateCursor(cm, state, keepSelection);
+ const searchLocation = searchNext(ctx, rev, query, isNewQuery, modifiers);
+
+ // We don't want to jump the editor
+ // when we're selecting text
+ if (!cm.state.selectingText && searchLocation && focusFirstResult) {
+ ed.alignLine(searchLocation.from.line, "center");
+ cm.setSelection(searchLocation.from, searchLocation.to);
+ }
+
+ return searchLocation ? searchLocation.from : defaultIndex;
+ });
+}
+
+export function searchSourceForHighlight(
+ ctx,
+ rev,
+ query,
+ keepSelection,
+ modifiers,
+ line,
+ ch
+) {
+ const { cm } = ctx;
+ if (!cm) {
+ return;
+ }
+
+ cm.operation(function () {
+ const state = getSearchState(cm, query);
+ const isNewQuery = state.query !== query;
+ state.query = query;
+
+ updateOverlay(cm, state, query, modifiers);
+ updateCursor(cm, state, keepSelection);
+ findNextOnLine(ctx, rev, query, isNewQuery, modifiers, line, ch);
+ });
+}
+
+function getCursorPos(newQuery, rev, state) {
+ if (newQuery) {
+ return rev ? state.posFrom : state.posTo;
+ }
+
+ return rev ? state.posTo : state.posFrom;
+}
+
+/**
+ * Selects the next result of a saved search.
+ *
+ * @memberof utils/source-search
+ * @static
+ */
+function searchNext(ctx, rev, query, newQuery, modifiers) {
+ const { cm } = ctx;
+ let nextMatch;
+ cm.operation(function () {
+ const state = getSearchState(cm, query);
+ const pos = getCursorPos(newQuery, rev, state);
+
+ if (!state.query) {
+ return;
+ }
+
+ let cursor = getSearchCursor(cm, state.query, pos, modifiers);
+
+ const location = rev
+ ? { line: cm.lastLine(), ch: null }
+ : { line: cm.firstLine(), ch: 0 };
+
+ if (!cursor.find(rev) && state.query) {
+ cursor = getSearchCursor(cm, state.query, location, modifiers);
+ if (!cursor.find(rev)) {
+ return;
+ }
+ }
+
+ nextMatch = { from: cursor.from(), to: cursor.to() };
+ });
+
+ return nextMatch;
+}
+
+function findNextOnLine(ctx, rev, query, newQuery, modifiers, line, ch) {
+ const { cm, ed } = ctx;
+ cm.operation(function () {
+ const pos = { line: line - 1, ch };
+ let cursor = getSearchCursor(cm, query, pos, modifiers);
+
+ if (!cursor.find(rev) && query) {
+ cursor = getSearchCursor(cm, query, pos, modifiers);
+ if (!cursor.find(rev)) {
+ return;
+ }
+ }
+
+ // We don't want to jump the editor
+ // when we're selecting text
+ if (!cm.state.selectingText) {
+ ed.alignLine(cursor.from().line, "center");
+ cm.setSelection(cursor.from(), cursor.to());
+ }
+ });
+}
+
+/**
+ * Remove overlay.
+ *
+ * @memberof utils/source-search
+ * @static
+ */
+export function removeOverlay(ctx, query) {
+ const state = getSearchState(ctx.cm, query);
+ ctx.cm.removeOverlay(state.overlay);
+ const { line, ch } = ctx.cm.getCursor();
+ ctx.cm.doc.setSelection({ line, ch }, { line, ch }, { scroll: false });
+}
+
+/**
+ * Clears the currently saved search.
+ *
+ * @memberof utils/source-search
+ * @static
+ */
+export function clearSearch(cm, query) {
+ const state = getSearchState(cm, query);
+
+ state.results = [];
+
+ if (!state.query) {
+ return;
+ }
+ cm.removeOverlay(state.overlay);
+ state.query = null;
+}
+
+/**
+ * Starts a new search.
+ *
+ * @memberof utils/source-search
+ * @static
+ */
+export function find(ctx, query, keepSelection, modifiers, focusFirstResult) {
+ clearSearch(ctx.cm, query);
+ return doSearch(
+ ctx,
+ false,
+ query,
+ keepSelection,
+ modifiers,
+ focusFirstResult
+ );
+}
+
+/**
+ * Finds the next item based on the currently saved search.
+ *
+ * @memberof utils/source-search
+ * @static
+ */
+export function findNext(ctx, query, keepSelection, modifiers) {
+ return doSearch(ctx, false, query, keepSelection, modifiers);
+}
+
+/**
+ * Finds the previous item based on the currently saved search.
+ *
+ * @memberof utils/source-search
+ * @static
+ */
+export function findPrev(ctx, query, keepSelection, modifiers) {
+ return doSearch(ctx, true, query, keepSelection, modifiers);
+}
+
+export { buildQuery };
diff --git a/devtools/client/debugger/src/utils/editor/tests/__snapshots__/create-editor.spec.js.snap b/devtools/client/debugger/src/utils/editor/tests/__snapshots__/create-editor.spec.js.snap
new file mode 100644
index 0000000000..843647731b
--- /dev/null
+++ b/devtools/client/debugger/src/utils/editor/tests/__snapshots__/create-editor.spec.js.snap
Binary files differ
diff --git a/devtools/client/debugger/src/utils/editor/tests/create-editor.spec.js b/devtools/client/debugger/src/utils/editor/tests/create-editor.spec.js
new file mode 100644
index 0000000000..fe7cd5dcc6
--- /dev/null
+++ b/devtools/client/debugger/src/utils/editor/tests/create-editor.spec.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 { createEditor } from "../create-editor";
+
+import { features } from "../../prefs";
+
+describe("createEditor", () => {
+ test("SourceEditor default config", () => {
+ const editor = createEditor();
+ expect(editor.config).toMatchSnapshot();
+ expect(editor.config.gutters).not.toContain("CodeMirror-foldgutter");
+ });
+
+ test("Adds codeFolding", () => {
+ features.codeFolding = true;
+ const editor = createEditor();
+ expect(editor.config).toMatchSnapshot();
+ expect(editor.config.gutters).toContain("CodeMirror-foldgutter");
+ });
+});
diff --git a/devtools/client/debugger/src/utils/editor/tests/editor.spec.js b/devtools/client/debugger/src/utils/editor/tests/editor.spec.js
new file mode 100644
index 0000000000..b3fcad17ff
--- /dev/null
+++ b/devtools/client/debugger/src/utils/editor/tests/editor.spec.js
@@ -0,0 +1,186 @@
+/* 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 {
+ toEditorLine,
+ toEditorPosition,
+ toSourceLine,
+ scrollToPosition,
+ markText,
+ lineAtHeight,
+ getSourceLocationFromMouseEvent,
+ forEachLine,
+ removeLineClass,
+ clearLineClass,
+ getTextForLine,
+ getCursorLine,
+} from "../index";
+
+import { makeMockSource } from "../../test-mockup";
+
+describe("toEditorLine", () => {
+ it("returns an editor line", () => {
+ const testId = "test-123";
+ const line = 30;
+ expect(toEditorLine(testId, line)).toEqual(29);
+ });
+});
+
+describe("toEditorPosition", () => {
+ it("returns an editor position", () => {
+ const loc = { source: { id: "source" }, line: 100, column: 25 };
+ expect(toEditorPosition(loc)).toEqual({
+ line: 99,
+ column: 25,
+ });
+ });
+});
+
+describe("toSourceLine", () => {
+ it("returns a source line", () => {
+ const testId = "test-123";
+ const line = 30;
+ expect(toSourceLine(testId, line)).toEqual(31);
+ });
+});
+
+const codeMirror = {
+ doc: {
+ iter: jest.fn((_, __, cb) => cb()),
+ },
+ lineCount: jest.fn(() => 100),
+ getLine: jest.fn(() => "something"),
+ getCursor: jest.fn(() => ({ line: 3 })),
+ getScrollerElement: jest.fn(() => ({
+ offsetWidth: 100,
+ offsetHeight: 100,
+ })),
+ getScrollInfo: () => ({
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0,
+ clientHeight: 100,
+ clientWidth: 100,
+ }),
+ removeLineClass: jest.fn(),
+ operation: jest.fn(cb => cb()),
+ charCoords: jest.fn(() => ({
+ top: 100,
+ right: 50,
+ bottom: 100,
+ left: 50,
+ })),
+ coordsChar: jest.fn(() => ({ line: 6, ch: 30 })),
+ lineAtHeight: jest.fn(() => 300),
+ markText: jest.fn(),
+ scrollTo: jest.fn(),
+ defaultCharWidth: jest.fn(() => 8),
+ defaultTextHeight: jest.fn(() => 16),
+};
+
+const editor = { codeMirror };
+
+describe("scrollToPosition", () => {
+ it("calls codemirror APIs charCoords, getScrollerElement, scrollTo", () => {
+ scrollToPosition(codeMirror, 60, 123);
+ expect(codeMirror.charCoords).toHaveBeenCalledWith(
+ { line: 60, ch: 123 },
+ "local"
+ );
+ expect(codeMirror.scrollTo).toHaveBeenCalledWith(0, 50);
+ });
+});
+
+describe("markText", () => {
+ it("calls codemirror API markText & returns marker", () => {
+ const loc = {
+ start: { line: 10, column: 0 },
+ end: { line: 30, column: 50 },
+ };
+ markText(editor, "test-123", loc);
+ expect(codeMirror.markText).toHaveBeenCalledWith(
+ { ch: loc.start.column, line: loc.start.line },
+ { ch: loc.end.column, line: loc.end.line },
+ { className: "test-123" }
+ );
+ });
+});
+
+describe("lineAtHeight", () => {
+ it("calls codemirror API lineAtHeight", () => {
+ const e = { clientX: 30, clientY: 60 };
+ expect(lineAtHeight(editor, "test-123", e)).toEqual(301);
+ expect(editor.codeMirror.lineAtHeight).toHaveBeenCalledWith(e.clientY);
+ });
+});
+
+describe("getSourceLocationFromMouseEvent", () => {
+ it("calls codemirror API coordsChar & returns location", () => {
+ const source = makeMockSource(undefined, "test-123");
+ const e = { clientX: 30, clientY: 60 };
+ expect(getSourceLocationFromMouseEvent(editor, source, e)).toEqual({
+ source,
+ line: 7,
+ column: 31,
+ sourceActorId: undefined,
+ sourceActor: null,
+ });
+ expect(editor.codeMirror.coordsChar).toHaveBeenCalledWith({
+ left: 30,
+ top: 60,
+ });
+ });
+});
+
+describe("forEachLine", () => {
+ it("calls codemirror API operation && doc.iter across a doc", () => {
+ const test = jest.fn();
+ forEachLine(codeMirror, test);
+ expect(codeMirror.operation).toHaveBeenCalled();
+ expect(codeMirror.doc.iter).toHaveBeenCalledWith(0, 100, test);
+ });
+});
+
+describe("removeLineClass", () => {
+ it("calls codemirror API removeLineClass", () => {
+ const line = 3;
+ const className = "test-class";
+ removeLineClass(codeMirror, line, className);
+ expect(codeMirror.removeLineClass).toHaveBeenCalledWith(
+ line,
+ "wrap",
+ className
+ );
+ });
+});
+
+describe("clearLineClass", () => {
+ it("Uses forEachLine & removeLineClass to clear class on all lines", () => {
+ codeMirror.operation.mockClear();
+ codeMirror.doc.iter.mockClear();
+ codeMirror.removeLineClass.mockClear();
+ clearLineClass(codeMirror, "test-class");
+ expect(codeMirror.operation).toHaveBeenCalled();
+ expect(codeMirror.doc.iter).toHaveBeenCalledWith(
+ 0,
+ 100,
+ expect.any(Function)
+ );
+ expect(codeMirror.removeLineClass).toHaveBeenCalled();
+ });
+});
+
+describe("getTextForLine", () => {
+ it("calls codemirror API getLine & returns line text", () => {
+ getTextForLine(codeMirror, 3);
+ expect(codeMirror.getLine).toHaveBeenCalledWith(2);
+ });
+});
+describe("getCursorLine", () => {
+ it("calls codemirror API getCursor & returns line number", () => {
+ getCursorLine(codeMirror);
+ expect(codeMirror.getCursor).toHaveBeenCalled();
+ });
+});
diff --git a/devtools/client/debugger/src/utils/editor/tests/source-documents.spec.js b/devtools/client/debugger/src/utils/editor/tests/source-documents.spec.js
new file mode 100644
index 0000000000..f85c6b43ff
--- /dev/null
+++ b/devtools/client/debugger/src/utils/editor/tests/source-documents.spec.js
@@ -0,0 +1,213 @@
+/* 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 { getMode } from "../source-documents.js";
+
+import {
+ makeMockSourceWithContent,
+ makeMockWasmSourceWithContent,
+} from "../../test-mockup";
+
+const defaultSymbolDeclarations = {
+ classes: [],
+ functions: [],
+ memberExpressions: [],
+ objectProperties: [],
+ identifiers: [],
+ comments: [],
+ literals: [],
+ hasJsx: false,
+ hasTypes: false,
+ framework: undefined,
+};
+
+describe("source-documents", () => {
+ describe("getMode", () => {
+ it("//", () => {
+ const source = makeMockSourceWithContent(
+ undefined,
+ undefined,
+ "text/javascript",
+ "// @flow"
+ );
+ expect(getMode(source, source.content)).toEqual({
+ name: "javascript",
+ typescript: true,
+ });
+ });
+
+ it("/* @flow */", () => {
+ const source = makeMockSourceWithContent(
+ undefined,
+ undefined,
+ "text/javascript",
+ " /* @flow */"
+ );
+ expect(getMode(source, source.content)).toEqual({
+ name: "javascript",
+ typescript: true,
+ });
+ });
+
+ it("mixed html", () => {
+ const source = makeMockSourceWithContent(
+ undefined,
+ undefined,
+ "",
+ " <html"
+ );
+ expect(getMode(source, source.content)).toEqual({ name: "htmlmixed" });
+ });
+
+ it("elm", () => {
+ const source = makeMockSourceWithContent(
+ undefined,
+ undefined,
+ "text/x-elm",
+ 'main = text "Hello, World!"'
+ );
+ expect(getMode(source, source.content)).toEqual({ name: "elm" });
+ });
+
+ it("returns jsx if contentType jsx is given", () => {
+ const source = makeMockSourceWithContent(
+ undefined,
+ undefined,
+ "text/jsx",
+ "<h1></h1>"
+ );
+ expect(getMode(source, source.content)).toEqual({ name: "jsx" });
+ });
+
+ it("returns jsx if sourceMetaData says it's a react component", () => {
+ const source = makeMockSourceWithContent(
+ undefined,
+ undefined,
+ "",
+ "<h1></h1>"
+ );
+ expect(
+ getMode(source, source.content, {
+ ...defaultSymbolDeclarations,
+ hasJsx: true,
+ })
+ ).toEqual({ name: "jsx" });
+ });
+
+ it("returns jsx if the fileExtension is .jsx", () => {
+ const source = makeMockSourceWithContent(
+ "myComponent.jsx",
+ undefined,
+ "",
+ "<h1></h1>"
+ );
+ expect(getMode(source, source.content)).toEqual({ name: "jsx" });
+ });
+
+ it("returns text/x-haxe if the file extension is .hx", () => {
+ const source = makeMockSourceWithContent(
+ "myComponent.hx",
+ undefined,
+ "",
+ "function foo(){}"
+ );
+ expect(getMode(source, source.content)).toEqual({ name: "text/x-haxe" });
+ });
+
+ it("typescript", () => {
+ const source = makeMockSourceWithContent(
+ undefined,
+ undefined,
+ "text/typescript",
+ "function foo(){}"
+ );
+ expect(getMode(source, source.content)).toEqual({
+ name: "javascript",
+ typescript: true,
+ });
+ });
+
+ it("typescript-jsx", () => {
+ const source = makeMockSourceWithContent(
+ undefined,
+ undefined,
+ "text/typescript-jsx",
+ "<h1></h1>"
+ );
+ expect(getMode(source, source.content).base).toEqual({
+ name: "javascript",
+ typescript: true,
+ });
+ });
+
+ it("cross-platform clojure(script) with reader conditionals", () => {
+ const source = makeMockSourceWithContent(
+ "my-clojurescript-source-with-reader-conditionals.cljc",
+ undefined,
+ "text/x-clojure",
+ "(defn str->int [s] " +
+ " #?(:clj (java.lang.Integer/parseInt s) " +
+ " :cljs (js/parseInt s)))"
+ );
+ expect(getMode(source, source.content)).toEqual({ name: "clojure" });
+ });
+
+ it("clojurescript", () => {
+ const source = makeMockSourceWithContent(
+ "my-clojurescript-source.cljs",
+ undefined,
+ "text/x-clojurescript",
+ "(+ 1 2 3)"
+ );
+ expect(getMode(source, source.content)).toEqual({ name: "clojure" });
+ });
+
+ it("coffeescript", () => {
+ const source = makeMockSourceWithContent(
+ undefined,
+ undefined,
+ "text/coffeescript",
+ "x = (a) -> 3"
+ );
+ expect(getMode(source, source.content)).toEqual({ name: "coffeescript" });
+ });
+
+ it("wasm", () => {
+ const source = makeMockWasmSourceWithContent({
+ binary: "\x00asm\x01\x00\x00\x00",
+ });
+ expect(getMode(source, source.content.value)).toEqual({ name: "text" });
+ });
+
+ it("marko", () => {
+ const source = makeMockSourceWithContent(
+ "http://localhost.com:7999/increment/sometestfile.marko",
+ undefined,
+ "does not matter",
+ "function foo(){}"
+ );
+ expect(getMode(source, source.content)).toEqual({ name: "javascript" });
+ });
+
+ it("es6", () => {
+ const source = makeMockSourceWithContent(
+ "http://localhost.com:7999/increment/sometestfile.es6",
+ undefined,
+ "does not matter",
+ "function foo(){}"
+ );
+ expect(getMode(source, source.content)).toEqual({ name: "javascript" });
+ });
+
+ it("vue", () => {
+ const source = makeMockSourceWithContent(
+ "http://localhost.com:7999/increment/sometestfile.vue?query=string",
+ undefined,
+ "does not matter",
+ "function foo(){}"
+ );
+ expect(getMode(source, source.content)).toEqual({ name: "javascript" });
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/utils/editor/tests/source-search.spec.js b/devtools/client/debugger/src/utils/editor/tests/source-search.spec.js
new file mode 100644
index 0000000000..33f479766a
--- /dev/null
+++ b/devtools/client/debugger/src/utils/editor/tests/source-search.spec.js
@@ -0,0 +1,182 @@
+/* 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 {
+ find,
+ searchSourceForHighlight,
+ getMatchIndex,
+ removeOverlay,
+} from "../source-search";
+
+const getCursor = jest.fn(() => ({ line: 90, ch: 54 }));
+const cursor = {
+ find: jest.fn(),
+ from: jest.fn(),
+ to: jest.fn(),
+};
+const getSearchCursor = jest.fn(() => cursor);
+const modifiers = {
+ caseSensitive: false,
+ regexMatch: false,
+ wholeWord: false,
+};
+
+const getCM = () => ({
+ operation: jest.fn(cb => cb()),
+ addOverlay: jest.fn(),
+ removeOverlay: jest.fn(),
+ getCursor,
+ getSearchCursor,
+ firstLine: jest.fn(),
+ state: {},
+});
+
+describe("source-search", () => {
+ describe("find", () => {
+ it("calls into CodeMirror APIs via clearSearch & doSearch", () => {
+ const ctx = { cm: getCM() };
+ expect(ctx.cm.state).toEqual({});
+ find(ctx, "test", false, modifiers);
+ // First we check the APIs called via clearSearch
+ expect(ctx.cm.removeOverlay).toHaveBeenCalledWith(null);
+ // Next those via doSearch
+ expect(ctx.cm.operation).toHaveBeenCalled();
+ expect(ctx.cm.removeOverlay).toHaveBeenCalledWith(null);
+ expect(ctx.cm.addOverlay).toHaveBeenCalledWith(
+ { token: expect.any(Function) },
+ { opaque: false }
+ );
+ expect(ctx.cm.getCursor).toHaveBeenCalledWith("anchor");
+ expect(ctx.cm.getCursor).toHaveBeenCalledWith("head");
+ const search = {
+ query: "test",
+ posTo: { line: 0, ch: 0 },
+ posFrom: { line: 0, ch: 0 },
+ overlay: { token: expect.any(Function) },
+ results: [],
+ };
+ expect(ctx.cm.state).toEqual({ search });
+ });
+
+ it("clears a previous overlay", () => {
+ const ctx = { cm: getCM() };
+ ctx.cm.state.search = {
+ query: "foo",
+ posTo: null,
+ posFrom: null,
+ overlay: { token: expect.any(Function) },
+ results: [],
+ };
+ find(ctx, "test", true, modifiers);
+ expect(ctx.cm.removeOverlay).toHaveBeenCalledWith({
+ token: expect.any(Function),
+ });
+ });
+
+ it("clears for empty queries", () => {
+ const ctx = { cm: getCM() };
+ ctx.cm.state.search = {
+ query: "foo",
+ posTo: null,
+ posFrom: null,
+ overlay: null,
+ results: [],
+ };
+ find(ctx, "", true, modifiers);
+ expect(ctx.cm.removeOverlay).toHaveBeenCalledWith(null);
+ ctx.cm.removeOverlay.mockClear();
+ ctx.cm.state.search.query = "bar";
+ find(ctx, "", true, modifiers);
+ expect(ctx.cm.removeOverlay).toHaveBeenCalledWith(null);
+ });
+ });
+
+ describe("searchSourceForHighlight", () => {
+ it("calls into CodeMirror APIs and sets the correct selection", () => {
+ const line = 15;
+ const from = { line, ch: 1 };
+ const to = { line, ch: 5 };
+ const cm = {
+ ...getCM(),
+ setSelection: jest.fn(),
+ getSearchCursor: () => ({
+ find: () => true,
+ from: () => from,
+ to: () => to,
+ }),
+ };
+ const ed = { alignLine: jest.fn() };
+ const ctx = { cm, ed };
+
+ expect(ctx.cm.state).toEqual({});
+ searchSourceForHighlight(ctx, false, "test", false, modifiers, line, 1);
+
+ expect(ctx.cm.operation).toHaveBeenCalled();
+ expect(ctx.cm.removeOverlay).toHaveBeenCalledWith(null);
+ expect(ctx.cm.addOverlay).toHaveBeenCalledWith(
+ { token: expect.any(Function) },
+ { opaque: false }
+ );
+ expect(ctx.cm.getCursor).toHaveBeenCalledWith("anchor");
+ expect(ctx.cm.getCursor).toHaveBeenCalledWith("head");
+ expect(ed.alignLine).toHaveBeenCalledWith(line, "center");
+ expect(cm.setSelection).toHaveBeenCalledWith(from, to);
+ });
+ });
+
+ describe("findNext", () => {});
+
+ describe("findPrev", () => {});
+
+ describe("getMatchIndex", () => {
+ it("iterates in the matches", () => {
+ const count = 3;
+
+ // reverse 2, 1, 0, 2
+
+ let matchIndex = getMatchIndex(count, 2, true);
+ expect(matchIndex).toBe(1);
+
+ matchIndex = getMatchIndex(count, 1, true);
+ expect(matchIndex).toBe(0);
+
+ matchIndex = getMatchIndex(count, 0, true);
+ expect(matchIndex).toBe(2);
+
+ // forward 1, 2, 0, 1
+
+ matchIndex = getMatchIndex(count, 1, false);
+ expect(matchIndex).toBe(2);
+
+ matchIndex = getMatchIndex(count, 2, false);
+ expect(matchIndex).toBe(0);
+
+ matchIndex = getMatchIndex(count, 0, false);
+ expect(matchIndex).toBe(1);
+ });
+ });
+
+ describe("removeOverlay", () => {
+ it("calls CodeMirror APIs: removeOverlay, getCursor & setSelection", () => {
+ const ctx = {
+ cm: {
+ removeOverlay: jest.fn(),
+ getCursor,
+ state: {},
+ doc: {
+ setSelection: jest.fn(),
+ },
+ },
+ };
+ removeOverlay(ctx, "test");
+ expect(ctx.cm.removeOverlay).toHaveBeenCalled();
+ expect(ctx.cm.getCursor).toHaveBeenCalled();
+ expect(ctx.cm.doc.setSelection).toHaveBeenCalledWith(
+ { line: 90, ch: 54 },
+ { line: 90, ch: 54 },
+ { scroll: false }
+ );
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/utils/editor/tokens.js b/devtools/client/debugger/src/utils/editor/tokens.js
new file mode 100644
index 0000000000..f8783c02fe
--- /dev/null
+++ b/devtools/client/debugger/src/utils/editor/tokens.js
@@ -0,0 +1,178 @@
+/* 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/>. */
+
+function _isInvalidTarget(target) {
+ if (!target || !target.innerText) {
+ return true;
+ }
+
+ const tokenText = target.innerText.trim();
+
+ // exclude syntax where the expression would be a syntax error
+ const invalidToken =
+ tokenText === "" || tokenText.match(/^[(){}\|&%,.;=<>\+-/\*\s](?=)/);
+ if (invalidToken) {
+ return true;
+ }
+
+ // exclude tokens for which it does not make sense to show a preview:
+ // - literal
+ // - primitives
+ // - operators
+ // - tags
+ const INVALID_TARGET_CLASSES = [
+ "cm-atom",
+ "cm-number",
+ "cm-operator",
+ "cm-string",
+ "cm-tag",
+ // also exclude editor element (defined in Editor component)
+ "editor-mount",
+ ];
+ if (
+ target.className === "" ||
+ INVALID_TARGET_CLASSES.some(cls => target.classList.contains(cls))
+ ) {
+ return true;
+ }
+
+ // We need to exclude keywords, but since codeMirror tags "this" as a keyword, we need
+ // to check the tokenText as well.
+ // This seems to be the only case that we want to exclude (see devtools/client/shared/sourceeditor/codemirror/mode/javascript/javascript.js#24-41)
+ if (target.classList.contains("cm-keyword") && tokenText !== "this") {
+ return true;
+ }
+
+ // exclude codemirror elements that are not tokens
+ if (
+ // exclude inline preview
+ target.closest(".CodeMirror-widget") ||
+ // exclude in-line "empty" space, as well as the gutter
+ target.matches(".CodeMirror-line, .CodeMirror-gutter-elt") ||
+ target.getBoundingClientRect().top == 0
+ ) {
+ return true;
+ }
+
+ // exclude popup
+ if (target.closest(".popover")) {
+ return true;
+ }
+
+ return false;
+}
+
+function _dispatch(codeMirror, eventName, data) {
+ codeMirror.constructor.signal(codeMirror, eventName, data);
+}
+
+function _invalidLeaveTarget(target) {
+ if (!target || target.closest(".popover")) {
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * Wraps the codemirror mouse events to generate token events
+ * @param {*} codeMirror
+ * @returns
+ */
+export function onMouseOver(codeMirror) {
+ let prevTokenPos = null;
+
+ function onMouseLeave(event) {
+ if (_invalidLeaveTarget(event.relatedTarget)) {
+ addMouseLeave(event.target);
+ return;
+ }
+
+ prevTokenPos = null;
+ _dispatch(codeMirror, "tokenleave", event);
+ }
+
+ function addMouseLeave(target) {
+ target.addEventListener("mouseleave", onMouseLeave, {
+ capture: true,
+ once: true,
+ });
+ }
+
+ return enterEvent => {
+ const { target } = enterEvent;
+
+ if (_isInvalidTarget(target)) {
+ return;
+ }
+
+ const tokenPos = getTokenLocation(codeMirror, target);
+
+ if (
+ prevTokenPos?.line !== tokenPos?.line ||
+ prevTokenPos?.column !== tokenPos?.column
+ ) {
+ addMouseLeave(target);
+
+ _dispatch(codeMirror, "tokenenter", {
+ event: enterEvent,
+ target,
+ tokenPos,
+ });
+ prevTokenPos = tokenPos;
+ }
+ };
+}
+
+/**
+ * Gets the end position of a token at a specific line/column
+ *
+ * @param {*} codeMirror
+ * @param {Number} line
+ * @param {Number} column
+ * @returns {Number}
+ */
+export function getTokenEnd(codeMirror, line, column) {
+ const token = codeMirror.getTokenAt({
+ line,
+ ch: column + 1,
+ });
+ const tokenString = token.string;
+
+ return tokenString === "{" || tokenString === "[" ? null : token.end;
+}
+
+/**
+ * Given the dom element related to the token, this gets its line and column.
+ *
+ * @param {*} codeMirror
+ * @param {*} tokenEl
+ * @returns {Object} An object of the form { line, column }
+ */
+export function getTokenLocation(codeMirror, tokenEl) {
+ // Get the quad (and not the bounding rect), as the span could wrap on multiple lines
+ // and the middle of the bounding rect may not be over the token:
+ // +───────────────────────+
+ // │ myLongVariableNa│
+ // │me + │
+ // +───────────────────────+
+ const { p1, p2, p3 } = tokenEl.getBoxQuads()[0];
+ const left = p1.x + (p2.x - p1.x) / 2;
+ const top = p1.y + (p3.y - p1.y) / 2;
+ const { line, ch } = codeMirror.coordsChar(
+ {
+ left,
+ top,
+ },
+ // Use the "window" context where the coordinates are relative to the top-left corner
+ // of the currently visible (scrolled) window.
+ // This enables codemirror also correctly handle wrappped lines in the editor.
+ "window"
+ );
+
+ return {
+ line: line + 1,
+ column: ch,
+ };
+}
diff --git a/devtools/client/debugger/src/utils/environment.js b/devtools/client/debugger/src/utils/environment.js
new file mode 100644
index 0000000000..6f68dd793c
--- /dev/null
+++ b/devtools/client/debugger/src/utils/environment.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 isNode() {
+ try {
+ return process.release.name == "node";
+ } catch (e) {
+ return false;
+ }
+}
+
+export function isNodeTest() {
+ return isNode() && process.env.NODE_ENV != "production";
+}
diff --git a/devtools/client/debugger/src/utils/evaluation-result.js b/devtools/client/debugger/src/utils/evaluation-result.js
new file mode 100644
index 0000000000..def02aacfa
--- /dev/null
+++ b/devtools/client/debugger/src/utils/evaluation-result.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/>. */
+
+function isFront(result) {
+ return !!result && typeof result === "object" && !!result.getGrip;
+}
+
+export function getGrip(result) {
+ if (isFront(result)) {
+ return result.getGrip();
+ }
+
+ return result;
+}
+
+export function getFront(result) {
+ return isFront(result) ? result : null;
+}
diff --git a/devtools/client/debugger/src/utils/expressions.js b/devtools/client/debugger/src/utils/expressions.js
new file mode 100644
index 0000000000..f30d0c089c
--- /dev/null
+++ b/devtools/client/debugger/src/utils/expressions.js
@@ -0,0 +1,67 @@
+/* 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 { correctIndentation } from "./indentation";
+import { getGrip, getFront } from "./evaluation-result";
+
+const UNAVAILABLE_GRIP = { unavailable: true };
+
+/*
+ * wrap the expression input in a try/catch so that it can be safely
+ * evaluated.
+ *
+ * NOTE: we add line after the expression to protect against comments.
+ */
+export function wrapExpression(input) {
+ return correctIndentation(`
+ try {
+ ${input}
+ } catch (e) {
+ e
+ }
+ `);
+}
+
+function isUnavailable(value) {
+ return (
+ value &&
+ !!value.isError &&
+ (value.class === "ReferenceError" || value.class === "TypeError")
+ );
+}
+
+/**
+ *
+ * @param {Object} expression: Expression item as stored in state.expressions in reducers/expressions.js
+ * @param {String} expression.input: evaluated expression string
+ * @param {Object} expression.value: evaluated expression result object as returned from ScriptCommand#execute
+ * @param {Object} expression.value.result: expression result, might be a primitive, a grip or a front
+ * @param {Object} expression.value.exception: expression result error, might be a primitive, a grip or a front
+ * @returns {Object} an object of the following shape:
+ * - expressionResultGrip: A primitive or a grip
+ * - expressionResultFront: An object front if it exists, or undefined
+ */
+export function getExpressionResultGripAndFront(expression) {
+ const { value } = expression;
+
+ if (!value) {
+ return { expressionResultGrip: UNAVAILABLE_GRIP };
+ }
+
+ const expressionResultReturn = value.exception || value.result;
+ const valueGrip = getGrip(expressionResultReturn);
+ if (!valueGrip || isUnavailable(valueGrip)) {
+ return { expressionResultGrip: UNAVAILABLE_GRIP };
+ }
+
+ if (valueGrip.isError) {
+ const { name, message } = valueGrip.preview;
+ return { expressionResultGrip: `${name}: ${message}` };
+ }
+
+ return {
+ expressionResultGrip: valueGrip,
+ expressionResultFront: getFront(expressionResultReturn),
+ };
+}
diff --git a/devtools/client/debugger/src/utils/function.js b/devtools/client/debugger/src/utils/function.js
new file mode 100644
index 0000000000..39d0f5a9a7
--- /dev/null
+++ b/devtools/client/debugger/src/utils/function.js
@@ -0,0 +1,37 @@
+/* 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 { isFulfilled } from "./async-value";
+import { findClosestFunction } from "./ast";
+import { correctIndentation } from "./indentation";
+
+export function findFunctionText(line, source, sourceTextContent, symbols) {
+ const func = findClosestFunction(symbols, {
+ sourceId: source.id,
+ line,
+ column: Infinity,
+ });
+
+ if (
+ source.isWasm ||
+ !func ||
+ !sourceTextContent ||
+ !isFulfilled(sourceTextContent) ||
+ sourceTextContent.value.type !== "text"
+ ) {
+ return null;
+ }
+
+ const {
+ location: { start, end },
+ } = func;
+ const lines = sourceTextContent.value.value.split("\n");
+ const firstLine = lines[start.line - 1].slice(start.column);
+ const lastLine = lines[end.line - 1].slice(0, end.column);
+ const middle = lines.slice(start.line, end.line - 1);
+ const functionText = [firstLine, ...middle, lastLine].join("\n");
+ const indentedFunctionText = correctIndentation(functionText);
+
+ return indentedFunctionText;
+}
diff --git a/devtools/client/debugger/src/utils/indentation.js b/devtools/client/debugger/src/utils/indentation.js
new file mode 100644
index 0000000000..80e5014f6d
--- /dev/null
+++ b/devtools/client/debugger/src/utils/indentation.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/>. */
+
+export function getIndentation(line) {
+ if (!line) {
+ return 0;
+ }
+
+ const lineMatch = line.match(/^\s*/);
+ if (!lineMatch) {
+ return 0;
+ }
+
+ return lineMatch[0].length;
+}
+
+function getMaxIndentation(lines) {
+ const firstLine = lines[0];
+ const secondLine = lines[1];
+ const lastLine = lines[lines.length - 1];
+
+ const indentations = [
+ getIndentation(firstLine),
+ getIndentation(secondLine),
+ getIndentation(lastLine),
+ ];
+
+ return Math.max(...indentations);
+}
+
+export function correctIndentation(text) {
+ const lines = text.trim().split("\n");
+ const indentation = getMaxIndentation(lines);
+ const formattedLines = lines.map(_line =>
+ _line.replace(new RegExp(`^\\s{0,${indentation - 1}}`), "")
+ );
+
+ return formattedLines.join("\n");
+}
diff --git a/devtools/client/debugger/src/utils/isMinified.js b/devtools/client/debugger/src/utils/isMinified.js
new file mode 100644
index 0000000000..dc5963b1b2
--- /dev/null
+++ b/devtools/client/debugger/src/utils/isMinified.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 { isFulfilled } from "./async-value";
+
+// Used to detect minification for automatic pretty printing
+const SAMPLE_SIZE = 50;
+const INDENT_COUNT_THRESHOLD = 5;
+const CHARACTER_LIMIT = 250;
+const _minifiedCache = new Map();
+
+export function isMinified(source, sourceTextContent) {
+ if (_minifiedCache.has(source.id)) {
+ return _minifiedCache.get(source.id);
+ }
+
+ if (
+ !sourceTextContent ||
+ !isFulfilled(sourceTextContent) ||
+ sourceTextContent.value.type !== "text"
+ ) {
+ return false;
+ }
+
+ let text = sourceTextContent.value.value;
+
+ let lineEndIndex = 0;
+ let lineStartIndex = 0;
+ let lines = 0;
+ let indentCount = 0;
+ let overCharLimit = false;
+
+ // Strip comments.
+ text = text.replace(/\/\*[\S\s]*?\*\/|\/\/(.+|\n)/g, "");
+
+ while (lines++ < SAMPLE_SIZE) {
+ lineEndIndex = text.indexOf("\n", lineStartIndex);
+ if (lineEndIndex == -1) {
+ break;
+ }
+ if (/^\s+/.test(text.slice(lineStartIndex, lineEndIndex))) {
+ indentCount++;
+ }
+ // For files with no indents but are not minified.
+ if (lineEndIndex - lineStartIndex > CHARACTER_LIMIT) {
+ overCharLimit = true;
+ break;
+ }
+ lineStartIndex = lineEndIndex + 1;
+ }
+
+ const minified =
+ (indentCount / lines) * 100 < INDENT_COUNT_THRESHOLD || overCharLimit;
+
+ _minifiedCache.set(source.id, minified);
+ return minified;
+}
diff --git a/devtools/client/debugger/src/utils/location.js b/devtools/client/debugger/src/utils/location.js
new file mode 100644
index 0000000000..46e5c4ae05
--- /dev/null
+++ b/devtools/client/debugger/src/utils/location.js
@@ -0,0 +1,124 @@
+/* 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 { getSelectedLocation } from "./selected-location";
+import { getSource } from "../selectors/index";
+
+/**
+ * Note that arguments can be created via `createLocation`.
+ * But they can also be created via `createPendingLocation` in reducer/pending-breakpoints.js.
+ * Both will have similar line and column attributes.
+ */
+export function comparePosition(a, b) {
+ return a && b && a.line == b.line && a.column == b.column;
+}
+
+export function createLocation({
+ source,
+ sourceActor = null,
+
+ // Line 0 represents no specific line chosen for action
+ line = 0,
+ column,
+}) {
+ return {
+ source,
+ sourceActor,
+ sourceActorId: sourceActor?.id,
+
+ // `line` and `column` are 1-based.
+ // This data is mostly coming from and driven by
+ // JSScript::lineno and JSScript::column
+ // https://searchfox.org/mozilla-central/rev/90dce6b0223b4dc17bb10f1125b44f70951585f9/js/src/vm/JSScript.h#1545-1548
+ line,
+ column,
+ };
+}
+
+/**
+ * Convert location objects created via `createLocation` into
+ * the format used by the Source Map Loader/Worker.
+ * It only needs sourceId, line and column attributes.
+ */
+export function debuggerToSourceMapLocation(location) {
+ return {
+ sourceId: location.source.id,
+ // In case of errors loading the source, we might not have a precise location.
+ // Defaults to first line and column.
+ line: location.line || 1,
+ column: location.column || 0,
+ };
+}
+
+/**
+ * Pending location only need these three attributes,
+ * and especially doesn't need the large source and sourceActor objects of the regular location objects.
+ *
+ * @param {Object} location
+ */
+export function createPendingSelectedLocation(location) {
+ return {
+ url: location.source.url,
+
+ line: location.line,
+ column: location.column,
+ };
+}
+
+export function sortSelectedLocations(locations, selectedSource) {
+ return Array.from(locations).sort((locationA, locationB) => {
+ const aSelected = getSelectedLocation(locationA, selectedSource);
+ const bSelected = getSelectedLocation(locationB, selectedSource);
+
+ // Order the locations by line number…
+ if (aSelected.line < bSelected.line) {
+ return -1;
+ }
+
+ if (aSelected.line > bSelected.line) {
+ return 1;
+ }
+
+ // … and if we have the same line, we want to return location with undefined columns
+ // first, and then order them by column
+ if (aSelected.column == bSelected.column) {
+ return 0;
+ }
+
+ if (aSelected.column === undefined) {
+ return -1;
+ }
+
+ if (bSelected.column === undefined) {
+ return 1;
+ }
+
+ return aSelected.column < bSelected.column ? -1 : 1;
+ });
+}
+
+/**
+ * Source map Loader/Worker and debugger frontend don't use the same objects for locations.
+ * Worker uses 'sourceId' attributes whereas the frontend has 'source' attribute.
+ */
+export function sourceMapToDebuggerLocation(state, location) {
+ // From MapScopes modules, we might re-process the exact same location objects
+ // for which we would already have computed the source object,
+ // and which would lack sourceId attribute.
+ if (location.source) {
+ return location;
+ }
+
+ // SourceMapLoader doesn't known about debugger's source objects
+ // so that we have to fetch it from here
+ const source = getSource(state, location.sourceId);
+ if (!source) {
+ throw new Error(`Could not find source-map source ${location.sourceId}`);
+ }
+
+ return createLocation({
+ ...location,
+ source,
+ });
+}
diff --git a/devtools/client/debugger/src/utils/log.js b/devtools/client/debugger/src/utils/log.js
new file mode 100644
index 0000000000..6e2e3b7b15
--- /dev/null
+++ b/devtools/client/debugger/src/utils/log.js
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+/* */
+
+/**
+ *
+ * Utils for logging to the console
+ * Suppresses logging in non-development environment
+ *
+ * @module utils/log
+ */
+
+import { prefs } from "./prefs";
+
+/**
+ * Produces a formatted console log line by imploding args, prefixed by [log]
+ *
+ * function input: log(["hello", "world"])
+ * console output: [log] hello world
+ *
+ * @memberof utils/log
+ * @static
+ */
+export function log(...args) {
+ if (prefs.logging) {
+ console.log(...args);
+ }
+}
diff --git a/devtools/client/debugger/src/utils/memoizableAction.js b/devtools/client/debugger/src/utils/memoizableAction.js
new file mode 100644
index 0000000000..0f465177e2
--- /dev/null
+++ b/devtools/client/debugger/src/utils/memoizableAction.js
@@ -0,0 +1,75 @@
+/* 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 "./async-value";
+import { validateContext } from "./context";
+
+/*
+ * memoizableActon is a utility for actions that should only be performed
+ * once per key. It is useful for loading sources, parsing symbols ...
+ *
+ * @getValue - gets the result from the redux store
+ * @createKey - creates a key for the requests map
+ * @action - kicks off the async work for the action
+ *
+ *
+ * For Example
+ *
+ * export const setItem = memoizeableAction(
+ * "setItem",
+ * {
+ * hasValue: ({ a }, { getState }) => hasItem(getState(), a),
+ * getValue: ({ a }, { getState }) => getItem(getState(), a),
+ * createKey: ({ a }) => a,
+ * action: ({ a }, thunkArgs) => doSetItem(a, thunkArgs)
+ * }
+ * );
+ *
+ */
+export function memoizeableAction(name, { getValue, createKey, action }) {
+ const requests = new Map();
+ return args => async thunkArgs => {
+ let result = asSettled(getValue(args, thunkArgs));
+ if (!result) {
+ const key = createKey(args, thunkArgs);
+ if (!requests.has(key)) {
+ requests.set(
+ key,
+ (async () => {
+ try {
+ await action(args, thunkArgs);
+ } catch (e) {
+ console.warn(`Action ${name} had an exception:`, e);
+ } finally {
+ requests.delete(key);
+ }
+ })()
+ );
+ }
+
+ await requests.get(key);
+
+ if (args.cx) {
+ validateContext(thunkArgs.getState(), args.cx);
+ }
+
+ result = asSettled(getValue(args, thunkArgs));
+ if (!result) {
+ // Returning null here is not ideal. This means that the action
+ // resolved but 'getValue' didn't return a loaded value, for instance
+ // if the data the action was meant to store was deleted. In a perfect
+ // world we'd throw a ContextError here or handle cancellation somehow.
+ // Throwing will also allow us to change the return type on the action
+ // to always return a promise for the getValue AsyncValue type, but
+ // for now we have to add an additional '| null' for this case.
+ return null;
+ }
+ }
+
+ if (result.state === "rejected") {
+ throw result.value;
+ }
+ return result.value;
+ };
+}
diff --git a/devtools/client/debugger/src/utils/memoize.js b/devtools/client/debugger/src/utils/memoize.js
new file mode 100644
index 0000000000..3f634b326d
--- /dev/null
+++ b/devtools/client/debugger/src/utils/memoize.js
@@ -0,0 +1,63 @@
+/* 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/>. */
+
+function hasValue(keys, store) {
+ let currentStore = store;
+ for (const key of keys) {
+ if (!currentStore || !currentStore.has(key)) {
+ return false;
+ }
+
+ currentStore = currentStore.get(key);
+ }
+ return true;
+}
+
+function getValue(keys, store) {
+ let currentStore = store;
+ for (const key of keys) {
+ if (!currentStore) {
+ return null;
+ }
+ currentStore = currentStore.get(key);
+ }
+
+ return currentStore;
+}
+
+function setValue(keys, store, value) {
+ const keysExceptLast = keys.slice(0, -1);
+ const lastKey = keys[keys.length - 1];
+
+ let currentStore = store;
+ for (const key of keysExceptLast) {
+ if (!currentStore) {
+ return;
+ }
+
+ if (!currentStore.has(key)) {
+ currentStore.set(key, new WeakMap());
+ }
+ currentStore = currentStore.get(key);
+ }
+
+ if (currentStore) {
+ currentStore.set(lastKey, value);
+ }
+}
+
+// memoize with n arguments
+export default function memoize(func) {
+ const store = new WeakMap();
+
+ return function (...keys) {
+ if (hasValue(keys, store)) {
+ return getValue(keys, store);
+ }
+
+ const newValue = func.apply(null, keys);
+ setValue(keys, store, newValue);
+ return newValue;
+ };
+}
diff --git a/devtools/client/debugger/src/utils/memoizeLast.js b/devtools/client/debugger/src/utils/memoizeLast.js
new file mode 100644
index 0000000000..b3c46cab57
--- /dev/null
+++ b/devtools/client/debugger/src/utils/memoizeLast.js
@@ -0,0 +1,27 @@
+/* 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 memoizeLast(fn) {
+ let lastArgs;
+ let lastResult;
+
+ const memoized = (...args) => {
+ if (
+ lastArgs &&
+ args.length === lastArgs.length &&
+ args.every((arg, i) => arg === lastArgs[i])
+ ) {
+ return lastResult;
+ }
+
+ lastArgs = args;
+ lastResult = fn(...args);
+
+ return lastResult;
+ };
+
+ return memoized;
+}
+
+export default memoizeLast;
diff --git a/devtools/client/debugger/src/utils/moz.build b/devtools/client/debugger/src/utils/moz.build
new file mode 100644
index 0000000000..8deb8e18db
--- /dev/null
+++ b/devtools/client/debugger/src/utils/moz.build
@@ -0,0 +1,53 @@
+# 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 += [
+ "breakpoint",
+ "editor",
+ "pause",
+ "sources-tree",
+]
+
+CompiledModules(
+ "assert.js",
+ "ast.js",
+ "async-value.js",
+ "bootstrap.js",
+ "build-query.js",
+ "clipboard.js",
+ "context.js",
+ "dbg.js",
+ "DevToolsUtils.js",
+ "environment.js",
+ "expressions.js",
+ "evaluation-result.js",
+ "function.js",
+ "indentation.js",
+ "isMinified.js",
+ "location.js",
+ "log.js",
+ "memoize.js",
+ "memoizeLast.js",
+ "memoizableAction.js",
+ "path.js",
+ "prefs.js",
+ "preview.js",
+ "quick-open.js",
+ "result-list.js",
+ "selected-location.js",
+ "shallow-equal.js",
+ "source-maps.js",
+ "source-queue.js",
+ "source.js",
+ "tabs.js",
+ "task.js",
+ "telemetry.js",
+ "text.js",
+ "ui.js",
+ "url.js",
+ "utils.js",
+ "wasm.js",
+ "worker.js",
+)
diff --git a/devtools/client/debugger/src/utils/path.js b/devtools/client/debugger/src/utils/path.js
new file mode 100644
index 0000000000..bc7f975919
--- /dev/null
+++ b/devtools/client/debugger/src/utils/path.js
@@ -0,0 +1,24 @@
+/* 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 basename(path) {
+ return path.split("/").pop();
+}
+
+export function dirname(path) {
+ const idx = path.lastIndexOf("/");
+ return path.slice(0, idx);
+}
+
+export function isURL(str) {
+ return str.includes("://");
+}
+
+export function isAbsolute(str) {
+ return str[0] === "/";
+}
+
+export function join(base, dir) {
+ return `${base}/${dir}`;
+}
diff --git a/devtools/client/debugger/src/utils/pause/frames/annotateFrames.js b/devtools/client/debugger/src/utils/pause/frames/annotateFrames.js
new file mode 100644
index 0000000000..3cdc4b1c1e
--- /dev/null
+++ b/devtools/client/debugger/src/utils/pause/frames/annotateFrames.js
@@ -0,0 +1,68 @@
+/* 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 { getLibraryFromUrl } from "./getLibraryFromUrl";
+
+/**
+ * Augment all frame objects with a 'library' attribute.
+ */
+export function annotateFramesWithLibrary(frames) {
+ for (const frame of frames) {
+ frame.library = getLibraryFromUrl(frame, frames);
+ }
+
+ // Babel need some special treatment to recognize some particular async stack pattern
+ for (const idx of getBabelFrameIndexes(frames)) {
+ const frame = frames[idx];
+ frame.library = "Babel";
+ }
+}
+
+/**
+ * Returns all the indexes that are part of a babel async call stack.
+ *
+ * @param {Array<Object>} frames
+ * @returns Array<Integer>
+ */
+function getBabelFrameIndexes(frames) {
+ const startIndexes = [];
+ const endIndexes = [];
+
+ for (let index = 0, length = frames.length; index < length; index++) {
+ const frame = frames[index];
+ const frameUrl = frame.location.source.url;
+
+ if (
+ frame.displayName === "tryCatch" &&
+ frameUrl.match(/regenerator-runtime/i)
+ ) {
+ startIndexes.push(index);
+ }
+
+ if (startIndexes.length > endIndexes.length) {
+ if (frame.displayName === "flush" && frameUrl.match(/_microtask/i)) {
+ endIndexes.push(index);
+ }
+ if (frame.displayName === "_asyncToGenerator/<") {
+ endIndexes.push(index + 1);
+ }
+ }
+ }
+
+ if (startIndexes.length != endIndexes.length || startIndexes.length === 0) {
+ return [];
+ }
+
+ const babelFrameIndexes = [];
+ // We have the same number of start and end indexes, we can loop through one of them to
+ // build our async call stack index ranges
+ // e.g. if we have startIndexes: [1,5] and endIndexes: [3,8], we want to return [1,2,3,5,6,7,8]
+ startIndexes.forEach((startIndex, index) => {
+ const matchingEndIndex = endIndexes[index];
+ for (let i = startIndex; i <= matchingEndIndex; i++) {
+ babelFrameIndexes.push(i);
+ }
+ });
+ return babelFrameIndexes;
+}
diff --git a/devtools/client/debugger/src/utils/pause/frames/collapseFrames.js b/devtools/client/debugger/src/utils/pause/frames/collapseFrames.js
new file mode 100644
index 0000000000..185b66e26c
--- /dev/null
+++ b/devtools/client/debugger/src/utils/pause/frames/collapseFrames.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/>. */
+
+function collapseLastFrames(frames) {
+ const index = frames.findIndex(frame =>
+ frame.location.source.url?.match(/webpack\/bootstrap/i)
+ );
+
+ if (index == -1) {
+ return { newFrames: frames, lastGroup: [] };
+ }
+
+ const newFrames = frames.slice(0, index);
+ const lastGroup = frames.slice(index);
+ return { newFrames, lastGroup };
+}
+
+export function collapseFrames(frames) {
+ // We collapse groups of one so that user frames
+ // are not in a group of one
+ function addGroupToList(group, list) {
+ if (!group) {
+ return list;
+ }
+
+ if (group.length > 1) {
+ list.push(group);
+ } else {
+ list = list.concat(group);
+ }
+
+ return list;
+ }
+ const { newFrames, lastGroup } = collapseLastFrames(frames);
+ frames = newFrames;
+ let items = [];
+ let currentGroup = null;
+ let prevItem = null;
+ for (const frame of frames) {
+ const prevLibrary = prevItem?.library;
+
+ if (!currentGroup) {
+ currentGroup = [frame];
+ } else if (prevLibrary && prevLibrary == frame.library) {
+ currentGroup.push(frame);
+ } else {
+ items = addGroupToList(currentGroup, items);
+ currentGroup = [frame];
+ }
+
+ prevItem = frame;
+ }
+
+ items = addGroupToList(currentGroup, items);
+ items = addGroupToList(lastGroup, items);
+ return items;
+}
diff --git a/devtools/client/debugger/src/utils/pause/frames/displayName.js b/devtools/client/debugger/src/utils/pause/frames/displayName.js
new file mode 100644
index 0000000000..0a47e9ac04
--- /dev/null
+++ b/devtools/client/debugger/src/utils/pause/frames/displayName.js
@@ -0,0 +1,114 @@
+/* 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/>. */
+
+// Decodes an anonymous naming scheme that
+// spider monkey implements based on "Naming Anonymous JavaScript Functions"
+// http://johnjbarton.github.io/nonymous/index.html
+const objectProperty = /([\w\d\$#]+)$/;
+const arrayProperty = /\[(.*?)\]$/;
+const functionProperty = /([\w\d]+)[\/\.<]*?$/;
+const annonymousProperty = /([\w\d]+)\(\^\)$/;
+const displayNameScenarios = [
+ objectProperty,
+ arrayProperty,
+ functionProperty,
+ annonymousProperty,
+];
+const includeSpace = /\s/;
+export function simplifyDisplayName(displayName) {
+ // if the display name has a space it has already been mapped
+ if (!displayName || includeSpace.exec(displayName)) {
+ return displayName;
+ }
+
+ for (const reg of displayNameScenarios) {
+ const match = reg.exec(displayName);
+ if (match) {
+ return match[1];
+ }
+ }
+
+ return displayName;
+}
+
+const displayNameLibraryMap = {
+ Babel: {
+ tryCatch: "Async",
+ },
+ Backbone: {
+ "extend/child": "Create Class",
+ ".create": "Create Model",
+ },
+ jQuery: {
+ "jQuery.event.dispatch": "Dispatch Event",
+ },
+ React: {
+ // eslint-disable-next-line max-len
+ "ReactCompositeComponent._renderValidatedComponentWithoutOwnerOrContext/renderedElement<":
+ "Render",
+ _renderValidatedComponentWithoutOwnerOrContext: "Render",
+ },
+ VueJS: {
+ "renderMixin/Vue.prototype._render": "Render",
+ },
+ Webpack: {
+ // eslint-disable-next-line camelcase
+ __webpack_require__: "Bootstrap",
+ },
+};
+
+/**
+ * Compute the typical way to show a frame or function to the user.
+ *
+ * @param {Object} frameOrFunc
+ * Either a frame or a func object.
+ * Frame object is typically created via create.js::createFrame
+ * Func object comes from ast reducer and getSymbols selector.
+ * @param {Boolean} shouldMapDisplayName
+ * True by default, will try to translate internal framework function name
+ * into a most explicit and simplier name.
+ * @param {Object} l10n
+ * The localization object.
+ */
+export function formatDisplayName(
+ frameOrFunc,
+ { shouldMapDisplayName = true } = {},
+ l10n
+) {
+ // All the following attributes are only available on Frame objects
+ const { library, displayName, originalDisplayName } = frameOrFunc;
+ let displayedName;
+
+ // If the frame was identified to relate to a library,
+ // lookup for pretty name for the most important method of some frameworks
+ if (library && shouldMapDisplayName) {
+ displayedName = displayNameLibraryMap[library]?.[displayName];
+ }
+
+ // Frames for original sources may have both displayName for the generated source,
+ // or originalDisplayName for the original source.
+ // (in case original and generated have distinct function names in uglified sources)
+ //
+ // Also fallback to "name" attribute when the passed object is a Func object.
+ if (!displayedName) {
+ displayedName = originalDisplayName || displayName || frameOrFunc.name;
+ }
+
+ if (!displayedName) {
+ return l10n.getStr("anonymousFunction");
+ }
+
+ return simplifyDisplayName(displayedName);
+}
+
+export function formatCopyName(frame, l10n, shouldDisplayOriginalLocation) {
+ const displayName = formatDisplayName(frame, undefined, l10n);
+ const location = shouldDisplayOriginalLocation
+ ? frame.location
+ : frame.generatedLocation;
+ const fileName = location.source.url || location.source.id;
+ const frameLocation = frame.location.line;
+
+ return `${displayName} (${fileName}#${frameLocation})`;
+}
diff --git a/devtools/client/debugger/src/utils/pause/frames/getLibraryFromUrl.js b/devtools/client/debugger/src/utils/pause/frames/getLibraryFromUrl.js
new file mode 100644
index 0000000000..40a4d83841
--- /dev/null
+++ b/devtools/client/debugger/src/utils/pause/frames/getLibraryFromUrl.js
@@ -0,0 +1,140 @@
+/* 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/>. */
+
+const libraryMap = [
+ {
+ label: "Backbone",
+ pattern: /backbone/i,
+ },
+ {
+ label: "Babel",
+ pattern: /node_modules\/@babel/i,
+ },
+ {
+ label: "jQuery",
+ pattern: /jquery/i,
+ },
+ {
+ label: "Preact",
+ pattern: /preact/i,
+ },
+ {
+ label: "React",
+ pattern:
+ /(node_modules\/(?:react(-dom)?(-dev)?\/))|(react(-dom)?(-dev)?(\.[a-z]+)*\.js$)/,
+ },
+ {
+ label: "Immutable",
+ pattern: /immutable/i,
+ },
+ {
+ label: "Webpack",
+ pattern: /webpack\/bootstrap/i,
+ },
+ {
+ label: "Express",
+ pattern: /node_modules\/express/,
+ },
+ {
+ label: "Pug",
+ pattern: /node_modules\/pug/,
+ },
+ {
+ label: "ExtJS",
+ pattern: /\/ext-all[\.\-]/,
+ },
+ {
+ label: "MobX",
+ pattern: /mobx/i,
+ },
+ {
+ label: "Underscore",
+ pattern: /underscore/i,
+ },
+ {
+ label: "Lodash",
+ pattern: /lodash/i,
+ },
+ {
+ label: "Ember",
+ pattern: /ember/i,
+ },
+ {
+ label: "Choo",
+ pattern: /choo/i,
+ },
+ {
+ label: "VueJS",
+ pattern: /vue(?:\.[a-z]+)*\.js/i,
+ },
+ {
+ label: "RxJS",
+ pattern: /rxjs/i,
+ },
+ {
+ label: "Angular",
+ pattern: /angular(?!.*\/app\/)/i,
+ contextPattern: /zone\.js/,
+ },
+ {
+ label: "Redux",
+ pattern: /redux/i,
+ },
+ {
+ label: "Dojo",
+ pattern: /dojo/i,
+ },
+ {
+ label: "Marko",
+ pattern: /marko/i,
+ },
+ {
+ label: "NuxtJS",
+ pattern: /[\._]nuxt/i,
+ },
+ {
+ label: "Aframe",
+ pattern: /aframe/i,
+ },
+ {
+ label: "NextJS",
+ pattern: /[\._]next/i,
+ },
+];
+
+export function getLibraryFromUrl(frame, callStack = []) {
+ const frameUrl = frame.location.source.url;
+
+ // Let's first check if the frame match a defined pattern.
+ let match = libraryMap.find(o => o.pattern.test(frameUrl));
+ if (match) {
+ return match.label;
+ }
+
+ // If it does not, it might still be one of the case where the file is used
+ // by a library but the name has not enough specificity. In such case, we want
+ // to only return the library name if there are frames matching the library
+ // pattern in the callStack (e.g. `zone.js` is used by Angular, but the name
+ // could be quite common and return false positive if evaluated alone. So we
+ // only return Angular if there are other frames matching Angular).
+ match = libraryMap.find(
+ o => o.contextPattern && o.contextPattern.test(frameUrl)
+ );
+ if (match) {
+ const contextMatch = callStack.some(f => {
+ const url = f.location.source.url;
+ if (!url) {
+ return false;
+ }
+
+ return libraryMap.some(o => o.pattern.test(url));
+ });
+
+ if (contextMatch) {
+ return match.label;
+ }
+ }
+
+ return null;
+}
diff --git a/devtools/client/debugger/src/utils/pause/frames/index.js b/devtools/client/debugger/src/utils/pause/frames/index.js
new file mode 100644
index 0000000000..8b232b3452
--- /dev/null
+++ b/devtools/client/debugger/src/utils/pause/frames/index.js
@@ -0,0 +1,8 @@
+/* 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 "./annotateFrames";
+export * from "./collapseFrames";
+export * from "./displayName";
+export * from "./getLibraryFromUrl";
diff --git a/devtools/client/debugger/src/utils/pause/frames/moz.build b/devtools/client/debugger/src/utils/pause/frames/moz.build
new file mode 100644
index 0000000000..67462d40c3
--- /dev/null
+++ b/devtools/client/debugger/src/utils/pause/frames/moz.build
@@ -0,0 +1,14 @@
+# 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(
+ "annotateFrames.js",
+ "collapseFrames.js",
+ "displayName.js",
+ "getLibraryFromUrl.js",
+ "index.js",
+)
diff --git a/devtools/client/debugger/src/utils/pause/frames/tests/__snapshots__/collapseFrames.spec.js.snap b/devtools/client/debugger/src/utils/pause/frames/tests/__snapshots__/collapseFrames.spec.js.snap
new file mode 100644
index 0000000000..a09cff5bee
--- /dev/null
+++ b/devtools/client/debugger/src/utils/pause/frames/tests/__snapshots__/collapseFrames.spec.js.snap
@@ -0,0 +1,87 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`collapseFrames default 1`] = `
+Array [
+ Object {
+ "displayName": "a",
+ "location": Object {
+ "source": Object {},
+ },
+ },
+ Array [
+ Object {
+ "displayName": "b",
+ "library": "React",
+ "location": Object {
+ "source": Object {},
+ },
+ },
+ Object {
+ "displayName": "c",
+ "library": "React",
+ "location": Object {
+ "source": Object {},
+ },
+ },
+ ],
+]
+`;
+
+exports[`collapseFrames promises 1`] = `
+Array [
+ Object {
+ "displayName": "a",
+ "location": Object {
+ "source": Object {},
+ },
+ },
+ Array [
+ Object {
+ "displayName": "b",
+ "library": "React",
+ "location": Object {
+ "source": Object {},
+ },
+ },
+ Object {
+ "displayName": "c",
+ "library": "React",
+ "location": Object {
+ "source": Object {},
+ },
+ },
+ ],
+ Object {
+ "asyncCause": "promise callback",
+ "displayName": "d",
+ "library": undefined,
+ "location": Object {
+ "source": Object {},
+ },
+ },
+ Array [
+ Object {
+ "displayName": "e",
+ "library": "React",
+ "location": Object {
+ "source": Object {},
+ },
+ },
+ Object {
+ "displayName": "f",
+ "library": "React",
+ "location": Object {
+ "source": Object {},
+ },
+ },
+ ],
+ Object {
+ "asyncCause": null,
+ "displayName": "g",
+ "library": undefined,
+ "location": Object {
+ "source": Object {},
+ },
+ },
+]
+`;
diff --git a/devtools/client/debugger/src/utils/pause/frames/tests/collapseFrames.spec.js b/devtools/client/debugger/src/utils/pause/frames/tests/collapseFrames.spec.js
new file mode 100644
index 0000000000..58cafc3211
--- /dev/null
+++ b/devtools/client/debugger/src/utils/pause/frames/tests/collapseFrames.spec.js
@@ -0,0 +1,43 @@
+/* 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 { collapseFrames } from "../collapseFrames";
+
+describe("collapseFrames", () => {
+ it("default", () => {
+ const groups = collapseFrames([
+ { displayName: "a", location: { source: {} } },
+
+ { displayName: "b", library: "React", location: { source: {} } },
+ { displayName: "c", library: "React", location: { source: {} } },
+ ]);
+
+ expect(groups).toMatchSnapshot();
+ });
+
+ it("promises", () => {
+ const groups = collapseFrames([
+ { displayName: "a", location: { source: {} } },
+
+ { displayName: "b", library: "React", location: { source: {} } },
+ { displayName: "c", library: "React", location: { source: {} } },
+ {
+ displayName: "d",
+ library: undefined,
+ asyncCause: "promise callback",
+ location: { source: {} },
+ },
+ { displayName: "e", library: "React", location: { source: {} } },
+ { displayName: "f", library: "React", location: { source: {} } },
+ {
+ displayName: "g",
+ library: undefined,
+ asyncCause: null,
+ location: { source: {} },
+ },
+ ]);
+
+ expect(groups).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/debugger/src/utils/pause/frames/tests/displayName.spec.js b/devtools/client/debugger/src/utils/pause/frames/tests/displayName.spec.js
new file mode 100644
index 0000000000..d969c18753
--- /dev/null
+++ b/devtools/client/debugger/src/utils/pause/frames/tests/displayName.spec.js
@@ -0,0 +1,129 @@
+/* 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 {
+ formatCopyName,
+ formatDisplayName,
+ simplifyDisplayName,
+} from "../displayName";
+
+import { makeMockFrame, makeMockSource } from "../../../test-mockup";
+
+describe("formatCopyName", () => {
+ it("simple", () => {
+ const source = makeMockSource("todo-view.js");
+ const frame = makeMockFrame(undefined, source, undefined, 12, "child");
+
+ expect(formatCopyName(frame, L10N)).toEqual("child (todo-view.js#12)");
+ });
+});
+
+describe("formatting display names", () => {
+ it("uses a library description", () => {
+ const source = makeMockSource("assets/backbone.js");
+ const frame = {
+ ...makeMockFrame(undefined, source, undefined, undefined, "extend/child"),
+ library: "Backbone",
+ };
+
+ expect(formatDisplayName(frame, undefined, L10N)).toEqual("Create Class");
+ });
+
+ it("shortens an anonymous function", () => {
+ const source = makeMockSource("assets/bar.js");
+ const frame = makeMockFrame(
+ undefined,
+ source,
+ undefined,
+ undefined,
+ "extend/child/bar/baz"
+ );
+
+ expect(formatDisplayName(frame, undefined, L10N)).toEqual("baz");
+ });
+
+ it("does not truncates long function names", () => {
+ const source = makeMockSource("extend/child/bar/baz");
+ const frame = makeMockFrame(
+ undefined,
+ source,
+ undefined,
+ undefined,
+ "bazbazbazbazbazbazbazbazbazbazbazbazbaz"
+ );
+
+ expect(formatDisplayName(frame, undefined, L10N)).toEqual(
+ "bazbazbazbazbazbazbazbazbazbazbazbazbaz"
+ );
+ });
+
+ it("returns the original function name when present", () => {
+ const source = makeMockSource("entry.js");
+ const frame = {
+ ...makeMockFrame(undefined, source),
+ originalDisplayName: "originalFn",
+ displayName: "fn",
+ };
+
+ expect(formatDisplayName(frame, undefined, L10N)).toEqual("originalFn");
+ });
+
+ it("returns anonymous when displayName is undefined", () => {
+ const frame = { ...makeMockFrame(), displayName: undefined };
+ expect(formatDisplayName(frame, undefined, L10N)).toEqual("<anonymous>");
+ });
+
+ it("returns anonymous when displayName is null", () => {
+ const frame = { ...makeMockFrame(), displayName: null };
+ expect(formatDisplayName(frame, undefined, L10N)).toEqual("<anonymous>");
+ });
+
+ it("returns anonymous when displayName is an empty string", () => {
+ const frame = { ...makeMockFrame(), displayName: "" };
+ expect(formatDisplayName(frame, undefined, L10N)).toEqual("<anonymous>");
+ });
+});
+
+describe("simplifying display names", () => {
+ const cases = {
+ defaultCase: [["define", "define"]],
+
+ objectProperty: [
+ ["z.foz", "foz"],
+ ["z.foz/baz", "baz"],
+ ["z.foz/baz/y.bay", "bay"],
+ ["outer/x.fox.bax.nx", "nx"],
+ ["outer/fow.baw", "baw"],
+ ["fromYUI._attach", "_attach"],
+ ["Y.ClassNameManager</getClassName", "getClassName"],
+ ["orion.textview.TextView</addHandler", "addHandler"],
+ ["this.eventPool_.createObject", "createObject"],
+ ],
+
+ arrayProperty: [
+ ["this.eventPool_[createObject]", "createObject"],
+ ["jQuery.each(^)/jQuery.fn[o]", "o"],
+ ["viewport[get+D]", "get+D"],
+ ["arr[0]", "0"],
+ ],
+
+ functionProperty: [
+ ["fromYUI._attach/<.", "_attach"],
+ ["Y.ClassNameManager<", "ClassNameManager"],
+ ["fromExtJS.setVisible/cb<", "cb"],
+ ["fromDojo.registerWin/<", "registerWin"],
+ ],
+
+ annonymousProperty: [["jQuery.each(^)", "each"]],
+
+ privateMethod: [["#privateFunc", "#privateFunc"]],
+ };
+
+ Object.keys(cases).forEach(type => {
+ cases[type].forEach(([kase, expected]) => {
+ it(`${type} - ${kase}`, () =>
+ expect(simplifyDisplayName(kase)).toEqual(expected));
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/utils/pause/frames/tests/getLibraryFromUrl.spec.js b/devtools/client/debugger/src/utils/pause/frames/tests/getLibraryFromUrl.spec.js
new file mode 100644
index 0000000000..ff5be43285
--- /dev/null
+++ b/devtools/client/debugger/src/utils/pause/frames/tests/getLibraryFromUrl.spec.js
@@ -0,0 +1,127 @@
+/* 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 { getLibraryFromUrl } from "../getLibraryFromUrl";
+import { makeMockFrameWithURL } from "../../../test-mockup";
+
+describe("getLibraryFromUrl", () => {
+ describe("When Preact is on the frame", () => {
+ it("should return Preact and not React", () => {
+ const frame = makeMockFrameWithURL(
+ "https://cdnjs.cloudflare.com/ajax/libs/preact/8.2.5/preact.js"
+ );
+ expect(getLibraryFromUrl(frame)).toEqual("Preact");
+ });
+ });
+
+ describe("When Vue is on the frame", () => {
+ it("should return VueJS for different builds", () => {
+ const buildTypeList = [
+ "vue.js",
+ "vue.common.js",
+ "vue.esm.js",
+ "vue.runtime.js",
+ "vue.runtime.common.js",
+ "vue.runtime.esm.js",
+ "vue.min.js",
+ "vue.runtime.min.js",
+ ];
+
+ buildTypeList.forEach(buildType => {
+ const frame = makeMockFrameWithURL(
+ `https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/${buildType}`
+ );
+ expect(getLibraryFromUrl(frame)).toEqual("VueJS");
+ });
+ });
+ });
+
+ describe("When React is in the URL", () => {
+ it("should not return React if it is not part of the filename", () => {
+ const notReactUrlList = [
+ "https://react.js.com/test.js",
+ "https://debugger-example.com/test.js",
+ "https://debugger-react-example.com/test.js",
+ "https://debugger-react-example.com/react/test.js",
+ "https://debugger-example.com/react-contextmenu.js",
+ ];
+ notReactUrlList.forEach(notReactUrl => {
+ const frame = makeMockFrameWithURL(notReactUrl);
+ expect(getLibraryFromUrl(frame)).toBeNull();
+ });
+ });
+ it("should return React if it is part of the filename", () => {
+ const reactUrlList = [
+ "https://debugger-example.com/react.js",
+ "https://debugger-example.com/react.development.js",
+ "https://debugger-example.com/react.production.min.js",
+ "https://debugger-react-example.com/react.js",
+ "https://debugger-react-example.com/react/react.js",
+ "https://debugger-example.com/react-dom.js",
+ "https://debugger-example.com/react-dom.development.js",
+ "https://debugger-example.com/react-dom.production.min.js",
+ "https://debugger-react-example.com/react-dom.js",
+ "https://debugger-react-example.com/react/react-dom.js",
+ "https://debugger-react-example.com/react-dom-dev.js",
+ "/node_modules/react/test.js",
+ "/node_modules/react-dev/test.js",
+ "/node_modules/react-dom/test.js",
+ "/node_modules/react-dom-dev/test.js",
+ ];
+ reactUrlList.forEach(reactUrl => {
+ const frame = makeMockFrameWithURL(reactUrl);
+ expect(getLibraryFromUrl(frame)).toEqual("React");
+ });
+ });
+ });
+
+ describe("When Angular is in the URL", () => {
+ it("should return Angular for AngularJS (1.x)", () => {
+ const frame = makeMockFrameWithURL(
+ "https://cdnjs.cloudflare.com/ajax/libs/angular/angular.js"
+ );
+ expect(getLibraryFromUrl(frame)).toEqual("Angular");
+ });
+
+ it("should return Angular for Angular (2.x)", () => {
+ const frame = makeMockFrameWithURL(
+ "https://stackblitz.io/turbo_modules/@angular/core@7.2.4/bundles/core.umd.js"
+ );
+ expect(getLibraryFromUrl(frame)).toEqual("Angular");
+ });
+
+ it("should not return Angular for Angular components", () => {
+ const frame = makeMockFrameWithURL(
+ "https://firefox-devtools-angular-log.stackblitz.io/~/src/app/hello.component.ts"
+ );
+ expect(getLibraryFromUrl(frame)).toBeNull();
+ });
+ });
+
+ describe("When zone.js is on the frame", () => {
+ it("should not return Angular when no callstack", () => {
+ const frame = makeMockFrameWithURL("/node_modules/zone/zone.js");
+ expect(getLibraryFromUrl(frame)).toBeNull();
+ });
+
+ it("should not return Angular when stack without Angular frames", () => {
+ const frame = makeMockFrameWithURL("/node_modules/zone/zone.js");
+ const callstack = [frame];
+
+ expect(getLibraryFromUrl(frame, callstack)).toBeNull();
+ });
+
+ it("should return Angular when stack with AngularJS (1.x) frames", () => {
+ const frame = makeMockFrameWithURL("/node_modules/zone/zone.js");
+ const callstack = [
+ frame,
+ makeMockFrameWithURL(
+ "https://cdnjs.cloudflare.com/ajax/libs/angular/angular.js"
+ ),
+ ];
+
+ expect(getLibraryFromUrl(frame, callstack)).toEqual("Angular");
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/utils/pause/index.js b/devtools/client/debugger/src/utils/pause/index.js
new file mode 100644
index 0000000000..f6966999b0
--- /dev/null
+++ b/devtools/client/debugger/src/utils/pause/index.js
@@ -0,0 +1,5 @@
+/* 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 "./why";
diff --git a/devtools/client/debugger/src/utils/pause/mapScopes/README.md b/devtools/client/debugger/src/utils/pause/mapScopes/README.md
new file mode 100644
index 0000000000..2f65b8e847
--- /dev/null
+++ b/devtools/client/debugger/src/utils/pause/mapScopes/README.md
@@ -0,0 +1,191 @@
+# mapScopes
+
+The files in this directory manage the Devtools logic for taking the original
+JS file from the sourcemap and using the content, combined with source
+mappings, to provide an enhanced user experience for debugging the code.
+
+In this document, we'll refer to the files as either:
+
+* `original` - The code that the user wrote originally.
+* `generated` - The code actually executing in the engine, which may have been
+ transformed from the `original`, and if a bundler was used, may contain the
+ final output for several different `original` files.
+
+The enhancements implemented here break down into as two primary improvements:
+
+* Rendering the scopes as the user expects them, using the position in the
+ original file, rather than the content of the generated file.
+* Allowing users to interact with the code in the console as if it were the
+ original code, rather than the generated file.
+
+
+## Overall Approach
+
+The core goal of scope mapping is to parse the original and generated files,
+and then infer a correlation between the bindings in the original file,
+and the bindings in the generated file. This is correlation is done via the
+source mappings provided alongside the original code in the sourcemap.
+
+The overall steps break down into:
+
+
+### 1. Parsing
+
+First the generated and original files are parsed into ASTs, and then the ASTs
+are each traversed in order to generate a full picture of the scopes that are
+present in the file. This covers information for each binding in each scope,
+including all metadata about the declarations themselves, and all references
+to each of the bindings. The exact location of each binding reference is the
+most important factor because these will be used when working with sourcemaps.
+
+Importantly, this scope tree's structure also mirrors the structure of the
+scope data that the engine itself returns when paused.
+
+
+### 2. Generated Binding -> Grip Correlation
+
+When the engine pauses, we get back a full description of the current scope,
+as well as `grip` objects that can be used as proxy-like objects in order to
+access the actual values in the engine itself.
+
+With this data, along with the location where the engine is paused, we can
+use the AST-based scope information from step 1 to attach a line/column
+range to each `grip`. Using those mappings we have enough information to get
+the real engine value of any variable based on its position in the generated
+code, as long as it is in scope at the paused location in the generated file.
+
+The generated and engine scopes are correlated depth-first, because in some
+cases the scope data from the engine will be deeper that the data from the
+parsed code, meaning there are additional unknown parent scopes. This can
+happen, for instance, if the executing code is running inside of an `eval`
+since the parser only knows about the scopes inside of `eval`, and cannot
+know what context it is executed inside of.
+
+
+### 3. Original Binding -> Generated Binding Correlation
+
+Now that we can get the value of a generated location, we need to decide which
+generated location correlates with which original location. For each of
+the original bindings in each original scope, we iterate through the
+individual references to the binding in the file and:
+
+1. Use the sourcemap to convert the original location into a range on the
+ generated code.
+2. Filter the available bindings in the generated file down to those that
+ overlap with this generated range.
+3. Perform heuristics to decide if any of the bindings in the range appear
+ to be a valid and safe mapping.
+
+These steps allow us to build a datastructure that describes the scope
+as it is declared in the original file, while rendering the value using the
+`grip` objects, as returned from the engine itself.
+
+There is additional complexity here in the exact details of the heuristics
+mentioned above. See the later Heuristics sections diving into these details.
+
+During this phase, one additional task is performed, which is the construction
+of expressions that relate back to the original binding. After matching each
+binding in a range, we know the name of the binding in the original scope,
+and we _also_ know the name of the generated binding, or more generally the
+expression to evaluate in order to get the value of the original binding.
+
+These expression values are important for ensuring that the developer console
+is able to allow users to interact with the original code. If a user types
+a variable into the console, it can be translated into the expression
+correlated with that original variable, allowing the code to behave as it
+would have, were it actually part of the original file.
+
+
+### 4. Generate a Scope Tree
+
+The structure generated in step 3 is converted into a structure compatible
+with the standard scope data returned from the engine itself in order to allow
+for consistent usage of scope data, irrespective of the scope data source.
+This stage also re-attaches the global scope data, which is otherwise ignored
+during the correlation process.
+
+
+### 5. Validation
+
+As a simple form of validation, we ensure that a large percentage of bindings
+were actually resolved to a `grip` object. This offers some amount of
+protection, if the sourcemap itself turned out to be somewhat low-quality.
+
+This happens often with tooling that generates maps that simply map an original
+line to a generated line. While line-to-line mappings still enable step
+debugging, they do not provide enough information for original-scope mapping.
+
+On the other hand, generally line-to-line mappings are usually generated by
+tooling that performs minimal transformations on their own, so it is often
+acceptable to fall back to the engine-only generated-file scope data in this
+case.
+
+
+## Range Mapping Heuristics
+
+Since we know lots of information about the original bindings when performing
+matches, it is possible to make educated guesses about how their generated
+code will have likely been transformed. This allows us to perform more
+aggressive matching logic what would normally be too false-positive-heavy
+for generic use, when it is deemed safe enough.
+
+
+### Standard Matching
+
+In the general case, we iterate through the bindings that were found in the
+generated range, and consider it a match if the binding overlaps with the
+start of the range. One extra case here is that ranges at the start of
+a line often point to the first binding in a range and do not overlap the
+first binding, so there is special-casing for the first binding in each range.
+
+
+### ES6 Import Reference Special Case
+
+References to ES6 imports are often transformed into property accesses on objects
+in order to emulate the "live-binding" behavior defined by ES6. The standard
+logic here would end up always resolving the imported value to be the import
+namespace object itself, rather than reading the value of the property.
+
+To support this, specifically for ES6 imports, we walk outward from the matched
+binding itself, taking property accesses into account, as long as the
+property access itself is also within the mapped range.
+
+While decently effective, there are currently two downsides to this approach:
+
+* The "live" behavior of imports is often implemented using accessor
+ properties, which as of the time of this writing, cannot be evaluated to
+ retrieve their real value.
+* The "live" behavior of imports is sometimes implemented with function calls,
+ which also also cannot be evaluated, causing their value to be
+ unknown.
+
+
+### ES6 Import Declaration Special Case
+
+If there are no references to an imported value, or matching based on the
+reference failed, we fall back to a second case.
+
+ES6 import declarations themselves often map back to the location of the
+declaration of the imported module's namespace object. By getting the range for
+the import declaration itself, we can infer which generated binding is the
+namespace object. Alongside that, we already know the name of the property on
+the namespace itself because it is statically knowable by analyzing the
+import declaration.
+
+By combining both of those pieces of information, we can access the namespace's
+property to get the imported value.
+
+
+### Typescript Classes
+
+Typescript have several non-ideal ways in which they output source maps, most
+of which center around classes that are either exported, or decorated. These
+issues are currently tracked in [Typescript issue #22833][1].
+
+To work around this issue, we use a method similar to the import declaration
+case above. While the class name itself often maps to unhelpful locations,
+the class declaration itself generally maps to the class's transformed binding,
+so we make use of the class declaration location instead of the location of
+the class's declared name in these cases.
+
+ [1]: https://github.com/Microsoft/TypeScript/issues/22833
diff --git a/devtools/client/debugger/src/utils/pause/mapScopes/buildGeneratedBindingList.js b/devtools/client/debugger/src/utils/pause/mapScopes/buildGeneratedBindingList.js
new file mode 100644
index 0000000000..5f90138013
--- /dev/null
+++ b/devtools/client/debugger/src/utils/pause/mapScopes/buildGeneratedBindingList.js
@@ -0,0 +1,141 @@
+/* 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 { clientCommands } from "../../../client/firefox";
+
+import { locColumn } from "./locColumn";
+import { getOptimizedOutGrip } from "./optimizedOut";
+
+export function buildGeneratedBindingList(
+ scopes,
+ generatedAstScopes,
+ thisBinding
+) {
+ // The server's binding data doesn't include general 'this' binding
+ // information, so we manually inject the one 'this' binding we have into
+ // the normal binding data we are working with.
+ const frameThisOwner = generatedAstScopes.find(
+ generated => "this" in generated.bindings
+ );
+
+ let globalScope = null;
+ const clientScopes = [];
+ for (let s = scopes; s; s = s.parent) {
+ const bindings = s.bindings
+ ? Object.assign({}, ...s.bindings.arguments, s.bindings.variables)
+ : {};
+
+ clientScopes.push(bindings);
+ globalScope = s;
+ }
+
+ const generatedMainScopes = generatedAstScopes.slice(0, -2);
+ const generatedGlobalScopes = generatedAstScopes.slice(-2);
+
+ const clientMainScopes = clientScopes.slice(0, generatedMainScopes.length);
+ const clientGlobalScopes = clientScopes.slice(generatedMainScopes.length);
+
+ // Map the main parsed script body using the nesting hierarchy of the
+ // generated and client scopes.
+ const generatedBindings = generatedMainScopes.reduce((acc, generated, i) => {
+ const bindings = clientMainScopes[i];
+
+ if (generated === frameThisOwner && thisBinding) {
+ bindings.this = {
+ value: thisBinding,
+ };
+ }
+
+ for (const name of Object.keys(generated.bindings)) {
+ // If there is no 'this' value, we exclude the binding entirely.
+ // Otherwise it would pass through as found, but "(unscoped)", causing
+ // the search logic to stop with a match.
+ if (name === "this" && !bindings[name]) {
+ continue;
+ }
+
+ const { refs } = generated.bindings[name];
+ for (const loc of refs) {
+ acc.push({
+ name,
+ loc,
+ desc: () => Promise.resolve(bindings[name] || null),
+ });
+ }
+ }
+ return acc;
+ }, []);
+
+ // Bindings in the global/lexical global of the generated code may or
+ // may not be the real global if the generated code is running inside
+ // of an evaled context. To handle this, we just look up the client scope
+ // hierarchy to find the closest binding with that name.
+ for (const generated of generatedGlobalScopes) {
+ for (const name of Object.keys(generated.bindings)) {
+ const { refs } = generated.bindings[name];
+ const bindings = clientGlobalScopes.find(b => name in b);
+
+ for (const loc of refs) {
+ if (bindings) {
+ generatedBindings.push({
+ name,
+ loc,
+ desc: () => Promise.resolve(bindings[name]),
+ });
+ } else {
+ const globalGrip = globalScope?.object;
+ if (globalGrip) {
+ // Should always exist, just checking to keep Flow happy.
+
+ generatedBindings.push({
+ name,
+ loc,
+ desc: async () => {
+ const objectFront =
+ clientCommands.createObjectFront(globalGrip);
+ return (await objectFront.getProperty(name)).descriptor;
+ },
+ });
+ }
+ }
+ }
+ }
+ }
+
+ // Sort so we can binary-search.
+ return sortBindings(generatedBindings);
+}
+
+export function buildFakeBindingList(generatedAstScopes) {
+ // TODO if possible, inject real bindings for the global scope
+ const generatedBindings = generatedAstScopes.reduce((acc, generated) => {
+ for (const name of Object.keys(generated.bindings)) {
+ if (name === "this") {
+ continue;
+ }
+ const { refs } = generated.bindings[name];
+ for (const loc of refs) {
+ acc.push({
+ name,
+ loc,
+ desc: () => Promise.resolve(getOptimizedOutGrip()),
+ });
+ }
+ }
+ return acc;
+ }, []);
+ return sortBindings(generatedBindings);
+}
+
+function sortBindings(generatedBindings) {
+ return generatedBindings.sort((a, b) => {
+ const aStart = a.loc.start;
+ const bStart = b.loc.start;
+
+ if (aStart.line === bStart.line) {
+ return locColumn(aStart) - locColumn(bStart);
+ }
+ return aStart.line - bStart.line;
+ });
+}
diff --git a/devtools/client/debugger/src/utils/pause/mapScopes/filtering.js b/devtools/client/debugger/src/utils/pause/mapScopes/filtering.js
new file mode 100644
index 0000000000..dec3772e82
--- /dev/null
+++ b/devtools/client/debugger/src/utils/pause/mapScopes/filtering.js
@@ -0,0 +1,45 @@
+/* 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/>. */
+
+function findInsertionLocation(array, callback) {
+ let left = 0;
+ let right = array.length;
+ while (left < right) {
+ const mid = Math.floor((left + right) / 2);
+ const item = array[mid];
+
+ const result = callback(item);
+ if (result === 0) {
+ left = mid;
+ break;
+ }
+ if (result >= 0) {
+ right = mid;
+ } else {
+ left = mid + 1;
+ }
+ }
+
+ // Ensure the value is the start of any set of matches.
+ let i = left;
+ if (i < array.length) {
+ while (i >= 0 && callback(array[i]) >= 0) {
+ i--;
+ }
+ return i + 1;
+ }
+
+ return i;
+}
+
+export function filterSortedArray(array, callback) {
+ const start = findInsertionLocation(array, callback);
+
+ const results = [];
+ for (let i = start; i < array.length && callback(array[i]) === 0; i++) {
+ results.push(array[i]);
+ }
+
+ return results;
+}
diff --git a/devtools/client/debugger/src/utils/pause/mapScopes/findGeneratedBindingFromPosition.js b/devtools/client/debugger/src/utils/pause/mapScopes/findGeneratedBindingFromPosition.js
new file mode 100644
index 0000000000..6e833959f4
--- /dev/null
+++ b/devtools/client/debugger/src/utils/pause/mapScopes/findGeneratedBindingFromPosition.js
@@ -0,0 +1,305 @@
+/* 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 { locColumn } from "./locColumn";
+import { mappingContains } from "./mappingContains";
+
+// eslint-disable-next-line max-len
+
+import { clientCommands } from "../../../client/firefox";
+
+/**
+ * Given a mapped range over the generated source, attempt to resolve a real
+ * binding descriptor that can be used to access the value.
+ */
+export async function findGeneratedReference(applicableBindings) {
+ // We can adjust this number as we go, but these are a decent start as a
+ // general heuristic to assume the bindings were bad or just map a chunk of
+ // whole line or something.
+ if (applicableBindings.length > 4) {
+ // Babel's for..of generates at least 3 bindings inside one range for
+ // block-scoped loop variables, so we shouldn't go below that.
+ applicableBindings = [];
+ }
+
+ for (const applicable of applicableBindings) {
+ const result = await mapBindingReferenceToDescriptor(applicable);
+ if (result) {
+ return result;
+ }
+ }
+ return null;
+}
+
+export async function findGeneratedImportReference(applicableBindings) {
+ // When wrapped, for instance as `Object(ns.default)`, the `Object` binding
+ // will be the first in the list. To avoid resolving `Object` as the
+ // value of the import itself, we potentially skip the first binding.
+ applicableBindings = applicableBindings.filter((applicable, i) => {
+ if (
+ !applicable.firstInRange ||
+ applicable.binding.loc.type !== "ref" ||
+ applicable.binding.loc.meta
+ ) {
+ return true;
+ }
+
+ const next =
+ i + 1 < applicableBindings.length ? applicableBindings[i + 1] : null;
+
+ return !next || next.binding.loc.type !== "ref" || !next.binding.loc.meta;
+ });
+
+ // We can adjust this number as we go, but these are a decent start as a
+ // general heuristic to assume the bindings were bad or just map a chunk of
+ // whole line or something.
+ if (applicableBindings.length > 2) {
+ // Babel's for..of generates at least 3 bindings inside one range for
+ // block-scoped loop variables, so we shouldn't go below that.
+ applicableBindings = [];
+ }
+
+ for (const applicable of applicableBindings) {
+ const result = await mapImportReferenceToDescriptor(applicable);
+ if (result) {
+ return result;
+ }
+ }
+
+ return null;
+}
+
+/**
+ * Given a mapped range over the generated source and the name of the imported
+ * value that is referenced, attempt to resolve a binding descriptor for
+ * the import's value.
+ */
+export async function findGeneratedImportDeclaration(
+ applicableBindings,
+ importName
+) {
+ // We can adjust this number as we go, but these are a decent start as a
+ // general heuristic to assume the bindings were bad or just map a chunk of
+ // whole line or something.
+ if (applicableBindings.length > 10) {
+ // Import declarations tend to have a large number of bindings for
+ // for things like 'require' and 'interop', so this number is larger
+ // than other binding count checks.
+ applicableBindings = [];
+ }
+
+ let result = null;
+
+ for (const { binding } of applicableBindings) {
+ if (binding.loc.type === "ref") {
+ continue;
+ }
+
+ const namespaceDesc = await binding.desc();
+ if (isPrimitiveValue(namespaceDesc)) {
+ continue;
+ }
+ if (!isObjectValue(namespaceDesc)) {
+ // We want to handle cases like
+ //
+ // var _mod = require(...);
+ // var _mod2 = _interopRequire(_mod);
+ //
+ // where "_mod" is optimized out because it is only referenced once. To
+ // allow that, we track the optimized-out value as a possible result,
+ // but allow later binding values to overwrite the result.
+ result = {
+ name: binding.name,
+ desc: namespaceDesc,
+ expression: binding.name,
+ };
+ continue;
+ }
+
+ const desc = await readDescriptorProperty(namespaceDesc, importName);
+ const expression = `${binding.name}.${importName}`;
+
+ if (desc) {
+ result = {
+ name: binding.name,
+ desc,
+ expression,
+ };
+ break;
+ }
+ }
+
+ return result;
+}
+
+/**
+ * Given a generated binding, and a range over the generated code, statically
+ * check if the given binding matches the range.
+ */
+async function mapBindingReferenceToDescriptor({
+ binding,
+ range,
+ firstInRange,
+ firstOnLine,
+}) {
+ // Allow the mapping to point anywhere within the generated binding
+ // location to allow for less than perfect sourcemaps. Since you also
+ // need at least one character between identifiers, we also give one
+ // characters of space at the front the generated binding in order
+ // to increase the probability of finding the right mapping.
+ if (
+ range.start.line === binding.loc.start.line &&
+ // If a binding is the first on a line, Babel will extend the mapping to
+ // include the whitespace between the newline and the binding. To handle
+ // that, we skip the range requirement for starting location.
+ (firstInRange ||
+ firstOnLine ||
+ locColumn(range.start) >= locColumn(binding.loc.start)) &&
+ locColumn(range.start) <= locColumn(binding.loc.end)
+ ) {
+ return {
+ name: binding.name,
+ desc: await binding.desc(),
+ expression: binding.name,
+ };
+ }
+
+ return null;
+}
+
+/**
+ * Given an generated binding, and a range over the generated code, statically
+ * evaluate accessed properties within the mapped range to resolve the actual
+ * imported value.
+ */
+async function mapImportReferenceToDescriptor({ binding, range }) {
+ if (binding.loc.type !== "ref") {
+ return null;
+ }
+
+ // Expression matches require broader searching because sourcemaps usage
+ // varies in how they map certain things. For instance given
+ //
+ // import { bar } from "mod";
+ // bar();
+ //
+ // The "bar()" expression is generally expanded into one of two possibly
+ // forms, both of which map the "bar" identifier in different ways. See
+ // the "^^" markers below for the ranges.
+ //
+ // (0, foo.bar)() // Babel
+ // ^^^^^^^ // mapping
+ // ^^^ // binding
+ // vs
+ //
+ // __webpack_require__.i(foo.bar)() // Webpack 2
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // mapping
+ // ^^^ // binding
+ // vs
+ //
+ // Object(foo.bar)() // Webpack >= 3
+ // ^^^^^^^^^^^^^^^ // mapping
+ // ^^^ // binding
+ //
+ // Unfortunately, Webpack also has a tendancy to over-map past the call
+ // expression to the start of the next line, at least when there isn't
+ // anything else on that line that is mapped, e.g.
+ //
+ // Object(foo.bar)()
+ // ^^^^^^^^^^^^^^^^^
+ // ^ // wrapped to column 0 of next line
+
+ if (!mappingContains(range, binding.loc)) {
+ return null;
+ }
+
+ // Webpack 2's import declarations wrap calls with an identity fn, so we
+ // need to make sure to skip that binding because it is mapped to the
+ // location of the original binding usage.
+ if (
+ binding.name === "__webpack_require__" &&
+ binding.loc.meta &&
+ binding.loc.meta.type === "member" &&
+ binding.loc.meta.property === "i"
+ ) {
+ return null;
+ }
+
+ let expression = binding.name;
+ let desc = await binding.desc();
+
+ if (binding.loc.type === "ref") {
+ const { meta } = binding.loc;
+
+ // Limit to 2 simple property or inherits operartions, since it would
+ // just be more work to search more and it is very unlikely that
+ // bindings would be mapped to more than a single member + inherits
+ // wrapper.
+ for (
+ let op = meta, index = 0;
+ op && mappingContains(range, op) && desc && index < 2;
+ index++, op = op?.parent
+ ) {
+ // Calling could potentially trigger side-effects, which would not
+ // be ideal for this case.
+ if (op.type === "call") {
+ return null;
+ }
+
+ if (op.type === "inherit") {
+ continue;
+ }
+
+ desc = await readDescriptorProperty(desc, op.property);
+ expression += `.${op.property}`;
+ }
+ }
+
+ return desc
+ ? {
+ name: binding.name,
+ desc,
+ expression,
+ }
+ : null;
+}
+
+function isPrimitiveValue(desc) {
+ return desc && (!desc.value || typeof desc.value !== "object");
+}
+function isObjectValue(desc) {
+ return (
+ desc &&
+ !isPrimitiveValue(desc) &&
+ desc.value.type === "object" &&
+ // Note: The check for `.type` might already cover the optimizedOut case
+ // but not 100% sure, so just being cautious.
+ !desc.value.optimizedOut
+ );
+}
+
+async function readDescriptorProperty(desc, property) {
+ if (!desc) {
+ return null;
+ }
+
+ if (typeof desc.value !== "object" || !desc.value) {
+ // If accessing a property on a primitive type, just return 'undefined'
+ // as the value.
+ return {
+ value: {
+ type: "undefined",
+ },
+ };
+ }
+
+ if (!isObjectValue(desc)) {
+ // If we got a non-primitive descriptor but it isn't an object, then
+ // it's definitely not the namespace and it is probably an error.
+ return desc;
+ }
+
+ const objectFront = clientCommands.createObjectFront(desc.value);
+ return (await objectFront.getProperty(property)).descriptor;
+}
diff --git a/devtools/client/debugger/src/utils/pause/mapScopes/getApplicableBindingsForOriginalPosition.js b/devtools/client/debugger/src/utils/pause/mapScopes/getApplicableBindingsForOriginalPosition.js
new file mode 100644
index 0000000000..c1a64ddfa0
--- /dev/null
+++ b/devtools/client/debugger/src/utils/pause/mapScopes/getApplicableBindingsForOriginalPosition.js
@@ -0,0 +1,112 @@
+/* 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 { positionCmp } from "./positionCmp";
+import { filterSortedArray } from "./filtering";
+import { mappingContains } from "./mappingContains";
+import { getGeneratedLocation } from "../../source-maps";
+
+export async function originalRangeStartsInside({ start, end }, thunkArgs) {
+ const endPosition = await getGeneratedLocation(end, thunkArgs);
+ const startPosition = await getGeneratedLocation(start, thunkArgs);
+
+ // If the start and end positions collapse into eachother, it means that
+ // the range in the original content didn't _start_ at the start position.
+ // Since this likely means that the range doesn't logically apply to this
+ // binding location, we skip it.
+ return positionCmp(startPosition, endPosition) !== 0;
+}
+
+export async function getApplicableBindingsForOriginalPosition(
+ generatedAstBindings,
+ source,
+ { start, end },
+ bindingType,
+ locationType,
+ thunkArgs
+) {
+ const { sourceMapLoader } = thunkArgs;
+ const ranges = await sourceMapLoader.getGeneratedRanges(start);
+
+ const resultRanges = ranges.map(mapRange => ({
+ start: {
+ line: mapRange.line,
+ column: mapRange.columnStart,
+ },
+ end: {
+ line: mapRange.line,
+ // SourceMapConsumer's 'lastColumn' is inclusive, so we add 1 to make
+ // it exclusive like all other locations.
+ column: mapRange.columnEnd + 1,
+ },
+ }));
+
+ // When searching for imports, we expand the range to up to the next available
+ // mapping to allow for import declarations that are composed of multiple
+ // variable statements, where the later ones are entirely unmapped.
+ // Babel 6 produces imports in this style, e.g.
+ //
+ // var _mod = require("mod"); // mapped from import statement
+ // var _mod2 = interop(_mod); // entirely unmapped
+ if (bindingType === "import" && locationType !== "ref") {
+ const endPosition = await getGeneratedLocation(end, thunkArgs);
+ const startPosition = await getGeneratedLocation(start, thunkArgs);
+
+ for (const range of resultRanges) {
+ if (
+ mappingContains(range, { start: startPosition, end: startPosition }) &&
+ positionCmp(range.end, endPosition) < 0
+ ) {
+ range.end = {
+ line: endPosition.line,
+ column: endPosition.column,
+ };
+ break;
+ }
+ }
+ }
+
+ return filterApplicableBindings(generatedAstBindings, resultRanges);
+}
+
+function filterApplicableBindings(bindings, ranges) {
+ const result = [];
+ for (const range of ranges) {
+ // Any binding overlapping a part of the mapping range.
+ const filteredBindings = filterSortedArray(bindings, binding => {
+ if (positionCmp(binding.loc.end, range.start) <= 0) {
+ return -1;
+ }
+ if (positionCmp(binding.loc.start, range.end) >= 0) {
+ return 1;
+ }
+
+ return 0;
+ });
+
+ let firstInRange = true;
+ let firstOnLine = true;
+ let line = -1;
+
+ for (const binding of filteredBindings) {
+ if (binding.loc.start.line === line) {
+ firstOnLine = false;
+ } else {
+ line = binding.loc.start.line;
+ firstOnLine = true;
+ }
+
+ result.push({
+ binding,
+ range,
+ firstOnLine,
+ firstInRange,
+ });
+
+ firstInRange = false;
+ }
+ }
+
+ return result;
+}
diff --git a/devtools/client/debugger/src/utils/pause/mapScopes/index.js b/devtools/client/debugger/src/utils/pause/mapScopes/index.js
new file mode 100644
index 0000000000..dbd99f8b11
--- /dev/null
+++ b/devtools/client/debugger/src/utils/pause/mapScopes/index.js
@@ -0,0 +1,586 @@
+/* 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 {
+ debuggerToSourceMapLocation,
+ sourceMapToDebuggerLocation,
+} from "../../location";
+import { locColumn } from "./locColumn";
+import { loadRangeMetadata, findMatchingRange } from "./rangeMetadata";
+
+// eslint-disable-next-line max-len
+import {
+ findGeneratedReference,
+ findGeneratedImportReference,
+ findGeneratedImportDeclaration,
+} from "./findGeneratedBindingFromPosition";
+import {
+ buildGeneratedBindingList,
+ buildFakeBindingList,
+} from "./buildGeneratedBindingList";
+import {
+ originalRangeStartsInside,
+ getApplicableBindingsForOriginalPosition,
+} from "./getApplicableBindingsForOriginalPosition";
+import { getOptimizedOutGrip } from "./optimizedOut";
+
+import { log } from "../../log";
+
+// Create real location objects for all location start and end.
+//
+// Parser worker returns scopes with location having a sourceId
+// instead of a source object as it doesn't know about main thread source objects.
+function updateLocationsInScopes(state, scopes) {
+ for (const item of scopes) {
+ for (const name of Object.keys(item.bindings)) {
+ for (const ref of item.bindings[name].refs) {
+ const locs = [ref];
+ if (ref.type !== "ref") {
+ locs.push(ref.declaration);
+ }
+ for (const loc of locs) {
+ loc.start = sourceMapToDebuggerLocation(state, loc.start);
+ loc.end = sourceMapToDebuggerLocation(state, loc.end);
+ }
+ }
+ }
+ }
+}
+
+export async function buildMappedScopes(
+ source,
+ content,
+ frame,
+ generatedScopes,
+ thunkArgs
+) {
+ const { getState, parserWorker } = thunkArgs;
+ if (!parserWorker.isLocationSupported(frame.location)) {
+ return null;
+ }
+ const originalAstScopes = await parserWorker.getScopes(frame.location);
+ updateLocationsInScopes(getState(), originalAstScopes);
+ const generatedAstScopes = await parserWorker.getScopes(
+ frame.generatedLocation
+ );
+ updateLocationsInScopes(getState(), generatedAstScopes);
+
+ if (!originalAstScopes || !generatedAstScopes) {
+ return null;
+ }
+
+ const originalRanges = await loadRangeMetadata(
+ frame.location,
+ originalAstScopes,
+ thunkArgs
+ );
+
+ if (hasLineMappings(originalRanges)) {
+ // Fallback to generated scopes as there are no clear mappings to original scopes
+ // This means the scope variable names are likely the same for both the original
+ // generated sources.
+ return { scope: generatedScopes };
+ }
+
+ let generatedAstBindings;
+ if (generatedScopes) {
+ generatedAstBindings = buildGeneratedBindingList(
+ generatedScopes,
+ generatedAstScopes,
+ frame.this
+ );
+ } else {
+ generatedAstBindings = buildFakeBindingList(generatedAstScopes);
+ }
+
+ const { mappedOriginalScopes, expressionLookup } =
+ await mapOriginalBindingsToGenerated(
+ source,
+ content,
+ originalRanges,
+ originalAstScopes,
+ generatedAstBindings,
+ thunkArgs
+ );
+
+ const globalLexicalScope = generatedScopes
+ ? getGlobalFromScope(generatedScopes)
+ : generateGlobalFromAst(generatedAstScopes);
+ const mappedGeneratedScopes = generateClientScope(
+ globalLexicalScope,
+ mappedOriginalScopes
+ );
+
+ return isReliableScope(mappedGeneratedScopes)
+ ? { mappings: expressionLookup, scope: mappedGeneratedScopes }
+ : { scope: generatedScopes };
+}
+
+async function mapOriginalBindingsToGenerated(
+ source,
+ content,
+ originalRanges,
+ originalAstScopes,
+ generatedAstBindings,
+ thunkArgs
+) {
+ const expressionLookup = {};
+ const mappedOriginalScopes = [];
+
+ const cachedSourceMaps = batchScopeMappings(
+ originalAstScopes,
+ source,
+ thunkArgs
+ );
+ // Override sourceMapLoader attribute with the special cached SourceMapLoader instance
+ // in order to make it used by all functions used in this method.
+ thunkArgs = { ...thunkArgs, sourceMapLoader: cachedSourceMaps };
+
+ for (const item of originalAstScopes) {
+ const generatedBindings = {};
+
+ for (const name of Object.keys(item.bindings)) {
+ const binding = item.bindings[name];
+
+ const result = await findGeneratedBinding(
+ source,
+ content,
+ name,
+ binding,
+ originalRanges,
+ generatedAstBindings,
+ thunkArgs
+ );
+
+ if (result) {
+ generatedBindings[name] = result.grip;
+
+ if (
+ binding.refs.length !== 0 &&
+ // These are assigned depth-first, so we don't want shadowed
+ // bindings in parent scopes overwriting the expression.
+ !Object.prototype.hasOwnProperty.call(expressionLookup, name)
+ ) {
+ expressionLookup[name] = result.expression;
+ }
+ }
+ }
+
+ mappedOriginalScopes.push({
+ ...item,
+ generatedBindings,
+ });
+ }
+
+ return {
+ mappedOriginalScopes,
+ expressionLookup,
+ };
+}
+
+/**
+ * Consider a scope and its parents reliable if the vast majority of its
+ * bindings were successfully mapped to generated scope bindings.
+ */
+function isReliableScope(scope) {
+ let totalBindings = 0;
+ let unknownBindings = 0;
+
+ for (let s = scope; s; s = s.parent) {
+ const vars = s.bindings?.variables || {};
+ for (const key of Object.keys(vars)) {
+ const binding = vars[key];
+
+ totalBindings += 1;
+ if (
+ binding.value &&
+ typeof binding.value === "object" &&
+ (binding.value.type === "unscoped" || binding.value.type === "unmapped")
+ ) {
+ unknownBindings += 1;
+ }
+ }
+ }
+
+ // As determined by fair dice roll.
+ return totalBindings === 0 || unknownBindings / totalBindings < 0.25;
+}
+
+function hasLineMappings(ranges) {
+ return ranges.every(
+ range => range.columnStart === 0 && range.columnEnd === Infinity
+ );
+}
+
+/**
+ * Build a special SourceMapLoader instance, based on the one passed in thunkArgs,
+ * which will both:
+ * - preload generated ranges/locations for original locations mentioned
+ * in originalAstScopes
+ * - cache the requests to fetch these genereated ranges/locations
+ */
+function batchScopeMappings(originalAstScopes, source, thunkArgs) {
+ const { sourceMapLoader } = thunkArgs;
+ const precalculatedRanges = new Map();
+ const precalculatedLocations = new Map();
+
+ // Explicitly dispatch all of the sourcemap requests synchronously up front so
+ // that they will be batched into a single request for the worker to process.
+ for (const item of originalAstScopes) {
+ for (const name of Object.keys(item.bindings)) {
+ for (const ref of item.bindings[name].refs) {
+ const locs = [ref];
+ if (ref.type !== "ref") {
+ locs.push(ref.declaration);
+ }
+
+ for (const loc of locs) {
+ precalculatedRanges.set(
+ buildLocationKey(loc.start),
+ sourceMapLoader.getGeneratedRanges(
+ debuggerToSourceMapLocation(loc.start)
+ )
+ );
+ precalculatedLocations.set(
+ buildLocationKey(loc.start),
+ sourceMapLoader.getGeneratedLocation(
+ debuggerToSourceMapLocation(loc.start)
+ )
+ );
+ precalculatedLocations.set(
+ buildLocationKey(loc.end),
+ sourceMapLoader.getGeneratedLocation(
+ debuggerToSourceMapLocation(loc.end)
+ )
+ );
+ }
+ }
+ }
+ }
+
+ return {
+ async getGeneratedRanges(pos) {
+ const key = buildLocationKey(pos);
+
+ if (!precalculatedRanges.has(key)) {
+ log("Bad precalculated mapping");
+ return sourceMapLoader.getGeneratedRanges(
+ debuggerToSourceMapLocation(pos)
+ );
+ }
+ return precalculatedRanges.get(key);
+ },
+
+ async getGeneratedLocation(pos) {
+ const key = buildLocationKey(pos);
+
+ if (!precalculatedLocations.has(key)) {
+ log("Bad precalculated mapping");
+ return sourceMapLoader.getGeneratedLocation(
+ debuggerToSourceMapLocation(pos)
+ );
+ }
+ return precalculatedLocations.get(key);
+ },
+ };
+}
+function buildLocationKey(loc) {
+ return `${loc.line}:${locColumn(loc)}`;
+}
+
+function generateClientScope(globalLexicalScope, originalScopes) {
+ // Build a structure similar to the client's linked scope object using
+ // the original AST scopes, but pulling in the generated bindings
+ // linked to each scope.
+ const result = originalScopes
+ .slice(0, -2)
+ .reverse()
+ .reduce((acc, orig, i) => {
+ const {
+ // The 'this' binding data we have is handled independently, so
+ // the binding data is not included here.
+ // eslint-disable-next-line no-unused-vars
+ this: _this,
+ ...variables
+ } = orig.generatedBindings;
+
+ return {
+ parent: acc,
+ actor: `originalActor${i}`,
+ type: orig.type,
+ scopeKind: orig.scopeKind,
+ bindings: {
+ arguments: [],
+ variables,
+ },
+ ...(orig.type === "function"
+ ? {
+ function: {
+ displayName: orig.displayName,
+ },
+ }
+ : null),
+ ...(orig.type === "block"
+ ? {
+ block: {
+ displayName: orig.displayName,
+ },
+ }
+ : null),
+ };
+ }, globalLexicalScope);
+
+ // The rendering logic in getScope 'this' bindings only runs on the current
+ // selected frame scope, so we pluck out the 'this' binding that was mapped,
+ // and put it in a special location
+ const thisScope = originalScopes.find(scope => scope.bindings.this);
+ if (result.bindings && thisScope) {
+ result.bindings.this = thisScope.generatedBindings.this || null;
+ }
+
+ return result;
+}
+
+function getGlobalFromScope(scopes) {
+ // Pull the root object scope and root lexical scope to reuse them in
+ // our mapped scopes. This assumes that file being processed is
+ // a CommonJS or ES6 module, which might not be ideal. Potentially
+ // should add some logic to try to detect those cases?
+ let globalLexicalScope = null;
+ for (let s = scopes; s.parent; s = s.parent) {
+ globalLexicalScope = s;
+ }
+ if (!globalLexicalScope) {
+ throw new Error("Assertion failure - there should always be a scope");
+ }
+ return globalLexicalScope;
+}
+
+function generateGlobalFromAst(generatedScopes) {
+ const globalLexicalAst = generatedScopes[generatedScopes.length - 2];
+ if (!globalLexicalAst) {
+ throw new Error("Assertion failure - there should always be a scope");
+ }
+ return {
+ actor: "generatedActor1",
+ type: "block",
+ scopeKind: "",
+ bindings: {
+ arguments: [],
+ variables: Object.fromEntries(
+ Object.keys(globalLexicalAst).map(key => [key, getOptimizedOutGrip()])
+ ),
+ },
+ parent: {
+ actor: "generatedActor0",
+ object: getOptimizedOutGrip(),
+ scopeKind: "",
+ type: "object",
+ },
+ };
+}
+
+function hasValidIdent(range, pos) {
+ return (
+ range.type === "match" ||
+ // For declarations, we allow the range on the identifier to be a
+ // more general "contains" to increase the chances of a match.
+ (pos.type !== "ref" && range.type === "contains")
+ );
+}
+
+// eslint-disable-next-line complexity
+async function findGeneratedBinding(
+ source,
+ content,
+ name,
+ originalBinding,
+ originalRanges,
+ generatedAstBindings,
+ thunkArgs
+) {
+ // If there are no references to the implicits, then we have no way to
+ // even attempt to map it back to the original since there is no location
+ // data to use. Bail out instead of just showing it as unmapped.
+ if (
+ originalBinding.type === "implicit" &&
+ !originalBinding.refs.some(item => item.type === "ref")
+ ) {
+ return null;
+ }
+
+ const loadApplicableBindings = async (pos, locationType) => {
+ let applicableBindings = await getApplicableBindingsForOriginalPosition(
+ generatedAstBindings,
+ source,
+ pos,
+ originalBinding.type,
+ locationType,
+ thunkArgs
+ );
+ if (applicableBindings.length) {
+ hadApplicableBindings = true;
+ }
+ if (locationType === "ref") {
+ // Some tooling creates ranges that map a line as a whole, which is useful
+ // for step-debugging, but can easily lead to finding the wrong binding.
+ // To avoid these false-positives, we entirely ignore bindings matched
+ // by ranges that cover full lines.
+ applicableBindings = applicableBindings.filter(
+ ({ range }) =>
+ !(range.start.column === 0 && range.end.column === Infinity)
+ );
+ }
+ if (
+ locationType !== "ref" &&
+ !(await originalRangeStartsInside(pos, thunkArgs))
+ ) {
+ applicableBindings = [];
+ }
+ return applicableBindings;
+ };
+
+ const { refs } = originalBinding;
+
+ let hadApplicableBindings = false;
+ let genContent = null;
+ for (const pos of refs) {
+ const applicableBindings = await loadApplicableBindings(pos, pos.type);
+
+ const range = findMatchingRange(originalRanges, pos);
+ if (range && hasValidIdent(range, pos)) {
+ if (originalBinding.type === "import") {
+ genContent = await findGeneratedImportReference(applicableBindings);
+ } else {
+ genContent = await findGeneratedReference(applicableBindings);
+ }
+ }
+
+ if (
+ (pos.type === "class-decl" || pos.type === "class-inner") &&
+ content.contentType &&
+ content.contentType.match(/\/typescript/)
+ ) {
+ const declRange = findMatchingRange(originalRanges, pos.declaration);
+ if (declRange && declRange.type !== "multiple") {
+ const applicableDeclBindings = await loadApplicableBindings(
+ pos.declaration,
+ pos.type
+ );
+
+ // Resolve to first binding in the range
+ const declContent = await findGeneratedReference(
+ applicableDeclBindings
+ );
+
+ if (declContent) {
+ // Prefer the declaration mapping in this case because TS sometimes
+ // maps class declaration names to "export.Foo = Foo;" or to
+ // the decorator logic itself
+ genContent = declContent;
+ }
+ }
+ }
+
+ if (
+ !genContent &&
+ pos.type === "import-decl" &&
+ typeof pos.importName === "string"
+ ) {
+ const { importName } = pos;
+ const declRange = findMatchingRange(originalRanges, pos.declaration);
+
+ // The import declaration should have an original position mapping,
+ // but otherwise we don't really have preferences on the range type
+ // because it can have multiple bindings, but we do want to make sure
+ // that all of the bindings that match the range are part of the same
+ // import declaration.
+ if (declRange?.singleDeclaration) {
+ const applicableDeclBindings = await loadApplicableBindings(
+ pos.declaration,
+ pos.type
+ );
+
+ // match the import declaration location
+ genContent = await findGeneratedImportDeclaration(
+ applicableDeclBindings,
+ importName
+ );
+ }
+ }
+
+ if (genContent) {
+ break;
+ }
+ }
+
+ if (genContent && genContent.desc) {
+ return {
+ grip: genContent.desc,
+ expression: genContent.expression,
+ };
+ } else if (genContent) {
+ // If there is no descriptor for 'this', then this is not the top-level
+ // 'this' that the server gave us a binding for, and we can just ignore it.
+ if (name === "this") {
+ return null;
+ }
+
+ // If the location is found but the descriptor is not, then it
+ // means that the server scope information didn't match the scope
+ // information from the DevTools parsed scopes.
+ return {
+ grip: {
+ configurable: false,
+ enumerable: true,
+ writable: false,
+ value: {
+ type: "unscoped",
+ unscoped: true,
+
+ // HACK: Until support for "unscoped" lands in devtools-reps,
+ // this will make these show as (unavailable).
+ missingArguments: true,
+ },
+ },
+ expression: null,
+ };
+ } else if (!hadApplicableBindings && name !== "this") {
+ // If there were no applicable bindings to consider while searching for
+ // matching bindings, then the source map for this file didn't make any
+ // attempt to map the binding, and that most likely means that the
+ // code was entirely emitted from the output code.
+ return {
+ grip: getOptimizedOutGrip(),
+ expression: `
+ (() => {
+ throw new Error('"' + ${JSON.stringify(
+ name
+ )} + '" has been optimized out.');
+ })()
+ `,
+ };
+ }
+
+ // If no location mapping is found, then the map is bad, or
+ // the map is okay but it original location is inside
+ // of some scope, but the generated location is outside, leading
+ // us to search for bindings that don't technically exist.
+ return {
+ grip: {
+ configurable: false,
+ enumerable: true,
+ writable: false,
+ value: {
+ type: "unmapped",
+ unmapped: true,
+
+ // HACK: Until support for "unmapped" lands in devtools-reps,
+ // this will make these show as (unavailable).
+ missingArguments: true,
+ },
+ },
+ expression: null,
+ };
+}
diff --git a/devtools/client/debugger/src/utils/pause/mapScopes/locColumn.js b/devtools/client/debugger/src/utils/pause/mapScopes/locColumn.js
new file mode 100644
index 0000000000..075e902299
--- /dev/null
+++ b/devtools/client/debugger/src/utils/pause/mapScopes/locColumn.js
@@ -0,0 +1,13 @@
+/* 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 locColumn(loc) {
+ if (typeof loc.column !== "number") {
+ // This shouldn't really happen with locations from the AST, but
+ // the datatype we are using allows null/undefined column.
+ return 0;
+ }
+
+ return loc.column;
+}
diff --git a/devtools/client/debugger/src/utils/pause/mapScopes/mappingContains.js b/devtools/client/debugger/src/utils/pause/mapScopes/mappingContains.js
new file mode 100644
index 0000000000..ca82fe42ec
--- /dev/null
+++ b/devtools/client/debugger/src/utils/pause/mapScopes/mappingContains.js
@@ -0,0 +1,12 @@
+/* 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 { positionCmp } from "./positionCmp";
+
+export function mappingContains(mapped, item) {
+ return (
+ positionCmp(item.start, mapped.start) >= 0 &&
+ positionCmp(item.end, mapped.end) <= 0
+ );
+}
diff --git a/devtools/client/debugger/src/utils/pause/mapScopes/moz.build b/devtools/client/debugger/src/utils/pause/mapScopes/moz.build
new file mode 100644
index 0000000000..05f2b7e3d8
--- /dev/null
+++ b/devtools/client/debugger/src/utils/pause/mapScopes/moz.build
@@ -0,0 +1,19 @@
+# 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(
+ "buildGeneratedBindingList.js",
+ "filtering.js",
+ "findGeneratedBindingFromPosition.js",
+ "getApplicableBindingsForOriginalPosition.js",
+ "index.js",
+ "locColumn.js",
+ "mappingContains.js",
+ "optimizedOut.js",
+ "positionCmp.js",
+ "rangeMetadata.js",
+)
diff --git a/devtools/client/debugger/src/utils/pause/mapScopes/optimizedOut.js b/devtools/client/debugger/src/utils/pause/mapScopes/optimizedOut.js
new file mode 100644
index 0000000000..755b308a2d
--- /dev/null
+++ b/devtools/client/debugger/src/utils/pause/mapScopes/optimizedOut.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 getOptimizedOutGrip() {
+ return {
+ configurable: false,
+ enumerable: true,
+ writable: false,
+ value: {
+ type: "null",
+ optimizedOut: true,
+ },
+ };
+}
diff --git a/devtools/client/debugger/src/utils/pause/mapScopes/positionCmp.js b/devtools/client/debugger/src/utils/pause/mapScopes/positionCmp.js
new file mode 100644
index 0000000000..5d53fb933e
--- /dev/null
+++ b/devtools/client/debugger/src/utils/pause/mapScopes/positionCmp.js
@@ -0,0 +1,24 @@
+/* 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 { locColumn } from "./locColumn";
+
+/**
+ * * === 0 - Positions are equal.
+ * * < 0 - first position before second position
+ * * > 0 - first position after second position
+ */
+export function positionCmp(p1, p2) {
+ if (p1.line === p2.line) {
+ const l1 = locColumn(p1);
+ const l2 = locColumn(p2);
+
+ if (l1 === l2) {
+ return 0;
+ }
+ return l1 < l2 ? -1 : 1;
+ }
+
+ return p1.line < p2.line ? -1 : 1;
+}
diff --git a/devtools/client/debugger/src/utils/pause/mapScopes/rangeMetadata.js b/devtools/client/debugger/src/utils/pause/mapScopes/rangeMetadata.js
new file mode 100644
index 0000000000..7e04c34e35
--- /dev/null
+++ b/devtools/client/debugger/src/utils/pause/mapScopes/rangeMetadata.js
@@ -0,0 +1,117 @@
+/* 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 { locColumn } from "./locColumn";
+import { positionCmp } from "./positionCmp";
+import { filterSortedArray } from "./filtering";
+
+// * match - Range contains a single identifier with matching start location
+// * contains - Range contains a single identifier with non-matching start
+// * multiple - Range contains multiple identifiers
+// * empty - Range contains no identifiers
+
+export async function loadRangeMetadata(
+ location,
+ originalAstScopes,
+ { sourceMapLoader }
+) {
+ const originalRanges = await sourceMapLoader.getOriginalRanges(
+ location.source.id
+ );
+
+ const sortedOriginalAstBindings = [];
+ for (const item of originalAstScopes) {
+ for (const name of Object.keys(item.bindings)) {
+ for (const ref of item.bindings[name].refs) {
+ sortedOriginalAstBindings.push(ref);
+ }
+ }
+ }
+ sortedOriginalAstBindings.sort((a, b) => positionCmp(a.start, b.start));
+
+ let i = 0;
+
+ return originalRanges.map(range => {
+ const bindings = [];
+
+ while (
+ i < sortedOriginalAstBindings.length &&
+ (sortedOriginalAstBindings[i].start.line < range.line ||
+ (sortedOriginalAstBindings[i].start.line === range.line &&
+ locColumn(sortedOriginalAstBindings[i].start) < range.columnStart))
+ ) {
+ i++;
+ }
+
+ while (
+ i < sortedOriginalAstBindings.length &&
+ sortedOriginalAstBindings[i].start.line === range.line &&
+ locColumn(sortedOriginalAstBindings[i].start) >= range.columnStart &&
+ locColumn(sortedOriginalAstBindings[i].start) < range.columnEnd
+ ) {
+ const lastBinding = bindings[bindings.length - 1];
+ // Only add bindings when they're in new positions
+ if (
+ !lastBinding ||
+ positionCmp(lastBinding.start, sortedOriginalAstBindings[i].start) !== 0
+ ) {
+ bindings.push(sortedOriginalAstBindings[i]);
+ }
+ i++;
+ }
+
+ let type = "empty";
+ let singleDeclaration = true;
+ if (bindings.length === 1) {
+ const binding = bindings[0];
+ if (
+ binding.start.line === range.line &&
+ binding.start.column === range.columnStart
+ ) {
+ type = "match";
+ } else {
+ type = "contains";
+ }
+ } else if (bindings.length > 1) {
+ type = "multiple";
+ const binding = bindings[0];
+ const declStart =
+ binding.type !== "ref" ? binding.declaration.start : null;
+
+ singleDeclaration = bindings.every(b => {
+ return (
+ declStart &&
+ b.type !== "ref" &&
+ positionCmp(declStart, b.declaration.start) === 0
+ );
+ });
+ }
+
+ return {
+ type,
+ singleDeclaration,
+ ...range,
+ };
+ });
+}
+
+export function findMatchingRange(sortedOriginalRanges, bindingRange) {
+ return filterSortedArray(sortedOriginalRanges, range => {
+ if (range.line < bindingRange.start.line) {
+ return -1;
+ }
+ if (range.line > bindingRange.start.line) {
+ return 1;
+ }
+
+ if (range.columnEnd <= locColumn(bindingRange.start)) {
+ return -1;
+ }
+ if (range.columnStart > locColumn(bindingRange.start)) {
+ return 1;
+ }
+
+ return 0;
+ }).pop();
+}
diff --git a/devtools/client/debugger/src/utils/pause/moz.build b/devtools/client/debugger/src/utils/pause/moz.build
new file mode 100644
index 0000000000..db8b733274
--- /dev/null
+++ b/devtools/client/debugger/src/utils/pause/moz.build
@@ -0,0 +1,15 @@
+# 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 += [
+ "frames",
+ "mapScopes",
+]
+
+CompiledModules(
+ "index.js",
+ "scopes.js",
+ "why.js",
+)
diff --git a/devtools/client/debugger/src/utils/pause/scopes.js b/devtools/client/debugger/src/utils/pause/scopes.js
new file mode 100644
index 0000000000..bdb53ba493
--- /dev/null
+++ b/devtools/client/debugger/src/utils/pause/scopes.js
@@ -0,0 +1,283 @@
+/* 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/>. */
+
+// This file contains utility functions which supports the structure & display of
+// scopes information in Scopes panel.
+
+import { objectInspector } from "devtools/client/shared/components/reps/index";
+import { simplifyDisplayName } from "../pause/frames/index";
+
+const {
+ utils: {
+ node: { NODE_TYPES },
+ },
+} = objectInspector;
+
+// The heading that should be displayed for the scope
+function _getScopeTitle(type, scope) {
+ if (type === "block" && scope.block && scope.block.displayName) {
+ return scope.block.displayName;
+ }
+
+ if (type === "function" && scope.function) {
+ return scope.function.displayName
+ ? simplifyDisplayName(scope.function.displayName)
+ : L10N.getStr("anonymousFunction");
+ }
+ return L10N.getStr("scopes.block");
+}
+
+function _getThisVariable(this_, path) {
+ if (!this_) {
+ return null;
+ }
+
+ return {
+ name: "<this>",
+ path: `${path}/<this>`,
+ contents: { value: this_ },
+ };
+}
+
+/**
+ * Builds a tree of nodes representing all the variables and arguments
+ * for the bindings from a scope.
+ *
+ * Each binding => { variables: Array, arguments: Array }
+ * Each binding argument => [name: string, contents: BindingContents]
+ *
+ * @param {Array} bindings
+ * @param {String} parentName
+ * @returns
+ */
+function _getBindingVariables(bindings, parentName) {
+ if (!bindings) {
+ return [];
+ }
+
+ const nodes = [];
+ const addNode = (name, contents) =>
+ nodes.push({ name, contents, path: `${parentName}/${name}` });
+
+ for (const arg of bindings.arguments) {
+ // `arg` is an object which only has a single property whose name is the name of the
+ // argument. So here we can directly pick the first (and only) entry of `arg`
+ const [name, contents] = Object.entries(arg)[0];
+ addNode(name, contents);
+ }
+
+ for (const name in bindings.variables) {
+ addNode(name, bindings.variables[name]);
+ }
+
+ return nodes;
+}
+
+/**
+ * This generates the scope item for rendering in the scopes panel.
+ *
+ * @param {*} scope
+ * @param {*} selectedFrame
+ * @param {*} frameScopes
+ * @param {*} why
+ * @param {*} scopeIndex
+ * @returns
+ */
+function _getScopeItem(scope, selectedFrame, frameScopes, why, scopeIndex) {
+ const { type, actor } = scope;
+
+ const isLocalScope = scope.actor === frameScopes.actor;
+
+ const key = `${actor}-${scopeIndex}`;
+ if (type === "function" || type === "block") {
+ const { bindings } = scope;
+
+ let vars = _getBindingVariables(bindings, key);
+
+ // show exception, return, and this variables in innermost scope
+ if (isLocalScope) {
+ vars = vars.concat(_getFrameExceptionOrReturnedValueVariables(why, key));
+
+ let thisDesc_ = selectedFrame.this;
+
+ if (bindings && "this" in bindings) {
+ // The presence of "this" means we're rendering a "this" binding
+ // generated from mapScopes and this can override the binding
+ // provided by the current frame.
+ thisDesc_ = bindings.this ? bindings.this.value : null;
+ }
+
+ const this_ = _getThisVariable(thisDesc_, key);
+
+ if (this_) {
+ vars.push(this_);
+ }
+ }
+
+ if (vars?.length) {
+ const title = _getScopeTitle(type, scope) || "";
+ vars.sort((a, b) => a.name.localeCompare(b.name));
+ return {
+ name: title,
+ path: key,
+ contents: vars,
+ type: NODE_TYPES.BLOCK,
+ };
+ }
+ } else if (type === "object" && scope.object) {
+ let value = scope.object;
+ // If this is the global window scope, mark it as such so that it will
+ // preview Window: Global instead of Window: Window
+ if (value.class === "Window") {
+ value = { ...value, displayClass: "Global" };
+ }
+ return {
+ name: scope.object.class,
+ path: key,
+ contents: { value },
+ };
+ }
+
+ return null;
+}
+/**
+ * Merge the scope bindings for lexical scopes and its parent function body scopes
+ * Note: block scopes are not merged. See browser_dbg-merge-scopes.js for test examples
+ * to better understand the scenario,
+ *
+ * @param {*} scope
+ * @param {*} parentScope
+ * @param {*} item
+ * @param {*} parentItem
+ * @returns
+ */
+export function _mergeLexicalScopesBindings(
+ scope,
+ parentScope,
+ item,
+ parentItem
+) {
+ if (scope.scopeKind == "function lexical" && parentScope.type == "function") {
+ const contents = item.contents.concat(parentItem.contents);
+ contents.sort((a, b) => a.name.localeCompare(b.name));
+
+ return {
+ name: parentItem.name,
+ path: parentItem.path,
+ contents,
+ type: NODE_TYPES.BLOCK,
+ };
+ }
+ return null;
+}
+
+/**
+ * Returns a string path for an scope item which can be used
+ * in different pauses for a thread.
+ *
+ * @param {Object} item
+ * @returns
+ */
+
+export function getScopeItemPath(item) {
+ // Calling toString() on item.path allows symbols to be handled.
+ return item.path.toString();
+}
+
+// Generate variables when the function throws an exception or returned a value.
+function _getFrameExceptionOrReturnedValueVariables(why, path) {
+ const vars = [];
+
+ if (why && why.frameFinished) {
+ const { frameFinished } = why;
+
+ // Always display a `throw` property if present, even if it is falsy.
+ if (Object.prototype.hasOwnProperty.call(frameFinished, "throw")) {
+ vars.push({
+ name: "<exception>",
+ path: `${path}/<exception>`,
+ contents: { value: frameFinished.throw },
+ });
+ }
+
+ if (Object.prototype.hasOwnProperty.call(frameFinished, "return")) {
+ const returned = frameFinished.return;
+
+ // Do not display undefined. Do display falsy values like 0 and false. The
+ // protocol grip for undefined is a JSON object: { type: "undefined" }.
+ if (typeof returned !== "object" || returned.type !== "undefined") {
+ vars.push({
+ name: "<return>",
+ path: `${path}/<return>`,
+ contents: { value: returned },
+ });
+ }
+ }
+ }
+
+ return vars;
+}
+
+/**
+ * Generates the scope items (for scopes related to selected frame) to be rendered in the scope panel
+ * @param {*} why
+ * @param {*} selectedFrame
+ * @param {*} frameScopes
+ * @returns
+ */
+export function getScopesItemsForSelectedFrame(
+ why,
+ selectedFrame,
+ frameScopes
+) {
+ if (!why || !selectedFrame) {
+ return null;
+ }
+
+ if (!frameScopes) {
+ return null;
+ }
+
+ const scopes = [];
+
+ let currentScope = frameScopes;
+ let currentScopeIndex = 1;
+
+ let prevScope = null,
+ prevScopeItem = null;
+
+ while (currentScope) {
+ let currentScopeItem = _getScopeItem(
+ currentScope,
+ selectedFrame,
+ frameScopes,
+ why,
+ currentScopeIndex
+ );
+
+ if (currentScopeItem) {
+ const mergedItem =
+ prevScope && prevScopeItem
+ ? _mergeLexicalScopesBindings(
+ prevScope,
+ currentScope,
+ prevScopeItem,
+ currentScopeItem
+ )
+ : null;
+ if (mergedItem) {
+ currentScopeItem = mergedItem;
+ scopes.pop();
+ }
+ scopes.push(currentScopeItem);
+ }
+
+ prevScope = currentScope;
+ prevScopeItem = currentScopeItem;
+ currentScopeIndex++;
+ currentScope = currentScope.parent;
+ }
+
+ return scopes;
+}
diff --git a/devtools/client/debugger/src/utils/pause/tests/scopes.spec.js b/devtools/client/debugger/src/utils/pause/tests/scopes.spec.js
new file mode 100644
index 0000000000..08ac425774
--- /dev/null
+++ b/devtools/client/debugger/src/utils/pause/tests/scopes.spec.js
@@ -0,0 +1,150 @@
+/* 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 { getScopesItemsForSelectedFrame } from "../scopes";
+import {
+ makeMockFrame,
+ makeMockScope,
+ makeWhyNormal,
+ makeWhyThrow,
+ mockScopeAddVariable,
+} from "../../test-mockup";
+
+function convertScope(scope) {
+ return scope;
+}
+
+describe("scopes", () => {
+ describe("getScopes", () => {
+ it("single scope", () => {
+ const pauseData = makeWhyNormal();
+ const scope = makeMockScope("actor1");
+ const selectedFrame = makeMockFrame(undefined, undefined, scope);
+
+ if (!selectedFrame.scope) {
+ throw new Error("Frame must include scopes");
+ }
+
+ const frameScopes = convertScope(selectedFrame.scope);
+ const scopes = getScopesItemsForSelectedFrame(
+ pauseData,
+ selectedFrame,
+ frameScopes
+ );
+ if (!scopes) {
+ throw new Error("missing scopes");
+ }
+ expect(scopes[0].path).toEqual("actor1-1");
+ expect(scopes[0].contents[0]).toEqual({
+ name: "<this>",
+ path: "actor1-1/<this>",
+ contents: { value: {} },
+ });
+ });
+
+ it("second scope", () => {
+ const pauseData = makeWhyNormal();
+ const scope0 = makeMockScope("actor2");
+ const scope1 = makeMockScope("actor1", undefined, scope0);
+ const selectedFrame = makeMockFrame(undefined, undefined, scope1);
+ mockScopeAddVariable(scope0, "foo");
+
+ if (!selectedFrame.scope) {
+ throw new Error("Frame must include scopes");
+ }
+
+ const frameScopes = convertScope(selectedFrame.scope);
+ const scopes = getScopesItemsForSelectedFrame(
+ pauseData,
+ selectedFrame,
+ frameScopes
+ );
+ if (!scopes) {
+ throw new Error("missing scopes");
+ }
+ expect(scopes[1].path).toEqual("actor2-2");
+ expect(scopes[1].contents[0]).toEqual({
+ name: "foo",
+ path: "actor2-2/foo",
+ contents: { value: null },
+ });
+ });
+
+ it("returning scope", () => {
+ const why = makeWhyNormal("to sender");
+ const scope = makeMockScope("actor1");
+ const selectedFrame = makeMockFrame(undefined, undefined, scope);
+
+ if (!selectedFrame.scope) {
+ throw new Error("Frame must include scopes");
+ }
+
+ const frameScopes = convertScope(selectedFrame.scope);
+ const scopes = getScopesItemsForSelectedFrame(
+ why,
+ selectedFrame,
+ frameScopes
+ );
+ expect(scopes).toMatchObject([
+ {
+ path: "actor1-1",
+ contents: [
+ {
+ name: "<return>",
+ path: "actor1-1/<return>",
+ contents: {
+ value: "to sender",
+ },
+ },
+ {
+ name: "<this>",
+ path: "actor1-1/<this>",
+ contents: {
+ value: {},
+ },
+ },
+ ],
+ },
+ ]);
+ });
+
+ it("throwing scope", () => {
+ const why = makeWhyThrow("a party");
+ const scope = makeMockScope("actor1");
+ const selectedFrame = makeMockFrame(undefined, undefined, scope);
+
+ if (!selectedFrame.scope) {
+ throw new Error("Frame must include scopes");
+ }
+
+ const frameScopes = convertScope(selectedFrame.scope);
+ const scopes = getScopesItemsForSelectedFrame(
+ why,
+ selectedFrame,
+ frameScopes
+ );
+ expect(scopes).toMatchObject([
+ {
+ path: "actor1-1",
+ contents: [
+ {
+ name: "<exception>",
+ path: "actor1-1/<exception>",
+ contents: {
+ value: "a party",
+ },
+ },
+ {
+ name: "<this>",
+ path: "actor1-1/<this>",
+ contents: {
+ value: {},
+ },
+ },
+ ],
+ },
+ ]);
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/utils/pause/why.js b/devtools/client/debugger/src/utils/pause/why.js
new file mode 100644
index 0000000000..115d94873b
--- /dev/null
+++ b/devtools/client/debugger/src/utils/pause/why.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 { DEBUGGER_PAUSED_REASONS_L10N_MAPPING } from "devtools/shared/constants";
+
+export function getPauseReason(why) {
+ if (!why) {
+ return null;
+ }
+
+ const reasonType = why.type;
+ if (!DEBUGGER_PAUSED_REASONS_L10N_MAPPING[reasonType]) {
+ console.log("Please file an issue: reasonType=", reasonType);
+ }
+
+ return DEBUGGER_PAUSED_REASONS_L10N_MAPPING[reasonType];
+}
+
+export function isException(why) {
+ return why?.type === "exception";
+}
+
+export function isInterrupted(why) {
+ return why?.type === "interrupted";
+}
+
+export function inDebuggerEval(why) {
+ if (
+ why &&
+ why.type === "exception" &&
+ why.exception &&
+ why.exception.preview &&
+ why.exception.preview.fileName
+ ) {
+ return why.exception.preview.fileName === "debugger eval code";
+ }
+
+ return false;
+}
diff --git a/devtools/client/debugger/src/utils/prefs.js b/devtools/client/debugger/src/utils/prefs.js
new file mode 100644
index 0000000000..287f3f8cc7
--- /dev/null
+++ b/devtools/client/debugger/src/utils/prefs.js
@@ -0,0 +1,170 @@
+/* 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/>. */
+
+const { PrefsHelper } = require("resource://devtools/client/shared/prefs.js");
+
+import { isNode } from "./environment";
+
+// Schema version to bump when the async store format has changed incompatibly
+// and old stores should be cleared.
+const prefsSchemaVersion = 11;
+const { pref } = Services;
+
+if (isNode()) {
+ pref("devtools.debugger.logging", false);
+ pref("devtools.debugger.alphabetize-outline", false);
+ pref("devtools.debugger.auto-pretty-print", false);
+ pref("devtools.source-map.client-service.enabled", true);
+ pref("devtools.chrome.enabled", false);
+ pref("devtools.debugger.pause-on-debugger-statement", true);
+ pref("devtools.debugger.pause-on-exceptions", false);
+ pref("devtools.debugger.pause-on-caught-exceptions", false);
+ pref("devtools.debugger.ignore-caught-exceptions", true);
+ pref("devtools.debugger.call-stack-visible", true);
+ pref("devtools.debugger.scopes-visible", true);
+ pref("devtools.debugger.threads-visible", true);
+ pref("devtools.debugger.expressions-visible", false);
+ pref("devtools.debugger.xhr-breakpoints-visible", false);
+ pref("devtools.debugger.breakpoints-visible", true);
+ pref("devtools.debugger.event-listeners-visible", false);
+ pref("devtools.debugger.dom-mutation-breakpoints-visible", false);
+ pref("devtools.debugger.start-panel-collapsed", false);
+ pref("devtools.debugger.end-panel-collapsed", false);
+ pref("devtools.debugger.start-panel-size", 300);
+ pref("devtools.debugger.end-panel-size", 300);
+ pref("devtools.debugger.ui.editor-wrapping", false);
+ pref("devtools.debugger.ui.framework-grouping-on", true);
+ pref("devtools.debugger.pending-selected-location", "{}");
+ pref("devtools.debugger.expressions", "[]");
+ pref("devtools.debugger.search-options", "{}");
+ pref("devtools.debugger.project-directory-root", "");
+ pref("devtools.debugger.map-scopes-enabled", false);
+ pref("devtools.debugger.prefs-schema-version", prefsSchemaVersion);
+ pref("devtools.debugger.skip-pausing", false);
+ pref("devtools.debugger.log-actions", true);
+ pref("devtools.debugger.log-event-breakpoints", false);
+ pref("devtools.debugger.javascript-tracing-log-method", "console");
+ pref("devtools.debugger.javascript-tracing-values", false);
+ pref("devtools.debugger.javascript-tracing-on-next-interaction", false);
+ pref("devtools.debugger.javascript-tracing-on-next-load", false);
+ pref("devtools.debugger.javascript-tracing-function-return", false);
+ pref("devtools.debugger.hide-ignored-sources", false);
+ pref("devtools.debugger.source-map-ignore-list-enabled", true);
+ pref("devtools.debugger.features.wasm", true);
+ pref("devtools.debugger.features.code-folding", false);
+ pref("devtools.debugger.features.autocomplete-expressions", false);
+ pref("devtools.debugger.features.map-expression-bindings", true);
+ pref("devtools.debugger.features.map-await-expression", true);
+ pref("devtools.debugger.features.log-points", true);
+ pref("devtools.debugger.features.inline-preview", true);
+ pref("devtools.debugger.features.javascript-tracing", false);
+ pref("devtools.debugger.features.codemirror-next", false);
+ pref("devtools.editor.tabsize", 2);
+ pref("devtools.editor.expandtab", false);
+ pref("devtools.editor.autoclosebrackets", false);
+}
+
+export const prefs = new PrefsHelper("devtools", {
+ logging: ["Bool", "debugger.logging"],
+ editorWrapping: ["Bool", "debugger.ui.editor-wrapping"],
+ alphabetizeOutline: ["Bool", "debugger.alphabetize-outline"],
+ autoPrettyPrint: ["Bool", "debugger.auto-pretty-print"],
+ clientSourceMapsEnabled: ["Bool", "source-map.client-service.enabled"],
+ chromeAndExtensionsEnabled: ["Bool", "chrome.enabled"],
+ pauseOnDebuggerStatement: ["Bool", "debugger.pause-on-debugger-statement"],
+ pauseOnExceptions: ["Bool", "debugger.pause-on-exceptions"],
+ pauseOnCaughtExceptions: ["Bool", "debugger.pause-on-caught-exceptions"],
+ ignoreCaughtExceptions: ["Bool", "debugger.ignore-caught-exceptions"],
+ callStackVisible: ["Bool", "debugger.call-stack-visible"],
+ scopesVisible: ["Bool", "debugger.scopes-visible"],
+ threadsVisible: ["Bool", "debugger.threads-visible"],
+ breakpointsVisible: ["Bool", "debugger.breakpoints-visible"],
+ expressionsVisible: ["Bool", "debugger.expressions-visible"],
+ xhrBreakpointsVisible: ["Bool", "debugger.xhr-breakpoints-visible"],
+ eventListenersVisible: ["Bool", "debugger.event-listeners-visible"],
+ domMutationBreakpointsVisible: [
+ "Bool",
+ "debugger.dom-mutation-breakpoints-visible",
+ ],
+ startPanelCollapsed: ["Bool", "debugger.start-panel-collapsed"],
+ endPanelCollapsed: ["Bool", "debugger.end-panel-collapsed"],
+ startPanelSize: ["Int", "debugger.start-panel-size"],
+ endPanelSize: ["Int", "debugger.end-panel-size"],
+ frameworkGroupingOn: ["Bool", "debugger.ui.framework-grouping-on"],
+ pendingSelectedLocation: ["Json", "debugger.pending-selected-location", {}],
+ expressions: ["Json", "debugger.expressions", []],
+ searchOptions: ["Json", "debugger.search-options"],
+ debuggerPrefsSchemaVersion: ["Int", "debugger.prefs-schema-version"],
+ projectDirectoryRoot: ["Char", "debugger.project-directory-root", ""],
+ projectDirectoryRootName: [
+ "Char",
+ "debugger.project-directory-root-name",
+ "",
+ ],
+ skipPausing: ["Bool", "debugger.skip-pausing"],
+ mapScopes: ["Bool", "debugger.map-scopes-enabled"],
+ logActions: ["Bool", "debugger.log-actions"],
+ logEventBreakpoints: ["Bool", "debugger.log-event-breakpoints"],
+ indentSize: ["Int", "editor.tabsize"],
+ javascriptTracingLogMethod: [
+ "String",
+ "debugger.javascript-tracing-log-method",
+ ],
+ javascriptTracingValues: ["Bool", "debugger.javascript-tracing-values"],
+ javascriptTracingOnNextInteraction: [
+ "Bool",
+ "debugger.javascript-tracing-on-next-interaction",
+ ],
+ javascriptTracingOnNextLoad: [
+ "Bool",
+ "debugger.javascript-tracing-on-next-load",
+ ],
+ javascriptTracingFunctionReturn: [
+ "Bool",
+ "debugger.javascript-tracing-function-return",
+ ],
+ hideIgnoredSources: ["Bool", "debugger.hide-ignored-sources"],
+ sourceMapIgnoreListEnabled: [
+ "Bool",
+ "debugger.source-map-ignore-list-enabled",
+ ],
+});
+
+// The pref may not be defined. Defaulting to null isn't viable (cursor never blinks).
+// Can't use CodeMirror.defaults here because it's loaded later.
+// Hardcode the fallback value to that of CodeMirror.defaults.cursorBlinkRate.
+prefs.cursorBlinkRate = Services.prefs.getIntPref("ui.caretBlinkTime", 530);
+
+export const features = new PrefsHelper("devtools.debugger.features", {
+ wasm: ["Bool", "wasm"],
+ outline: ["Bool", "outline"],
+ codeFolding: ["Bool", "code-folding"],
+ autocompleteExpression: ["Bool", "autocomplete-expressions"],
+ mapExpressionBindings: ["Bool", "map-expression-bindings"],
+ mapAwaitExpression: ["Bool", "map-await-expression"],
+ logPoints: ["Bool", "log-points"],
+ inlinePreview: ["Bool", "inline-preview"],
+ windowlessServiceWorkers: ["Bool", "windowless-service-workers"],
+ javascriptTracing: ["Bool", "javascript-tracing"],
+ codemirrorNext: ["Bool", "codemirror-next"],
+});
+
+// Import the asyncStore already spawned by the TargetMixin class
+const ThreadUtils = require("resource://devtools/client/shared/thread-utils.js");
+export const asyncStore = ThreadUtils.asyncStore;
+
+export function resetSchemaVersion() {
+ prefs.debuggerPrefsSchemaVersion = prefsSchemaVersion;
+}
+
+export function verifyPrefSchema() {
+ if (prefs.debuggerPrefsSchemaVersion < prefsSchemaVersion) {
+ asyncStore.pendingBreakpoints = {};
+ asyncStore.tabs = [];
+ asyncStore.xhrBreakpoints = [];
+ asyncStore.eventListenerBreakpoints = undefined;
+ asyncStore.blackboxedRanges = {};
+ prefs.debuggerPrefsSchemaVersion = prefsSchemaVersion;
+ }
+}
diff --git a/devtools/client/debugger/src/utils/preview.js b/devtools/client/debugger/src/utils/preview.js
new file mode 100644
index 0000000000..48b9d2bc98
--- /dev/null
+++ b/devtools/client/debugger/src/utils/preview.js
@@ -0,0 +1,7 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+export function isConsole(expression) {
+ return /^console/.test(expression);
+}
diff --git a/devtools/client/debugger/src/utils/quick-open.js b/devtools/client/debugger/src/utils/quick-open.js
new file mode 100644
index 0000000000..e2624559bc
--- /dev/null
+++ b/devtools/client/debugger/src/utils/quick-open.js
@@ -0,0 +1,113 @@
+/* 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 { endTruncateStr } from "./utils";
+import {
+ getFilename,
+ getSourceClassnames,
+ getSourceQueryString,
+ getRelativeUrl,
+} from "./source";
+
+export const MODIFIERS = {
+ "@": "functions",
+ "#": "variables",
+ ":": "goto",
+ "?": "shortcuts",
+};
+
+export function parseQuickOpenQuery(query) {
+ const startsWithModifier =
+ query[0] === "@" ||
+ query[0] === "#" ||
+ query[0] === ":" ||
+ query[0] === "?";
+
+ if (startsWithModifier) {
+ const modifier = query[0];
+ return MODIFIERS[modifier];
+ }
+
+ const isGotoSource = query.includes(":", 1);
+
+ if (isGotoSource) {
+ return "gotoSource";
+ }
+
+ return "sources";
+}
+
+export function parseLineColumn(query) {
+ const [, line, column] = query.split(":");
+ const lineNumber = parseInt(line, 10);
+ const columnNumber = parseInt(column, 10);
+ if (isNaN(lineNumber)) {
+ return null;
+ }
+ if (isNaN(columnNumber)) {
+ return { line: lineNumber };
+ }
+ // columnNumber here is the user input value which is 1-based.
+ // Whereas in location objects, line is 1-based, and column is 0-based.
+ return {
+ line: lineNumber,
+ column: columnNumber - 1,
+ };
+}
+
+export function formatSourceForList(
+ source,
+ hasTabOpened,
+ isBlackBoxed,
+ projectDirectoryRoot
+) {
+ const title = getFilename(source);
+ const relativeUrlWithQuery = `${getRelativeUrl(
+ source,
+ projectDirectoryRoot
+ )}${getSourceQueryString(source) || ""}`;
+ const subtitle = endTruncateStr(relativeUrlWithQuery, 100);
+ const value = relativeUrlWithQuery;
+ return {
+ value,
+ title,
+ subtitle,
+ icon: hasTabOpened
+ ? "tab result-item-icon"
+ : `result-item-icon ${getSourceClassnames(source, null, isBlackBoxed)}`,
+ id: source.id,
+ url: source.url,
+ source,
+ };
+}
+
+export function formatSymbol(symbol) {
+ return {
+ id: `${symbol.name}:${symbol.location.start.line}`,
+ title: symbol.name,
+ subtitle: `${symbol.location.start.line}`,
+ value: symbol.name,
+ location: symbol.location,
+ };
+}
+
+export function formatShortcutResults() {
+ return [
+ {
+ value: L10N.getStr("symbolSearch.search.functionsPlaceholder.title"),
+ title: `@ ${L10N.getStr("symbolSearch.search.functionsPlaceholder")}`,
+ id: "@",
+ },
+ {
+ value: L10N.getStr("symbolSearch.search.variablesPlaceholder.title"),
+ title: `# ${L10N.getStr("symbolSearch.search.variablesPlaceholder")}`,
+ id: "#",
+ },
+ {
+ value: L10N.getStr("gotoLineModal.title"),
+ title: `: ${L10N.getStr("gotoLineModal.placeholder")}`,
+ id: ":",
+ },
+ ];
+}
diff --git a/devtools/client/debugger/src/utils/result-list.js b/devtools/client/debugger/src/utils/result-list.js
new file mode 100644
index 0000000000..c8fe97ade6
--- /dev/null
+++ b/devtools/client/debugger/src/utils/result-list.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 scrollList(resultList, index) {
+ if (!resultList.hasOwnProperty(index)) {
+ return;
+ }
+
+ const resultEl = resultList[index];
+
+ const scroll = () => {
+ // Avoid expensive DOM computations involved in scrollIntoView
+ // https://nolanlawson.com/2018/09/25/accurately-measuring-layout-on-the-web/
+ requestAnimationFrame(() => {
+ setTimeout(() => {
+ if (!resultEl.scrollIntoView) {
+ return;
+ }
+ resultEl.scrollIntoView({ block: "nearest", behavior: "auto" });
+ });
+ });
+ };
+
+ scroll();
+}
diff --git a/devtools/client/debugger/src/utils/selected-location.js b/devtools/client/debugger/src/utils/selected-location.js
new file mode 100644
index 0000000000..628186ff22
--- /dev/null
+++ b/devtools/client/debugger/src/utils/selected-location.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 getSelectedLocation(mappedLocation, context) {
+ if (!context) {
+ return mappedLocation.location;
+ }
+
+ // `context` may be a location or directly a source object.
+ const source = context.source || context;
+ return source.isOriginal
+ ? mappedLocation.location
+ : mappedLocation.generatedLocation;
+}
diff --git a/devtools/client/debugger/src/utils/shallow-equal.js b/devtools/client/debugger/src/utils/shallow-equal.js
new file mode 100644
index 0000000000..f75430f476
--- /dev/null
+++ b/devtools/client/debugger/src/utils/shallow-equal.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/>. */
+
+/**
+ * Shallow equal will consider equal:
+ * - exact same values (strict '===' equality)
+ * - distinct array instances having the exact same values in them (same number and strict equality).
+ * - distinct object instances having the exact same attributes and values.
+ *
+ * It will typically consider different array and object whose values
+ * aren't strictly equal. You may consider using "deep equality" checks for this scenario.
+ */
+export function shallowEqual(value, other) {
+ if (value === other) {
+ return true;
+ }
+
+ if (Array.isArray(value) && Array.isArray(other)) {
+ return arrayShallowEqual(value, other);
+ }
+
+ if (isObject(value) && isObject(other)) {
+ return objectShallowEqual(value, other);
+ }
+
+ return false;
+}
+
+export function arrayShallowEqual(value, other) {
+ // Redo this check in case we are called directly from the selectors.
+ if (value === other) {
+ return true;
+ }
+ return value.length === other.length && value.every((k, i) => k === other[i]);
+}
+
+function objectShallowEqual(value, other) {
+ const existingKeys = Object.keys(other);
+ const keys = Object.keys(value);
+
+ return (
+ keys.length === existingKeys.length &&
+ keys.every((k, i) => k === existingKeys[i]) &&
+ keys.every(k => value[k] === other[k])
+ );
+}
+
+function isObject(value) {
+ return typeof value === "object" && !!value;
+}
diff --git a/devtools/client/debugger/src/utils/source-maps.js b/devtools/client/debugger/src/utils/source-maps.js
new file mode 100644
index 0000000000..7a77639522
--- /dev/null
+++ b/devtools/client/debugger/src/utils/source-maps.js
@@ -0,0 +1,122 @@
+/* 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 {
+ debuggerToSourceMapLocation,
+ sourceMapToDebuggerLocation,
+} from "./location";
+import { waitForSourceToBeRegisteredInStore } from "../client/firefox/create";
+
+/**
+ * For any location, return the matching generated location.
+ * If this is already a generated location, returns the same location.
+ *
+ * In additional to `SourceMapLoader.getGeneratedLocation`,
+ * this asserts that the related source is still registered in the reducer current state.
+ *
+ * @param {Object} location
+ * @param {Object} thunkArgs
+ * Redux action thunk arguments
+ * @param {Object}
+ * The matching generated location.
+ */
+export async function getGeneratedLocation(location, thunkArgs) {
+ if (!location.source.isOriginal) {
+ return location;
+ }
+
+ const { sourceMapLoader, getState } = thunkArgs;
+ const generatedLocation = await sourceMapLoader.getGeneratedLocation(
+ debuggerToSourceMapLocation(location)
+ );
+ if (!generatedLocation) {
+ return location;
+ }
+
+ return sourceMapToDebuggerLocation(getState(), generatedLocation);
+}
+
+/**
+ * For any location, return the matching original location.
+ * If this is already an original location, returns the same location.
+ *
+ * In additional to `SourceMapLoader.getOriginalLocation`,
+ * this automatically fetches the original source object in order to build
+ * the original location object.
+ *
+ * @param {Object} location
+ * @param {Object} thunkArgs
+ * Redux action thunk arguments
+ * @param {Object} options
+ * @param {boolean} options.waitForSource
+ * Default to false. If true is passed, this function will
+ * ensure waiting, possibly asynchronously for the related original source
+ * to be registered in the redux store.
+ * @param {boolean} options.looseSearch
+ * Default to false. If true, this won't query an exact mapping,
+ * but will also lookup for a loose match at the first column and next lines.
+ *
+ * @param {Object}
+ * The matching original location.
+ */
+export async function getOriginalLocation(
+ location,
+ thunkArgs,
+ { waitForSource = false, looseSearch = false } = {}
+) {
+ if (location.source.isOriginal) {
+ return location;
+ }
+ const { getState, sourceMapLoader } = thunkArgs;
+ const originalLocation = await sourceMapLoader.getOriginalLocation(
+ debuggerToSourceMapLocation(location),
+ { looseSearch }
+ );
+ if (!originalLocation) {
+ return location;
+ }
+
+ // When we are mapping frames while being paused,
+ // the original source may not be registered yet in the reducer.
+ if (waitForSource) {
+ await waitForSourceToBeRegisteredInStore(originalLocation.sourceId);
+ }
+
+ return sourceMapToDebuggerLocation(getState(), originalLocation);
+}
+
+export async function getMappedLocation(location, thunkArgs) {
+ if (location.source.isOriginal) {
+ const generatedLocation = await getGeneratedLocation(location, thunkArgs);
+ return { location, generatedLocation };
+ }
+
+ const generatedLocation = location;
+ const originalLocation = await getOriginalLocation(
+ generatedLocation,
+ thunkArgs
+ );
+
+ return { location: originalLocation, generatedLocation };
+}
+
+/**
+ * Gets the "mapped location".
+ *
+ * If the passed location is on a generated source, it gets the
+ * related location in the original source.
+ * If the passed location is on an original source, it gets the
+ * related location in the generated source.
+ */
+export async function getRelatedMapLocation(location, thunkArgs) {
+ if (!location.source) {
+ return location;
+ }
+
+ if (location.source.isOriginal) {
+ return getGeneratedLocation(location, thunkArgs);
+ }
+
+ return getOriginalLocation(location, thunkArgs);
+}
diff --git a/devtools/client/debugger/src/utils/source-queue.js b/devtools/client/debugger/src/utils/source-queue.js
new file mode 100644
index 0000000000..3b11b0616d
--- /dev/null
+++ b/devtools/client/debugger/src/utils/source-queue.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/>. */
+
+const { throttle } = require("resource://devtools/shared/throttle.js");
+
+// This SourceQueue module is now only used for source mapped sources
+let newOriginalQueuedSources;
+let queuedOriginalSources;
+let currentWork;
+
+async function dispatchNewSources() {
+ const sources = queuedOriginalSources;
+ if (!sources.length) {
+ return;
+ }
+ queuedOriginalSources = [];
+ currentWork = await newOriginalQueuedSources(sources);
+}
+
+const queue = throttle(dispatchNewSources, 100);
+
+export default {
+ initialize: actions => {
+ newOriginalQueuedSources = actions.newOriginalSources;
+ queuedOriginalSources = [];
+ },
+ queueOriginalSources: sources => {
+ if (sources.length) {
+ queuedOriginalSources.push(...sources);
+ queue();
+ }
+ },
+
+ flush: () => Promise.all([queue.flush(), currentWork]),
+ clear: () => {
+ queuedOriginalSources = [];
+ queue.cancel();
+ },
+};
diff --git a/devtools/client/debugger/src/utils/source.js b/devtools/client/debugger/src/utils/source.js
new file mode 100644
index 0000000000..31920453eb
--- /dev/null
+++ b/devtools/client/debugger/src/utils/source.js
@@ -0,0 +1,534 @@
+/* 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/>. */
+
+/**
+ * Utils for working with Source URLs
+ * @module utils/source
+ */
+
+const {
+ getUnicodeUrl,
+} = require("resource://devtools/client/shared/unicode-url.js");
+const {
+ micromatch,
+} = require("resource://devtools/client/shared/vendor/micromatch/micromatch.js");
+
+import { getRelativePath } from "../utils/sources-tree/utils";
+import { endTruncateStr } from "./utils";
+import { truncateMiddleText } from "../utils/text";
+import { parse as parseURL } from "../utils/url";
+import { memoizeLast } from "../utils/memoizeLast";
+import { renderWasmText } from "./wasm";
+import { toEditorLine } from "./editor/index";
+export { isMinified } from "./isMinified";
+
+import { isFulfilled } from "./async-value";
+
+export const sourceTypes = {
+ coffee: "coffeescript",
+ js: "javascript",
+ jsx: "react",
+ ts: "typescript",
+ tsx: "typescript",
+ vue: "vue",
+};
+
+export const javascriptLikeExtensions = new Set(["marko", "es6", "vue", "jsm"]);
+
+function getPath(source) {
+ const { path } = source.displayURL;
+ let lastIndex = path.lastIndexOf("/");
+ let nextToLastIndex = path.lastIndexOf("/", lastIndex - 1);
+
+ const result = [];
+ do {
+ result.push(path.slice(nextToLastIndex + 1, lastIndex));
+ lastIndex = nextToLastIndex;
+ nextToLastIndex = path.lastIndexOf("/", lastIndex - 1);
+ } while (lastIndex !== nextToLastIndex);
+
+ result.push("");
+
+ return result;
+}
+
+export function shouldBlackbox(source) {
+ if (!source) {
+ return false;
+ }
+
+ if (!source.url) {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Checks if the frame is within a line ranges which are blackboxed
+ * in the source.
+ *
+ * @param {Object} frame
+ * The current frame
+ * @param {Object} blackboxedRanges
+ * The currently blackboxedRanges for all the sources.
+ * @param {Boolean} isFrameBlackBoxed
+ * If the frame is within the blackboxed range
+ * or not.
+ */
+export function isFrameBlackBoxed(frame, blackboxedRanges) {
+ const { source } = frame.location;
+ return (
+ !!blackboxedRanges[source.url] &&
+ (!blackboxedRanges[source.url].length ||
+ !!findBlackBoxRange(source, blackboxedRanges, {
+ start: frame.location.line,
+ end: frame.location.line,
+ }))
+ );
+}
+
+/**
+ * Checks if a blackbox range exist for the line range.
+ * That is if any start and end lines overlap any of the
+ * blackbox ranges
+ *
+ * @param {Object} source
+ * The current selected source
+ * @param {Object} blackboxedRanges
+ * The store of blackboxedRanges
+ * @param {Object} lineRange
+ * The start/end line range `{ start: <Number>, end: <Number> }`
+ * @return {Object} blackboxRange
+ * The first matching blackbox range that all or part of the
+ * specified lineRange sits within.
+ */
+export function findBlackBoxRange(source, blackboxedRanges, lineRange) {
+ const ranges = blackboxedRanges[source.url];
+ if (!ranges || !ranges.length) {
+ return null;
+ }
+
+ return ranges.find(
+ range =>
+ (lineRange.start >= range.start.line &&
+ lineRange.start <= range.end.line) ||
+ (lineRange.end >= range.start.line && lineRange.end <= range.end.line)
+ );
+}
+
+/**
+ * Checks if a source line is blackboxed
+ * @param {Array} ranges - Line ranges that are blackboxed
+ * @param {Number} line
+ * @param {Boolean} isSourceOnIgnoreList - is the line in a source that is on
+ * the sourcemap ignore lists then the line is blackboxed.
+ * @returns boolean
+ */
+export function isLineBlackboxed(ranges, line, isSourceOnIgnoreList) {
+ if (isSourceOnIgnoreList) {
+ return true;
+ }
+
+ if (!ranges) {
+ return false;
+ }
+ // If the whole source is ignored , then the line is
+ // ignored.
+ if (!ranges.length) {
+ return true;
+ }
+ return !!ranges.find(
+ range => line >= range.start.line && line <= range.end.line
+ );
+}
+
+/**
+ * Returns true if the specified url and/or content type are specific to
+ * javascript files.
+ *
+ * @return boolean
+ * True if the source is likely javascript.
+ *
+ * @memberof utils/source
+ * @static
+ */
+export function isJavaScript(source, content) {
+ const extension = source.displayURL.fileExtension;
+ const contentType = content.type === "wasm" ? null : content.contentType;
+ return (
+ javascriptLikeExtensions.has(extension) ||
+ !!(contentType && contentType.includes("javascript"))
+ );
+}
+
+/**
+ * @memberof utils/source
+ * @static
+ */
+export function isPretty(source) {
+ return isPrettyURL(source.url);
+}
+
+export function isPrettyURL(url) {
+ return url ? url.endsWith(":formatted") : false;
+}
+
+/**
+ * @memberof utils/source
+ * @static
+ */
+export function getPrettySourceURL(url) {
+ if (!url) {
+ url = "";
+ }
+ return `${url}:formatted`;
+}
+
+/**
+ * @memberof utils/source
+ * @static
+ */
+export function getRawSourceURL(url) {
+ return url && url.endsWith(":formatted")
+ ? url.slice(0, -":formatted".length)
+ : url;
+}
+
+function resolveFileURL(
+ url,
+ transformUrl = initialUrl => initialUrl,
+ truncate = true
+) {
+ url = getRawSourceURL(url || "");
+ const name = transformUrl(url);
+ if (!truncate) {
+ return name;
+ }
+ return endTruncateStr(name, 50);
+}
+
+export function getFormattedSourceId(id) {
+ return id.substring(id.lastIndexOf("/") + 1);
+}
+
+/**
+ * Gets a readable filename from a source URL for display purposes.
+ * If the source does not have a URL, the source ID will be returned instead.
+ *
+ * @memberof utils/source
+ * @static
+ */
+export function getFilename(
+ source,
+ rawSourceURL = getRawSourceURL(source.url)
+) {
+ const { id } = source;
+ if (!rawSourceURL) {
+ return getFormattedSourceId(id);
+ }
+
+ const { filename } = source.displayURL;
+ return getRawSourceURL(filename);
+}
+
+/**
+ * Provides a middle-trunated filename
+ *
+ * @memberof utils/source
+ * @static
+ */
+export function getTruncatedFileName(source, querystring = "", length = 30) {
+ return truncateMiddleText(`${getFilename(source)}${querystring}`, length);
+}
+
+/* Gets path for files with same filename for editor tabs, breakpoints, etc.
+ * Pass the source, and list of other sources
+ *
+ * @memberof utils/source
+ * @static
+ */
+
+export function getDisplayPath(mySource, sources) {
+ const rawSourceURL = getRawSourceURL(mySource.url);
+ const filename = getFilename(mySource, rawSourceURL);
+
+ // Find sources that have the same filename, but different paths
+ // as the original source
+ const similarSources = sources.filter(source => {
+ const rawSource = getRawSourceURL(source.url);
+ return (
+ rawSourceURL != rawSource && filename == getFilename(source, rawSource)
+ );
+ });
+
+ if (!similarSources.length) {
+ return undefined;
+ }
+
+ // get an array of source path directories e.g. ['a/b/c.html'] => [['b', 'a']]
+ const paths = new Array(similarSources.length + 1);
+
+ paths[0] = getPath(mySource);
+ for (let i = 0; i < similarSources.length; ++i) {
+ paths[i + 1] = getPath(similarSources[i]);
+ }
+
+ // create an array of similar path directories and one dis-similar directory
+ // for example [`a/b/c.html`, `a1/b/c.html`] => ['b', 'a']
+ // where 'b' is the similar directory and 'a' is the dis-similar directory.
+ let displayPath = "";
+ for (let i = 0; i < paths[0].length; i++) {
+ let similar = false;
+ for (let k = 1; k < paths.length; ++k) {
+ if (paths[k][i] === paths[0][i]) {
+ similar = true;
+ break;
+ }
+ }
+
+ displayPath = paths[0][i] + (i !== 0 ? "/" : "") + displayPath;
+
+ if (!similar) {
+ break;
+ }
+ }
+
+ return displayPath;
+}
+
+/**
+ * Gets a readable source URL for display purposes.
+ * If the source does not have a URL, the source ID will be returned instead.
+ *
+ * @memberof utils/source
+ * @static
+ */
+export function getFileURL(source, truncate = true) {
+ const { url, id } = source;
+ if (!url) {
+ return getFormattedSourceId(id);
+ }
+
+ return resolveFileURL(url, getUnicodeUrl, truncate);
+}
+
+export function getSourcePath(url) {
+ if (!url) {
+ return "";
+ }
+
+ const { path, href } = parseURL(url);
+ // for URLs like "about:home" the path is null so we pass the full href
+ return path || href;
+}
+
+/**
+ * Returns amount of lines in the source. If source is a WebAssembly binary,
+ * the function returns amount of bytes.
+ */
+export function getSourceLineCount(content) {
+ if (content.type === "wasm") {
+ const { binary } = content.value;
+ return binary.length;
+ }
+
+ let count = 0;
+
+ for (let i = 0; i < content.value.length; ++i) {
+ if (content.value[i] === "\n") {
+ ++count;
+ }
+ }
+
+ return count + 1;
+}
+
+export function isInlineScript(source) {
+ return source.introductionType === "scriptElement";
+}
+
+function getNthLine(str, lineNum) {
+ let startIndex = -1;
+
+ let newLinesFound = 0;
+ while (newLinesFound < lineNum) {
+ const nextIndex = str.indexOf("\n", startIndex + 1);
+ if (nextIndex === -1) {
+ return null;
+ }
+ startIndex = nextIndex;
+ newLinesFound++;
+ }
+ const endIndex = str.indexOf("\n", startIndex + 1);
+ if (endIndex === -1) {
+ return str.slice(startIndex + 1);
+ }
+
+ return str.slice(startIndex + 1, endIndex);
+}
+
+export const getLineText = memoizeLast((sourceId, asyncContent, line) => {
+ if (!asyncContent || !isFulfilled(asyncContent)) {
+ return "";
+ }
+
+ const content = asyncContent.value;
+
+ if (content.type === "wasm") {
+ const editorLine = toEditorLine(sourceId, line);
+ const lines = renderWasmText(sourceId, content);
+ return lines[editorLine] || "";
+ }
+
+ const lineText = getNthLine(content.value, line - 1);
+ return lineText || "";
+});
+
+export function getTextAtPosition(sourceId, asyncContent, location) {
+ const { column, line = 0 } = location;
+
+ const lineText = getLineText(sourceId, asyncContent, line);
+ return lineText.slice(column, column + 100).trim();
+}
+
+/**
+ * Compute the CSS classname string to use for the icon of a given source.
+ *
+ * @param {Object} source
+ * The reducer source object.
+ * @param {Object} symbols
+ * The reducer symbol object for the given source.
+ * @param {Boolean} isBlackBoxed
+ * To be set to true, when the given source is blackboxed.
+ * @param {Boolean} hasPrettyTab
+ * To be set to true, if the given source isn't the pretty printed one,
+ * but another tab for that source is opened pretty printed.
+ * @return String
+ * The classname to use.
+ */
+export function getSourceClassnames(
+ source,
+ symbols,
+ isBlackBoxed,
+ hasPrettyTab = false
+) {
+ // Conditionals should be ordered by priority of icon!
+ const defaultClassName = "file";
+
+ if (!source || !source.url) {
+ return defaultClassName;
+ }
+
+ // In the SourceTree, we don't show the pretty printed sources,
+ // but still want to show the pretty print icon when a pretty printed tab
+ // for the current source is opened.
+ if (isPretty(source) || hasPrettyTab) {
+ return "prettyPrint";
+ }
+
+ if (isBlackBoxed) {
+ return "blackBox";
+ }
+
+ if (symbols && symbols.framework) {
+ return symbols.framework.toLowerCase();
+ }
+
+ if (isUrlExtension(source.url)) {
+ return "extension";
+ }
+
+ return sourceTypes[source.displayURL.fileExtension] || defaultClassName;
+}
+
+export function getRelativeUrl(source, root) {
+ const { group, path } = source.displayURL;
+ if (!root) {
+ return path;
+ }
+
+ // + 1 removes the leading "/"
+ const url = group + path;
+ return url.slice(url.indexOf(root) + root.length + 1);
+}
+
+/**
+ * source.url doesn't include thread actor ID, so before calling underRoot(), the thread actor ID
+ * must be removed from the root, which this function handles.
+ * @param {string} root The root url to be cleaned
+ * @param {Set<Thread>} threads The list of threads
+ * @returns {string} The root url with thread actor IDs removed
+ */
+export function removeThreadActorId(root, threads) {
+ threads.forEach(thread => {
+ if (root.includes(thread.actor)) {
+ root = root.slice(thread.actor.length + 1);
+ }
+ });
+ return root;
+}
+
+/**
+ * Checks if the source is descendant of the root identified by the
+ * root url specified. The root might likely be projectDirectoryRoot which
+ * is a defined by a pref that allows users restrict the source tree to
+ * a subset of sources.
+ *
+ * @param {Object} source
+ * The source object
+ * @param {String} rootUrlWithoutThreadActor
+ * The url for the root node, without the thread actor ID. This can be obtained
+ * by calling removeThreadActorId()
+ */
+export function isDescendantOfRoot(source, rootUrlWithoutThreadActor) {
+ if (source.url && source.url.includes("chrome://")) {
+ const { group, path } = source.displayURL;
+ return (group + path).includes(rootUrlWithoutThreadActor);
+ }
+
+ return !!source.url && source.url.includes(rootUrlWithoutThreadActor);
+}
+
+export function getSourceQueryString(source) {
+ if (!source) {
+ return "";
+ }
+
+ return parseURL(getRawSourceURL(source.url)).search;
+}
+
+export function isUrlExtension(url) {
+ return url.includes("moz-extension:") || url.includes("chrome-extension");
+}
+
+/**
+* Checks that source url matches one of the glob patterns
+*
+* @param {Object} source
+* @param {String} excludePatterns
+ String of comma-seperated glob patterns
+* @return {return} Boolean value specifies if the string matches any
+ of the patterns.
+*/
+export function matchesGlobPatterns(source, excludePatterns) {
+ if (!excludePatterns) {
+ return false;
+ }
+ const patterns = excludePatterns
+ .split(",")
+ .map(pattern => pattern.trim())
+ .filter(pattern => pattern !== "");
+
+ if (!patterns.length) {
+ return false;
+ }
+
+ return micromatch.contains(
+ // Makes sure we format the url or id exactly the way its displayed in the search ui,
+ // as user wil usually create glob patterns based on what is seen in the ui.
+ source.url ? getRelativePath(source.url) : getFormattedSourceId(source.id),
+ patterns
+ );
+}
diff --git a/devtools/client/debugger/src/utils/sources-tree/getURL.js b/devtools/client/debugger/src/utils/sources-tree/getURL.js
new file mode 100644
index 0000000000..c01fce5f23
--- /dev/null
+++ b/devtools/client/debugger/src/utils/sources-tree/getURL.js
@@ -0,0 +1,180 @@
+/* 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 { parse } from "../url";
+
+const {
+ getUnicodeHostname,
+ getUnicodeUrlPath,
+} = require("resource://devtools/client/shared/unicode-url.js");
+
+export function getFilenameFromPath(pathname) {
+ let filename = "";
+ if (pathname) {
+ filename = pathname.substring(pathname.lastIndexOf("/") + 1);
+ // This file does not have a name. Default should be (index).
+ if (filename == "") {
+ filename = "(index)";
+ } else if (filename == ":formatted") {
+ filename = "(index:formatted)";
+ }
+ }
+ return filename;
+}
+
+function getFileExtension(path) {
+ if (!path) {
+ return "";
+ }
+
+ const lastIndex = path.lastIndexOf(".");
+ return lastIndex !== -1 ? path.slice(lastIndex + 1).toLowerCase() : "";
+}
+
+const NoDomain = "(no domain)";
+const def = {
+ path: "",
+ search: "",
+ group: "",
+ filename: "",
+ fileExtension: "",
+};
+
+/**
+ * Compute the URL which may be displayed in the Source Tree.
+ *
+ * @param {String} url
+ * The source absolute URL as a string
+ * @param {String} extensionName
+ * Optional, but mandatory when passing a moz-extension URL.
+ * Name of the extension serving this moz-extension source.
+ * @return URL Object
+ * A URL object to represent this source.
+ *
+ * Note that this isn't the standard URL object.
+ * This is augmented with custom properties like:
+ * - `group`, which is mostly the host of the source's URL.
+ * This is used to sort sources in the Source tree.
+ * - `fileExtension`, lowercased file extension of the source
+ * (if any extension is available)
+ * - `path` and `pathname` have some special behavior.
+ * See `parse` implementation.
+ */
+export function getDisplayURL(url, extensionName = null) {
+ if (!url) {
+ return def;
+ }
+
+ const { pathname, search, protocol, host } = parse(url);
+ const filename = getUnicodeUrlPath(getFilenameFromPath(pathname));
+
+ switch (protocol) {
+ case "javascript:":
+ // Ignore `javascript:` URLs for now
+ return def;
+
+ case "moz-extension:":
+ return {
+ ...def,
+ path: pathname,
+ search,
+ filename,
+ fileExtension: getFileExtension(pathname),
+ // For moz-extension, we replace the uuid by the extension name
+ // that we receive from the SourceActor.extensionName attribute.
+ // `extensionName` might be null for content script of disabled add-ons.
+ group: extensionName || `${protocol}//${host}`,
+ };
+ case "resource:":
+ return {
+ ...def,
+ path: pathname,
+ search,
+ filename,
+ fileExtension: getFileExtension(pathname),
+ group: `${protocol}//${host || ""}`,
+ };
+ case "webpack:":
+ return {
+ ...def,
+ path: pathname,
+ search,
+ filename,
+ fileExtension: getFileExtension(pathname),
+ group: `Webpack`,
+ };
+ case "ng:":
+ return {
+ ...def,
+ path: pathname,
+ search,
+ filename,
+ fileExtension: getFileExtension(pathname),
+ group: `Angular`,
+ };
+ case "about:":
+ // An about page is a special case
+ return {
+ ...def,
+ path: "/",
+ search,
+ filename,
+ fileExtension: getFileExtension("/"),
+ group: url,
+ };
+
+ case "data:":
+ return {
+ ...def,
+ path: "/",
+ search,
+ filename: url,
+ fileExtension: getFileExtension("/"),
+ group: NoDomain,
+ };
+
+ case "":
+ if (pathname && pathname.startsWith("/")) {
+ // use file protocol for a URL like "/foo/bar.js"
+ return {
+ ...def,
+ path: pathname,
+ search,
+ filename,
+ fileExtension: getFileExtension(pathname),
+ group: "file://",
+ };
+ } else if (!host) {
+ return {
+ ...def,
+ path: pathname,
+ search,
+ filename,
+ fileExtension: getFileExtension(pathname),
+ group: "",
+ };
+ }
+ break;
+
+ case "http:":
+ case "https:":
+ return {
+ ...def,
+ path: pathname,
+ search,
+ filename,
+ fileExtension: getFileExtension(pathname),
+ group: getUnicodeHostname(host),
+ };
+ }
+
+ return {
+ ...def,
+ path: pathname,
+ search,
+ fileExtension: getFileExtension(pathname),
+ filename,
+ group: protocol ? `${protocol}//` : "",
+ };
+}
diff --git a/devtools/client/debugger/src/utils/sources-tree/moz.build b/devtools/client/debugger/src/utils/sources-tree/moz.build
new file mode 100644
index 0000000000..400c0f0d1a
--- /dev/null
+++ b/devtools/client/debugger/src/utils/sources-tree/moz.build
@@ -0,0 +1,9 @@
+# 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/.
+
+CompiledModules(
+ "getURL.js",
+ "utils.js",
+)
diff --git a/devtools/client/debugger/src/utils/sources-tree/tests/getUrl.spec.js b/devtools/client/debugger/src/utils/sources-tree/tests/getUrl.spec.js
new file mode 100644
index 0000000000..51919ffc4e
--- /dev/null
+++ b/devtools/client/debugger/src/utils/sources-tree/tests/getUrl.spec.js
@@ -0,0 +1,50 @@
+/* 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 { getDisplayURL } from "../getURL";
+
+describe("getUrl", () => {
+ it("handles normal url with http and https for filename", function () {
+ const urlObject = getDisplayURL("https://a/b.js");
+ expect(urlObject.filename).toBe("b.js");
+
+ const urlObject2 = getDisplayURL("http://a/b.js");
+ expect(urlObject2.filename).toBe("b.js");
+ });
+
+ it("handles url with querystring for filename", function () {
+ const urlObject = getDisplayURL("https://a/b.js?key=randomKey");
+ expect(urlObject.filename).toBe("b.js");
+ });
+
+ it("handles url with '#' for filename", function () {
+ const urlObject = getDisplayURL("https://a/b.js#specialSection");
+ expect(urlObject.filename).toBe("b.js");
+ });
+
+ it("handles url with no file extension for filename", function () {
+ const urlObject = getDisplayURL("https://a/c");
+ expect(urlObject.filename).toBe("c");
+ });
+
+ it("handles url with no name for filename", function () {
+ const urlObject = getDisplayURL("https://a/");
+ expect(urlObject.filename).toBe("(index)");
+ });
+
+ it("separates resources by protocol and host", () => {
+ const urlObject = getDisplayURL("moz-extension://xyz/123");
+ expect(urlObject.group).toBe("moz-extension://xyz");
+ });
+
+ it("creates a group name for webpack", () => {
+ const urlObject = getDisplayURL("webpack:///src/component.jsx");
+ expect(urlObject.group).toBe("Webpack");
+ });
+
+ it("creates a group name for angular source", () => {
+ const urlObject = getDisplayURL("ng://src/component.jsx");
+ expect(urlObject.group).toBe("Angular");
+ });
+});
diff --git a/devtools/client/debugger/src/utils/sources-tree/utils.js b/devtools/client/debugger/src/utils/sources-tree/utils.js
new file mode 100644
index 0000000000..0a2f41752b
--- /dev/null
+++ b/devtools/client/debugger/src/utils/sources-tree/utils.js
@@ -0,0 +1,44 @@
+/* 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 { parse } from "../../utils/url";
+
+/**
+ * Get the relative path of the url
+ * Does not include any query parameters or fragment parts
+ *
+ * @param string url
+ * @returns string path
+ */
+export function getRelativePath(url) {
+ const { pathname } = parse(url);
+ if (!pathname) {
+ return url;
+ }
+ const index = pathname.indexOf("/");
+ if (index !== -1) {
+ const path = pathname.slice(index + 1);
+ // If the path is empty this is likely the index file.
+ // e.g http://foo.com/
+ if (path == "") {
+ return "(index)";
+ }
+ return path;
+ }
+ return "";
+}
+
+/**
+ *
+ * @param {String} name: Name (e.g. computed in SourcesTreeItem renderItemName),
+ * which might include URI search.
+ * @returns {String} result of `decodedURI(name)`, or name if it `name` is malformed.
+ */
+export function safeDecodeItemName(name) {
+ try {
+ return decodeURI(name);
+ } catch (e) {
+ return name;
+ }
+}
diff --git a/devtools/client/debugger/src/utils/tabs.js b/devtools/client/debugger/src/utils/tabs.js
new file mode 100644
index 0000000000..5f16b6ce63
--- /dev/null
+++ b/devtools/client/debugger/src/utils/tabs.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/>. */
+
+/*
+ * Finds the hidden tabs by comparing the tabs' top offset.
+ * hidden tabs will have a great top offset.
+ *
+ * @param sourceTabs Array
+ * @param sourceTabEls HTMLCollection
+ *
+ * @returns Array
+ */
+
+export function getHiddenTabs(sourceTabs, sourceTabEls) {
+ sourceTabEls = [].slice.call(sourceTabEls);
+ function getTopOffset() {
+ const topOffsets = sourceTabEls.map(t => t.getBoundingClientRect().top);
+ return Math.min(...topOffsets);
+ }
+
+ function hasTopOffset(el) {
+ // adding 10px helps account for cases where the tab might be offset by
+ // styling such as selected tabs which don't have a border.
+ const tabTopOffset = getTopOffset();
+ return el.getBoundingClientRect().top > tabTopOffset + 10;
+ }
+
+ return sourceTabs.filter((tab, index) => {
+ const element = sourceTabEls[index];
+ return element && hasTopOffset(element);
+ });
+}
+
+export function getTabMenuItems() {
+ return {
+ closeTab: {
+ id: "node-menu-close-tab",
+ label: L10N.getStr("sourceTabs.closeTab"),
+ accesskey: L10N.getStr("sourceTabs.closeTab.accesskey"),
+ disabled: false,
+ },
+ closeOtherTabs: {
+ id: "node-menu-close-other-tabs",
+ label: L10N.getStr("sourceTabs.closeOtherTabs"),
+ accesskey: L10N.getStr("sourceTabs.closeOtherTabs.accesskey"),
+ disabled: false,
+ },
+ closeTabsToEnd: {
+ id: "node-menu-close-tabs-to-end",
+ label: L10N.getStr("sourceTabs.closeTabsToEnd"),
+ accesskey: L10N.getStr("sourceTabs.closeTabsToEnd.accesskey"),
+ disabled: false,
+ },
+ closeAllTabs: {
+ id: "node-menu-close-all-tabs",
+ label: L10N.getStr("sourceTabs.closeAllTabs"),
+ accesskey: L10N.getStr("sourceTabs.closeAllTabs.accesskey"),
+ disabled: false,
+ },
+ showSource: {
+ id: "node-menu-show-source",
+ label: L10N.getStr("sourceTabs.revealInTree"),
+ accesskey: L10N.getStr("sourceTabs.revealInTree.accesskey"),
+ disabled: false,
+ },
+ copySource: {
+ id: "node-menu-copy-source",
+ label: L10N.getStr("copySource.label"),
+ accesskey: L10N.getStr("copySource.accesskey"),
+ disabled: false,
+ },
+ copySourceUri2: {
+ id: "node-menu-copy-source-url",
+ label: L10N.getStr("copySourceUri2"),
+ accesskey: L10N.getStr("copySourceUri2.accesskey"),
+ disabled: false,
+ },
+ toggleBlackBox: {
+ id: "node-menu-blackbox",
+ label: L10N.getStr("ignoreContextItem.ignore"),
+ accesskey: L10N.getStr("ignoreContextItem.ignore.accesskey"),
+ disabled: false,
+ },
+ prettyPrint: {
+ id: "node-menu-pretty-print",
+ label: L10N.getStr("sourceTabs.prettyPrint"),
+ accesskey: L10N.getStr("sourceTabs.prettyPrint.accesskey"),
+ disabled: false,
+ },
+ };
+}
+
+/**
+ * Determines if a tab exists with the following properties
+ *
+ * @param {Object} tab
+ * @param {String} url
+ * @param {Boolean} isOriginal
+ */
+export function isSimilarTab(tab, url, isOriginal) {
+ return tab.url === url && tab.isOriginal === isOriginal;
+}
+
+/**
+ * This cleans up some tab info (source id and thread info),
+ * mostly for persiting to pref and for navigation or reload.
+ * This is neccesary because the source and thread are destroyed
+ * and re-created across navigations / reloads.
+ *
+ * @param {Array} tabs
+ */
+export function persistTabs(tabs) {
+ return [...tabs]
+ .filter(tab => tab.url)
+ .map(tab => ({
+ ...tab,
+ source: null,
+ sourceActor: null,
+ }));
+}
diff --git a/devtools/client/debugger/src/utils/task.js b/devtools/client/debugger/src/utils/task.js
new file mode 100644
index 0000000000..25663fdd16
--- /dev/null
+++ b/devtools/client/debugger/src/utils/task.js
@@ -0,0 +1,44 @@
+/* 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/>. */
+
+/**
+ * This object provides the public module functions.
+ */
+export const Task = {
+ // XXX: Not sure if this works in all cases...
+ async: function (task) {
+ return function () {
+ return Task.spawn(task, this, arguments);
+ };
+ },
+
+ /**
+ * Creates and starts a new task.
+ * @param task A generator function
+ * @return A promise, resolved when the task terminates
+ */
+ spawn: function (task, scope, args) {
+ return new Promise(function (resolve, reject) {
+ const iterator = task.apply(scope, args);
+
+ const callNext = lastValue => {
+ const iteration = iterator.next(lastValue);
+ Promise.resolve(iteration.value)
+ .then(value => {
+ if (iteration.done) {
+ resolve(value);
+ } else {
+ callNext(value);
+ }
+ })
+ .catch(error => {
+ reject(error);
+ iterator.throw(error);
+ });
+ };
+
+ callNext(undefined);
+ });
+ },
+};
diff --git a/devtools/client/debugger/src/utils/telemetry.js b/devtools/client/debugger/src/utils/telemetry.js
new file mode 100644
index 0000000000..0e0c8766bb
--- /dev/null
+++ b/devtools/client/debugger/src/utils/telemetry.js
@@ -0,0 +1,72 @@
+/* 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/>. */
+
+/**
+ * Usage:
+ *
+ * import { recordEvent } from "src/utils/telemetry";
+ *
+ * // Event without extra properties
+ * recordEvent("add_breakpoint");
+ *
+ * // Event with extra properties
+ * recordEvent("pause", {
+ * "reason": "debugger-statement",
+ * "collapsed_callstacks": 1
+ * });
+ *
+ * // If the properties are in multiple code paths and you can't send them all
+ * // in one go you will need to use the full telemetry API.
+ *
+ * const Telemetry = require("devtools/client/shared/telemetry");
+ *
+ * const telemetry = new Telemetry();
+ *
+ * // Prepare the event and define which properties to expect.
+ * //
+ * // NOTE: You CAN send properties before preparing the event.
+ * //
+ * telemetry.preparePendingEvent(this, "pause", "debugger", null, [
+ * "reason", "collapsed_callstacks"
+ * ]);
+ *
+ * // Elsewhere in another codepath send the reason property
+ * telemetry.addEventProperty(
+ * this, "pause", "debugger", null, "reason", "debugger-statement"
+ * );
+ *
+ * // Elsewhere in another codepath send the collapsed_callstacks property
+ * telemetry.addEventProperty(
+ * this, "pause", "debugger", null, "collapsed_callstacks", 1
+ * );
+ */
+
+import { isNode } from "./environment";
+
+let telemetry;
+
+if (isNode()) {
+ const Telemetry = require("resource://devtools/client/shared/telemetry.js");
+ telemetry = new Telemetry();
+}
+
+export function setToolboxTelemetry(toolboxTelemetry) {
+ telemetry = toolboxTelemetry;
+}
+
+/**
+ * @memberof utils/telemetry
+ * @static
+ */
+export function recordEvent(eventName, fields = {}) {
+ telemetry.recordEvent(eventName, "debugger", null, fields);
+
+ if (isNode()) {
+ const { events } = window.dbg._telemetry;
+ if (!events[eventName]) {
+ events[eventName] = [];
+ }
+ events[eventName].push(fields);
+ }
+}
diff --git a/devtools/client/debugger/src/utils/test-head.js b/devtools/client/debugger/src/utils/test-head.js
new file mode 100644
index 0000000000..c21f408b61
--- /dev/null
+++ b/devtools/client/debugger/src/utils/test-head.js
@@ -0,0 +1,283 @@
+/* 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/>. */
+
+/**
+ * Utils for Jest
+ * @module utils/test-head
+ */
+
+import { combineReducers } from "devtools/client/shared/vendor/redux";
+import reducers from "../reducers/index";
+import actions from "../actions/index";
+import * as selectors from "../selectors/index";
+import {
+ searchWorker,
+ prettyPrintWorker,
+ parserWorker,
+} from "../test/tests-setup";
+import configureStore from "../actions/utils/create-store";
+import sourceQueue from "../utils/source-queue";
+import { setupCreate } from "../client/firefox/create";
+import { createLocation } from "./location";
+
+// Import the internal module used by the source-map worker
+// as node doesn't have Web Worker support and require path mapping
+// doesn't work from nodejs worker thread and break mappings to devtools/ folder.
+import sourceMapLoader from "devtools/client/shared/source-map-loader/source-map";
+
+/**
+ * This file contains older interfaces used by tests that have not been
+ * converted to use test-mockup.js
+ */
+
+/**
+ * @memberof utils/test-head
+ * @static
+ */
+function createStore(client, initialState = {}, sourceMapLoaderMock) {
+ const store = configureStore({
+ log: false,
+ makeThunkArgs: args => {
+ return {
+ ...args,
+ client,
+ sourceMapLoader:
+ sourceMapLoaderMock !== undefined
+ ? sourceMapLoaderMock
+ : sourceMapLoader,
+ parserWorker,
+ prettyPrintWorker,
+ searchWorker,
+ };
+ },
+ })(combineReducers(reducers), initialState);
+ sourceQueue.clear();
+ sourceQueue.initialize({
+ newOriginalSources: sources =>
+ store.dispatch(actions.newOriginalSources(sources)),
+ });
+
+ store.thunkArgs = () => ({
+ dispatch: store.dispatch,
+ getState: store.getState,
+ client,
+ sourceMapLoader,
+ panel: {},
+ });
+
+ // Put the initial context in the store, for convenience to unit tests.
+ store.cx = selectors.getThreadContext(store.getState());
+
+ setupCreate({ store });
+
+ return store;
+}
+
+/**
+ * @memberof utils/test-head
+ * @static
+ */
+function commonLog(msg, data = {}) {
+ console.log(`[INFO] ${msg} ${JSON.stringify(data)}`);
+}
+
+function makeFrame({ id, sourceId, thread }, opts = {}) {
+ const source = createSourceObject(sourceId);
+ const sourceActor = {
+ id: `${sourceId}-actor`,
+ actor: `${sourceId}-actor`,
+ source: sourceId,
+ sourceObject: source,
+ };
+ const location = createLocation({ source, sourceActor, line: 4 });
+ return {
+ id,
+ scope: { bindings: { variables: {}, arguments: [] } },
+ location,
+ generatedLocation: location,
+ thread: thread || "FakeThread",
+ ...opts,
+ };
+}
+
+function createSourceObject(filename, props = {}) {
+ return {
+ id: filename,
+ url: makeSourceURL(filename),
+ isPrettyPrinted: false,
+ isExtension: false,
+ isOriginal: filename.includes("originalSource"),
+ displayURL: makeSourceURL(filename),
+ };
+}
+
+function makeSourceURL(filename) {
+ return `http://localhost:8000/examples/${filename}`;
+}
+
+function createMakeSource() {
+ const indicies = {};
+
+ return function (name, props = {}) {
+ const index = (indicies[name] | 0) + 1;
+ indicies[name] = index;
+
+ // Mock a SOURCE Resource, which happens to be the SourceActor's form
+ // with resourceType and targetFront additional attributes
+ return {
+ resourceType: "source",
+ // Mock the targetFront to support makeSourceId function
+ targetFront: {
+ isDestroyed() {
+ return false;
+ },
+ getCachedFront(typeName) {
+ return typeName == "thread" ? { actorID: "FakeThread" } : null;
+ },
+ },
+ // Allow to use custom ID's for reducer source objects
+ mockedJestID: name,
+ actor: `${name}-${index}-actor`,
+ url: `http://localhost:8000/examples/${name}`,
+ sourceMapBaseURL: props.sourceMapBaseURL || null,
+ sourceMapURL: props.sourceMapURL || null,
+ introductionType: props.introductionType || null,
+ extensionName: null,
+ };
+ };
+}
+
+/**
+ * @memberof utils/test-head
+ * @static
+ */
+let creator;
+beforeEach(() => {
+ creator = createMakeSource();
+});
+afterEach(() => {
+ creator = null;
+});
+function makeSource(name, props) {
+ if (!creator) {
+ throw new Error("makeSource() cannot be called outside of a test");
+ }
+
+ return creator(name, props);
+}
+
+function makeOriginalSource(source) {
+ return {
+ id: `${source.id}/originalSource`,
+ url: `${source.url}-original`,
+ sourceActor: {
+ id: `${source.id}-1-actor`,
+ thread: "FakeThread",
+ },
+ };
+}
+
+function makeFuncLocation(startLine, endLine) {
+ if (!endLine) {
+ endLine = startLine + 1;
+ }
+ return {
+ start: {
+ line: startLine,
+ },
+ end: {
+ line: endLine,
+ },
+ };
+}
+
+function makeSymbolDeclaration(name, start, end, klass) {
+ return {
+ id: `${name}:${start}`,
+ name,
+ location: makeFuncLocation(start, end),
+ klass,
+ };
+}
+
+/**
+ * @memberof utils/test-head
+ * @static
+ */
+function waitForState(store, predicate) {
+ return new Promise(resolve => {
+ let ret = predicate(store.getState());
+ if (ret) {
+ resolve(ret);
+ }
+
+ const unsubscribe = store.subscribe(() => {
+ ret = predicate(store.getState());
+ if (ret) {
+ unsubscribe();
+ // NOTE: memoizableAction adds an additional tick for validating context
+ setTimeout(() => resolve(ret));
+ }
+ });
+ });
+}
+
+function watchForState(store, predicate) {
+ let sawState = false;
+ const checkState = function () {
+ if (!sawState && predicate(store.getState())) {
+ sawState = true;
+ }
+ return sawState;
+ };
+
+ let unsubscribe;
+ if (!checkState()) {
+ unsubscribe = store.subscribe(() => {
+ if (checkState()) {
+ unsubscribe();
+ }
+ });
+ }
+
+ return function read() {
+ if (unsubscribe) {
+ unsubscribe();
+ }
+
+ return sawState;
+ };
+}
+
+function getTelemetryEvents(eventName) {
+ return window.dbg._telemetry.events[eventName] || [];
+}
+
+function waitATick(callback) {
+ return new Promise(resolve => {
+ setTimeout(() => {
+ callback();
+ resolve();
+ });
+ });
+}
+
+export {
+ actions,
+ selectors,
+ reducers,
+ createStore,
+ commonLog,
+ getTelemetryEvents,
+ makeFrame,
+ createSourceObject,
+ createMakeSource,
+ makeSourceURL,
+ makeSource,
+ makeOriginalSource,
+ makeSymbolDeclaration,
+ waitForState,
+ watchForState,
+ waitATick,
+};
diff --git a/devtools/client/debugger/src/utils/test-mockup.js b/devtools/client/debugger/src/utils/test-mockup.js
new file mode 100644
index 0000000000..521872b7cb
--- /dev/null
+++ b/devtools/client/debugger/src/utils/test-mockup.js
@@ -0,0 +1,268 @@
+/* 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/>. */
+
+/**
+ * This file is for use by unit tests for isolated debugger components that do
+ * not need to interact with the redux store. When these tests need to construct
+ * debugger objects, these interfaces should be used instead of plain object
+ * literals.
+ */
+
+import * as asyncValue from "./async-value";
+
+import { initialState } from "../reducers/index";
+
+import { getDisplayURL } from "./sources-tree/getURL";
+import { createLocation } from "./location";
+
+function makeMockSource(url = "url", id = "source", thread = "FakeThread") {
+ return {
+ id,
+ url,
+ displayURL: getDisplayURL(url),
+ thread,
+ isPrettyPrinted: false,
+ isWasm: false,
+ extensionName: null,
+ isExtension: false,
+ isOriginal: id.includes("originalSource"),
+ };
+}
+
+function makeMockDisplaySource(
+ url = "url",
+ id = "source",
+ thread = "FakeThread"
+) {
+ return makeMockSource(url, id, thread);
+}
+
+function makeMockSourceWithContent(
+ url,
+ id,
+ contentType = "text/javascript",
+ text = ""
+) {
+ const source = makeMockSource(url, id);
+
+ return {
+ ...source,
+ content: text
+ ? asyncValue.fulfilled({
+ type: "text",
+ value: text,
+ contentType,
+ })
+ : null,
+ };
+}
+
+function makeMockSourceAndContent(
+ url,
+ id,
+ contentType = "text/javascript",
+ text = ""
+) {
+ const source = makeMockSource(url, id);
+
+ return {
+ ...source,
+ content: {
+ type: "text",
+ value: text,
+ contentType,
+ },
+ };
+}
+
+function makeFullfilledMockSourceContent(
+ text = "",
+ contentType = "text/javascript"
+) {
+ return asyncValue.fulfilled({
+ type: "text",
+ value: text,
+ contentType,
+ });
+}
+
+function makeMockWasmSource() {
+ return {
+ id: "wasm-source-id",
+ url: "url",
+ displayURL: getDisplayURL("url"),
+ thread: "FakeThread",
+ isPrettyPrinted: false,
+ isWasm: true,
+ extensionName: null,
+ isExtension: false,
+ isOriginal: false,
+ };
+}
+
+function makeMockWasmSourceWithContent(text) {
+ const source = makeMockWasmSource();
+
+ return {
+ ...source,
+ content: asyncValue.fulfilled({
+ type: "wasm",
+ value: text,
+ }),
+ };
+}
+
+function makeMockScope(actor = "scope-actor", type = "block", parent = null) {
+ return {
+ actor,
+ parent,
+ bindings: {
+ arguments: [],
+ variables: {},
+ },
+ object: null,
+ function: null,
+ type,
+ scopeKind: "",
+ };
+}
+
+function mockScopeAddVariable(scope, name) {
+ if (!scope.bindings) {
+ throw new Error("no scope bindings");
+ }
+ scope.bindings.variables[name] = { value: null };
+}
+
+function makeMockBreakpoint(source = makeMockSource(), line = 1, column) {
+ const location = column ? { source, line, column } : { source, line };
+ return {
+ id: "breakpoint",
+ location,
+ generatedLocation: location,
+ disabled: false,
+ text: "text",
+ originalText: "text",
+ options: {},
+ };
+}
+
+function makeMockFrame(
+ id = "frame",
+ source = makeMockSource("url"),
+ scope = makeMockScope(),
+ line = 4,
+ displayName = `display-${id}`,
+ index = 0
+) {
+ const sourceActor = {
+ id: `${source.id}-actor`,
+ actor: `${source.id}-actor`,
+ source: source.id,
+ sourceObject: source,
+ };
+ const location = createLocation({ source, sourceActor, line });
+ return {
+ id,
+ thread: "FakeThread",
+ displayName,
+ location,
+ generatedLocation: location,
+ source,
+ scope,
+ this: {},
+ index,
+ asyncCause: null,
+ state: "on-stack",
+ type: "call",
+ };
+}
+
+function makeMockFrameWithURL(url) {
+ return makeMockFrame(undefined, makeMockSource(url));
+}
+
+function makeWhyNormal(frameReturnValue = undefined) {
+ if (frameReturnValue) {
+ return { type: "why-normal", frameFinished: { return: frameReturnValue } };
+ }
+ return { type: "why-normal" };
+}
+
+function makeWhyThrow(frameThrowValue) {
+ return { type: "why-throw", frameFinished: { throw: frameThrowValue } };
+}
+
+function makeMockExpression(value) {
+ return {
+ input: "input",
+ value,
+ from: "from",
+ updating: false,
+ };
+}
+
+// Mock contexts for use in tests that do not create a redux store.
+const mockcx = { navigateCounter: 0 };
+const mockthreadcx = {
+ navigateCounter: 0,
+ thread: "FakeThread",
+ pauseCounter: 0,
+ isPaused: false,
+};
+
+function makeMockThread(fields) {
+ return {
+ actor: "test",
+ url: "example.com",
+ type: "worker",
+ name: "test",
+ ...fields,
+ };
+}
+
+function makeMockState(state) {
+ return {
+ ...initialState(),
+ ...state,
+ };
+}
+
+function formatTree(tree, depth = 0, str = "") {
+ const whitespace = new Array(depth * 2).join(" ");
+
+ if (tree.type === "directory") {
+ str += `${whitespace} - ${tree.name} path=${tree.path} \n`;
+ tree.contents.forEach(t => {
+ str = formatTree(t, depth + 1, str);
+ });
+ } else {
+ str += `${whitespace} - ${tree.name} path=${tree.path} source_id=${tree.contents.id} \n`;
+ }
+
+ return str;
+}
+
+export {
+ makeMockDisplaySource,
+ makeMockSource,
+ makeMockSourceWithContent,
+ makeMockSourceAndContent,
+ makeMockWasmSource,
+ makeMockWasmSourceWithContent,
+ makeMockScope,
+ mockScopeAddVariable,
+ makeMockBreakpoint,
+ makeMockFrame,
+ makeMockFrameWithURL,
+ makeWhyNormal,
+ makeWhyThrow,
+ makeMockExpression,
+ mockcx,
+ mockthreadcx,
+ makeMockState,
+ makeMockThread,
+ makeFullfilledMockSourceContent,
+ formatTree,
+};
diff --git a/devtools/client/debugger/src/utils/tests/DevToolsUtils.spec.js b/devtools/client/debugger/src/utils/tests/DevToolsUtils.spec.js
new file mode 100644
index 0000000000..be405d8628
--- /dev/null
+++ b/devtools/client/debugger/src/utils/tests/DevToolsUtils.spec.js
@@ -0,0 +1,41 @@
+/* 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 { reportException, executeSoon } from "../DevToolsUtils.js";
+
+describe("DevToolsUtils", () => {
+ describe("reportException", () => {
+ beforeEach(() => {
+ global.console = { error: jest.fn() };
+ });
+
+ it("calls console.error", () => {
+ reportException("caller", ["you broke it"]);
+ expect(console.error).toHaveBeenCalled();
+ });
+
+ it("returns a description of caller and exception text", () => {
+ const who = "who",
+ exception = "this is an error",
+ msgTxt = " threw an exception: ";
+
+ reportException(who, [exception]);
+
+ expect(console.error).toHaveBeenCalledWith(`${who}${msgTxt}`, [
+ exception,
+ ]);
+ });
+ });
+
+ describe("executeSoon", () => {
+ it("calls setTimeout with 0 ms", () => {
+ global.setTimeout = jest.fn();
+ const fnc = () => {};
+
+ executeSoon(fnc);
+
+ expect(setTimeout).toHaveBeenCalledWith(fnc, 0);
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/utils/tests/__snapshots__/expressions.spec.js.snap b/devtools/client/debugger/src/utils/tests/__snapshots__/expressions.spec.js.snap
new file mode 100644
index 0000000000..a0d6b17bf6
--- /dev/null
+++ b/devtools/client/debugger/src/utils/tests/__snapshots__/expressions.spec.js.snap
@@ -0,0 +1,25 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`expressions wrap expression should wrap an expression 1`] = `
+"try {
+ foo
+} catch (e) {
+ e
+}"
+`;
+
+exports[`expressions wrap expression should wrap expression with a comment 1`] = `
+"try {
+ foo // yo yo
+} catch (e) {
+ e
+}"
+`;
+
+exports[`expressions wrap expression should wrap quotes 1`] = `
+"try {
+ \\"2\\"
+} catch (e) {
+ e
+}"
+`;
diff --git a/devtools/client/debugger/src/utils/tests/__snapshots__/function.spec.js.snap b/devtools/client/debugger/src/utils/tests/__snapshots__/function.spec.js.snap
new file mode 100644
index 0000000000..8d83a48d3b
--- /dev/null
+++ b/devtools/client/debugger/src/utils/tests/__snapshots__/function.spec.js.snap
@@ -0,0 +1,25 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`function findFunctionText finds class function 1`] = `
+"bar() {
+ 2 + 2;
+}"
+`;
+
+exports[`function findFunctionText finds function 1`] = `
+"async function exSlowFoo() {
+ return \\"yay in a bit\\";
+}"
+`;
+
+exports[`function findFunctionText finds function signature 1`] = `
+"async function exSlowFoo() {
+ return \\"yay in a bit\\";
+}"
+`;
+
+exports[`function findFunctionText finds property function 1`] = `
+"function name() {
+ 2 + 2;
+}"
+`;
diff --git a/devtools/client/debugger/src/utils/tests/__snapshots__/indentation.spec.js.snap b/devtools/client/debugger/src/utils/tests/__snapshots__/indentation.spec.js.snap
new file mode 100644
index 0000000000..883d48a7ff
--- /dev/null
+++ b/devtools/client/debugger/src/utils/tests/__snapshots__/indentation.spec.js.snap
@@ -0,0 +1,27 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`indentation mad indentation 1`] = `
+"try {
+console.log(\\"yo\\")
+} catch (e) {
+console.log(\\"yo\\")
+ }"
+`;
+
+exports[`indentation one function 1`] = `
+"function foo() {
+ console.log(\\"yo\\")
+}"
+`;
+
+exports[`indentation one line 1`] = `"foo"`;
+
+exports[`indentation simple 1`] = `"foo"`;
+
+exports[`indentation try catch 1`] = `
+"try {
+ console.log(\\"yo\\")
+} catch (e) {
+ console.log(\\"yo\\")
+}"
+`;
diff --git a/devtools/client/debugger/src/utils/tests/assert.spec.js b/devtools/client/debugger/src/utils/tests/assert.spec.js
new file mode 100644
index 0000000000..031665c5d3
--- /dev/null
+++ b/devtools/client/debugger/src/utils/tests/assert.spec.js
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+import assert from "../assert.js";
+
+let testAssertMessageHead, testAssertMessage;
+
+describe("assert", () => {
+ beforeEach(() => {
+ testAssertMessageHead = "Assertion failure: ";
+ testAssertMessage = "Test assert.js Message";
+ });
+
+ describe("when condition is truthy", () => {
+ it("does not throw an Error", () => {
+ expect(() => {
+ assert(true, testAssertMessage);
+ }).not.toThrow();
+ });
+ });
+
+ describe("when condition is falsy", () => {
+ it("throws an Error displaying the passed message", () => {
+ expect(() => {
+ assert(false, testAssertMessage);
+ }).toThrow(new Error(testAssertMessageHead + testAssertMessage));
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/utils/tests/build-query.spec.js b/devtools/client/debugger/src/utils/tests/build-query.spec.js
new file mode 100644
index 0000000000..dbd0eba4a4
--- /dev/null
+++ b/devtools/client/debugger/src/utils/tests/build-query.spec.js
@@ -0,0 +1,256 @@
+/* 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 buildQuery from "../build-query";
+
+describe("build-query", () => {
+ it("case-sensitive, whole-word, regex search", () => {
+ const query = buildQuery(
+ "hi.*",
+ {
+ caseSensitive: true,
+ wholeWord: true,
+ regexMatch: true,
+ },
+ {}
+ );
+
+ expect(query.source).toBe("\\bhi.*\\b");
+ expect(query.flags).toBe("");
+ expect(query.ignoreCase).toBe(false);
+ });
+
+ it("case-sensitive, whole-word, regex search, global", () => {
+ const query = buildQuery(
+ "hi.*",
+ {
+ caseSensitive: true,
+ wholeWord: true,
+ regexMatch: true,
+ },
+ { isGlobal: true }
+ );
+
+ expect(query.source).toBe("\\bhi.*\\b");
+ expect(query.flags).toBe("g");
+ expect(query.ignoreCase).toBe(false);
+ });
+
+ it("case-insensitive, non-whole, string search", () => {
+ const query = buildQuery(
+ "hi",
+ {
+ caseSensitive: false,
+ wholeWord: false,
+ regexMatch: false,
+ },
+ {}
+ );
+
+ expect(query.source).toBe("hi");
+ expect(query.flags).toBe("i");
+ expect(query.ignoreCase).toBe(true);
+ });
+
+ it("case-insensitive, non-whole, string search, global", () => {
+ const query = buildQuery(
+ "hi",
+ {
+ caseSensitive: false,
+ wholeWord: false,
+ regexMatch: false,
+ },
+ { isGlobal: true }
+ );
+
+ expect(query.source).toBe("hi");
+ expect(query.flags).toBe("gi");
+ expect(query.ignoreCase).toBe(true);
+ });
+
+ it("case-sensitive string search", () => {
+ const query = buildQuery(
+ "hi",
+ {
+ caseSensitive: true,
+ wholeWord: false,
+ regexMatch: false,
+ },
+ {}
+ );
+
+ expect(query.source).toBe("hi");
+ expect(query.flags).toBe("");
+ expect(query.ignoreCase).toBe(false);
+ });
+
+ it("string search with wholeWord", () => {
+ const query = buildQuery(
+ "hi",
+ {
+ caseSensitive: false,
+ wholeWord: true,
+ regexMatch: false,
+ },
+ {}
+ );
+
+ expect(query.source).toBe("\\bhi\\b");
+ expect(query.flags).toBe("i");
+ expect(query.ignoreCase).toBe(true);
+ });
+
+ it("case-insensitive, regex search", () => {
+ const query = buildQuery(
+ "hi.*",
+ {
+ caseSensitive: false,
+ wholeWord: false,
+ regexMatch: true,
+ },
+ {}
+ );
+
+ expect(query.source).toBe("hi.*");
+ expect(query.flags).toBe("i");
+ expect(query.global).toBe(false);
+ expect(query.ignoreCase).toBe(true);
+ });
+
+ it("string search with wholeWord and case sensitivity", () => {
+ const query = buildQuery(
+ "hi",
+ {
+ caseSensitive: true,
+ wholeWord: true,
+ regexMatch: false,
+ },
+ {}
+ );
+
+ expect(query.source).toBe("\\bhi\\b");
+ expect(query.flags).toBe("");
+ expect(query.global).toBe(false);
+ expect(query.ignoreCase).toBe(false);
+ });
+
+ it("string search with wholeWord and case sensitivity, global", () => {
+ const query = buildQuery(
+ "hi",
+ {
+ caseSensitive: true,
+ wholeWord: true,
+ regexMatch: false,
+ },
+ { isGlobal: true }
+ );
+
+ expect(query.source).toBe("\\bhi\\b");
+ expect(query.flags).toBe("g");
+ expect(query.global).toBe(true);
+ expect(query.ignoreCase).toBe(false);
+ });
+
+ it("string search with regex chars escaped", () => {
+ const query = buildQuery(
+ "hi.*",
+ {
+ caseSensitive: true,
+ wholeWord: true,
+ regexMatch: false,
+ },
+ {}
+ );
+
+ expect(query.source).toBe("\\bhi\\.\\*\\b");
+ expect(query.flags).toBe("");
+ expect(query.global).toBe(false);
+ expect(query.ignoreCase).toBe(false);
+ });
+
+ it("string search with regex chars escaped, global", () => {
+ const query = buildQuery(
+ "hi.*",
+ {
+ caseSensitive: true,
+ wholeWord: true,
+ regexMatch: false,
+ },
+ { isGlobal: true }
+ );
+
+ expect(query.source).toBe("\\bhi\\.\\*\\b");
+ expect(query.flags).toBe("g");
+ expect(query.global).toBe(true);
+ expect(query.ignoreCase).toBe(false);
+ });
+
+ it("ignore spaces w/o spaces", () => {
+ const query = buildQuery(
+ "hi",
+ {
+ caseSensitive: true,
+ wholeWord: false,
+ regexMatch: false,
+ },
+ { ignoreSpaces: true }
+ );
+
+ expect(query.source).toBe("hi");
+ expect(query.flags).toBe("");
+ expect(query.global).toBe(false);
+ expect(query.ignoreCase).toBe(false);
+ });
+
+ it("ignore spaces w/o spaces, global", () => {
+ const query = buildQuery(
+ "hi",
+ {
+ caseSensitive: true,
+ wholeWord: false,
+ regexMatch: false,
+ },
+ { isGlobal: true, ignoreSpaces: true }
+ );
+
+ expect(query.source).toBe("hi");
+ expect(query.flags).toBe("g");
+ expect(query.global).toBe(true);
+ expect(query.ignoreCase).toBe(false);
+ });
+
+ it("ignore spaces w/ spaces", () => {
+ const query = buildQuery(
+ " ",
+ {
+ caseSensitive: true,
+ wholeWord: false,
+ regexMatch: false,
+ },
+ { ignoreSpaces: true }
+ );
+
+ expect(query.source).toBe("(?!\\s*.*)");
+ expect(query.flags).toBe("");
+ expect(query.global).toBe(false);
+ expect(query.ignoreCase).toBe(false);
+ });
+
+ it("ignore spaces w/ spaces, global", () => {
+ const query = buildQuery(
+ " ",
+ {
+ caseSensitive: true,
+ wholeWord: false,
+ regexMatch: false,
+ },
+ { isGlobal: true, ignoreSpaces: true }
+ );
+
+ expect(query.source).toBe("(?!\\s*.*)");
+ expect(query.flags).toBe("g");
+ expect(query.global).toBe(true);
+ expect(query.ignoreCase).toBe(false);
+ });
+});
diff --git a/devtools/client/debugger/src/utils/tests/clipboard.spec.js b/devtools/client/debugger/src/utils/tests/clipboard.spec.js
new file mode 100644
index 0000000000..dcba730149
--- /dev/null
+++ b/devtools/client/debugger/src/utils/tests/clipboard.spec.js
@@ -0,0 +1,45 @@
+/* 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 { copyToTheClipboard } from "../clipboard";
+
+let clipboardTestCopyString, expectedCopyEvent;
+const addEventListener = jest.fn();
+const execCommand = jest.fn();
+const removeEventListener = jest.fn();
+
+describe("copyToTheClipboard()", () => {
+ beforeEach(() => {
+ expectedCopyEvent = "copy";
+ clipboardTestCopyString = "content intended for clipboard";
+
+ global.document.addEventListener = addEventListener;
+ global.document.execCommand = execCommand;
+ global.document.removeEventListener = removeEventListener;
+ });
+
+ it("listens for 'copy' event", () => {
+ copyToTheClipboard(clipboardTestCopyString);
+
+ expect(document.addEventListener).toHaveBeenCalledWith(
+ expectedCopyEvent,
+ expect.anything()
+ );
+ });
+
+ it("calls document.execCommand() with 'copy' command", () => {
+ copyToTheClipboard(clipboardTestCopyString);
+
+ expect(execCommand).toHaveBeenCalledWith(expectedCopyEvent, false, null);
+ });
+
+ it("removes event listener for 'copy' event", () => {
+ copyToTheClipboard(clipboardTestCopyString);
+
+ expect(document.removeEventListener).toHaveBeenCalledWith(
+ expectedCopyEvent,
+ expect.anything()
+ );
+ });
+});
diff --git a/devtools/client/debugger/src/utils/tests/expressions.spec.js b/devtools/client/debugger/src/utils/tests/expressions.spec.js
new file mode 100644
index 0000000000..b7c91c4e07
--- /dev/null
+++ b/devtools/client/debugger/src/utils/tests/expressions.spec.js
@@ -0,0 +1,67 @@
+/* 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 {
+ wrapExpression,
+ getExpressionResultGripAndFront,
+} from "../expressions";
+import { makeMockExpression } from "../test-mockup";
+
+function createError(type, preview) {
+ return makeMockExpression({
+ result: { getGrip: () => ({ class: type, isError: true, preview }) },
+ });
+}
+
+describe("expressions", () => {
+ describe("wrap expression", () => {
+ it("should wrap an expression", () => {
+ expect(wrapExpression("foo")).toMatchSnapshot();
+ });
+
+ it("should wrap expression with a comment", () => {
+ expect(wrapExpression("foo // yo yo")).toMatchSnapshot();
+ });
+
+ it("should wrap quotes", () => {
+ expect(wrapExpression('"2"')).toMatchSnapshot();
+ });
+ });
+
+ describe("sanitize input", () => {
+ it("sanitizes quotes", () => {
+ expect('foo"').toEqual('foo"');
+ });
+
+ it("sanitizes 2 quotes", () => {
+ expect('"3"').toEqual('"3"');
+ });
+
+ it("evaluates \\u{61} as a", () => {
+ expect("\u{61}").toEqual("a");
+ });
+
+ it("evaluates N\\u{61}N as NaN", () => {
+ expect("N\u{61}N").toEqual("NaN");
+ });
+ });
+
+ describe("getValue", () => {
+ it("Reference Errors should be shown as (unavailable)", () => {
+ const { expressionResultGrip } = getExpressionResultGripAndFront(
+ createError("ReferenceError", { name: "ReferenceError" })
+ );
+ expect(expressionResultGrip).toEqual({
+ unavailable: true,
+ });
+ });
+
+ it("Errors messages should be shown", () => {
+ const { expressionResultGrip } = getExpressionResultGripAndFront(
+ createError("Error", { name: "Foo", message: "YO" })
+ );
+ expect(expressionResultGrip).toEqual("Foo: YO");
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/utils/tests/function.spec.js b/devtools/client/debugger/src/utils/tests/function.spec.js
new file mode 100644
index 0000000000..db77736ab5
--- /dev/null
+++ b/devtools/client/debugger/src/utils/tests/function.spec.js
@@ -0,0 +1,61 @@
+/* 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 { findFunctionText } from "../function";
+
+import { getFunctionSymbols } from "../../workers/parser/getSymbols";
+import { populateOriginalSource } from "../../workers/parser/tests/helpers";
+
+describe("function", () => {
+ describe("findFunctionText", () => {
+ it("finds function", () => {
+ const source = populateOriginalSource("func");
+ const functions = getFunctionSymbols(source.id);
+ const text = findFunctionText(14, source, source.content, { functions });
+ expect(text).toMatchSnapshot();
+ });
+
+ it("finds function signature", () => {
+ const source = populateOriginalSource("func");
+ const functions = getFunctionSymbols(source.id);
+
+ const text = findFunctionText(13, source, source.content, { functions });
+ expect(text).toMatchSnapshot();
+ });
+
+ it("misses function closing brace", () => {
+ const source = populateOriginalSource("func");
+ const functions = getFunctionSymbols(source.id);
+
+ const text = findFunctionText(15, source, source.content, { functions });
+
+ // TODO: we should try and match the closing bracket.
+ expect(text).toEqual(null);
+ });
+
+ it("finds property function", () => {
+ const source = populateOriginalSource("func");
+ const functions = getFunctionSymbols(source.id);
+
+ const text = findFunctionText(29, source, source.content, { functions });
+ expect(text).toMatchSnapshot();
+ });
+
+ it("finds class function", () => {
+ const source = populateOriginalSource("func");
+ const functions = getFunctionSymbols(source.id);
+
+ const text = findFunctionText(33, source, source.content, { functions });
+ expect(text).toMatchSnapshot();
+ });
+
+ it("cant find function", () => {
+ const source = populateOriginalSource("func");
+ const functions = getFunctionSymbols(source.id);
+
+ const text = findFunctionText(20, source, source.content, { functions });
+ expect(text).toEqual(null);
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/utils/tests/indentation.spec.js b/devtools/client/debugger/src/utils/tests/indentation.spec.js
new file mode 100644
index 0000000000..5ee7419371
--- /dev/null
+++ b/devtools/client/debugger/src/utils/tests/indentation.spec.js
@@ -0,0 +1,61 @@
+/* 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 { correctIndentation, getIndentation } from "../indentation";
+
+describe("indentation", () => {
+ it("simple", () => {
+ expect(
+ correctIndentation(`
+ foo
+ `)
+ ).toMatchSnapshot();
+ });
+
+ it("one line", () => {
+ expect(correctIndentation("foo")).toMatchSnapshot();
+ });
+
+ it("one function", () => {
+ const text = `
+ function foo() {
+ console.log("yo")
+ }
+ `;
+
+ expect(correctIndentation(text)).toMatchSnapshot();
+ });
+
+ it("try catch", () => {
+ const text = `
+ try {
+ console.log("yo")
+ } catch (e) {
+ console.log("yo")
+ }
+ `;
+
+ expect(correctIndentation(text)).toMatchSnapshot();
+ });
+
+ it("mad indentation", () => {
+ const text = `
+ try {
+ console.log("yo")
+ } catch (e) {
+ console.log("yo")
+ }
+ `;
+
+ expect(correctIndentation(text)).toMatchSnapshot();
+ });
+});
+
+describe("indentation length", () => {
+ it("leading spaces", () => {
+ const line = " console.log('Hello World');";
+
+ expect(getIndentation(line)).toEqual(16);
+ });
+});
diff --git a/devtools/client/debugger/src/utils/tests/isMinified.spec.js b/devtools/client/debugger/src/utils/tests/isMinified.spec.js
new file mode 100644
index 0000000000..1c49f96737
--- /dev/null
+++ b/devtools/client/debugger/src/utils/tests/isMinified.spec.js
@@ -0,0 +1,18 @@
+/* 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 { isMinified } from "../isMinified";
+import { makeMockSourceWithContent } from "../test-mockup";
+
+describe("isMinified", () => {
+ it("no indents", () => {
+ const sourceWithContent = makeMockSourceWithContent(
+ undefined,
+ undefined,
+ undefined,
+ "function base(boo) {\n}"
+ );
+ expect(isMinified(sourceWithContent, sourceWithContent.content)).toBe(true);
+ });
+});
diff --git a/devtools/client/debugger/src/utils/tests/location.spec.js b/devtools/client/debugger/src/utils/tests/location.spec.js
new file mode 100644
index 0000000000..2be71f3cae
--- /dev/null
+++ b/devtools/client/debugger/src/utils/tests/location.spec.js
@@ -0,0 +1,31 @@
+/* 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 { sortSelectedLocations } from "../location";
+
+function loc(line, column) {
+ return {
+ location: { sourceId: "foo", line, column },
+ generatedLocation: { sourceId: "foo", line, column },
+ };
+}
+describe("location.spec.js", () => {
+ it("sorts lines", () => {
+ const a = loc(3, 5);
+ const b = loc(1, 10);
+ expect(sortSelectedLocations([a, b], { id: "foo" })).toEqual([b, a]);
+ });
+
+ it("sorts columns", () => {
+ const a = loc(3, 10);
+ const b = loc(3, 5);
+ expect(sortSelectedLocations([a, b], { id: "foo" })).toEqual([b, a]);
+ });
+
+ it("prioritizes undefined columns", () => {
+ const a = loc(3, 10);
+ const b = loc(3, undefined);
+ expect(sortSelectedLocations([a, b], { id: "foo" })).toEqual([b, a]);
+ });
+});
diff --git a/devtools/client/debugger/src/utils/tests/log.spec.js b/devtools/client/debugger/src/utils/tests/log.spec.js
new file mode 100644
index 0000000000..ea7e4ca4d2
--- /dev/null
+++ b/devtools/client/debugger/src/utils/tests/log.spec.js
@@ -0,0 +1,35 @@
+/* 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 { prefs } from "../prefs";
+import { log } from "../log.js";
+
+let logArgFirst, logArgSecond, logArgArray;
+
+describe("log()", () => {
+ beforeEach(() => {
+ logArgFirst = "my info";
+ logArgSecond = "my other info";
+ logArgArray = [logArgFirst, logArgSecond];
+ global.console = { log: jest.fn() };
+ });
+
+ afterEach(() => {
+ prefs.logging = false;
+ });
+
+ describe("when logging pref is true", () => {
+ it("prints arguments", () => {
+ prefs.logging = true;
+ log(logArgArray);
+
+ expect(global.console.log).toHaveBeenCalledWith(logArgArray);
+ });
+
+ it("does not print by default", () => {
+ log(logArgArray);
+ expect(global.console.log).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/utils/tests/memoize.spec.js b/devtools/client/debugger/src/utils/tests/memoize.spec.js
new file mode 100644
index 0000000000..fb72958516
--- /dev/null
+++ b/devtools/client/debugger/src/utils/tests/memoize.spec.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 memoize from "../memoize";
+
+const a = { number: 3 };
+const b = { number: 4 };
+const c = { number: 5 };
+const d = { number: 6 };
+
+function add(...numberObjects) {
+ return numberObjects.reduce((prev, cur) => prev + cur.number, 0);
+}
+
+describe("memozie", () => {
+ it("should work for one arg as key", () => {
+ const mAdd = memoize(add);
+ mAdd(a);
+ expect(mAdd(a)).toEqual(3);
+ mAdd(b);
+ expect(mAdd(b)).toEqual(4);
+ });
+
+ it("should only be called once", () => {
+ const spy = jest.fn(() => 2);
+ const mAdd = memoize(spy);
+
+ mAdd(a);
+ mAdd(a);
+ mAdd(a);
+ expect(spy).toHaveBeenCalledTimes(1);
+ });
+
+ it("should work for two args as key", () => {
+ const mAdd = memoize(add);
+ expect(mAdd(a, b)).toEqual(7);
+ expect(mAdd(a, b)).toEqual(7);
+ expect(mAdd(a, c)).toEqual(8);
+ });
+
+ it("should work with many args as key", () => {
+ const mAdd = memoize(add);
+ expect(mAdd(a, b, c)).toEqual(12);
+ expect(mAdd(a, b, d)).toEqual(13);
+ expect(mAdd(a, b, c)).toEqual(12);
+ });
+});
diff --git a/devtools/client/debugger/src/utils/tests/memoizeLast.spec.js b/devtools/client/debugger/src/utils/tests/memoizeLast.spec.js
new file mode 100644
index 0000000000..a5622510e3
--- /dev/null
+++ b/devtools/client/debugger/src/utils/tests/memoizeLast.spec.js
@@ -0,0 +1,31 @@
+/* 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 { memoizeLast } from "../memoizeLast";
+
+const a = { number: 3 };
+const b = { number: 4 };
+
+function add(...numberObjects) {
+ return numberObjects.reduce((prev, cur) => prev + cur.number, 0);
+}
+
+describe("memozie", () => {
+ it("should re-calculate when a value changes", () => {
+ const mAdd = memoizeLast(add);
+ mAdd(a);
+ expect(mAdd(a)).toEqual(3);
+ mAdd(b);
+ expect(mAdd(b)).toEqual(4);
+ });
+
+ it("should only run once", () => {
+ const mockAdd = jest.fn(add);
+ const mAdd = memoizeLast(mockAdd);
+ mAdd(a);
+ mAdd(a);
+
+ expect(mockAdd.mock.calls[0]).toEqual([{ number: 3 }]);
+ });
+});
diff --git a/devtools/client/debugger/src/utils/tests/path.spec.js b/devtools/client/debugger/src/utils/tests/path.spec.js
new file mode 100644
index 0000000000..58bdf046f0
--- /dev/null
+++ b/devtools/client/debugger/src/utils/tests/path.spec.js
@@ -0,0 +1,49 @@
+/* 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 { basename, dirname, isURL, isAbsolute, join } from "../path";
+
+const fullTestURL = "https://www.example.com/some/endpoint";
+const absoluteTestPath = "/some/absolute/path/to/resource";
+const aTestName = "name";
+
+describe("basename()", () => {
+ it("returns the basename of the path", () => {
+ expect(basename(fullTestURL)).toBe("endpoint");
+ });
+});
+
+describe("dirname()", () => {
+ it("returns the current directory in a path", () => {
+ expect(dirname(fullTestURL)).toBe("https://www.example.com/some");
+ });
+});
+
+describe("isURL()", () => {
+ it("returns true if a string contains characters denoting a scheme", () => {
+ expect(isURL(fullTestURL)).toBe(true);
+ });
+
+ it("returns false if string does not denote a scheme", () => {
+ expect(isURL(absoluteTestPath)).toBe(false);
+ });
+});
+
+describe("isAbsolute()", () => {
+ it("returns true if a string begins with a slash", () => {
+ expect(isAbsolute(absoluteTestPath)).toBe(true);
+ });
+
+ it("returns false if a string does not begin with a slash", () => {
+ expect(isAbsolute(fullTestURL)).toBe(false);
+ });
+});
+
+describe("join()", () => {
+ it("concatenates a base path and a directory name", () => {
+ expect(join(absoluteTestPath, aTestName)).toBe(
+ "/some/absolute/path/to/resource/name"
+ );
+ });
+});
diff --git a/devtools/client/debugger/src/utils/tests/quick-open.spec.js b/devtools/client/debugger/src/utils/tests/quick-open.spec.js
new file mode 100644
index 0000000000..66cceb6825
--- /dev/null
+++ b/devtools/client/debugger/src/utils/tests/quick-open.spec.js
@@ -0,0 +1,35 @@
+/* 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 cases from "jest-in-case";
+import { parseQuickOpenQuery, parseLineColumn } from "../quick-open";
+
+cases(
+ "parseQuickOpenQuery utility",
+ ({ type, query }) => expect(parseQuickOpenQuery(query)).toEqual(type),
+ [
+ { name: "empty query defaults to sources", type: "sources", query: "" },
+ { name: "sources query", type: "sources", query: "test" },
+ { name: "functions query", type: "functions", query: "@test" },
+ { name: "variables query", type: "variables", query: "#test" },
+ { name: "goto line", type: "goto", query: ":30" },
+ { name: "goto line:column", type: "goto", query: ":30:60" },
+ { name: "goto source line", type: "gotoSource", query: "test:30:60" },
+ { name: "shortcuts", type: "shortcuts", query: "?" },
+ ]
+);
+
+cases(
+ "parseLineColumn utility",
+ ({ query, location }) => expect(parseLineColumn(query)).toEqual(location),
+ [
+ { name: "empty query", query: "", location: null },
+ { name: "just line", query: ":30", location: { line: 30 } },
+ {
+ name: "line and column",
+ query: ":30:90",
+ location: { column: 89, line: 30 },
+ },
+ ]
+);
diff --git a/devtools/client/debugger/src/utils/tests/result-list.spec.js b/devtools/client/debugger/src/utils/tests/result-list.spec.js
new file mode 100644
index 0000000000..dcbaf421c5
--- /dev/null
+++ b/devtools/client/debugger/src/utils/tests/result-list.spec.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 { scrollList } from "../result-list.js";
+
+describe("scrollList", () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ });
+
+ /* eslint-disable jest/expect-expect */
+ it("just returns if element not found", () => {
+ const li = document.createElement("li");
+ scrollList([li], 1);
+ });
+ /* eslint-enable jest/expect-expect */
+
+ it("calls scrollIntoView", () => {
+ const ul = document.createElement("ul");
+ const li = document.createElement("li");
+
+ li.scrollIntoView = jest.fn();
+ ul.appendChild(li);
+
+ scrollList([li], 0);
+
+ jest.runAllTimers();
+
+ expect(li.scrollIntoView).toHaveBeenCalled();
+ });
+});
diff --git a/devtools/client/debugger/src/utils/tests/source.spec.js b/devtools/client/debugger/src/utils/tests/source.spec.js
new file mode 100644
index 0000000000..484c8ce570
--- /dev/null
+++ b/devtools/client/debugger/src/utils/tests/source.spec.js
@@ -0,0 +1,367 @@
+/* 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 {
+ getFilename,
+ getTruncatedFileName,
+ getFileURL,
+ getDisplayPath,
+ getSourceLineCount,
+ isJavaScript,
+ isDescendantOfRoot,
+ removeThreadActorId,
+ isUrlExtension,
+ getLineText,
+} from "../source.js";
+
+import {
+ makeMockSource,
+ makeMockSourceWithContent,
+ makeMockSourceAndContent,
+ makeMockWasmSourceWithContent,
+ makeMockThread,
+ makeFullfilledMockSourceContent,
+} from "../test-mockup";
+import { isFulfilled } from "../async-value.js";
+
+describe("sources", () => {
+ const unicode = "\u6e2c";
+ const encodedUnicode = encodeURIComponent(unicode);
+
+ describe("getFilename", () => {
+ it("should give us a default of (index)", () => {
+ expect(
+ getFilename(makeMockSource("http://localhost.com:7999/increment/"))
+ ).toBe("(index)");
+ });
+ it("should give us the filename", () => {
+ expect(
+ getFilename(
+ makeMockSource("http://localhost.com:7999/increment/hello.html")
+ )
+ ).toBe("hello.html");
+ });
+ it("should give us the readable Unicode filename if encoded", () => {
+ expect(
+ getFilename(
+ makeMockSource(
+ `http://localhost.com:7999/increment/${encodedUnicode}.html`
+ )
+ )
+ ).toBe(`${unicode}.html`);
+ });
+ it("should give us the filename excluding the query strings", () => {
+ expect(
+ getFilename(
+ makeMockSource(
+ "http://localhost.com:7999/increment/hello.html?query_strings"
+ )
+ )
+ ).toBe("hello.html");
+ });
+ it("should give us the proper filename for pretty files", () => {
+ expect(
+ getFilename(
+ makeMockSource(
+ "http://localhost.com:7999/increment/hello.html:formatted"
+ )
+ )
+ ).toBe("hello.html");
+ });
+ });
+
+ describe("getTruncatedFileName", () => {
+ it("should truncate the file name when it is more than 30 chars", () => {
+ expect(
+ getTruncatedFileName(
+ makeMockSource(
+ "really-really-really-really-really-really-long-name.html"
+ ),
+ "",
+ 30
+ )
+ ).toBe("really-really…long-name.html");
+ });
+ it("should first decode the filename and then truncate it", () => {
+ expect(
+ getTruncatedFileName(
+ makeMockSource(`${encodedUnicode.repeat(30)}.html`),
+ "",
+ 30
+ )
+ ).toBe("測測測測測測測測測測測測測…測測測測測測測測測.html");
+ });
+ });
+
+ describe("getDisplayPath", () => {
+ it("should give us the path for files with same name", () => {
+ const sources = [
+ makeMockSource("http://localhost.com:7999/increment/xyz/hello.html"),
+ makeMockSource("http://localhost.com:7999/increment/abc/hello.html"),
+ makeMockSource("http://localhost.com:7999/increment/hello.html"),
+ ];
+ expect(
+ getDisplayPath(
+ makeMockSource("http://localhost.com:7999/increment/abc/hello.html"),
+ sources
+ )
+ ).toBe("abc");
+ });
+
+ it(`should give us the path for files with same name
+ in directories with same name`, () => {
+ const sources = [
+ makeMockSource(
+ "http://localhost.com:7999/increment/xyz/web/hello.html"
+ ),
+ makeMockSource(
+ "http://localhost.com:7999/increment/abc/web/hello.html"
+ ),
+ makeMockSource("http://localhost.com:7999/increment/hello.html"),
+ ];
+ expect(
+ getDisplayPath(
+ makeMockSource(
+ "http://localhost.com:7999/increment/abc/web/hello.html"
+ ),
+ sources
+ )
+ ).toBe("abc/web");
+ });
+
+ it("should give no path for files with unique name", () => {
+ const sources = [
+ makeMockSource("http://localhost.com:7999/increment/xyz.html"),
+ makeMockSource("http://localhost.com:7999/increment/abc.html"),
+ makeMockSource("http://localhost.com:7999/increment/hello.html"),
+ ];
+ expect(
+ getDisplayPath(
+ makeMockSource("http://localhost.com:7999/increment/abc/web.html"),
+ sources
+ )
+ ).toBe(undefined);
+ });
+ it("should not show display path for pretty file", () => {
+ const sources = [
+ makeMockSource("http://localhost.com:7999/increment/abc/web/hell.html"),
+ makeMockSource(
+ "http://localhost.com:7999/increment/abc/web/hello.html"
+ ),
+ makeMockSource(
+ "http://localhost.com:7999/increment/xyz.html:formatted"
+ ),
+ ];
+ expect(
+ getDisplayPath(
+ makeMockSource(
+ "http://localhost.com:7999/increment/abc/web/hello.html:formatted"
+ ),
+ sources
+ )
+ ).toBe(undefined);
+ });
+ it(`should give us the path for files with same name when both
+ are pretty and different path`, () => {
+ const sources = [
+ makeMockSource(
+ "http://localhost.com:7999/increment/xyz/web/hello.html:formatted"
+ ),
+ makeMockSource(
+ "http://localhost.com:7999/increment/abc/web/hello.html:formatted"
+ ),
+ makeMockSource(
+ "http://localhost.com:7999/increment/hello.html:formatted"
+ ),
+ ];
+ expect(
+ getDisplayPath(
+ makeMockSource(
+ "http://localhost.com:7999/increment/abc/web/hello.html:formatted"
+ ),
+ sources
+ )
+ ).toBe("abc/web");
+ });
+ });
+
+ describe("getFileURL", () => {
+ it("should give us the file url", () => {
+ expect(
+ getFileURL(
+ makeMockSource("http://localhost.com:7999/increment/hello.html")
+ )
+ ).toBe("http://localhost.com:7999/increment/hello.html");
+ });
+ it("should truncate the file url when it is more than 50 chars", () => {
+ expect(
+ getFileURL(
+ makeMockSource("http://localhost-long.com:7999/increment/hello.html")
+ )
+ ).toBe("…ttp://localhost-long.com:7999/increment/hello.html");
+ });
+ it("should first decode the file URL and then truncate it", () => {
+ expect(
+ getFileURL(makeMockSource(`http://${encodedUnicode.repeat(39)}.html`))
+ ).toBe(`…ttp://${unicode.repeat(39)}.html`);
+ });
+ });
+
+ describe("isJavaScript", () => {
+ it("is not JavaScript", () => {
+ {
+ const source = makeMockSourceAndContent("foo.html", undefined, "");
+ expect(isJavaScript(source, source.content)).toBe(false);
+ }
+ {
+ const source = makeMockSourceAndContent(
+ undefined,
+ undefined,
+ "text/html"
+ );
+ expect(isJavaScript(source, source.content)).toBe(false);
+ }
+ });
+
+ it("is JavaScript", () => {
+ {
+ const source = makeMockSourceAndContent("foo.js");
+ expect(isJavaScript(source, source.content)).toBe(true);
+ }
+ {
+ const source = makeMockSourceAndContent("bar.jsm");
+ expect(isJavaScript(source, source.content)).toBe(true);
+ }
+ {
+ const source = makeMockSourceAndContent(
+ undefined,
+ undefined,
+ "text/javascript"
+ );
+ expect(isJavaScript(source, source.content)).toBe(true);
+ }
+ {
+ const source = makeMockSourceAndContent(
+ undefined,
+ undefined,
+ "application/javascript"
+ );
+ expect(isJavaScript(source, source.content)).toBe(true);
+ }
+ });
+ });
+
+ describe("getSourceLineCount", () => {
+ it("should give us the amount bytes for wasm source", () => {
+ const { content } = makeMockWasmSourceWithContent({
+ binary: "\x00asm\x01\x00\x00\x00",
+ });
+ expect(getSourceLineCount(content.value)).toEqual(8);
+ });
+
+ it("should give us the amout of lines for js source", () => {
+ const { content } = makeMockSourceWithContent(
+ undefined,
+ undefined,
+ "text/javascript",
+ "function foo(){\n}"
+ );
+ if (!content || !isFulfilled(content)) {
+ throw new Error("Unexpected content value");
+ }
+ expect(getSourceLineCount(content.value)).toEqual(2);
+ });
+ });
+
+ describe("isDescendantOfRoot", () => {
+ const threads = [
+ makeMockThread({ actor: "server0.conn1.child1/thread19" }),
+ ];
+
+ it("should detect normal source urls", () => {
+ const source = makeMockSource(
+ "resource://activity-stream/vendor/react.js"
+ );
+ const rootWithoutThreadActor = removeThreadActorId(
+ "resource://activity-stream",
+ threads
+ );
+ expect(isDescendantOfRoot(source, rootWithoutThreadActor)).toBe(true);
+ });
+
+ it("should detect source urls under chrome:// as root", () => {
+ const source = makeMockSource(
+ "chrome://browser/content/contentSearchUI.js"
+ );
+ const rootWithoutThreadActor = removeThreadActorId("chrome://", threads);
+ expect(isDescendantOfRoot(source, rootWithoutThreadActor)).toBe(true);
+ });
+
+ it("should detect source urls if root is a thread actor Id", () => {
+ const source = makeMockSource(
+ "resource://activity-stream/vendor/react-dom.js"
+ );
+ const rootWithoutThreadActor = removeThreadActorId(
+ "server0.conn1.child1/thread19",
+ threads
+ );
+ expect(isDescendantOfRoot(source, rootWithoutThreadActor)).toBe(true);
+ });
+ });
+
+ describe("isUrlExtension", () => {
+ it("should detect mozilla extension", () => {
+ expect(isUrlExtension("moz-extension://id/js/content.js")).toBe(true);
+ });
+ it("should detect chrome extension", () => {
+ expect(isUrlExtension("chrome-extension://id/js/content.js")).toBe(true);
+ });
+ it("should return false for non-extension assets", () => {
+ expect(isUrlExtension("https://example.org/init.js")).toBe(false);
+ });
+ });
+
+ describe("getLineText", () => {
+ it("first line", () => {
+ const text = getLineText(
+ "fake-source",
+ makeFullfilledMockSourceContent("aaa\nbbb\nccc"),
+ 1
+ );
+
+ expect(text).toEqual("aaa");
+ });
+
+ it("last line", () => {
+ const text = getLineText(
+ "fake-source",
+ makeFullfilledMockSourceContent("aaa\nbbb\nccc"),
+ 3
+ );
+
+ expect(text).toEqual("ccc");
+ });
+
+ it("one line", () => {
+ const text = getLineText(
+ "fake-source",
+ makeFullfilledMockSourceContent("aaa"),
+ 1
+ );
+
+ expect(text).toEqual("aaa");
+ });
+
+ it("bad line", () => {
+ const text = getLineText(
+ "fake-source",
+ makeFullfilledMockSourceContent("aaa\nbbb\nccc"),
+
+ 5
+ );
+
+ expect(text).toEqual("");
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/utils/tests/telemetry.spec.js b/devtools/client/debugger/src/utils/tests/telemetry.spec.js
new file mode 100644
index 0000000000..7223641afd
--- /dev/null
+++ b/devtools/client/debugger/src/utils/tests/telemetry.spec.js
@@ -0,0 +1,13 @@
+/* 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 { recordEvent } from "../telemetry";
+
+describe("telemetry.recordEvent()", () => {
+ it("Receives the correct telemetry information", () => {
+ recordEvent("foo", { bar: 1 });
+
+ expect(window.dbg._telemetry.events.foo).toStrictEqual([{ bar: 1 }]);
+ });
+});
diff --git a/devtools/client/debugger/src/utils/tests/text.spec.js b/devtools/client/debugger/src/utils/tests/text.spec.js
new file mode 100644
index 0000000000..5786bc6232
--- /dev/null
+++ b/devtools/client/debugger/src/utils/tests/text.spec.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/>. */
+
+import { truncateMiddleText } from "../text";
+
+describe("text", () => {
+ it("should truncate the text in the middle", () => {
+ const sourceText = "this is a very long text and ends here";
+ expect(truncateMiddleText(sourceText, 30)).toMatch(
+ "this is a ver… and ends here"
+ );
+ });
+ it("should keep the text as it is", () => {
+ const sourceText = "this is a short text ends here";
+ expect(truncateMiddleText(sourceText, 30)).toMatch(
+ "this is a short text ends here"
+ );
+ });
+});
diff --git a/devtools/client/debugger/src/utils/tests/ui.spec.js b/devtools/client/debugger/src/utils/tests/ui.spec.js
new file mode 100644
index 0000000000..aa091e6798
--- /dev/null
+++ b/devtools/client/debugger/src/utils/tests/ui.spec.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/>. */
+
+import { isVisible } from "../ui";
+
+describe("ui", () => {
+ it("should return #mount width", () => {
+ if (!document.body) {
+ throw new Error("no document body");
+ }
+ document.body.innerHTML = "<div id='mount'></div>";
+ expect(isVisible()).toBe(false);
+ });
+});
diff --git a/devtools/client/debugger/src/utils/tests/url.spec.js b/devtools/client/debugger/src/utils/tests/url.spec.js
new file mode 100644
index 0000000000..d842acf7db
--- /dev/null
+++ b/devtools/client/debugger/src/utils/tests/url.spec.js
@@ -0,0 +1,89 @@
+/* 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 { parse } from "../url";
+
+describe("url", () => {
+ describe("parse", () => {
+ it("parses an absolute URL", () => {
+ const val = parse("http://example.com:8080/path/file.js");
+
+ expect(val.protocol).toBe("http:");
+ expect(val.host).toBe("example.com:8080");
+ expect(val.pathname).toBe("/path/file.js");
+ expect(val.search).toBe("");
+ expect(val.hash).toBe("");
+ });
+
+ it("parses an absolute URL with query params", () => {
+ const val = parse("http://example.com:8080/path/file.js?param");
+
+ expect(val.protocol).toBe("http:");
+ expect(val.host).toBe("example.com:8080");
+ expect(val.pathname).toBe("/path/file.js");
+ expect(val.search).toBe("?param");
+ expect(val.hash).toBe("");
+ });
+
+ it("parses an absolute URL with a fragment", () => {
+ const val = parse("http://example.com:8080/path/file.js#hash");
+
+ expect(val.protocol).toBe("http:");
+ expect(val.host).toBe("example.com:8080");
+ expect(val.pathname).toBe("/path/file.js");
+ expect(val.search).toBe("");
+ expect(val.hash).toBe("#hash");
+ });
+
+ it("parses an absolute URL with query params and a fragment", () => {
+ const val = parse("http://example.com:8080/path/file.js?param#hash");
+
+ expect(val.protocol).toBe("http:");
+ expect(val.host).toBe("example.com:8080");
+ expect(val.pathname).toBe("/path/file.js");
+ expect(val.search).toBe("?param");
+ expect(val.hash).toBe("#hash");
+ });
+
+ it("parses a partial URL", () => {
+ const val = parse("/path/file.js");
+
+ expect(val.protocol).toBe("");
+ expect(val.host).toBe("");
+ expect(val.pathname).toBe("/path/file.js");
+ expect(val.search).toBe("");
+ expect(val.hash).toBe("");
+ });
+
+ it("parses a partial URL with query params", () => {
+ const val = parse("/path/file.js?param");
+
+ expect(val.protocol).toBe("");
+ expect(val.host).toBe("");
+ expect(val.pathname).toBe("/path/file.js");
+ expect(val.search).toBe("?param");
+ expect(val.hash).toBe("");
+ });
+
+ it("parses a partial URL with a fragment", () => {
+ const val = parse("/path/file.js#hash");
+
+ expect(val.protocol).toBe("");
+ expect(val.host).toBe("");
+ expect(val.pathname).toBe("/path/file.js");
+ expect(val.search).toBe("");
+ expect(val.hash).toBe("#hash");
+ });
+
+ it("parses a partial URL with query params and a fragment", () => {
+ const val = parse("/path/file.js?param#hash");
+
+ expect(val.protocol).toBe("");
+ expect(val.host).toBe("");
+ expect(val.pathname).toBe("/path/file.js");
+ expect(val.search).toBe("?param");
+ expect(val.hash).toBe("#hash");
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/utils/tests/utils.spec.js b/devtools/client/debugger/src/utils/tests/utils.spec.js
new file mode 100644
index 0000000000..f358ffc42a
--- /dev/null
+++ b/devtools/client/debugger/src/utils/tests/utils.spec.js
@@ -0,0 +1,87 @@
+/* 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 { handleError, promisify, endTruncateStr, waitForMs } from "../utils";
+
+describe("handleError()", () => {
+ const testErrorText = "ERROR: ";
+ const testErrorObject = { oh: "noes" };
+
+ beforeEach(() => {
+ global.console = { log: jest.fn() };
+ });
+
+ it("logs error text with error value", () => {
+ handleError(testErrorObject);
+
+ expect(console.log).toHaveBeenCalledWith(testErrorText, testErrorObject);
+ });
+});
+
+describe("promisify()", () => {
+ let testPromise, testContext, testMethod, testArgs;
+
+ beforeEach(() => {
+ testContext = {};
+ testMethod = jest.fn();
+ testArgs = [];
+ });
+
+ it("returns a Promise", () => {
+ testPromise = promisify(testContext, testMethod, testArgs);
+
+ expect(testPromise instanceof Promise).toBe(true);
+ });
+
+ it("applies promisified method", () => {
+ testPromise = promisify(testContext, testMethod, testArgs);
+
+ expect(testMethod).toHaveBeenCalledWith(testArgs, expect.anything());
+ });
+});
+
+describe("endTruncateStr()", () => {
+ let testString;
+ const testSize = 11;
+
+ describe("when the string is larger than the specified size", () => {
+ it("returns an elipsis and characters at the end of the string", () => {
+ testString = "Mozilla Firefox is my favorite web browser";
+
+ expect(endTruncateStr(testString, testSize)).toBe("…web browser");
+ });
+ });
+
+ describe("when the string is not larger than the specified size", () => {
+ it("returns the string unchanged", () => {
+ testString = "Firefox";
+
+ expect(endTruncateStr(testString, testSize)).toBe(testString);
+ });
+ });
+});
+
+describe("waitForMs()", () => {
+ let testPromise;
+ const testMilliseconds = 10;
+
+ beforeEach(() => {
+ global.setTimeout = jest.fn();
+ });
+
+ it("returns a Promise", () => {
+ testPromise = waitForMs(testMilliseconds);
+
+ expect(testPromise instanceof Promise).toBe(true);
+ });
+
+ it("calls setTimeout() on the resolve of the Promise", () => {
+ testPromise = waitForMs(testMilliseconds);
+
+ expect(setTimeout).toHaveBeenCalledWith(
+ expect.anything(),
+ testMilliseconds
+ );
+ });
+});
diff --git a/devtools/client/debugger/src/utils/tests/wasm.spec.js b/devtools/client/debugger/src/utils/tests/wasm.spec.js
new file mode 100644
index 0000000000..6cf3b1083b
--- /dev/null
+++ b/devtools/client/debugger/src/utils/tests/wasm.spec.js
@@ -0,0 +1,96 @@
+/* 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 {
+ isWasm,
+ lineToWasmOffset,
+ wasmOffsetToLine,
+ clearWasmStates,
+ renderWasmText,
+} from "../wasm.js";
+
+import { makeMockWasmSourceWithContent } from "../test-mockup";
+
+describe("wasm", () => {
+ // Compiled version of `(module (func (nop)))`
+ const SIMPLE_WASM = {
+ binary:
+ "\x00asm\x01\x00\x00\x00\x01\x84\x80\x80\x80\x00\x01`\x00\x00" +
+ "\x03\x82\x80\x80\x80\x00\x01\x00\x06\x81\x80\x80\x80\x00\x00" +
+ "\n\x89\x80\x80\x80\x00\x01\x83\x80\x80\x80\x00\x00\x01\v",
+ };
+
+ // malformed binary which contains an unknown operator (\x09) which
+ // should cause the wasm parser to throw.
+ const MALFORMED_SIMPLE_WASM = {
+ binary:
+ "\x00asm\x01\x00\x00\x00\x09\x84\x80\x80\x80\x00\x01`\x00\x00" +
+ "\x03\x82\x80\x80\x80\x00\x01\x00\x06\x81\x80\x80\x80\x00\x00" +
+ "\n\x89\x80\x80\x80\x00\x01\x83\x80\x80\x80\x00\x00\x01\v",
+ };
+
+ const SIMPLE_WASM_TEXT = `(module
+ (func $func0
+ nop
+ )
+)`;
+ const SIMPLE_WASM_NOP_TEXT_LINE = 2;
+ const SIMPLE_WASM_NOP_OFFSET = 46;
+
+ describe("isWasm", () => {
+ it("should give us the false when wasm text was not registered", () => {
+ const sourceId = "source.0";
+ expect(isWasm(sourceId)).toEqual(false);
+ });
+ it("should give us the true when wasm text was registered", () => {
+ const source = makeMockWasmSourceWithContent(SIMPLE_WASM);
+ renderWasmText(source.id, source.content.value);
+ expect(isWasm(source.id)).toEqual(true);
+ // clear shall remove
+ clearWasmStates();
+ expect(isWasm(source.id)).toEqual(false);
+ });
+ });
+
+ describe("renderWasmText", () => {
+ it("render simple wasm", () => {
+ const source = makeMockWasmSourceWithContent(SIMPLE_WASM);
+ const lines = renderWasmText(source.id, source.content.value);
+ expect(lines.join("\n")).toEqual(SIMPLE_WASM_TEXT);
+ clearWasmStates();
+ });
+
+ it("should return error information when the parser throws", () => {
+ const source = makeMockWasmSourceWithContent(MALFORMED_SIMPLE_WASM);
+ const lines = renderWasmText(source.id, source.content.value);
+ expect(lines.join("\n")).toEqual(
+ "Error occured during wast conversion : Unsupported element segment type 96"
+ );
+ clearWasmStates();
+ });
+ });
+
+ describe("lineToWasmOffset", () => {
+ // Test data sanity check: checking if 'nop' is found in the SIMPLE_WASM.
+ expect(SIMPLE_WASM.binary[SIMPLE_WASM_NOP_OFFSET]).toEqual("\x01");
+
+ it("get simple wasm nop offset", () => {
+ const source = makeMockWasmSourceWithContent(SIMPLE_WASM);
+ renderWasmText(source.id, source.content.value);
+ const offset = lineToWasmOffset(source.id, SIMPLE_WASM_NOP_TEXT_LINE);
+ expect(offset).toEqual(SIMPLE_WASM_NOP_OFFSET);
+ clearWasmStates();
+ });
+ });
+
+ describe("wasmOffsetToLine", () => {
+ it("get simple wasm nop line", () => {
+ const source = makeMockWasmSourceWithContent(SIMPLE_WASM);
+ renderWasmText(source.id, source.content.value);
+ const line = wasmOffsetToLine(source.id, SIMPLE_WASM_NOP_OFFSET);
+ expect(line).toEqual(SIMPLE_WASM_NOP_TEXT_LINE);
+ clearWasmStates();
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/utils/text.js b/devtools/client/debugger/src/utils/text.js
new file mode 100644
index 0000000000..19f99b3175
--- /dev/null
+++ b/devtools/client/debugger/src/utils/text.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/>. */
+
+/**
+ * Utils for keyboard command strings
+ * @module utils/text
+ */
+
+const isMacOS = Services.appinfo.OS === "Darwin";
+
+/**
+ * Formats key for use in tooltips
+ * For macOS we use the following unicode
+ *
+ * cmd ⌘ = \u2318
+ * shift ⇧ – \u21E7
+ * option (alt) ⌥ \u2325
+ *
+ * For Win/Lin this replaces CommandOrControl or CmdOrCtrl with Ctrl
+ *
+ * @memberof utils/text
+ * @static
+ */
+export function formatKeyShortcut(shortcut) {
+ if (isMacOS) {
+ return shortcut
+ .replace(/Shift\+/g, "\u21E7")
+ .replace(/Command\+|Cmd\+/g, "\u2318")
+ .replace(/CommandOrControl\+|CmdOrCtrl\+/g, "\u2318")
+ .replace(/Alt\+/g, "\u2325");
+ }
+ return shortcut
+ .replace(/CommandOrControl\+|CmdOrCtrl\+/g, `${L10N.getStr("ctrl")}+`)
+ .replace(/Shift\+/g, "Shift+");
+}
+
+/**
+ * Truncates the received text to the maxLength in the format:
+ * Original: 'this is a very long text and ends here'
+ * Truncated: 'this is a ver...and ends here'
+ * @param {String} sourceText - Source text
+ * @param {Number} maxLength - Max allowed length
+ * @memberof utils/text
+ * @static
+ */
+export function truncateMiddleText(sourceText, maxLength) {
+ let truncatedText = sourceText;
+ if (sourceText.length > maxLength) {
+ truncatedText = `${sourceText.substring(
+ 0,
+ Math.round(maxLength / 2) - 2
+ )}…${sourceText.substring(
+ sourceText.length - Math.round(maxLength / 2 - 1)
+ )}`;
+ }
+ return truncatedText;
+}
diff --git a/devtools/client/debugger/src/utils/ui.js b/devtools/client/debugger/src/utils/ui.js
new file mode 100644
index 0000000000..eab5bb1e07
--- /dev/null
+++ b/devtools/client/debugger/src/utils/ui.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/>. */
+
+/* Checks to see if the root element is available and
+ * if the element is visible. We check the width of the element
+ * because it is more reliable than either checking a focus state or
+ * the visibleState or hidden property.
+ */
+export function isVisible() {
+ const el = document.querySelector("#mount");
+ return !!(el && el.getBoundingClientRect().width > 0);
+}
+
+/* Gets the line numbers width in the code editor
+ */
+export function getLineNumberWidth(editor) {
+ const { gutters } = editor.display;
+ const lineNumbers = gutters.querySelector(".CodeMirror-linenumbers");
+ return lineNumbers?.clientWidth;
+}
+
+/**
+ * Forces the breakpoint gutter to be the same size as the line
+ * numbers gutter. Editor CSS will absolutely position the gutter
+ * beneath the line numbers. This makes it easy to be flexible with
+ * how we overlay breakpoints.
+ */
+export function resizeBreakpointGutter(editor) {
+ const { gutters } = editor.display;
+ const breakpoints = gutters.querySelector(".breakpoints");
+ if (breakpoints) {
+ breakpoints.style.width = `${getLineNumberWidth(editor)}px`;
+ }
+}
+
+/**
+ * Forces the left toggle button in source header to be the same size
+ * as the line numbers gutter.
+ */
+export function resizeToggleButton(editor) {
+ const toggleButton = document.querySelector(
+ ".source-header .toggle-button-start"
+ );
+ if (toggleButton) {
+ toggleButton.style.width = `${getLineNumberWidth(editor)}px`;
+ }
+}
diff --git a/devtools/client/debugger/src/utils/url.js b/devtools/client/debugger/src/utils/url.js
new file mode 100644
index 0000000000..2aabc31258
--- /dev/null
+++ b/devtools/client/debugger/src/utils/url.js
@@ -0,0 +1,75 @@
+/* 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/>. */
+
+const defaultUrl = {
+ hash: "",
+ host: "",
+ hostname: "",
+ href: "",
+ origin: "null",
+ password: "",
+ path: "",
+ pathname: "",
+ port: "",
+ protocol: "",
+ search: "",
+ // This should be a "URLSearchParams" object
+ searchParams: {},
+ username: "",
+};
+
+const parseCache = new Map();
+export function parse(url) {
+ if (parseCache.has(url)) {
+ return parseCache.get(url);
+ }
+
+ let urlObj;
+ try {
+ urlObj = new URL(url);
+ } catch (err) {
+ urlObj = { ...defaultUrl };
+ // If we're given simply a filename...
+ if (url) {
+ const hashStart = url.indexOf("#");
+ if (hashStart >= 0) {
+ urlObj.hash = url.slice(hashStart);
+ url = url.slice(0, hashStart);
+
+ if (urlObj.hash === "#") {
+ // The standard URL parser does not include the ? unless there are
+ // parameters included in the search value.
+ urlObj.hash = "";
+ }
+ }
+
+ const queryStart = url.indexOf("?");
+ if (queryStart >= 0) {
+ urlObj.search = url.slice(queryStart);
+ url = url.slice(0, queryStart);
+
+ if (urlObj.search === "?") {
+ // The standard URL parser does not include the ? unless there are
+ // parameters included in the search value.
+ urlObj.search = "";
+ }
+ }
+
+ urlObj.pathname = url;
+ }
+ }
+ // When provided a special URL like "webpack:///webpack/foo",
+ // prevents passing the three slashes in the path, and pass only onea.
+ // This will prevent displaying modules in empty-name sub folders.
+ urlObj.pathname = urlObj.pathname.replace(/\/+/, "/");
+ urlObj.path = urlObj.pathname + urlObj.search;
+
+ // Cache the result
+ parseCache.set(url, urlObj);
+ return urlObj;
+}
+
+export function sameOrigin(firstUrl, secondUrl) {
+ return parse(firstUrl).origin == parse(secondUrl).origin;
+}
diff --git a/devtools/client/debugger/src/utils/utils.js b/devtools/client/debugger/src/utils/utils.js
new file mode 100644
index 0000000000..7e3ed83032
--- /dev/null
+++ b/devtools/client/debugger/src/utils/utils.js
@@ -0,0 +1,59 @@
+/* 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/>. */
+
+const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
+
+/**
+ * Utils for utils, by utils
+ * @module utils/utils
+ */
+
+/**
+ * @memberof utils/utils
+ * @static
+ */
+export function handleError(err) {
+ console.log("ERROR: ", err);
+}
+
+/**
+ * @memberof utils/utils
+ * @static
+ */
+export function promisify(context, method, ...args) {
+ return new Promise((resolve, reject) => {
+ args.push(response => {
+ if (response.error) {
+ reject(response);
+ } else {
+ resolve(response);
+ }
+ });
+ method.apply(context, args);
+ });
+}
+
+/**
+ * @memberof utils/utils
+ * @static
+ */
+export function endTruncateStr(str, size) {
+ if (str.length > size) {
+ return `…${str.slice(str.length - size)}`;
+ }
+ return str;
+}
+
+export function waitForMs(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+export async function saveAsLocalFile(content, fileName) {
+ if (content.type !== "text") {
+ return null;
+ }
+
+ const data = new TextEncoder().encode(content.value);
+ return DevToolsUtils.saveAs(window, data, fileName);
+}
diff --git a/devtools/client/debugger/src/utils/wasm.js b/devtools/client/debugger/src/utils/wasm.js
new file mode 100644
index 0000000000..f2879af535
--- /dev/null
+++ b/devtools/client/debugger/src/utils/wasm.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 { BinaryReader } from "devtools/client/shared/vendor/WasmParser";
+import {
+ WasmDisassembler,
+ NameSectionReader,
+} from "devtools/client/shared/vendor/WasmDis";
+
+var wasmStates = Object.create(null);
+
+function maybeWasmSectionNameResolver(data) {
+ try {
+ const parser = new BinaryReader();
+ parser.setData(data.buffer, 0, data.length);
+ const reader = new NameSectionReader();
+ reader.read(parser);
+ return reader.hasValidNames() ? reader.getNameResolver() : null;
+ } catch (ex) {
+ // Ignoring any errors during names section retrival.
+ return null;
+ }
+}
+
+/**
+ * @memberof utils/wasm
+ * @static
+ */
+export function getWasmText(sourceId, data) {
+ const nameResolver = maybeWasmSectionNameResolver(data);
+ const parser = new BinaryReader();
+ let result;
+ parser.setData(data.buffer, 0, data.length);
+ const dis = new WasmDisassembler();
+ if (nameResolver) {
+ dis.nameResolver = nameResolver;
+ }
+ dis.addOffsets = true;
+ try {
+ const done = dis.disassembleChunk(parser);
+ result = dis.getResult();
+ if (result.lines.length === 0) {
+ result = { lines: ["No luck with wast conversion"], offsets: [0], done };
+ }
+ } catch (e) {
+ result = {
+ lines: [`Error occured during wast conversion : ${e.message}`],
+ offsets: [0],
+ done: null,
+ };
+ }
+
+ const { offsets } = result;
+ const lines = [];
+ for (let i = 0; i < offsets.length; i++) {
+ lines[offsets[i]] = i;
+ }
+
+ wasmStates[sourceId] = { offsets, lines };
+
+ return { lines: result.lines, done: result.done };
+}
+
+/**
+ * @memberof utils/wasm
+ * @static
+ */
+export function getWasmLineNumberFormatter(sourceId) {
+ const codeOf0 = 48,
+ codeOfA = 65;
+ const buffer = [
+ codeOf0,
+ codeOf0,
+ codeOf0,
+ codeOf0,
+ codeOf0,
+ codeOf0,
+ codeOf0,
+ codeOf0,
+ ];
+ let last0 = 7;
+ return function (number) {
+ const offset = lineToWasmOffset(sourceId, number - 1);
+ if (offset == undefined) {
+ return "";
+ }
+ let i = 7;
+ for (let n = offset; n !== 0 && i >= 0; n >>= 4, i--) {
+ const nibble = n & 15;
+ buffer[i] = nibble < 10 ? codeOf0 + nibble : codeOfA - 10 + nibble;
+ }
+ for (let j = i; j > last0; j--) {
+ buffer[j] = codeOf0;
+ }
+ last0 = i;
+ return String.fromCharCode.apply(null, buffer);
+ };
+}
+
+/**
+ * @memberof utils/wasm
+ * @static
+ */
+export function isWasm(sourceId) {
+ return sourceId in wasmStates;
+}
+
+/**
+ * @memberof utils/wasm
+ * @static
+ */
+export function lineToWasmOffset(sourceId, number) {
+ const wasmState = wasmStates[sourceId];
+ if (!wasmState) {
+ return undefined;
+ }
+ let offset = wasmState.offsets[number];
+ while (offset === undefined && number > 0) {
+ offset = wasmState.offsets[--number];
+ }
+ return offset;
+}
+
+/**
+ * @memberof utils/wasm
+ * @static
+ */
+export function wasmOffsetToLine(sourceId, offset) {
+ const wasmState = wasmStates[sourceId];
+ if (!wasmState) {
+ return undefined;
+ }
+ return wasmState.lines[offset];
+}
+
+/**
+ * @memberof utils/wasm
+ * @static
+ */
+export function clearWasmStates() {
+ wasmStates = Object.create(null);
+}
+
+const wasmLines = new WeakMap();
+export function renderWasmText(sourceId, content) {
+ if (wasmLines.has(content)) {
+ return wasmLines.get(content) || [];
+ }
+
+ // binary does not survive as Uint8Array, converting from string
+ const { binary } = content.value;
+ const data = new Uint8Array(binary.length);
+ for (let i = 0; i < data.length; i++) {
+ data[i] = binary.charCodeAt(i);
+ }
+ const { lines } = getWasmText(sourceId, data);
+ const MAX_LINES = 1000000;
+ if (lines.length > MAX_LINES) {
+ lines.splice(MAX_LINES, lines.length - MAX_LINES);
+ lines.push(";; .... text is truncated due to the size");
+ }
+
+ wasmLines.set(content, lines);
+ return lines;
+}
diff --git a/devtools/client/debugger/src/utils/worker.js b/devtools/client/debugger/src/utils/worker.js
new file mode 100644
index 0000000000..8cd7071466
--- /dev/null
+++ b/devtools/client/debugger/src/utils/worker.js
@@ -0,0 +1,49 @@
+/* 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/>. */
+
+let msgId = 1;
+/**
+ * @memberof utils/utils
+ * @static
+ */
+function workerTask(worker, method) {
+ return function (...args) {
+ return new Promise((resolve, reject) => {
+ const id = msgId++;
+ worker.postMessage({ id, method, args });
+
+ const listener = ({ data: result }) => {
+ if (result.id !== id) {
+ return;
+ }
+
+ worker.removeEventListener("message", listener);
+ if (result.error) {
+ reject(result.error);
+ } else {
+ resolve(result.response);
+ }
+ };
+
+ worker.addEventListener("message", listener);
+ });
+ };
+}
+
+function workerHandler(publicInterface) {
+ return function onTask(msg) {
+ const { id, method, args } = msg.data;
+ const response = publicInterface[method].apply(null, args);
+
+ if (response instanceof Promise) {
+ response
+ .then(val => self.postMessage({ id, response: val }))
+ .catch(error => self.postMessage({ id, error }));
+ } else {
+ self.postMessage({ id, response });
+ }
+ };
+}
+
+export { workerTask, workerHandler };
diff --git a/devtools/client/debugger/src/workers/moz.build b/devtools/client/debugger/src/workers/moz.build
new file mode 100644
index 0000000000..12327bf177
--- /dev/null
+++ b/devtools/client/debugger/src/workers/moz.build
@@ -0,0 +1,12 @@
+# 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 += [
+ "parser",
+ "pretty-print",
+ "search",
+]
+
+CompiledModules()
diff --git a/devtools/client/debugger/src/workers/parser/findBestMatchExpression.js b/devtools/client/debugger/src/workers/parser/findBestMatchExpression.js
new file mode 100644
index 0000000000..6fd5d5e70e
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/findBestMatchExpression.js
@@ -0,0 +1,37 @@
+/* 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 { getInternalSymbols } from "./getSymbols";
+
+function findBestMatchExpression(sourceId, tokenPos) {
+ const symbols = getInternalSymbols(sourceId);
+ if (!symbols) {
+ return null;
+ }
+
+ const { line, column } = tokenPos;
+ const { memberExpressions, identifiers, literals } = symbols;
+
+ function matchExpression(expression) {
+ const { location } = expression;
+ const { start, end } = location;
+ return start.line == line && start.column <= column && end.column >= column;
+ }
+ function matchMemberExpression(expression) {
+ // For member expressions we ignore "computed" member expressions `foo[bar]`,
+ // to only match the one that looks like: `foo.bar`.
+ return !expression.computed && matchExpression(expression);
+ }
+ // Avoid duplicating these arrays and be careful about performance as they can be large
+ //
+ // Process member expressions first as they can be including identifiers which
+ // are subset of the member expression.
+ // Ex: `foo.bar` is a member expression made of `foo` and `bar` identifiers.
+ return (
+ memberExpressions.find(matchMemberExpression) ||
+ literals.find(matchExpression) ||
+ identifiers.find(matchExpression)
+ );
+}
+export default findBestMatchExpression;
diff --git a/devtools/client/debugger/src/workers/parser/findOutOfScopeLocations.js b/devtools/client/debugger/src/workers/parser/findOutOfScopeLocations.js
new file mode 100644
index 0000000000..6c466965c2
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/findOutOfScopeLocations.js
@@ -0,0 +1,132 @@
+/* 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 { containsLocation, containsPosition } from "./utils/contains";
+
+import { getInternalSymbols } from "./getSymbols";
+
+function findSymbols(source) {
+ const { functions, comments } = getInternalSymbols(source);
+ return { functions, comments };
+}
+
+/**
+ * Returns the location for a given function path. If the path represents a
+ * function declaration, the location will begin after the function identifier
+ * but before the function parameters.
+ */
+
+function getLocation(func) {
+ const location = { ...func.location };
+
+ // if the function has an identifier, start the block after it so the
+ // identifier is included in the "scope" of its parent
+ const identifierEnd = func?.identifier?.loc?.end;
+ if (identifierEnd) {
+ location.start = identifierEnd;
+ }
+
+ return location;
+}
+
+/**
+ * Find the nearest location containing the input position and
+ * return inner locations under that nearest location
+ *
+ * @param {Array<Object>} locations Notice! The locations MUST be sorted by `sortByStart`
+ * so that we can do linear time complexity operation.
+ * @returns {Array<Object>}
+ */
+function getInnerLocations(locations, position) {
+ // First, let's find the nearest position-enclosing function location,
+ // which is to find the last location enclosing the position.
+ let parentIndex;
+ for (let i = locations.length - 1; i >= 0; i--) {
+ const loc = locations[i];
+ if (containsPosition(loc, position)) {
+ parentIndex = i;
+ break;
+ }
+ }
+
+ if (parentIndex == undefined) {
+ return [];
+ }
+ const parentLoc = locations[parentIndex];
+
+ // Then, from the nearest location, loop locations again and put locations into
+ // the innerLocations array until we get to a location not enclosed by the nearest location.
+ const innerLocations = [];
+ for (let i = parentIndex + 1; i < locations.length; i++) {
+ const loc = locations[i];
+ if (!containsLocation(parentLoc, loc)) {
+ break;
+ }
+
+ innerLocations.push(loc);
+ }
+
+ return innerLocations;
+}
+
+/**
+ * Return an new locations array which excludes
+ * items that are completely enclosed by another location in the input locations
+ *
+ * @param locations Notice! The locations MUST be sorted by `sortByStart`
+ * so that we can do linear time complexity operation.
+ */
+function removeOverlaps(locations) {
+ if (!locations.length) {
+ return [];
+ }
+ const firstParent = locations[0];
+ return locations.reduce(deduplicateNode, [firstParent]);
+}
+
+function deduplicateNode(nodes, location) {
+ const parent = nodes[nodes.length - 1];
+ if (!containsLocation(parent, location)) {
+ nodes.push(location);
+ }
+ return nodes;
+}
+
+/**
+ * Sorts an array of locations by start position
+ */
+function sortByStart(a, b) {
+ if (a.start.line < b.start.line) {
+ return -1;
+ } else if (a.start.line === b.start.line) {
+ return a.start.column - b.start.column;
+ }
+
+ return 1;
+}
+
+/**
+ * Returns an array of locations that are considered out of scope for the given
+ * location.
+ */
+function findOutOfScopeLocations(location) {
+ const { functions, comments } = findSymbols(location.source.id);
+ const commentLocations = comments.map(c => c.location);
+ const locations = functions
+ .map(getLocation)
+ .concat(commentLocations)
+ .sort(sortByStart);
+
+ const innerLocations = getInnerLocations(locations, location);
+ const outerLocations = locations.filter(loc => {
+ if (innerLocations.includes(loc)) {
+ return false;
+ }
+
+ return !containsPosition(loc, location);
+ });
+ return removeOverlaps(outerLocations);
+}
+
+export default findOutOfScopeLocations;
diff --git a/devtools/client/debugger/src/workers/parser/frameworks.js b/devtools/client/debugger/src/workers/parser/frameworks.js
new file mode 100644
index 0000000000..aacaa5186f
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/frameworks.js
@@ -0,0 +1,60 @@
+/* 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 * as t from "@babel/types";
+
+export function getFramework(symbols) {
+ if (isReactComponent(symbols)) {
+ return "React";
+ }
+ if (isAngularComponent(symbols)) {
+ return "Angular";
+ }
+ if (isVueComponent(symbols)) {
+ return "Vue";
+ }
+
+ return null;
+}
+
+function isReactComponent({ importsReact, classes, identifiers }) {
+ return (
+ importsReact ||
+ extendsReactComponent(classes) ||
+ isReact(identifiers) ||
+ isRedux(identifiers)
+ );
+}
+
+function extendsReactComponent(classes) {
+ return classes.some(
+ classObj =>
+ t.isIdentifier(classObj.parent, { name: "Component" }) ||
+ t.isIdentifier(classObj.parent, { name: "PureComponent" }) ||
+ (t.isMemberExpression(classObj.parent, { computed: false }) &&
+ t.isIdentifier(classObj.parent, { name: "Component" }))
+ );
+}
+
+function isAngularComponent({ memberExpressions }) {
+ return memberExpressions.some(
+ item =>
+ item.expression == "angular.controller" ||
+ item.expression == "angular.module"
+ );
+}
+
+function isVueComponent({ identifiers }) {
+ return identifiers.some(identifier => identifier.name == "Vue");
+}
+
+/* This identifies the react lib file */
+function isReact(identifiers) {
+ return identifiers.some(identifier => identifier.name == "isReactComponent");
+}
+
+/* This identifies the redux lib file */
+function isRedux(identifiers) {
+ return identifiers.some(identifier => identifier.name == "Redux");
+}
diff --git a/devtools/client/debugger/src/workers/parser/getScopes/index.js b/devtools/client/debugger/src/workers/parser/getScopes/index.js
new file mode 100644
index 0000000000..82631ab614
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/getScopes/index.js
@@ -0,0 +1,65 @@
+/* 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 { buildScopeList, parseSourceScopes } from "./visitor";
+
+const parsedScopesCache = new Map();
+
+export default function getScopes(location) {
+ const sourceId = location.source.id;
+ let parsedScopes = parsedScopesCache.get(sourceId);
+ if (!parsedScopes) {
+ parsedScopes = parseSourceScopes(sourceId);
+ parsedScopesCache.set(sourceId, parsedScopes);
+ }
+ return parsedScopes ? findScopes(parsedScopes, location) : [];
+}
+
+export function clearScopes(sourceIds) {
+ for (const sourceId of sourceIds) {
+ parsedScopesCache.delete(sourceId);
+ }
+}
+
+export { buildScopeList };
+
+/**
+ * Searches all scopes and their bindings at the specific location.
+ */
+function findScopes(scopes, location) {
+ // Find inner most in the tree structure.
+ let searchInScopes = scopes;
+ const found = [];
+ while (searchInScopes) {
+ const foundOne = searchInScopes.some(s => {
+ if (
+ compareLocations(s.start, location) <= 0 &&
+ compareLocations(location, s.end) < 0
+ ) {
+ // Found the next scope, trying to search recusevly in its children.
+ found.unshift(s);
+ searchInScopes = s.children;
+ return true;
+ }
+ return false;
+ });
+ if (!foundOne) {
+ break;
+ }
+ }
+ return found.map(i => ({
+ type: i.type,
+ scopeKind: i.scopeKind,
+ displayName: i.displayName,
+ start: i.start,
+ end: i.end,
+ bindings: i.bindings,
+ }));
+}
+
+function compareLocations(a, b) {
+ // According to type of Location.column can be undefined, if will not be the
+ // case here, ignoring flow error.
+ return a.line == b.line ? a.column - b.column : a.line - b.line;
+}
diff --git a/devtools/client/debugger/src/workers/parser/getScopes/visitor.js b/devtools/client/debugger/src/workers/parser/getScopes/visitor.js
new file mode 100644
index 0000000000..8046c7e89d
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/getScopes/visitor.js
@@ -0,0 +1,909 @@
+/* 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 * as t from "@babel/types";
+
+import getFunctionName from "../utils/getFunctionName";
+import { getAst } from "../utils/ast";
+
+/**
+ * "implicit"
+ * Variables added automaticly like "this" and "arguments"
+ *
+ * "var"
+ * Variables declared with "var" or non-block function declarations
+ *
+ * "let"
+ * Variables declared with "let".
+ *
+ * "const"
+ * Variables declared with "const", or added as const
+ * bindings like inner function expressions and inner class names.
+ *
+ * "import"
+ * Imported binding names exposed from other modules.
+ *
+ * "global"
+ * Variables that reference undeclared global values.
+ */
+
+// Location information about the expression immediartely surrounding a
+// given binding reference.
+
+function isGeneratedId(id) {
+ return !/\/originalSource/.test(id);
+}
+
+export function parseSourceScopes(sourceId) {
+ const ast = getAst(sourceId);
+ if (!ast || !Object.keys(ast).length) {
+ return null;
+ }
+
+ return buildScopeList(ast, sourceId);
+}
+
+export function buildScopeList(ast, sourceId) {
+ const { global, lexical } = createGlobalScope(ast, sourceId);
+
+ const state = {
+ // The id for the source that scope list is generated for
+ sourceId,
+
+ // A map of any free variables(variables which are used within the current scope but not
+ // declared within the scope). This changes when a new scope is created.
+ freeVariables: new Map(),
+
+ // A stack of all the free variables created across all the scopes that have
+ // been created.
+ freeVariableStack: [],
+
+ inType: null,
+
+ // The current scope, a new scope is potentially created on a visit to each node
+ // depending in the criteria. Initially set to the lexical global scope which is the
+ // child to the global scope.
+ scope: lexical,
+
+ // A stack of all the existing scopes, this is mainly used retrieve the parent scope
+ // (which is the last scope push onto the stack) on exiting a visited node.
+ scopeStack: [],
+
+ declarationBindingIds: new Set(),
+ };
+ t.traverse(ast, scopeCollectionVisitor, state);
+
+ for (const [key, freeVariables] of state.freeVariables) {
+ let binding = global.bindings[key];
+ if (!binding) {
+ binding = {
+ type: "global",
+ refs: [],
+ };
+ global.bindings[key] = binding;
+ }
+
+ binding.refs = freeVariables.concat(binding.refs);
+ }
+
+ // TODO: This should probably check for ".mjs" extension on the
+ // original file, and should also be skipped if the the generated
+ // code is an ES6 module rather than a script.
+ if (
+ isGeneratedId(sourceId) ||
+ (ast.program.sourceType === "script" && !looksLikeCommonJS(global))
+ ) {
+ stripModuleScope(global);
+ }
+
+ return toParsedScopes([global], sourceId) || [];
+}
+
+function toParsedScopes(children, sourceId) {
+ if (!children || children.length === 0) {
+ return undefined;
+ }
+ return children.map(scope => ({
+ // Removing unneed information from TempScope such as parent reference.
+ // We also need to convert BabelLocation to the Location type.
+ start: scope.loc.start,
+ end: scope.loc.end,
+ type:
+ scope.type === "module" || scope.type === "function-body"
+ ? "block"
+ : scope.type,
+ scopeKind: "",
+ displayName: scope.displayName,
+ bindings: scope.bindings,
+ children: toParsedScopes(scope.children, sourceId),
+ }));
+}
+
+/**
+ * Create a new scope object and link the scope to it parent.
+ *
+ * @param {String} type - scope type
+ * @param {String} displayName - The scope display name
+ * @param {Object} parent - The parent object scope
+ * @param {Object} loc - The start and end postions (line/columns) of the scope
+ * @returns {Object} The newly created scope
+ */
+function createTempScope(type, displayName, parent, loc) {
+ const scope = {
+ type,
+ displayName,
+ parent,
+
+ // A list of all the child scopes
+ children: [],
+ loc,
+
+ // All the bindings defined in this scope
+ // bindings = [binding, ...]
+ // binding = { type: "", refs: []}
+ bindings: Object.create(null),
+ };
+
+ if (parent) {
+ parent.children.push(scope);
+ }
+ return scope;
+}
+
+// Sets a new current scope and creates a new map to store the free variables
+// that may exist in this scope.
+function pushTempScope(state, type, displayName, loc) {
+ const scope = createTempScope(type, displayName, state.scope, loc);
+
+ state.scope = scope;
+
+ state.freeVariableStack.push(state.freeVariables);
+ state.freeVariables = new Map();
+ return scope;
+}
+
+function isNode(node, type) {
+ return node ? node.type === type : false;
+}
+
+// Walks up the scope tree to the top most variable scope
+function getVarScope(scope) {
+ let s = scope;
+ while (s.type !== "function" && s.type !== "module") {
+ if (!s.parent) {
+ return s;
+ }
+ s = s.parent;
+ }
+ return s;
+}
+
+function fromBabelLocation(location, sourceId) {
+ return {
+ sourceId,
+ line: location.line,
+ column: location.column,
+ };
+}
+
+function parseDeclarator(
+ declaratorId,
+ targetScope,
+ type,
+ locationType,
+ declaration,
+ state
+) {
+ if (isNode(declaratorId, "Identifier")) {
+ let existing = targetScope.bindings[declaratorId.name];
+ if (!existing) {
+ existing = {
+ type,
+ refs: [],
+ };
+ targetScope.bindings[declaratorId.name] = existing;
+ }
+ state.declarationBindingIds.add(declaratorId);
+ existing.refs.push({
+ type: locationType,
+ start: fromBabelLocation(declaratorId.loc.start, state.sourceId),
+ end: fromBabelLocation(declaratorId.loc.end, state.sourceId),
+ declaration: {
+ start: fromBabelLocation(declaration.loc.start, state.sourceId),
+ end: fromBabelLocation(declaration.loc.end, state.sourceId),
+ },
+ });
+ } else if (isNode(declaratorId, "ObjectPattern")) {
+ declaratorId.properties.forEach(prop => {
+ parseDeclarator(
+ prop.value,
+ targetScope,
+ type,
+ locationType,
+ declaration,
+ state
+ );
+ });
+ } else if (isNode(declaratorId, "ArrayPattern")) {
+ declaratorId.elements.forEach(item => {
+ parseDeclarator(
+ item,
+ targetScope,
+ type,
+ locationType,
+ declaration,
+ state
+ );
+ });
+ } else if (isNode(declaratorId, "AssignmentPattern")) {
+ parseDeclarator(
+ declaratorId.left,
+ targetScope,
+ type,
+ locationType,
+ declaration,
+ state
+ );
+ } else if (isNode(declaratorId, "RestElement")) {
+ parseDeclarator(
+ declaratorId.argument,
+ targetScope,
+ type,
+ locationType,
+ declaration,
+ state
+ );
+ } else if (t.isTSParameterProperty(declaratorId)) {
+ parseDeclarator(
+ declaratorId.parameter,
+ targetScope,
+ type,
+ locationType,
+ declaration,
+ state
+ );
+ }
+}
+
+function isLetOrConst(node) {
+ return node.kind === "let" || node.kind === "const";
+}
+
+function hasLexicalDeclaration(node, parent) {
+ const nodes = [];
+ if (t.isSwitchStatement(node)) {
+ for (const caseNode of node.cases) {
+ nodes.push(...caseNode.consequent);
+ }
+ } else {
+ nodes.push(...node.body);
+ }
+
+ const isFunctionBody = t.isFunction(parent, { body: node });
+
+ return nodes.some(
+ child =>
+ isLexicalVariable(child) ||
+ t.isClassDeclaration(child) ||
+ (!isFunctionBody && t.isFunctionDeclaration(child))
+ );
+}
+function isLexicalVariable(node) {
+ return isNode(node, "VariableDeclaration") && isLetOrConst(node);
+}
+
+// Creates the global scopes for this source, the overall global scope
+// and a lexical global scope.
+function createGlobalScope(ast, sourceId) {
+ const global = createTempScope("object", "Global", null, {
+ start: fromBabelLocation(ast.loc.start, sourceId),
+ end: fromBabelLocation(ast.loc.end, sourceId),
+ });
+
+ const lexical = createTempScope("block", "Lexical Global", global, {
+ start: fromBabelLocation(ast.loc.start, sourceId),
+ end: fromBabelLocation(ast.loc.end, sourceId),
+ });
+
+ return { global, lexical };
+}
+
+const scopeCollectionVisitor = {
+ // eslint-disable-next-line complexity
+ enter(node, ancestors, state) {
+ state.scopeStack.push(state.scope);
+
+ const parentNode =
+ ancestors.length === 0 ? null : ancestors[ancestors.length - 1].node;
+
+ if (state.inType) {
+ return;
+ }
+
+ if (t.isProgram(node)) {
+ const scope = pushTempScope(state, "module", "Module", {
+ start: fromBabelLocation(node.loc.start, state.sourceId),
+ end: fromBabelLocation(node.loc.end, state.sourceId),
+ });
+ scope.bindings.this = {
+ type: "implicit",
+ refs: [],
+ };
+ } else if (t.isFunction(node)) {
+ let { scope } = state;
+
+ if (t.isFunctionExpression(node) && isNode(node.id, "Identifier")) {
+ scope = pushTempScope(state, "block", "Function Expression", {
+ start: fromBabelLocation(node.loc.start, state.sourceId),
+ end: fromBabelLocation(node.loc.end, state.sourceId),
+ });
+ state.declarationBindingIds.add(node.id);
+ scope.bindings[node.id.name] = {
+ type: "const",
+ refs: [
+ {
+ type: "fn-expr",
+ start: fromBabelLocation(node.id.loc.start, state.sourceId),
+ end: fromBabelLocation(node.id.loc.end, state.sourceId),
+ declaration: {
+ start: fromBabelLocation(node.loc.start, state.sourceId),
+ end: fromBabelLocation(node.loc.end, state.sourceId),
+ },
+ },
+ ],
+ };
+ }
+
+ if (t.isFunctionDeclaration(node) && isNode(node.id, "Identifier")) {
+ // This ignores Annex B function declaration hoisting, which
+ // is probably a fine assumption.
+ state.declarationBindingIds.add(node.id);
+ const refs = [
+ {
+ type: "fn-decl",
+ start: fromBabelLocation(node.id.loc.start, state.sourceId),
+ end: fromBabelLocation(node.id.loc.end, state.sourceId),
+ declaration: {
+ start: fromBabelLocation(node.loc.start, state.sourceId),
+ end: fromBabelLocation(node.loc.end, state.sourceId),
+ },
+ },
+ ];
+
+ if (scope.type === "block") {
+ scope.bindings[node.id.name] = {
+ type: "let",
+ refs,
+ };
+ } else {
+ // Add the binding to the ancestor scope
+ getVarScope(scope).bindings[node.id.name] = {
+ type: "var",
+ refs,
+ };
+ }
+ }
+
+ scope = pushTempScope(
+ state,
+ "function",
+ getFunctionName(node, parentNode),
+ {
+ // Being at the start of a function doesn't count as
+ // being inside of it.
+ start: fromBabelLocation(
+ node.params[0] ? node.params[0].loc.start : node.loc.start,
+ state.sourceId
+ ),
+ end: fromBabelLocation(node.loc.end, state.sourceId),
+ }
+ );
+
+ node.params.forEach(param =>
+ parseDeclarator(param, scope, "var", "fn-param", node, state)
+ );
+
+ if (!t.isArrowFunctionExpression(node)) {
+ scope.bindings.this = {
+ type: "implicit",
+ refs: [],
+ };
+ scope.bindings.arguments = {
+ type: "implicit",
+ refs: [],
+ };
+ }
+
+ if (
+ t.isBlockStatement(node.body) &&
+ hasLexicalDeclaration(node.body, node)
+ ) {
+ scope = pushTempScope(state, "function-body", "Function Body", {
+ start: fromBabelLocation(node.body.loc.start, state.sourceId),
+ end: fromBabelLocation(node.body.loc.end, state.sourceId),
+ });
+ }
+ } else if (t.isClass(node)) {
+ if (t.isIdentifier(node.id)) {
+ // For decorated classes, the AST considers the first the decorator
+ // to be the start of the class. For the purposes of mapping class
+ // declarations however, we really want to look for the "class Foo"
+ // piece. To achieve that, we estimate the location of the declaration
+ // instead.
+ let declStart = node.loc.start;
+ if (node.decorators && node.decorators.length) {
+ // Estimate the location of the "class" keyword since it
+ // is unlikely to be a different line than the class name.
+ declStart = {
+ line: node.id.loc.start.line,
+ column: node.id.loc.start.column - "class ".length,
+ };
+ }
+
+ const declaration = {
+ start: fromBabelLocation(declStart, state.sourceId),
+ end: fromBabelLocation(node.loc.end, state.sourceId),
+ };
+
+ if (t.isClassDeclaration(node)) {
+ state.declarationBindingIds.add(node.id);
+ state.scope.bindings[node.id.name] = {
+ type: "let",
+ refs: [
+ {
+ type: "class-decl",
+ start: fromBabelLocation(node.id.loc.start, state.sourceId),
+ end: fromBabelLocation(node.id.loc.end, state.sourceId),
+ declaration,
+ },
+ ],
+ };
+ }
+
+ const scope = pushTempScope(state, "block", "Class", {
+ start: fromBabelLocation(node.loc.start, state.sourceId),
+ end: fromBabelLocation(node.loc.end, state.sourceId),
+ });
+
+ state.declarationBindingIds.add(node.id);
+ scope.bindings[node.id.name] = {
+ type: "const",
+ refs: [
+ {
+ type: "class-inner",
+ start: fromBabelLocation(node.id.loc.start, state.sourceId),
+ end: fromBabelLocation(node.id.loc.end, state.sourceId),
+ declaration,
+ },
+ ],
+ };
+ }
+ } else if (t.isForXStatement(node) || t.isForStatement(node)) {
+ const init = node.init || node.left;
+ if (isNode(init, "VariableDeclaration") && isLetOrConst(init)) {
+ // Debugger will create new lexical environment for the for.
+ pushTempScope(state, "block", "For", {
+ // Being at the start of a for loop doesn't count as
+ // being inside it.
+ start: fromBabelLocation(init.loc.start, state.sourceId),
+ end: fromBabelLocation(node.loc.end, state.sourceId),
+ });
+ }
+ } else if (t.isCatchClause(node)) {
+ const scope = pushTempScope(state, "block", "Catch", {
+ start: fromBabelLocation(node.loc.start, state.sourceId),
+ end: fromBabelLocation(node.loc.end, state.sourceId),
+ });
+ parseDeclarator(node.param, scope, "var", "catch", node, state);
+ } else if (
+ t.isBlockStatement(node) &&
+ // Function body's are handled in the function logic above.
+ !t.isFunction(parentNode) &&
+ hasLexicalDeclaration(node, parentNode)
+ ) {
+ // Debugger will create new lexical environment for the block.
+ pushTempScope(state, "block", "Block", {
+ start: fromBabelLocation(node.loc.start, state.sourceId),
+ end: fromBabelLocation(node.loc.end, state.sourceId),
+ });
+ } else if (
+ t.isVariableDeclaration(node) &&
+ (node.kind === "var" ||
+ // Lexical declarations in for statements are handled above.
+ !t.isForStatement(parentNode, { init: node }) ||
+ !t.isForXStatement(parentNode, { left: node }))
+ ) {
+ // Finds right lexical environment
+ const hoistAt = !isLetOrConst(node)
+ ? getVarScope(state.scope)
+ : state.scope;
+ node.declarations.forEach(declarator => {
+ parseDeclarator(
+ declarator.id,
+ hoistAt,
+ node.kind,
+ node.kind,
+ node,
+ state
+ );
+ });
+ } else if (
+ t.isImportDeclaration(node) &&
+ (!node.importKind || node.importKind === "value")
+ ) {
+ node.specifiers.forEach(spec => {
+ if (spec.importKind && spec.importKind !== "value") {
+ return;
+ }
+
+ if (t.isImportNamespaceSpecifier(spec)) {
+ state.declarationBindingIds.add(spec.local);
+
+ state.scope.bindings[spec.local.name] = {
+ // Imported namespaces aren't live import bindings, they are
+ // just normal const bindings.
+ type: "const",
+ refs: [
+ {
+ type: "import-ns-decl",
+ start: fromBabelLocation(spec.local.loc.start, state.sourceId),
+ end: fromBabelLocation(spec.local.loc.end, state.sourceId),
+ declaration: {
+ start: fromBabelLocation(node.loc.start, state.sourceId),
+ end: fromBabelLocation(node.loc.end, state.sourceId),
+ },
+ },
+ ],
+ };
+ } else {
+ state.declarationBindingIds.add(spec.local);
+
+ state.scope.bindings[spec.local.name] = {
+ type: "import",
+ refs: [
+ {
+ type: "import-decl",
+ start: fromBabelLocation(spec.local.loc.start, state.sourceId),
+ end: fromBabelLocation(spec.local.loc.end, state.sourceId),
+ importName: t.isImportDefaultSpecifier(spec)
+ ? "default"
+ : spec.imported.name,
+ declaration: {
+ start: fromBabelLocation(node.loc.start, state.sourceId),
+ end: fromBabelLocation(node.loc.end, state.sourceId),
+ },
+ },
+ ],
+ };
+ }
+ });
+ } else if (t.isTSEnumDeclaration(node)) {
+ state.declarationBindingIds.add(node.id);
+ state.scope.bindings[node.id.name] = {
+ type: "const",
+ refs: [
+ {
+ type: "ts-enum-decl",
+ start: fromBabelLocation(node.id.loc.start, state.sourceId),
+ end: fromBabelLocation(node.id.loc.end, state.sourceId),
+ declaration: {
+ start: fromBabelLocation(node.loc.start, state.sourceId),
+ end: fromBabelLocation(node.loc.end, state.sourceId),
+ },
+ },
+ ],
+ };
+ } else if (t.isTSModuleDeclaration(node)) {
+ state.declarationBindingIds.add(node.id);
+ state.scope.bindings[node.id.name] = {
+ type: "const",
+ refs: [
+ {
+ type: "ts-namespace-decl",
+ start: fromBabelLocation(node.id.loc.start, state.sourceId),
+ end: fromBabelLocation(node.id.loc.end, state.sourceId),
+ declaration: {
+ start: fromBabelLocation(node.loc.start, state.sourceId),
+ end: fromBabelLocation(node.loc.end, state.sourceId),
+ },
+ },
+ ],
+ };
+ } else if (t.isTSModuleBlock(node)) {
+ pushTempScope(state, "block", "TypeScript Namespace", {
+ start: fromBabelLocation(node.loc.start, state.sourceId),
+ end: fromBabelLocation(node.loc.end, state.sourceId),
+ });
+ } else if (
+ t.isIdentifier(node) &&
+ t.isReferenced(node, parentNode) &&
+ // Babel doesn't cover this in 'isReferenced' yet, but it should
+ // eventually.
+ !t.isTSEnumMember(parentNode, { id: node }) &&
+ !t.isTSModuleDeclaration(parentNode, { id: node }) &&
+ // isReferenced above fails to see `var { foo } = ...` as a non-reference
+ // because the direct parent is not enough to know that the pattern is
+ // used within a variable declaration.
+ !state.declarationBindingIds.has(node)
+ ) {
+ let freeVariables = state.freeVariables.get(node.name);
+ if (!freeVariables) {
+ freeVariables = [];
+ state.freeVariables.set(node.name, freeVariables);
+ }
+
+ freeVariables.push({
+ type: "ref",
+ start: fromBabelLocation(node.loc.start, state.sourceId),
+ end: fromBabelLocation(node.loc.end, state.sourceId),
+ meta: buildMetaBindings(state.sourceId, node, ancestors),
+ });
+ } else if (isOpeningJSXIdentifier(node, ancestors)) {
+ let freeVariables = state.freeVariables.get(node.name);
+ if (!freeVariables) {
+ freeVariables = [];
+ state.freeVariables.set(node.name, freeVariables);
+ }
+
+ freeVariables.push({
+ type: "ref",
+ start: fromBabelLocation(node.loc.start, state.sourceId),
+ end: fromBabelLocation(node.loc.end, state.sourceId),
+ meta: buildMetaBindings(state.sourceId, node, ancestors),
+ });
+ } else if (t.isThisExpression(node)) {
+ let freeVariables = state.freeVariables.get("this");
+ if (!freeVariables) {
+ freeVariables = [];
+ state.freeVariables.set("this", freeVariables);
+ }
+
+ freeVariables.push({
+ type: "ref",
+ start: fromBabelLocation(node.loc.start, state.sourceId),
+ end: fromBabelLocation(node.loc.end, state.sourceId),
+ meta: buildMetaBindings(state.sourceId, node, ancestors),
+ });
+ } else if (t.isClassProperty(parentNode, { value: node })) {
+ const scope = pushTempScope(state, "function", "Class Field", {
+ start: fromBabelLocation(node.loc.start, state.sourceId),
+ end: fromBabelLocation(node.loc.end, state.sourceId),
+ });
+ scope.bindings.this = {
+ type: "implicit",
+ refs: [],
+ };
+ scope.bindings.arguments = {
+ type: "implicit",
+ refs: [],
+ };
+ } else if (
+ t.isSwitchStatement(node) &&
+ hasLexicalDeclaration(node, parentNode)
+ ) {
+ pushTempScope(state, "block", "Switch", {
+ start: fromBabelLocation(node.loc.start, state.sourceId),
+ end: fromBabelLocation(node.loc.end, state.sourceId),
+ });
+ }
+
+ if (
+ // In general Flow expressions are deleted, so they can't contain
+ // runtime bindings, but typecasts are the one exception there.
+ (t.isFlow(node) && !t.isTypeCastExpression(node)) ||
+ // In general TS items are deleted, but TS has a few wrapper node
+ // types that can contain general JS expressions.
+ (node.type.startsWith("TS") &&
+ !t.isTSTypeAssertion(node) &&
+ !t.isTSAsExpression(node) &&
+ !t.isTSNonNullExpression(node) &&
+ !t.isTSModuleDeclaration(node) &&
+ !t.isTSModuleBlock(node) &&
+ !t.isTSParameterProperty(node) &&
+ !t.isTSExportAssignment(node))
+ ) {
+ // Flag this node as a root "type" node. All items inside of this
+ // will be skipped entirely.
+ state.inType = node;
+ }
+ },
+ exit(node, ancestors, state) {
+ const currentScope = state.scope;
+ const parentScope = state.scopeStack.pop();
+ if (!parentScope) {
+ throw new Error("Assertion failure - unsynchronized pop");
+ }
+ state.scope = parentScope;
+
+ // It is possible, as in the case of function expressions, that a single
+ // node has added multiple scopes, so we need to traverse upward here
+ // rather than jumping stright to 'parentScope'.
+ for (
+ let scope = currentScope;
+ scope && scope !== parentScope;
+ scope = scope.parent
+ ) {
+ const { freeVariables } = state;
+ state.freeVariables = state.freeVariableStack.pop();
+ const parentFreeVariables = state.freeVariables;
+
+ // Match up any free variables that match this scope's bindings and
+ // merge then into the refs.
+ for (const key of Object.keys(scope.bindings)) {
+ const binding = scope.bindings[key];
+
+ const freeVars = freeVariables.get(key);
+ if (freeVars) {
+ binding.refs.push(...freeVars);
+ freeVariables.delete(key);
+ }
+ }
+
+ // Move any undeclared references in this scope into the parent for
+ // processing in higher scopes.
+ for (const [key, value] of freeVariables) {
+ let refs = parentFreeVariables.get(key);
+ if (!refs) {
+ refs = [];
+ parentFreeVariables.set(key, refs);
+ }
+
+ refs.push(...value);
+ }
+ }
+
+ if (state.inType === node) {
+ state.inType = null;
+ }
+ },
+};
+
+function isOpeningJSXIdentifier(node, ancestors) {
+ if (!t.isJSXIdentifier(node)) {
+ return false;
+ }
+
+ for (let i = ancestors.length - 1; i >= 0; i--) {
+ const { node: parent, key } = ancestors[i];
+
+ if (t.isJSXOpeningElement(parent) && key === "name") {
+ return true;
+ } else if (!t.isJSXMemberExpression(parent) || key !== "object") {
+ break;
+ }
+ }
+
+ return false;
+}
+
+function buildMetaBindings(
+ sourceId,
+ node,
+ ancestors,
+ parentIndex = ancestors.length - 1
+) {
+ if (parentIndex <= 1) {
+ return null;
+ }
+ const parent = ancestors[parentIndex].node;
+ const grandparent = ancestors[parentIndex - 1].node;
+
+ // Consider "0, foo" to be equivalent to "foo".
+ if (
+ t.isSequenceExpression(parent) &&
+ parent.expressions.length === 2 &&
+ t.isNumericLiteral(parent.expressions[0]) &&
+ parent.expressions[1] === node
+ ) {
+ let { start, end } = parent.loc;
+
+ if (t.isCallExpression(grandparent, { callee: parent })) {
+ // Attempt to expand the range around parentheses, e.g.
+ // (0, foo.bar)()
+ start = grandparent.loc.start;
+ end = Object.assign({}, end);
+ end.column += 1;
+ }
+
+ return {
+ type: "inherit",
+ start: fromBabelLocation(start, sourceId),
+ end: fromBabelLocation(end, sourceId),
+ parent: buildMetaBindings(sourceId, parent, ancestors, parentIndex - 1),
+ };
+ }
+
+ // Consider "Object(foo)", and "__webpack_require__.i(foo)" to be
+ // equivalent to "foo" since they are essentially identity functions.
+ if (
+ t.isCallExpression(parent) &&
+ (t.isIdentifier(parent.callee, { name: "Object" }) ||
+ (t.isMemberExpression(parent.callee, { computed: false }) &&
+ t.isIdentifier(parent.callee.object, { name: "__webpack_require__" }) &&
+ t.isIdentifier(parent.callee.property, { name: "i" }))) &&
+ parent.arguments.length === 1 &&
+ parent.arguments[0] === node
+ ) {
+ return {
+ type: "inherit",
+ start: fromBabelLocation(parent.loc.start, sourceId),
+ end: fromBabelLocation(parent.loc.end, sourceId),
+ parent: buildMetaBindings(sourceId, parent, ancestors, parentIndex - 1),
+ };
+ }
+
+ if (t.isMemberExpression(parent, { object: node })) {
+ if (parent.computed) {
+ if (t.isStringLiteral(parent.property)) {
+ return {
+ type: "member",
+ start: fromBabelLocation(parent.loc.start, sourceId),
+ end: fromBabelLocation(parent.loc.end, sourceId),
+ property: parent.property.value,
+ parent: buildMetaBindings(
+ sourceId,
+ parent,
+ ancestors,
+ parentIndex - 1
+ ),
+ };
+ }
+ } else {
+ return {
+ type: "member",
+ start: fromBabelLocation(parent.loc.start, sourceId),
+ end: fromBabelLocation(parent.loc.end, sourceId),
+ property: parent.property.name,
+ parent: buildMetaBindings(sourceId, parent, ancestors, parentIndex - 1),
+ };
+ }
+ }
+ if (
+ t.isCallExpression(parent, { callee: node }) &&
+ !parent.arguments.length
+ ) {
+ return {
+ type: "call",
+ start: fromBabelLocation(parent.loc.start, sourceId),
+ end: fromBabelLocation(parent.loc.end, sourceId),
+ parent: buildMetaBindings(sourceId, parent, ancestors, parentIndex - 1),
+ };
+ }
+
+ return null;
+}
+
+function looksLikeCommonJS(rootScope) {
+ const hasRefs = name =>
+ rootScope.bindings[name] && !!rootScope.bindings[name].refs.length;
+
+ return (
+ hasRefs("__dirname") ||
+ hasRefs("__filename") ||
+ hasRefs("require") ||
+ hasRefs("exports") ||
+ hasRefs("module")
+ );
+}
+
+function stripModuleScope(rootScope) {
+ const rootLexicalScope = rootScope.children[0];
+ const moduleScope = rootLexicalScope.children[0];
+ if (moduleScope.type !== "module") {
+ throw new Error("Assertion failure - should be module");
+ }
+
+ Object.keys(moduleScope.bindings).forEach(name => {
+ const binding = moduleScope.bindings[name];
+ if (binding.type === "let" || binding.type === "const") {
+ rootLexicalScope.bindings[name] = binding;
+ } else {
+ rootScope.bindings[name] = binding;
+ }
+ });
+ rootLexicalScope.children = moduleScope.children;
+ rootLexicalScope.children.forEach(child => {
+ child.parent = rootLexicalScope;
+ });
+}
diff --git a/devtools/client/debugger/src/workers/parser/getSymbols.js b/devtools/client/debugger/src/workers/parser/getSymbols.js
new file mode 100644
index 0000000000..fb288adcfb
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/getSymbols.js
@@ -0,0 +1,567 @@
+/* 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 * as t from "@babel/types";
+
+import createSimplePath from "./utils/simple-path";
+import { traverseAst } from "./utils/ast";
+import {
+ isFunction,
+ isObjectShorthand,
+ isComputedExpression,
+ getObjectExpressionValue,
+ addPatternIdentifiers,
+ getComments,
+ getCode,
+ nodeLocationKey,
+ getFunctionParameterNames,
+} from "./utils/helpers";
+
+import { inferClassName } from "./utils/inferClassName";
+import getFunctionName from "./utils/getFunctionName";
+import { getFramework } from "./frameworks";
+
+const symbolDeclarations = new Map();
+
+function extractFunctionSymbol(path, state, symbols) {
+ const name = getFunctionName(path.node, path.parent);
+
+ if (!state.fnCounts[name]) {
+ state.fnCounts[name] = 0;
+ }
+ const index = state.fnCounts[name]++;
+ symbols.functions.push({
+ name,
+ klass: inferClassName(path),
+ location: path.node.loc,
+ parameterNames: getFunctionParameterNames(path),
+ identifier: path.node.id,
+ // indicates the occurence of the function in a file
+ // e.g { name: foo, ... index: 4 } is the 4th foo function
+ // in the file
+ index,
+ });
+}
+
+function extractSymbol(path, symbols, state) {
+ if (isFunction(path)) {
+ extractFunctionSymbol(path, state, symbols);
+ }
+
+ if (t.isJSXElement(path)) {
+ symbols.hasJsx = true;
+ }
+
+ if (t.isGenericTypeAnnotation(path)) {
+ symbols.hasTypes = true;
+ }
+
+ if (t.isClassDeclaration(path)) {
+ symbols.classes.push(getClassDeclarationSymbol(path.node));
+ }
+
+ if (!symbols.importsReact) {
+ if (t.isImportDeclaration(path)) {
+ symbols.importsReact = isReactImport(path.node);
+ } else if (t.isCallExpression(path)) {
+ symbols.importsReact = isReactRequire(path.node);
+ }
+ }
+
+ if (t.isMemberExpression(path) || t.isOptionalMemberExpression(path)) {
+ symbols.memberExpressions.push(getMemberExpressionSymbol(path));
+ }
+
+ if (
+ (t.isStringLiteral(path) || t.isNumericLiteral(path)) &&
+ t.isMemberExpression(path.parentPath)
+ ) {
+ // We only need literals that are part of computed memeber expressions
+ const { start, end } = path.node.loc;
+ symbols.literals.push({
+ location: { start, end },
+ get expression() {
+ delete this.expression;
+ this.expression = getSnippet(path.parentPath);
+ return this.expression;
+ },
+ });
+ }
+
+ getIdentifierSymbols(symbols.identifiers, symbols.identifiersKeys, path);
+}
+
+function extractSymbols(sourceId) {
+ const symbols = {
+ functions: [],
+ memberExpressions: [],
+ comments: [],
+ identifiers: [],
+ // This holds a set of unique identifier location key (string)
+ // It helps registering only the first identifier when there is duplicated ones for the same location.
+ identifiersKeys: new Set(),
+ classes: [],
+ literals: [],
+ hasJsx: false,
+ hasTypes: false,
+ framework: undefined,
+ importsReact: false,
+ };
+
+ const state = {
+ fnCounts: Object.create(null),
+ };
+
+ const ast = traverseAst(sourceId, {
+ enter(node, ancestors) {
+ try {
+ const path = createSimplePath(ancestors);
+ if (path) {
+ extractSymbol(path, symbols, state);
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ },
+ });
+
+ // comments are extracted separately from the AST
+ symbols.comments = getComments(ast);
+ symbols.framework = getFramework(symbols);
+
+ return symbols;
+}
+
+function extendSnippet(name, expression, path, prevPath) {
+ const computed = path?.node.computed;
+ const optional = path?.node.optional;
+ const prevComputed = prevPath?.node.computed;
+ const prevArray = t.isArrayExpression(prevPath);
+ const array = t.isArrayExpression(path);
+ const value = path?.node.property?.extra?.raw || "";
+
+ if (expression === "") {
+ if (computed) {
+ return name === undefined ? `[${value}]` : `[${name}]`;
+ }
+ return name;
+ }
+
+ if (computed || array) {
+ if (prevComputed || prevArray) {
+ return `[${name}]${expression}`;
+ }
+ return `[${name === undefined ? value : name}].${expression}`;
+ }
+
+ if (prevComputed || prevArray) {
+ return `${name}${expression}`;
+ }
+
+ if (isComputedExpression(expression) && name !== undefined) {
+ return `${name}${expression}`;
+ }
+
+ if (optional) {
+ return `${name}?.${expression}`;
+ }
+
+ return `${name}.${expression}`;
+}
+
+function getMemberSnippet(node, expression = "", optional = false) {
+ if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
+ const name = t.isPrivateName(node.property)
+ ? `#${node.property.id.name}`
+ : node.property.name;
+ const snippet = getMemberSnippet(
+ node.object,
+ extendSnippet(name, expression, { node }),
+ node.optional
+ );
+ return snippet;
+ }
+
+ if (t.isCallExpression(node)) {
+ return "";
+ }
+
+ if (t.isThisExpression(node)) {
+ return `this.${expression}`;
+ }
+
+ if (t.isIdentifier(node)) {
+ if (isComputedExpression(expression)) {
+ return `${node.name}${expression}`;
+ }
+ if (optional) {
+ return `${node.name}?.${expression}`;
+ }
+ return `${node.name}.${expression}`;
+ }
+
+ return expression;
+}
+
+function getObjectSnippet(path, prevPath, expression = "") {
+ if (!path) {
+ return expression;
+ }
+
+ const { name } = path.node.key;
+
+ const extendedExpression = extendSnippet(name, expression, path, prevPath);
+
+ const nextPrevPath = path;
+ const nextPath = path.parentPath && path.parentPath.parentPath;
+
+ return getSnippet(nextPath, nextPrevPath, extendedExpression);
+}
+
+function getArraySnippet(path, prevPath, expression) {
+ if (!prevPath.parentPath) {
+ throw new Error("Assertion failure - path should exist");
+ }
+
+ const index = `${prevPath.parentPath.containerIndex}`;
+ const extendedExpression = extendSnippet(index, expression, path, prevPath);
+
+ const nextPrevPath = path;
+ const nextPath = path.parentPath && path.parentPath.parentPath;
+
+ return getSnippet(nextPath, nextPrevPath, extendedExpression);
+}
+
+function getSnippet(path, prevPath, expression = "") {
+ if (!path) {
+ return expression;
+ }
+
+ if (t.isVariableDeclaration(path)) {
+ const node = path.node.declarations[0];
+ const { name } = node.id;
+ return extendSnippet(name, expression, path, prevPath);
+ }
+
+ if (t.isVariableDeclarator(path)) {
+ const node = path.node.id;
+ if (t.isObjectPattern(node)) {
+ return expression;
+ }
+
+ const prop = extendSnippet(node.name, expression, path, prevPath);
+ return prop;
+ }
+
+ if (t.isAssignmentExpression(path)) {
+ const node = path.node.left;
+ const name = t.isMemberExpression(node)
+ ? getMemberSnippet(node)
+ : node.name;
+
+ const prop = extendSnippet(name, expression, path, prevPath);
+ return prop;
+ }
+
+ if (isFunction(path)) {
+ return expression;
+ }
+
+ if (t.isIdentifier(path)) {
+ return `${path.node.name}.${expression}`;
+ }
+
+ if (t.isObjectProperty(path)) {
+ return getObjectSnippet(path, prevPath, expression);
+ }
+
+ if (t.isObjectExpression(path)) {
+ const parentPath = prevPath?.parentPath;
+ return getObjectSnippet(parentPath, prevPath, expression);
+ }
+
+ if (t.isMemberExpression(path) || t.isOptionalMemberExpression(path)) {
+ return getMemberSnippet(path.node, expression);
+ }
+
+ if (t.isArrayExpression(path)) {
+ if (!prevPath) {
+ throw new Error("Assertion failure - path should exist");
+ }
+
+ return getArraySnippet(path, prevPath, expression);
+ }
+
+ return "";
+}
+
+export function clearSymbols(sourceIds) {
+ for (const sourceId of sourceIds) {
+ symbolDeclarations.delete(sourceId);
+ }
+}
+
+export function getInternalSymbols(sourceId) {
+ if (symbolDeclarations.has(sourceId)) {
+ const symbols = symbolDeclarations.get(sourceId);
+ if (symbols) {
+ return symbols;
+ }
+ }
+
+ const symbols = extractSymbols(sourceId);
+
+ symbolDeclarations.set(sourceId, symbols);
+ return symbols;
+}
+
+export function getFunctionSymbols(sourceId, maxResults) {
+ const symbols = getInternalSymbols(sourceId);
+ if (!symbols) {
+ return [];
+ }
+ let { functions } = symbols;
+ // Avoid transferring more symbols than necessary
+ if (maxResults && functions.length > maxResults) {
+ functions = functions.slice(0, maxResults);
+ }
+ // The Outline & the Quick open panels do not need anonymous functions
+ return functions.filter(fn => fn.name !== "anonymous");
+}
+
+export function getClassSymbols(sourceId) {
+ const symbols = getInternalSymbols(sourceId);
+ if (!symbols) {
+ return [];
+ }
+
+ return symbols.classes;
+}
+
+function containsPosition(a, b) {
+ const bColumn = b.column || 0;
+ const startsBefore =
+ a.start.line < b.line ||
+ (a.start.line === b.line && a.start.column <= bColumn);
+ const endsAfter =
+ a.end.line > b.line || (a.end.line === b.line && a.end.column >= bColumn);
+
+ return startsBefore && endsAfter;
+}
+
+export function getClosestFunctionName(location) {
+ const symbols = getInternalSymbols(location.source.id);
+ if (!symbols || !symbols.functions) {
+ return "";
+ }
+
+ const closestFunction = symbols.functions.reduce((found, currNode) => {
+ if (
+ currNode.name === "anonymous" ||
+ !containsPosition(currNode.location, {
+ line: location.line,
+ column: location.column || 0,
+ })
+ ) {
+ return found;
+ }
+
+ if (!found) {
+ return currNode;
+ }
+
+ if (found.location.start.line > currNode.location.start.line) {
+ return found;
+ }
+ if (
+ found.location.start.line === currNode.location.start.line &&
+ found.location.start.column > currNode.location.start.column
+ ) {
+ return found;
+ }
+
+ return currNode;
+ }, null);
+
+ if (!closestFunction) {
+ return "";
+ }
+ return closestFunction.name;
+}
+
+// This is only called from the main thread and we return a subset of attributes
+export function getSymbols(sourceId) {
+ const symbols = getInternalSymbols(sourceId);
+ return {
+ // This is used in the main thread by:
+ // * The `getFunctionSymbols` function which is used by the Outline, QuickOpen panels.
+ // * The `getClosestFunctionName` function used in the mapping of frame function names.
+ // * The `findOutOfScopeLocations` function use to determine in scope lines.
+ // functions: symbols.functions,
+
+ // The three following attributes are only used by `findBestMatchExpression` within the worker thread
+ // `memberExpressions`, `literals`
+ // This one is also used within the worker for framework computation
+ // `identifiers`
+ //
+ // These three memberExpressions, literals and identifiers attributes are arrays containing objects whose attributes are:
+ // * name: string
+ // * location: object {start: number, end: number}
+ // * expression: string
+ // * computed: boolean (only for memberExpressions)
+ //
+ // `findBestMatchExpression` uses `location`, `computed` and `expression` (not name).
+ // `expression` isn't used from the worker thread implementation of `findBestMatchExpression`.
+ // The main thread only uses `expression` and `location`.
+ // framework computation uses only:
+ // * `name` for identifiers
+ // * `expression` for memberExpression
+
+ // This is used within the worker for framework computation,
+ // and in the `getClassSymbols` function
+ // `classes`
+
+ // The two following are only used by the main thread for computing CodeMirror "mode"
+ hasJsx: symbols.hasJsx,
+ hasTypes: symbols.hasTypes,
+
+ // This is used in the main thread only to compute the source icon
+ framework: symbols.framework,
+
+ // This is only used by `findOutOfScopeLocations`:
+ // `comments`
+ };
+}
+
+function getMemberExpressionSymbol(path) {
+ const { start, end } = path.node.property.loc;
+ return {
+ location: { start, end },
+ get expression() {
+ delete this.expression;
+ this.expression = getSnippet(path);
+ return this.expression;
+ },
+ computed: path.node.computed,
+ };
+}
+
+function isReactImport(node) {
+ return (
+ node.source.value == "react" &&
+ node.specifiers?.some(specifier => specifier.local?.name == "React")
+ );
+}
+
+function isReactRequire(node) {
+ const { callee } = node;
+ const name = t.isMemberExpression(callee)
+ ? callee.property.name
+ : callee.loc.identifierName;
+ return name == "require" && node.arguments.some(arg => arg.value == "react");
+}
+
+function getClassParentName(superClass) {
+ return t.isMemberExpression(superClass)
+ ? getCode(superClass)
+ : superClass.name;
+}
+
+function getClassParentSymbol(superClass) {
+ if (!superClass) {
+ return null;
+ }
+ return {
+ name: getClassParentName(superClass),
+ location: superClass.loc,
+ };
+}
+
+function getClassDeclarationSymbol(node) {
+ const { loc, superClass } = node;
+ return {
+ name: node.id.name,
+ parent: getClassParentSymbol(superClass),
+ location: loc,
+ };
+}
+
+/**
+ * Get a list of identifiers that are part of the given path.
+ *
+ * @param {Array.<Object>} identifiers
+ * the current list of identifiers where to push the new identifiers
+ * related to this path.
+ * @param {Set<String>} identifiersKeys
+ * List of currently registered identifier location key.
+ * @param {Object} path
+ */
+function getIdentifierSymbols(identifiers, identifiersKeys, path) {
+ if (t.isStringLiteral(path) && t.isProperty(path.parentPath)) {
+ if (!identifiersKeys.has(nodeLocationKey(path.node.loc))) {
+ identifiers.push({
+ name: path.node.value,
+ get expression() {
+ delete this.expression;
+ this.expression = getObjectExpressionValue(path.parent);
+ return this.expression;
+ },
+ location: path.node.loc,
+ });
+ }
+ return;
+ }
+
+ if (t.isIdentifier(path) && !t.isGenericTypeAnnotation(path.parent)) {
+ // We want to include function params, but exclude the function name
+ if (t.isClassMethod(path.parent) && !path.inList) {
+ return;
+ }
+
+ if (t.isProperty(path.parentPath) && !isObjectShorthand(path.parent)) {
+ if (!identifiersKeys.has(nodeLocationKey(path.node.loc))) {
+ identifiers.push({
+ name: path.node.name,
+ get expression() {
+ delete this.expression;
+ this.expression = getObjectExpressionValue(path.parent);
+ return this.expression;
+ },
+ location: path.node.loc,
+ });
+ }
+ return;
+ }
+
+ let { start, end } = path.node.loc;
+ if (path.node.typeAnnotation) {
+ const { column } = path.node.typeAnnotation.loc.start;
+ end = { ...end, column };
+ }
+
+ if (!identifiersKeys.has(nodeLocationKey({ start, end }))) {
+ identifiers.push({
+ name: path.node.name,
+ expression: path.node.name,
+ location: { start, end },
+ });
+ }
+ }
+
+ if (t.isThisExpression(path.node)) {
+ if (!identifiersKeys.has(nodeLocationKey(path.node.loc))) {
+ identifiers.push({
+ name: "this",
+ location: path.node.loc,
+ expression: "this",
+ });
+ }
+ }
+
+ if (t.isVariableDeclarator(path)) {
+ const nodeId = path.node.id;
+
+ addPatternIdentifiers(identifiers, identifiersKeys, nodeId);
+ }
+}
diff --git a/devtools/client/debugger/src/workers/parser/index.js b/devtools/client/debugger/src/workers/parser/index.js
new file mode 100644
index 0000000000..d60c3d32aa
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/index.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 { WorkerDispatcher } from "devtools/client/shared/worker-utils";
+
+const WORKER_URL = "resource://devtools/client/debugger/dist/parser-worker.js";
+
+export class ParserDispatcher extends WorkerDispatcher {
+ constructor(jestUrl) {
+ super(jestUrl || WORKER_URL);
+ }
+
+ findOutOfScopeLocations = this.task("findOutOfScopeLocations");
+ findBestMatchExpression = this.task("findBestMatchExpression");
+
+ getScopes = this.task("getScopes");
+
+ getSymbols = this.task("getSymbols");
+ getFunctionSymbols = this.task("getFunctionSymbols");
+ getClassSymbols = this.task("getClassSymbols");
+ getClosestFunctionName = this.task("getClosestFunctionName");
+
+ async setSource(sourceId, content) {
+ const astSource = {
+ id: sourceId,
+ text: content.type === "wasm" ? "" : content.value,
+ contentType: content.contentType || null,
+ isWasm: content.type === "wasm",
+ };
+
+ return this.invoke("setSource", astSource);
+ }
+
+ mapExpression = this.task("mapExpression");
+
+ clearSources = this.task("clearSources");
+
+ /**
+ * Reports if the location's source can be parsed by this worker.
+ *
+ * @param {Object} location
+ * A debugger frontend location object. See createLocation().
+ * @return {Boolean}
+ * True, if the worker may be able to parse this source.
+ */
+ isLocationSupported(location) {
+ // There might be more sources that the worker doesn't support,
+ // like original sources which aren't JavaScript.
+ // But we can only know with the source's content type,
+ // which isn't available right away.
+ // These source will be ignored from within the worker.
+ return !location.source.isWasm;
+ }
+}
diff --git a/devtools/client/debugger/src/workers/parser/mapAwaitExpression.js b/devtools/client/debugger/src/workers/parser/mapAwaitExpression.js
new file mode 100644
index 0000000000..ec29d6cf21
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/mapAwaitExpression.js
@@ -0,0 +1,208 @@
+/* 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 generate from "@babel/generator";
+import * as t from "@babel/types";
+
+import { hasNode, replaceNode } from "./utils/ast";
+import { isTopLevel } from "./utils/helpers";
+
+function hasTopLevelAwait(ast) {
+ const hasAwait = hasNode(
+ ast,
+ (node, ancestors, b) => t.isAwaitExpression(node) && isTopLevel(ancestors)
+ );
+
+ return hasAwait;
+}
+
+// translates new bindings `var a = 3` into `a = 3`.
+function translateDeclarationIntoAssignment(node) {
+ return node.declarations.reduce((acc, declaration) => {
+ // Don't translate declaration without initial assignment (e.g. `var a;`)
+ if (!declaration.init) {
+ return acc;
+ }
+ acc.push(
+ t.expressionStatement(
+ t.assignmentExpression("=", declaration.id, declaration.init)
+ )
+ );
+ return acc;
+ }, []);
+}
+
+/**
+ * Given an AST, modify it to return the last evaluated statement's expression value if possible.
+ * This is to preserve existing console behavior of displaying the last executed expression value.
+ */
+function addReturnNode(ast) {
+ const statements = ast.program.body;
+ const lastStatement = statements.pop();
+
+ // if the last expression is an awaitExpression, strip the `await` part and directly
+ // return the argument to avoid calling the argument's `then` function twice when the
+ // mapped expression gets evaluated (See Bug 1771428)
+ if (t.isAwaitExpression(lastStatement.expression)) {
+ lastStatement.expression = lastStatement.expression.argument;
+ }
+
+ // NOTE: For more complicated cases such as an if/for statement, the last evaluated
+ // expression value probably can not be displayed, unless doing hacky workarounds such
+ // as returning the `eval` of the final statement (won't always work due to CSP issues?)
+ // or SpiderMonkey support (See Bug 1839588) at which point this entire module can be removed.
+ statements.push(
+ t.isExpressionStatement(lastStatement)
+ ? t.returnStatement(lastStatement.expression)
+ : lastStatement
+ );
+ return statements;
+}
+
+function getDeclarations(node) {
+ const { kind, declarations } = node;
+ const declaratorNodes = declarations.reduce((acc, d) => {
+ const declarators = getVariableDeclarators(d.id);
+ return acc.concat(declarators);
+ }, []);
+
+ // We can't declare const variables outside of the async iife because we
+ // wouldn't be able to re-assign them. As a workaround, we transform them
+ // to `let` which should be good enough for those case.
+ return t.variableDeclaration(
+ kind === "const" ? "let" : kind,
+ declaratorNodes
+ );
+}
+
+function getVariableDeclarators(node) {
+ if (t.isIdentifier(node)) {
+ return t.variableDeclarator(t.identifier(node.name));
+ }
+
+ if (t.isObjectProperty(node)) {
+ return getVariableDeclarators(node.value);
+ }
+ if (t.isRestElement(node)) {
+ return getVariableDeclarators(node.argument);
+ }
+
+ if (t.isAssignmentPattern(node)) {
+ return getVariableDeclarators(node.left);
+ }
+
+ if (t.isArrayPattern(node)) {
+ return node.elements.reduce(
+ (acc, element) => acc.concat(getVariableDeclarators(element)),
+ []
+ );
+ }
+ if (t.isObjectPattern(node)) {
+ return node.properties.reduce(
+ (acc, property) => acc.concat(getVariableDeclarators(property)),
+ []
+ );
+ }
+ return [];
+}
+
+/**
+ * Given an AST and an array of variableDeclaration nodes, return a new AST with
+ * all the declarations at the top of the AST.
+ */
+function addTopDeclarationNodes(ast, declarationNodes) {
+ const statements = [];
+ declarationNodes.forEach(declarationNode => {
+ statements.push(getDeclarations(declarationNode));
+ });
+ statements.push(ast);
+ return t.program(statements);
+}
+
+/**
+ * Given an AST, return an object of the following shape:
+ * - newAst: {AST} the AST where variable declarations were transformed into
+ * variable assignments
+ * - declarations: {Array<Node>} An array of all the declaration nodes needed
+ * outside of the async iife.
+ */
+function translateDeclarationsIntoAssignment(ast) {
+ const declarations = [];
+ t.traverse(ast, (node, ancestors) => {
+ const parent = ancestors[ancestors.length - 1];
+
+ if (
+ t.isWithStatement(node) ||
+ !isTopLevel(ancestors) ||
+ t.isAssignmentExpression(node) ||
+ !t.isVariableDeclaration(node) ||
+ t.isForStatement(parent.node) ||
+ t.isForXStatement(parent.node) ||
+ !Array.isArray(node.declarations) ||
+ node.declarations.length === 0
+ ) {
+ return;
+ }
+
+ const newNodes = translateDeclarationIntoAssignment(node);
+ replaceNode(ancestors, newNodes);
+ declarations.push(node);
+ });
+
+ return {
+ newAst: ast,
+ declarations,
+ };
+}
+
+/**
+ * Given an AST, wrap its body in an async iife, transform variable declarations
+ * in assignments and move the variable declarations outside of the async iife.
+ * Example: With the AST for the following expression: `let a = await 123`, the
+ * function will return:
+ * let a;
+ * (async => {
+ * return a = await 123;
+ * })();
+ */
+function wrapExpressionFromAst(ast) {
+ // Transform let and var declarations into assignments, and get back an array
+ // of variable declarations.
+ let { newAst, declarations } = translateDeclarationsIntoAssignment(ast);
+ const body = addReturnNode(newAst);
+
+ // Create the async iife.
+ newAst = t.expressionStatement(
+ t.callExpression(
+ t.arrowFunctionExpression([], t.blockStatement(body), true),
+ []
+ )
+ );
+
+ // Now let's put all the variable declarations at the top of the async iife.
+ newAst = addTopDeclarationNodes(newAst, declarations);
+
+ return generate(newAst).code;
+}
+
+export default function mapTopLevelAwait(expression, ast) {
+ if (!ast) {
+ // If there's no ast this means the expression is malformed. And if the
+ // expression contains the await keyword, we still want to wrap it in an
+ // async iife in order to get a meaningful message (without this, the
+ // engine will throw an Error stating that await keywords are only valid
+ // in async functions and generators).
+ if (expression.includes("await ")) {
+ return `(async () => { ${expression} })();`;
+ }
+
+ return expression;
+ }
+
+ if (!hasTopLevelAwait(ast)) {
+ return expression;
+ }
+
+ return wrapExpressionFromAst(ast);
+}
diff --git a/devtools/client/debugger/src/workers/parser/mapBindings.js b/devtools/client/debugger/src/workers/parser/mapBindings.js
new file mode 100644
index 0000000000..f8260787f1
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/mapBindings.js
@@ -0,0 +1,120 @@
+/* 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 { replaceNode } from "./utils/ast";
+import { isTopLevel } from "./utils/helpers";
+
+import generate from "@babel/generator";
+import * as t from "@babel/types";
+
+function getAssignmentTarget(node, bindings) {
+ if (t.isObjectPattern(node)) {
+ for (const property of node.properties) {
+ if (t.isRestElement(property)) {
+ property.argument = getAssignmentTarget(property.argument, bindings);
+ } else {
+ property.value = getAssignmentTarget(property.value, bindings);
+ }
+ }
+
+ return node;
+ }
+
+ if (t.isArrayPattern(node)) {
+ for (const [i, element] of node.elements.entries()) {
+ node.elements[i] = getAssignmentTarget(element, bindings);
+ }
+
+ return node;
+ }
+
+ if (t.isAssignmentPattern(node)) {
+ node.left = getAssignmentTarget(node.left, bindings);
+
+ return node;
+ }
+
+ if (t.isRestElement(node)) {
+ node.argument = getAssignmentTarget(node.argument, bindings);
+
+ return node;
+ }
+
+ if (t.isIdentifier(node)) {
+ return bindings.includes(node.name)
+ ? node
+ : t.memberExpression(t.identifier("self"), node);
+ }
+
+ return node;
+}
+
+// translates new bindings `var a = 3` into `self.a = 3`
+// and existing bindings `var a = 3` into `a = 3` for re-assignments
+function globalizeDeclaration(node, bindings) {
+ return node.declarations.map(declaration =>
+ t.expressionStatement(
+ t.assignmentExpression(
+ "=",
+ getAssignmentTarget(declaration.id, bindings),
+ declaration.init || t.unaryExpression("void", t.numericLiteral(0))
+ )
+ )
+ );
+}
+
+// translates new bindings `a = 3` into `self.a = 3`
+// and keeps assignments the same for existing bindings.
+function globalizeAssignment(node, bindings) {
+ return t.assignmentExpression(
+ node.operator,
+ getAssignmentTarget(node.left, bindings),
+ node.right
+ );
+}
+
+export default function mapExpressionBindings(expression, ast, bindings = []) {
+ let isMapped = false;
+ let shouldUpdate = true;
+
+ t.traverse(ast, (node, ancestors) => {
+ const parent = ancestors[ancestors.length - 1];
+
+ if (t.isWithStatement(node)) {
+ shouldUpdate = false;
+ return;
+ }
+
+ if (!isTopLevel(ancestors)) {
+ return;
+ }
+
+ if (t.isAssignmentExpression(node)) {
+ if (t.isIdentifier(node.left) || t.isPattern(node.left)) {
+ const newNode = globalizeAssignment(node, bindings);
+ isMapped = true;
+ replaceNode(ancestors, newNode);
+ return;
+ }
+
+ return;
+ }
+
+ if (!t.isVariableDeclaration(node)) {
+ return;
+ }
+
+ if (!t.isForStatement(parent.node)) {
+ const newNodes = globalizeDeclaration(node, bindings);
+ isMapped = true;
+ replaceNode(ancestors, newNodes);
+ }
+ });
+
+ if (!shouldUpdate || !isMapped) {
+ return expression;
+ }
+
+ return generate(ast).code;
+}
diff --git a/devtools/client/debugger/src/workers/parser/mapExpression.js b/devtools/client/debugger/src/workers/parser/mapExpression.js
new file mode 100644
index 0000000000..71f3b016fc
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/mapExpression.js
@@ -0,0 +1,50 @@
+/* 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 { parseConsoleScript } from "./utils/ast";
+import mapOriginalExpression from "./mapOriginalExpression";
+import mapExpressionBindings from "./mapBindings";
+import mapTopLevelAwait from "./mapAwaitExpression";
+
+export default function mapExpression(
+ expression,
+ mappings,
+ bindings,
+ shouldMapBindings = true,
+ shouldMapAwait = true
+) {
+ const mapped = {
+ await: false,
+ bindings: false,
+ originalExpression: false,
+ };
+
+ const ast = parseConsoleScript(expression);
+ try {
+ if (mappings && ast) {
+ const beforeOriginalExpression = expression;
+ expression = mapOriginalExpression(expression, ast, mappings);
+ mapped.originalExpression = beforeOriginalExpression !== expression;
+ }
+
+ if (shouldMapBindings && ast) {
+ const beforeBindings = expression;
+ expression = mapExpressionBindings(expression, ast, bindings);
+ mapped.bindings = beforeBindings !== expression;
+ }
+
+ if (shouldMapAwait) {
+ const beforeAwait = expression;
+ expression = mapTopLevelAwait(expression, ast);
+ mapped.await = beforeAwait !== expression;
+ }
+ } catch (e) {
+ console.warn(`Error when mapping ${expression} expression:`, e);
+ }
+
+ return {
+ expression,
+ mapped,
+ };
+}
diff --git a/devtools/client/debugger/src/workers/parser/mapOriginalExpression.js b/devtools/client/debugger/src/workers/parser/mapOriginalExpression.js
new file mode 100644
index 0000000000..1724db9838
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/mapOriginalExpression.js
@@ -0,0 +1,106 @@
+/* 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 { parseScript } from "./utils/ast";
+import { buildScopeList } from "./getScopes";
+import generate from "@babel/generator";
+import * as t from "@babel/types";
+
+// NOTE: this will only work if we are replacing an original identifier
+function replaceNode(ancestors, node) {
+ const ancestor = ancestors[ancestors.length - 1];
+
+ if (typeof ancestor.index === "number") {
+ ancestor.node[ancestor.key][ancestor.index] = node;
+ } else {
+ ancestor.node[ancestor.key] = node;
+ }
+}
+
+function getFirstExpression(ast) {
+ const statements = ast.program.body;
+ if (!statements.length) {
+ return null;
+ }
+
+ return statements[0].expression;
+}
+
+function locationKey(start) {
+ return `${start.line}:${start.column}`;
+}
+
+export default function mapOriginalExpression(expression, ast, mappings) {
+ const scopes = buildScopeList(ast, "");
+ let shouldUpdate = false;
+
+ const nodes = new Map();
+ const replacements = new Map();
+
+ // The ref-only global bindings are the ones that are accessed, but not
+ // declared anywhere in the parsed code, meaning they are either global,
+ // or declared somewhere in a scope outside the parsed code, so we
+ // rewrite all of those specifically to avoid rewritting declarations that
+ // shadow outer mappings.
+ for (const name of Object.keys(scopes[0].bindings)) {
+ const { refs } = scopes[0].bindings[name];
+ const mapping = mappings[name];
+
+ if (
+ !refs.every(ref => ref.type === "ref") ||
+ !mapping ||
+ mapping === name
+ ) {
+ continue;
+ }
+
+ let node = nodes.get(name);
+ if (!node) {
+ node = getFirstExpression(parseScript(mapping));
+ nodes.set(name, node);
+ }
+
+ for (const ref of refs) {
+ let { line, column } = ref.start;
+
+ // This shouldn't happen, just keeping Flow happy.
+ if (typeof column !== "number") {
+ column = 0;
+ }
+
+ replacements.set(locationKey({ line, column }), node);
+ }
+ }
+
+ if (replacements.size === 0) {
+ // Avoid the extra code generation work and also avoid potentially
+ // reformatting the user's code unnecessarily.
+ return expression;
+ }
+
+ t.traverse(ast, (node, ancestors) => {
+ if (!t.isIdentifier(node) && !t.isThisExpression(node)) {
+ return;
+ }
+
+ const ancestor = ancestors[ancestors.length - 1];
+ // Shorthand properties can have a key and value with `node.loc.start` value
+ // and we only want to replace the value.
+ if (t.isObjectProperty(ancestor.node) && ancestor.key !== "value") {
+ return;
+ }
+
+ const replacement = replacements.get(locationKey(node.loc.start));
+ if (replacement) {
+ replaceNode(ancestors, t.cloneNode(replacement));
+ shouldUpdate = true;
+ }
+ });
+
+ if (shouldUpdate) {
+ return generate(ast).code;
+ }
+
+ return expression;
+}
diff --git a/devtools/client/debugger/src/workers/parser/moz.build b/devtools/client/debugger/src/workers/parser/moz.build
new file mode 100644
index 0000000000..b7223ac81a
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/moz.build
@@ -0,0 +1,10 @@
+# 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(
+ "index.js",
+)
diff --git a/devtools/client/debugger/src/workers/parser/sources.js b/devtools/client/debugger/src/workers/parser/sources.js
new file mode 100644
index 0000000000..24d358c566
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/sources.js
@@ -0,0 +1,24 @@
+/* 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/>. */
+
+const cachedSources = new Map();
+
+export function setSource(source) {
+ cachedSources.set(source.id, source);
+}
+
+export function getSource(sourceId) {
+ const source = cachedSources.get(sourceId);
+ if (!source) {
+ throw new Error(`Parser: source ${sourceId} was not provided.`);
+ }
+
+ return source;
+}
+
+export function clearSources(sourceIds) {
+ for (const sourceId of sourceIds) {
+ cachedSources.delete(sourceId);
+ }
+}
diff --git a/devtools/client/debugger/src/workers/parser/tests/__snapshots__/findOutOfScopeLocations.spec.js.snap b/devtools/client/debugger/src/workers/parser/tests/__snapshots__/findOutOfScopeLocations.spec.js.snap
new file mode 100644
index 0000000000..4689f0c824
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/__snapshots__/findOutOfScopeLocations.spec.js.snap
@@ -0,0 +1,47 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Parser.findOutOfScopeLocations should exclude function for locations on declaration 1`] = `
+"(1, 0) -> (1, 16)
+(3, 14) -> (27, 1)
+(29, 16) -> (33, 1)
+(35, 20) -> (37, 1)
+(39, 26) -> (41, 1)
+(43, 20) -> (45, 1)
+(47, 31) -> (49, 1)
+(51, 19) -> (62, 1)"
+`;
+
+exports[`Parser.findOutOfScopeLocations should exclude non-enclosing function blocks 1`] = `
+"(1, 0) -> (1, 16)
+(8, 16) -> (10, 3)
+(12, 22) -> (14, 3)
+(16, 16) -> (18, 3)
+(20, 27) -> (22, 3)
+(24, 9) -> (26, 3)
+(29, 16) -> (33, 1)
+(35, 20) -> (37, 1)
+(39, 26) -> (41, 1)
+(43, 20) -> (45, 1)
+(47, 31) -> (49, 1)
+(51, 19) -> (62, 1)"
+`;
+
+exports[`Parser.findOutOfScopeLocations should not exclude in-scope inner locations 1`] = `
+"(1, 0) -> (1, 16)
+(3, 14) -> (27, 1)
+(29, 16) -> (33, 1)
+(35, 20) -> (37, 1)
+(39, 26) -> (41, 1)
+(43, 20) -> (45, 1)
+(47, 31) -> (49, 1)"
+`;
+
+exports[`Parser.findOutOfScopeLocations should roll up function blocks 1`] = `
+"(1, 0) -> (1, 16)
+(29, 16) -> (33, 1)
+(35, 20) -> (37, 1)
+(39, 26) -> (41, 1)
+(43, 20) -> (45, 1)
+(47, 31) -> (49, 1)
+(51, 19) -> (62, 1)"
+`;
diff --git a/devtools/client/debugger/src/workers/parser/tests/__snapshots__/getScopes.spec.js.snap b/devtools/client/debugger/src/workers/parser/tests/__snapshots__/getScopes.spec.js.snap
new file mode 100644
index 0000000000..b579b89587
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/__snapshots__/getScopes.spec.js.snap
@@ -0,0 +1,18895 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Parser.getScopes finds scope bindings and exclude Flowtype: getScopes finds scope bindings and exclude Flowtype at line 8 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "aConst": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 39,
+ "line": 7,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 7,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 7,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 7,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ "type": "const",
+ },
+ ],
+ "type": "const",
+ },
+ "root": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ "start": Object {
+ "column": 15,
+ "line": 9,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 28,
+ "line": 9,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ "start": Object {
+ "column": 24,
+ "line": 9,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "window": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 22,
+ "line": 7,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 16,
+ "line": 7,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings and exclude Flowtype: getScopes finds scope bindings and exclude Flowtype at line 10 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "root",
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 15,
+ "line": 9,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "aConst": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 39,
+ "line": 7,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 7,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 7,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 7,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ "type": "const",
+ },
+ ],
+ "type": "const",
+ },
+ "root": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ "start": Object {
+ "column": 15,
+ "line": 9,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 28,
+ "line": 9,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ "start": Object {
+ "column": 24,
+ "line": 9,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "window": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 22,
+ "line": 7,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 16,
+ "line": 7,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/flowtype-bindings/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for arrow functions: getScopes finds scope bindings for arrow functions at line 2 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "outer": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 2,
+ "line": 11,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 3,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ "this": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 18,
+ "line": 4,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 14,
+ "line": 4,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "console": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 9,
+ "line": 4,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 13,
+ "line": 4,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 2,
+ "line": 4,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 4,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 13,
+ "line": 8,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 17,
+ "line": 8,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 6,
+ "line": 8,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 8,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for arrow functions: getScopes finds scope bindings for arrow functions at line 4 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "p1": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "start": Object {
+ "column": 12,
+ "line": 3,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 15,
+ "line": 3,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "start": Object {
+ "column": 13,
+ "line": 3,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "fn-param",
+ },
+ ],
+ "type": "var",
+ },
+ },
+ "displayName": "outer",
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 13,
+ "line": 3,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "outer": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 2,
+ "line": 11,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 3,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ "this": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 18,
+ "line": 4,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 14,
+ "line": 4,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "console": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 9,
+ "line": 4,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 13,
+ "line": 4,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 2,
+ "line": 4,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 4,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 13,
+ "line": 8,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 17,
+ "line": 8,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 6,
+ "line": 8,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 8,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for arrow functions: getScopes finds scope bindings for arrow functions at line 7 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "inner": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 6,
+ "line": 9,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 7,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 7,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 7,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 22,
+ "line": 8,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 18,
+ "line": 8,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "implicit",
+ },
+ },
+ "displayName": "anonymous",
+ "end": Object {
+ "column": 3,
+ "line": 10,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 3,
+ "line": 6,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "p1": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "start": Object {
+ "column": 12,
+ "line": 3,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 15,
+ "line": 3,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "start": Object {
+ "column": 13,
+ "line": 3,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "fn-param",
+ },
+ ],
+ "type": "var",
+ },
+ },
+ "displayName": "outer",
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 13,
+ "line": 3,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "outer": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 2,
+ "line": 11,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 3,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ "this": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 18,
+ "line": 4,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 14,
+ "line": 4,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "console": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 9,
+ "line": 4,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 13,
+ "line": 4,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 2,
+ "line": 4,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 4,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 13,
+ "line": 8,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 17,
+ "line": 8,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 6,
+ "line": 8,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 8,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for arrow functions: getScopes finds scope bindings for arrow functions at line 8 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "p2": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 5,
+ "line": 9,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "start": Object {
+ "column": 16,
+ "line": 7,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 19,
+ "line": 7,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "start": Object {
+ "column": 17,
+ "line": 7,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "fn-param",
+ },
+ ],
+ "type": "var",
+ },
+ },
+ "displayName": "inner",
+ "end": Object {
+ "column": 5,
+ "line": 9,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 17,
+ "line": 7,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "inner": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 6,
+ "line": 9,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 7,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 7,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 7,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 22,
+ "line": 8,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 18,
+ "line": 8,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "implicit",
+ },
+ },
+ "displayName": "anonymous",
+ "end": Object {
+ "column": 3,
+ "line": 10,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 3,
+ "line": 6,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "p1": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "start": Object {
+ "column": 12,
+ "line": 3,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 15,
+ "line": 3,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "start": Object {
+ "column": 13,
+ "line": 3,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "fn-param",
+ },
+ ],
+ "type": "var",
+ },
+ },
+ "displayName": "outer",
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 13,
+ "line": 3,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "outer": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 2,
+ "line": 11,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 3,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ "this": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 18,
+ "line": 4,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 14,
+ "line": 4,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "console": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 9,
+ "line": 4,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 13,
+ "line": 4,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 2,
+ "line": 4,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 4,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 13,
+ "line": 8,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 17,
+ "line": 8,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 6,
+ "line": 8,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 8,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/arrow-function/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for block statements: getScopes finds scope bindings for block statements at line 2 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "first": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 10,
+ "line": 3,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 3,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ "second": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 13,
+ "line": 6,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 6,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 6,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 6,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "seventh": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 13,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 13,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 13,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 13,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for block statements: getScopes finds scope bindings for block statements at line 6 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "Fourth": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 17,
+ "line": 8,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 8,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 14,
+ "line": 8,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 8,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "type": "class-decl",
+ },
+ ],
+ "type": "let",
+ },
+ "fifth": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 9,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 9,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 9,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 9,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ "sixth": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 18,
+ "line": 10,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 10,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 10,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 10,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "type": "const",
+ },
+ ],
+ "type": "const",
+ },
+ "third": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 21,
+ "line": 7,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 7,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 16,
+ "line": 7,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 11,
+ "line": 7,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "let",
+ },
+ },
+ "displayName": "Block",
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 5,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "first": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 10,
+ "line": 3,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 3,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ "second": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 13,
+ "line": 6,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 6,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 6,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 6,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "seventh": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 13,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 13,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 13,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 13,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/block-statement/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for class declarations: getScopes finds scope bindings for class declarations at line 2 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "Outer": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 3,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 3,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "class-decl",
+ },
+ ],
+ "type": "let",
+ },
+ "Second": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 15,
+ "line": 14,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 14,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 14,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "class-decl",
+ },
+ ],
+ "type": "let",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 15,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 15,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "console": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 15,
+ "line": 7,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 19,
+ "line": 7,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 8,
+ "line": 7,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 7,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ "decorator": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 10,
+ "line": 13,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 1,
+ "line": 13,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 15,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for class declarations: getScopes finds scope bindings for class declarations at line 5 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "Inner": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 5,
+ "line": 9,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 5,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 15,
+ "line": 5,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 10,
+ "line": 5,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "class-decl",
+ },
+ ],
+ "type": "let",
+ },
+ },
+ "displayName": "Function Body",
+ "end": Object {
+ "column": 3,
+ "line": 10,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 11,
+ "line": 4,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "method",
+ "end": Object {
+ "column": 3,
+ "line": 10,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 2,
+ "line": 4,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "Outer": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 3,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 3,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "class-inner",
+ },
+ ],
+ "type": "const",
+ },
+ },
+ "displayName": "Class",
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "Outer": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 3,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 3,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "class-decl",
+ },
+ ],
+ "type": "let",
+ },
+ "Second": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 15,
+ "line": 14,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 14,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 14,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "class-decl",
+ },
+ ],
+ "type": "let",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 15,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 15,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "console": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 15,
+ "line": 7,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 19,
+ "line": 7,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 8,
+ "line": 7,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 7,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ "decorator": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 10,
+ "line": 13,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 1,
+ "line": 13,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 15,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for class declarations: getScopes finds scope bindings for class declarations at line 7 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "this": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 24,
+ "line": 7,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 20,
+ "line": 7,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "implicit",
+ },
+ },
+ "displayName": "m",
+ "end": Object {
+ "column": 7,
+ "line": 8,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 6,
+ "line": 6,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "Inner": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 5,
+ "line": 9,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 5,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 15,
+ "line": 5,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 10,
+ "line": 5,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "class-inner",
+ },
+ ],
+ "type": "const",
+ },
+ },
+ "displayName": "Class",
+ "end": Object {
+ "column": 5,
+ "line": 9,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 4,
+ "line": 5,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "Inner": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 5,
+ "line": 9,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 5,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 15,
+ "line": 5,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 10,
+ "line": 5,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "class-decl",
+ },
+ ],
+ "type": "let",
+ },
+ },
+ "displayName": "Function Body",
+ "end": Object {
+ "column": 3,
+ "line": 10,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 11,
+ "line": 4,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "method",
+ "end": Object {
+ "column": 3,
+ "line": 10,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 2,
+ "line": 4,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "Outer": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 3,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 3,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "class-inner",
+ },
+ ],
+ "type": "const",
+ },
+ },
+ "displayName": "Class",
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "Outer": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 3,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 3,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "class-decl",
+ },
+ ],
+ "type": "let",
+ },
+ "Second": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 15,
+ "line": 14,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 14,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 14,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "class-decl",
+ },
+ ],
+ "type": "let",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 15,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 15,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "console": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 15,
+ "line": 7,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 19,
+ "line": 7,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 8,
+ "line": 7,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 7,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ "decorator": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 10,
+ "line": 13,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 1,
+ "line": 13,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 15,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/class-declaration/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for class expressions: getScopes finds scope bindings for class expressions at line 2 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "Outer": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 2,
+ "line": 11,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 3,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "console": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 15,
+ "line": 7,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 19,
+ "line": 7,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 8,
+ "line": 7,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 7,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for class expressions: getScopes finds scope bindings for class expressions at line 5 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "Inner": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 6,
+ "line": 9,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 5,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 5,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 5,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "method",
+ "end": Object {
+ "column": 3,
+ "line": 10,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 2,
+ "line": 4,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "Outer": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "start": Object {
+ "column": 12,
+ "line": 3,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 23,
+ "line": 3,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "start": Object {
+ "column": 18,
+ "line": 3,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "type": "class-inner",
+ },
+ ],
+ "type": "const",
+ },
+ },
+ "displayName": "Class",
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 12,
+ "line": 3,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "Outer": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 2,
+ "line": 11,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 3,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "console": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 15,
+ "line": 7,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 19,
+ "line": 7,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 8,
+ "line": 7,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 7,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for class expressions: getScopes finds scope bindings for class expressions at line 7 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "this": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 24,
+ "line": 7,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 20,
+ "line": 7,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "implicit",
+ },
+ },
+ "displayName": "m",
+ "end": Object {
+ "column": 7,
+ "line": 8,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 6,
+ "line": 6,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "Inner": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 6,
+ "line": 9,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 5,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 5,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 5,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "method",
+ "end": Object {
+ "column": 3,
+ "line": 10,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 2,
+ "line": 4,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "Outer": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "start": Object {
+ "column": 12,
+ "line": 3,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 23,
+ "line": 3,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "start": Object {
+ "column": 18,
+ "line": 3,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "type": "class-inner",
+ },
+ ],
+ "type": "const",
+ },
+ },
+ "displayName": "Class",
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 12,
+ "line": 3,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "Outer": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 2,
+ "line": 11,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 3,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "console": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 15,
+ "line": 7,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 19,
+ "line": 7,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 8,
+ "line": 7,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 7,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/class-expression/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for class properties: getScopes finds scope bindings for class properties at line 2 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "Foo": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 10,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 3,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "type": "class-decl",
+ },
+ ],
+ "type": "let",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 11,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 11,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 11,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for class properties: getScopes finds scope bindings for class properties at line 4 column 16 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "this": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 13,
+ "line": 4,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 18,
+ "line": 4,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "parent": Object {
+ "end": Object {
+ "column": 20,
+ "line": 4,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "parent": null,
+ "start": Object {
+ "column": 9,
+ "line": 4,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "type": "call",
+ },
+ "property": "init",
+ "start": Object {
+ "column": 9,
+ "line": 4,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 4,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Class Field",
+ "end": Object {
+ "column": 20,
+ "line": 4,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 9,
+ "line": 4,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "Foo": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 10,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 3,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "type": "class-inner",
+ },
+ ],
+ "type": "const",
+ },
+ },
+ "displayName": "Class",
+ "end": Object {
+ "column": 1,
+ "line": 10,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "Foo": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 10,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 3,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "type": "class-decl",
+ },
+ ],
+ "type": "let",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 11,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 11,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 11,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for class properties: getScopes finds scope bindings for class properties at line 6 column 12 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "one": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 7,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 7,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 7,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 7,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Class Field",
+ "end": Object {
+ "column": 3,
+ "line": 9,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 10,
+ "line": 6,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "Foo": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 10,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 3,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "type": "class-inner",
+ },
+ ],
+ "type": "const",
+ },
+ },
+ "displayName": "Class",
+ "end": Object {
+ "column": 1,
+ "line": 10,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "Foo": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 10,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 3,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "type": "class-decl",
+ },
+ ],
+ "type": "let",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 11,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 11,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 11,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for class properties: getScopes finds scope bindings for class properties at line 7 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "two": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 8,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 8,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 8,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 8,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ },
+ "displayName": "Block",
+ "end": Object {
+ "column": 3,
+ "line": 9,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 13,
+ "line": 6,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "one": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 7,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 7,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 7,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 7,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Class Field",
+ "end": Object {
+ "column": 3,
+ "line": 9,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 10,
+ "line": 6,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "Foo": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 10,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 3,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "type": "class-inner",
+ },
+ ],
+ "type": "const",
+ },
+ },
+ "displayName": "Class",
+ "end": Object {
+ "column": 1,
+ "line": 10,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "Foo": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 10,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 3,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "type": "class-decl",
+ },
+ ],
+ "type": "let",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 11,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 11,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 11,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/class-property/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for complex binding nesting: getScopes finds scope bindings for complex binding nesting at line 16 column 4 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "_arguments": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 31,
+ "line": 13,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 12,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 18,
+ "line": 13,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 13,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "var",
+ },
+ Object {
+ "end": Object {
+ "column": 35,
+ "line": 19,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 25,
+ "line": 19,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "var",
+ },
+ "_this": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 31,
+ "line": 13,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 12,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 12,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 12,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "var",
+ },
+ Object {
+ "end": Object {
+ "column": 23,
+ "line": 19,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 18,
+ "line": 19,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "var",
+ },
+ "arg": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 3,
+ "line": 23,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 11,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 17,
+ "line": 11,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 14,
+ "line": 11,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "fn-param",
+ },
+ ],
+ "type": "var",
+ },
+ "arguments": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 30,
+ "line": 13,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 21,
+ "line": 13,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 31,
+ "line": 15,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 22,
+ "line": 15,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "implicit",
+ },
+ "arrow": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 6,
+ "line": 21,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 18,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 18,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 18,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "var",
+ },
+ Object {
+ "end": Object {
+ "column": 9,
+ "line": 22,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 4,
+ "line": 22,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 20,
+ "line": 12,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 16,
+ "line": 12,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 20,
+ "line": 15,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 16,
+ "line": 15,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "implicit",
+ },
+ },
+ "displayName": "fn",
+ "end": Object {
+ "column": 3,
+ "line": 23,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 14,
+ "line": 11,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "fn": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 3,
+ "line": 23,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 11,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 11,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 11,
+ "line": 11,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ Object {
+ "end": Object {
+ "column": 32,
+ "line": 16,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 30,
+ "line": 16,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 34,
+ "line": 20,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 32,
+ "line": 20,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 4,
+ "line": 25,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 9,
+ "line": 25,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "parent": null,
+ "property": "call",
+ "start": Object {
+ "column": 2,
+ "line": 25,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 25,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "root",
+ "end": Object {
+ "column": 1,
+ "line": 26,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 10,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "__webpack_require__": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 29,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 51,
+ "line": 1,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 32,
+ "line": 1,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "fn-param",
+ },
+ ],
+ "type": "var",
+ },
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "exports": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 29,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 30,
+ "line": 1,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 23,
+ "line": 1,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "fn-param",
+ },
+ Object {
+ "end": Object {
+ "column": 29,
+ "line": 6,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 22,
+ "line": 6,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 7,
+ "line": 9,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 15,
+ "line": 9,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "parent": null,
+ "property": "default",
+ "start": Object {
+ "column": 0,
+ "line": 9,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 9,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 24,
+ "line": 27,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 35,
+ "line": 27,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "parent": null,
+ "property": "default",
+ "start": Object {
+ "column": 17,
+ "line": 27,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 17,
+ "line": 27,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "var",
+ },
+ "module": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 29,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 21,
+ "line": 1,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 15,
+ "line": 1,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "fn-param",
+ },
+ Object {
+ "end": Object {
+ "column": 6,
+ "line": 27,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 14,
+ "line": 27,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "parent": null,
+ "property": "exports",
+ "start": Object {
+ "column": 0,
+ "line": 27,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 27,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "var",
+ },
+ "root": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 26,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 10,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 10,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 10,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ Object {
+ "end": Object {
+ "column": 22,
+ "line": 9,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 18,
+ "line": 9,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 38,
+ "line": 16,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 34,
+ "line": 16,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 40,
+ "line": 20,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 36,
+ "line": 20,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "named",
+ "end": Object {
+ "column": 1,
+ "line": 29,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 15,
+ "line": 1,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 30,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "Object": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 6,
+ "line": 6,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 21,
+ "line": 6,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "parent": null,
+ "property": "defineProperty",
+ "start": Object {
+ "column": 0,
+ "line": 6,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 6,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ "console": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 15,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 15,
+ "line": 15,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 4,
+ "line": 15,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 15,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 16,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 15,
+ "line": 16,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 4,
+ "line": 16,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 16,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 13,
+ "line": 19,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 17,
+ "line": 19,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 6,
+ "line": 19,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 19,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 13,
+ "line": 20,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 17,
+ "line": 20,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 6,
+ "line": 20,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 20,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ "named": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 29,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 14,
+ "line": 1,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 1,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 30,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for complex binding nesting: getScopes finds scope bindings for complex binding nesting at line 20 column 6 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "argArrow": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 5,
+ "line": 21,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 16,
+ "line": 18,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 39,
+ "line": 18,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 31,
+ "line": 18,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "fn-param",
+ },
+ ],
+ "type": "var",
+ },
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "arrow",
+ "end": Object {
+ "column": 5,
+ "line": 21,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 31,
+ "line": 18,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "arrow": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 5,
+ "line": 21,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 16,
+ "line": 18,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 30,
+ "line": 18,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 25,
+ "line": 18,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "fn-expr",
+ },
+ ],
+ "type": "const",
+ },
+ },
+ "displayName": "Function Expression",
+ "end": Object {
+ "column": 5,
+ "line": 21,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 16,
+ "line": 18,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "_arguments": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 31,
+ "line": 13,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 12,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 18,
+ "line": 13,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 13,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "var",
+ },
+ Object {
+ "end": Object {
+ "column": 35,
+ "line": 19,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 25,
+ "line": 19,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "var",
+ },
+ "_this": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 31,
+ "line": 13,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 12,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 12,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 12,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "var",
+ },
+ Object {
+ "end": Object {
+ "column": 23,
+ "line": 19,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 18,
+ "line": 19,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "var",
+ },
+ "arg": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 3,
+ "line": 23,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 11,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 17,
+ "line": 11,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 14,
+ "line": 11,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "fn-param",
+ },
+ ],
+ "type": "var",
+ },
+ "arguments": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 30,
+ "line": 13,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 21,
+ "line": 13,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 31,
+ "line": 15,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 22,
+ "line": 15,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "implicit",
+ },
+ "arrow": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 6,
+ "line": 21,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 18,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 18,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 18,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "var",
+ },
+ Object {
+ "end": Object {
+ "column": 9,
+ "line": 22,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 4,
+ "line": 22,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 20,
+ "line": 12,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 16,
+ "line": 12,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 20,
+ "line": 15,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 16,
+ "line": 15,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "implicit",
+ },
+ },
+ "displayName": "fn",
+ "end": Object {
+ "column": 3,
+ "line": 23,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 14,
+ "line": 11,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "fn": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 3,
+ "line": 23,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 11,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 11,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 11,
+ "line": 11,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ Object {
+ "end": Object {
+ "column": 32,
+ "line": 16,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 30,
+ "line": 16,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 34,
+ "line": 20,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 32,
+ "line": 20,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 4,
+ "line": 25,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 9,
+ "line": 25,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "parent": null,
+ "property": "call",
+ "start": Object {
+ "column": 2,
+ "line": 25,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 25,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "root",
+ "end": Object {
+ "column": 1,
+ "line": 26,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 10,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "__webpack_require__": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 29,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 51,
+ "line": 1,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 32,
+ "line": 1,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "fn-param",
+ },
+ ],
+ "type": "var",
+ },
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "exports": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 29,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 30,
+ "line": 1,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 23,
+ "line": 1,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "fn-param",
+ },
+ Object {
+ "end": Object {
+ "column": 29,
+ "line": 6,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 22,
+ "line": 6,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 7,
+ "line": 9,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 15,
+ "line": 9,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "parent": null,
+ "property": "default",
+ "start": Object {
+ "column": 0,
+ "line": 9,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 9,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 24,
+ "line": 27,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 35,
+ "line": 27,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "parent": null,
+ "property": "default",
+ "start": Object {
+ "column": 17,
+ "line": 27,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 17,
+ "line": 27,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "var",
+ },
+ "module": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 29,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 21,
+ "line": 1,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 15,
+ "line": 1,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "fn-param",
+ },
+ Object {
+ "end": Object {
+ "column": 6,
+ "line": 27,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 14,
+ "line": 27,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "parent": null,
+ "property": "exports",
+ "start": Object {
+ "column": 0,
+ "line": 27,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 27,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "var",
+ },
+ "root": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 26,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 10,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 10,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 10,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ Object {
+ "end": Object {
+ "column": 22,
+ "line": 9,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 18,
+ "line": 9,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 38,
+ "line": 16,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 34,
+ "line": 16,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 40,
+ "line": 20,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 36,
+ "line": 20,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "named",
+ "end": Object {
+ "column": 1,
+ "line": 29,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 15,
+ "line": 1,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 30,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "Object": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 6,
+ "line": 6,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 21,
+ "line": 6,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "parent": null,
+ "property": "defineProperty",
+ "start": Object {
+ "column": 0,
+ "line": 6,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 6,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ "console": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 15,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 15,
+ "line": 15,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 4,
+ "line": 15,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 15,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 16,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 15,
+ "line": 16,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 4,
+ "line": 16,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 16,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 13,
+ "line": 19,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 17,
+ "line": 19,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 6,
+ "line": 19,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 19,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 13,
+ "line": 20,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 17,
+ "line": 20,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 6,
+ "line": 20,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 20,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ "named": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 29,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 14,
+ "line": 1,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 1,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 30,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/complex-nesting/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for declarations with patterns: getScopes finds scope bindings for declarations with patterns at line 1 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/pattern-declarations/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/pattern-declarations/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "one": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 23,
+ "line": 1,
+ "sourceId": "scopes/pattern-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/pattern-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 15,
+ "line": 1,
+ "sourceId": "scopes/pattern-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 12,
+ "line": 1,
+ "sourceId": "scopes/pattern-declarations/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "two": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 17,
+ "line": 2,
+ "sourceId": "scopes/pattern-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 2,
+ "sourceId": "scopes/pattern-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 9,
+ "line": 2,
+ "sourceId": "scopes/pattern-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 2,
+ "sourceId": "scopes/pattern-declarations/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/pattern-declarations/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/pattern-declarations/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for for loops: getScopes finds scope bindings for for loops at line 2 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "four": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 13,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "one": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "seven": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 14,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 14,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for for loops: getScopes finds scope bindings for for loops at line 3 column 17 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "four": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 13,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "one": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "seven": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 14,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 14,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for for loops: getScopes finds scope bindings for for loops at line 4 column 17 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "two": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 4,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 4,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 4,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 4,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ },
+ "displayName": "For",
+ "end": Object {
+ "column": 18,
+ "line": 4,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 5,
+ "line": 4,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "four": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 13,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "one": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "seven": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 14,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 14,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for for loops: getScopes finds scope bindings for for loops at line 5 column 25 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "three": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 20,
+ "line": 5,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 5,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 16,
+ "line": 5,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 11,
+ "line": 5,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "const",
+ },
+ ],
+ "type": "const",
+ },
+ },
+ "displayName": "For",
+ "end": Object {
+ "column": 26,
+ "line": 5,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 5,
+ "line": 5,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "four": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 13,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "one": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "seven": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 14,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 14,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for for loops: getScopes finds scope bindings for for loops at line 7 column 22 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "four": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 13,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "one": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "seven": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 14,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 14,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for for loops: getScopes finds scope bindings for for loops at line 8 column 22 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "five": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 13,
+ "line": 8,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 8,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 8,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 8,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ },
+ "displayName": "For",
+ "end": Object {
+ "column": 23,
+ "line": 8,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 5,
+ "line": 8,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "four": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 13,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "one": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "seven": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 14,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 14,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for for loops: getScopes finds scope bindings for for loops at line 9 column 23 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "six": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 14,
+ "line": 9,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 9,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 14,
+ "line": 9,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 11,
+ "line": 9,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "const",
+ },
+ ],
+ "type": "const",
+ },
+ },
+ "displayName": "For",
+ "end": Object {
+ "column": 24,
+ "line": 9,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 5,
+ "line": 9,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "four": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 13,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "one": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "seven": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 14,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 14,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for for loops: getScopes finds scope bindings for for loops at line 11 column 23 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "four": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 13,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "one": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "seven": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 14,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 14,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for for loops: getScopes finds scope bindings for for loops at line 12 column 23 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "eight": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 14,
+ "line": 12,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 12,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 14,
+ "line": 12,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 12,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ },
+ "displayName": "For",
+ "end": Object {
+ "column": 24,
+ "line": 12,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 5,
+ "line": 12,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "four": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 13,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "one": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "seven": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 14,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 14,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for for loops: getScopes finds scope bindings for for loops at line 13 column 24 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "nine": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 15,
+ "line": 13,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 13,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 15,
+ "line": 13,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 11,
+ "line": 13,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "const",
+ },
+ ],
+ "type": "const",
+ },
+ },
+ "displayName": "For",
+ "end": Object {
+ "column": 25,
+ "line": 13,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 5,
+ "line": 13,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "four": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 13,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 7,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "one": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "seven": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 14,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 14,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 11,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/for-loops/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for function declarations: getScopes finds scope bindings for function declarations at line 2 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "outer": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 21,
+ "line": 3,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 14,
+ "line": 3,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "console": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 9,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 15,
+ "line": 9,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 4,
+ "line": 9,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 9,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for function declarations: getScopes finds scope bindings for function declarations at line 3 column 20 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "p1": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 21,
+ "line": 3,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 17,
+ "line": 3,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 15,
+ "line": 3,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "fn-param",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "outer",
+ "end": Object {
+ "column": 21,
+ "line": 3,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 15,
+ "line": 3,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "outer": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 21,
+ "line": 3,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 14,
+ "line": 3,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "console": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 9,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 15,
+ "line": 9,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 4,
+ "line": 9,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 9,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for function declarations: getScopes finds scope bindings for function declarations at line 5 column 1 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "middle": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 3,
+ "line": 10,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 6,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 17,
+ "line": 6,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 11,
+ "line": 6,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "let",
+ },
+ },
+ "displayName": "Block",
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 5,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "outer": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 21,
+ "line": 3,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 14,
+ "line": 3,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "console": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 9,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 15,
+ "line": 9,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 4,
+ "line": 9,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 9,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for function declarations: getScopes finds scope bindings for function declarations at line 9 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "inner": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 25,
+ "line": 7,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 7,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 18,
+ "line": 7,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 13,
+ "line": 7,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "var",
+ },
+ "p2": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 3,
+ "line": 10,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 6,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 20,
+ "line": 6,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 18,
+ "line": 6,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "fn-param",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 20,
+ "line": 9,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 16,
+ "line": 9,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "implicit",
+ },
+ },
+ "displayName": "middle",
+ "end": Object {
+ "column": 3,
+ "line": 10,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 18,
+ "line": 6,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "middle": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 3,
+ "line": 10,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 6,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 17,
+ "line": 6,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 11,
+ "line": 6,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "let",
+ },
+ },
+ "displayName": "Block",
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 5,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "outer": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 21,
+ "line": 3,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 14,
+ "line": 3,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "console": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 9,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 15,
+ "line": 9,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 4,
+ "line": 9,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 9,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/function-declaration/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for function expressions: getScopes finds scope bindings for function expressions at line 2 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "fn": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 25,
+ "line": 3,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 6,
+ "line": 3,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 3,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ "fn2": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 2,
+ "line": 7,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 5,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 7,
+ "line": 5,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 5,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 8,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 8,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "console": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 9,
+ "line": 6,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 13,
+ "line": 6,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 2,
+ "line": 6,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 6,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 8,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for function expressions: getScopes finds scope bindings for function expressions at line 3 column 23 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "p1": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 24,
+ "line": 3,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 20,
+ "line": 3,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "start": Object {
+ "column": 18,
+ "line": 3,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "type": "fn-param",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "fn",
+ "end": Object {
+ "column": 24,
+ "line": 3,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 18,
+ "line": 3,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "fn": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 25,
+ "line": 3,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 6,
+ "line": 3,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 3,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ "fn2": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 2,
+ "line": 7,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 5,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 7,
+ "line": 5,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 5,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 8,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 8,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "console": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 9,
+ "line": 6,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 13,
+ "line": 6,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 2,
+ "line": 6,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 6,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 8,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for function expressions: getScopes finds scope bindings for function expressions at line 6 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "p2": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 7,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "start": Object {
+ "column": 10,
+ "line": 5,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 30,
+ "line": 5,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "start": Object {
+ "column": 28,
+ "line": 5,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "type": "fn-param",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 18,
+ "line": 6,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 14,
+ "line": 6,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "implicit",
+ },
+ },
+ "displayName": "withName",
+ "end": Object {
+ "column": 1,
+ "line": 7,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 28,
+ "line": 5,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "withName": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 7,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "start": Object {
+ "column": 10,
+ "line": 5,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 27,
+ "line": 5,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "start": Object {
+ "column": 19,
+ "line": 5,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "type": "fn-expr",
+ },
+ ],
+ "type": "const",
+ },
+ },
+ "displayName": "Function Expression",
+ "end": Object {
+ "column": 1,
+ "line": 7,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 10,
+ "line": 5,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "fn": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 25,
+ "line": 3,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 6,
+ "line": 3,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 3,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ "fn2": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 2,
+ "line": 7,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 5,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 7,
+ "line": 5,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 5,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 8,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 8,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "console": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 9,
+ "line": 6,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 13,
+ "line": 6,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 2,
+ "line": 6,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 6,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 8,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/function-expression/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for out of order declarations: getScopes finds scope bindings for out of order declarations at line 2 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "aDefault": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 33,
+ "line": 21,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 21,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 15,
+ "line": 21,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "importName": "default",
+ "start": Object {
+ "column": 7,
+ "line": 21,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "import-decl",
+ },
+ ],
+ "type": "import",
+ },
+ "root": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 19,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 15,
+ "line": 3,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 28,
+ "line": 3,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 24,
+ "line": 3,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ Object {
+ "end": Object {
+ "column": 39,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 35,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "val": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 8,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 7,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 22,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 22,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "console": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 15,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 4,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 22,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for out of order declarations: getScopes finds scope bindings for out of order declarations at line 5 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "callback": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 3,
+ "line": 16,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 10,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 19,
+ "line": 10,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 11,
+ "line": 10,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ Object {
+ "end": Object {
+ "column": 33,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 25,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 10,
+ "line": 18,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 12,
+ "line": 18,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "parent": null,
+ "start": Object {
+ "column": 2,
+ "line": 18,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "call",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 18,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "var",
+ },
+ "fn": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 9,
+ "line": 6,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 6,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 8,
+ "line": 6,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 6,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 6,
+ "line": 8,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 2,
+ "line": 8,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "implicit",
+ },
+ "val": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 10,
+ "line": 4,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 4,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 9,
+ "line": 4,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 4,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ },
+ "displayName": "root",
+ "end": Object {
+ "column": 1,
+ "line": 19,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 15,
+ "line": 3,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "aDefault": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 33,
+ "line": 21,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 21,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 15,
+ "line": 21,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "importName": "default",
+ "start": Object {
+ "column": 7,
+ "line": 21,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "import-decl",
+ },
+ ],
+ "type": "import",
+ },
+ "root": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 19,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 15,
+ "line": 3,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 28,
+ "line": 3,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 24,
+ "line": 3,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ Object {
+ "end": Object {
+ "column": 39,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 35,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "val": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 8,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 7,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 22,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 22,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "console": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 15,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 4,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 22,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for out of order declarations: getScopes finds scope bindings for out of order declarations at line 11 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "fn": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 19,
+ "line": 15,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 15,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 15,
+ "line": 15,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 13,
+ "line": 15,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ Object {
+ "end": Object {
+ "column": 23,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 21,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 45,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 41,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "implicit",
+ },
+ "val": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 13,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 13,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 13,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 13,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "var",
+ },
+ Object {
+ "end": Object {
+ "column": 19,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 16,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "var",
+ },
+ },
+ "displayName": "callback",
+ "end": Object {
+ "column": 3,
+ "line": 16,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 2,
+ "line": 10,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "callback": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 3,
+ "line": 16,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 10,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 19,
+ "line": 10,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 11,
+ "line": 10,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ Object {
+ "end": Object {
+ "column": 33,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 25,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 10,
+ "line": 18,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 12,
+ "line": 18,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "parent": null,
+ "start": Object {
+ "column": 2,
+ "line": 18,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "call",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 18,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "var",
+ },
+ "fn": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 9,
+ "line": 6,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 6,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 8,
+ "line": 6,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 6,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 6,
+ "line": 8,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 2,
+ "line": 8,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "implicit",
+ },
+ "val": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 10,
+ "line": 4,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 4,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 9,
+ "line": 4,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 4,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ },
+ "displayName": "root",
+ "end": Object {
+ "column": 1,
+ "line": 19,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 15,
+ "line": 3,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "aDefault": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 33,
+ "line": 21,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 21,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 15,
+ "line": 21,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "importName": "default",
+ "start": Object {
+ "column": 7,
+ "line": 21,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "import-decl",
+ },
+ ],
+ "type": "import",
+ },
+ "root": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 19,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 15,
+ "line": 3,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 28,
+ "line": 3,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 24,
+ "line": 3,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ Object {
+ "end": Object {
+ "column": 39,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 35,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "val": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 8,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 7,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 22,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 22,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "console": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 15,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 4,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 22,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for out of order declarations: getScopes finds scope bindings for out of order declarations at line 14 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "fn": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 19,
+ "line": 15,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 15,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 15,
+ "line": 15,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 13,
+ "line": 15,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ Object {
+ "end": Object {
+ "column": 23,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 21,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 45,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 41,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "implicit",
+ },
+ "val": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 13,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 13,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 13,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 13,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "var",
+ },
+ Object {
+ "end": Object {
+ "column": 19,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 16,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "var",
+ },
+ },
+ "displayName": "callback",
+ "end": Object {
+ "column": 3,
+ "line": 16,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 2,
+ "line": 10,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "callback": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 3,
+ "line": 16,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 10,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 19,
+ "line": 10,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 11,
+ "line": 10,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ Object {
+ "end": Object {
+ "column": 33,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 25,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 10,
+ "line": 18,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 12,
+ "line": 18,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "parent": null,
+ "start": Object {
+ "column": 2,
+ "line": 18,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "call",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 18,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "var",
+ },
+ "fn": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 9,
+ "line": 6,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 6,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 8,
+ "line": 6,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 6,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 6,
+ "line": 8,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 2,
+ "line": 8,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "implicit",
+ },
+ "val": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 10,
+ "line": 4,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 4,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 9,
+ "line": 4,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 4,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ },
+ "displayName": "root",
+ "end": Object {
+ "column": 1,
+ "line": 19,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 15,
+ "line": 3,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "aDefault": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 33,
+ "line": 21,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 21,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 15,
+ "line": 21,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "importName": "default",
+ "start": Object {
+ "column": 7,
+ "line": 21,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "import-decl",
+ },
+ ],
+ "type": "import",
+ },
+ "root": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 19,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 15,
+ "line": 3,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 28,
+ "line": 3,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 24,
+ "line": 3,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ Object {
+ "end": Object {
+ "column": 39,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 35,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "val": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 8,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 7,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 22,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 22,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "console": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 15,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 4,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 22,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for out of order declarations: getScopes finds scope bindings for out of order declarations at line 17 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "callback": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 3,
+ "line": 16,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 10,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 19,
+ "line": 10,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 11,
+ "line": 10,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ Object {
+ "end": Object {
+ "column": 33,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 25,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 10,
+ "line": 18,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 12,
+ "line": 18,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "parent": null,
+ "start": Object {
+ "column": 2,
+ "line": 18,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "call",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 18,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "var",
+ },
+ "fn": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 9,
+ "line": 6,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 6,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 8,
+ "line": 6,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 6,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 6,
+ "line": 8,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 2,
+ "line": 8,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "implicit",
+ },
+ "val": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 10,
+ "line": 4,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 4,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 9,
+ "line": 4,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 4,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ },
+ "displayName": "root",
+ "end": Object {
+ "column": 1,
+ "line": 19,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 15,
+ "line": 3,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "aDefault": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 33,
+ "line": 21,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 21,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 15,
+ "line": 21,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "importName": "default",
+ "start": Object {
+ "column": 7,
+ "line": 21,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "import-decl",
+ },
+ ],
+ "type": "import",
+ },
+ "root": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 19,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 15,
+ "line": 3,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 28,
+ "line": 3,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 24,
+ "line": 3,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ Object {
+ "end": Object {
+ "column": 39,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 35,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "val": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 8,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 7,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 22,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 22,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "console": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 15,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 4,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 11,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 22,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/out-of-order-declarations/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for switch statements: getScopes finds scope bindings for switch statements at line 2 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "zero": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 13,
+ "line": 5,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 5,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 5,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 5,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 23,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 23,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "foo": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 3,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 8,
+ "line": 3,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 15,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 8,
+ "line": 15,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 19,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 8,
+ "line": 19,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 23,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for switch statements: getScopes finds scope bindings for switch statements at line 5 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "one": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 7,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 7,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 7,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 7,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ "two": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 9,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 9,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 9,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 9,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ },
+ "displayName": "Switch",
+ "end": Object {
+ "column": 1,
+ "line": 13,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "zero": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 13,
+ "line": 5,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 5,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 5,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 5,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 23,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 23,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "foo": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 3,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 8,
+ "line": 3,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 15,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 8,
+ "line": 15,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 19,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 8,
+ "line": 19,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 23,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for switch statements: getScopes finds scope bindings for switch statements at line 7 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "one": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 7,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 7,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 7,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 7,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ "two": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 9,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 9,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 9,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 9,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ },
+ "displayName": "Switch",
+ "end": Object {
+ "column": 1,
+ "line": 13,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "zero": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 13,
+ "line": 5,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 5,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 5,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 5,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 23,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 23,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "foo": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 3,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 8,
+ "line": 3,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 15,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 8,
+ "line": 15,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 19,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 8,
+ "line": 19,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 23,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for switch statements: getScopes finds scope bindings for switch statements at line 9 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "one": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 7,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 7,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 7,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 7,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ "two": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 9,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 9,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 9,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 9,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ },
+ "displayName": "Switch",
+ "end": Object {
+ "column": 1,
+ "line": 13,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "zero": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 13,
+ "line": 5,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 5,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 5,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 5,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 23,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 23,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "foo": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 3,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 8,
+ "line": 3,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 15,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 8,
+ "line": 15,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 19,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 8,
+ "line": 19,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 23,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for switch statements: getScopes finds scope bindings for switch statements at line 11 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "three": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 14,
+ "line": 11,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 11,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 11,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 11,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ },
+ "displayName": "Block",
+ "end": Object {
+ "column": 3,
+ "line": 12,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 16,
+ "line": 10,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "one": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 7,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 7,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 7,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 7,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ "two": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 9,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 9,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 9,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 9,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ },
+ "displayName": "Switch",
+ "end": Object {
+ "column": 1,
+ "line": 13,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "zero": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 13,
+ "line": 5,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 5,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 5,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 5,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 23,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 23,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "foo": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 3,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 8,
+ "line": 3,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 15,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 8,
+ "line": 15,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 19,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 8,
+ "line": 19,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 23,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for switch statements: getScopes finds scope bindings for switch statements at line 17 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "two": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 20,
+ "line": 17,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 17,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 16,
+ "line": 17,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 13,
+ "line": 17,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "let",
+ },
+ },
+ "displayName": "Switch",
+ "end": Object {
+ "column": 1,
+ "line": 18,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 15,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "zero": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 13,
+ "line": 5,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 5,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 5,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 5,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 23,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 23,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "foo": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 3,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 8,
+ "line": 3,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 15,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 8,
+ "line": 15,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 19,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 8,
+ "line": 19,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 23,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for switch statements: getScopes finds scope bindings for switch statements at line 21 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "three": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 18,
+ "line": 21,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 21,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 15,
+ "line": 21,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 10,
+ "line": 21,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "class-decl",
+ },
+ ],
+ "type": "let",
+ },
+ },
+ "displayName": "Switch",
+ "end": Object {
+ "column": 1,
+ "line": 22,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 19,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "zero": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 13,
+ "line": 5,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 5,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 5,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 5,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 23,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 23,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "foo": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 3,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 8,
+ "line": 3,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 15,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 8,
+ "line": 15,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 19,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 8,
+ "line": 19,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 23,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/switch-statement/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for try..catch: getScopes finds scope bindings for try..catch at line 2 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "first": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 4,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 4,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 4,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 4,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "third": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 7,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 7,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 7,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 7,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 10,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 10,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 10,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for try..catch: getScopes finds scope bindings for try..catch at line 4 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "second": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 13,
+ "line": 5,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 5,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 5,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 5,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ },
+ "displayName": "Block",
+ "end": Object {
+ "column": 1,
+ "line": 6,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 4,
+ "line": 3,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "first": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 4,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 4,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 4,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 4,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "third": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 7,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 7,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 7,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 7,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 10,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 10,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 10,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings for try..catch: getScopes finds scope bindings for try..catch at line 7 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "fourth": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 13,
+ "line": 8,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 8,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 8,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 8,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ },
+ "displayName": "Block",
+ "end": Object {
+ "column": 1,
+ "line": 9,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 14,
+ "line": 6,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "err": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 9,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 6,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 6,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 6,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "type": "catch",
+ },
+ ],
+ "type": "var",
+ },
+ },
+ "displayName": "Catch",
+ "end": Object {
+ "column": 1,
+ "line": 9,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 2,
+ "line": 6,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "first": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 4,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 4,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 4,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 4,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "third": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 7,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 7,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 7,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 7,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 10,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 10,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 10,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/try-catch/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings in a JSX element: getScopes finds scope bindings in a JSX element at line 2 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "SomeComponent": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 29,
+ "line": 1,
+ "sourceId": "scopes/jsx-component/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/jsx-component/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 20,
+ "line": 1,
+ "sourceId": "scopes/jsx-component/originalSource-1",
+ },
+ "importName": "default",
+ "start": Object {
+ "column": 7,
+ "line": 1,
+ "sourceId": "scopes/jsx-component/originalSource-1",
+ },
+ "type": "import-decl",
+ },
+ Object {
+ "end": Object {
+ "column": 14,
+ "line": 3,
+ "sourceId": "scopes/jsx-component/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 1,
+ "line": 3,
+ "sourceId": "scopes/jsx-component/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 14,
+ "line": 4,
+ "sourceId": "scopes/jsx-component/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 1,
+ "line": 4,
+ "sourceId": "scopes/jsx-component/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 14,
+ "line": 5,
+ "sourceId": "scopes/jsx-component/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 1,
+ "line": 5,
+ "sourceId": "scopes/jsx-component/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 14,
+ "line": 6,
+ "sourceId": "scopes/jsx-component/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 1,
+ "line": 6,
+ "sourceId": "scopes/jsx-component/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "import",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 7,
+ "sourceId": "scopes/jsx-component/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/jsx-component/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 7,
+ "sourceId": "scopes/jsx-component/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/jsx-component/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 7,
+ "sourceId": "scopes/jsx-component/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/jsx-component/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings in a module: getScopes finds scope bindings in a module at line 7 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "fn": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 16,
+ "line": 9,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 9,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 9,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 9,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "var",
+ },
+ "foo": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 22,
+ "line": 1,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 10,
+ "line": 1,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ "importName": "default",
+ "start": Object {
+ "column": 7,
+ "line": 1,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ "type": "import-decl",
+ },
+ Object {
+ "end": Object {
+ "column": 15,
+ "line": 3,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 12,
+ "line": 3,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "import",
+ },
+ "one": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 5,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 5,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 7,
+ "line": 5,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 5,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 4,
+ "line": 11,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 0,
+ "line": 11,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "implicit",
+ },
+ "three": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 16,
+ "line": 7,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 7,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 7,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 7,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ "type": "const",
+ },
+ ],
+ "type": "const",
+ },
+ "two": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 12,
+ "line": 6,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 6,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 7,
+ "line": 6,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 6,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "console": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 7,
+ "line": 3,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 11,
+ "line": 3,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ "parent": null,
+ "property": "log",
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 12,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/simple-module/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings in a typescript file: getScopes finds scope bindings in a typescript file at line 9 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "Color": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 8,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 10,
+ "line": 3,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 3,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "ts-enum-decl",
+ },
+ ],
+ "type": "const",
+ },
+ "Example": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 19,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 10,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 10,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 10,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "class-decl",
+ },
+ Object {
+ "end": Object {
+ "column": 16,
+ "line": 41,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 9,
+ "line": 41,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "let",
+ },
+ "TheSpace": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 35,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 31,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 18,
+ "line": 31,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 10,
+ "line": 31,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "ts-namespace-decl",
+ },
+ ],
+ "type": "const",
+ },
+ "foo": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 22,
+ "line": 22,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 22,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 7,
+ "line": 22,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 22,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 42,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 42,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "Error": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 19,
+ "line": 17,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 14,
+ "line": 17,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ "window": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 21,
+ "line": 22,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 15,
+ "line": 22,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 7,
+ "line": 25,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 1,
+ "line": 25,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 7,
+ "line": 28,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 1,
+ "line": 28,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 42,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings in a typescript file: getScopes finds scope bindings in a typescript file at line 13 column 4 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "foo": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 3,
+ "line": 14,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 12,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 24,
+ "line": 12,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 21,
+ "line": 12,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "fn-param",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "constructor",
+ "end": Object {
+ "column": 3,
+ "line": 14,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 14,
+ "line": 12,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "Example": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 19,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 10,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 10,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 10,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "class-inner",
+ },
+ ],
+ "type": "const",
+ },
+ },
+ "displayName": "Class",
+ "end": Object {
+ "column": 1,
+ "line": 19,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 10,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "Color": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 8,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 10,
+ "line": 3,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 3,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "ts-enum-decl",
+ },
+ ],
+ "type": "const",
+ },
+ "Example": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 19,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 10,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 10,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 10,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "class-decl",
+ },
+ Object {
+ "end": Object {
+ "column": 16,
+ "line": 41,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 9,
+ "line": 41,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "let",
+ },
+ "TheSpace": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 35,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 31,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 18,
+ "line": 31,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 10,
+ "line": 31,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "ts-namespace-decl",
+ },
+ ],
+ "type": "const",
+ },
+ "foo": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 22,
+ "line": 22,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 22,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 7,
+ "line": 22,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 22,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 42,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 42,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "Error": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 19,
+ "line": 17,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 14,
+ "line": 17,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ "window": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 21,
+ "line": 22,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 15,
+ "line": 22,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 7,
+ "line": 25,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 1,
+ "line": 25,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 7,
+ "line": 28,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 1,
+ "line": 28,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 42,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings in a typescript file: getScopes finds scope bindings in a typescript file at line 17 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "method",
+ "end": Object {
+ "column": 3,
+ "line": 18,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 2,
+ "line": 16,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "Example": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 19,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 10,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 10,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 10,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "class-inner",
+ },
+ ],
+ "type": "const",
+ },
+ },
+ "displayName": "Class",
+ "end": Object {
+ "column": 1,
+ "line": 19,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 10,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "Color": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 8,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 10,
+ "line": 3,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 3,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "ts-enum-decl",
+ },
+ ],
+ "type": "const",
+ },
+ "Example": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 19,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 10,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 10,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 10,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "class-decl",
+ },
+ Object {
+ "end": Object {
+ "column": 16,
+ "line": 41,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 9,
+ "line": 41,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "let",
+ },
+ "TheSpace": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 35,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 31,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 18,
+ "line": 31,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 10,
+ "line": 31,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "ts-namespace-decl",
+ },
+ ],
+ "type": "const",
+ },
+ "foo": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 22,
+ "line": 22,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 22,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 7,
+ "line": 22,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 22,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 42,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 42,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "Error": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 19,
+ "line": 17,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 14,
+ "line": 17,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ "window": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 21,
+ "line": 22,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 15,
+ "line": 22,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 7,
+ "line": 25,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 1,
+ "line": 25,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 7,
+ "line": 28,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 1,
+ "line": 28,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 42,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings in a typescript file: getScopes finds scope bindings in a typescript file at line 33 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "fn",
+ "end": Object {
+ "column": 3,
+ "line": 34,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 2,
+ "line": 32,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "fn": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 3,
+ "line": 34,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 32,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 32,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 11,
+ "line": 32,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "let",
+ },
+ },
+ "displayName": "TypeScript Namespace",
+ "end": Object {
+ "column": 1,
+ "line": 35,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 19,
+ "line": 31,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "Color": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 8,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 10,
+ "line": 3,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 3,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "ts-enum-decl",
+ },
+ ],
+ "type": "const",
+ },
+ "Example": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 19,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 10,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 10,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 10,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "class-decl",
+ },
+ Object {
+ "end": Object {
+ "column": 16,
+ "line": 41,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 9,
+ "line": 41,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "let",
+ },
+ "TheSpace": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 35,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 31,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 18,
+ "line": 31,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 10,
+ "line": 31,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "ts-namespace-decl",
+ },
+ ],
+ "type": "const",
+ },
+ "foo": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 22,
+ "line": 22,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 22,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 7,
+ "line": 22,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 22,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 42,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 42,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "Error": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 19,
+ "line": 17,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 14,
+ "line": 17,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ "window": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 21,
+ "line": 22,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 15,
+ "line": 22,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 7,
+ "line": 25,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 1,
+ "line": 25,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 7,
+ "line": 28,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 1,
+ "line": 28,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 42,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/ts-sample/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings in a typescript-jsx file: getScopes finds scope bindings in a typescript-jsx file at line 9 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "Color": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 8,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 10,
+ "line": 3,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 3,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "ts-enum-decl",
+ },
+ ],
+ "type": "const",
+ },
+ "Example": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 19,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 10,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 10,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 10,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "class-decl",
+ },
+ Object {
+ "end": Object {
+ "column": 16,
+ "line": 41,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 9,
+ "line": 41,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "let",
+ },
+ "TheSpace": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 35,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 31,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 18,
+ "line": 31,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 10,
+ "line": 31,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "ts-namespace-decl",
+ },
+ ],
+ "type": "const",
+ },
+ "foo": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 28,
+ "line": 22,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 22,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 7,
+ "line": 22,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 22,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 42,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 42,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "Error": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 19,
+ "line": 17,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 14,
+ "line": 17,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ "any": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 14,
+ "line": 22,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 11,
+ "line": 22,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ "window": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 7,
+ "line": 25,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 1,
+ "line": 25,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 7,
+ "line": 28,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 1,
+ "line": 28,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 42,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings in a typescript-jsx file: getScopes finds scope bindings in a typescript-jsx file at line 13 column 4 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "foo": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 3,
+ "line": 14,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 12,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 24,
+ "line": 12,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 21,
+ "line": 12,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "fn-param",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "constructor",
+ "end": Object {
+ "column": 3,
+ "line": 14,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 14,
+ "line": 12,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "Example": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 19,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 10,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 10,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 10,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "class-inner",
+ },
+ ],
+ "type": "const",
+ },
+ },
+ "displayName": "Class",
+ "end": Object {
+ "column": 1,
+ "line": 19,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 10,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "Color": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 8,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 10,
+ "line": 3,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 3,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "ts-enum-decl",
+ },
+ ],
+ "type": "const",
+ },
+ "Example": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 19,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 10,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 10,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 10,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "class-decl",
+ },
+ Object {
+ "end": Object {
+ "column": 16,
+ "line": 41,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 9,
+ "line": 41,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "let",
+ },
+ "TheSpace": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 35,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 31,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 18,
+ "line": 31,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 10,
+ "line": 31,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "ts-namespace-decl",
+ },
+ ],
+ "type": "const",
+ },
+ "foo": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 28,
+ "line": 22,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 22,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 7,
+ "line": 22,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 22,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 42,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 42,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "Error": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 19,
+ "line": 17,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 14,
+ "line": 17,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ "any": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 14,
+ "line": 22,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 11,
+ "line": 22,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ "window": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 7,
+ "line": 25,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 1,
+ "line": 25,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 7,
+ "line": 28,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 1,
+ "line": 28,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 42,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings in a typescript-jsx file: getScopes finds scope bindings in a typescript-jsx file at line 17 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "method",
+ "end": Object {
+ "column": 3,
+ "line": 18,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 2,
+ "line": 16,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "Example": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 19,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 10,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 10,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 10,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "class-inner",
+ },
+ ],
+ "type": "const",
+ },
+ },
+ "displayName": "Class",
+ "end": Object {
+ "column": 1,
+ "line": 19,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 10,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "Color": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 8,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 10,
+ "line": 3,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 3,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "ts-enum-decl",
+ },
+ ],
+ "type": "const",
+ },
+ "Example": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 19,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 10,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 10,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 10,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "class-decl",
+ },
+ Object {
+ "end": Object {
+ "column": 16,
+ "line": 41,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 9,
+ "line": 41,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "let",
+ },
+ "TheSpace": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 35,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 31,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 18,
+ "line": 31,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 10,
+ "line": 31,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "ts-namespace-decl",
+ },
+ ],
+ "type": "const",
+ },
+ "foo": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 28,
+ "line": 22,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 22,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 7,
+ "line": 22,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 22,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 42,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 42,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "Error": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 19,
+ "line": 17,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 14,
+ "line": 17,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ "any": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 14,
+ "line": 22,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 11,
+ "line": 22,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ "window": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 7,
+ "line": 25,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 1,
+ "line": 25,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 7,
+ "line": 28,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 1,
+ "line": 28,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 42,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings in a typescript-jsx file: getScopes finds scope bindings in a typescript-jsx file at line 33 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "fn",
+ "end": Object {
+ "column": 3,
+ "line": 34,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 2,
+ "line": 32,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "fn": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 3,
+ "line": 34,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 32,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 32,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 11,
+ "line": 32,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "let",
+ },
+ },
+ "displayName": "TypeScript Namespace",
+ "end": Object {
+ "column": 1,
+ "line": 35,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 19,
+ "line": 31,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "Color": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 8,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 10,
+ "line": 3,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 5,
+ "line": 3,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "ts-enum-decl",
+ },
+ ],
+ "type": "const",
+ },
+ "Example": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 19,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 10,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 10,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 10,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "class-decl",
+ },
+ Object {
+ "end": Object {
+ "column": 16,
+ "line": 41,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 9,
+ "line": 41,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "let",
+ },
+ "TheSpace": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 35,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 31,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 18,
+ "line": 31,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 10,
+ "line": 31,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "ts-namespace-decl",
+ },
+ ],
+ "type": "const",
+ },
+ "foo": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 28,
+ "line": 22,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 22,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 7,
+ "line": 22,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 22,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 42,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 42,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "Error": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 19,
+ "line": 17,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 14,
+ "line": 17,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ "any": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 14,
+ "line": 22,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 11,
+ "line": 22,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ "window": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 7,
+ "line": 25,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 1,
+ "line": 25,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 7,
+ "line": 28,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 1,
+ "line": 28,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 42,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/tsx-sample/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings in a vue file: getScopes finds scope bindings in a vue file at line 14 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "fnVar": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 18,
+ "line": 13,
+ "sourceId": "scopes/vue-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 13,
+ "sourceId": "scopes/vue-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 13,
+ "sourceId": "scopes/vue-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 13,
+ "sourceId": "scopes/vue-sample/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "data",
+ "end": Object {
+ "column": 3,
+ "line": 18,
+ "sourceId": "scopes/vue-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 2,
+ "line": 12,
+ "sourceId": "scopes/vue-sample/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "moduleVar": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 23,
+ "line": 8,
+ "sourceId": "scopes/vue-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 8,
+ "sourceId": "scopes/vue-sample/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 8,
+ "sourceId": "scopes/vue-sample/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 8,
+ "sourceId": "scopes/vue-sample/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 1,
+ "line": 27,
+ "sourceId": "scopes/vue-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/vue-sample/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 1,
+ "line": 27,
+ "sourceId": "scopes/vue-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/vue-sample/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Global",
+ "end": Object {
+ "column": 1,
+ "line": 27,
+ "sourceId": "scopes/vue-sample/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/vue-sample/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings in fn body with both lex and non-lex items: getScopes finds scope bindings in fn body with both lex and non-lex items at line 4 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "lex": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 10,
+ "line": 3,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 3,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 3,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ },
+ "displayName": "Function Body",
+ "end": Object {
+ "column": 1,
+ "line": 5,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 14,
+ "line": 1,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "nonlex": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 13,
+ "line": 2,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 2,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 2,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 2,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "fn",
+ "end": Object {
+ "column": 1,
+ "line": 5,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 24,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "fn": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 5,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 1,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 1,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "var",
+ },
+ "fn2": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 7,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 7,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 7,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "var",
+ },
+ "fn3": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 17,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 13,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 13,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 13,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "var",
+ },
+ "fn4": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 23,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 19,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 19,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 19,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 24,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings in fn body with both lex and non-lex items: getScopes finds scope bindings in fn body with both lex and non-lex items at line 10 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "lex": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 10,
+ "line": 9,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 9,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 9,
+ "line": 9,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 9,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ },
+ "displayName": "Function Body",
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 15,
+ "line": 7,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "nonlex": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 21,
+ "line": 8,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 8,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 17,
+ "line": 8,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 11,
+ "line": 8,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "fn2",
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 7,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 24,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "fn": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 5,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 1,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 1,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "var",
+ },
+ "fn2": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 7,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 7,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 7,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "var",
+ },
+ "fn3": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 17,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 13,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 13,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 13,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "var",
+ },
+ "fn4": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 23,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 19,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 19,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 19,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 24,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings in fn body with both lex and non-lex items: getScopes finds scope bindings in fn body with both lex and non-lex items at line 16 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "Thing": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 16,
+ "line": 15,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 15,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 15,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 15,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "class-decl",
+ },
+ ],
+ "type": "let",
+ },
+ },
+ "displayName": "Function Body",
+ "end": Object {
+ "column": 1,
+ "line": 17,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 15,
+ "line": 13,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "nonlex": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 13,
+ "line": 14,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 14,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 14,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 14,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "fn3",
+ "end": Object {
+ "column": 1,
+ "line": 17,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 13,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 24,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "fn": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 5,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 1,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 1,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "var",
+ },
+ "fn2": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 7,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 7,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 7,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "var",
+ },
+ "fn3": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 17,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 13,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 13,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 13,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "var",
+ },
+ "fn4": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 23,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 19,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 19,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 19,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 24,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings in fn body with both lex and non-lex items: getScopes finds scope bindings in fn body with both lex and non-lex items at line 22 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "Thing": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 16,
+ "line": 21,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 21,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 13,
+ "line": 21,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 8,
+ "line": 21,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "class-decl",
+ },
+ ],
+ "type": "let",
+ },
+ },
+ "displayName": "Function Body",
+ "end": Object {
+ "column": 1,
+ "line": 23,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 15,
+ "line": 19,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "nonlex": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 21,
+ "line": 20,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 20,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 17,
+ "line": 20,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 11,
+ "line": 20,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "fn4",
+ "end": Object {
+ "column": 1,
+ "line": 23,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 19,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 24,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "fn": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 5,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 1,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 1,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "var",
+ },
+ "fn2": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 7,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 7,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 7,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "var",
+ },
+ "fn3": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 17,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 13,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 13,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 13,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "var",
+ },
+ "fn4": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 23,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 19,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 19,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 19,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "var",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 24,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/fn-body-lex-and-nonlex/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings with expression metadata: getScopes finds scope bindings with expression metadata at line 2 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "foo": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 15,
+ "line": 1,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 9,
+ "line": 1,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 1,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "type": "const",
+ },
+ Object {
+ "end": Object {
+ "column": 3,
+ "line": 3,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 7,
+ "line": 3,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "parent": Object {
+ "end": Object {
+ "column": 9,
+ "line": 3,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "parent": Object {
+ "end": Object {
+ "column": 13,
+ "line": 3,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "parent": null,
+ "property": "baz",
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "type": "call",
+ },
+ "property": "bar",
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 7,
+ "line": 4,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 11,
+ "line": 4,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "parent": Object {
+ "end": Object {
+ "column": 12,
+ "line": 4,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "parent": Object {
+ "end": Object {
+ "column": 14,
+ "line": 4,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "parent": Object {
+ "end": Object {
+ "column": 18,
+ "line": 4,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "parent": null,
+ "property": "baz",
+ "start": Object {
+ "column": 0,
+ "line": 4,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 4,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "type": "call",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 4,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "type": "inherit",
+ },
+ "property": "bar",
+ "start": Object {
+ "column": 4,
+ "line": 4,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 4,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 10,
+ "line": 5,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 14,
+ "line": 5,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "parent": Object {
+ "end": Object {
+ "column": 15,
+ "line": 5,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "parent": Object {
+ "end": Object {
+ "column": 17,
+ "line": 5,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "parent": Object {
+ "end": Object {
+ "column": 21,
+ "line": 5,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "parent": null,
+ "property": "baz",
+ "start": Object {
+ "column": 0,
+ "line": 5,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 5,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "type": "call",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 5,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "type": "inherit",
+ },
+ "property": "bar",
+ "start": Object {
+ "column": 7,
+ "line": 5,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 7,
+ "line": 5,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "type": "ref",
+ },
+ Object {
+ "end": Object {
+ "column": 25,
+ "line": 6,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 29,
+ "line": 6,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "parent": Object {
+ "end": Object {
+ "column": 30,
+ "line": 6,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "parent": Object {
+ "end": Object {
+ "column": 32,
+ "line": 6,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "parent": Object {
+ "end": Object {
+ "column": 36,
+ "line": 6,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "parent": null,
+ "property": "baz",
+ "start": Object {
+ "column": 0,
+ "line": 6,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 6,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "type": "call",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 6,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "type": "inherit",
+ },
+ "property": "bar",
+ "start": Object {
+ "column": 22,
+ "line": 6,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 22,
+ "line": 6,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "const",
+ },
+ },
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 7,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "Object": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 6,
+ "line": 5,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 0,
+ "line": 5,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ "__webpack_require__": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 19,
+ "line": 6,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "meta": Object {
+ "end": Object {
+ "column": 21,
+ "line": 6,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "parent": null,
+ "property": "i",
+ "start": Object {
+ "column": 0,
+ "line": 6,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "type": "member",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 6,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "global",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 7,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/expressions/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings with proper types: getScopes finds scope bindings with proper types at line 5 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "aConst": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 18,
+ "line": 15,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 15,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 15,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 15,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "const",
+ },
+ ],
+ "type": "const",
+ },
+ "aLet": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 9,
+ "line": 14,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 8,
+ "line": 14,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 14,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ "aVar": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 9,
+ "line": 13,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 13,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 8,
+ "line": 13,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 13,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "cls": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 7,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 9,
+ "line": 7,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 7,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "class-decl",
+ },
+ ],
+ "type": "let",
+ },
+ "def": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 19,
+ "line": 1,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 10,
+ "line": 1,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "importName": "default",
+ "start": Object {
+ "column": 7,
+ "line": 1,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "import-decl",
+ },
+ ],
+ "type": "import",
+ },
+ "fn": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 16,
+ "line": 6,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 6,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 6,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 6,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "var",
+ },
+ "named": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 25,
+ "line": 2,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 2,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 14,
+ "line": 2,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "importName": "named",
+ "start": Object {
+ "column": 9,
+ "line": 2,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "import-decl",
+ },
+ ],
+ "type": "import",
+ },
+ "namespace": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 30,
+ "line": 4,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 4,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 21,
+ "line": 4,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 12,
+ "line": 4,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "import-ns-decl",
+ },
+ ],
+ "type": "const",
+ },
+ "otherNamed": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 39,
+ "line": 3,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 28,
+ "line": 3,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "importName": "thing",
+ "start": Object {
+ "column": 18,
+ "line": 3,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "import-decl",
+ },
+ ],
+ "type": "import",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 25,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 25,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 25,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings with proper types: getScopes finds scope bindings with proper types at line 9 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "method",
+ "end": Object {
+ "column": 3,
+ "line": 10,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 2,
+ "line": 8,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "cls": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 7,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 9,
+ "line": 7,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 7,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "class-inner",
+ },
+ ],
+ "type": "const",
+ },
+ },
+ "displayName": "Class",
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 7,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "aConst": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 18,
+ "line": 15,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 15,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 15,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 15,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "const",
+ },
+ ],
+ "type": "const",
+ },
+ "aLet": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 9,
+ "line": 14,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 8,
+ "line": 14,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 14,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ "aVar": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 9,
+ "line": 13,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 13,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 8,
+ "line": 13,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 13,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "cls": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 7,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 9,
+ "line": 7,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 7,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "class-decl",
+ },
+ ],
+ "type": "let",
+ },
+ "def": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 19,
+ "line": 1,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 10,
+ "line": 1,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "importName": "default",
+ "start": Object {
+ "column": 7,
+ "line": 1,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "import-decl",
+ },
+ ],
+ "type": "import",
+ },
+ "fn": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 16,
+ "line": 6,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 6,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 6,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 6,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "var",
+ },
+ "named": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 25,
+ "line": 2,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 2,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 14,
+ "line": 2,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "importName": "named",
+ "start": Object {
+ "column": 9,
+ "line": 2,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "import-decl",
+ },
+ ],
+ "type": "import",
+ },
+ "namespace": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 30,
+ "line": 4,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 4,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 21,
+ "line": 4,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 12,
+ "line": 4,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "import-ns-decl",
+ },
+ ],
+ "type": "const",
+ },
+ "otherNamed": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 39,
+ "line": 3,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 28,
+ "line": 3,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "importName": "thing",
+ "start": Object {
+ "column": 18,
+ "line": 3,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "import-decl",
+ },
+ ],
+ "type": "import",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 25,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 25,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 25,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings with proper types: getScopes finds scope bindings with proper types at line 18 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "arguments": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 11,
+ "line": 19,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 2,
+ "line": 19,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "implicit",
+ },
+ "this": Object {
+ "refs": Array [
+ Object {
+ "end": Object {
+ "column": 6,
+ "line": 18,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "meta": null,
+ "start": Object {
+ "column": 2,
+ "line": 18,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "ref",
+ },
+ ],
+ "type": "implicit",
+ },
+ },
+ "displayName": "inner",
+ "end": Object {
+ "column": 1,
+ "line": 20,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 1,
+ "line": 17,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "function",
+ },
+ Object {
+ "bindings": Object {
+ "inner": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 20,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 1,
+ "line": 17,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 15,
+ "line": 17,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 10,
+ "line": 17,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "fn-expr",
+ },
+ ],
+ "type": "const",
+ },
+ },
+ "displayName": "Function Expression",
+ "end": Object {
+ "column": 1,
+ "line": 20,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 1,
+ "line": 17,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "aConst": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 18,
+ "line": 15,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 15,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 15,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 15,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "const",
+ },
+ ],
+ "type": "const",
+ },
+ "aLet": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 9,
+ "line": 14,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 8,
+ "line": 14,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 14,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ "aVar": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 9,
+ "line": 13,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 13,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 8,
+ "line": 13,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 13,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "cls": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 7,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 9,
+ "line": 7,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 7,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "class-decl",
+ },
+ ],
+ "type": "let",
+ },
+ "def": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 19,
+ "line": 1,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 10,
+ "line": 1,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "importName": "default",
+ "start": Object {
+ "column": 7,
+ "line": 1,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "import-decl",
+ },
+ ],
+ "type": "import",
+ },
+ "fn": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 16,
+ "line": 6,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 6,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 6,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 6,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "var",
+ },
+ "named": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 25,
+ "line": 2,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 2,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 14,
+ "line": 2,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "importName": "named",
+ "start": Object {
+ "column": 9,
+ "line": 2,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "import-decl",
+ },
+ ],
+ "type": "import",
+ },
+ "namespace": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 30,
+ "line": 4,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 4,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 21,
+ "line": 4,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 12,
+ "line": 4,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "import-ns-decl",
+ },
+ ],
+ "type": "const",
+ },
+ "otherNamed": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 39,
+ "line": 3,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 28,
+ "line": 3,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "importName": "thing",
+ "start": Object {
+ "column": 18,
+ "line": 3,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "import-decl",
+ },
+ ],
+ "type": "import",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 25,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 25,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 25,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
+
+exports[`Parser.getScopes finds scope bindings with proper types: getScopes finds scope bindings with proper types at line 23 column 0 1`] = `
+Array [
+ Object {
+ "bindings": Object {
+ "blockFn": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 22,
+ "line": 23,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 2,
+ "line": 23,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 18,
+ "line": 23,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 11,
+ "line": 23,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "let",
+ },
+ },
+ "displayName": "Block",
+ "end": Object {
+ "column": 1,
+ "line": 24,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 22,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {
+ "aConst": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 18,
+ "line": 15,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 15,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 12,
+ "line": 15,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 15,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "const",
+ },
+ ],
+ "type": "const",
+ },
+ "aLet": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 9,
+ "line": 14,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 14,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 8,
+ "line": 14,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 14,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "let",
+ },
+ ],
+ "type": "let",
+ },
+ "aVar": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 9,
+ "line": 13,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 13,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 8,
+ "line": 13,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 4,
+ "line": 13,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "var",
+ },
+ ],
+ "type": "var",
+ },
+ "cls": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 1,
+ "line": 11,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 7,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 9,
+ "line": 7,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 6,
+ "line": 7,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "class-decl",
+ },
+ ],
+ "type": "let",
+ },
+ "def": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 19,
+ "line": 1,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 10,
+ "line": 1,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "importName": "default",
+ "start": Object {
+ "column": 7,
+ "line": 1,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "import-decl",
+ },
+ ],
+ "type": "import",
+ },
+ "fn": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 16,
+ "line": 6,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 6,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 11,
+ "line": 6,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 9,
+ "line": 6,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "fn-decl",
+ },
+ ],
+ "type": "var",
+ },
+ "named": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 25,
+ "line": 2,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 2,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 14,
+ "line": 2,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "importName": "named",
+ "start": Object {
+ "column": 9,
+ "line": 2,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "import-decl",
+ },
+ ],
+ "type": "import",
+ },
+ "namespace": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 30,
+ "line": 4,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 4,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 21,
+ "line": 4,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 12,
+ "line": 4,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "import-ns-decl",
+ },
+ ],
+ "type": "const",
+ },
+ "otherNamed": Object {
+ "refs": Array [
+ Object {
+ "declaration": Object {
+ "end": Object {
+ "column": 39,
+ "line": 3,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "start": Object {
+ "column": 0,
+ "line": 3,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ },
+ "end": Object {
+ "column": 28,
+ "line": 3,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "importName": "thing",
+ "start": Object {
+ "column": 18,
+ "line": 3,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "import-decl",
+ },
+ ],
+ "type": "import",
+ },
+ "this": Object {
+ "refs": Array [],
+ "type": "implicit",
+ },
+ },
+ "displayName": "Module",
+ "end": Object {
+ "column": 0,
+ "line": 25,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Lexical Global",
+ "end": Object {
+ "column": 0,
+ "line": 25,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "block",
+ },
+ Object {
+ "bindings": Object {},
+ "displayName": "Global",
+ "end": Object {
+ "column": 0,
+ "line": 25,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "scopeKind": "",
+ "start": Object {
+ "column": 0,
+ "line": 1,
+ "sourceId": "scopes/binding-types/originalSource-1",
+ },
+ "type": "object",
+ },
+]
+`;
diff --git a/devtools/client/debugger/src/workers/parser/tests/__snapshots__/getSymbols.spec.js.snap b/devtools/client/debugger/src/workers/parser/tests/__snapshots__/getSymbols.spec.js.snap
new file mode 100644
index 0000000000..d1fd35376d
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/__snapshots__/getSymbols.spec.js.snap
@@ -0,0 +1,169 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Parser.getSymbols allSymbols 1`] = `
+"hasJsx: false
+
+hasTypes: false
+
+framework: null"
+`;
+
+exports[`Parser.getSymbols call expression 1`] = `
+"hasJsx: false
+
+hasTypes: false
+
+framework: null"
+`;
+
+exports[`Parser.getSymbols call sites 1`] = `
+"hasJsx: false
+
+hasTypes: false
+
+framework: null"
+`;
+
+exports[`Parser.getSymbols class 1`] = `
+"hasJsx: false
+
+hasTypes: false
+
+framework: null"
+`;
+
+exports[`Parser.getSymbols component 1`] = `
+"hasJsx: true
+
+hasTypes: false
+
+framework: null"
+`;
+
+exports[`Parser.getSymbols destruct 1`] = `
+"hasJsx: false
+
+hasTypes: false
+
+framework: null"
+`;
+
+exports[`Parser.getSymbols es6 1`] = `
+"hasJsx: false
+
+hasTypes: false
+
+framework: null"
+`;
+
+exports[`Parser.getSymbols expression 1`] = `
+"hasJsx: false
+
+hasTypes: false
+
+framework: null"
+`;
+
+exports[`Parser.getSymbols finds symbols in an html file 1`] = `
+"hasJsx: false
+
+hasTypes: false
+
+framework: null"
+`;
+
+exports[`Parser.getSymbols flow 1`] = `
+"hasJsx: false
+
+hasTypes: true
+
+framework: null"
+`;
+
+exports[`Parser.getSymbols func 1`] = `
+"hasJsx: false
+
+hasTypes: false
+
+framework: null"
+`;
+
+exports[`Parser.getSymbols function names 1`] = `
+"hasJsx: false
+
+hasTypes: false
+
+framework: null"
+`;
+
+exports[`Parser.getSymbols jsx 1`] = `
+"hasJsx: true
+
+hasTypes: false
+
+framework: null"
+`;
+
+exports[`Parser.getSymbols math 1`] = `
+"hasJsx: false
+
+hasTypes: false
+
+framework: null"
+`;
+
+exports[`Parser.getSymbols object expressions 1`] = `
+"hasJsx: false
+
+hasTypes: false
+
+framework: null"
+`;
+
+exports[`Parser.getSymbols optional chaining 1`] = `
+"hasJsx: false
+
+hasTypes: false
+
+framework: null"
+`;
+
+exports[`Parser.getSymbols private fields 1`] = `
+"hasJsx: false
+
+hasTypes: false
+
+framework: null"
+`;
+
+exports[`Parser.getSymbols proto 1`] = `
+"hasJsx: false
+
+hasTypes: false
+
+framework: null"
+`;
+
+exports[`Parser.getSymbols react component 1`] = `
+"hasJsx: false
+
+hasTypes: false
+
+framework: React"
+`;
+
+exports[`Parser.getSymbols regexp 1`] = `
+"hasJsx: false
+
+hasTypes: false
+
+framework: null"
+`;
+
+exports[`Parser.getSymbols var 1`] = `
+"hasJsx: false
+
+hasTypes: false
+
+framework: null"
+`;
diff --git a/devtools/client/debugger/src/workers/parser/tests/__snapshots__/validate.spec.js.snap b/devtools/client/debugger/src/workers/parser/tests/__snapshots__/validate.spec.js.snap
new file mode 100644
index 0000000000..a341538a5d
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/__snapshots__/validate.spec.js.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`has syntax error should return the error object for the invalid expression 1`] = `"SyntaxError : Missing semicolon. (1:3)"`;
diff --git a/devtools/client/debugger/src/workers/parser/tests/contains.spec.js b/devtools/client/debugger/src/workers/parser/tests/contains.spec.js
new file mode 100644
index 0000000000..741f1a29fc
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/contains.spec.js
@@ -0,0 +1,250 @@
+/* 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 { containsPosition, containsLocation } from "../utils/contains";
+
+function getTestLoc() {
+ return {
+ start: {
+ line: 10,
+ column: 2,
+ },
+ end: {
+ line: 12,
+ column: 10,
+ },
+ };
+}
+
+// AstPosition.column is typed as a number, but many parts of this test set it
+// to undefined. Using zero instead causes test failures, and allowing it to be
+// undefined causes many flow errors in code manipulating AstPosition.
+// Fake a coercion of undefined to number as a workaround for now.
+function undefinedColumn() {
+ return undefined;
+}
+
+function startPos(lineOffset, columnOffset) {
+ const { start } = getTestLoc();
+ return {
+ line: start.line + lineOffset,
+ column: start.column + columnOffset,
+ };
+}
+
+function endPos(lineOffset, columnOffset) {
+ const { end } = getTestLoc();
+ return {
+ line: end.line + lineOffset,
+ column: end.column + columnOffset,
+ };
+}
+
+function startLine(lineOffset = 0) {
+ const { start } = getTestLoc();
+ return {
+ line: start.line + lineOffset,
+ column: undefinedColumn(),
+ };
+}
+
+function endLine(lineOffset = 0) {
+ const { end } = getTestLoc();
+ return {
+ line: end.line + lineOffset,
+ column: undefinedColumn(),
+ };
+}
+
+function testContains(pos, bool) {
+ const loc = getTestLoc();
+ expect(containsPosition(loc, pos)).toEqual(bool);
+}
+
+describe("containsPosition", () => {
+ describe("location and postion both with the column criteria", () => {
+ /* eslint-disable jest/expect-expect */
+ it("should contain position within the location range", () =>
+ testContains(startPos(1, 1), true));
+
+ it("should not contain position out of the start line", () =>
+ testContains(startPos(-1, 0), false));
+
+ it("should not contain position out of the start column", () =>
+ testContains(startPos(0, -1), false));
+
+ it(`should contain position on the same start line and
+ within the start column`, () => testContains(startPos(0, 1), true));
+
+ it("should not contain position out of the end line", () =>
+ testContains(endPos(1, 0), false));
+
+ it("should not contain position out of the end column", () =>
+ testContains(endPos(0, 1), false));
+
+ // eslint-disable-next-line max-len
+ it("should contain position on the same end line and within the end column", () =>
+ testContains(endPos(0, -1), true));
+ /* eslint-enable jest/expect-expect */
+ });
+
+ describe("position without the column criterion", () => {
+ /* eslint-disable jest/expect-expect */
+ it("should contain position on the same start line", () =>
+ testContains(startLine(0), true));
+
+ it("should contain position on the same end line", () =>
+ testContains(endLine(0), true));
+ /* eslint-enable jest/expect-expect */
+ });
+
+ describe("location without the column criterion", () => {
+ it("should contain position on the same start line", () => {
+ const loc = getTestLoc();
+ loc.start.column = undefinedColumn();
+ const pos = {
+ line: loc.start.line,
+ column: 1,
+ };
+ expect(containsPosition(loc, pos)).toEqual(true);
+ });
+
+ it("should contain position on the same end line", () => {
+ const loc = getTestLoc();
+ loc.end.column = undefinedColumn();
+ const pos = {
+ line: loc.end.line,
+ column: 1,
+ };
+ expect(containsPosition(loc, pos)).toEqual(true);
+ });
+ });
+
+ describe("location and postion both without the column criterion", () => {
+ it("should contain position on the same start line", () => {
+ const loc = getTestLoc();
+ loc.start.column = undefinedColumn();
+ const pos = startLine();
+ expect(containsPosition(loc, pos)).toEqual(true);
+ });
+
+ it("should contain position on the same end line", () => {
+ const loc = getTestLoc();
+ loc.end.column = undefinedColumn();
+ const pos = endLine();
+ expect(containsPosition(loc, pos)).toEqual(true);
+ });
+ });
+});
+
+describe("containsLocation", () => {
+ describe("locations both with the column criteria", () => {
+ it("should contian location within the range", () => {
+ const locA = getTestLoc();
+ const locB = {
+ start: startPos(1, 1),
+ end: endPos(-1, -1),
+ };
+ expect(containsLocation(locA, locB)).toEqual(true);
+ });
+
+ it("should not contian location out of the start line", () => {
+ const locA = getTestLoc();
+ const locB = getTestLoc();
+ locB.start.line--;
+ expect(containsLocation(locA, locB)).toEqual(false);
+ });
+
+ it("should not contian location out of the start column", () => {
+ const locA = getTestLoc();
+ const locB = getTestLoc();
+ locB.start.column--;
+ expect(containsLocation(locA, locB)).toEqual(false);
+ });
+
+ it("should not contian location out of the end line", () => {
+ const locA = getTestLoc();
+ const locB = getTestLoc();
+ locB.end.line++;
+ expect(containsLocation(locA, locB)).toEqual(false);
+ });
+
+ it("should not contian location out of the end column", () => {
+ const locA = getTestLoc();
+ const locB = getTestLoc();
+ locB.end.column++;
+ expect(containsLocation(locA, locB)).toEqual(false);
+ });
+
+ it(`should contain location on the same start line and
+ within the start column`, () => {
+ const locA = getTestLoc();
+ const locB = {
+ start: startPos(0, 1),
+ end: endPos(-1, -1),
+ };
+ expect(containsLocation(locA, locB)).toEqual(true);
+ });
+
+ it(`should contain location on the same end line and
+ within the end column`, () => {
+ const locA = getTestLoc();
+ const locB = {
+ start: startPos(1, 1),
+ end: endPos(0, -1),
+ };
+ expect(containsLocation(locA, locB)).toEqual(true);
+ });
+ });
+
+ describe("location A without the column criterion", () => {
+ it("should contain location on the same start line", () => {
+ const locA = getTestLoc();
+ locA.start.column = undefinedColumn();
+ const locB = getTestLoc();
+ expect(containsLocation(locA, locB)).toEqual(true);
+ });
+
+ it("should contain location on the same end line", () => {
+ const locA = getTestLoc();
+ locA.end.column = undefinedColumn();
+ const locB = getTestLoc();
+ expect(containsLocation(locA, locB)).toEqual(true);
+ });
+ });
+
+ describe("location B without the column criterion", () => {
+ it("should contain location on the same start line", () => {
+ const locA = getTestLoc();
+ const locB = getTestLoc();
+ locB.start.column = undefinedColumn();
+ expect(containsLocation(locA, locB)).toEqual(true);
+ });
+
+ it("should contain location on the same end line", () => {
+ const locA = getTestLoc();
+ const locB = getTestLoc();
+ locB.end.column = undefinedColumn();
+ expect(containsLocation(locA, locB)).toEqual(true);
+ });
+ });
+
+ describe("locations both without the column criteria", () => {
+ it("should contain location on the same start line", () => {
+ const locA = getTestLoc();
+ const locB = getTestLoc();
+ locA.start.column = undefinedColumn();
+ locB.start.column = undefinedColumn();
+ expect(containsLocation(locA, locB)).toEqual(true);
+ });
+
+ it("should contain location on the same end line", () => {
+ const locA = getTestLoc();
+ const locB = getTestLoc();
+ locA.end.column = undefinedColumn();
+ locB.end.column = undefinedColumn();
+ expect(containsLocation(locA, locB)).toEqual(true);
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/workers/parser/tests/findOutOfScopeLocations.spec.js b/devtools/client/debugger/src/workers/parser/tests/findOutOfScopeLocations.spec.js
new file mode 100644
index 0000000000..a2177c22eb
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/findOutOfScopeLocations.spec.js
@@ -0,0 +1,80 @@
+/* eslint max-nested-callbacks: ["error", 4]*/
+/* 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 findOutOfScopeLocations from "../findOutOfScopeLocations";
+
+import { populateSource } from "./helpers";
+
+function formatLines(actual) {
+ return actual
+ .map(
+ ({ start, end }) =>
+ `(${start.line}, ${start.column}) -> (${end.line}, ${end.column})`
+ )
+ .join("\n");
+}
+
+describe("Parser.findOutOfScopeLocations", () => {
+ it("should exclude non-enclosing function blocks", () => {
+ const source = populateSource("outOfScope");
+ const actual = findOutOfScopeLocations({
+ source,
+ line: 5,
+ column: 5,
+ });
+
+ expect(formatLines(actual)).toMatchSnapshot();
+ });
+
+ it("should roll up function blocks", () => {
+ const source = populateSource("outOfScope");
+ const actual = findOutOfScopeLocations({
+ source,
+ line: 24,
+ column: 0,
+ });
+
+ expect(formatLines(actual)).toMatchSnapshot();
+ });
+
+ it("should exclude function for locations on declaration", () => {
+ const source = populateSource("outOfScope");
+ const actual = findOutOfScopeLocations({
+ source,
+ line: 3,
+ column: 12,
+ });
+
+ expect(formatLines(actual)).toMatchSnapshot();
+ });
+
+ it("should treat comments as out of scope", () => {
+ const source = populateSource("outOfScopeComment");
+ const actual = findOutOfScopeLocations({
+ source,
+ line: 3,
+ column: 2,
+ });
+
+ expect(actual.length).toBe(1);
+
+ const location = actual[0];
+ expect(location.start.line).toBe(1);
+ expect(location.start.column).toBe(0);
+
+ expect(location.end.line).toBe(1);
+ expect(location.end.column).toBe(15);
+ });
+
+ it("should not exclude in-scope inner locations", () => {
+ const source = populateSource("outOfScope");
+ const actual = findOutOfScopeLocations({
+ source,
+ line: 61,
+ column: 0,
+ });
+ expect(formatLines(actual)).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/allSymbols.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/allSymbols.js
new file mode 100644
index 0000000000..bebda9f36a
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/allSymbols.js
@@ -0,0 +1,33 @@
+const TIME = 60;
+let count = 0;
+
+function incrementCounter(counter) {
+ return counter++;
+}
+
+const sum = (a, b) => a + b;
+
+const Obj = {
+ foo: 1,
+ doThing() {
+ console.log("hey");
+ },
+ doOtherThing: function() {
+ return 42;
+ }
+};
+
+Obj.property = () => {};
+Obj.otherProperty = 1;
+
+class Ultra {
+ constructor() {
+ this.awesome = true;
+ }
+
+ beAwesome(person) {
+ console.log(`${person} is Awesome!`);
+ }
+}
+
+this.props.history.push(`/dacs/${this.props.dac.id}`);
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/async.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/async.js
new file mode 100644
index 0000000000..43216be635
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/async.js
@@ -0,0 +1,10 @@
+async function foo() {
+ return new Promise(resolve => {
+ setTimeout(resolve, 10);
+ });
+}
+
+async function stuff() {
+ await foo(1);
+ await foo(2);
+}
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/call-sites.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/call-sites.js
new file mode 100644
index 0000000000..aa73700d93
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/call-sites.js
@@ -0,0 +1,4 @@
+aaa(bbb(), ccc());
+dddd()
+ .eee()
+ .ffff();
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/callExpressions.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/callExpressions.js
new file mode 100644
index 0000000000..2771dede6a
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/callExpressions.js
@@ -0,0 +1,7 @@
+dispatch({ d });
+function evaluate(script, { frameId } = {frameId: 3}, {c} = {c: 2}) {}
+
+a(b(c()));
+
+a.b().c();
+a.b.c.d();
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/calls.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/calls.js
new file mode 100644
index 0000000000..f05d445db7
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/calls.js
@@ -0,0 +1,21 @@
+foo(1, '2', bar())
+
+foo()
+ .bar()
+ .bazz()
+
+console.log('yo')
+
+foo(
+ 1,
+ bar()
+)
+
+var a = 3;
+
+// set a step point at the first call expression in step expressions
+var x = { a: a(), b: b(), c: c() };
+var b = [ foo() ];
+[ a(), b(), c() ];
+(1, a(), b());
+x(1, a(), b());
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/class.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/class.js
new file mode 100644
index 0000000000..59d612ebfe
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/class.js
@@ -0,0 +1,28 @@
+class Test {
+ publicProperty;
+ #privateProperty = "default";
+ static myStatic = "static";
+ static hello() {
+ return "Hello " + this.myStatic
+ }
+ static {
+ const x = this.myStatic;
+ }
+
+ constructor() {
+ this.publicProperty = "foo";
+ this.#privateProperty = "bar";
+ }
+
+ bar(a) {
+ console.log("bar", a);
+ }
+
+ baz = b => {
+ return b * 2;
+ };
+}
+
+class Test2 {}
+
+let expressiveClass = class {};
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/component.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/component.js
new file mode 100644
index 0000000000..38d9b00096
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/component.js
@@ -0,0 +1,84 @@
+/*
+ * class
+ */
+
+class Punny extends Component {
+ constructor(props) {
+ super();
+ this.onClick = this.onClick.bind(this);
+ }
+
+ componentDidMount() {}
+
+ onClick() {}
+
+ renderMe() {
+ return <div onClick={this.onClick} />;
+ }
+
+ render() {}
+}
+
+/*
+ * CALL EXPRESSIONS - createClass, extend
+ */
+
+const TodoView = Backbone.View.extend({
+ tagName: "li",
+
+ render: function() {
+ console.log("yo");
+ }
+});
+
+const TodoClass = createClass({
+ tagName: "li",
+
+ render: function() {
+ console.log("yo");
+ }
+});
+
+TodoClass = createClass({
+ tagName: "li",
+
+ render: function() {
+ console.log("yo");
+ }
+});
+
+app.TodoClass = createClass({
+ tagName: "li",
+
+ render: function() {
+ console.log("yo");
+ }
+});
+
+/*
+ * PROTOTYPE
+ */
+
+function Button() {
+ if (!(this instanceof Button)) return new Button();
+ this.color = null;
+ Nanocomponent.call(this);
+}
+
+Button.prototype = Object.create(Nanocomponent.prototype);
+
+var x = function() {};
+
+Button.prototype.createElement = function(color) {
+ this.color = color;
+ return html`
+ <button style="background-color: ${color}">
+ Click Me
+ </button>
+ `;
+};
+
+// Implement conditional rendering
+Button.prototype.update = function(newColor) {
+ return newColor !== this.color;
+};
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/computed-props.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/computed-props.js
new file mode 100644
index 0000000000..4c0f182f33
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/computed-props.js
@@ -0,0 +1,8 @@
+(function(key) {
+ let obj = {
+ b: 5
+ };
+ obj[key] = 0;
+ const c = obj.b;
+ return obj;
+})("a");
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/control-flow.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/control-flow.js
new file mode 100644
index 0000000000..b9e859ff74
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/control-flow.js
@@ -0,0 +1,39 @@
+
+if (x) {
+ foo();
+}
+else if (y) {
+ foo();
+}
+else {
+ foo();
+}
+
+for (var i=0; i< 5; i++ ) {
+ foo();
+}
+
+while (x) {
+ foo();
+}
+
+switch (c) {
+ case a:
+ console.log('hi')
+}
+
+var a = 3;
+
+for (const val of [1, 2]) {
+ console.log("pause again", val);
+}
+
+for (const val of vals) {
+ console.log("pause again", val);
+}
+
+try {
+} catch (e) {
+}
+
+with (e) {}
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/decorators.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/decorators.js
new file mode 100644
index 0000000000..22c0a1398d
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/decorators.js
@@ -0,0 +1,2 @@
+@annotation
+class MyClass { }
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/destructuring.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/destructuring.js
new file mode 100644
index 0000000000..52686e6573
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/destructuring.js
@@ -0,0 +1,16 @@
+const { b, resty } = compute(stuff);
+const { first: f, last: l } = obj;
+
+const [a, ...rest] = compute(stuff);
+const [x] = ["a"];
+
+for (const [index, element] of arr.entries()) {
+ console.log(index, element);
+}
+
+const { a: aa = 10, b: bb = 5 } = { a: 3 };
+const { temp: [{ foo: foooo }] } = obj;
+
+let { [key]: foo } = { z: "bar" };
+
+let [, prefName] = prefsBlueprint[accessorName];
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/es6.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/es6.js
new file mode 100644
index 0000000000..90b53141f4
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/es6.js
@@ -0,0 +1 @@
+dispatch({ ...action, [PROMISE]: promise });
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/expression.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/expression.js
new file mode 100644
index 0000000000..fa80b68add
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/expression.js
@@ -0,0 +1,25 @@
+const obj = { a: { b: 2 } }; // e.g. obj.a.b
+const foo = obj2.c.secondProperty; // e.g. foo.obj2.c.secondProperty
+
+// computed properties
+const com = { [a]: { b: "c", [d]: "e" }, [b]: 3 }; // e.g. com[a].b
+const firstAuthor = collection.books[1].author;
+const firstActionDirector = collection.genres["sci-fi"].movies[0].director;
+
+app.TodoView = Backbone.extend({
+ render: function() {}
+});
+
+// assignments
+obj.foo = { a: { b: "c" }, b: 3 }; // e.g. obj.foo.a.b
+com = { a: { b: "c" }, b: 3 }; // e.g. com.a.b
+
+// arrays
+const res = [{ a: 2 }, { b: 3 }]; // e.g. res[1].b
+const res2 = { a: [{ b: 2 }] }; // e.g. res.a[0].b
+const res3 = { a: [{ b: 2 }], b: [{ c: 3 }] }; // e.g. res.a[0].b
+const res4 = [[{ a: 3 }], [{ b: a.b.c.v.d }]]; // e.g. res[1][0].b
+
+function params({ a, b }) {} // e.g. a
+var pars = function({ a, b }) {};
+const evil = obj2.doEvil().c.secondProperty; // e.g. obj2.doEvil or ""
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/flow.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/flow.js
new file mode 100644
index 0000000000..1135bd62a4
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/flow.js
@@ -0,0 +1,5 @@
+class App extends Component {
+ renderHello(name: string, action: ReduxAction, { todos }: Props) {
+ return `howdy ${name}`;
+ }
+}
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/angular1FalsePositive.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/angular1FalsePositive.js
new file mode 100644
index 0000000000..5220f26c8c
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/angular1FalsePositive.js
@@ -0,0 +1,11 @@
+const frameworkStars = {
+ angular: 40779,
+ react: 111576,
+ vue: 114358,
+};
+
+const container = new Container();
+container.module("sum", (...args) => args.reduce((s, c) => s + c, 0));
+
+container.get("sum",
+ (sum) => sum(Object.values(frameworkStars)));
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/angular1Module.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/angular1Module.js
new file mode 100644
index 0000000000..f0e8c381ef
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/angular1Module.js
@@ -0,0 +1,4 @@
+angular.module('something', ['ngRoute', 'ngResource'])
+ .config(function ($routeProvider) {
+ 'use strict';
+ });
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/plainJavascript.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/plainJavascript.js
new file mode 100644
index 0000000000..41609f8715
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/plainJavascript.js
@@ -0,0 +1,8 @@
+"use strict"
+
+const a = 2;
+
+function test(a) {
+ return a;
+}
+
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/reactComponent.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/reactComponent.js
new file mode 100644
index 0000000000..37f3ac49e5
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/reactComponent.js
@@ -0,0 +1,3 @@
+import React, { Component } from "react";
+
+class PrimaryPanes extends Component {}
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/reactComponentEs5.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/reactComponentEs5.js
new file mode 100644
index 0000000000..a4370b5369
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/reactComponentEs5.js
@@ -0,0 +1,3 @@
+var React = require("react");
+
+class PrimaryPanes extends React.Component {}
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/reactLibrary.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/reactLibrary.js
new file mode 100644
index 0000000000..9ca048e77f
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/reactLibrary.js
@@ -0,0 +1,19 @@
+/**
+ * Base class helpers for the updating state of a component.
+ */
+function Component(props, context, updater) {
+ this.props = props;
+ this.context = context;
+ // If a component has string refs, we will assign a different object later.
+ this.refs = emptyObject;
+ // We initialize the default updater but the real one gets injected by the
+ // renderer.
+ this.updater = updater || ReactNoopUpdateQueue;
+}
+
+Component.prototype.isReactComponent = {};
+Component.prototype.setState = function (partialState, callback) {
+ !(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null)
+ ? invariant(false, 'setState(...): takes an object of state variables to update or a function which returns an object of state variables.') : void 0;
+ this.updater.enqueueSetState(this, partialState, callback, 'setState');
+};
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/reduxLibrary.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/reduxLibrary.js
new file mode 100644
index 0000000000..b86c8fb0cf
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/reduxLibrary.js
@@ -0,0 +1,39 @@
+(function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
+ (factory((global.Redux = {})));
+ }(this, (function (exports) { 'use strict';
+
+ function symbolObservablePonyfill(root) {
+ var result;
+ var Symbol = root.Symbol;
+
+ if (typeof Symbol === 'function') {
+ if (Symbol.observable) {
+ result = Symbol.observable;
+ } else {
+ result = Symbol('observable');
+ Symbol.observable = result;
+ }
+ } else {
+ result = '@@observable';
+ }
+
+ return result;
+ }
+
+ /* global window */
+
+ var root;
+
+ if (typeof self !== 'undefined') {
+ root = self;
+ } else if (typeof window !== 'undefined') {
+ root = window;
+ } else if (typeof global !== 'undefined') {
+ root = global;
+ } else if (typeof module !== 'undefined') {
+ root = module;
+ } else {
+ root = Function('return this')();
+ } \ No newline at end of file
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/vueFileComponent.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/vueFileComponent.js
new file mode 100644
index 0000000000..d41e55b72b
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/vueFileComponent.js
@@ -0,0 +1,3 @@
+Vue.component('test-item', {
+ template: '<li>This is a test item</li>'
+}) \ No newline at end of file
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/vueFileDeclarative.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/vueFileDeclarative.js
new file mode 100644
index 0000000000..844fce1451
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/frameworks/vueFileDeclarative.js
@@ -0,0 +1,6 @@
+var app = new Vue({
+ el: '#app',
+ data: {
+ message: 'Hello Vue!'
+ }
+}) \ No newline at end of file
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/func.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/func.js
new file mode 100644
index 0000000000..bb0d4c1b05
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/func.js
@@ -0,0 +1,50 @@
+function square(n) {
+ return n * n;
+}
+
+export function exFoo() {
+ return "yay";
+}
+
+async function slowFoo() {
+ return "meh";
+}
+
+export async function exSlowFoo() {
+ return "yay in a bit";
+}
+
+function ret() {
+ return foo();
+}
+
+child = function() {};
+
+(function() {
+ 2;
+})();
+
+const obj = {
+ foo: function name() {
+ 2 + 2;
+ },
+
+ bar() {
+ 2 + 2;
+ }
+};
+
+export default function root() {
+}
+
+function test(a1, a2 = 45, { a3, a4, a5: { a6: a7 } = {} } = {}) {
+ console.log("pause next here");
+}
+
+() => (x = 4);
+
+function ret2() {
+ return (
+ foo()
+ );
+}
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/functionNames.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/functionNames.js
new file mode 100644
index 0000000000..2ae38fc548
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/functionNames.js
@@ -0,0 +1,50 @@
+/* eslint-disable */
+
+({
+ foo: function() {},
+ "foo": function() {},
+ 42: function() {},
+
+ foo() {},
+ "foo"() {},
+ 42() {},
+});
+
+foo = function() {};
+obj.foo = function() {};
+
+var foo = function(){};
+var [foo = function(){}] = [];
+var {foo = function(){}} = {};
+
+[foo = function(){}] = [];
+({foo = function(){}} = {});
+({bar: foo = function(){}} = {});
+
+function fn([foo = function(){}]){}
+function f2({foo = function(){}} = {}){}
+function f3({bar: foo = function(){}} = {}){}
+
+class Cls {
+ foo = function() {};
+ "foo" = function() {};
+ 42 = function() {};
+
+ foo() {}
+ "foo"() {}
+ 42() {}
+}
+
+(function(){});
+
+export default function (){}
+
+const defaultObj = {a: 1};
+const defaultArr = ['smthng'];
+function a(first, second){}
+function b(first = 'bla', second){}
+function c(first = {}, second){}
+function d(first = [], second){}
+function e(first = defaultObj, second){}
+function f(first = defaultArr, second){}
+function g(first = null, second){}
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/generators.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/generators.js
new file mode 100644
index 0000000000..7c71838827
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/generators.js
@@ -0,0 +1,4 @@
+function* foo() {
+ yield 1;
+ yield 2;
+}
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/jsx.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/jsx.js
new file mode 100644
index 0000000000..c781f7666f
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/jsx.js
@@ -0,0 +1,5 @@
+const jsxElement = <h1> Hi ! I'm here ! </h1>;
+
+<div id="3" res={foo()}>
+ <Item>{foo()}</Item>
+</div>
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/math.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/math.js
new file mode 100644
index 0000000000..508d28ca99
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/math.js
@@ -0,0 +1,15 @@
+function math(n) {
+ function square(n) {
+ // inline comment
+ n * n;
+ }
+
+ // document some lines
+ const two = square(2);
+
+ const four = squaare(4);
+ return two * four;
+}
+
+var child = function() {};
+child2 = function() {};
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/modules.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/modules.js
new file mode 100644
index 0000000000..8ba351497e
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/modules.js
@@ -0,0 +1,10 @@
+import {x} from "y"
+import z from "y";
+
+export class AppComponent {
+ title = 'app'
+}
+
+export default class AppComponent {
+ title = 'app'
+}
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/object-expressions.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/object-expressions.js
new file mode 100644
index 0000000000..b7f4806e04
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/object-expressions.js
@@ -0,0 +1,6 @@
+const y = ({ params });
+const foo = ({ b });
+const bar = { x: 3, y: "434", z: true, a: null, s: c * 2, d : getValue(2) }
+const params = frameId ? { frameActor: frameId } : {}
+
+const collection = {genres: {"sci-fi": {movies: [{director: "yo"}]}}}
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/optional-chaining.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/optional-chaining.js
new file mode 100644
index 0000000000..757335f78b
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/optional-chaining.js
@@ -0,0 +1,3 @@
+const obj = {};
+obj?.a;
+obj?.a?.b ?? [];
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/outOfScope.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/outOfScope.js
new file mode 100644
index 0000000000..30218f546b
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/outOfScope.js
@@ -0,0 +1,62 @@
+// Program Scope
+
+function outer() {
+ function inner() {
+ const x = 1;
+ }
+
+ const arrow = () => {
+ const x = 1;
+ };
+
+ const declaration = function() {
+ const x = 1;
+ };
+
+ assignment = (function() {
+ const x = 1;
+ })();
+
+ const iifeDeclaration = (function() {
+ const x = 1;
+ })();
+
+ return function() {
+ const x = 1;
+ };
+}
+
+function exclude() {
+ function another() {
+ const x = 1;
+ }
+}
+
+const globalArrow = () => {
+ const x = 1;
+};
+
+const globalDeclaration = function() {
+ const x = 1;
+};
+
+globalAssignment = (function() {
+ const x = 1;
+})();
+
+const globalIifeDeclaration = (function() {
+ const x = 1;
+})();
+
+function parentFunc() {
+ let MAX = 3;
+ let nums = [0, 1, 2, 3];
+ let x = 1;
+ let y = nums.find(function(n) {
+ return n == x;
+ });
+ function innerFunc(a) {
+ return Math.max(a, MAX);
+ }
+ return innerFunc(y);
+}
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/outOfScopeComment.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/outOfScopeComment.js
new file mode 100644
index 0000000000..bc02f94671
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/outOfScopeComment.js
@@ -0,0 +1,4 @@
+// Some comment
+(function() {
+ const x = 1;
+})();
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/parseScriptTags.html b/devtools/client/debugger/src/workers/parser/tests/fixtures/parseScriptTags.html
new file mode 100644
index 0000000000..6283f0ee98
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/parseScriptTags.html
@@ -0,0 +1,42 @@
+<html>
+<head>
+ <script type="text/javascript">
+ var globalObject = {
+ first: "name",
+ last: "words"
+ };
+ function sayHello (name) {
+ return `Hello, ${name}!`;
+ }
+ </script>
+ <style>
+ BODY {
+ font-size: 48px;
+ color: rebeccapurple;
+ }
+ </style>
+</head>
+<body>
+ <h1>Testing Script Tags in HTML</h1>
+ <script>
+ const capitalize = name => {
+ return name[0].toUpperCase() + name.substring(1)
+ };
+ const greetAll = ["my friend", "buddy", "world"]
+ .map(capitalize)
+ .map(sayHello)
+ .join("\n");
+
+ globalObject.greetings = greetAll;
+ </script>
+ <p>
+ Some arbitrary intermediate content to affect the offsets of the scripts
+ </p>
+ <script>
+ (function iife() {
+ const greeting = sayHello("Ryan");
+ console.log(greeting);
+ })();
+ </script>
+</body>
+</html>
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/private-fields.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/private-fields.js
new file mode 100644
index 0000000000..ca9abc8f1d
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/private-fields.js
@@ -0,0 +1,24 @@
+class MyClass {
+ constructor(secret, ...rest) {
+ const self = this;
+ this.#secret = secret;
+ self.#restParams = rest;
+ }
+
+ #secret;
+ #restParams;
+ #salt = "bloup";
+ creationDate = new Date();
+
+ #getSalt() {
+ return this.#salt;
+ }
+
+ debug() {
+ const self = this;
+ const creationDate = this.creationDate;
+ const secret = this.#secret;
+ const salt = self.#getSalt();
+ return `${creationDate}|${salt}|${secret}`;
+ }
+}
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/proto.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/proto.js
new file mode 100644
index 0000000000..38c3b63ac7
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/proto.js
@@ -0,0 +1,14 @@
+const foo = function() {};
+
+const bar = () => {};
+
+const TodoView = Backbone.View.extend({
+ tagName: "li",
+ initialize: function() {},
+ doThing(b) {
+ console.log("hi", b);
+ },
+ render: function() {
+ return this;
+ }
+});
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/regexp.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/regexp.js
new file mode 100644
index 0000000000..fb0f13b0d0
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/regexp.js
@@ -0,0 +1 @@
+const re = /^\p{RGI_Emoji}$/v; \ No newline at end of file
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/resolveToken.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/resolveToken.js
new file mode 100644
index 0000000000..4660f0f568
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/resolveToken.js
@@ -0,0 +1,40 @@
+const a = 1;
+let b = 0;
+
+function getA() {
+ return a;
+}
+
+function setB(newB) {
+ b = newB;
+}
+
+const plusAB = (function(x, y) {
+ const obj = { x, y };
+ function insideClosure(alpha, beta) {
+ return alpha + beta + obj.x + obj.y;
+ }
+
+ return insideClosure;
+})(a, b);
+
+function withMultipleScopes() {
+ var outer = 1;
+ function innerScope() {
+ var inner = outer + 1;
+ return inner;
+ }
+
+ const fromIIFE = (function(toIIFE) {
+ return innerScope() + toIIFE;
+ })(1);
+
+ {
+ // random block
+ let x = outer + fromIIFE;
+ if (x) {
+ const y = x * x;
+ console.log(y);
+ }
+ }
+}
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/arrow-function.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/arrow-function.js
new file mode 100644
index 0000000000..4ec72fd450
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/arrow-function.js
@@ -0,0 +1,11 @@
+export {};
+
+let outer = (p1) => {
+ console.log(this);
+
+ (function() {
+ var inner = (p2) => {
+ console.log(this);
+ };
+ })();
+};
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/binding-types.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/binding-types.js
new file mode 100644
index 0000000000..8c9cc05d1d
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/binding-types.js
@@ -0,0 +1,24 @@
+import def from "";
+import { named } from "";
+import { thing as otherNamed } from "";
+import * as namespace from "";
+
+function fn() {}
+class cls {
+ method(){
+
+ }
+}
+
+var aVar;
+let aLet;
+const aConst = "";
+
+(function inner() {
+ this;
+ arguments;
+});
+
+{
+ function blockFn(){}
+}
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/block-statement.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/block-statement.js
new file mode 100644
index 0000000000..5e64d1180f
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/block-statement.js
@@ -0,0 +1,13 @@
+export {};
+
+let first;
+
+{
+ var second;
+ function third() {}
+ class Fourth {}
+ let fifth;
+ const sixth = 6;
+}
+
+var seventh;
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/class-declaration.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/class-declaration.js
new file mode 100644
index 0000000000..1c2c74dcbc
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/class-declaration.js
@@ -0,0 +1,14 @@
+export {};
+
+class Outer {
+ method() {
+ class Inner {
+ m() {
+ console.log(this);
+ }
+ }
+ }
+}
+
+@decorator
+class Second {}
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/class-expression.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/class-expression.js
new file mode 100644
index 0000000000..16b6841d71
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/class-expression.js
@@ -0,0 +1,11 @@
+export {};
+
+var Outer = class Outer {
+ method() {
+ var Inner = class {
+ m() {
+ console.log(this);
+ }
+ };
+ }
+};
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/class-property.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/class-property.js
new file mode 100644
index 0000000000..a80cad3a87
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/class-property.js
@@ -0,0 +1,10 @@
+export {};
+
+class Foo {
+ prop = this.init();
+
+ other = do {
+ var one;
+ let two;
+ };
+}
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/complex-nesting.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/complex-nesting.js
new file mode 100644
index 0000000000..40763f556f
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/complex-nesting.js
@@ -0,0 +1,29 @@
+function named(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.default = root;
+function root() {
+ function fn(arg) {
+ var _this = this,
+ _arguments = arguments;
+
+ console.log(this, arguments);
+ console.log("pause here", fn, root);
+
+ var arrow = function arrow(argArrow) {
+ console.log(_this, _arguments);
+ console.log("pause here", fn, root);
+ };
+ arrow("arrow-arg");
+ }
+
+ fn.call("this-value", "arg-value");
+}
+module.exports = exports["default"];
+
+}
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/expressions.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/expressions.js
new file mode 100644
index 0000000000..6698acd8e8
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/expressions.js
@@ -0,0 +1,6 @@
+const foo = {};
+
+foo.bar().baz;
+(0, foo.bar)().baz;
+Object(foo.bar)().baz;
+__webpack_require__.i(foo.bar)().baz;
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/flowtype-bindings.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/flowtype-bindings.js
new file mode 100644
index 0000000000..385797c044
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/flowtype-bindings.js
@@ -0,0 +1,11 @@
+import type { One, Two, Three } from "./src/mod";
+
+type Other = {
+ root: typeof root,
+};
+
+const aConst = (window: Array<string>);
+
+export default function root() {
+
+}
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/fn-body-lex-and-nonlex.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/fn-body-lex-and-nonlex.js
new file mode 100644
index 0000000000..a7f6d7670a
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/fn-body-lex-and-nonlex.js
@@ -0,0 +1,23 @@
+function fn() {
+ var nonlex;
+ let lex;
+
+}
+
+function fn2() {
+ function nonlex(){}
+ let lex;
+
+}
+
+function fn3() {
+ var nonlex;
+ class Thing {}
+
+}
+
+function fn4() {
+ function nonlex(){}
+ class Thing {}
+
+}
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/for-loops.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/for-loops.js
new file mode 100644
index 0000000000..747eeeae7d
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/for-loops.js
@@ -0,0 +1,13 @@
+export {};
+
+for (var one;;) {}
+for (let two;;) {}
+for (const three = 3;;) {}
+
+for (var four in {}) {}
+for (let five in {}) {}
+for (const six in {}) {}
+
+for (var seven of {}) {}
+for (let eight of {}) {}
+for (const nine of {}) {}
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/function-declaration.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/function-declaration.js
new file mode 100644
index 0000000000..759374b437
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/function-declaration.js
@@ -0,0 +1,11 @@
+export {};
+
+function outer(p1) {}
+
+{
+ function middle(p2) {
+ function inner(p3) {}
+
+ console.log(this);
+ }
+}
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/function-expression.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/function-expression.js
new file mode 100644
index 0000000000..436413533e
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/function-expression.js
@@ -0,0 +1,7 @@
+export {};
+
+let fn = function(p1) {};
+
+let fn2 = function withName(p2) {
+ console.log(this);
+};
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/jsx-component.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/jsx-component.js
new file mode 100644
index 0000000000..ffe1c1dfce
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/jsx-component.js
@@ -0,0 +1,6 @@
+import SomeComponent from "";
+
+<SomeComponent attr="value" />;
+<SomeComponent attr="value"></SomeComponent>;
+<SomeComponent.prop attr="value" />;
+<SomeComponent.prop.child attr="value" />;
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/out-of-order-declarations.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/out-of-order-declarations.js
new file mode 100644
index 0000000000..4f36d9fc38
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/out-of-order-declarations.js
@@ -0,0 +1,21 @@
+var val;
+
+export default function root() {
+ var val;
+
+ var fn;
+
+ this;
+
+ function callback() {
+ console.log(val, fn, callback, root, this);
+
+ var val;
+
+ function fn(){};
+ }
+
+ callback();
+}
+
+import aDefault from "./src/mod";
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/pattern-declarations.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/pattern-declarations.js
new file mode 100644
index 0000000000..6810ef4614
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/pattern-declarations.js
@@ -0,0 +1,2 @@
+var { prop: one } = {};
+var [ two ] = [];
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/simple-module.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/simple-module.js
new file mode 100644
index 0000000000..b25cc7a863
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/simple-module.js
@@ -0,0 +1,11 @@
+import foo from "foo";
+
+console.log(foo);
+
+var one = 1;
+let two = 2;
+const three = 3;
+
+function fn() {}
+
+this;
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/switch-statement.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/switch-statement.js
new file mode 100644
index 0000000000..7163c3811d
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/switch-statement.js
@@ -0,0 +1,22 @@
+export {};
+
+switch (foo) {
+ case "zero":
+ var zero;
+ case "one":
+ let one;
+ case "two":
+ let two;
+ case "three": {
+ let three;
+ }
+}
+
+switch (foo) {
+ case "":
+ function two(){}
+}
+switch (foo) {
+ case "":
+ class three {}
+}
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/try-catch.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/try-catch.js
new file mode 100644
index 0000000000..8ccf4db592
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/try-catch.js
@@ -0,0 +1,9 @@
+export {};
+
+try {
+ var first;
+ let second;
+} catch (err) {
+ var third;
+ let fourth;
+}
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/ts-sample.ts b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/ts-sample.ts
new file mode 100644
index 0000000000..e23ff83754
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/ts-sample.ts
@@ -0,0 +1,41 @@
+
+// TSEnumDeclaration
+enum Color {
+ // TSEnumMember
+ Red,
+ Green,
+ Blue,
+}
+
+class Example<T> {
+ // TSParameterProperty
+ constructor(public foo) {
+
+ }
+
+ method(): never {
+ throw new Error();
+ }
+}
+
+// TSTypeAssertion
+var foo = <any>window;
+
+// TSAsExpression
+(window as any);
+
+// TSNonNullExpression
+(window!);
+
+// TSModuleDeclaration
+namespace TheSpace {
+ function fn() {
+
+ }
+}
+
+// TSImportEqualsDeclaration
+import ImportedClass = require("mod");
+
+// TSExportAssignment
+export = Example;
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/tsx-sample.tsx b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/tsx-sample.tsx
new file mode 100644
index 0000000000..9e2b9faf8b
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/tsx-sample.tsx
@@ -0,0 +1,41 @@
+
+// TSEnumDeclaration
+enum Color {
+ // TSEnumMember
+ Red,
+ Green,
+ Blue,
+}
+
+class Example<T> {
+ // TSParameterProperty
+ constructor(public foo) {
+
+ }
+
+ method(): never {
+ throw new Error();
+ }
+}
+
+// JSXElement
+var foo = <any>window</any>;
+
+// TSAsExpression
+(window as any);
+
+// TSNonNullExpression
+(window!);
+
+// TSModuleDeclaration
+namespace TheSpace {
+ function fn() {
+
+ }
+}
+
+// TSImportEqualsDeclaration
+import ImportedClass = require("mod");
+
+// TSExportAssignment
+export = Example;
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/vue-sample.vue b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/vue-sample.vue
new file mode 100644
index 0000000000..0fceee99d1
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/scopes/vue-sample.vue
@@ -0,0 +1,26 @@
+<template>
+ <div class="hello">
+ <h1>{{ msg }}</h1>
+ </div>
+</template>
+
+<script>
+var moduleVar = "data";
+
+export default {
+ name: 'HelloWorld',
+ data () {
+ var fnVar = 4;
+
+ return {
+ msg: 'Welcome to Your Vue.js App'
+ };
+ }
+};
+</script>
+
+<style scoped>
+a {
+ color: red;
+}
+</style>
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/statements.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/statements.js
new file mode 100644
index 0000000000..f2c113570e
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/statements.js
@@ -0,0 +1,40 @@
+debugger; debugger;
+console.log("a"); console.log("a");
+
+// assignments with valid pause locations
+this.x = 3;
+var a = 4;
+var d = [foo()]
+var f = 3, e = 4;
+var g = [], h = {};
+
+// assignments with invalid pause locations
+var b = foo();
+c = foo();
+
+
+const arr = [
+ '1',
+ 2,
+ foo()
+]
+
+const obj = {
+ a: '1',
+ b: 2,
+ c: foo(),
+}
+
+foo(
+ 1,
+ foo(
+ 1
+ ),
+ 3
+)
+
+throw new Error("3");
+3;
+
+while (i < 6) { break }
+while (i < 6) { continue;}
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/thisExpression.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/thisExpression.js
new file mode 100644
index 0000000000..dd398db426
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/thisExpression.js
@@ -0,0 +1,11 @@
+class Test {
+ constructor() {
+ this.foo = {
+ a: "foobar"
+ };
+ }
+
+ bar() {
+ console.log(this.foo.a);
+ }
+}
diff --git a/devtools/client/debugger/src/workers/parser/tests/fixtures/var.js b/devtools/client/debugger/src/workers/parser/tests/fixtures/var.js
new file mode 100644
index 0000000000..509ad368e8
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/fixtures/var.js
@@ -0,0 +1,21 @@
+var foo = 1;
+let bar = 2;
+const baz = 3;
+const a = 4,
+ b = 5;
+a = 5;
+
+var { foo: { baw } } = {}
+var {bap} = {}
+var {ll = 3} = {}
+
+
+var [first] = [1]
+
+var { a: _a } = 3
+
+var [oh, {my: god}] = [{},{}]
+
+var [[oj], [{oy, vey: _vey, mitzvot: _mitz = 4}]] = [{},{}]
+
+var [one, ...stuff] = []
diff --git a/devtools/client/debugger/src/workers/parser/tests/framework.spec.js b/devtools/client/debugger/src/workers/parser/tests/framework.spec.js
new file mode 100644
index 0000000000..d41f45b71c
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/framework.spec.js
@@ -0,0 +1,63 @@
+/* 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 { getSymbols } from "../getSymbols";
+import { populateOriginalSource } from "./helpers";
+import cases from "jest-in-case";
+
+cases(
+ "Parser.getFramework",
+ ({ name, file, value }) => {
+ const source = populateOriginalSource("frameworks/plainJavascript");
+ const symbols = getSymbols(source.id);
+ expect(symbols.framework).toBeNull();
+ },
+ [
+ {
+ name: "is undefined when no framework",
+ file: "frameworks/plainJavascript",
+ value: null,
+ },
+ {
+ name: "does not get confused with angular (#6833)",
+ file: "frameworks/angular1FalsePositive",
+ value: null,
+ },
+ {
+ name: "recognizes ES6 React component",
+ file: "frameworks/reactComponent",
+ value: "React",
+ },
+ {
+ name: "recognizes ES5 React component",
+ file: "frameworks/reactComponentEs5",
+ value: "React",
+ },
+ {
+ name: "recognizes Angular 1 module",
+ file: "frameworks/angular1Module",
+ value: "Angular",
+ },
+ {
+ name: "recognizes declarative Vue file",
+ file: "frameworks/vueFileDeclarative",
+ value: "Vue",
+ },
+ {
+ name: "recognizes component Vue file",
+ file: "frameworks/vueFileComponent",
+ value: "Vue",
+ },
+ {
+ name: "recognizes the react library file",
+ file: "framework/reactLibrary",
+ value: "React",
+ },
+ {
+ name: "recognizes the redux library file",
+ file: "framework/reduxLibrary",
+ value: "React",
+ },
+ ]
+);
diff --git a/devtools/client/debugger/src/workers/parser/tests/getScopes.spec.js b/devtools/client/debugger/src/workers/parser/tests/getScopes.spec.js
new file mode 100644
index 0000000000..a2d394ae96
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/getScopes.spec.js
@@ -0,0 +1,227 @@
+/* eslint max-nested-callbacks: ["error", 4]*/
+/* 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 getScopes from "../getScopes";
+import { populateOriginalSource } from "./helpers";
+import cases from "jest-in-case";
+
+cases(
+ "Parser.getScopes",
+ ({ name, file, type, locations }) => {
+ const source = populateOriginalSource(file, type);
+
+ locations.forEach(([line, column]) => {
+ const scopes = getScopes({
+ source,
+ line,
+ column,
+ });
+
+ expect(scopes).toMatchSnapshot(
+ `getScopes ${name} at line ${line} column ${column}`
+ );
+ });
+ },
+ [
+ {
+ name: "finds scope bindings in fn body with both lex and non-lex items",
+ file: "scopes/fn-body-lex-and-nonlex",
+ locations: [
+ [4, 0],
+ [10, 0],
+ [16, 0],
+ [22, 0],
+ ],
+ },
+ {
+ name: "finds scope bindings in a vue file",
+ file: "scopes/vue-sample",
+ type: "vue",
+ locations: [[14, 0]],
+ },
+ {
+ name: "finds scope bindings in a typescript file",
+ file: "scopes/ts-sample",
+ type: "ts",
+ locations: [
+ [9, 0],
+ [13, 4],
+ [17, 0],
+ [33, 0],
+ ],
+ },
+ {
+ name: "finds scope bindings in a typescript-jsx file",
+ file: "scopes/tsx-sample",
+ type: "tsx",
+ locations: [
+ [9, 0],
+ [13, 4],
+ [17, 0],
+ [33, 0],
+ ],
+ },
+ {
+ name: "finds scope bindings in a module",
+ file: "scopes/simple-module",
+ locations: [[7, 0]],
+ },
+ {
+ name: "finds scope bindings in a JSX element",
+ file: "scopes/jsx-component",
+ locations: [[2, 0]],
+ },
+ {
+ name: "finds scope bindings for complex binding nesting",
+ file: "scopes/complex-nesting",
+ locations: [
+ [16, 4],
+ [20, 6],
+ ],
+ },
+ {
+ name: "finds scope bindings for function declarations",
+ file: "scopes/function-declaration",
+ locations: [
+ [2, 0],
+ [3, 20],
+ [5, 1],
+ [9, 0],
+ ],
+ },
+ {
+ name: "finds scope bindings for function expressions",
+ file: "scopes/function-expression",
+ locations: [
+ [2, 0],
+ [3, 23],
+ [6, 0],
+ ],
+ },
+ {
+ name: "finds scope bindings for arrow functions",
+ file: "scopes/arrow-function",
+ locations: [
+ [2, 0],
+ [4, 0],
+ [7, 0],
+ [8, 0],
+ ],
+ },
+ {
+ name: "finds scope bindings for class declarations",
+ file: "scopes/class-declaration",
+ locations: [
+ [2, 0],
+ [5, 0],
+ [7, 0],
+ ],
+ },
+ {
+ name: "finds scope bindings for class expressions",
+ file: "scopes/class-expression",
+ locations: [
+ [2, 0],
+ [5, 0],
+ [7, 0],
+ ],
+ },
+ {
+ name: "finds scope bindings for for loops",
+ file: "scopes/for-loops",
+ locations: [
+ [2, 0],
+ [3, 17],
+ [4, 17],
+ [5, 25],
+ [7, 22],
+ [8, 22],
+ [9, 23],
+ [11, 23],
+ [12, 23],
+ [13, 24],
+ ],
+ },
+ {
+ name: "finds scope bindings for try..catch",
+ file: "scopes/try-catch",
+ locations: [
+ [2, 0],
+ [4, 0],
+ [7, 0],
+ ],
+ },
+ {
+ name: "finds scope bindings for out of order declarations",
+ file: "scopes/out-of-order-declarations",
+ locations: [
+ [2, 0],
+ [5, 0],
+ [11, 0],
+ [14, 0],
+ [17, 0],
+ ],
+ },
+ {
+ name: "finds scope bindings for block statements",
+ file: "scopes/block-statement",
+ locations: [
+ [2, 0],
+ [6, 0],
+ ],
+ },
+ {
+ name: "finds scope bindings for class properties",
+ file: "scopes/class-property",
+ locations: [
+ [2, 0],
+ [4, 16],
+ [6, 12],
+ [7, 0],
+ ],
+ },
+ {
+ name: "finds scope bindings and exclude Flowtype",
+ file: "scopes/flowtype-bindings",
+ locations: [
+ [8, 0],
+ [10, 0],
+ ],
+ },
+ {
+ name: "finds scope bindings for declarations with patterns",
+ file: "scopes/pattern-declarations",
+ locations: [[1, 0]],
+ },
+ {
+ name: "finds scope bindings for switch statements",
+ file: "scopes/switch-statement",
+ locations: [
+ [2, 0],
+ [5, 0],
+ [7, 0],
+ [9, 0],
+ [11, 0],
+ [17, 0],
+ [21, 0],
+ ],
+ },
+ {
+ name: "finds scope bindings with proper types",
+ file: "scopes/binding-types",
+ locations: [
+ [5, 0],
+ [9, 0],
+ [18, 0],
+ [23, 0],
+ ],
+ },
+ {
+ name: "finds scope bindings with expression metadata",
+ file: "scopes/expressions",
+ locations: [[2, 0]],
+ },
+ ]
+);
diff --git a/devtools/client/debugger/src/workers/parser/tests/getSymbols.spec.js b/devtools/client/debugger/src/workers/parser/tests/getSymbols.spec.js
new file mode 100644
index 0000000000..723eef1fd9
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/getSymbols.spec.js
@@ -0,0 +1,51 @@
+/* eslint max-nested-callbacks: ["error", 4]*/
+/* 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 { formatSymbols } from "../utils/formatSymbols";
+import { populateSource, populateOriginalSource } from "./helpers";
+import cases from "jest-in-case";
+
+cases(
+ "Parser.getSymbols",
+ ({ name, file, original, type }) => {
+ const source = original
+ ? populateOriginalSource(file, type)
+ : populateSource(file, type);
+
+ expect(formatSymbols(source.id)).toMatchSnapshot();
+ },
+ [
+ { name: "es6", file: "es6", original: true },
+ { name: "func", file: "func", original: true },
+ { name: "function names", file: "functionNames", original: true },
+ { name: "math", file: "math" },
+ { name: "proto", file: "proto" },
+ { name: "class", file: "class", original: true },
+ { name: "var", file: "var" },
+ { name: "expression", file: "expression" },
+ { name: "allSymbols", file: "allSymbols" },
+ { name: "call sites", file: "call-sites" },
+ { name: "call expression", file: "callExpressions" },
+ { name: "object expressions", file: "object-expressions" },
+ { name: "optional chaining", file: "optional-chaining" },
+ { name: "private fields", file: "private-fields" },
+ {
+ name: "finds symbols in an html file",
+ file: "parseScriptTags",
+ type: "html",
+ },
+ { name: "component", file: "component", original: true },
+ {
+ name: "react component",
+ file: "frameworks/reactComponent",
+ original: true,
+ },
+ { name: "flow", file: "flow", original: true },
+ { name: "jsx", file: "jsx", original: true },
+ { name: "destruct", file: "destructuring" },
+
+ { name: "regexp", file: "regexp" },
+ ]
+);
diff --git a/devtools/client/debugger/src/workers/parser/tests/helpers/index.js b/devtools/client/debugger/src/workers/parser/tests/helpers/index.js
new file mode 100644
index 0000000000..47c358ae66
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/helpers/index.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 fs from "fs";
+import path from "path";
+
+import { makeMockSourceAndContent } from "../../../../utils/test-mockup";
+import { setSource } from "../../sources";
+import * as asyncValue from "../../../../utils/async-value";
+
+export function getFixture(name, type = "js") {
+ return fs.readFileSync(
+ path.join(__dirname, `../fixtures/${name}.${type}`),
+ "utf8"
+ );
+}
+
+function getSourceContent(name, type = "js") {
+ const text = getFixture(name, type);
+ let contentType = "text/javascript";
+ if (type === "html") {
+ contentType = "text/html";
+ } else if (type === "vue") {
+ contentType = "text/vue";
+ } else if (type === "ts") {
+ contentType = "text/typescript";
+ } else if (type === "tsx") {
+ contentType = "text/typescript-jsx";
+ }
+
+ return {
+ type: "text",
+ value: text,
+ contentType,
+ };
+}
+
+export function getSource(name, type) {
+ const { value: text, contentType } = getSourceContent(name, type);
+
+ return makeMockSourceAndContent(undefined, name, contentType, text);
+}
+
+export function populateSource(name, type) {
+ const { content, ...source } = getSource(name, type);
+ setSource({
+ id: source.id,
+ text: content.value,
+ contentType: content.contentType,
+ isWasm: false,
+ });
+ return {
+ ...source,
+ content: asyncValue.fulfilled(content),
+ };
+}
+
+export function getOriginalSource(name, type) {
+ return getOriginalSourceWithContent(name, type);
+}
+
+export function getOriginalSourceWithContent(name, type) {
+ const { value: text, contentType } = getSourceContent(name, type);
+
+ return makeMockSourceAndContent(
+ undefined,
+ `${name}/originalSource-1`,
+ contentType,
+ text
+ );
+}
+
+export function populateOriginalSource(name, type) {
+ const { content, ...source } = getOriginalSourceWithContent(name, type);
+ setSource({
+ id: source.id,
+ text: content.value,
+ contentType: content.contentType,
+ isWasm: false,
+ });
+ return {
+ ...source,
+ content: asyncValue.fulfilled(content),
+ };
+}
diff --git a/devtools/client/debugger/src/workers/parser/tests/mapBindings.spec.js b/devtools/client/debugger/src/workers/parser/tests/mapBindings.spec.js
new file mode 100644
index 0000000000..8c23ab5873
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/mapBindings.spec.js
@@ -0,0 +1,161 @@
+/* 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 mapExpressionBindings from "../mapBindings";
+import { parseConsoleScript } from "../utils/ast";
+import cases from "jest-in-case";
+
+const prettier = require("prettier");
+
+function format(code) {
+ return prettier.format(code, { semi: false, parser: "babel" });
+}
+
+function excludedTest({ name, expression, bindings = [] }) {
+ const safeExpression = mapExpressionBindings(
+ expression,
+ parseConsoleScript(expression),
+ bindings
+ );
+ expect(format(safeExpression)).toEqual(format(expression));
+}
+
+function includedTest({ name, expression, newExpression, bindings }) {
+ const safeExpression = mapExpressionBindings(
+ expression,
+ parseConsoleScript(expression),
+ bindings
+ );
+ expect(format(safeExpression)).toEqual(format(newExpression));
+}
+
+describe("mapExpressionBindings", () => {
+ cases("included cases", includedTest, [
+ {
+ name: "single declaration",
+ expression: "const a = 2; let b = 3; var c = 4;",
+ newExpression: "self.a = 2; self.b = 3; self.c = 4;",
+ },
+ {
+ name: "multiple declarations",
+ expression: "const a = 2, b = 3",
+ newExpression: "self.a = 2; self.b = 3",
+ },
+ {
+ name: "declaration with separate assignment",
+ expression: "let a; a = 2;",
+ newExpression: "self.a = void 0; self.a = 2;",
+ },
+ {
+ name: "multiple declarations with no assignment",
+ expression: "let a = 2, b;",
+ newExpression: "self.a = 2; self.b = void 0;",
+ },
+ {
+ name: "local bindings become assignments",
+ bindings: ["a"],
+ expression: "var a = 2;",
+ newExpression: "a = 2;",
+ },
+ {
+ name: "assignments",
+ expression: "a = 2;",
+ newExpression: "self.a = 2;",
+ },
+ {
+ name: "assignments with +=",
+ expression: "a += 2;",
+ newExpression: "self.a += 2;",
+ },
+ {
+ name: "destructuring (objects)",
+ expression: "const { a } = {}; ",
+ newExpression: "({ a: self.a } = {})",
+ },
+ {
+ name: "destructuring (arrays)",
+ expression: " var [a, ...foo] = [];",
+ newExpression: "([self.a, ...self.foo] = [])",
+ },
+ {
+ name: "destructuring (declarations)",
+ expression: "var {d,e} = {}, {f} = {}; ",
+ newExpression: `({ d: self.d, e: self.e } = {});
+ ({ f: self.f } = {})
+ `,
+ },
+ {
+ name: "destructuring & declaration",
+ expression: "const { a } = {}; var b = 3",
+ newExpression: `({ a: self.a } = {});
+ self.b = 3
+ `,
+ },
+ {
+ name: "destructuring assignment",
+ expression: "[a] = [3]",
+ newExpression: "[self.a] = [3]",
+ },
+ {
+ name: "destructuring assignment (declarations)",
+ expression: "[a] = [3]; var b = 4",
+ newExpression: "[self.a] = [3];\n self.b = 4",
+ },
+ ]);
+
+ cases("excluded cases", excludedTest, [
+ { name: "local variables", expression: "function a() { var b = 2}" },
+ { name: "functions", expression: "function a() {}" },
+ { name: "classes", expression: "class a {}" },
+
+ { name: "with", expression: "with (obj) {var a = 2;}" },
+ {
+ name: "with & declaration",
+ expression: "with (obj) {var a = 2;}; ; var b = 3",
+ },
+ {
+ name: "hoisting",
+ expression: "{ const h = 3; }",
+ },
+ {
+ name: "assignments",
+ bindings: ["a"],
+ expression: "a = 2;",
+ },
+ {
+ name: "identifier",
+ expression: "a",
+ },
+ ]);
+
+ cases("cases that we should map in the future", excludedTest, [
+ { name: "blocks (IF)", expression: "if (true) { var a = 3; }" },
+ {
+ name: "hoisting",
+ expression: "{ var g = 5; }",
+ },
+ {
+ name: "for loops bindings",
+ expression: "for (let foo = 4; false;){}",
+ },
+ ]);
+
+ cases("cases that we shouldn't map in the future", includedTest, [
+ {
+ name: "window properties",
+ expression: "var innerHeight = 3; var location = 5;",
+ newExpression: "self.innerHeight = 3; self.location = 5;",
+ },
+ {
+ name: "self declaration",
+ expression: "var self = 3",
+ newExpression: "self.self = 3",
+ },
+ {
+ name: "self assignment",
+ expression: "self = 3",
+ newExpression: "self.self = 3",
+ },
+ ]);
+});
diff --git a/devtools/client/debugger/src/workers/parser/tests/mapExpression.spec.js b/devtools/client/debugger/src/workers/parser/tests/mapExpression.spec.js
new file mode 100644
index 0000000000..6c89231012
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/mapExpression.spec.js
@@ -0,0 +1,796 @@
+/* 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 mapExpression from "../mapExpression";
+import { format } from "prettier";
+import cases from "jest-in-case";
+
+function test({
+ expression,
+ newExpression,
+ bindings,
+ mappings,
+ shouldMapBindings,
+ expectedMapped,
+ parseExpression = true,
+}) {
+ const res = mapExpression(expression, mappings, bindings, shouldMapBindings);
+
+ if (parseExpression) {
+ expect(
+ format(res.expression, {
+ parser: "babel",
+ })
+ ).toEqual(format(newExpression, { parser: "babel" }));
+ } else {
+ expect(res.expression).toEqual(newExpression);
+ }
+
+ expect(res.mapped).toEqual(expectedMapped);
+}
+
+function formatAwait(body) {
+ return `(async () => { ${body} })();`;
+}
+
+describe("mapExpression", () => {
+ cases("mapExpressions", test, [
+ {
+ name: "await",
+ expression: "await a()",
+ newExpression: formatAwait("return a()"),
+ bindings: [],
+ mappings: {},
+ shouldMapBindings: true,
+ expectedMapped: {
+ await: true,
+ bindings: false,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (multiple statements)",
+ expression: "const x = await a(); x + x",
+ newExpression: formatAwait("self.x = await a(); return x + x;"),
+ bindings: [],
+ mappings: {},
+ shouldMapBindings: true,
+ expectedMapped: {
+ await: true,
+ bindings: true,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (inner)",
+ expression: "async () => await a();",
+ newExpression: "async () => await a();",
+ bindings: [],
+ mappings: {},
+ shouldMapBindings: true,
+ expectedMapped: {
+ await: false,
+ bindings: false,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (multiple awaits)",
+ expression: "const x = await a(); await b(x)",
+ newExpression: formatAwait("self.x = await a(); return b(x);"),
+ bindings: [],
+ mappings: {},
+ shouldMapBindings: true,
+ expectedMapped: {
+ await: true,
+ bindings: true,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (assignment)",
+ expression: "let x = await sleep(100, 2)",
+ newExpression: formatAwait("return (self.x = await sleep(100, 2))"),
+ bindings: [],
+ mappings: {},
+ shouldMapBindings: true,
+ expectedMapped: {
+ await: true,
+ bindings: true,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (destructuring)",
+ expression: "const { a, c: y } = await b()",
+ newExpression: formatAwait(
+ "return ({ a: self.a, c: self.y } = await b())"
+ ),
+ bindings: [],
+ mappings: {},
+ shouldMapBindings: true,
+ expectedMapped: {
+ await: true,
+ bindings: true,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (array destructuring)",
+ expression: "const [a, y] = await b();",
+ newExpression: formatAwait("return ([self.a, self.y] = await b())"),
+ bindings: [],
+ mappings: {},
+ shouldMapBindings: true,
+ expectedMapped: {
+ await: true,
+ bindings: true,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (mixed destructuring)",
+ expression: "const [{ a }] = await b();",
+ newExpression: formatAwait("return ([{ a: self.a }] = await b())"),
+ bindings: [],
+ mappings: {},
+ shouldMapBindings: true,
+ expectedMapped: {
+ await: true,
+ bindings: true,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (destructuring, multiple statements)",
+ expression: "const { a, c: y } = await b(), { x } = await y()",
+ newExpression: formatAwait(`
+ ({ a: self.a, c: self.y } = await b())
+ return ({ x: self.x } = await y());
+ `),
+ bindings: [],
+ mappings: {},
+ shouldMapBindings: true,
+ expectedMapped: {
+ await: true,
+ bindings: true,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (destructuring, bindings)",
+ expression: "const { a, c: y } = await b();",
+ newExpression: formatAwait("return ({ a, c: y } = await b())"),
+ bindings: ["a", "y"],
+ mappings: {},
+ shouldMapBindings: true,
+ expectedMapped: {
+ await: true,
+ bindings: true,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (array destructuring, bindings)",
+ expression: "const [a, y] = await b();",
+ newExpression: formatAwait("return ([a, y] = await b())"),
+ bindings: ["a", "y"],
+ mappings: {},
+ shouldMapBindings: true,
+ expectedMapped: {
+ await: true,
+ bindings: true,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (mixed destructuring, bindings)",
+ expression: "const [{ a }] = await b();",
+ newExpression: formatAwait("return ([{ a }] = await b())"),
+ bindings: ["a"],
+ mappings: {},
+ shouldMapBindings: true,
+ expectedMapped: {
+ await: true,
+ bindings: true,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (destructuring with defaults, bindings)",
+ expression: "const { c, a = 5 } = await b();",
+ newExpression: formatAwait("return ({ c: self.c, a = 5 } = await b())"),
+ bindings: ["a", "y"],
+ mappings: {},
+ shouldMapBindings: true,
+ expectedMapped: {
+ await: true,
+ bindings: true,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (array destructuring with defaults, bindings)",
+ expression: "const [a, y = 10] = await b();",
+ newExpression: formatAwait("return ([a, y = 10] = await b())"),
+ bindings: ["a", "y"],
+ mappings: {},
+ shouldMapBindings: true,
+ expectedMapped: {
+ await: true,
+ bindings: true,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (mixed destructuring with defaults, bindings)",
+ expression: "const [{ c = 5 }, a = 5] = await b();",
+ newExpression: formatAwait(
+ "return ([ { c: self.c = 5 }, a = 5] = await b())"
+ ),
+ bindings: ["a"],
+ mappings: {},
+ shouldMapBindings: true,
+ expectedMapped: {
+ await: true,
+ bindings: true,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (nested destructuring, bindings)",
+ expression: "const { a, c: { y } } = await b();",
+ newExpression: formatAwait(`
+ return ({
+ a,
+ c: { y }
+ } = await b());
+ `),
+ bindings: ["a", "y"],
+ mappings: {},
+ shouldMapBindings: true,
+ expectedMapped: {
+ await: true,
+ bindings: true,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (nested destructuring with defaults)",
+ expression: "const { a, c: { y = 5 } = {} } = await b();",
+ newExpression: formatAwait(`return ({
+ a: self.a,
+ c: { y: self.y = 5 } = {},
+ } = await b());
+ `),
+ bindings: [],
+ mappings: {},
+ shouldMapBindings: true,
+ expectedMapped: {
+ await: true,
+ bindings: true,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (very nested destructuring with defaults)",
+ expression:
+ "const { a, c: { y: { z = 10, b } = { b: 5 } } } = await b();",
+ newExpression: formatAwait(`
+ return ({
+ a: self.a,
+ c: {
+ y: { z: self.z = 10, b: self.b } = {
+ b: 5
+ }
+ }
+ } = await b());
+ `),
+ bindings: [],
+ mappings: {},
+ shouldMapBindings: true,
+ expectedMapped: {
+ await: true,
+ bindings: true,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (with SyntaxError)",
+ expression: "await new Promise())",
+ newExpression: formatAwait("await new Promise())"),
+ parseExpression: false,
+ bindings: [],
+ mappings: {},
+ shouldMapBindings: true,
+ expectedMapped: {
+ await: true,
+ bindings: false,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (no bindings, let assignment)",
+ expression: "let a = await 123;",
+ newExpression: `let a;
+ (async () => {
+ return a = await 123;
+ })()`,
+ shouldMapBindings: false,
+ expectedMapped: {
+ await: true,
+ bindings: false,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (no bindings, var assignment)",
+ expression: "var a = await 123;",
+ newExpression: `var a;
+ (async () => {
+ return a = await 123;
+ })()`,
+ shouldMapBindings: false,
+ expectedMapped: {
+ await: true,
+ bindings: false,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (no bindings, const assignment)",
+ expression: "const a = await 123;",
+ newExpression: `let a;
+ (async () => {
+ return a = await 123;
+ })()`,
+ shouldMapBindings: false,
+ expectedMapped: {
+ await: true,
+ bindings: false,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (no bindings, multiple assignments)",
+ expression: "let a = 1, b, c = 3; b = await 123; a + b + c",
+ newExpression: `let a, b, c;
+ (async () => {
+ a = 1;
+ c = 3;
+ b = await 123;
+ return a + b + c;
+ })()`,
+ shouldMapBindings: false,
+ expectedMapped: {
+ await: true,
+ bindings: false,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (no bindings, object destructuring)",
+ expression: "let {a, b, c} = await x;",
+ newExpression: `let a, b, c;
+ (async () => {
+ return ({a, b, c} = await x);
+ })()`,
+ shouldMapBindings: false,
+ expectedMapped: {
+ await: true,
+ bindings: false,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (no bindings, object destructuring with rest)",
+ expression: "let {a, ...rest} = await x;",
+ newExpression: `let a, rest;
+ (async () => {
+ return ({a, ...rest} = await x);
+ })()`,
+ shouldMapBindings: false,
+ expectedMapped: {
+ await: true,
+ bindings: false,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (no bindings, object destructuring with renaming and default)",
+ expression: "let {a: hello, b, c: world, d: $ = 4} = await x;",
+ newExpression: `let hello, b, world, $;
+ (async () => {
+ return ({a: hello, b, c: world, d: $ = 4} = await x);
+ })()`,
+ shouldMapBindings: false,
+ expectedMapped: {
+ await: true,
+ bindings: false,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (no bindings, nested object destructuring + renaming + default)",
+ expression: `let {
+ a: hello, c: { y: { z = 10, b: bill, d: [e, f = 20] }}
+ } = await x; z;`,
+ newExpression: `let hello, z, bill, e, f;
+ (async () => {
+ ({ a: hello, c: { y: { z = 10, b: bill, d: [e, f = 20] }}} = await x);
+ return z;
+ })()`,
+ shouldMapBindings: false,
+ expectedMapped: {
+ await: true,
+ bindings: false,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (no bindings, array destructuring)",
+ expression: "let [a, b, c] = await x; c;",
+ newExpression: `let a, b, c;
+ (async () => {
+ [a, b, c] = await x;
+ return c;
+ })()`,
+ shouldMapBindings: false,
+ expectedMapped: {
+ await: true,
+ bindings: false,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (no bindings, array destructuring with default)",
+ expression: "let [a, b = 1, c = 2] = await x; c;",
+ newExpression: `let a, b, c;
+ (async () => {
+ [a, b = 1, c = 2] = await x;
+ return c;
+ })()`,
+ shouldMapBindings: false,
+ expectedMapped: {
+ await: true,
+ bindings: false,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (no bindings, array destructuring with default and rest)",
+ expression: "let [a, b = 1, c = 2, ...rest] = await x; rest;",
+ newExpression: `let a, b, c, rest;
+ (async () => {
+ [a, b = 1, c = 2, ...rest] = await x;
+ return rest;
+ })()`,
+ shouldMapBindings: false,
+ expectedMapped: {
+ await: true,
+ bindings: false,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (no bindings, nested array destructuring with default)",
+ expression: "let [a, b = 1, [c = 2, [d = 3, e = 4]]] = await x; c;",
+ newExpression: `let a, b, c, d, e;
+ (async () => {
+ [a, b = 1, [c = 2, [d = 3, e = 4]]] = await x;
+ return c;
+ })()`,
+ shouldMapBindings: false,
+ expectedMapped: {
+ await: true,
+ bindings: false,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (no bindings, dynamic import)",
+ expression: `
+ var {rainbowLog} = await import("./cool-module.js");
+ rainbowLog("dynamic");`,
+ newExpression: `var rainbowLog;
+ (async () => {
+ ({rainbowLog} = await import("./cool-module.js"));
+ return rainbowLog("dynamic");
+ })()`,
+ shouldMapBindings: false,
+ expectedMapped: {
+ await: true,
+ bindings: false,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (nullish coalesce operator)",
+ expression: "await x; true ?? false",
+ newExpression: `(async () => {
+ await x;
+ return true ?? false;
+ })()`,
+ shouldMapBindings: false,
+ expectedMapped: {
+ await: true,
+ bindings: false,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (optional chaining operator)",
+ expression: "await x; x?.y?.z",
+ newExpression: `(async () => {
+ await x;
+ return x?.y?.z;
+ })()`,
+ shouldMapBindings: false,
+ expectedMapped: {
+ await: true,
+ bindings: false,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (async function declaration with nullish coalesce operator)",
+ expression: "async function coalesce(x) { await x; return x ?? false; }",
+ newExpression:
+ "async function coalesce(x) { await x; return x ?? false; }",
+ shouldMapBindings: false,
+ expectedMapped: {
+ await: false,
+ bindings: false,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (async function declaration with optional chaining operator)",
+ expression: "async function chain(x) { await x; return x?.y?.z; }",
+ newExpression: "async function chain(x) { await x; return x?.y?.z; }",
+ shouldMapBindings: false,
+ expectedMapped: {
+ await: false,
+ bindings: false,
+ originalExpression: false,
+ },
+ },
+ {
+ // check that variable declaration in for loop is not put outside of the async iife
+ name: "await (for loop)",
+ expression: "for (let i=0;i<2;i++) {}; var b = await 1;",
+ newExpression: `var b;
+ (async () => {
+ for (let i=0;i<2;i++) {}
+ return (b = await 1);
+ })()`,
+ shouldMapBindings: false,
+ expectedMapped: {
+ await: true,
+ bindings: false,
+ originalExpression: false,
+ },
+ },
+ {
+ // check that variable declaration in for-in loop is not put outside of the async iife
+ name: "await (for..in loop)",
+ expression: "for (let i in {}) {}; var b = await 1;",
+ newExpression: `var b;
+ (async () => {
+ for (let i in {}) {}
+ return (b = await 1);
+ })()`,
+ shouldMapBindings: false,
+ expectedMapped: {
+ await: true,
+ bindings: false,
+ originalExpression: false,
+ },
+ },
+ {
+ // check that variable declaration in for-of loop is not put outside of the async iife
+ name: "await (for..of loop)",
+ expression: "for (let i of []) {}; var b = await 1;",
+ newExpression: `var b;
+ (async () => {
+ for (let i of []) {}
+ return (b = await 1);
+ })()`,
+ shouldMapBindings: false,
+ expectedMapped: {
+ await: true,
+ bindings: false,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (if condition)",
+ expression: "if (await true) console.log(1);",
+ newExpression: formatAwait("if (await true) console.log(1);"),
+ shouldMapBindings: false,
+ expectedMapped: {
+ await: true,
+ bindings: false,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "await (non-expression final statement: bug 1851759)",
+ expression: `j = { "foo": 1, "bar": 2 }; await 42; for (var k in j) { console.log(k); }`,
+ newExpression: formatAwait(`
+ j = {
+ foo: 1,
+ bar: 2,
+ };
+ await 42;
+ for (var k in j) {
+ console.log(k);
+ }`),
+ shouldMapBindings: false,
+ expectedMapped: {
+ await: true,
+ bindings: false,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "simple",
+ expression: "a",
+ newExpression: "a",
+ bindings: [],
+ mappings: {},
+ shouldMapBindings: true,
+ expectedMapped: {
+ await: false,
+ bindings: false,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "mappings",
+ expression: "a",
+ newExpression: "_a",
+ bindings: [],
+ mappings: {
+ a: "_a",
+ },
+ shouldMapBindings: true,
+ expectedMapped: {
+ await: false,
+ bindings: false,
+ originalExpression: true,
+ },
+ },
+ {
+ name: "declaration",
+ expression: "var a = 3;",
+ newExpression: "self.a = 3",
+ bindings: [],
+ mappings: {},
+ shouldMapBindings: true,
+ expectedMapped: {
+ await: false,
+ bindings: true,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "declaration + destructuring",
+ expression: "var { a } = { a: 3 };",
+ newExpression: "({ a: self.a } = {\n a: 3 \n})",
+ bindings: [],
+ mappings: {},
+ shouldMapBindings: true,
+ expectedMapped: {
+ await: false,
+ bindings: true,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "bindings",
+ expression: "var a = 3;",
+ newExpression: "a = 3",
+ bindings: ["a"],
+ mappings: {},
+ shouldMapBindings: true,
+ expectedMapped: {
+ await: false,
+ bindings: true,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "bindings + destructuring",
+ expression: "var { a } = { a: 3 };",
+ newExpression: "({ a } = { \n a: 3 \n })",
+ bindings: ["a"],
+ mappings: {},
+ shouldMapBindings: true,
+ expectedMapped: {
+ await: false,
+ bindings: true,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "bindings + destructuring + rest",
+ expression: "var { a, ...foo } = {}",
+ newExpression: "({ a, ...self.foo } = {})",
+ bindings: ["a"],
+ mappings: {},
+ shouldMapBindings: true,
+ expectedMapped: {
+ await: false,
+ bindings: true,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "bindings + array destructuring + rest",
+ expression: "var [a, ...foo] = []",
+ newExpression: "([a, ...self.foo] = [])",
+ bindings: ["a"],
+ mappings: {},
+ shouldMapBindings: true,
+ expectedMapped: {
+ await: false,
+ bindings: true,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "bindings + mappings",
+ expression: "a = 3;",
+ newExpression: "self.a = 3",
+ bindings: ["_a"],
+ mappings: { a: "_a" },
+ shouldMapBindings: true,
+ expectedMapped: {
+ await: false,
+ bindings: true,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "bindings + mappings + destructuring",
+ expression: "var { a } = { a: 4 }",
+ newExpression: "({ a: self.a } = {\n a: 4 \n})",
+ bindings: ["_a"],
+ mappings: { a: "_a" },
+ shouldMapBindings: true,
+ expectedMapped: {
+ await: false,
+ bindings: true,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "bindings without mappings",
+ expression: "a = 3;",
+ newExpression: "a = 3",
+ bindings: [],
+ mappings: { a: "_a" },
+ shouldMapBindings: false,
+ expectedMapped: {
+ await: false,
+ bindings: false,
+ originalExpression: false,
+ },
+ },
+ {
+ name: "object destructuring + bindings without mappings",
+ expression: "({ a } = {});",
+ newExpression: "({ a: _a } = {})",
+ bindings: [],
+ mappings: { a: "_a" },
+ shouldMapBindings: false,
+ expectedMapped: {
+ await: false,
+ bindings: false,
+ originalExpression: true,
+ },
+ },
+ ]);
+});
diff --git a/devtools/client/debugger/src/workers/parser/tests/mapOriginalExpression.spec.js b/devtools/client/debugger/src/workers/parser/tests/mapOriginalExpression.spec.js
new file mode 100644
index 0000000000..4949d65cd9
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/mapOriginalExpression.spec.js
@@ -0,0 +1,93 @@
+/* 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 mapExpression from "../mapExpression";
+import { format } from "prettier";
+
+const formatOutput = output =>
+ format(output, {
+ parser: "babel",
+ });
+
+const mapOriginalExpression = (expression, mappings) =>
+ mapExpression(expression, mappings, [], false, false).expression;
+
+describe("mapOriginalExpression", () => {
+ it("simple", () => {
+ const generatedExpression = mapOriginalExpression("a + b;", {
+ a: "foo",
+ b: "bar",
+ });
+ expect(generatedExpression).toEqual("foo + bar;");
+ });
+
+ it("this", () => {
+ const generatedExpression = mapOriginalExpression("this.prop;", {
+ this: "_this",
+ });
+ expect(generatedExpression).toEqual("_this.prop;");
+ });
+
+ it("member expressions", () => {
+ const generatedExpression = mapOriginalExpression("a + b", {
+ a: "_mod.foo",
+ b: "_mod.bar",
+ });
+ expect(generatedExpression).toEqual("_mod.foo + _mod.bar;");
+ });
+
+ it("block", () => {
+ // todo: maybe wrap with parens ()
+ const generatedExpression = mapOriginalExpression("{a}", {
+ a: "_mod.foo",
+ b: "_mod.bar",
+ });
+ expect(generatedExpression).toEqual("{\n _mod.foo;\n}");
+ });
+
+ it("skips codegen with no mappings", () => {
+ const generatedExpression = mapOriginalExpression("a + b", {
+ a: "a",
+ c: "_c",
+ });
+ expect(generatedExpression).toEqual("a + b");
+ });
+
+ it("object destructuring", () => {
+ const generatedExpression = mapOriginalExpression("({ a } = { a: 4 })", {
+ a: "_mod.foo",
+ });
+
+ expect(formatOutput(generatedExpression)).toEqual(
+ formatOutput("({ a: _mod.foo } = {\n a: 4 \n})")
+ );
+ });
+
+ it("nested object destructuring", () => {
+ const generatedExpression = mapOriginalExpression(
+ "({ a: { b, c } } = { a: 4 })",
+ {
+ a: "_mod.foo",
+ b: "_mod.bar",
+ }
+ );
+
+ expect(formatOutput(generatedExpression)).toEqual(
+ formatOutput("({ a: { b: _mod.bar, c } } = {\n a: 4 \n})")
+ );
+ });
+
+ it("shadowed bindings", () => {
+ const generatedExpression = mapOriginalExpression(
+ "window.thing = function fn(){ var a; a; b; }; a; b; ",
+ {
+ a: "_a",
+ b: "_b",
+ }
+ );
+ expect(generatedExpression).toEqual(
+ "window.thing = function fn() {\n var a;\n a;\n _b;\n};\n_a;\n_b;"
+ );
+ });
+});
diff --git a/devtools/client/debugger/src/workers/parser/tests/sources.spec.js b/devtools/client/debugger/src/workers/parser/tests/sources.spec.js
new file mode 100644
index 0000000000..e84ae4ad22
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/tests/sources.spec.js
@@ -0,0 +1,14 @@
+/* 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 { getSource } from "../sources";
+
+describe("sources", () => {
+ it("fail getSource", () => {
+ const sourceId = "some.nonexistent.source.id";
+ expect(() => {
+ getSource(sourceId);
+ }).toThrow();
+ });
+});
diff --git a/devtools/client/debugger/src/workers/parser/utils/ast.js b/devtools/client/debugger/src/workers/parser/utils/ast.js
new file mode 100644
index 0000000000..445645c12f
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/utils/ast.js
@@ -0,0 +1,230 @@
+/* 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 { parseScriptTags } from "./parse-script-tags";
+import * as babelParser from "@babel/parser";
+import * as t from "@babel/types";
+import { getSource } from "../sources";
+
+const ASTs = new Map();
+
+function _parse(code, opts) {
+ return babelParser.parse(code, {
+ ...opts,
+ tokens: true,
+ });
+}
+
+const sourceOptions = {
+ generated: {
+ sourceType: "unambiguous",
+ tokens: true,
+ plugins: [
+ "classStaticBlock",
+ "classPrivateProperties",
+ "classPrivateMethods",
+ "classProperties",
+ "objectRestSpread",
+ "optionalChaining",
+ "privateIn",
+ "nullishCoalescingOperator",
+ "regexpUnicodeSets",
+ ],
+ },
+ original: {
+ sourceType: "unambiguous",
+ tokens: true,
+ plugins: [
+ "jsx",
+ "flow",
+ "doExpressions",
+ "optionalChaining",
+ "nullishCoalescingOperator",
+ "decorators-legacy",
+ "objectRestSpread",
+ "classStaticBlock",
+ "classPrivateProperties",
+ "classPrivateMethods",
+ "classProperties",
+ "exportDefaultFrom",
+ "exportNamespaceFrom",
+ "asyncGenerators",
+ "functionBind",
+ "functionSent",
+ "dynamicImport",
+ "react-jsx",
+ "regexpUnicodeSets",
+ ],
+ },
+};
+
+export function parse(text, opts) {
+ let ast = {};
+ if (!text) {
+ return ast;
+ }
+
+ try {
+ ast = _parse(text, opts);
+ } catch (error) {
+ console.error(error);
+ }
+
+ return ast;
+}
+
+// Custom parser for parse-script-tags that adapts its input structure to
+// our parser's signature
+function htmlParser({ source, line }) {
+ return parse(source, { startLine: line, ...sourceOptions.generated });
+}
+
+const VUE_COMPONENT_START = /^\s*</;
+function vueParser({ source, line }) {
+ return parse(source, {
+ startLine: line,
+ ...sourceOptions.original,
+ });
+}
+function parseVueScript(code) {
+ if (typeof code !== "string") {
+ return {};
+ }
+
+ let ast;
+
+ // .vue files go through several passes, so while there is a
+ // single-file-component Vue template, there are also generally .vue files
+ // that are still just JS as well.
+ if (code.match(VUE_COMPONENT_START)) {
+ ast = parseScriptTags(code, vueParser);
+ if (t.isFile(ast)) {
+ // parseScriptTags is currently hard-coded to return scripts, but Vue
+ // always expects ESM syntax, so we just hard-code it.
+ ast.program.sourceType = "module";
+ }
+ } else {
+ ast = parse(code, sourceOptions.original);
+ }
+ return ast;
+}
+
+export function parseConsoleScript(text, opts) {
+ try {
+ return _parse(text, {
+ plugins: [
+ "classStaticBlock",
+ "classPrivateProperties",
+ "classPrivateMethods",
+ "objectRestSpread",
+ "dynamicImport",
+ "nullishCoalescingOperator",
+ "optionalChaining",
+ "regexpUnicodeSets",
+ ],
+ ...opts,
+ allowAwaitOutsideFunction: true,
+ });
+ } catch (e) {
+ return null;
+ }
+}
+
+export function parseScript(text, opts) {
+ return _parse(text, opts);
+}
+
+export function getAst(sourceId) {
+ if (ASTs.has(sourceId)) {
+ return ASTs.get(sourceId);
+ }
+
+ const source = getSource(sourceId);
+
+ if (source.isWasm) {
+ return null;
+ }
+
+ let ast = {};
+ const { contentType } = source;
+ if (contentType == "text/html") {
+ ast = parseScriptTags(source.text, htmlParser) || {};
+ } else if (contentType && contentType === "text/vue") {
+ ast = parseVueScript(source.text) || {};
+ } else if (
+ contentType &&
+ contentType.match(/(javascript|jsx)/) &&
+ !contentType.match(/typescript-jsx/)
+ ) {
+ const type = source.id.includes("original") ? "original" : "generated";
+ const options = sourceOptions[type];
+ ast = parse(source.text, options);
+ } else if (contentType && contentType.match(/typescript/)) {
+ const options = {
+ ...sourceOptions.original,
+ plugins: [
+ ...sourceOptions.original.plugins.filter(
+ p =>
+ p !== "flow" &&
+ p !== "decorators" &&
+ p !== "decorators2" &&
+ (p !== "jsx" || contentType.match(/typescript-jsx/))
+ ),
+ "decorators-legacy",
+ "typescript",
+ ],
+ };
+ ast = parse(source.text, options);
+ }
+
+ ASTs.set(source.id, ast);
+ return ast;
+}
+
+export function clearASTs(sourceIds) {
+ for (const sourceId of sourceIds) {
+ ASTs.delete(sourceId);
+ }
+}
+
+export function traverseAst(sourceId, visitor, state) {
+ const ast = getAst(sourceId);
+ if (!ast || !Object.keys(ast).length) {
+ return null;
+ }
+
+ t.traverse(ast, visitor, state);
+ return ast;
+}
+
+export function hasNode(rootNode, predicate) {
+ try {
+ t.traverse(rootNode, {
+ enter: (node, ancestors) => {
+ if (predicate(node, ancestors)) {
+ throw new Error("MATCH");
+ }
+ },
+ });
+ } catch (e) {
+ if (e.message === "MATCH") {
+ return true;
+ }
+ }
+ return false;
+}
+
+export function replaceNode(ancestors, node) {
+ const parent = ancestors[ancestors.length - 1];
+
+ if (typeof parent.index === "number") {
+ if (Array.isArray(node)) {
+ parent.node[parent.key].splice(parent.index, 1, ...node);
+ } else {
+ parent.node[parent.key][parent.index] = node;
+ }
+ } else {
+ parent.node[parent.key] = node;
+ }
+}
diff --git a/devtools/client/debugger/src/workers/parser/utils/contains.js b/devtools/client/debugger/src/workers/parser/utils/contains.js
new file mode 100644
index 0000000000..ed4cb31c1d
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/utils/contains.js
@@ -0,0 +1,29 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+function startsBefore(a, b) {
+ let before = a.start.line < b.line;
+ if (a.start.line === b.line) {
+ before =
+ a.start.column >= 0 && b.column >= 0 ? a.start.column <= b.column : true;
+ }
+ return before;
+}
+
+function endsAfter(a, b) {
+ let after = a.end.line > b.line;
+ if (a.end.line === b.line) {
+ after =
+ a.end.column >= 0 && b.column >= 0 ? a.end.column >= b.column : true;
+ }
+ return after;
+}
+
+export function containsPosition(a, b) {
+ return startsBefore(a, b) && endsAfter(a, b);
+}
+
+export function containsLocation(a, b) {
+ return containsPosition(a, b.start) && containsPosition(a, b.end);
+}
diff --git a/devtools/client/debugger/src/workers/parser/utils/formatSymbols.js b/devtools/client/debugger/src/workers/parser/utils/formatSymbols.js
new file mode 100644
index 0000000000..3bcf37e7c4
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/utils/formatSymbols.js
@@ -0,0 +1,65 @@
+/* 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 { getSymbols } from "../getSymbols";
+
+function formatLocation(loc) {
+ if (!loc) {
+ return "";
+ }
+
+ const { start, end } = loc;
+ const startLoc = `(${start.line}, ${start.column})`;
+ const endLoc = `(${end.line}, ${end.column})`;
+
+ return `[${startLoc}, ${endLoc}]`;
+}
+
+function summarize(symbol) {
+ if (typeof symbol == "boolean") {
+ return symbol ? "true" : "false";
+ }
+
+ const loc = formatLocation(symbol.location);
+ const params = symbol.parameterNames
+ ? `(${symbol.parameterNames.join(", ")})`
+ : "";
+ const expression = symbol.expression || "";
+ const klass = symbol.klass || "";
+ const name = symbol.name == undefined ? "" : symbol.name;
+ const names = symbol.specifiers ? symbol.specifiers.join(", ") : "";
+ const values = symbol.values ? symbol.values.join(", ") : "";
+ const index = symbol.index ? symbol.index : "";
+
+ return `${loc} ${expression} ${name}${params} ${klass} ${names} ${values} ${index}`.trim(); // eslint-disable-line max-len
+}
+const bools = ["hasJsx", "hasTypes"];
+const strings = ["framework"];
+function formatBool(name, symbols) {
+ return `${name}: ${symbols[name] ? "true" : "false"}`;
+}
+
+function formatString(name, symbols) {
+ return `${name}: ${symbols[name]}`;
+}
+
+function formatKey(name, symbols) {
+ if (bools.includes(name)) {
+ return formatBool(name, symbols);
+ }
+
+ if (strings.includes(name)) {
+ return formatString(name, symbols);
+ }
+
+ return `${name}:\n${symbols[name].map(summarize).join("\n")}`;
+}
+
+export function formatSymbols(sourceId) {
+ const symbols = getSymbols(sourceId);
+
+ return Object.keys(symbols)
+ .map(name => formatKey(name, symbols))
+ .join("\n\n");
+}
diff --git a/devtools/client/debugger/src/workers/parser/utils/getFunctionName.js b/devtools/client/debugger/src/workers/parser/utils/getFunctionName.js
new file mode 100644
index 0000000000..1fe85a5c69
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/utils/getFunctionName.js
@@ -0,0 +1,96 @@
+/* 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 * as t from "@babel/types";
+
+// Perform ES6's anonymous function name inference for all
+// locations where static analysis is possible.
+// eslint-disable-next-line complexity
+export default function getFunctionName(node, parent) {
+ if (t.isIdentifier(node.id)) {
+ return node.id.name;
+ }
+
+ if (
+ t.isObjectMethod(node, { computed: false }) ||
+ t.isClassMethod(node, { computed: false }) ||
+ t.isClassPrivateMethod(node)
+ ) {
+ const { key } = node;
+
+ if (t.isIdentifier(key)) {
+ return key.name;
+ }
+ if (t.isStringLiteral(key)) {
+ return key.value;
+ }
+ if (t.isNumericLiteral(key)) {
+ return `${key.value}`;
+ }
+
+ if (t.isPrivateName(key)) {
+ return `#${key.id.name}`;
+ }
+ }
+
+ if (
+ t.isObjectProperty(parent, { computed: false, value: node }) ||
+ // TODO: Babylon 6 doesn't support computed class props. It is included
+ // here so that it is most flexible. Once Babylon 7 is used, this
+ // can change to use computed: false like ObjectProperty.
+ (t.isClassProperty(parent, { value: node }) && !parent.computed) ||
+ (t.isClassPrivateProperty(parent, { value: node }) && !parent.computed)
+ ) {
+ const { key } = parent;
+
+ if (t.isIdentifier(key)) {
+ return key.name;
+ }
+ if (t.isStringLiteral(key)) {
+ return key.value;
+ }
+ if (t.isNumericLiteral(key)) {
+ return `${key.value}`;
+ }
+
+ if (t.isPrivateName(key)) {
+ return `#${key.id.name}`;
+ }
+ }
+
+ if (t.isAssignmentExpression(parent, { operator: "=", right: node })) {
+ if (t.isIdentifier(parent.left)) {
+ return parent.left.name;
+ }
+
+ // This case is not supported in standard ES6 name inference, but it
+ // is included here since it is still a helpful case during debugging.
+ if (t.isMemberExpression(parent.left, { computed: false })) {
+ return parent.left.property.name;
+ }
+ }
+
+ if (
+ t.isAssignmentPattern(parent, { right: node }) &&
+ t.isIdentifier(parent.left)
+ ) {
+ return parent.left.name;
+ }
+
+ if (
+ t.isVariableDeclarator(parent, { init: node }) &&
+ t.isIdentifier(parent.id)
+ ) {
+ return parent.id.name;
+ }
+
+ if (
+ t.isExportDefaultDeclaration(parent, { declaration: node }) &&
+ t.isFunctionDeclaration(node)
+ ) {
+ return "default";
+ }
+
+ return "anonymous";
+}
diff --git a/devtools/client/debugger/src/workers/parser/utils/helpers.js b/devtools/client/debugger/src/workers/parser/utils/helpers.js
new file mode 100644
index 0000000000..0045b54136
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/utils/helpers.js
@@ -0,0 +1,232 @@
+/* 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 * as t from "@babel/types";
+import generate from "@babel/generator";
+
+export function isFunction(node) {
+ return (
+ t.isFunction(node) ||
+ t.isArrowFunctionExpression(node) ||
+ t.isObjectMethod(node) ||
+ t.isClassMethod(node)
+ );
+}
+
+export function isAwaitExpression(path) {
+ const { node, parent } = path;
+ return (
+ t.isAwaitExpression(node) ||
+ t.isAwaitExpression(parent.init) ||
+ t.isAwaitExpression(parent)
+ );
+}
+
+export function isYieldExpression(path) {
+ const { node, parent } = path;
+ return (
+ t.isYieldExpression(node) ||
+ t.isYieldExpression(parent.init) ||
+ t.isYieldExpression(parent)
+ );
+}
+
+export function isObjectShorthand(parent) {
+ if (!t.isObjectProperty(parent)) {
+ return false;
+ }
+
+ if (parent.value && parent.value.left) {
+ return (
+ parent.value.type === "AssignmentPattern" &&
+ parent.value.left.type === "Identifier"
+ );
+ }
+
+ return (
+ parent.value &&
+ parent.key.start == parent.value.start &&
+ parent.key.loc.identifierName === parent.value.loc.identifierName
+ );
+}
+
+export function getObjectExpressionValue(node) {
+ const { value } = node;
+
+ if (t.isIdentifier(value)) {
+ return value.name;
+ }
+
+ if (t.isCallExpression(value) || t.isFunctionExpression(value)) {
+ return "";
+ }
+ const code = generate(value).code;
+
+ const shouldWrap = t.isObjectExpression(value);
+ return shouldWrap ? `(${code})` : code;
+}
+
+export function getCode(node) {
+ return generate(node).code;
+}
+
+export function getComments(ast) {
+ if (!ast || !ast.comments) {
+ return [];
+ }
+ return ast.comments.map(comment => ({
+ name: comment.location,
+ location: comment.loc,
+ }));
+}
+
+export function isComputedExpression(expression) {
+ return /^\[/m.test(expression);
+}
+
+export function getMemberExpression(root) {
+ function _getMemberExpression(node, expr) {
+ if (t.isMemberExpression(node)) {
+ expr = [node.property.name].concat(expr);
+ return _getMemberExpression(node.object, expr);
+ }
+
+ if (t.isCallExpression(node)) {
+ return [];
+ }
+
+ if (t.isThisExpression(node)) {
+ return ["this"].concat(expr);
+ }
+
+ return [node.name].concat(expr);
+ }
+
+ const expr = _getMemberExpression(root, []);
+ return expr.join(".");
+}
+
+export function getVariables(dec) {
+ if (!dec.id) {
+ return [];
+ }
+
+ if (t.isArrayPattern(dec.id)) {
+ if (!dec.id.elements) {
+ return [];
+ }
+
+ // NOTE: it's possible that an element is empty or has several variables
+ // e.g. const [, a] = arr
+ // e.g. const [{a, b }] = 2
+ return dec.id.elements
+ .filter(Boolean)
+ .map(element => ({
+ name: t.isAssignmentPattern(element)
+ ? element.left.name
+ : element.name || element.argument?.name,
+ location: element.loc,
+ }))
+ .filter(({ name }) => name);
+ }
+
+ return [
+ {
+ name: dec.id.name,
+ location: dec.loc,
+ },
+ ];
+}
+
+/**
+ * Add the identifiers for a given object pattern.
+ *
+ * @param {Array.<Object>} identifiers
+ * the current list of identifiers where to push the new identifiers
+ * related to this path.
+ * @param {Set<String>} identifiersKeys
+ * List of currently registered identifier location key.
+ * @param {Object} pattern
+ */
+export function addPatternIdentifiers(identifiers, identifiersKeys, pattern) {
+ let items;
+ if (t.isObjectPattern(pattern)) {
+ items = pattern.properties.map(({ value }) => value);
+ }
+
+ if (t.isArrayPattern(pattern)) {
+ items = pattern.elements;
+ }
+
+ if (items) {
+ addIdentifiers(identifiers, identifiersKeys, items);
+ }
+}
+
+function addIdentifiers(identifiers, identifiersKeys, items) {
+ for (const item of items) {
+ if (t.isObjectPattern(item) || t.isArrayPattern(item)) {
+ addPatternIdentifiers(identifiers, identifiersKeys, item);
+ } else if (t.isIdentifier(item)) {
+ if (!identifiersKeys.has(nodeLocationKey(item.loc))) {
+ identifiers.push({
+ name: item.name,
+ expression: item.name,
+ location: item.loc,
+ });
+ }
+ }
+ }
+}
+
+// Top Level checks the number of "body" nodes in the ancestor chain
+// if the node is top-level, then it shoul only have one body.
+export function isTopLevel(ancestors) {
+ return ancestors.filter(ancestor => ancestor.key == "body").length == 1;
+}
+
+export function nodeLocationKey({ start, end }) {
+ return `${start.line}:${start.column}:${end.line}:${end.column}`;
+}
+
+export function getFunctionParameterNames(path) {
+ if (path.node.params != null) {
+ return path.node.params.map(param => {
+ if (param.type !== "AssignmentPattern") {
+ return param.name;
+ }
+
+ // Parameter with default value
+ if (
+ param.left.type === "Identifier" &&
+ param.right.type === "Identifier"
+ ) {
+ return `${param.left.name} = ${param.right.name}`;
+ } else if (
+ param.left.type === "Identifier" &&
+ param.right.type === "StringLiteral"
+ ) {
+ return `${param.left.name} = ${param.right.value}`;
+ } else if (
+ param.left.type === "Identifier" &&
+ param.right.type === "ObjectExpression"
+ ) {
+ return `${param.left.name} = {}`;
+ } else if (
+ param.left.type === "Identifier" &&
+ param.right.type === "ArrayExpression"
+ ) {
+ return `${param.left.name} = []`;
+ } else if (
+ param.left.type === "Identifier" &&
+ param.right.type === "NullLiteral"
+ ) {
+ return `${param.left.name} = null`;
+ }
+
+ return null;
+ });
+ }
+ return [];
+}
diff --git a/devtools/client/debugger/src/workers/parser/utils/inferClassName.js b/devtools/client/debugger/src/workers/parser/utils/inferClassName.js
new file mode 100644
index 0000000000..09d25f275d
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/utils/inferClassName.js
@@ -0,0 +1,93 @@
+/* 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 * as t from "@babel/types";
+
+// the function class is inferred from a call like
+// createClass or extend
+function fromCallExpression(callExpression) {
+ const allowlist = ["extend", "createClass"];
+ const { callee } = callExpression.node;
+ if (!callee) {
+ return null;
+ }
+
+ const name = t.isMemberExpression(callee)
+ ? callee.property.name
+ : callee.name;
+
+ if (!allowlist.includes(name)) {
+ return null;
+ }
+
+ const variable = callExpression.findParent(p =>
+ t.isVariableDeclarator(p.node)
+ );
+ if (variable) {
+ return variable.node.id.name;
+ }
+
+ const assignment = callExpression.findParent(p =>
+ t.isAssignmentExpression(p.node)
+ );
+
+ if (!assignment) {
+ return null;
+ }
+
+ const { left } = assignment.node;
+
+ if (left.name) {
+ return name;
+ }
+
+ if (t.isMemberExpression(left)) {
+ return left.property.name;
+ }
+
+ return null;
+}
+
+// the function class is inferred from a prototype assignment
+// e.g. TodoClass.prototype.render = function() {}
+function fromPrototype(assignment) {
+ const { left } = assignment.node;
+ if (!left) {
+ return null;
+ }
+
+ if (
+ t.isMemberExpression(left) &&
+ left.object &&
+ t.isMemberExpression(left.object) &&
+ left.object.property.identifier === "prototype"
+ ) {
+ return left.object.object.name;
+ }
+
+ return null;
+}
+
+// infer class finds an appropriate class for functions
+// that are defined inside of a class like thing.
+// e.g. `class Foo`, `TodoClass.prototype.foo`,
+// `Todo = createClass({ foo: () => {}})`
+export function inferClassName(path) {
+ const classDeclaration = path.findParent(p => t.isClassDeclaration(p.node));
+ if (classDeclaration) {
+ return classDeclaration.node.id.name;
+ }
+
+ const callExpression = path.findParent(p => t.isCallExpression(p.node));
+ if (callExpression) {
+ return fromCallExpression(callExpression);
+ }
+
+ const assignment = path.findParent(p => t.isAssignmentExpression(p.node));
+ if (assignment) {
+ return fromPrototype(assignment);
+ }
+
+ return null;
+}
diff --git a/devtools/client/debugger/src/workers/parser/utils/parse-script-tags/customParse.js b/devtools/client/debugger/src/workers/parser/utils/parse-script-tags/customParse.js
new file mode 100644
index 0000000000..5c7fed480d
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/utils/parse-script-tags/customParse.js
@@ -0,0 +1,132 @@
+/**
+ * https://github.com/ryanjduffy/parse-script-tags
+ *
+ * Copyright (c) by Ryan Duff
+ * Licensed under the MIT License.
+ */
+
+import * as types from "@babel/types";
+
+import parseFragment from "./parseScriptFragment.js";
+
+const startScript = /<script[^>]*>/im;
+const endScript = /<\/script\s*>/im;
+// https://stackoverflow.com/questions/5034781/js-regex-to-split-by-line#comment5633979_5035005
+const newLines = /\r\n|[\n\v\f\r\x85\u2028\u2029]/;
+
+function getType(tag) {
+ const fragment = parseFragment(tag);
+
+ if (fragment) {
+ const type = fragment.attributes.type;
+
+ return type ? type.toLowerCase() : null;
+ }
+
+ return null;
+}
+
+function getCandidateScriptLocations(source, index) {
+ const i = index || 0;
+ const str = source.substring(i);
+
+ const startMatch = startScript.exec(str);
+ if (startMatch) {
+ const startsAt = startMatch.index + startMatch[0].length;
+ const afterStart = str.substring(startsAt);
+ const endMatch = endScript.exec(afterStart);
+ if (endMatch) {
+ const locLength = endMatch.index;
+ const locIndex = i + startsAt;
+ const endIndex = locIndex + locLength + endMatch[0].length;
+
+ // extract the complete tag (incl start and end tags and content). if the
+ // type is invalid (= not JS), skip this tag and continue
+ const tag = source.substring(i + startMatch.index, endIndex);
+ const type = getType(tag);
+ if (
+ type &&
+ type !== "javascript" &&
+ type !== "text/javascript" &&
+ type !== "module"
+ ) {
+ return getCandidateScriptLocations(source, endIndex);
+ }
+
+ return [
+ adjustForLineAndColumn(source, {
+ index: locIndex,
+ length: locLength,
+ source: source.substring(locIndex, locIndex + locLength),
+ }),
+ ...getCandidateScriptLocations(source, endIndex),
+ ];
+ }
+ }
+
+ return [];
+}
+
+function parseScripts(locations, parser) {
+ return locations.map(parser);
+}
+
+function calcLineAndColumn(source, index) {
+ const lines = source.substring(0, index).split(newLines);
+ const line = lines.length;
+ const column = lines.pop().length + 1;
+
+ return {
+ column,
+ line,
+ };
+}
+
+function adjustForLineAndColumn(fullSource, location) {
+ const { column, line } = calcLineAndColumn(fullSource, location.index);
+ return Object.assign({}, location, {
+ line,
+ column,
+ // prepend whitespace for scripts that do not start on the first column
+ // NOTE: `column` is 1-based
+ source: " ".repeat(column - 1) + location.source,
+ });
+}
+
+function parseScriptTags(source, parser) {
+ const scripts = parseScripts(getCandidateScriptLocations(source), parser)
+ .filter(types.isFile)
+ .reduce(
+ (main, script) => {
+ return {
+ statements: main.statements.concat(script.program.body),
+ comments: main.comments.concat(script.comments),
+ tokens: main.tokens.concat(script.tokens),
+ };
+ },
+ {
+ statements: [],
+ comments: [],
+ tokens: [],
+ }
+ );
+
+ const program = types.program(scripts.statements);
+ const file = types.file(program, scripts.comments, scripts.tokens);
+
+ const end = calcLineAndColumn(source, source.length);
+ file.start = program.start = 0;
+ file.end = program.end = source.length;
+ file.loc = program.loc = {
+ start: {
+ line: 1,
+ column: 0,
+ },
+ end,
+ };
+
+ return file;
+}
+
+export default parseScriptTags;
+export { getCandidateScriptLocations, parseScripts, parseScriptTags };
diff --git a/devtools/client/debugger/src/workers/parser/utils/parse-script-tags/index.js b/devtools/client/debugger/src/workers/parser/utils/parse-script-tags/index.js
new file mode 100644
index 0000000000..e85d0cdc53
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/utils/parse-script-tags/index.js
@@ -0,0 +1,60 @@
+/**
+ * https://github.com/ryanjduffy/parse-script-tags
+ *
+ * Copyright (c) by Ryan Duff
+ * Licensed under the MIT License.
+ */
+
+import * as types from "@babel/types";
+import * as babelParser from "@babel/parser";
+
+import {
+ getCandidateScriptLocations,
+ parseScripts as customParseScripts,
+ parseScriptTags as customParseScriptTags,
+} from "./customParse.js";
+
+function parseScript({ source, line }) {
+ // remove empty or only whitespace scripts
+ if (source.length === 0 || /^\s+$/.test(source)) {
+ return null;
+ }
+
+ try {
+ return babelParser.parse(source, {
+ sourceType: "script",
+ startLine: line,
+ });
+ } catch (e) {
+ return null;
+ }
+}
+
+function parseScripts(locations, parser = parseScript) {
+ return customParseScripts(locations, parser);
+}
+
+function extractScriptTags(source) {
+ return parseScripts(getCandidateScriptLocations(source), loc => {
+ const ast = parseScript(loc);
+
+ if (ast) {
+ return loc;
+ }
+
+ return null;
+ }).filter(types.isFile);
+}
+
+function parseScriptTags(source, parser = parseScript) {
+ return customParseScriptTags(source, parser);
+}
+
+export default parseScriptTags;
+export {
+ extractScriptTags,
+ getCandidateScriptLocations,
+ parseScript,
+ parseScripts,
+ parseScriptTags,
+};
diff --git a/devtools/client/debugger/src/workers/parser/utils/parse-script-tags/parseScriptFragment.js b/devtools/client/debugger/src/workers/parser/utils/parse-script-tags/parseScriptFragment.js
new file mode 100644
index 0000000000..11dfe4dc1b
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/utils/parse-script-tags/parseScriptFragment.js
@@ -0,0 +1,155 @@
+/**
+ * https://github.com/ryanjduffy/parse-script-tags
+ *
+ * Copyright (c) by Ryan Duff
+ * Licensed under the MIT License.
+ */
+
+const alphanum = /[a-z0-9\-]/i;
+
+function parseToken(str, start) {
+ let i = start;
+ while (i < str.length && alphanum.test(str.charAt(i++))) {
+ continue;
+ }
+
+ if (i !== start) {
+ return {
+ token: str.substring(start, i - 1),
+ index: i,
+ };
+ }
+
+ return null;
+}
+
+function parseAttributes(str, start) {
+ let i = start;
+ const attributes = {};
+ let attribute = null;
+
+ while (i < str.length) {
+ const c = str.charAt(i);
+
+ if (attribute === null && c == ">") {
+ break;
+ } else if (attribute === null && alphanum.test(c)) {
+ attribute = {
+ name: null,
+ value: true,
+ bool: true,
+ terminator: null,
+ };
+
+ const attributeNameNode = parseToken(str, i);
+ if (attributeNameNode) {
+ attribute.name = attributeNameNode.token;
+ i = attributeNameNode.index - 2;
+ }
+ } else if (attribute !== null) {
+ if (c === "=") {
+ // once we've started an attribute, look for = to indicate
+ // it's a non-boolean attribute
+ attribute.bool = false;
+ if (attribute.value === true) {
+ attribute.value = "";
+ }
+ } else if (
+ !attribute.bool &&
+ attribute.terminator === null &&
+ (c === '"' || c === "'")
+ ) {
+ // once we've determined it's non-boolean, look for a
+ // value terminator (", ')
+ attribute.terminator = c;
+ } else if (attribute.terminator) {
+ if (c === attribute.terminator) {
+ // if we had a terminator and found another, we've
+ // reach the end of the attribute
+ attributes[attribute.name] = attribute.value;
+ attribute = null;
+ } else {
+ // otherwise, append the character to the attribute value
+ attribute.value += c;
+
+ // check for an escaped terminator and push it as well
+ // to avoid terminating prematurely
+ if (c === "\\") {
+ const next = str.charAt(i + 1);
+ if (next === attribute.terminator) {
+ attribute.value += next;
+ i += 1;
+ }
+ }
+ }
+ } else if (!/\s/.test(c)) {
+ // if we've hit a non-space character and aren't processing a value,
+ // we're starting a new attribute so push the attribute and clear the
+ // local variable
+ attributes[attribute.name] = attribute.value;
+ attribute = null;
+
+ // move the cursor back to re-find the start of the attribute
+ i -= 1;
+ }
+ }
+
+ i++;
+ }
+
+ if (i !== start) {
+ return {
+ attributes,
+ index: i,
+ };
+ }
+
+ return null;
+}
+
+function parseFragment(str, start = 0) {
+ let tag = null;
+ let open = false;
+ let attributes = {};
+
+ let i = start;
+ while (i < str.length) {
+ const c = str.charAt(i++);
+
+ if (!open && !tag && c === "<") {
+ // Open Start Tag
+ open = true;
+
+ const tagNode = parseToken(str, i);
+ if (!tagNode) {
+ return null;
+ }
+
+ i = tagNode.index - 1;
+ tag = tagNode.token;
+ } else if (open && c === ">") {
+ // Close Start Tag
+ break;
+ } else if (open) {
+ // Attributes
+ const attributeNode = parseAttributes(str, i - 1);
+
+ if (attributeNode) {
+ i = attributeNode.index;
+ attributes = attributeNode.attributes || attributes;
+ }
+ }
+ }
+
+ if (tag) {
+ return {
+ tag,
+ attributes,
+ };
+ }
+
+ return null;
+}
+
+export default parseFragment;
+export { parseFragment };
diff --git a/devtools/client/debugger/src/workers/parser/utils/simple-path.js b/devtools/client/debugger/src/workers/parser/utils/simple-path.js
new file mode 100644
index 0000000000..b3958dcf42
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/utils/simple-path.js
@@ -0,0 +1,104 @@
+/* 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 default function createSimplePath(ancestors) {
+ if (ancestors.length === 0) {
+ return null;
+ }
+
+ // Slice the array because babel-types traverse may continue mutating
+ // the ancestors array in later traversal logic.
+ return new SimplePath(ancestors.slice());
+}
+
+/**
+ * Mimics @babel/traverse's NodePath API in a simpler fashion that isn't as
+ * heavy, but still allows the ease of passing paths around to process nested
+ * AST structures.
+ */
+class SimplePath {
+ _index;
+ _ancestors;
+ _ancestor;
+
+ _parentPath;
+
+ constructor(ancestors, index = ancestors.length - 1) {
+ if (index < 0 || index >= ancestors.length) {
+ console.error(ancestors);
+ throw new Error("Created invalid path");
+ }
+
+ this._ancestors = ancestors;
+ this._ancestor = ancestors[index];
+ this._index = index;
+ }
+
+ get parentPath() {
+ let path = this._parentPath;
+ if (path === undefined) {
+ if (this._index === 0) {
+ path = null;
+ } else {
+ path = new SimplePath(this._ancestors, this._index - 1);
+ }
+ this._parentPath = path;
+ }
+
+ return path;
+ }
+
+ get parent() {
+ return this._ancestor.node;
+ }
+
+ get node() {
+ const { node, key, index } = this._ancestor;
+
+ if (typeof index === "number") {
+ return node[key][index];
+ }
+
+ return node[key];
+ }
+
+ get key() {
+ return this._ancestor.key;
+ }
+
+ get type() {
+ return this.node.type;
+ }
+
+ get inList() {
+ return typeof this._ancestor.index === "number";
+ }
+
+ get containerIndex() {
+ const { index } = this._ancestor;
+
+ if (typeof index !== "number") {
+ throw new Error("Cannot get index of non-array node");
+ }
+
+ return index;
+ }
+
+ find(predicate) {
+ for (let path = this; path; path = path.parentPath) {
+ if (predicate(path)) {
+ return path;
+ }
+ }
+ return null;
+ }
+
+ findParent(predicate) {
+ if (!this.parentPath) {
+ throw new Error("Cannot use findParent on root path");
+ }
+
+ return this.parentPath.find(predicate);
+ }
+}
diff --git a/devtools/client/debugger/src/workers/parser/utils/tests/ast.spec.js b/devtools/client/debugger/src/workers/parser/utils/tests/ast.spec.js
new file mode 100644
index 0000000000..e8d2964205
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/utils/tests/ast.spec.js
@@ -0,0 +1,41 @@
+/* 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 { getAst } from "../ast";
+import { setSource } from "../../sources";
+import cases from "jest-in-case";
+
+import { makeMockSourceAndContent } from "../../../../utils/test-mockup";
+
+const astKeys = [
+ "type",
+ "start",
+ "end",
+ "loc",
+ "errors",
+ "program",
+ "comments",
+ "tokens",
+];
+
+cases(
+ "ast.getAst",
+ ({ name }) => {
+ const source = makeMockSourceAndContent(undefined, "foo", name, "2");
+ setSource({
+ id: source.id,
+ text: source.content.value || "",
+ contentType: source.content.contentType,
+ isWasm: false,
+ });
+ const ast = getAst("foo");
+ expect(ast && Object.keys(ast)).toEqual(astKeys);
+ },
+ [
+ { name: "text/javascript" },
+ { name: "application/javascript" },
+ { name: "application/x-javascript" },
+ { name: "text/jsx" },
+ ]
+);
diff --git a/devtools/client/debugger/src/workers/parser/worker.js b/devtools/client/debugger/src/workers/parser/worker.js
new file mode 100644
index 0000000000..e0c25632f3
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/worker.js
@@ -0,0 +1,39 @@
+/* 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 {
+ getSymbols,
+ getFunctionSymbols,
+ getClassSymbols,
+ getClosestFunctionName,
+ clearSymbols,
+} from "./getSymbols";
+import { clearASTs } from "./utils/ast";
+import getScopes, { clearScopes } from "./getScopes";
+import { setSource, clearSources } from "./sources";
+import findOutOfScopeLocations from "./findOutOfScopeLocations";
+import findBestMatchExpression from "./findBestMatchExpression";
+import mapExpression from "./mapExpression";
+
+import { workerHandler } from "../../../../shared/worker-utils";
+
+function clearAllHelpersForSources(sourceIds) {
+ clearASTs(sourceIds);
+ clearScopes(sourceIds);
+ clearSources(sourceIds);
+ clearSymbols(sourceIds);
+}
+
+self.onmessage = workerHandler({
+ findOutOfScopeLocations,
+ findBestMatchExpression,
+ getSymbols,
+ getFunctionSymbols,
+ getClassSymbols,
+ getClosestFunctionName,
+ getScopes,
+ clearSources: clearAllHelpersForSources,
+ mapExpression,
+ setSource,
+});
diff --git a/devtools/client/debugger/src/workers/pretty-print/LICENSE.md b/devtools/client/debugger/src/workers/pretty-print/LICENSE.md
new file mode 100644
index 0000000000..cc8e9a752c
--- /dev/null
+++ b/devtools/client/debugger/src/workers/pretty-print/LICENSE.md
@@ -0,0 +1,23 @@
+Copyright (c) 2013, Nick Fitzgerald
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/devtools/client/debugger/src/workers/pretty-print/index.js b/devtools/client/debugger/src/workers/pretty-print/index.js
new file mode 100644
index 0000000000..4b151b39e4
--- /dev/null
+++ b/devtools/client/debugger/src/workers/pretty-print/index.js
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+import { WorkerDispatcher } from "devtools/client/shared/worker-utils";
+
+const WORKER_URL =
+ "resource://devtools/client/debugger/dist/pretty-print-worker.js";
+
+export class PrettyPrintDispatcher extends WorkerDispatcher {
+ constructor(jestUrl) {
+ super(jestUrl || WORKER_URL);
+ }
+
+ #prettyPrintTask = this.task("prettyPrint");
+ #prettyPrintInlineScriptTask = this.task("prettyPrintInlineScript");
+ #getSourceMapForTask = this.task("getSourceMapForTask");
+
+ prettyPrint(options) {
+ return this.#prettyPrintTask(options);
+ }
+
+ prettyPrintInlineScript(options) {
+ return this.#prettyPrintInlineScriptTask(options);
+ }
+
+ getSourceMap(taskId) {
+ return this.#getSourceMapForTask(taskId);
+ }
+}
diff --git a/devtools/client/debugger/src/workers/pretty-print/moz.build b/devtools/client/debugger/src/workers/pretty-print/moz.build
new file mode 100644
index 0000000000..b7223ac81a
--- /dev/null
+++ b/devtools/client/debugger/src/workers/pretty-print/moz.build
@@ -0,0 +1,10 @@
+# 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(
+ "index.js",
+)
diff --git a/devtools/client/debugger/src/workers/pretty-print/pretty-fast.js b/devtools/client/debugger/src/workers/pretty-print/pretty-fast.js
new file mode 100644
index 0000000000..44b07f4eda
--- /dev/null
+++ b/devtools/client/debugger/src/workers/pretty-print/pretty-fast.js
@@ -0,0 +1,1178 @@
+/* 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/>. */
+
+/* eslint-disable complexity */
+
+var acorn = require("acorn");
+var sourceMap = require("source-map");
+const NEWLINE_CODE = 10;
+
+export function prettyFast(input, options) {
+ return new PrettyFast(options).getPrettifiedCodeAndSourceMap(input);
+}
+
+// If any of these tokens are seen before a "[" token, we know that "[" token
+// is the start of an array literal, rather than a property access.
+//
+// The only exception is "}", which would need to be disambiguated by
+// parsing. The majority of the time, an open bracket following a closing
+// curly is going to be an array literal, so we brush the complication under
+// the rug, and handle the ambiguity by always assuming that it will be an
+// array literal.
+const PRE_ARRAY_LITERAL_TOKENS = new Set([
+ "typeof",
+ "void",
+ "delete",
+ "case",
+ "do",
+ "=",
+ "in",
+ "of",
+ "...",
+ "{",
+ "*",
+ "/",
+ "%",
+ "else",
+ ";",
+ "++",
+ "--",
+ "+",
+ "-",
+ "~",
+ "!",
+ ":",
+ "?",
+ ">>",
+ ">>>",
+ "<<",
+ "||",
+ "&&",
+ "<",
+ ">",
+ "<=",
+ ">=",
+ "instanceof",
+ "&",
+ "^",
+ "|",
+ "==",
+ "!=",
+ "===",
+ "!==",
+ ",",
+ "}",
+]);
+
+// If any of these tokens are seen before a "{" token, we know that "{" token
+// is the start of an object literal, rather than the start of a block.
+const PRE_OBJECT_LITERAL_TOKENS = new Set([
+ "typeof",
+ "void",
+ "delete",
+ "=",
+ "in",
+ "of",
+ "...",
+ "*",
+ "/",
+ "%",
+ "++",
+ "--",
+ "+",
+ "-",
+ "~",
+ "!",
+ ">>",
+ ">>>",
+ "<<",
+ "<",
+ ">",
+ "<=",
+ ">=",
+ "instanceof",
+ "&",
+ "^",
+ "|",
+ "==",
+ "!=",
+ "===",
+ "!==",
+]);
+
+class PrettyFast {
+ /**
+ * @param {Object} options: Provides configurability of the pretty printing.
+ * @param {String} options.url: The URL string of the ugly JS code.
+ * @param {String} options.indent: The string to indent code by.
+ * @param {SourceMapGenerator} options.sourceMapGenerator: An optional sourceMapGenerator
+ * the mappings will be added to.
+ * @param {Boolean} options.prefixWithNewLine: When true, the pretty printed code will start
+ * with a line break
+ * @param {Integer} options.originalStartLine: The line the passed script starts at (1-based).
+ * This is used for inline scripts where we need to account for the lines
+ * before the script tag
+ * @param {Integer} options.originalStartColumn: The column the passed script starts at (1-based).
+ * This is used for inline scripts where we need to account for the position
+ * of the script tag within the line.
+ * @param {Integer} options.generatedStartLine: The line where the pretty printed script
+ * will start at (1-based). This is used for pretty printing HTML file,
+ * where we might have handle previous inline scripts that impact the
+ * position of this script.
+ */
+ constructor(options = {}) {
+ // The level of indents deep we are.
+ this.#indentLevel = 0;
+ this.#indentChar = options.indent;
+
+ // We will handle mappings between ugly and pretty printed code in this SourceMapGenerator.
+ this.#sourceMapGenerator =
+ options.sourceMapGenerator ||
+ new sourceMap.SourceMapGenerator({
+ file: options.url,
+ });
+
+ this.#file = options.url;
+ this.#hasOriginalStartLine = "originalStartLine" in options;
+ this.#hasOriginalStartColumn = "originalStartColumn" in options;
+ this.#hasGeneratedStartLine = "generatedStartLine" in options;
+ this.#originalStartLine = options.originalStartLine;
+ this.#originalStartColumn = options.originalStartColumn;
+ this.#generatedStartLine = options.generatedStartLine;
+ this.#prefixWithNewLine = options.prefixWithNewLine;
+ }
+
+ /* options */
+ #indentChar;
+ #indentLevel;
+ #file;
+ #hasOriginalStartLine;
+ #hasOriginalStartColumn;
+ #hasGeneratedStartLine;
+ #originalStartLine;
+ #originalStartColumn;
+ #prefixWithNewLine;
+ #generatedStartLine;
+ #sourceMapGenerator;
+
+ /* internals */
+
+ // Whether or not we added a newline on after we added the previous token.
+ #addedNewline = false;
+ // Whether or not we added a space after we added the previous token.
+ #addedSpace = false;
+ #currentCode = "";
+ #currentLine = 1;
+ #currentColumn = 0;
+ // The tokens parsed by acorn.
+ #tokenQueue;
+ // The index of the current token in this.#tokenQueue.
+ #currentTokenIndex;
+ // The previous token we added to the pretty printed code.
+ #previousToken;
+ // Stack of token types/keywords that can affect whether we want to add a
+ // newline or a space. We can make that decision based on what token type is
+ // on the top of the stack. For example, a comma in a parameter list should
+ // be followed by a space, while a comma in an object literal should be
+ // followed by a newline.
+ //
+ // Strings that go on the stack:
+ //
+ // - "{"
+ // - "{\n"
+ // - "("
+ // - "(\n"
+ // - "["
+ // - "[\n"
+ // - "do"
+ // - "?"
+ // - "switch"
+ // - "case"
+ // - "default"
+ //
+ // The difference between "[" and "[\n" (as well as "{" and "{\n", and "(" and "(\n")
+ // is that "\n" is used when we are treating (curly) brackets/parens as line delimiters
+ // and should increment and decrement the indent level when we find them.
+ // "[" can represent either a property access (e.g. `x["hi"]`), or an empty array literal
+ // "{" only represents an empty object literals
+ // "(" can represent lots of different things (wrapping expression, if/loop condition, function call, …)
+ #stack = [];
+
+ /**
+ * @param {String} input: The ugly JS code we want to pretty print.
+ * @returns {Object}
+ * An object with the following properties:
+ * - code: The pretty printed code string.
+ * - map: A SourceMapGenerator instance.
+ */
+ getPrettifiedCodeAndSourceMap(input) {
+ // Add the initial new line if needed
+ if (this.#prefixWithNewLine) {
+ this.#write("\n");
+ }
+
+ // Pass through acorn's tokenizer and append tokens and comments into a
+ // single queue to process. For example, the source file:
+ //
+ // foo
+ // // a
+ // // b
+ // bar
+ //
+ // After this process, tokenQueue has the following token stream:
+ //
+ // [ foo, '// a', '// b', bar]
+ this.#tokenQueue = this.#getTokens(input);
+
+ for (let i = 0, len = this.#tokenQueue.length; i < len; i++) {
+ this.#currentTokenIndex = i;
+ const token = this.#tokenQueue[i];
+ const nextToken = this.#tokenQueue[i + 1];
+ this.#handleToken(token, nextToken);
+
+ // Acorn's tokenizer re-uses tokens, so we have to copy the previous token on
+ // every iteration. We follow acorn's lead here, and reuse the previousToken
+ // object the same way that acorn reuses the token object. This allows us
+ // to avoid allocations and minimize GC pauses.
+ if (!this.#previousToken) {
+ this.#previousToken = { loc: { start: {}, end: {} } };
+ }
+ this.#previousToken.start = token.start;
+ this.#previousToken.end = token.end;
+ this.#previousToken.loc.start.line = token.loc.start.line;
+ this.#previousToken.loc.start.column = token.loc.start.column;
+ this.#previousToken.loc.end.line = token.loc.end.line;
+ this.#previousToken.loc.end.column = token.loc.end.column;
+ this.#previousToken.type = token.type;
+ this.#previousToken.value = token.value;
+ }
+
+ return { code: this.#currentCode, map: this.#sourceMapGenerator };
+ }
+
+ /**
+ * Write a pretty printed string to the prettified string and for tokens, add their
+ * mapping to the SourceMapGenerator.
+ *
+ * @param String str
+ * The string to be added to the result.
+ * @param Number line
+ * The line number the string came from in the ugly source.
+ * @param Number column
+ * The column number the string came from in the ugly source.
+ * @param Boolean isToken
+ * Set to true when writing tokens, so we can differentiate them from the
+ * whitespace we add.
+ */
+ #write(str, line, column, isToken) {
+ this.#currentCode += str;
+ if (isToken) {
+ this.#sourceMapGenerator.addMapping({
+ source: this.#file,
+ // We need to swap original and generated locations, as the prettified text should
+ // be seen by the sourcemap service as the "original" one.
+ generated: {
+ // originalStartLine is 1-based, and here we just want to offset by a number of
+ // lines, so we need to decrement it
+ line: this.#hasOriginalStartLine
+ ? line + (this.#originalStartLine - 1)
+ : line,
+ // We only need to adjust the column number if we're looking at the first line, to
+ // account for the html text before the opening <script> tag.
+ column:
+ line == 1 && this.#hasOriginalStartColumn
+ ? column + this.#originalStartColumn
+ : column,
+ },
+ original: {
+ // generatedStartLine is 1-based, and here we just want to offset by a number of
+ // lines, so we need to decrement it.
+ line: this.#hasGeneratedStartLine
+ ? this.#currentLine + (this.#generatedStartLine - 1)
+ : this.#currentLine,
+ column: this.#currentColumn,
+ },
+ name: null,
+ });
+ }
+
+ for (let idx = 0, length = str.length; idx < length; idx++) {
+ if (str.charCodeAt(idx) === NEWLINE_CODE) {
+ this.#currentLine++;
+ this.#currentColumn = 0;
+ } else {
+ this.#currentColumn++;
+ }
+ }
+ }
+
+ /**
+ * Add the given token to the pretty printed results.
+ *
+ * @param Object token
+ * The token to add.
+ */
+ #writeToken(token) {
+ if (token.type.label == "string") {
+ this.#write(
+ `'${sanitize(token.value)}'`,
+ token.loc.start.line,
+ token.loc.start.column,
+ true
+ );
+ } else if (token.type.label == "regexp") {
+ this.#write(
+ String(token.value.value),
+ token.loc.start.line,
+ token.loc.start.column,
+ true
+ );
+ } else {
+ let value;
+ if (token.value != null) {
+ value = token.value;
+ if (token.type.label === "privateId") {
+ value = `#${value}`;
+ }
+ } else {
+ value = token.type.label;
+ }
+ this.#write(
+ String(value),
+ token.loc.start.line,
+ token.loc.start.column,
+ true
+ );
+ }
+ }
+
+ /**
+ * Returns the tokens computed with acorn.
+ *
+ * @param String input
+ * The JS code we want the tokens of.
+ * @returns Array<Object>
+ */
+ #getTokens(input) {
+ const tokens = [];
+
+ const res = acorn.tokenizer(input, {
+ locations: true,
+ ecmaVersion: "latest",
+ onComment(block, text, start, end, startLoc, endLoc) {
+ tokens.push({
+ type: {},
+ comment: true,
+ block,
+ text,
+ loc: { start: startLoc, end: endLoc },
+ });
+ },
+ });
+
+ for (;;) {
+ const token = res.getToken();
+ tokens.push(token);
+ if (token.type.label == "eof") {
+ break;
+ }
+ }
+
+ return tokens;
+ }
+
+ /**
+ * Add the required whitespace before this token, whether that is a single
+ * space, newline, and/or the indent on fresh lines.
+ *
+ * @param Object token
+ * The token we are currently handling.
+ * @param {Object|undefined} nextToken
+ * The next token, might not exist if we're on the last token
+ */
+ #handleToken(token, nextToken) {
+ if (token.comment) {
+ let commentIndentLevel = this.#indentLevel;
+ if (this.#previousToken?.loc?.end?.line == token.loc.start.line) {
+ commentIndentLevel = 0;
+ this.#write(" ");
+ }
+ this.#addComment(
+ commentIndentLevel,
+ token.block,
+ token.text,
+ token.loc.start.line,
+ nextToken
+ );
+ return;
+ }
+
+ // Shorthand for token.type.keyword, so we don't have to repeatedly access
+ // properties.
+ const ttk = token.type.keyword;
+
+ if (ttk && this.#previousToken?.type?.label == ".") {
+ token.type = acorn.tokTypes.name;
+ }
+
+ // Shorthand for token.type.label, so we don't have to repeatedly access
+ // properties.
+ const ttl = token.type.label;
+
+ if (ttl == "eof") {
+ if (!this.#addedNewline) {
+ this.#write("\n");
+ }
+ return;
+ }
+
+ if (belongsOnStack(token)) {
+ let stackEntry;
+
+ if (isArrayLiteral(token, this.#previousToken)) {
+ // Don't add new lines for empty array literals
+ stackEntry = nextToken?.type?.label === "]" ? "[" : "[\n";
+ } else if (isObjectLiteral(token, this.#previousToken)) {
+ // Don't add new lines for empty object literals
+ stackEntry = nextToken?.type?.label === "}" ? "{" : "{\n";
+ } else if (
+ isRoundBracketStartingLongParenthesis(
+ token,
+ this.#tokenQueue,
+ this.#currentTokenIndex
+ )
+ ) {
+ stackEntry = "(\n";
+ } else if (ttl == "{") {
+ // We need to add a line break for "{" which are not empty object literals
+ stackEntry = "{\n";
+ } else {
+ stackEntry = ttl || ttk;
+ }
+
+ this.#stack.push(stackEntry);
+ }
+
+ this.#maybeDecrementIndent(token);
+ this.#prependWhiteSpace(token);
+ this.#writeToken(token);
+ this.#addedSpace = false;
+
+ // If the next token is going to be a comment starting on the same line,
+ // then no need to add a new line here
+ if (
+ !nextToken ||
+ !nextToken.comment ||
+ token.loc.end.line != nextToken.loc.start.line
+ ) {
+ this.#maybeAppendNewline(token);
+ }
+
+ this.#maybePopStack(token);
+ this.#maybeIncrementIndent(token);
+ }
+
+ /**
+ * Returns true if the given token should cause us to pop the stack.
+ */
+ #maybePopStack(token) {
+ const ttl = token.type.label;
+ const ttk = token.type.keyword;
+ const top = this.#stack.at(-1);
+
+ if (
+ ttl == "]" ||
+ ttl == ")" ||
+ ttl == "}" ||
+ (ttl == ":" && (top == "case" || top == "default" || top == "?")) ||
+ (ttk == "while" && top == "do")
+ ) {
+ this.#stack.pop();
+ if (ttl == "}" && this.#stack.at(-1) == "switch") {
+ this.#stack.pop();
+ }
+ }
+ }
+
+ #maybeIncrementIndent(token) {
+ if (
+ // Don't increment indent for empty object literals
+ (token.type.label == "{" && this.#stack.at(-1) === "{\n") ||
+ // Don't increment indent for empty array literals
+ (token.type.label == "[" && this.#stack.at(-1) === "[\n") ||
+ token.type.keyword == "switch" ||
+ (token.type.label == "(" && this.#stack.at(-1) === "(\n")
+ ) {
+ this.#indentLevel++;
+ }
+ }
+
+ #shouldDecrementIndent(token) {
+ const top = this.#stack.at(-1);
+ const ttl = token.type.label;
+ return (
+ (ttl == "}" && top == "{\n") ||
+ (ttl == "]" && top == "[\n") ||
+ (ttl == ")" && top == "(\n")
+ );
+ }
+
+ #maybeDecrementIndent(token) {
+ if (!this.#shouldDecrementIndent(token)) {
+ return;
+ }
+
+ const ttl = token.type.label;
+ this.#indentLevel--;
+ if (ttl == "}" && this.#stack.at(-2) == "switch") {
+ this.#indentLevel--;
+ }
+ }
+
+ /**
+ * Add a comment to the pretty printed code.
+ *
+ * @param Number indentLevel
+ * The number of indents deep we are (might be different from this.#indentLevel).
+ * @param Boolean block
+ * True if the comment is a multiline block style comment.
+ * @param String text
+ * The text of the comment.
+ * @param Number line
+ * The line number to comment appeared on.
+ * @param Object nextToken
+ * The next token if any.
+ */
+ #addComment(indentLevel, block, text, line, nextToken) {
+ const indentString = this.#indentChar.repeat(indentLevel);
+ const needNewLineAfter =
+ !block || !(nextToken && nextToken.loc.start.line == line);
+
+ if (block) {
+ const commentLinesText = text
+ .split(new RegExp(`/\n${indentString}/`, "g"))
+ .join(`\n${indentString}`);
+
+ this.#write(
+ `${indentString}/*${commentLinesText}*/${needNewLineAfter ? "\n" : " "}`
+ );
+ } else {
+ this.#write(`${indentString}//${text}\n`);
+ }
+
+ this.#addedNewline = needNewLineAfter;
+ this.#addedSpace = !needNewLineAfter;
+ }
+
+ /**
+ * Add the required whitespace before this token, whether that is a single
+ * space, newline, and/or the indent on fresh lines.
+ *
+ * @param Object token
+ * The token we are about to add to the pretty printed code.
+ */
+ #prependWhiteSpace(token) {
+ const ttk = token.type.keyword;
+ const ttl = token.type.label;
+ let newlineAdded = this.#addedNewline;
+ let spaceAdded = this.#addedSpace;
+ const ltt = this.#previousToken?.type?.label;
+
+ // Handle whitespace and newlines after "}" here instead of in
+ // `isLineDelimiter` because it is only a line delimiter some of the
+ // time. For example, we don't want to put "else if" on a new line after
+ // the first if's block.
+ if (this.#previousToken && ltt == "}") {
+ if (
+ (ttk == "while" && this.#stack.at(-1) == "do") ||
+ needsSpaceBeforeClosingCurlyBracket(ttk)
+ ) {
+ this.#write(" ");
+ spaceAdded = true;
+ } else if (needsLineBreakBeforeClosingCurlyBracket(ttl)) {
+ this.#write("\n");
+ newlineAdded = true;
+ }
+ }
+
+ if (
+ (ttl == ":" && this.#stack.at(-1) == "?") ||
+ (ttl == "}" && this.#stack.at(-1) == "${")
+ ) {
+ this.#write(" ");
+ spaceAdded = true;
+ }
+
+ if (this.#previousToken && ltt != "}" && ltt != "." && ttk == "else") {
+ this.#write(" ");
+ spaceAdded = true;
+ }
+
+ const ensureNewline = () => {
+ if (!newlineAdded) {
+ this.#write("\n");
+ newlineAdded = true;
+ }
+ };
+
+ if (isASI(token, this.#previousToken)) {
+ ensureNewline();
+ }
+
+ if (this.#shouldDecrementIndent(token)) {
+ ensureNewline();
+ }
+
+ if (newlineAdded) {
+ let indentLevel = this.#indentLevel;
+ if (ttk == "case" || ttk == "default") {
+ indentLevel--;
+ }
+ this.#write(this.#indentChar.repeat(indentLevel));
+ } else if (!spaceAdded && needsSpaceAfter(token, this.#previousToken)) {
+ this.#write(" ");
+ spaceAdded = true;
+ }
+ }
+
+ /**
+ * Append the necessary whitespace to the result after we have added the given
+ * token.
+ *
+ * @param Object token
+ * The token that was just added to the result.
+ */
+ #maybeAppendNewline(token) {
+ if (!isLineDelimiter(token, this.#stack)) {
+ this.#addedNewline = false;
+ return;
+ }
+
+ this.#write("\n");
+ this.#addedNewline = true;
+ }
+}
+
+/**
+ * Determines if we think that the given token starts an array literal.
+ *
+ * @param Object token
+ * The token we want to determine if it is an array literal.
+ * @param Object previousToken
+ * The previous token we added to the pretty printed results.
+ *
+ * @returns Boolean
+ * True if we believe it is an array literal, false otherwise.
+ */
+function isArrayLiteral(token, previousToken) {
+ if (token.type.label != "[") {
+ return false;
+ }
+ if (!previousToken) {
+ return true;
+ }
+ if (previousToken.type.isAssign) {
+ return true;
+ }
+
+ return PRE_ARRAY_LITERAL_TOKENS.has(
+ previousToken.type.keyword ||
+ // Some tokens ('of', 'yield', …) have a `token.type.keyword` of 'name' and their
+ // actual value in `token.value`
+ (previousToken.type.label == "name"
+ ? previousToken.value
+ : previousToken.type.label)
+ );
+}
+
+/**
+ * Determines if we think that the given token starts an object literal.
+ *
+ * @param Object token
+ * The token we want to determine if it is an object literal.
+ * @param Object previousToken
+ * The previous token we added to the pretty printed results.
+ *
+ * @returns Boolean
+ * True if we believe it is an object literal, false otherwise.
+ */
+function isObjectLiteral(token, previousToken) {
+ if (token.type.label != "{") {
+ return false;
+ }
+ if (!previousToken) {
+ return false;
+ }
+ if (previousToken.type.isAssign) {
+ return true;
+ }
+ return PRE_OBJECT_LITERAL_TOKENS.has(
+ previousToken.type.keyword || previousToken.type.label
+ );
+}
+
+/**
+ * Determines if we think that the given token starts a long parenthesis
+ *
+ * @param {Object} token
+ * The token we want to determine if it is the beginning of a long paren.
+ * @param {Array<Object>} tokenQueue
+ * The whole list of tokens parsed by acorn
+ * @param {Integer} currentTokenIndex
+ * The index of `token` in `tokenQueue`
+ * @returns
+ */
+function isRoundBracketStartingLongParenthesis(
+ token,
+ tokenQueue,
+ currentTokenIndex
+) {
+ if (token.type.label !== "(") {
+ return false;
+ }
+
+ // If we're just wrapping an object, we'll have a new line right after
+ if (tokenQueue[currentTokenIndex + 1].type.label == "{") {
+ return false;
+ }
+
+ // We're going to iterate through the following tokens until :
+ // - we find the closing parent
+ // - or we reached the maximum character we think should be in parenthesis
+ const longParentContentLength = 60;
+
+ // Keep track of other parens so we know when we get the closing one for `token`
+ let parenCount = 0;
+ let parenContentLength = 0;
+ for (let i = currentTokenIndex + 1, len = tokenQueue.length; i < len; i++) {
+ const currToken = tokenQueue[i];
+ const ttl = currToken.type.label;
+
+ if (ttl == "(") {
+ parenCount++;
+ } else if (ttl == ")") {
+ if (parenCount == 0) {
+ // Matching closing paren, if we got here, we didn't reach the length limit,
+ // as we return when parenContentLength is greater than the limit.
+ return false;
+ }
+ parenCount--;
+ }
+
+ // Aside block comments, all tokens start and end location are on the same line, so
+ // we can use `start` and `end` to deduce the token length.
+ const tokenLength = currToken.comment
+ ? currToken.text.length
+ : currToken.end - currToken.start;
+ parenContentLength += tokenLength;
+
+ // If we didn't find the matching closing paren yet and the characters from the
+ // tokens we evaluated so far are longer than the limit, so consider the token
+ // a long paren.
+ if (parenContentLength > longParentContentLength) {
+ return true;
+ }
+ }
+
+ // if we get to here, we didn't found a closing paren, which shouldn't happen
+ // (scripts with syntax error are not displayed in the debugger), but just to
+ // be safe, return false.
+ return false;
+}
+
+// If any of these tokens are followed by a token on a new line, we know that
+// ASI cannot happen.
+const PREVENT_ASI_AFTER_TOKENS = new Set([
+ // Binary operators
+ "*",
+ "/",
+ "%",
+ "+",
+ "-",
+ "<<",
+ ">>",
+ ">>>",
+ "<",
+ ">",
+ "<=",
+ ">=",
+ "instanceof",
+ "in",
+ "==",
+ "!=",
+ "===",
+ "!==",
+ "&",
+ "^",
+ "|",
+ "&&",
+ "||",
+ ",",
+ ".",
+ "=",
+ "*=",
+ "/=",
+ "%=",
+ "+=",
+ "-=",
+ "<<=",
+ ">>=",
+ ">>>=",
+ "&=",
+ "^=",
+ "|=",
+ // Unary operators
+ "delete",
+ "void",
+ "typeof",
+ "~",
+ "!",
+ "new",
+ // Function calls and grouped expressions
+ "(",
+]);
+
+// If any of these tokens are on a line after the token before it, we know
+// that ASI cannot happen.
+const PREVENT_ASI_BEFORE_TOKENS = new Set([
+ // Binary operators
+ "*",
+ "/",
+ "%",
+ "<<",
+ ">>",
+ ">>>",
+ "<",
+ ">",
+ "<=",
+ ">=",
+ "instanceof",
+ "in",
+ "==",
+ "!=",
+ "===",
+ "!==",
+ "&",
+ "^",
+ "|",
+ "&&",
+ "||",
+ ",",
+ ".",
+ "=",
+ "*=",
+ "/=",
+ "%=",
+ "+=",
+ "-=",
+ "<<=",
+ ">>=",
+ ">>>=",
+ "&=",
+ "^=",
+ "|=",
+ // Function calls
+ "(",
+]);
+
+/**
+ * Determine if a token can look like an identifier. More precisely,
+ * this determines if the token may end or start with a character from
+ * [A-Za-z0-9_].
+ *
+ * @param Object token
+ * The token we are looking at.
+ *
+ * @returns Boolean
+ * True if identifier-like.
+ */
+function isIdentifierLike(token) {
+ const ttl = token.type.label;
+ return (
+ ttl == "name" || ttl == "num" || ttl == "privateId" || !!token.type.keyword
+ );
+}
+
+/**
+ * Determines if Automatic Semicolon Insertion (ASI) occurs between these
+ * tokens.
+ *
+ * @param Object token
+ * The current token.
+ * @param Object previousToken
+ * The previous token we added to the pretty printed results.
+ *
+ * @returns Boolean
+ * True if we believe ASI occurs.
+ */
+function isASI(token, previousToken) {
+ if (!previousToken) {
+ return false;
+ }
+ if (token.loc.start.line === previousToken.loc.start.line) {
+ return false;
+ }
+ if (
+ previousToken.type.keyword == "return" ||
+ previousToken.type.keyword == "yield" ||
+ (previousToken.type.label == "name" && previousToken.value == "yield")
+ ) {
+ return true;
+ }
+ if (
+ PREVENT_ASI_AFTER_TOKENS.has(
+ previousToken.type.label || previousToken.type.keyword
+ )
+ ) {
+ return false;
+ }
+ if (PREVENT_ASI_BEFORE_TOKENS.has(token.type.label || token.type.keyword)) {
+ return false;
+ }
+ return true;
+}
+
+/**
+ * Determine if we should add a newline after the given token.
+ *
+ * @param Object token
+ * The token we are looking at.
+ * @param Array stack
+ * The stack of open parens/curlies/brackets/etc.
+ *
+ * @returns Boolean
+ * True if we should add a newline.
+ */
+function isLineDelimiter(token, stack) {
+ const ttl = token.type.label;
+ const top = stack.at(-1);
+ return (
+ (ttl == ";" && top != "(") ||
+ // Don't add a new line for empty object literals
+ (ttl == "{" && top == "{\n") ||
+ // Don't add a new line for empty array literals
+ (ttl == "[" && top == "[\n") ||
+ ((ttl == "," || ttl == "||" || ttl == "&&") && top != "(") ||
+ (ttl == ":" && (top == "case" || top == "default")) ||
+ (ttl == "(" && top == "(\n")
+ );
+}
+
+/**
+ * Determines if we need to add a space after the token we are about to add.
+ *
+ * @param Object token
+ * The token we are about to add to the pretty printed code.
+ * @param Object [previousToken]
+ * Optional previous token added to the pretty printed code.
+ */
+function needsSpaceAfter(token, previousToken) {
+ if (previousToken && needsSpaceBetweenTokens(token, previousToken)) {
+ return true;
+ }
+
+ if (token.type.isAssign) {
+ return true;
+ }
+ if (token.type.binop != null && previousToken) {
+ return true;
+ }
+ if (token.type.label == "?") {
+ return true;
+ }
+ if (token.type.label == "=>") {
+ return true;
+ }
+
+ return false;
+}
+
+function needsSpaceBeforePreviousToken(previousToken) {
+ if (previousToken.type.isLoop) {
+ return true;
+ }
+ if (previousToken.type.isAssign) {
+ return true;
+ }
+ if (previousToken.type.binop != null) {
+ return true;
+ }
+ if (previousToken.value == "of") {
+ return true;
+ }
+
+ const previousTokenTypeLabel = previousToken.type.label;
+ if (previousTokenTypeLabel == "?") {
+ return true;
+ }
+ if (previousTokenTypeLabel == ":") {
+ return true;
+ }
+ if (previousTokenTypeLabel == ",") {
+ return true;
+ }
+ if (previousTokenTypeLabel == ";") {
+ return true;
+ }
+ if (previousTokenTypeLabel == "${") {
+ return true;
+ }
+ if (previousTokenTypeLabel == "=>") {
+ return true;
+ }
+ return false;
+}
+
+function isBreakContinueOrReturnStatement(previousTokenKeyword) {
+ return (
+ previousTokenKeyword == "break" ||
+ previousTokenKeyword == "continue" ||
+ previousTokenKeyword == "return"
+ );
+}
+
+function needsSpaceBeforePreviousTokenKeywordAfterNotDot(previousTokenKeyword) {
+ return (
+ previousTokenKeyword != "debugger" &&
+ previousTokenKeyword != "null" &&
+ previousTokenKeyword != "true" &&
+ previousTokenKeyword != "false" &&
+ previousTokenKeyword != "this" &&
+ previousTokenKeyword != "default"
+ );
+}
+
+function needsSpaceBeforeClosingParen(tokenTypeLabel) {
+ return (
+ tokenTypeLabel != ")" &&
+ tokenTypeLabel != "]" &&
+ tokenTypeLabel != ";" &&
+ tokenTypeLabel != "," &&
+ tokenTypeLabel != "."
+ );
+}
+
+/**
+ * Determines if we need to add a space between the previous token we added and
+ * the token we are about to add.
+ *
+ * @param Object token
+ * The token we are about to add to the pretty printed code.
+ * @param Object previousToken
+ * The previous token added to the pretty printed code.
+ */
+function needsSpaceBetweenTokens(token, previousToken) {
+ if (needsSpaceBeforePreviousToken(previousToken)) {
+ return true;
+ }
+
+ const ltt = previousToken.type.label;
+ if (ltt == "num" && token.type.label == ".") {
+ return true;
+ }
+
+ const ltk = previousToken.type.keyword;
+ const ttl = token.type.label;
+ if (ltk != null && ttl != ".") {
+ if (isBreakContinueOrReturnStatement(ltk)) {
+ return ttl != ";";
+ }
+ if (needsSpaceBeforePreviousTokenKeywordAfterNotDot(ltk)) {
+ return true;
+ }
+ }
+
+ if (ltt == ")" && needsSpaceBeforeClosingParen(ttl)) {
+ return true;
+ }
+
+ if (isIdentifierLike(token) && isIdentifierLike(previousToken)) {
+ // We must emit a space to avoid merging the tokens.
+ return true;
+ }
+
+ if (token.type.label == "{" && previousToken.type.label == "name") {
+ return true;
+ }
+
+ return false;
+}
+
+function needsSpaceBeforeClosingCurlyBracket(tokenTypeKeyword) {
+ return (
+ tokenTypeKeyword == "else" ||
+ tokenTypeKeyword == "catch" ||
+ tokenTypeKeyword == "finally"
+ );
+}
+
+function needsLineBreakBeforeClosingCurlyBracket(tokenTypeLabel) {
+ return (
+ tokenTypeLabel != "(" &&
+ tokenTypeLabel != ";" &&
+ tokenTypeLabel != "," &&
+ tokenTypeLabel != ")" &&
+ tokenTypeLabel != "." &&
+ tokenTypeLabel != "template" &&
+ tokenTypeLabel != "`"
+ );
+}
+
+const escapeCharacters = {
+ // Backslash
+ "\\": "\\\\",
+ // Newlines
+ "\n": "\\n",
+ // Carriage return
+ "\r": "\\r",
+ // Tab
+ "\t": "\\t",
+ // Vertical tab
+ "\v": "\\v",
+ // Form feed
+ "\f": "\\f",
+ // Null character
+ "\0": "\\x00",
+ // Line separator
+ "\u2028": "\\u2028",
+ // Paragraph separator
+ "\u2029": "\\u2029",
+ // Single quotes
+ "'": "\\'",
+};
+
+// eslint-disable-next-line prefer-template
+const regExpString = "(" + Object.values(escapeCharacters).join("|") + ")";
+const escapeCharactersRegExp = new RegExp(regExpString, "g");
+
+function sanitizerReplaceFunc(_, c) {
+ return escapeCharacters[c];
+}
+
+/**
+ * Make sure that we output the escaped character combination inside string
+ * literals instead of various problematic characters.
+ */
+function sanitize(str) {
+ return str.replace(escapeCharactersRegExp, sanitizerReplaceFunc);
+}
+
+/**
+ * Returns true if the given token type belongs on the stack.
+ */
+function belongsOnStack(token) {
+ const ttl = token.type.label;
+ const ttk = token.type.keyword;
+ return (
+ ttl == "{" ||
+ ttl == "(" ||
+ ttl == "[" ||
+ ttl == "?" ||
+ ttl == "${" ||
+ ttk == "do" ||
+ ttk == "switch" ||
+ ttk == "case" ||
+ ttk == "default"
+ );
+}
diff --git a/devtools/client/debugger/src/workers/pretty-print/tests/__snapshots__/prettyFast.spec.js.snap b/devtools/client/debugger/src/workers/pretty-print/tests/__snapshots__/prettyFast.spec.js.snap
new file mode 100644
index 0000000000..498bee267e
--- /dev/null
+++ b/devtools/client/debugger/src/workers/pretty-print/tests/__snapshots__/prettyFast.spec.js.snap
@@ -0,0 +1,1974 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ASI return 1`] = `
+"function f() {
+ return
+ {
+ }
+}
+"
+`;
+
+exports[`ASI return 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 9) -> (1, 9)",
+ "(1, 10) -> (1, 10)",
+ "(1, 11) -> (1, 11)",
+ "(1, 13) -> (1, 13)",
+ "(2, 2) -> (2, 13)",
+ "(3, 2) -> (3, 13)",
+ "(4, 2) -> (3, 14)",
+ "(5, 0) -> (4, 11)",
+]
+`;
+
+exports[`Arrays 1`] = `
+"var a = [
+ 1,
+ 2,
+ 3
+];
+"
+`;
+
+exports[`Arrays 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 6) -> (1, 5)",
+ "(1, 8) -> (1, 6)",
+ "(2, 2) -> (1, 7)",
+ "(2, 3) -> (1, 8)",
+ "(3, 2) -> (1, 9)",
+ "(3, 3) -> (1, 10)",
+ "(4, 2) -> (1, 11)",
+ "(5, 0) -> (1, 12)",
+ "(5, 1) -> (1, 13)",
+]
+`;
+
+exports[`Arrays and spread operator 1`] = `
+"var a = [
+ 1,
+ ...[
+ 2,
+ 3
+ ],
+ ...[],
+ 4
+];
+"
+`;
+
+exports[`Arrays and spread operator 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 6) -> (1, 5)",
+ "(1, 8) -> (1, 6)",
+ "(2, 2) -> (1, 7)",
+ "(2, 3) -> (1, 8)",
+ "(3, 2) -> (1, 9)",
+ "(3, 5) -> (1, 12)",
+ "(4, 4) -> (1, 13)",
+ "(4, 5) -> (1, 14)",
+ "(5, 4) -> (1, 15)",
+ "(6, 2) -> (1, 16)",
+ "(6, 3) -> (1, 17)",
+ "(7, 2) -> (1, 18)",
+ "(7, 5) -> (1, 21)",
+ "(7, 6) -> (1, 22)",
+ "(7, 7) -> (1, 23)",
+ "(8, 2) -> (1, 25)",
+ "(9, 0) -> (1, 26)",
+ "(9, 1) -> (1, 27)",
+]
+`;
+
+exports[`Binary operators 1`] = `
+"var a = 5 * 30;
+var b = 5 >> 3;
+"
+`;
+
+exports[`Binary operators 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 6) -> (1, 5)",
+ "(1, 8) -> (1, 6)",
+ "(1, 10) -> (1, 7)",
+ "(1, 12) -> (1, 8)",
+ "(1, 14) -> (1, 10)",
+ "(2, 0) -> (1, 11)",
+ "(2, 4) -> (1, 15)",
+ "(2, 6) -> (1, 16)",
+ "(2, 8) -> (1, 17)",
+ "(2, 10) -> (1, 18)",
+ "(2, 13) -> (1, 20)",
+ "(2, 14) -> (1, 21)",
+]
+`;
+
+exports[`Bug 975477 don't move end of line comments to next line 1`] = `
+"switch (request.action) {
+ case 'show': //$NON-NLS-0$
+ if (localStorage.hideicon !== 'true') { //$NON-NLS-0$
+ chrome.pageAction.show(sender.tab.id);
+ }
+ break;
+ case 'hide': /*Multiline
+ Comment */
+ break;
+ default:
+ console.warn('unknown request'); //$NON-NLS-0$
+ // don't respond if you don't understand the message.
+ return;
+}
+"
+`;
+
+exports[`Bug 975477 don't move end of line comments to next line 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 7) -> (1, 7)",
+ "(1, 8) -> (1, 8)",
+ "(1, 15) -> (1, 15)",
+ "(1, 16) -> (1, 16)",
+ "(1, 22) -> (1, 22)",
+ "(1, 24) -> (1, 24)",
+ "(2, 2) -> (2, 13)",
+ "(2, 7) -> (2, 18)",
+ "(2, 13) -> (2, 24)",
+ "(3, 4) -> (3, 15)",
+ "(3, 7) -> (3, 18)",
+ "(3, 8) -> (3, 19)",
+ "(3, 20) -> (3, 31)",
+ "(3, 21) -> (3, 32)",
+ "(3, 30) -> (3, 41)",
+ "(3, 34) -> (3, 45)",
+ "(3, 40) -> (3, 51)",
+ "(3, 42) -> (3, 53)",
+ "(4, 6) -> (4, 17)",
+ "(4, 12) -> (4, 23)",
+ "(4, 13) -> (4, 24)",
+ "(4, 23) -> (4, 34)",
+ "(4, 24) -> (4, 35)",
+ "(4, 28) -> (4, 39)",
+ "(4, 29) -> (4, 40)",
+ "(4, 35) -> (4, 46)",
+ "(4, 36) -> (4, 47)",
+ "(4, 39) -> (4, 50)",
+ "(4, 40) -> (4, 51)",
+ "(4, 42) -> (4, 53)",
+ "(4, 43) -> (4, 54)",
+ "(5, 4) -> (5, 15)",
+ "(6, 4) -> (6, 15)",
+ "(6, 9) -> (6, 20)",
+ "(7, 2) -> (7, 13)",
+ "(7, 7) -> (7, 18)",
+ "(7, 13) -> (7, 24)",
+ "(9, 4) -> (9, 15)",
+ "(9, 9) -> (9, 20)",
+ "(10, 2) -> (10, 13)",
+ "(10, 9) -> (10, 20)",
+ "(11, 4) -> (11, 15)",
+ "(11, 11) -> (11, 22)",
+ "(11, 12) -> (11, 23)",
+ "(11, 16) -> (11, 27)",
+ "(11, 17) -> (11, 28)",
+ "(11, 34) -> (11, 45)",
+ "(11, 35) -> (11, 46)",
+ "(13, 4) -> (13, 15)",
+ "(13, 10) -> (13, 21)",
+ "(14, 0) -> (14, 11)",
+]
+`;
+
+exports[`Bug 977082 - space between grouping operator and dot notation 1`] = `
+"JSON.stringify(3).length;
+([1,
+2,
+3]).length;
+(new Date()).toLocaleString();
+"
+`;
+
+exports[`Bug 977082 - space between grouping operator and dot notation 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 5) -> (1, 5)",
+ "(1, 14) -> (1, 14)",
+ "(1, 15) -> (1, 15)",
+ "(1, 16) -> (1, 16)",
+ "(1, 17) -> (1, 17)",
+ "(1, 18) -> (1, 18)",
+ "(1, 24) -> (1, 24)",
+ "(2, 0) -> (2, 11)",
+ "(2, 1) -> (2, 12)",
+ "(2, 2) -> (2, 13)",
+ "(2, 3) -> (2, 14)",
+ "(3, 0) -> (2, 15)",
+ "(3, 1) -> (2, 16)",
+ "(4, 0) -> (2, 17)",
+ "(4, 1) -> (2, 18)",
+ "(4, 2) -> (2, 19)",
+ "(4, 3) -> (2, 20)",
+ "(4, 4) -> (2, 21)",
+ "(4, 10) -> (2, 27)",
+ "(5, 0) -> (3, 11)",
+ "(5, 1) -> (3, 12)",
+ "(5, 5) -> (3, 16)",
+ "(5, 9) -> (3, 20)",
+ "(5, 10) -> (3, 21)",
+ "(5, 11) -> (3, 22)",
+ "(5, 12) -> (3, 23)",
+ "(5, 13) -> (3, 24)",
+ "(5, 27) -> (3, 38)",
+ "(5, 28) -> (3, 39)",
+ "(5, 29) -> (3, 40)",
+]
+`;
+
+exports[`Bug 1206633 - spaces in for of 1`] = `
+"for (let tab of tabs) {
+}
+"
+`;
+
+exports[`Bug 1206633 - spaces in for of 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 5) -> (1, 5)",
+ "(1, 9) -> (1, 9)",
+ "(1, 13) -> (1, 13)",
+ "(1, 16) -> (1, 16)",
+ "(1, 20) -> (1, 20)",
+ "(1, 22) -> (1, 22)",
+ "(2, 0) -> (1, 23)",
+]
+`;
+
+exports[`Bug 1261971 - indentation after switch statement 1`] = `
+"{
+ switch (x) {
+ }
+ if (y) {
+ }
+ done();
+}
+"
+`;
+
+exports[`Bug 1261971 - indentation after switch statement 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(2, 2) -> (1, 1)",
+ "(2, 9) -> (1, 7)",
+ "(2, 10) -> (1, 8)",
+ "(2, 11) -> (1, 9)",
+ "(2, 13) -> (1, 10)",
+ "(3, 2) -> (1, 11)",
+ "(4, 2) -> (1, 12)",
+ "(4, 5) -> (1, 14)",
+ "(4, 6) -> (1, 15)",
+ "(4, 7) -> (1, 16)",
+ "(4, 9) -> (1, 17)",
+ "(5, 2) -> (1, 18)",
+ "(6, 2) -> (1, 19)",
+ "(6, 6) -> (1, 23)",
+ "(6, 7) -> (1, 24)",
+ "(6, 8) -> (1, 25)",
+ "(7, 0) -> (1, 26)",
+]
+`;
+
+exports[`Bug pretty-sure-3 - escaping line and paragraph separators 1`] = `
+"x = '\\\\u2029\\\\u2028';
+"
+`;
+
+exports[`Bug pretty-sure-3 - escaping line and paragraph separators 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 2) -> (1, 2)",
+ "(1, 4) -> (1, 4)",
+ "(1, 18) -> (1, 18)",
+]
+`;
+
+exports[`Bug pretty-sure-4 - escaping null character before digit 1`] = `
+"x = '\\\\x001';
+"
+`;
+
+exports[`Bug pretty-sure-4 - escaping null character before digit 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 2) -> (1, 2)",
+ "(1, 4) -> (1, 4)",
+ "(1, 11) -> (1, 13)",
+]
+`;
+
+exports[`Bug pretty-sure-5 - empty multiline comment shouldn't throw exception 1`] = `
+"{
+ /*
+ */
+ return;
+}
+"
+`;
+
+exports[`Bug pretty-sure-5 - empty multiline comment shouldn't throw exception 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(4, 2) -> (4, 13)",
+ "(4, 8) -> (4, 19)",
+ "(5, 0) -> (5, 11)",
+]
+`;
+
+exports[`Bug pretty-sure-6 - inline comment shouldn't move parenthesis to next line 1`] = `
+"return /* inline comment */ (1 + 1);
+"
+`;
+
+exports[`Bug pretty-sure-6 - inline comment shouldn't move parenthesis to next line 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 28) -> (1, 28)",
+ "(1, 29) -> (2, 13)",
+ "(1, 31) -> (2, 14)",
+ "(1, 33) -> (2, 15)",
+ "(1, 34) -> (2, 16)",
+ "(1, 35) -> (2, 17)",
+]
+`;
+
+exports[`Bug pretty-sure-7 - accessing a literal number property requires a space 1`] = `
+"0 .toString() + x.toString();
+"
+`;
+
+exports[`Bug pretty-sure-7 - accessing a literal number property requires a space 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 2) -> (1, 2)",
+ "(1, 3) -> (1, 3)",
+ "(1, 11) -> (1, 11)",
+ "(1, 12) -> (1, 12)",
+ "(1, 14) -> (1, 13)",
+ "(1, 16) -> (1, 14)",
+ "(1, 17) -> (1, 15)",
+ "(1, 18) -> (1, 16)",
+ "(1, 26) -> (1, 24)",
+ "(1, 27) -> (1, 25)",
+ "(1, 28) -> (1, 26)",
+]
+`;
+
+exports[`Bug pretty-sure-8 - return and yield only accept arguments when on the same line 1`] = `
+"{
+ return
+ (x)
+ yield
+ (x)
+ yield
+ * x
+}
+"
+`;
+
+exports[`Bug pretty-sure-8 - return and yield only accept arguments when on the same line 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(2, 2) -> (2, 13)",
+ "(3, 2) -> (3, 13)",
+ "(3, 3) -> (3, 14)",
+ "(3, 4) -> (3, 15)",
+ "(4, 2) -> (4, 13)",
+ "(5, 2) -> (5, 13)",
+ "(5, 3) -> (5, 14)",
+ "(5, 4) -> (5, 15)",
+ "(6, 2) -> (6, 13)",
+ "(7, 2) -> (7, 13)",
+ "(7, 4) -> (7, 14)",
+ "(8, 0) -> (8, 11)",
+]
+`;
+
+exports[`Bug pretty-sure-9 - accept unary operator at start of file 1`] = `
+"+ 0
+"
+`;
+
+exports[`Bug pretty-sure-9 - accept unary operator at start of file 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 2) -> (1, 2)",
+]
+`;
+
+exports[`Class extension within a function 1`] = `
+"(function () {
+ class X extends Y {
+ constructor() {
+ }
+ }
+}) ()
+"
+`;
+
+exports[`Class extension within a function 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 1) -> (1, 1)",
+ "(1, 10) -> (1, 9)",
+ "(1, 11) -> (1, 10)",
+ "(1, 13) -> (1, 12)",
+ "(2, 2) -> (1, 15)",
+ "(2, 8) -> (1, 21)",
+ "(2, 10) -> (1, 23)",
+ "(2, 18) -> (1, 31)",
+ "(2, 20) -> (1, 33)",
+ "(3, 4) -> (1, 35)",
+ "(3, 15) -> (1, 46)",
+ "(3, 16) -> (1, 47)",
+ "(3, 18) -> (1, 49)",
+ "(4, 4) -> (1, 50)",
+ "(5, 2) -> (1, 52)",
+ "(6, 0) -> (1, 55)",
+ "(6, 1) -> (1, 56)",
+ "(6, 3) -> (1, 57)",
+ "(6, 4) -> (1, 58)",
+]
+`;
+
+exports[`Class handling 1`] = `
+"class Class {
+ constructor() {
+ }
+}
+"
+`;
+
+exports[`Class handling 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 6) -> (1, 7)",
+ "(1, 12) -> (1, 12)",
+ "(2, 2) -> (1, 13)",
+ "(2, 13) -> (1, 24)",
+ "(2, 14) -> (1, 25)",
+ "(2, 16) -> (1, 26)",
+ "(3, 2) -> (1, 27)",
+ "(4, 0) -> (1, 28)",
+]
+`;
+
+exports[`Code that relies on ASI 1`] = `
+"var foo = 10
+var bar = 20
+function g() {
+ a()
+ b()
+}
+"
+`;
+
+exports[`Code that relies on ASI 2`] = `
+Array [
+ "(1, 0) -> (2, 11)",
+ "(1, 4) -> (2, 15)",
+ "(1, 8) -> (2, 19)",
+ "(1, 10) -> (2, 21)",
+ "(2, 0) -> (3, 11)",
+ "(2, 4) -> (3, 15)",
+ "(2, 8) -> (3, 19)",
+ "(2, 10) -> (3, 21)",
+ "(3, 0) -> (4, 11)",
+ "(3, 9) -> (4, 20)",
+ "(3, 10) -> (4, 21)",
+ "(3, 11) -> (4, 22)",
+ "(3, 13) -> (4, 24)",
+ "(4, 2) -> (5, 13)",
+ "(4, 3) -> (5, 14)",
+ "(4, 4) -> (5, 15)",
+ "(5, 2) -> (6, 13)",
+ "(5, 3) -> (6, 14)",
+ "(5, 4) -> (6, 15)",
+ "(6, 0) -> (7, 11)",
+]
+`;
+
+exports[`Const handling 1`] = `
+"const d = 'yes';
+"
+`;
+
+exports[`Const handling 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 6) -> (1, 6)",
+ "(1, 8) -> (1, 8)",
+ "(1, 10) -> (1, 10)",
+ "(1, 15) -> (1, 15)",
+]
+`;
+
+exports[`Continue/break statements 1`] = `
+"while (1) {
+ if (x) {
+ continue
+ }
+ if (y) {
+ break
+ }
+ if (z) {
+ break foo
+ }
+}
+"
+`;
+
+exports[`Continue/break statements 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 6) -> (1, 5)",
+ "(1, 7) -> (1, 6)",
+ "(1, 8) -> (1, 7)",
+ "(1, 10) -> (1, 8)",
+ "(2, 2) -> (1, 9)",
+ "(2, 5) -> (1, 11)",
+ "(2, 6) -> (1, 12)",
+ "(2, 7) -> (1, 13)",
+ "(2, 9) -> (1, 14)",
+ "(3, 4) -> (1, 15)",
+ "(4, 2) -> (1, 23)",
+ "(5, 2) -> (1, 24)",
+ "(5, 5) -> (1, 26)",
+ "(5, 6) -> (1, 27)",
+ "(5, 7) -> (1, 28)",
+ "(5, 9) -> (1, 29)",
+ "(6, 4) -> (1, 30)",
+ "(7, 2) -> (1, 35)",
+ "(8, 2) -> (1, 36)",
+ "(8, 5) -> (1, 38)",
+ "(8, 6) -> (1, 39)",
+ "(8, 7) -> (1, 40)",
+ "(8, 9) -> (1, 41)",
+ "(9, 4) -> (1, 42)",
+ "(9, 10) -> (1, 48)",
+ "(10, 2) -> (1, 51)",
+ "(11, 0) -> (1, 52)",
+]
+`;
+
+exports[`Delete 1`] = `
+"delete obj.prop;
+"
+`;
+
+exports[`Delete 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 7) -> (1, 7)",
+ "(1, 10) -> (1, 10)",
+ "(1, 11) -> (1, 11)",
+ "(1, 15) -> (1, 15)",
+]
+`;
+
+exports[`Do/while loop 1`] = `
+"do {
+ x
+} while (y)
+"
+`;
+
+exports[`Do/while loop 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 3) -> (1, 2)",
+ "(2, 2) -> (1, 3)",
+ "(3, 0) -> (1, 4)",
+ "(3, 2) -> (1, 5)",
+ "(3, 8) -> (1, 10)",
+ "(3, 9) -> (1, 11)",
+ "(3, 10) -> (1, 12)",
+]
+`;
+
+exports[`Dot handling with keywords which are identifier name 1`] = `
+"y.await.break.const.delete.else.return.new.yield = 1.23;
+"
+`;
+
+exports[`Dot handling with keywords which are identifier name 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 1) -> (1, 1)",
+ "(1, 2) -> (1, 2)",
+ "(1, 7) -> (1, 7)",
+ "(1, 8) -> (1, 8)",
+ "(1, 13) -> (1, 13)",
+ "(1, 14) -> (1, 14)",
+ "(1, 19) -> (1, 19)",
+ "(1, 20) -> (1, 20)",
+ "(1, 26) -> (1, 26)",
+ "(1, 27) -> (1, 27)",
+ "(1, 31) -> (1, 31)",
+ "(1, 32) -> (1, 32)",
+ "(1, 38) -> (1, 38)",
+ "(1, 39) -> (1, 39)",
+ "(1, 42) -> (1, 42)",
+ "(1, 43) -> (1, 43)",
+ "(1, 49) -> (1, 49)",
+ "(1, 51) -> (1, 51)",
+ "(1, 55) -> (1, 55)",
+]
+`;
+
+exports[`Dot handling with let which is identifier name 1`] = `
+"y.let.let = 1.23;
+"
+`;
+
+exports[`Dot handling with let which is identifier name 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 1) -> (1, 1)",
+ "(1, 2) -> (1, 2)",
+ "(1, 5) -> (1, 5)",
+ "(1, 6) -> (1, 6)",
+ "(1, 10) -> (1, 10)",
+ "(1, 12) -> (1, 12)",
+ "(1, 16) -> (1, 16)",
+]
+`;
+
+exports[`Empty object/array literals 1`] = `
+"let a = [];
+const b = {};
+c = {
+ ...{},
+ d: 42
+};
+for (let x of []) {
+ for (let y in {}) {
+ }
+}
+"
+`;
+
+exports[`Empty object/array literals 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 6) -> (1, 5)",
+ "(1, 8) -> (1, 6)",
+ "(1, 9) -> (1, 7)",
+ "(1, 10) -> (1, 8)",
+ "(2, 0) -> (1, 9)",
+ "(2, 6) -> (1, 15)",
+ "(2, 8) -> (1, 16)",
+ "(2, 10) -> (1, 17)",
+ "(2, 11) -> (1, 18)",
+ "(2, 12) -> (1, 19)",
+ "(3, 0) -> (1, 20)",
+ "(3, 2) -> (1, 21)",
+ "(3, 4) -> (1, 22)",
+ "(4, 2) -> (1, 23)",
+ "(4, 5) -> (1, 26)",
+ "(4, 6) -> (1, 27)",
+ "(4, 7) -> (1, 28)",
+ "(5, 2) -> (1, 29)",
+ "(5, 3) -> (1, 30)",
+ "(5, 5) -> (1, 32)",
+ "(6, 0) -> (1, 34)",
+ "(6, 1) -> (1, 35)",
+ "(7, 0) -> (1, 36)",
+ "(7, 4) -> (1, 39)",
+ "(7, 5) -> (1, 40)",
+ "(7, 9) -> (1, 44)",
+ "(7, 11) -> (1, 46)",
+ "(7, 14) -> (1, 49)",
+ "(7, 15) -> (1, 50)",
+ "(7, 16) -> (1, 51)",
+ "(7, 18) -> (1, 52)",
+ "(8, 2) -> (1, 53)",
+ "(8, 6) -> (1, 56)",
+ "(8, 7) -> (1, 57)",
+ "(8, 11) -> (1, 61)",
+ "(8, 13) -> (1, 63)",
+ "(8, 16) -> (1, 66)",
+ "(8, 17) -> (1, 67)",
+ "(8, 18) -> (1, 68)",
+ "(8, 20) -> (1, 69)",
+ "(9, 2) -> (1, 70)",
+ "(10, 0) -> (1, 71)",
+]
+`;
+
+exports[`Escaping backslashes in strings 1`] = `
+"'\\\\\\\\'
+"
+`;
+
+exports[`Escaping backslashes in strings 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+]
+`;
+
+exports[`Escaping carriage return in strings 1`] = `
+"'\\\\r'
+"
+`;
+
+exports[`Escaping carriage return in strings 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+]
+`;
+
+exports[`Escaping form feed in strings 1`] = `
+"'\\\\f'
+"
+`;
+
+exports[`Escaping form feed in strings 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+]
+`;
+
+exports[`Escaping null character in strings 1`] = `
+"'\\\\x00'
+"
+`;
+
+exports[`Escaping null character in strings 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+]
+`;
+
+exports[`Escaping tab in strings 1`] = `
+"'\\\\t'
+"
+`;
+
+exports[`Escaping tab in strings 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+]
+`;
+
+exports[`Escaping vertical tab in strings 1`] = `
+"'\\\\v'
+"
+`;
+
+exports[`Escaping vertical tab in strings 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+]
+`;
+
+exports[`False assignment 1`] = `
+"var foo = false;
+"
+`;
+
+exports[`False assignment 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 8) -> (1, 7)",
+ "(1, 10) -> (1, 8)",
+ "(1, 15) -> (1, 13)",
+]
+`;
+
+exports[`Fat arrow function 1`] = `
+"const a = () => 42
+addEventListener('click', e => {
+ return false
+});
+const sum = (c, d) => c + d
+"
+`;
+
+exports[`Fat arrow function 2`] = `
+Array [
+ "(1, 0) -> (2, 6)",
+ "(1, 6) -> (2, 12)",
+ "(1, 8) -> (2, 14)",
+ "(1, 10) -> (2, 16)",
+ "(1, 11) -> (2, 17)",
+ "(1, 13) -> (2, 19)",
+ "(1, 16) -> (2, 22)",
+ "(2, 0) -> (3, 6)",
+ "(2, 16) -> (3, 22)",
+ "(2, 17) -> (3, 23)",
+ "(2, 24) -> (3, 30)",
+ "(2, 26) -> (3, 32)",
+ "(2, 28) -> (3, 34)",
+ "(2, 31) -> (3, 37)",
+ "(3, 2) -> (3, 39)",
+ "(3, 9) -> (3, 46)",
+ "(4, 0) -> (3, 52)",
+ "(4, 1) -> (3, 53)",
+ "(4, 2) -> (3, 54)",
+ "(5, 0) -> (4, 6)",
+ "(5, 6) -> (4, 12)",
+ "(5, 10) -> (4, 16)",
+ "(5, 12) -> (4, 18)",
+ "(5, 13) -> (4, 19)",
+ "(5, 14) -> (4, 20)",
+ "(5, 16) -> (4, 21)",
+ "(5, 17) -> (4, 22)",
+ "(5, 19) -> (4, 24)",
+ "(5, 22) -> (4, 27)",
+ "(5, 24) -> (4, 28)",
+ "(5, 26) -> (4, 29)",
+]
+`;
+
+exports[`For loop 1`] = `
+"for (var i = 0; i < n; i++) {
+ console.log(i);
+}
+"
+`;
+
+exports[`For loop 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 5) -> (1, 5)",
+ "(1, 9) -> (1, 9)",
+ "(1, 11) -> (1, 11)",
+ "(1, 13) -> (1, 13)",
+ "(1, 14) -> (1, 14)",
+ "(1, 16) -> (1, 16)",
+ "(1, 18) -> (1, 18)",
+ "(1, 20) -> (1, 20)",
+ "(1, 21) -> (1, 21)",
+ "(1, 23) -> (1, 23)",
+ "(1, 24) -> (1, 24)",
+ "(1, 26) -> (1, 26)",
+ "(1, 28) -> (1, 28)",
+ "(2, 2) -> (1, 30)",
+ "(2, 9) -> (1, 37)",
+ "(2, 10) -> (1, 38)",
+ "(2, 13) -> (1, 41)",
+ "(2, 14) -> (1, 42)",
+ "(2, 15) -> (1, 43)",
+ "(2, 16) -> (1, 44)",
+ "(3, 0) -> (1, 46)",
+]
+`;
+
+exports[`Function calls 1`] = `
+"var result = func(a, b, c, d);
+"
+`;
+
+exports[`Function calls 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 11) -> (1, 10)",
+ "(1, 13) -> (1, 11)",
+ "(1, 17) -> (1, 15)",
+ "(1, 18) -> (1, 16)",
+ "(1, 19) -> (1, 17)",
+ "(1, 21) -> (1, 18)",
+ "(1, 22) -> (1, 19)",
+ "(1, 24) -> (1, 20)",
+ "(1, 25) -> (1, 21)",
+ "(1, 27) -> (1, 22)",
+ "(1, 28) -> (1, 23)",
+ "(1, 29) -> (1, 24)",
+]
+`;
+
+exports[`Getter and setter literals 1`] = `
+"var obj = {
+ get foo() {
+ return this._foo
+ },
+ set foo(v) {
+ this._foo = v
+ }
+}
+"
+`;
+
+exports[`Getter and setter literals 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 8) -> (1, 7)",
+ "(1, 10) -> (1, 8)",
+ "(2, 2) -> (1, 9)",
+ "(2, 6) -> (1, 13)",
+ "(2, 9) -> (1, 16)",
+ "(2, 10) -> (1, 17)",
+ "(2, 12) -> (1, 18)",
+ "(3, 4) -> (1, 19)",
+ "(3, 11) -> (1, 26)",
+ "(3, 15) -> (1, 30)",
+ "(3, 16) -> (1, 31)",
+ "(4, 2) -> (1, 35)",
+ "(4, 3) -> (1, 36)",
+ "(5, 2) -> (1, 37)",
+ "(5, 6) -> (1, 41)",
+ "(5, 9) -> (1, 44)",
+ "(5, 10) -> (1, 45)",
+ "(5, 11) -> (1, 46)",
+ "(5, 13) -> (1, 47)",
+ "(6, 4) -> (1, 48)",
+ "(6, 8) -> (1, 52)",
+ "(6, 9) -> (1, 53)",
+ "(6, 14) -> (1, 57)",
+ "(6, 16) -> (1, 58)",
+ "(7, 2) -> (1, 59)",
+ "(8, 0) -> (1, 60)",
+]
+`;
+
+exports[`If/else statement 1`] = `
+"if (c) {
+ then()
+} else {
+ other()
+}
+"
+`;
+
+exports[`If/else statement 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 3) -> (1, 2)",
+ "(1, 4) -> (1, 3)",
+ "(1, 5) -> (1, 4)",
+ "(1, 7) -> (1, 5)",
+ "(2, 2) -> (1, 6)",
+ "(2, 6) -> (1, 10)",
+ "(2, 7) -> (1, 11)",
+ "(3, 0) -> (1, 12)",
+ "(3, 2) -> (1, 13)",
+ "(3, 7) -> (1, 17)",
+ "(4, 2) -> (1, 18)",
+ "(4, 7) -> (1, 23)",
+ "(4, 8) -> (1, 24)",
+ "(5, 0) -> (1, 25)",
+]
+`;
+
+exports[`If/else without curlies 1`] = `
+"if (c) a else b
+"
+`;
+
+exports[`If/else without curlies 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 3) -> (1, 2)",
+ "(1, 4) -> (1, 3)",
+ "(1, 5) -> (1, 4)",
+ "(1, 7) -> (1, 6)",
+ "(1, 9) -> (1, 8)",
+ "(1, 14) -> (1, 13)",
+]
+`;
+
+exports[`Immediately invoked function expression 1`] = `
+"(function () {
+ thingy()
+}())
+"
+`;
+
+exports[`Immediately invoked function expression 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 1) -> (1, 1)",
+ "(1, 10) -> (1, 9)",
+ "(1, 11) -> (1, 10)",
+ "(1, 13) -> (1, 11)",
+ "(2, 2) -> (1, 12)",
+ "(2, 8) -> (1, 18)",
+ "(2, 9) -> (1, 19)",
+ "(3, 0) -> (1, 20)",
+ "(3, 1) -> (1, 21)",
+ "(3, 2) -> (1, 22)",
+ "(3, 3) -> (1, 23)",
+]
+`;
+
+exports[`In operator 1`] = `
+"if (foo in bar) {
+ doThing()
+}
+"
+`;
+
+exports[`In operator 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 3) -> (1, 2)",
+ "(1, 4) -> (1, 3)",
+ "(1, 8) -> (1, 7)",
+ "(1, 11) -> (1, 10)",
+ "(1, 14) -> (1, 13)",
+ "(1, 16) -> (1, 14)",
+ "(2, 2) -> (1, 15)",
+ "(2, 9) -> (1, 22)",
+ "(2, 10) -> (1, 23)",
+ "(3, 0) -> (1, 24)",
+]
+`;
+
+exports[`Indented multiline comment 1`] = `
+"function foo() {
+ /**
+ * java doc style comment
+ * more comment
+ */
+ bar();
+}
+"
+`;
+
+exports[`Indented multiline comment 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 9) -> (1, 9)",
+ "(1, 12) -> (1, 12)",
+ "(1, 13) -> (1, 13)",
+ "(1, 15) -> (1, 15)",
+ "(6, 2) -> (6, 13)",
+ "(6, 5) -> (6, 16)",
+ "(6, 6) -> (6, 17)",
+ "(6, 7) -> (6, 18)",
+ "(7, 0) -> (7, 11)",
+]
+`;
+
+exports[`Instanceof 1`] = `
+"var a = x instanceof y;
+"
+`;
+
+exports[`Instanceof 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 6) -> (1, 5)",
+ "(1, 8) -> (1, 6)",
+ "(1, 10) -> (1, 8)",
+ "(1, 21) -> (1, 19)",
+ "(1, 22) -> (1, 20)",
+]
+`;
+
+exports[`Let handling with value 1`] = `
+"let d = 'yes';
+"
+`;
+
+exports[`Let handling with value 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 6) -> (1, 6)",
+ "(1, 8) -> (1, 8)",
+ "(1, 13) -> (1, 13)",
+]
+`;
+
+exports[`Let handling without value 1`] = `
+"let d;
+"
+`;
+
+exports[`Let handling without value 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 5) -> (1, 5)",
+]
+`;
+
+exports[`Long parenthesis 1`] = `
+"if (
+ thisIsAVeryLongVariable &&
+ thisIsAnotherOne ||
+ yetAnotherVeryLongVariable
+) {
+ (
+ thisIsAnotherOne = thisMayReturnNull() ||
+ 'hi',
+ thisIsAVeryLongVariable = 42,
+ yetAnotherVeryLongVariable &&
+ doSomething(
+ true /* do it well */ ,
+ thisIsAVeryLongVariable,
+ thisIsAnotherOne,
+ yetAnotherVeryLongVariable
+ )
+ )
+}
+for (
+ let thisIsAnotherVeryLongVariable = 0;
+ i < thisIsAnotherVeryLongVariable.length;
+ thisIsAnotherVeryLongVariable++
+) {
+}
+const x = ({
+ thisIsAnotherVeryLongPropertyName: 'but should not cause the paren to be a line delimiter'
+})
+"
+`;
+
+exports[`Long parenthesis 2`] = `
+Array [
+ "(1, 0) -> (2, 6)",
+ "(1, 3) -> (2, 9)",
+ "(2, 2) -> (2, 10)",
+ "(2, 26) -> (2, 34)",
+ "(3, 2) -> (2, 37)",
+ "(3, 19) -> (2, 54)",
+ "(4, 2) -> (2, 57)",
+ "(5, 0) -> (2, 83)",
+ "(5, 2) -> (2, 85)",
+ "(6, 2) -> (3, 8)",
+ "(7, 4) -> (3, 9)",
+ "(7, 21) -> (3, 26)",
+ "(7, 23) -> (3, 28)",
+ "(7, 40) -> (3, 45)",
+ "(7, 41) -> (3, 46)",
+ "(7, 43) -> (3, 48)",
+ "(8, 4) -> (3, 51)",
+ "(8, 8) -> (3, 55)",
+ "(9, 4) -> (3, 57)",
+ "(9, 28) -> (3, 81)",
+ "(9, 30) -> (3, 83)",
+ "(9, 32) -> (3, 85)",
+ "(10, 4) -> (3, 87)",
+ "(10, 31) -> (3, 114)",
+ "(11, 4) -> (3, 117)",
+ "(11, 15) -> (3, 128)",
+ "(12, 6) -> (3, 129)",
+ "(12, 28) -> (3, 150)",
+ "(13, 6) -> (3, 151)",
+ "(13, 29) -> (3, 174)",
+ "(14, 6) -> (3, 176)",
+ "(14, 22) -> (3, 192)",
+ "(15, 6) -> (3, 194)",
+ "(16, 4) -> (3, 220)",
+ "(17, 2) -> (3, 221)",
+ "(18, 0) -> (4, 6)",
+ "(19, 0) -> (5, 6)",
+ "(19, 4) -> (5, 10)",
+ "(20, 2) -> (5, 11)",
+ "(20, 6) -> (5, 15)",
+ "(20, 36) -> (5, 45)",
+ "(20, 38) -> (5, 47)",
+ "(20, 39) -> (5, 48)",
+ "(21, 2) -> (5, 50)",
+ "(21, 4) -> (5, 52)",
+ "(21, 6) -> (5, 54)",
+ "(21, 35) -> (5, 83)",
+ "(21, 36) -> (5, 84)",
+ "(21, 42) -> (5, 90)",
+ "(22, 2) -> (5, 92)",
+ "(22, 31) -> (5, 121)",
+ "(23, 0) -> (5, 123)",
+ "(23, 2) -> (5, 125)",
+ "(24, 0) -> (5, 126)",
+ "(25, 0) -> (6, 6)",
+ "(25, 6) -> (6, 12)",
+ "(25, 8) -> (6, 14)",
+ "(25, 10) -> (6, 16)",
+ "(25, 11) -> (6, 17)",
+ "(26, 2) -> (6, 18)",
+ "(26, 35) -> (6, 51)",
+ "(26, 37) -> (6, 53)",
+ "(27, 0) -> (6, 108)",
+ "(27, 1) -> (6, 109)",
+]
+`;
+
+exports[`Multi line comment 1`] = `
+"/* Comment
+ more comment */
+function foo() {
+ bar();
+}
+"
+`;
+
+exports[`Multi line comment 2`] = `
+Array [
+ "(3, 0) -> (4, 4)",
+ "(3, 9) -> (4, 13)",
+ "(3, 12) -> (4, 16)",
+ "(3, 13) -> (4, 17)",
+ "(3, 15) -> (4, 19)",
+ "(4, 2) -> (4, 21)",
+ "(4, 5) -> (4, 24)",
+ "(4, 6) -> (4, 25)",
+ "(4, 7) -> (4, 26)",
+ "(5, 0) -> (4, 28)",
+]
+`;
+
+exports[`Multiple single line comments 1`] = `
+"function f() {
+ // a
+ // b
+ // c
+}
+"
+`;
+
+exports[`Multiple single line comments 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 9) -> (1, 9)",
+ "(1, 10) -> (1, 10)",
+ "(1, 11) -> (1, 11)",
+ "(1, 13) -> (1, 13)",
+ "(5, 0) -> (5, 11)",
+]
+`;
+
+exports[`Named class handling 1`] = `
+"let unnamed = class Class {
+ constructor() {
+ }
+}
+"
+`;
+
+exports[`Named class handling 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 12) -> (1, 11)",
+ "(1, 14) -> (1, 12)",
+ "(1, 20) -> (1, 18)",
+ "(1, 26) -> (1, 23)",
+ "(2, 2) -> (1, 24)",
+ "(2, 13) -> (1, 35)",
+ "(2, 14) -> (1, 36)",
+ "(2, 16) -> (1, 37)",
+ "(3, 2) -> (1, 38)",
+ "(4, 0) -> (1, 39)",
+]
+`;
+
+exports[`Nested function 1`] = `
+"function foo() {
+ function bar() {
+ debugger;
+ }
+ bar();
+}
+"
+`;
+
+exports[`Nested function 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 9) -> (1, 9)",
+ "(1, 12) -> (1, 12)",
+ "(1, 13) -> (1, 13)",
+ "(1, 15) -> (1, 15)",
+ "(2, 2) -> (1, 17)",
+ "(2, 11) -> (1, 26)",
+ "(2, 14) -> (1, 29)",
+ "(2, 15) -> (1, 30)",
+ "(2, 17) -> (1, 32)",
+ "(3, 4) -> (1, 34)",
+ "(3, 12) -> (1, 42)",
+ "(4, 2) -> (1, 44)",
+ "(5, 2) -> (1, 46)",
+ "(5, 5) -> (1, 49)",
+ "(5, 6) -> (1, 50)",
+ "(5, 7) -> (1, 51)",
+ "(6, 0) -> (1, 53)",
+]
+`;
+
+exports[`New expression 1`] = `
+"var foo = new Foo();
+"
+`;
+
+exports[`New expression 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 8) -> (1, 7)",
+ "(1, 10) -> (1, 8)",
+ "(1, 14) -> (1, 12)",
+ "(1, 17) -> (1, 15)",
+ "(1, 18) -> (1, 16)",
+ "(1, 19) -> (1, 17)",
+]
+`;
+
+exports[`Non-ASI function call 1`] = `
+"f()
+"
+`;
+
+exports[`Non-ASI function call 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 1) -> (2, 11)",
+ "(1, 2) -> (2, 12)",
+]
+`;
+
+exports[`Non-ASI in 1`] = `
+"'x' in foo
+"
+`;
+
+exports[`Non-ASI in 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (2, 11)",
+ "(1, 7) -> (2, 14)",
+]
+`;
+
+exports[`Non-ASI new 1`] = `
+"new F()
+"
+`;
+
+exports[`Non-ASI new 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (2, 11)",
+ "(1, 5) -> (2, 12)",
+ "(1, 6) -> (2, 13)",
+]
+`;
+
+exports[`Non-ASI property access 1`] = `
+"[
+ 1,
+ 2,
+ 3
+]
+[0]
+"
+`;
+
+exports[`Non-ASI property access 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(2, 2) -> (1, 1)",
+ "(2, 3) -> (1, 2)",
+ "(3, 2) -> (1, 3)",
+ "(3, 3) -> (1, 4)",
+ "(4, 2) -> (1, 5)",
+ "(5, 0) -> (1, 6)",
+ "(6, 0) -> (2, 11)",
+ "(6, 1) -> (2, 12)",
+ "(6, 2) -> (2, 13)",
+]
+`;
+
+exports[`Null assignment 1`] = `
+"var i = null;
+"
+`;
+
+exports[`Null assignment 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 6) -> (1, 5)",
+ "(1, 8) -> (1, 6)",
+ "(1, 12) -> (1, 10)",
+]
+`;
+
+exports[`Objects 1`] = `
+"var o = {
+ a: 1,
+ b: 2
+};
+"
+`;
+
+exports[`Objects 2`] = `
+Array [
+ "(1, 0) -> (2, 6)",
+ "(1, 4) -> (2, 10)",
+ "(1, 6) -> (2, 11)",
+ "(1, 8) -> (2, 12)",
+ "(2, 2) -> (2, 13)",
+ "(2, 3) -> (2, 14)",
+ "(2, 5) -> (2, 15)",
+ "(2, 6) -> (2, 16)",
+ "(3, 2) -> (3, 6)",
+ "(3, 3) -> (3, 7)",
+ "(3, 5) -> (3, 8)",
+ "(4, 0) -> (3, 9)",
+ "(4, 1) -> (3, 10)",
+]
+`;
+
+exports[`Optional chaining parsing support 1`] = `
+"x?.y?.z?.['a']?.check();
+"
+`;
+
+exports[`Optional chaining parsing support 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 1) -> (1, 1)",
+ "(1, 3) -> (1, 3)",
+ "(1, 4) -> (1, 4)",
+ "(1, 6) -> (1, 6)",
+ "(1, 7) -> (1, 7)",
+ "(1, 9) -> (1, 9)",
+ "(1, 10) -> (1, 10)",
+ "(1, 13) -> (1, 13)",
+ "(1, 14) -> (1, 14)",
+ "(1, 16) -> (1, 16)",
+ "(1, 21) -> (1, 21)",
+ "(1, 22) -> (1, 22)",
+ "(1, 23) -> (1, 23)",
+]
+`;
+
+exports[`Private fields parsing support 1`] = `
+"class MyClass {
+ constructor(a) {
+ this.#a = a;
+ this.#b = Math.random();
+ this.ab = this.#getAB();
+ }
+ #a
+ #b = 'default value'
+ static #someStaticPrivate
+ #getA() {
+ return this.#a;
+ }
+ #getAB() {
+ return this.#getA() + this.#b
+ }
+}
+"
+`;
+
+exports[`Private fields parsing support 2`] = `
+Array [
+ "(1, 0) -> (2, 6)",
+ "(1, 6) -> (2, 12)",
+ "(1, 14) -> (2, 20)",
+ "(2, 2) -> (3, 8)",
+ "(2, 13) -> (3, 19)",
+ "(2, 14) -> (3, 20)",
+ "(2, 15) -> (3, 21)",
+ "(2, 17) -> (3, 23)",
+ "(3, 4) -> (4, 10)",
+ "(3, 8) -> (4, 14)",
+ "(3, 9) -> (4, 15)",
+ "(3, 12) -> (4, 18)",
+ "(3, 14) -> (4, 20)",
+ "(3, 15) -> (4, 21)",
+ "(4, 4) -> (4, 22)",
+ "(4, 8) -> (4, 26)",
+ "(4, 9) -> (4, 27)",
+ "(4, 12) -> (4, 30)",
+ "(4, 14) -> (4, 32)",
+ "(4, 18) -> (4, 36)",
+ "(4, 19) -> (4, 37)",
+ "(4, 25) -> (4, 43)",
+ "(4, 26) -> (4, 44)",
+ "(4, 27) -> (4, 45)",
+ "(5, 4) -> (4, 46)",
+ "(5, 8) -> (4, 50)",
+ "(5, 9) -> (4, 51)",
+ "(5, 12) -> (4, 54)",
+ "(5, 14) -> (4, 56)",
+ "(5, 18) -> (4, 60)",
+ "(5, 19) -> (4, 61)",
+ "(5, 25) -> (4, 67)",
+ "(5, 26) -> (4, 68)",
+ "(5, 27) -> (4, 69)",
+ "(6, 2) -> (5, 8)",
+ "(7, 2) -> (6, 8)",
+ "(8, 2) -> (7, 8)",
+ "(8, 5) -> (7, 11)",
+ "(8, 7) -> (7, 13)",
+ "(9, 2) -> (8, 8)",
+ "(9, 9) -> (8, 15)",
+ "(10, 2) -> (9, 8)",
+ "(10, 7) -> (9, 13)",
+ "(10, 8) -> (9, 14)",
+ "(10, 10) -> (9, 16)",
+ "(11, 4) -> (10, 10)",
+ "(11, 11) -> (10, 17)",
+ "(11, 15) -> (10, 21)",
+ "(11, 16) -> (10, 22)",
+ "(11, 18) -> (10, 24)",
+ "(12, 2) -> (11, 8)",
+ "(13, 2) -> (12, 8)",
+ "(13, 8) -> (12, 14)",
+ "(13, 9) -> (12, 15)",
+ "(13, 11) -> (12, 17)",
+ "(14, 4) -> (13, 10)",
+ "(14, 11) -> (13, 17)",
+ "(14, 15) -> (13, 21)",
+ "(14, 16) -> (13, 22)",
+ "(14, 21) -> (13, 27)",
+ "(14, 22) -> (13, 28)",
+ "(14, 24) -> (13, 29)",
+ "(14, 26) -> (13, 30)",
+ "(14, 30) -> (13, 34)",
+ "(14, 31) -> (14, 12)",
+ "(15, 2) -> (15, 8)",
+ "(16, 0) -> (16, 6)",
+]
+`;
+
+exports[`Regexp 1`] = `
+"var r = /foobar/g;
+"
+`;
+
+exports[`Regexp 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 6) -> (1, 5)",
+ "(1, 8) -> (1, 6)",
+ "(1, 17) -> (1, 15)",
+]
+`;
+
+exports[`Simple function 1`] = `
+"function foo() {
+ bar();
+}
+"
+`;
+
+exports[`Simple function 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 9) -> (1, 9)",
+ "(1, 12) -> (1, 12)",
+ "(1, 13) -> (1, 13)",
+ "(1, 15) -> (1, 15)",
+ "(2, 2) -> (1, 17)",
+ "(2, 5) -> (1, 20)",
+ "(2, 6) -> (1, 21)",
+ "(2, 7) -> (1, 22)",
+ "(3, 0) -> (1, 24)",
+]
+`;
+
+exports[`Single line comment 1`] = `
+"// Comment
+function foo() {
+ bar();
+}
+"
+`;
+
+exports[`Single line comment 2`] = `
+Array [
+ "(2, 0) -> (3, 4)",
+ "(2, 9) -> (3, 13)",
+ "(2, 12) -> (3, 16)",
+ "(2, 13) -> (3, 17)",
+ "(2, 15) -> (3, 19)",
+ "(3, 2) -> (3, 21)",
+ "(3, 5) -> (3, 24)",
+ "(3, 6) -> (3, 25)",
+ "(3, 7) -> (3, 26)",
+ "(4, 0) -> (3, 28)",
+]
+`;
+
+exports[`Stack-keyword property access 1`] = `
+"foo.a = 1.1;
+foo.do.switch.case.default = 2.2;
+foo.b = 3.3;
+"
+`;
+
+exports[`Stack-keyword property access 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 3) -> (1, 3)",
+ "(1, 4) -> (1, 4)",
+ "(1, 6) -> (1, 5)",
+ "(1, 8) -> (1, 6)",
+ "(1, 11) -> (1, 9)",
+ "(2, 0) -> (1, 10)",
+ "(2, 3) -> (1, 13)",
+ "(2, 4) -> (1, 14)",
+ "(2, 6) -> (1, 16)",
+ "(2, 7) -> (1, 17)",
+ "(2, 13) -> (1, 23)",
+ "(2, 14) -> (1, 24)",
+ "(2, 18) -> (1, 28)",
+ "(2, 19) -> (1, 29)",
+ "(2, 27) -> (1, 36)",
+ "(2, 29) -> (1, 37)",
+ "(2, 32) -> (1, 40)",
+ "(3, 0) -> (1, 41)",
+ "(3, 3) -> (1, 44)",
+ "(3, 4) -> (1, 45)",
+ "(3, 6) -> (1, 46)",
+ "(3, 8) -> (1, 47)",
+ "(3, 11) -> (1, 50)",
+]
+`;
+
+exports[`String with quote 1`] = `
+"var foo = '\\\\'';
+"
+`;
+
+exports[`String with quote 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 8) -> (1, 8)",
+ "(1, 10) -> (1, 10)",
+ "(1, 14) -> (1, 13)",
+]
+`;
+
+exports[`String with semicolon 1`] = `
+"var foo = ';';
+"
+`;
+
+exports[`String with semicolon 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 8) -> (1, 8)",
+ "(1, 10) -> (1, 10)",
+ "(1, 13) -> (1, 13)",
+]
+`;
+
+exports[`Subclass handling 1`] = `
+"class Class extends Base {
+ constructor() {
+ }
+}
+"
+`;
+
+exports[`Subclass handling 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 6) -> (1, 7)",
+ "(1, 12) -> (1, 14)",
+ "(1, 20) -> (1, 23)",
+ "(1, 25) -> (1, 27)",
+ "(2, 2) -> (1, 28)",
+ "(2, 13) -> (1, 39)",
+ "(2, 14) -> (1, 40)",
+ "(2, 16) -> (1, 41)",
+ "(3, 2) -> (1, 42)",
+ "(4, 0) -> (1, 43)",
+]
+`;
+
+exports[`Switch statements 1`] = `
+"switch (x) {
+ case a:
+ foo();
+ break;
+ default:
+ bar()
+}
+"
+`;
+
+exports[`Switch statements 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 7) -> (1, 6)",
+ "(1, 8) -> (1, 7)",
+ "(1, 9) -> (1, 8)",
+ "(1, 11) -> (1, 9)",
+ "(2, 2) -> (1, 10)",
+ "(2, 7) -> (1, 15)",
+ "(2, 8) -> (1, 16)",
+ "(3, 4) -> (1, 17)",
+ "(3, 7) -> (1, 20)",
+ "(3, 8) -> (1, 21)",
+ "(3, 9) -> (1, 22)",
+ "(4, 4) -> (1, 23)",
+ "(4, 9) -> (1, 28)",
+ "(5, 2) -> (1, 29)",
+ "(5, 9) -> (1, 36)",
+ "(6, 4) -> (1, 37)",
+ "(6, 7) -> (1, 40)",
+ "(6, 8) -> (1, 41)",
+ "(7, 0) -> (1, 42)",
+]
+`;
+
+exports[`Template literals 1`] = `
+"\`abc\${ JSON.stringify({
+ clas: 'testing'
+}) }def\`;
+{
+ a();
+}
+"
+`;
+
+exports[`Template literals 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 1) -> (1, 1)",
+ "(1, 4) -> (1, 4)",
+ "(1, 7) -> (1, 6)",
+ "(1, 11) -> (1, 10)",
+ "(1, 12) -> (1, 11)",
+ "(1, 21) -> (1, 20)",
+ "(1, 22) -> (1, 21)",
+ "(2, 2) -> (1, 22)",
+ "(2, 6) -> (1, 26)",
+ "(2, 8) -> (1, 28)",
+ "(3, 0) -> (1, 37)",
+ "(3, 1) -> (1, 38)",
+ "(3, 3) -> (1, 39)",
+ "(3, 4) -> (1, 40)",
+ "(3, 7) -> (1, 43)",
+ "(3, 8) -> (1, 44)",
+ "(4, 0) -> (1, 45)",
+ "(5, 2) -> (1, 46)",
+ "(5, 3) -> (1, 47)",
+ "(5, 4) -> (1, 48)",
+ "(5, 5) -> (1, 49)",
+ "(6, 0) -> (1, 50)",
+]
+`;
+
+exports[`Ternary operator 1`] = `
+"bar ? baz : bang;
+"
+`;
+
+exports[`Ternary operator 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 3)",
+ "(1, 6) -> (1, 4)",
+ "(1, 10) -> (1, 7)",
+ "(1, 12) -> (1, 8)",
+ "(1, 16) -> (1, 12)",
+]
+`;
+
+exports[`This property access 1`] = `
+"var foo = this.foo;
+"
+`;
+
+exports[`This property access 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 8) -> (1, 7)",
+ "(1, 10) -> (1, 8)",
+ "(1, 14) -> (1, 12)",
+ "(1, 15) -> (1, 13)",
+ "(1, 18) -> (1, 16)",
+]
+`;
+
+exports[`True assignment 1`] = `
+"var foo = true;
+"
+`;
+
+exports[`True assignment 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 8) -> (1, 7)",
+ "(1, 10) -> (1, 8)",
+ "(1, 14) -> (1, 12)",
+]
+`;
+
+exports[`Try/catch/finally statement 1`] = `
+"try {
+ dangerous()
+} catch (e) {
+ handle(e)
+} finally {
+ cleanup()
+}
+"
+`;
+
+exports[`Try/catch/finally statement 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 3)",
+ "(2, 2) -> (1, 4)",
+ "(2, 11) -> (1, 13)",
+ "(2, 12) -> (1, 14)",
+ "(3, 0) -> (1, 15)",
+ "(3, 2) -> (1, 16)",
+ "(3, 8) -> (1, 21)",
+ "(3, 9) -> (1, 22)",
+ "(3, 10) -> (1, 23)",
+ "(3, 12) -> (1, 24)",
+ "(4, 2) -> (1, 25)",
+ "(4, 8) -> (1, 31)",
+ "(4, 9) -> (1, 32)",
+ "(4, 10) -> (1, 33)",
+ "(5, 0) -> (1, 34)",
+ "(5, 2) -> (1, 35)",
+ "(5, 10) -> (1, 42)",
+ "(6, 2) -> (1, 43)",
+ "(6, 9) -> (1, 50)",
+ "(6, 10) -> (1, 51)",
+ "(7, 0) -> (1, 52)",
+]
+`;
+
+exports[`Undefined assignment 1`] = `
+"var i = undefined;
+"
+`;
+
+exports[`Undefined assignment 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 6) -> (1, 5)",
+ "(1, 8) -> (1, 6)",
+ "(1, 17) -> (1, 15)",
+]
+`;
+
+exports[`Unnamed class handling 1`] = `
+"let unnamed = class {
+ constructor() {
+ }
+}
+"
+`;
+
+exports[`Unnamed class handling 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 12) -> (1, 11)",
+ "(1, 14) -> (1, 12)",
+ "(1, 20) -> (1, 17)",
+ "(2, 2) -> (1, 18)",
+ "(2, 13) -> (1, 29)",
+ "(2, 14) -> (1, 30)",
+ "(2, 16) -> (1, 31)",
+ "(3, 2) -> (1, 32)",
+ "(4, 0) -> (1, 33)",
+]
+`;
+
+exports[`Void 0 assignment 1`] = `
+"var i = void 0;
+"
+`;
+
+exports[`Void 0 assignment 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 6) -> (1, 5)",
+ "(1, 8) -> (1, 6)",
+ "(1, 13) -> (1, 11)",
+ "(1, 14) -> (1, 12)",
+]
+`;
+
+exports[`With statement 1`] = `
+"with (obj) {
+ crock()
+}
+"
+`;
+
+exports[`With statement 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 5) -> (1, 4)",
+ "(1, 6) -> (1, 5)",
+ "(1, 9) -> (1, 8)",
+ "(1, 11) -> (1, 9)",
+ "(2, 2) -> (1, 10)",
+ "(2, 7) -> (1, 15)",
+ "(2, 8) -> (1, 16)",
+ "(3, 0) -> (1, 17)",
+]
+`;
+
+exports[`for..of loop 1`] = `
+"for (const x of [
+ 1,
+ 2,
+ 3
+]) {
+ console.log(x)
+}
+"
+`;
+
+exports[`for..of loop 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 5) -> (1, 5)",
+ "(1, 11) -> (1, 11)",
+ "(1, 13) -> (1, 13)",
+ "(1, 16) -> (1, 16)",
+ "(2, 2) -> (1, 17)",
+ "(2, 3) -> (1, 18)",
+ "(3, 2) -> (1, 19)",
+ "(3, 3) -> (1, 20)",
+ "(4, 2) -> (1, 21)",
+ "(5, 0) -> (1, 22)",
+ "(5, 1) -> (1, 23)",
+ "(5, 3) -> (1, 25)",
+ "(6, 2) -> (1, 27)",
+ "(6, 9) -> (1, 34)",
+ "(6, 10) -> (1, 35)",
+ "(6, 13) -> (1, 38)",
+ "(6, 14) -> (1, 39)",
+ "(6, 15) -> (1, 40)",
+ "(7, 0) -> (1, 42)",
+]
+`;
diff --git a/devtools/client/debugger/src/workers/pretty-print/tests/prettyFast.spec.js b/devtools/client/debugger/src/workers/pretty-print/tests/prettyFast.spec.js
new file mode 100644
index 0000000000..b9c19093ad
--- /dev/null
+++ b/devtools/client/debugger/src/workers/pretty-print/tests/prettyFast.spec.js
@@ -0,0 +1,435 @@
+/* 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/>. */
+
+/*
+ * Copyright 2013 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE.md or:
+ * http://opensource.org/licenses/BSD-2-Clause
+ */
+import { prettyFast } from "../pretty-fast";
+import { SourceMapConsumer } from "devtools/client/shared/vendor/source-map/source-map";
+
+const cases = [
+ {
+ name: "Simple function",
+ input: "function foo() { bar(); }",
+ },
+ {
+ name: "Nested function",
+ input: "function foo() { function bar() { debugger; } bar(); }",
+ },
+ {
+ name: "Immediately invoked function expression",
+ input: "(function(){thingy()}())",
+ },
+ {
+ name: "Single line comment",
+ input: `
+ // Comment
+ function foo() { bar(); }`,
+ },
+ {
+ name: "Multi line comment",
+ input: `
+ /* Comment
+ more comment */
+ function foo() { bar(); }
+ `,
+ },
+ { name: "Null assignment", input: "var i=null;" },
+ { name: "Undefined assignment", input: "var i=undefined;" },
+ { name: "Void 0 assignment", input: "var i=void 0;" },
+ {
+ name: "This property access",
+ input: "var foo=this.foo;\n",
+ },
+
+ {
+ name: "True assignment",
+ input: "var foo=true;\n",
+ },
+
+ {
+ name: "False assignment",
+ input: "var foo=false;\n",
+ },
+
+ {
+ name: "For loop",
+ input: "for (var i = 0; i < n; i++) { console.log(i); }",
+ },
+
+ {
+ name: "for..of loop",
+ input: "for (const x of [1,2,3]) { console.log(x) }",
+ },
+
+ {
+ name: "String with semicolon",
+ input: "var foo = ';';\n",
+ },
+
+ {
+ name: "String with quote",
+ input: 'var foo = "\'";\n',
+ },
+
+ {
+ name: "Function calls",
+ input: "var result=func(a,b,c,d);",
+ },
+
+ {
+ name: "Regexp",
+ input: "var r=/foobar/g;",
+ },
+
+ {
+ name: "In operator",
+ input: "if(foo in bar){doThing()}",
+ output: "if (foo in bar) {\n" + " doThing()\n" + "}\n",
+ },
+ {
+ name: "With statement",
+ input: "with(obj){crock()}",
+ },
+ {
+ name: "New expression",
+ input: "var foo=new Foo();",
+ },
+ {
+ name: "Continue/break statements",
+ input: "while(1){if(x){continue}if(y){break}if(z){break foo}}",
+ },
+ {
+ name: "Instanceof",
+ input: "var a=x instanceof y;",
+ },
+ {
+ name: "Binary operators",
+ input: "var a=5*30;var b=5>>3;",
+ },
+ {
+ name: "Delete",
+ input: "delete obj.prop;",
+ },
+
+ {
+ name: "Try/catch/finally statement",
+ input: "try{dangerous()}catch(e){handle(e)}finally{cleanup()}",
+ },
+ {
+ name: "If/else statement",
+ input: "if(c){then()}else{other()}",
+ },
+ {
+ name: "If/else without curlies",
+ input: "if(c) a else b",
+ },
+ {
+ name: "Objects",
+ input: `
+ var o={a:1,
+ b:2};`,
+ },
+ {
+ name: "Do/while loop",
+ input: "do{x}while(y)",
+ },
+ {
+ name: "Arrays",
+ input: "var a=[1,2,3];",
+ },
+ {
+ name: "Arrays and spread operator",
+ input: "var a=[1,...[2,3],...[], 4];",
+ },
+ {
+ name: "Empty object/array literals",
+ input: `let a=[];const b={};c={...{},d: 42};for(let x of []){for(let y in {}){}}`,
+ },
+ {
+ name: "Code that relies on ASI",
+ input: `
+ var foo = 10
+ var bar = 20
+ function g() {
+ a()
+ b()
+ }`,
+ },
+ {
+ name: "Ternary operator",
+ input: "bar?baz:bang;",
+ },
+ {
+ name: "Switch statements",
+ input: "switch(x){case a:foo();break;default:bar()}",
+ },
+
+ {
+ name: "Multiple single line comments",
+ input: `function f() {
+ // a
+ // b
+ // c
+ }`,
+ },
+ {
+ name: "Indented multiline comment",
+ input: `function foo() {
+ /**
+ * java doc style comment
+ * more comment
+ */
+ bar();
+ }`,
+ },
+ {
+ name: "ASI return",
+ input: `function f() {
+ return
+ {}
+ }`,
+ },
+ {
+ name: "Non-ASI property access",
+ input: `[1,2,3]
+ [0]`,
+ },
+ {
+ name: "Non-ASI in",
+ input: `'x'
+ in foo`,
+ },
+
+ {
+ name: "Non-ASI function call",
+ input: `f
+ ()`,
+ },
+ {
+ name: "Non-ASI new",
+ input: `new
+ F()`,
+ },
+ {
+ name: "Getter and setter literals",
+ input: "var obj={get foo(){return this._foo},set foo(v){this._foo=v}}",
+ },
+ {
+ name: "Escaping backslashes in strings",
+ input: "'\\\\'\n",
+ },
+ {
+ name: "Escaping carriage return in strings",
+ input: "'\\r'\n",
+ },
+ {
+ name: "Escaping tab in strings",
+ input: "'\\t'\n",
+ },
+ {
+ name: "Escaping vertical tab in strings",
+ input: "'\\v'\n",
+ },
+ {
+ name: "Escaping form feed in strings",
+ input: "'\\f'\n",
+ },
+ {
+ name: "Escaping null character in strings",
+ input: "'\\0'\n",
+ },
+ {
+ name: "Bug 977082 - space between grouping operator and dot notation",
+ input: `JSON.stringify(3).length;
+ ([1,2,3]).length;
+ (new Date()).toLocaleString();`,
+ },
+ {
+ name: "Bug 975477 don't move end of line comments to next line",
+ input: `switch (request.action) {
+ case 'show': //$NON-NLS-0$
+ if (localStorage.hideicon !== 'true') { //$NON-NLS-0$
+ chrome.pageAction.show(sender.tab.id);
+ }
+ break;
+ case 'hide': /*Multiline
+ Comment */
+ break;
+ default:
+ console.warn('unknown request'); //$NON-NLS-0$
+ // don't respond if you don't understand the message.
+ return;
+ }`,
+ },
+ {
+ name: "Const handling",
+ input: "const d = 'yes';\n",
+ },
+ {
+ name: "Let handling without value",
+ input: "let d;\n",
+ },
+ {
+ name: "Let handling with value",
+ input: "let d = 'yes';\n",
+ },
+ {
+ name: "Template literals",
+ // issue in acorn
+ input: "`abc${JSON.stringify({clas: 'testing'})}def`;{a();}",
+ },
+ {
+ name: "Class handling",
+ input: "class Class{constructor(){}}",
+ },
+ {
+ name: "Subclass handling",
+ input: "class Class extends Base{constructor(){}}",
+ },
+ {
+ name: "Unnamed class handling",
+ input: "let unnamed=class{constructor(){}}",
+ },
+ {
+ name: "Named class handling",
+ input: "let unnamed=class Class{constructor(){}}",
+ },
+ {
+ name: "Class extension within a function",
+ input: "(function() { class X extends Y { constructor() {} } })()",
+ },
+ {
+ name: "Bug 1261971 - indentation after switch statement",
+ input: "{switch(x){}if(y){}done();}",
+ },
+ {
+ name: "Bug 1206633 - spaces in for of",
+ input: "for (let tab of tabs) {}",
+ },
+ {
+ name: "Bug pretty-sure-3 - escaping line and paragraph separators",
+ input: "x = '\\u2029\\u2028';",
+ },
+ {
+ name: "Bug pretty-sure-4 - escaping null character before digit",
+ input: "x = '\\u00001';",
+ },
+ {
+ name: "Bug pretty-sure-5 - empty multiline comment shouldn't throw exception",
+ input: `{
+ /*
+ */
+ return;
+ }`,
+ },
+ {
+ name: "Bug pretty-sure-6 - inline comment shouldn't move parenthesis to next line",
+ input: `return /* inline comment */ (
+ 1+1);`,
+ },
+ {
+ name: "Bug pretty-sure-7 - accessing a literal number property requires a space",
+ input: "0..toString()+x.toString();",
+ },
+ {
+ name: "Bug pretty-sure-8 - return and yield only accept arguments when on the same line",
+ input: `{
+ return
+ (x)
+ yield
+ (x)
+ yield
+ *x
+ }`,
+ },
+ {
+ name: "Bug pretty-sure-9 - accept unary operator at start of file",
+ input: "+ 0",
+ },
+ {
+ name: "Stack-keyword property access",
+ input: "foo.a=1.1;foo.do.switch.case.default=2.2;foo.b=3.3;\n",
+ },
+ {
+ name: "Dot handling with let which is identifier name",
+ input: "y.let.let = 1.23;\n",
+ },
+ {
+ name: "Dot handling with keywords which are identifier name",
+ input: "y.await.break.const.delete.else.return.new.yield = 1.23;\n",
+ },
+ {
+ name: "Optional chaining parsing support",
+ input: "x?.y?.z?.['a']?.check();\n",
+ },
+ {
+ name: "Private fields parsing support",
+ input: `
+ class MyClass {
+ constructor(a) {
+ this.#a = a;this.#b = Math.random();this.ab = this.#getAB();
+ }
+ #a
+ #b = "default value"
+ static #someStaticPrivate
+ #getA() {
+ return this.#a;
+ }
+ #getAB() {
+ return this.#getA()+this.
+ #b
+ }
+ }
+ `,
+ },
+ {
+ name: "Long parenthesis",
+ input: `
+ if (thisIsAVeryLongVariable && thisIsAnotherOne || yetAnotherVeryLongVariable) {
+ (thisIsAnotherOne = thisMayReturnNull() || "hi", thisIsAVeryLongVariable = 42, yetAnotherVeryLongVariable && doSomething(true /* do it well */,thisIsAVeryLongVariable, thisIsAnotherOne, yetAnotherVeryLongVariable))
+ }
+ for (let thisIsAnotherVeryLongVariable = 0; i < thisIsAnotherVeryLongVariable.length; thisIsAnotherVeryLongVariable++) {}
+ const x = ({thisIsAnotherVeryLongPropertyName: "but should not cause the paren to be a line delimiter"})
+ `,
+ },
+ {
+ name: "Fat arrow function",
+ input: `
+ const a = () => 42
+ addEventListener("click", e => { return false });
+ const sum = (c,d) => c+d
+ `,
+ },
+];
+
+const includesOnly = cases.find(({ only }) => only);
+
+for (const { name, input, only, skip } of cases) {
+ if ((includesOnly && !only) || skip) {
+ continue;
+ }
+ // Wrapping name into a string to avoid jest/valid-title failures
+ test(`${name}`, async () => {
+ const actual = prettyFast(input, {
+ indent: " ",
+ url: "test.js",
+ });
+
+ expect(actual.code).toMatchSnapshot();
+
+ const smc = await new SourceMapConsumer(actual.map.toJSON());
+ const mappings = [];
+ smc.eachMapping(
+ ({ generatedColumn, generatedLine, originalColumn, originalLine }) => {
+ mappings.push(
+ `(${originalLine}, ${originalColumn}) -> (${generatedLine}, ${generatedColumn})`
+ );
+ }
+ );
+ expect(mappings).toMatchSnapshot();
+ });
+}
diff --git a/devtools/client/debugger/src/workers/pretty-print/worker.js b/devtools/client/debugger/src/workers/pretty-print/worker.js
new file mode 100644
index 0000000000..1c5587db9f
--- /dev/null
+++ b/devtools/client/debugger/src/workers/pretty-print/worker.js
@@ -0,0 +1,98 @@
+/* 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 { workerHandler } from "../../../../shared/worker-utils";
+import { prettyFast } from "./pretty-fast";
+
+var { SourceMapGenerator } = require("source-map");
+
+const sourceMapGeneratorByTaskId = new Map();
+
+function prettyPrint({ url, indent, sourceText }) {
+ const { code, map: sourceMapGenerator } = prettyFast(sourceText, {
+ url,
+ indent,
+ });
+
+ return {
+ code,
+ sourceMap: sourceMapGenerator.toJSON(),
+ };
+}
+
+function prettyPrintInlineScript({
+ taskId,
+ url,
+ indent,
+ sourceText,
+ originalStartLine,
+ originalStartColumn,
+ generatedStartLine,
+}) {
+ let taskSourceMapGenerator;
+ if (!sourceMapGeneratorByTaskId.has(taskId)) {
+ taskSourceMapGenerator = new SourceMapGenerator({ file: url });
+ sourceMapGeneratorByTaskId.set(taskId, taskSourceMapGenerator);
+ } else {
+ taskSourceMapGenerator = sourceMapGeneratorByTaskId.get(taskId);
+ }
+
+ const { code } = prettyFast(sourceText, {
+ url,
+ indent,
+ sourceMapGenerator: taskSourceMapGenerator,
+ /*
+ * By default prettyPrint will trim the text, and we'd have the pretty text displayed
+ * just after the script tag, e.g.:
+ *
+ * ```
+ * <script>if (true) {
+ * something()
+ * }
+ * </script>
+ * ```
+ *
+ * We want the text to start on a new line, so prepend a line break, so we get
+ * something like:
+ *
+ * ```
+ * <script>
+ * if (true) {
+ * something()
+ * }
+ * </script>
+ * ```
+ */
+ prefixWithNewLine: true,
+ originalStartLine,
+ originalStartColumn,
+ generatedStartLine,
+ });
+
+ // When a taskId was passed, we only return the pretty printed text.
+ // The source map should be retrieved with getSourceMapForTask.
+ return code;
+}
+
+/**
+ * Get the source map for a pretty-print task
+ *
+ * @param {Integer} taskId: The taskId that was used to call prettyPrint
+ * @returns {Object} A source map object
+ */
+function getSourceMapForTask(taskId) {
+ if (!sourceMapGeneratorByTaskId.has(taskId)) {
+ return null;
+ }
+
+ const taskSourceMapGenerator = sourceMapGeneratorByTaskId.get(taskId);
+ sourceMapGeneratorByTaskId.delete(taskId);
+ return taskSourceMapGenerator.toJSON();
+}
+
+self.onmessage = workerHandler({
+ prettyPrint,
+ prettyPrintInlineScript,
+ getSourceMapForTask,
+});
diff --git a/devtools/client/debugger/src/workers/search/get-matches.js b/devtools/client/debugger/src/workers/search/get-matches.js
new file mode 100644
index 0000000000..972dea6818
--- /dev/null
+++ b/devtools/client/debugger/src/workers/search/get-matches.js
@@ -0,0 +1,45 @@
+/* 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 assert from "../../utils/assert";
+import buildQuery from "../../utils/build-query";
+
+export default function getMatches(query, text, options) {
+ if (!query || !text || !options) {
+ return [];
+ }
+ const regexQuery = buildQuery(query, options, {
+ isGlobal: true,
+ });
+ const matchedLocations = [];
+ const lines = text.split("\n");
+ for (let i = 0; i < lines.length; i++) {
+ let singleMatch;
+ const line = lines[i];
+ while ((singleMatch = regexQuery.exec(line)) !== null) {
+ // Flow doesn't understand the test above.
+ if (!singleMatch) {
+ throw new Error("no singleMatch");
+ }
+
+ matchedLocations.push({
+ line: i,
+ ch: singleMatch.index,
+ match: singleMatch[0],
+ });
+
+ // When the match is an empty string the regexQuery.lastIndex will not
+ // change resulting in an infinite loop so we need to check for this and
+ // increment it manually in that case. See issue #7023
+ if (singleMatch[0] === "") {
+ assert(
+ !regexQuery.unicode,
+ "lastIndex++ can cause issues in unicode mode"
+ );
+ regexQuery.lastIndex++;
+ }
+ }
+ }
+ return matchedLocations;
+}
diff --git a/devtools/client/debugger/src/workers/search/index.js b/devtools/client/debugger/src/workers/search/index.js
new file mode 100644
index 0000000000..928bbf50e6
--- /dev/null
+++ b/devtools/client/debugger/src/workers/search/index.js
@@ -0,0 +1,17 @@
+/* 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 { WorkerDispatcher } from "devtools/client/shared/worker-utils";
+
+const WORKER_URL = "resource://devtools/client/debugger/dist/search-worker.js";
+
+export class SearchDispatcher extends WorkerDispatcher {
+ constructor(jestUrl) {
+ super(jestUrl || WORKER_URL);
+ }
+
+ getMatches = this.task("getMatches");
+
+ findSourceMatches = this.task("findSourceMatches");
+}
diff --git a/devtools/client/debugger/src/workers/search/moz.build b/devtools/client/debugger/src/workers/search/moz.build
new file mode 100644
index 0000000000..b7223ac81a
--- /dev/null
+++ b/devtools/client/debugger/src/workers/search/moz.build
@@ -0,0 +1,10 @@
+# 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(
+ "index.js",
+)
diff --git a/devtools/client/debugger/src/workers/search/project-search.js b/devtools/client/debugger/src/workers/search/project-search.js
new file mode 100644
index 0000000000..f3751b57c4
--- /dev/null
+++ b/devtools/client/debugger/src/workers/search/project-search.js
@@ -0,0 +1,70 @@
+/* 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/>. */
+
+// Maybe reuse file search's functions?
+
+import getMatches from "./get-matches";
+
+export function findSourceMatches(content, queryText, options) {
+ if (queryText == "") {
+ return [];
+ }
+
+ const text = content.value;
+ const lines = text.split("\n");
+
+ return getMatches(queryText, text, options).map(({ line, ch, match }) => {
+ const { value, matchIndex } = truncateLine(lines[line], ch);
+ return {
+ line: line + 1,
+ column: ch,
+
+ matchIndex,
+ match,
+ value,
+ };
+ });
+}
+
+// This is used to find start of a word, so that cropped string look nice
+const startRegex = /([ !@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?])/g;
+// Similarly, find
+const endRegex = new RegExp(
+ [
+ "([ !@#$%^&*()_+-=[]{};':\"\\|,.<>/?])",
+ '[^ !@#$%^&*()_+-=[]{};\':"\\|,.<>/?]*$"/',
+ ].join("")
+);
+// For texts over 100 characters this truncates the text (for display)
+// around the context of the matched text.
+function truncateLine(text, column) {
+ if (text.length < 100) {
+ return {
+ matchIndex: column,
+ value: text,
+ };
+ }
+
+ // Initially take 40 chars left to the match
+ const offset = Math.max(column - 40, 0);
+ // 400 characters should be enough to figure out the context of the match
+ const truncStr = text.slice(offset, column + 400);
+ let start = truncStr.search(startRegex);
+ let end = truncStr.search(endRegex);
+
+ if (start > column) {
+ // No word separator found before the match, so we take all characters
+ // before the match
+ start = -1;
+ }
+ if (end < column) {
+ end = truncStr.length;
+ }
+ const value = truncStr.slice(start + 1, end);
+
+ return {
+ matchIndex: column - start - offset - 1,
+ value,
+ };
+}
diff --git a/devtools/client/debugger/src/workers/search/tests/get-matches.spec.js b/devtools/client/debugger/src/workers/search/tests/get-matches.spec.js
new file mode 100644
index 0000000000..30b0ce316d
--- /dev/null
+++ b/devtools/client/debugger/src/workers/search/tests/get-matches.spec.js
@@ -0,0 +1,99 @@
+/* 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 getMatches from "../get-matches";
+
+describe("search", () => {
+ describe("getMatches", () => {
+ it("gets basic string match", () => {
+ const text = "the test string with test in it multiple times test.";
+ const query = "test";
+ const matchLocations = getMatches(query, text, {
+ caseSensitive: true,
+ wholeWord: false,
+ regexMatch: false,
+ });
+ expect(matchLocations).toHaveLength(3);
+ });
+
+ it("gets basic string match case-sensitive", () => {
+ const text = "the Test string with test in it multiple times test.";
+ const query = "Test";
+ const matchLocations = getMatches(query, text, {
+ caseSensitive: true,
+ wholeWord: false,
+ regexMatch: false,
+ });
+ expect(matchLocations).toHaveLength(1);
+ });
+
+ it("gets whole word string match", () => {
+ const text = "the test string test in it multiple times whoatestthe.";
+ const query = "test";
+ const matchLocations = getMatches(query, text, {
+ caseSensitive: true,
+ wholeWord: true,
+ regexMatch: false,
+ });
+ expect(matchLocations).toHaveLength(2);
+ });
+
+ it("gets regex match", () => {
+ const text = "the test string test in it multiple times whoatestthe.";
+ const query = "(\\w+)\\s+(\\w+)";
+ const matchLocations = getMatches(query, text, {
+ caseSensitive: true,
+ wholeWord: false,
+ regexMatch: true,
+ });
+ expect(matchLocations).toHaveLength(4);
+ });
+
+ it("doesnt fail on empty data", () => {
+ const text = "";
+ const query = "";
+ const matchLocations = getMatches(query, text, {
+ caseSensitive: true,
+ wholeWord: false,
+ regexMatch: true,
+ });
+ expect(matchLocations).toHaveLength(0);
+ });
+
+ it("fails gracefully when the line is too long", () => {
+ const text = Array(100002).join("x");
+ const query = "query";
+ const matchLocations = getMatches(query, text, {
+ caseSensitive: true,
+ wholeWord: false,
+ regexMatch: true,
+ });
+ expect(matchLocations).toHaveLength(0);
+ });
+
+ // regression test for #6896
+ it("doesn't crash on the regex 'a*'", () => {
+ const text = "abc";
+ const query = "a*";
+ const matchLocations = getMatches(query, text, {
+ caseSensitive: true,
+ wholeWord: false,
+ regexMatch: true,
+ });
+ expect(matchLocations).toHaveLength(4);
+ });
+
+ // regression test for #6896
+ it("doesn't crash on the regex '^'", () => {
+ const text = "012";
+ const query = "^";
+ const matchLocations = getMatches(query, text, {
+ caseSensitive: true,
+ wholeWord: false,
+ regexMatch: true,
+ });
+ expect(matchLocations).toHaveLength(1);
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/workers/search/worker.js b/devtools/client/debugger/src/workers/search/worker.js
new file mode 100644
index 0000000000..b452697516
--- /dev/null
+++ b/devtools/client/debugger/src/workers/search/worker.js
@@ -0,0 +1,9 @@
+/* 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 getMatches from "./get-matches";
+import { findSourceMatches } from "./project-search";
+import { workerHandler } from "../../../../shared/worker-utils";
+
+self.onmessage = workerHandler({ getMatches, findSourceMatches });