Bug 1585692 - Adds functionality for marking function calls.

Identifying the function calls and marking them is the first step for adding the feature 'command click should jump to function call'.

Steps to see the function call marks:

1. Pause the debugger.
2. Press command key. (Function calls are highlighted in blue).

Differential Revision: https://phabricator.services.mozilla.com/D52390

--HG--
extra : moz-landing-system : lando
This commit is contained in:
janelledement 2020-02-10 13:51:14 +00:00
Родитель 7c81402e17
Коммит 417834dd2b
20 изменённых файлов: 406 добавлений и 8 удалений

Просмотреть файл

@ -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/>. */
// @flow
import { getSymbols, getSource } from "../../selectors";
import type { ThreadContext, Frame } from "../../types";
import type { ThunkArgs } from "../types";
// a is an ast location with start and end positions (line and column).
// b is a single position (line and column).
// This function tests to see if the b position
// falls within the range given in a.
function inHouseContainsPosition(a: Object, b: Object) {
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 highlightCalls(cx: ThreadContext, frame: ?Frame) {
return async function({ dispatch, getState, parser, client }: ThunkArgs) {
if (!frame || !cx) {
return;
}
const { thread } = cx;
const originalAstScopes = await parser.getScopes(frame.location);
if (!originalAstScopes) {
return;
}
const source = getSource(getState(), frame.location.sourceId);
if (!source) {
return;
}
const symbols = getSymbols(getState(), source);
if (!symbols || symbols.loading) {
return;
}
if (!symbols.callExpressions) {
return;
}
const localAstScope = originalAstScopes[0];
const allFunctionCalls = symbols.callExpressions;
const highlightedCalls = allFunctionCalls.filter(function(call) {
const containsStart = inHouseContainsPosition(
localAstScope,
call.location.start
);
const containsEnd = inHouseContainsPosition(
localAstScope,
call.location.end
);
return containsStart && containsEnd;
});
return dispatch({
type: "HIGHLIGHT_CALLS",
thread,
highlightedCalls,
});
};
}
export function unhighlightCalls(cx: ThreadContext) {
return async function({ dispatch, getState, parser, client }: ThunkArgs) {
const { thread } = cx;
return dispatch({
type: "UNHIGHLIGHT_CALLS",
thread,
});
};
}

Просмотреть файл

@ -32,6 +32,7 @@ export { toggleSkipPausing, setSkipPausing } from "./skipPausing";
export { toggleMapScopes } from "./mapScopes";
export { setExpandedScope } from "./expandScopes";
export { generateInlinePreview } from "./inlinePreview";
export { highlightCalls, unhighlightCalls } from "./highlightCalls";
export {
previewPausedLocation,
clearPreviewPausedLocation,

Просмотреть файл

@ -24,5 +24,6 @@ CompiledModules(
'resumed.js',
'selectFrame.js',
'setFramePositions.js',
'highlightCalls.js',
'skipPausing.js',
)

Просмотреть файл

@ -12,6 +12,7 @@ import type {
Why,
ThreadContext,
Previews,
HighlightedCalls,
ExecutionPoint,
} from "../../types";
@ -166,6 +167,13 @@ export type PauseAction =
+frame: Frame,
+previews: Previews,
|}
| {|
+type: "HIGHLIGHT_CALLS",
+highlightedCalls: HighlightedCalls,
|}
| {|
+type: "UNHIGHLIGHT_CALLS",
|}
| {|
+type: "SET_FRAME_POSITIONS",
+frame: Frame,

Просмотреть файл

@ -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/>. */
.highlight-function-calls {
background-color: rgba(202, 227, 255, 0.5);
}
.theme-dark .highlight-function-calls {
background-color: #743884;
}
.highlight-function-calls:hover {
cursor: default;
}

Просмотреть файл

@ -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/>. */
// @flow
import { Component } from "react";
import { connect } from "../../utils/connect";
import {
getHighlightedCalls,
getThreadContext,
getCurrentThread,
} from "../../selectors";
import { getSourceLocationFromMouseEvent } from "../../utils/editor";
import actions from "../../actions";
import "./HighlightCalls.css";
import type {
ThreadContext,
SourceWithContent,
HighlightedCalls as HighlightedCallsType,
HighlightedCall,
} from "../../types";
type OwnProps = {|
editor: Object,
selectedSource: ?SourceWithContent,
|};
type Props = {
editor: Object,
highlightedCalls: ?HighlightedCallsType,
cx: ThreadContext,
selectedSource: ?SourceWithContent,
continueToHere: typeof actions.continueToHere,
};
export class HighlightCalls extends Component<Props> {
previousCalls: HighlightedCallsType | null = null;
componentDidUpdate() {
this.unhighlightFunctionCalls();
this.highlightFunctioCalls();
}
markCall = (call: HighlightedCall) => {
const { editor } = this.props;
const startLine = call.location.start.line - 1;
const endLine = call.location.end.line - 1;
const startColumn = call.location.start.column;
const endColumn = call.location.end.column;
const markedCall = editor.codeMirror.markText(
{ line: startLine, ch: startColumn },
{ line: endLine, ch: endColumn },
{ className: "highlight-function-calls" }
);
return markedCall;
};
onClick = (e: MouseEvent) => {
const { editor, selectedSource, cx, continueToHere } = this.props;
if (selectedSource) {
const location = getSourceLocationFromMouseEvent(
editor,
selectedSource,
e
);
continueToHere(cx, location);
editor.codeMirror.execCommand("singleSelection");
editor.codeMirror.execCommand("goGroupLeft");
}
};
highlightFunctioCalls() {
const { highlightedCalls } = this.props;
if (!highlightedCalls) {
return;
}
let markedCalls = [];
markedCalls = highlightedCalls.map(this.markCall);
const allMarkedElements = document.getElementsByClassName(
"highlight-function-calls"
);
for (let i = 0; i < allMarkedElements.length; i++) {
allMarkedElements[i].addEventListener("click", this.onClick);
}
this.previousCalls = markedCalls;
}
unhighlightFunctionCalls() {
if (!this.previousCalls) {
return;
}
this.previousCalls.forEach(call => call.clear());
this.previousCalls = null;
}
render() {
return null;
}
}
const mapStateToProps = state => {
const thread = getCurrentThread(state);
return {
highlightedCalls: getHighlightedCalls(state, thread),
cx: getThreadContext(state),
};
};
const { continueToHere } = actions;
const mapDispatchToProps = { continueToHere };
export default connect<Props, OwnProps, _, _, _, _>(
mapStateToProps,
mapDispatchToProps
)(HighlightCalls);

Просмотреть файл

@ -9,10 +9,15 @@ import { connect } from "../../../utils/connect";
import Popup from "./Popup";
import { getPreview, getThreadContext } from "../../../selectors";
import {
getPreview,
getThreadContext,
getCurrentThread,
getHighlightedCalls,
} from "../../../selectors";
import actions from "../../../actions";
import type { ThreadContext } from "../../../types";
import type { ThreadContext, HighlightedCalls } from "../../../types";
import type { Preview as PreviewType } from "../../../reducers/types";
@ -24,6 +29,7 @@ type Props = {
cx: ThreadContext,
editor: any,
editorRef: ?HTMLDivElement,
highlightedCalls: ?HighlightedCalls,
preview: ?PreviewType,
clearPreview: typeof actions.clearPreview,
addExpression: typeof actions.addExpression,
@ -65,9 +71,9 @@ class Preview extends PureComponent<Props, State> {
}
onTokenEnter = ({ target, tokenPos }: any) => {
const { cx, editor, updatePreview } = this.props;
const { cx, editor, updatePreview, highlightedCalls } = this.props;
if (cx.isPaused && !this.state.selecting) {
if (cx.isPaused && !this.state.selecting && highlightedCalls === null) {
updatePreview(cx, target, tokenPos, editor.codeMirror);
}
};
@ -108,10 +114,14 @@ class Preview extends PureComponent<Props, State> {
}
}
const mapStateToProps = state => ({
cx: getThreadContext(state),
preview: getPreview(state),
});
const mapStateToProps = state => {
const thread = getCurrentThread(state);
return {
highlightedCalls: getHighlightedCalls(state, thread),
cx: getThreadContext(state),
preview: getPreview(state),
};
};
export default connect<Props, OwnProps, _, _, _, _>(
mapStateToProps,

Просмотреть файл

@ -39,6 +39,8 @@ import {
getThreadContext,
getSkipPausing,
getInlinePreview,
getSelectedFrame,
getHighlightedCalls,
} from "../../selectors";
// Redux actions
@ -55,6 +57,7 @@ import EmptyLines from "./EmptyLines";
import EditorMenu from "./EditorMenu";
import ConditionalPanel from "./ConditionalPanel";
import InlinePreviews from "./InlinePreviews";
import HighlightCalls from "./HighlightCalls";
import {
showSourceText,
@ -88,6 +91,8 @@ import type {
SourceLocation,
SourceWithContent,
ThreadContext,
Frame,
HighlightedCalls as highlightedCallsType,
} from "../../types";
const cssVars = {
@ -110,6 +115,8 @@ export type Props = {
isPaused: boolean,
skipPausing: boolean,
inlinePreviewEnabled: boolean,
selectedFrame: ?Frame,
highlightedCalls: ?highlightedCallsType,
// Actions
openConditionalPanel: typeof actions.openConditionalPanel,
@ -125,6 +132,8 @@ export type Props = {
breakpointActions: BreakpointItemActions,
editorActions: EditorItemActions,
toggleBlackBox: typeof actions.toggleBlackBox,
highlightCalls: typeof actions.highlightCalls,
unhighlightCalls: typeof actions.unhighlightCalls,
};
type State = {
@ -181,6 +190,11 @@ class Editor extends PureComponent<Props, State> {
codeMirror.on("gutterClick", this.onGutterClick);
if (features.commandClick) {
document.addEventListener("keydown", this.commandKeyDown);
document.addEventListener("keyup", this.commandKeyUp);
}
// Set code editor wrapper to be focusable
codeMirrorWrapper.tabIndex = 0;
codeMirrorWrapper.addEventListener("keydown", e => this.onKeyDown(e));
@ -321,6 +335,22 @@ class Editor extends PureComponent<Props, State> {
onEditorScroll = debounce(this.props.updateViewport, 75);
commandKeyDown = (e: KeyboardEvent) => {
const { key } = e;
if (this.props.isPaused && key === "Meta") {
const { cx, selectedFrame, highlightCalls } = this.props;
highlightCalls(cx, selectedFrame);
}
};
commandKeyUp = (e: KeyboardEvent) => {
const { key } = e;
if (key === "Meta") {
const { cx, unhighlightCalls } = this.props;
unhighlightCalls(cx);
}
};
onKeyDown(e: KeyboardEvent) {
const { codeMirror } = this.state.editor;
const { key, target } = e;
@ -618,6 +648,7 @@ class Editor extends PureComponent<Props, State> {
return (
<div>
<HighlightCalls editor={editor} selectedSource={selectedSource} />
<DebugLine />
<HighlightLine />
<EmptyLines editor={editor} />
@ -691,6 +722,8 @@ const mapStateToProps = state => {
isPaused: getIsPaused(state, getCurrentThread(state)),
skipPausing: getSkipPausing(state),
inlinePreviewEnabled: getInlinePreview(state),
selectedFrame: getSelectedFrame(state, getCurrentThread(state)),
highlightedCalls: getHighlightedCalls(state, getCurrentThread(state)),
};
};
@ -708,6 +741,8 @@ const mapDispatchToProps = dispatch => ({
updateCursorPosition: actions.updateCursorPosition,
closeTab: actions.closeTab,
toggleBlackBox: actions.toggleBlackBox,
highlightCalls: actions.highlightCalls,
unhighlightCalls: actions.unhighlightCalls,
},
dispatch
),

Просмотреть файл

@ -18,6 +18,7 @@ CompiledModules(
'EditorMenu.js',
'EmptyLines.js',
'Footer.js',
'HighlightCalls.js',
'HighlightLine.js',
'HighlightLines.js',
'index.js',
@ -34,6 +35,7 @@ DevToolsModules(
'ConditionalPanel.css',
'Editor.css',
'Footer.css',
'HighlightCalls.css',
'InlinePreview.css',
'Preview.css',
'SearchBar.css',

Просмотреть файл

@ -32,6 +32,7 @@
@import url("./components/Editor/InlinePreview.css");
@import url("./components/Editor/Preview.css");
@import url("./components/Editor/Preview/Popup.css");
@import url("./components/Editor/HighlightCalls.css");
@import url("./components/Editor/SearchBar.css");
@import url("./components/Editor/Tabs.css");
@import url("./components/PrimaryPanes/Outline.css");

Просмотреть файл

@ -31,6 +31,7 @@ import type {
Previews,
SourceLocation,
ExecutionPoint,
HighlightedCalls,
} from "../types";
export type Command =
@ -90,6 +91,7 @@ type ThreadPauseState = {
inlinePreview: {
[FrameId]: Object,
},
highlightedCalls: ?HighlightedCalls,
};
// Pause state describing all threads.
@ -116,6 +118,7 @@ function createPauseState(thread: ThreadId = "UnknownThread") {
pauseCounter: 0,
},
previewLocation: null,
highlightedCalls: null,
threads: {},
skipPausing: prefs.skipPausing,
mapScopes: prefs.mapScopes,
@ -135,6 +138,7 @@ const resumedPauseState = {
selectedFrameId: null,
why: null,
inlinePreview: {},
highlightedCalls: null,
};
const createInitialPauseState = () => ({
@ -406,6 +410,18 @@ function update(
},
});
}
case "HIGHLIGHT_CALLS": {
const { highlightedCalls } = action;
return updateThreadState({ ...threadState(), highlightedCalls });
}
case "UNHIGHLIGHT_CALLS": {
return updateThreadState({
...threadState(),
highlightedCalls: null,
});
}
}
return state;
@ -645,6 +661,10 @@ export function getSkipPausing(state: State) {
return state.pause.skipPausing;
}
export function getHighlightedCalls(state: State, thread: ThreadId) {
return getThreadPauseState(state.pause, thread).highlightedCalls;
}
export function isMapScopesEnabled(state: State) {
return state.pause.mapScopes;
}

Просмотреть файл

@ -8,6 +8,7 @@ import type { SettledValue, FulfilledValue } from "./utils/async-value";
import type { SourcePayload, LongStringFront } from "./client/firefox/types";
import type { SourceActorId, SourceActor } from "./reducers/source-actors";
import type { SourceBase } from "./reducers/sources";
import type { CallDeclaration } from "./workers/parser/getSymbols";
export type { SourceActorId, SourceActor, SourceBase };
@ -520,6 +521,9 @@ export type Previews = {
line: Array<Preview>,
};
export type HighlightedCall = CallDeclaration & { clear: Object };
export type HighlightedCalls = Array<HighlightedCall>;
export type Preview = {
name: string,
value: any,

Просмотреть файл

@ -59,6 +59,7 @@ if (isDevelopment()) {
pref("devtools.debugger.features.map-scopes", true);
pref("devtools.debugger.features.remove-command-bar-options", true);
pref("devtools.debugger.features.code-folding", false);
pref("devtools.debugger.features.command-click", false);
pref("devtools.debugger.features.outline", true);
pref("devtools.debugger.features.column-breakpoints", true);
pref("devtools.debugger.features.skip-pausing", true);
@ -140,6 +141,7 @@ export const features = new PrefsHelper("devtools.debugger.features", {
eventListenersBreakpoints: ["Bool", "event-listeners-breakpoints"],
domMutationBreakpoints: ["Bool", "dom-mutation-breakpoints"],
logPoints: ["Bool", "log-points"],
commandClick: ["Bool", "command-click"],
showOverlay: ["Bool", "overlay"],
inlinePreview: ["Bool", "inline-preview"],
watchpoints: ["Bool", "watchpoints"],

Просмотреть файл

@ -54,6 +54,7 @@ skip-if = !e10s || verify # This test is only valid in e10s
[browser_dbg-chrome-create.js]
skip-if = (verify && !debug && (os == 'linux'))
[browser_dbg-chrome-debugging.js]
[browser_dbg-command-click.js]
[browser_dbg-console.js]
[browser_dbg-console-async.js]
[browser_dbg-console-eval.js]
@ -77,6 +78,7 @@ skip-if = debug # Window leaks: bug 1575332
[browser_dbg-fission-switch-target.js]
skip-if = true # Landed disabled for intermittent issues on navigation. See Bug 1605743.
[browser_dbg-go-to-line.js]
[browser_dbg-highlights-calls.js]
[browser_dbg-html-breakpoints.js]
[browser_dbg-iframes.js]
[browser_dbg-inline-cache.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/>. */
// This test checks to see if holding down the command key and clicking on function call
// will jump the debugger to that call.
add_task(async function() {
await pushPref("devtools.debugger.features.command-click", true);
info("Checking to see if command click will jump the debugger to another highlighted call.");
const dbg = await initDebugger("doc-command-click.html", "simple4.js");
const source = findSource(dbg, "simple4.js");
await selectSource(dbg, source.url);
await waitForSelectedSource(dbg, source.url);
invokeInTab("funcA");
await waitForPaused(dbg);
await waitForInlinePreviews(dbg);
pressKey(dbg, "commandKeyDown");
await waitForDispatch(dbg, "HIGHLIGHT_CALLS");
const calls = dbg.win.document.querySelectorAll(".highlight-function-calls");
is(calls.length, 2);
const funcB = calls[0];
clickDOMElement(dbg, funcB);
await waitForDispatch(dbg, "RESUME");
await waitForPaused(dbg);
assertDebugLine(dbg, 3, 2);
const nocalls = dbg.win.document.querySelectorAll(".highlight-function-calls");
is(nocalls.length, 0);
});

Просмотреть файл

@ -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/>. */
// This test checks to see if command button highlights and unhighlights
// calls when debugger is paused.
add_task(async function() {
await pushPref("devtools.debugger.features.command-click", true);
const dbg = await initDebugger("doc-command-click.html", "simple4.js");
const source = findSource(dbg, "simple4.js");
await selectSource(dbg, source.url);
await waitForSelectedSource(dbg, source.url);
invokeInTab("funcA");
await waitForPaused(dbg);
await waitForInlinePreviews(dbg);
pressKey(dbg, "commandKeyDown");
await waitForDispatch(dbg, "HIGHLIGHT_CALLS");
const calls = dbg.win.document.querySelectorAll(".highlight-function-calls");
is(calls.length, 2);
pressKey(dbg, "commandKeyUp");
const nocalls = dbg.win.document.querySelectorAll(".highlight-function-calls");
is(nocalls.length, 0);
});

Просмотреть файл

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Command Click</title>
</head>
<body>
<script src="simple4.js"></script>
</body>
</html>

Просмотреть файл

@ -0,0 +1,15 @@
function funcA() {
debugger;
funcB();
funcC();
}
function funcB() {
console.log("Hello!");
}
function funcC() {
console.log("You made it!");
}
funcA();

Просмотреть файл

@ -1128,6 +1128,8 @@ const startKey = isMac
const keyMappings = {
close: { code: "w", modifiers: cmdOrCtrl },
commandKeyDown: {code: "VK_META", modifiers: {type: "keydown"}},
commandKeyUp: {code: "VK_META", modifiers: {type: "keyup"}},
debugger: { code: "s", modifiers: shiftOrAlt },
// test conditional panel shortcut
toggleCondPanel: { code: "b", modifiers: cmdShift },

Просмотреть файл

@ -72,6 +72,7 @@ pref("devtools.debugger.features.remove-command-bar-options", false);
pref("devtools.debugger.features.workers", true);
pref("devtools.debugger.features.code-coverage", false);
pref("devtools.debugger.features.code-folding", false);
pref("devtools.debugger.features.command-click", false);
pref("devtools.debugger.features.outline", true);
pref("devtools.debugger.features.component-pane", false);
pref("devtools.debugger.features.async-stepping", false);