summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/src/components/PrimaryPanes/Outline.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/debugger/src/components/PrimaryPanes/Outline.js')
-rw-r--r--devtools/client/debugger/src/components/PrimaryPanes/Outline.js376
1 files changed, 376 insertions, 0 deletions
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..3adb3a2fbc
--- /dev/null
+++ b/devtools/client/debugger/src/components/PrimaryPanes/Outline.js
@@ -0,0 +1,376 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 React, { Component } from "react";
+import { showMenu } from "../../context-menu/menu";
+import { connect } from "../../utils/connect";
+import { score as fuzzaldrinScore } from "fuzzaldrin-plus";
+const classnames = require("classnames");
+
+import { containsPosition, positionAfter } from "../../utils/ast";
+import { copyToTheClipboard } from "../../utils/clipboard";
+import { findFunctionText } from "../../utils/function";
+
+import actions from "../../actions";
+import {
+ getSelectedSourceWithContent,
+ getSymbols,
+ getCursorPosition,
+ getContext,
+} from "../../selectors";
+
+import OutlineFilter from "./OutlineFilter";
+import "./Outline.css";
+import PreviewFunction from "../shared/PreviewFunction";
+import { uniq, sortBy } from "lodash";
+
+import type { Symbols } from "../../reducers/ast";
+
+import type {
+ SymbolDeclaration,
+ FunctionDeclaration,
+} from "../../workers/parser";
+import type { SourceWithContent, Context, SourceLocation } from "../../types";
+
+type OwnProps = {|
+ alphabetizeOutline: boolean,
+ onAlphabetizeClick: Function,
+|};
+type Props = {
+ cx: Context,
+ symbols: ?Symbols,
+ selectedSource: ?SourceWithContent,
+ alphabetizeOutline: boolean,
+ onAlphabetizeClick: Function,
+ getFunctionText: Function,
+ cursorPosition: ?SourceLocation,
+ selectLocation: typeof actions.selectLocation,
+ flashLineRange: typeof actions.flashLineRange,
+};
+
+type State = {
+ filter: string,
+ focusedItem: ?SymbolDeclaration,
+};
+
+// 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: string, filter: string) => {
+ 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: HTMLLIElement, parent: HTMLElement) {
+ 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<Props, State> {
+ focusedElRef: ?React$ElementRef<"li">;
+
+ constructor(props: Props) {
+ super(props);
+ this.focusedElRef = null;
+ this.state = { filter: "", focusedItem: null };
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ const { cursorPosition, symbols } = this.props;
+ if (
+ cursorPosition &&
+ symbols &&
+ cursorPosition !== prevProps.cursorPosition
+ ) {
+ this.setFocus(cursorPosition);
+ }
+
+ if (
+ this.focusedElRef &&
+ !isVisible(this.focusedElRef, this.refs.outlineList)
+ ) {
+ this.focusedElRef.scrollIntoView({ block: "center" });
+ }
+ }
+
+ setFocus(cursorPosition: SourceLocation) {
+ const { symbols } = this.props;
+ let classes = [];
+ let functions = [];
+
+ if (symbols && !symbols.loading) {
+ ({ classes, functions } = symbols);
+ }
+
+ // Find items that enclose the selected location
+ const enclosedItems = [...classes, ...functions].filter(
+ ({ name, location }) =>
+ name != "anonymous" && containsPosition(location, cursorPosition)
+ );
+
+ if (enclosedItems.length == 0) {
+ return this.setState({ focusedItem: null });
+ }
+
+ // 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: ?SymbolDeclaration) {
+ const { cx, selectedSource, selectLocation } = this.props;
+ if (!selectedSource || !selectedItem) {
+ return;
+ }
+
+ selectLocation(cx, {
+ sourceId: selectedSource.id,
+ line: selectedItem.location.start.line,
+ column: selectedItem.location.start.column,
+ });
+
+ this.setState({ focusedItem: selectedItem });
+ }
+
+ onContextMenu(event: SyntheticEvent<HTMLElement>, func: SymbolDeclaration) {
+ event.stopPropagation();
+ event.preventDefault();
+
+ const { selectedSource, flashLineRange, getFunctionText } = this.props;
+
+ if (!selectedSource) {
+ return;
+ }
+
+ const sourceLine = func.location.start.line;
+ const functionText = getFunctionText(sourceLine);
+
+ const copyFunctionItem = {
+ id: "node-menu-copy-function",
+ label: L10N.getStr("copyFunction.label"),
+ accesskey: L10N.getStr("copyFunction.accesskey"),
+ disabled: !functionText,
+ click: () => {
+ flashLineRange({
+ start: sourceLine,
+ end: func.location.end.line,
+ sourceId: selectedSource.id,
+ });
+ return copyToTheClipboard(functionText);
+ },
+ };
+ const menuOptions = [copyFunctionItem];
+ showMenu(event, menuOptions);
+ }
+
+ updateFilter = (filter: string) => {
+ this.setState({ filter: filter.trim() });
+ };
+
+ renderPlaceholder() {
+ const placeholderMessage = this.props.selectedSource
+ ? L10N.getStr("outline.noFunctions")
+ : L10N.getStr("outline.noFileSelected");
+
+ return <div className="outline-pane-info">{placeholderMessage}</div>;
+ }
+
+ renderLoading() {
+ return (
+ <div className="outline-pane-info">{L10N.getStr("loadingText")}</div>
+ );
+ }
+
+ renderFunction(func: FunctionDeclaration) {
+ 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">λ</span>
+ <PreviewFunction func={{ name, parameterNames }} />
+ </li>
+ );
+ }
+
+ renderClassHeader(klass: string) {
+ return (
+ <div>
+ <span className="keyword">class</span> {klass}
+ </div>
+ );
+ }
+
+ renderClassFunctions(klass: ?string, functions: FunctionDeclaration[]) {
+ const { symbols } = this.props;
+
+ if (!symbols || symbols.loading || klass == null || functions.length == 0) {
+ 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)}
+ </h2>
+ <ul className="outline-list__class-list">
+ {classFunctions.map(func => this.renderFunction(func))}
+ </ul>
+ </li>
+ );
+ }
+
+ renderFunctions(functions: Array<FunctionDeclaration>) {
+ const { filter } = this.state;
+ let classes = uniq(functions.map(({ klass }) => klass));
+ let namedFunctions = functions.filter(
+ ({ name, klass }) =>
+ filterOutlineItem(name, filter) && !klass && !classes.includes(name)
+ );
+
+ let classFunctions = functions.filter(
+ ({ name, klass }) => filterOutlineItem(name, filter) && !!klass
+ );
+
+ if (this.props.alphabetizeOutline) {
+ namedFunctions = sortBy(namedFunctions, "name");
+ classes = classes.sort();
+ classFunctions = sortBy(classFunctions, "name");
+ }
+
+ return (
+ <ul
+ ref="outlineList"
+ className="outline-list devtools-monospace"
+ dir="ltr"
+ >
+ {namedFunctions.map(func => this.renderFunction(func))}
+ {classes.map(klass => this.renderClassFunctions(klass, classFunctions))}
+ </ul>
+ );
+ }
+
+ renderFooter() {
+ return (
+ <div className="outline-footer">
+ <button
+ onClick={this.props.onAlphabetizeClick}
+ className={this.props.alphabetizeOutline ? "active" : ""}
+ >
+ {L10N.getStr("outline.sortLabel")}
+ </button>
+ </div>
+ );
+ }
+
+ render() {
+ const { symbols, selectedSource } = this.props;
+ const { filter } = this.state;
+
+ if (!selectedSource) {
+ return this.renderPlaceholder();
+ }
+
+ if (!symbols || symbols.loading) {
+ return this.renderLoading();
+ }
+
+ const symbolsToDisplay = symbols.functions.filter(
+ ({ name }) => name != "anonymous"
+ );
+
+ if (symbolsToDisplay.length === 0) {
+ return this.renderPlaceholder();
+ }
+
+ return (
+ <div className="outline">
+ <div>
+ <OutlineFilter filter={filter} updateFilter={this.updateFilter} />
+ {this.renderFunctions(symbolsToDisplay)}
+ {this.renderFooter()}
+ </div>
+ </div>
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ const selectedSource = getSelectedSourceWithContent(state);
+ const symbols = selectedSource ? getSymbols(state, selectedSource) : null;
+
+ return {
+ cx: getContext(state),
+ symbols,
+ selectedSource: (selectedSource: ?SourceWithContent),
+ cursorPosition: getCursorPosition(state),
+ getFunctionText: line => {
+ if (selectedSource) {
+ return findFunctionText(line, selectedSource, symbols);
+ }
+
+ return null;
+ },
+ };
+};
+
+export default connect<Props, OwnProps, _, _, _, _>(mapStateToProps, {
+ selectLocation: actions.selectLocation,
+ flashLineRange: actions.flashLineRange,
+})(Outline);