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.js372
1 files changed, 372 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..8e0aa17ca4
--- /dev/null
+++ b/devtools/client/debugger/src/components/PrimaryPanes/Outline.js
@@ -0,0 +1,372 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. 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 "react";
+import PropTypes from "prop-types";
+import { showMenu } from "../../context-menu/menu";
+import { connect } from "../../utils/connect";
+import { score as fuzzaldrinScore } from "fuzzaldrin-plus";
+
+import { containsPosition, positionAfter } from "../../utils/ast";
+import { copyToTheClipboard } from "../../utils/clipboard";
+import { findFunctionText } from "../../utils/function";
+import { createLocation } from "../../utils/location";
+
+import actions from "../../actions";
+import {
+ getSelectedLocation,
+ getSelectedSource,
+ getSelectedSourceTextContent,
+ getSymbols,
+ getCursorPosition,
+ getContext,
+} from "../../selectors";
+
+import OutlineFilter from "./OutlineFilter";
+import "./Outline.css";
+import PreviewFunction from "../shared/PreviewFunction";
+
+const classnames = require("devtools/client/shared/classnames.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 };
+ }
+
+ static get propTypes() {
+ return {
+ alphabetizeOutline: PropTypes.bool.isRequired,
+ cursorPosition: PropTypes.object,
+ cx: PropTypes.object.isRequired,
+ flashLineRange: PropTypes.func.isRequired,
+ getFunctionText: PropTypes.func.isRequired,
+ onAlphabetizeClick: PropTypes.func.isRequired,
+ selectLocation: PropTypes.func.isRequired,
+ selectedSource: PropTypes.object.isRequired,
+ symbols: PropTypes.object.isRequired,
+ };
+ }
+
+ componentDidUpdate(prevProps) {
+ 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) {
+ const { symbols } = this.props;
+ let classes = [];
+ let functions = [];
+
+ if (symbols) {
+ ({ 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) {
+ 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 { cx, selectedSource, selectLocation } = this.props;
+ if (!selectedSource || !selectedItem) {
+ return;
+ }
+
+ selectLocation(
+ cx,
+ createLocation({
+ source: selectedSource,
+ line: selectedItem.location.start.line,
+ column: selectedItem.location.start.column,
+ })
+ );
+
+ this.setState({ focusedItem: selectedItem });
+ }
+
+ onContextMenu(event, func) {
+ 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 => {
+ 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) {
+ 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) {
+ return (
+ <div>
+ <span className="keyword">class</span> {klass}
+ </div>
+ );
+ }
+
+ renderClassFunctions(klass, functions) {
+ const { symbols } = this.props;
+
+ 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)}
+ </h2>
+ <ul className="outline-list__class-list">
+ {classFunctions.map(func => this.renderFunction(func))}
+ </ul>
+ </li>
+ );
+ }
+
+ 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))}
+ </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) {
+ 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 = getSelectedSource(state);
+ const symbols = getSymbols(state, getSelectedLocation(state));
+
+ return {
+ cx: getContext(state),
+ symbols,
+ selectedSource,
+ cursorPosition: getCursorPosition(state),
+ getFunctionText: line => {
+ if (selectedSource) {
+ const selectedSourceTextContent = getSelectedSourceTextContent(state);
+ return findFunctionText(
+ line,
+ selectedSource,
+ selectedSourceTextContent,
+ symbols
+ );
+ }
+
+ return null;
+ },
+ };
+};
+
+export default connect(mapStateToProps, {
+ selectLocation: actions.selectLocation,
+ flashLineRange: actions.flashLineRange,
+})(Outline);