/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at . */ 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
{placeholderMessage}
; } renderLoading() { return (
{L10N.getStr("loadingText")}
); } renderFunction(func) { const { focusedItem } = this.state; const { name, location, parameterNames } = func; const isFocused = focusedItem === func; return (
  • { if (isFocused) { this.focusedElRef = el; } }} onClick={() => this.selectItem(func)} onContextMenu={e => this.onContextMenu(e, func)} > λ
  • ); } renderClassHeader(klass) { return (
    class {klass}
    ); } 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 (
  • { if (isFocused) { this.focusedElRef = el; } }} key={klass} >

    this.selectItem(item)} > {classFunc ? this.renderFunction(classFunc) : this.renderClassHeader(klass)}

  • ); } 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 ( ); } renderFooter() { return (
    ); } 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 (
    {this.renderFunctions(symbolsToDisplay)} {this.renderFooter()}
    ); } } 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);