tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

Outline.js (10204B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
      4 
      5 import React, { Component } from "devtools/client/shared/vendor/react";
      6 import {
      7  div,
      8  ul,
      9  li,
     10  span,
     11  h2,
     12  button,
     13 } from "devtools/client/shared/vendor/react-dom-factories";
     14 import PropTypes from "devtools/client/shared/vendor/react-prop-types";
     15 import { connect } from "devtools/client/shared/vendor/react-redux";
     16 
     17 import { containsPosition, positionAfter } from "../../utils/ast";
     18 import { createLocation } from "../../utils/location";
     19 
     20 import actions from "../../actions/index";
     21 import {
     22  getSelectedLocation,
     23  getSelectedSourceTextContent,
     24 } from "../../selectors/index";
     25 
     26 import OutlineFilter from "./OutlineFilter";
     27 import PreviewFunction from "../shared/PreviewFunction";
     28 
     29 import { isFulfilled } from "../../utils/async-value";
     30 
     31 const classnames = require("resource://devtools/client/shared/classnames.js");
     32 const {
     33  score: fuzzaldrinScore,
     34 } = require("resource://devtools/client/shared/vendor/fuzzaldrin-plus.js");
     35 
     36 // Set higher to make the fuzzaldrin filter more specific
     37 const FUZZALDRIN_FILTER_THRESHOLD = 15000;
     38 
     39 /**
     40 * Check whether the name argument matches the fuzzy filter argument
     41 */
     42 const filterOutlineItem = (name, filter) => {
     43  if (!filter) {
     44    return true;
     45  }
     46 
     47  if (filter.length === 1) {
     48    // when filter is a single char just check if it starts with the char
     49    return filter.toLowerCase() === name.toLowerCase()[0];
     50  }
     51  return fuzzaldrinScore(name, filter) > FUZZALDRIN_FILTER_THRESHOLD;
     52 };
     53 
     54 // Checks if an element is visible inside its parent element
     55 function isVisible(element, parent) {
     56  const parentRect = parent.getBoundingClientRect();
     57  const elementRect = element.getBoundingClientRect();
     58 
     59  const parentTop = parentRect.top;
     60  const parentBottom = parentRect.bottom;
     61  const elTop = elementRect.top;
     62  const elBottom = elementRect.bottom;
     63 
     64  return parentTop < elTop && parentBottom > elBottom;
     65 }
     66 
     67 export class Outline extends Component {
     68  constructor(props) {
     69    super(props);
     70    this.focusedElRef = null;
     71    this.state = { filter: "", focusedItem: null, symbols: null };
     72  }
     73 
     74  static get propTypes() {
     75    return {
     76      alphabetizeOutline: PropTypes.bool.isRequired,
     77      onAlphabetizeClick: PropTypes.func.isRequired,
     78      selectLocation: PropTypes.func.isRequired,
     79      selectedLocation: PropTypes.object,
     80      getFunctionSymbols: PropTypes.func.isRequired,
     81      getClassSymbols: PropTypes.func.isRequired,
     82      selectedSourceTextContent: PropTypes.object,
     83      canFetchSymbols: PropTypes.bool,
     84    };
     85  }
     86 
     87  componentDidMount() {
     88    if (!this.props.canFetchSymbols) {
     89      return;
     90    }
     91    this.getClassAndFunctionSymbols();
     92  }
     93 
     94  componentDidUpdate(prevProps) {
     95    const { selectedLocation, selectedSourceTextContent, canFetchSymbols } =
     96      this.props;
     97    if (selectedLocation && selectedLocation !== prevProps.selectedLocation) {
     98      this.setFocus(selectedLocation);
     99    }
    100 
    101    if (
    102      this.focusedElRef &&
    103      !isVisible(this.focusedElRef, this.refs.outlineList)
    104    ) {
    105      this.focusedElRef.scrollIntoView({ block: "center" });
    106    }
    107 
    108    // Lets make sure the source text has been loaded and it is different
    109    if (
    110      canFetchSymbols &&
    111      prevProps.selectedSourceTextContent !== selectedSourceTextContent
    112    ) {
    113      this.getClassAndFunctionSymbols();
    114    }
    115  }
    116 
    117  async getClassAndFunctionSymbols() {
    118    const { selectedLocation, getFunctionSymbols, getClassSymbols } =
    119      this.props;
    120 
    121    const functions = await getFunctionSymbols(selectedLocation);
    122    const classes = await getClassSymbols(selectedLocation);
    123 
    124    this.setState({ symbols: { functions, classes } });
    125  }
    126 
    127  async setFocus(selectedLocation) {
    128    const { symbols } = this.state;
    129 
    130    let classes = [];
    131    let functions = [];
    132 
    133    if (symbols) {
    134      ({ classes, functions } = symbols);
    135    }
    136 
    137    // Find items that enclose the selected location
    138    const enclosedItems = [...classes, ...functions].filter(({ location }) =>
    139      containsPosition(location, selectedLocation)
    140    );
    141 
    142    if (!enclosedItems.length) {
    143      this.setState({ focusedItem: null });
    144      return;
    145    }
    146 
    147    // Find the closest item to the selected location to focus
    148    const closestItem = enclosedItems.reduce((item, closest) =>
    149      positionAfter(item.location, closest.location) ? item : closest
    150    );
    151 
    152    this.setState({ focusedItem: closestItem });
    153  }
    154 
    155  selectItem(selectedItem) {
    156    const { selectedLocation, selectLocation } = this.props;
    157    if (!selectedLocation || !selectedItem) {
    158      return;
    159    }
    160 
    161    selectLocation(
    162      createLocation({
    163        source: selectedLocation.source,
    164        line: selectedItem.location.start.line,
    165        column: selectedItem.location.start.column,
    166      })
    167    );
    168 
    169    this.setState({ focusedItem: selectedItem });
    170  }
    171 
    172  onContextMenu(event, func) {
    173    event.stopPropagation();
    174    event.preventDefault();
    175 
    176    const { symbols } = this.state;
    177    this.props.showOutlineContextMenu(event, func, symbols);
    178  }
    179 
    180  updateFilter = filter => {
    181    this.setState({ filter: filter.trim() });
    182  };
    183 
    184  renderPlaceholder() {
    185    const placeholderMessage = this.props.selectedLocation
    186      ? L10N.getStr("outline.noFunctions")
    187      : L10N.getStr("outline.noFileSelected");
    188    return div(
    189      {
    190        className: "outline-pane-info",
    191      },
    192      placeholderMessage
    193    );
    194  }
    195 
    196  renderLoading() {
    197    return div(
    198      {
    199        className: "outline-pane-info",
    200      },
    201      L10N.getStr("loadingText")
    202    );
    203  }
    204 
    205  renderFunction(func) {
    206    const { focusedItem } = this.state;
    207    const { name, location, parameterNames } = func;
    208    const isFocused = focusedItem === func;
    209    return li(
    210      {
    211        key: `${name}:${location.start.line}:${location.start.column}`,
    212        className: classnames("outline-list__element", {
    213          focused: isFocused,
    214        }),
    215        ref: el => {
    216          if (isFocused) {
    217            this.focusedElRef = el;
    218          }
    219        },
    220        onClick: () => this.selectItem(func),
    221        onContextMenu: e => this.onContextMenu(e, func),
    222      },
    223      span(
    224        {
    225          className: "outline-list__element-icon",
    226        },
    227        "λ"
    228      ),
    229      React.createElement(PreviewFunction, {
    230        func: {
    231          name,
    232          parameterNames,
    233        },
    234      })
    235    );
    236  }
    237 
    238  renderClassHeader(klass) {
    239    return div(
    240      null,
    241      span(
    242        {
    243          className: "keyword",
    244        },
    245        "class"
    246      ),
    247      " ",
    248      klass
    249    );
    250  }
    251 
    252  renderClassFunctions(klass, functions) {
    253    const { symbols } = this.state;
    254 
    255    if (!symbols || klass == null || !functions.length) {
    256      return null;
    257    }
    258 
    259    const { focusedItem } = this.state;
    260    const classFunc = functions.find(func => func.name === klass);
    261    const classFunctions = functions.filter(func => func.klass === klass);
    262    const classInfo = symbols.classes.find(c => c.name === klass);
    263 
    264    const item = classFunc || classInfo;
    265    const isFocused = focusedItem === item;
    266 
    267    return li(
    268      {
    269        className: "outline-list__class",
    270        ref: el => {
    271          if (isFocused) {
    272            this.focusedElRef = el;
    273          }
    274        },
    275        key: klass,
    276      },
    277      h2(
    278        {
    279          className: classnames({
    280            focused: isFocused,
    281          }),
    282          onClick: () => this.selectItem(item),
    283        },
    284        classFunc
    285          ? this.renderFunction(classFunc)
    286          : this.renderClassHeader(klass)
    287      ),
    288      ul(
    289        {
    290          className: "outline-list__class-list",
    291        },
    292        classFunctions.map(func => this.renderFunction(func))
    293      )
    294    );
    295  }
    296 
    297  renderFunctions(functions) {
    298    const { filter } = this.state;
    299    let classes = [...new Set(functions.map(({ klass }) => klass))];
    300    const namedFunctions = functions.filter(
    301      ({ name, klass }) =>
    302        filterOutlineItem(name, filter) && !klass && !classes.includes(name)
    303    );
    304    const classFunctions = functions.filter(
    305      ({ name, klass }) => filterOutlineItem(name, filter) && !!klass
    306    );
    307 
    308    if (this.props.alphabetizeOutline) {
    309      const sortByName = (a, b) => (a.name < b.name ? -1 : 1);
    310      namedFunctions.sort(sortByName);
    311      classes = classes.sort();
    312      classFunctions.sort(sortByName);
    313    }
    314    return ul(
    315      {
    316        ref: "outlineList",
    317        className: "outline-list devtools-monospace",
    318        dir: "ltr",
    319      },
    320      namedFunctions.map(func => this.renderFunction(func)),
    321      classes.map(klass => this.renderClassFunctions(klass, classFunctions))
    322    );
    323  }
    324 
    325  renderFooter() {
    326    return div(
    327      {
    328        className: "outline-footer",
    329      },
    330      button(
    331        {
    332          onClick: this.props.onAlphabetizeClick,
    333          className: this.props.alphabetizeOutline ? "active" : "",
    334        },
    335        L10N.getStr("outline.sortLabel")
    336      )
    337    );
    338  }
    339 
    340  render() {
    341    const { selectedLocation } = this.props;
    342    const { filter, symbols } = this.state;
    343 
    344    if (!selectedLocation) {
    345      return this.renderPlaceholder();
    346    }
    347 
    348    if (!symbols) {
    349      return this.renderLoading();
    350    }
    351 
    352    const { functions } = symbols;
    353 
    354    if (functions.length === 0) {
    355      return this.renderPlaceholder();
    356    }
    357 
    358    return div(
    359      {
    360        className: "outline",
    361      },
    362      div(
    363        null,
    364        React.createElement(OutlineFilter, {
    365          filter,
    366          updateFilter: this.updateFilter,
    367        }),
    368        this.renderFunctions(functions),
    369        this.renderFooter()
    370      )
    371    );
    372  }
    373 }
    374 
    375 const mapStateToProps = state => {
    376  const selectedSourceTextContent = getSelectedSourceTextContent(state);
    377  return {
    378    selectedSourceTextContent,
    379    selectedLocation: getSelectedLocation(state),
    380    canFetchSymbols:
    381      selectedSourceTextContent && isFulfilled(selectedSourceTextContent),
    382  };
    383 };
    384 
    385 export default connect(mapStateToProps, {
    386  selectLocation: actions.selectLocation,
    387  showOutlineContextMenu: actions.showOutlineContextMenu,
    388  getFunctionSymbols: actions.getFunctionSymbols,
    389  getClassSymbols: actions.getClassSymbols,
    390 })(Outline);