tor-browser

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

SearchInFileBar.js (11716B)


      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 PropTypes from "devtools/client/shared/vendor/react-prop-types";
      6 import React, { Component } from "devtools/client/shared/vendor/react";
      7 import { div } from "devtools/client/shared/vendor/react-dom-factories";
      8 import { connect } from "devtools/client/shared/vendor/react-redux";
      9 import actions from "../../actions/index";
     10 import {
     11  getActiveSearch,
     12  getSelectedSource,
     13  getIsCurrentThreadPaused,
     14  getSelectedSourceTextContent,
     15  getSearchOptions,
     16 } from "../../selectors/index";
     17 
     18 import { searchKeys } from "../../constants";
     19 import { scrollList } from "../../utils/result-list";
     20 import { createLocation } from "../../utils/location";
     21 
     22 import SearchInput from "../shared/SearchInput";
     23 
     24 const { PluralForm } = require("resource://devtools/shared/plural-form.js");
     25 const { debounce } = require("resource://devtools/shared/debounce.js");
     26 import {
     27  clearSearch,
     28  find,
     29  findNext,
     30  findPrev,
     31 } from "../../utils/editor/index";
     32 import { isFulfilled } from "../../utils/async-value";
     33 
     34 function getSearchShortcut() {
     35  return L10N.getStr("sourceSearch.search.key2");
     36 }
     37 
     38 class SearchInFileBar extends Component {
     39  constructor(props) {
     40    super(props);
     41    this.state = {
     42      query: "",
     43      selectedResultIndex: 0,
     44      results: {
     45        matches: [],
     46        matchIndex: -1,
     47        count: 0,
     48        index: -1,
     49      },
     50      inputFocused: false,
     51    };
     52  }
     53 
     54  static get propTypes() {
     55    return {
     56      closeFileSearch: PropTypes.func.isRequired,
     57      editor: PropTypes.object,
     58      modifiers: PropTypes.object.isRequired,
     59      searchInFileEnabled: PropTypes.bool.isRequired,
     60      selectedSourceTextContent: PropTypes.object,
     61      selectedSource: PropTypes.object.isRequired,
     62      setActiveSearch: PropTypes.func.isRequired,
     63      querySearchWorker: PropTypes.func.isRequired,
     64      selectLocation: PropTypes.func.isRequired,
     65      isPaused: PropTypes.bool.isRequired,
     66    };
     67  }
     68 
     69  componentWillUnmount() {
     70    const { shortcuts } = this.context;
     71 
     72    shortcuts.off(getSearchShortcut(), this.toggleSearch);
     73    shortcuts.off("Escape", this.onEscape);
     74 
     75    this.doSearch.cancel();
     76  }
     77 
     78  // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
     79  UNSAFE_componentWillReceiveProps(nextProps) {
     80    const { query } = this.state;
     81    // Trigger a search to update the search results ...
     82    if (
     83      // if there is a search query and ...
     84      (query &&
     85        // the file search bar is toggled open or ...
     86        ((!this.props.searchInFileEnabled && nextProps.searchInFileEnabled) ||
     87          // a new source is selected.
     88          this.props.selectedSource.id !== nextProps.selectedSource.id)) ||
     89      // the source content changes
     90      this.props.selectedSourceTextContent !==
     91        nextProps.selectedSourceTextContent
     92    ) {
     93      // Do not scroll to the search location, if we just switched to a new source
     94      // and debugger is already paused on a selected line.
     95      this.doSearch(query, !nextProps.isPaused);
     96    }
     97  }
     98 
     99  componentDidMount() {
    100    // overwrite this.doSearch with debounced version to
    101    // reduce frequency of queries
    102    this.doSearch = debounce(this.doSearch, 100);
    103    const { shortcuts } = this.context;
    104 
    105    shortcuts.on(getSearchShortcut(), this.toggleSearch);
    106    shortcuts.on("Escape", this.onEscape);
    107  }
    108 
    109  componentDidUpdate() {
    110    if (this.refs.resultList && this.refs.resultList.refs) {
    111      scrollList(this.refs.resultList.refs, this.state.selectedResultIndex);
    112    }
    113  }
    114 
    115  onEscape = e => {
    116    this.closeSearch(e);
    117  };
    118 
    119  clearSearch = () => {
    120    const { editor } = this.props;
    121    if (!editor) {
    122      return;
    123    }
    124    editor.clearSearchMatches();
    125    editor.removePositionContentMarker("active-selection-marker");
    126  };
    127 
    128  closeSearch = e => {
    129    const { closeFileSearch, editor, searchInFileEnabled } = this.props;
    130    this.clearSearch();
    131    if (editor && searchInFileEnabled) {
    132      closeFileSearch();
    133      e.stopPropagation();
    134      e.preventDefault();
    135    }
    136    this.setState({ inputFocused: false });
    137  };
    138 
    139  toggleSearch = e => {
    140    e.stopPropagation();
    141    e.preventDefault();
    142    const { editor, searchInFileEnabled, setActiveSearch } = this.props;
    143 
    144    // Set inputFocused to false, so that search query is highlighted whenever search shortcut is used, even if the input already has focus.
    145    this.setState({ inputFocused: false });
    146 
    147    if (!searchInFileEnabled) {
    148      setActiveSearch("file");
    149    }
    150 
    151    if (searchInFileEnabled && editor) {
    152      const selectedText = editor.getSelectedText();
    153      const query = selectedText || this.state.query;
    154 
    155      if (query !== "") {
    156        this.setState({ query, inputFocused: true });
    157        this.doSearch(query);
    158      } else {
    159        this.setState({ query: "", inputFocused: true });
    160      }
    161    }
    162  };
    163 
    164  doSearch = async (query, shouldScroll = true) => {
    165    const { editor, modifiers, selectedSourceTextContent } = this.props;
    166    if (
    167      !editor ||
    168      !selectedSourceTextContent ||
    169      !isFulfilled(selectedSourceTextContent) ||
    170      !modifiers
    171    ) {
    172      return;
    173    }
    174    const selectedContent = selectedSourceTextContent.value;
    175 
    176    const ctx = { editor, cm: editor.codeMirror };
    177 
    178    if (!query) {
    179      clearSearch(ctx);
    180      return;
    181    }
    182 
    183    let text;
    184    if (selectedContent.type === "wasm") {
    185      text = editor.renderWasmText(selectedContent).join("\n");
    186    } else {
    187      text = selectedContent.value;
    188    }
    189 
    190    const matches = await this.props.querySearchWorker(query, text, modifiers);
    191    const results = find(ctx, query, true, modifiers, {
    192      shouldScroll,
    193    });
    194    this.setSearchResults(results, matches, shouldScroll);
    195  };
    196 
    197  traverseResults = (e, reverse = false) => {
    198    e.stopPropagation();
    199    e.preventDefault();
    200    const { editor } = this.props;
    201 
    202    if (!editor) {
    203      return;
    204    }
    205 
    206    const ctx = { editor, cm: editor.codeMirror };
    207 
    208    const { modifiers } = this.props;
    209    const { query } = this.state;
    210    const { matches } = this.state.results;
    211 
    212    if (query === "" && !this.props.searchInFileEnabled) {
    213      this.props.setActiveSearch("file");
    214    }
    215 
    216    if (modifiers) {
    217      const findArgs = [ctx, query, true, modifiers];
    218      const results = reverse ? findPrev(...findArgs) : findNext(...findArgs);
    219      this.setSearchResults(results, matches, true);
    220    }
    221  };
    222 
    223  /**
    224   * Update the state with the results and matches from the search.
    225   * This will also scroll to result's location in CodeMirror.
    226   *
    227   * @param {object} results
    228   * @param {Array} matches
    229   * @returns
    230   */
    231  setSearchResults(results, matches, shouldScroll) {
    232    if (!results) {
    233      this.setState({
    234        results: {
    235          matches,
    236          matchIndex: 0,
    237          count: matches.length,
    238          index: -1,
    239        },
    240      });
    241      return;
    242    }
    243    const { ch, line } = results;
    244    let matchContent = "";
    245    const matchIndex = matches.findIndex(elm => {
    246      if (elm.line === line && elm.ch === ch) {
    247        matchContent = elm.match;
    248        return true;
    249      }
    250      return false;
    251    });
    252 
    253    // Only change the selected location if we should scroll to it,
    254    // otherwise we are most likely updating the search results while being paused
    255    // and don't want to change the selected location from the current paused location
    256    if (shouldScroll) {
    257      this.setCursorLocation(line, ch, matchContent);
    258    }
    259    this.setState({
    260      results: {
    261        matches,
    262        matchIndex,
    263        count: matches.length,
    264        index: ch,
    265      },
    266    });
    267  }
    268 
    269  /**
    270   * Ensure showing the search result in CodeMirror editor,
    271   * and setting the cursor at the end of the matched string.
    272   *
    273   * @param {number} line
    274   * @param {number} ch
    275   * @param {string} matchContent
    276   */
    277  setCursorLocation = (line, ch, matchContent) => {
    278    this.props.selectLocation(
    279      createLocation({
    280        source: this.props.selectedSource,
    281        line: line + 1,
    282        column: ch + matchContent.length,
    283      }),
    284      {
    285        // Reset the context, so that we don't switch to original
    286        // while moving the cursor within a bundle
    287        keepContext: false,
    288 
    289        // Avoid highlighting the selected line
    290        highlight: false,
    291 
    292        // We should ensure showing the search result by scrolling it
    293        // into the viewport.
    294        // We won't be scrolling when receiving redux updates and we are paused.
    295        scroll: true,
    296      }
    297    );
    298  };
    299 
    300  // Handlers
    301  onChange = e => {
    302    this.setState({ query: e.target.value });
    303 
    304    return this.doSearch(e.target.value);
    305  };
    306 
    307  onFocus = () => {
    308    this.setState({ inputFocused: true });
    309  };
    310 
    311  onBlur = () => {
    312    this.setState({ inputFocused: false });
    313  };
    314 
    315  onKeyDown = e => {
    316    if (e.key !== "Enter" && e.key !== "F3") {
    317      return;
    318    }
    319 
    320    e.preventDefault();
    321    this.traverseResults(e, e.shiftKey);
    322  };
    323 
    324  onHistoryScroll = query => {
    325    this.setState({ query });
    326    this.doSearch(query);
    327  };
    328 
    329  // Renderers
    330  buildSummaryMsg() {
    331    const {
    332      query,
    333      results: { matchIndex, count, index },
    334    } = this.state;
    335 
    336    if (query.trim() == "") {
    337      return "";
    338    }
    339 
    340    if (count == 0) {
    341      return L10N.getStr("editor.noResultsFound");
    342    }
    343 
    344    if (index == -1) {
    345      const resultsSummaryString = L10N.getStr("sourceSearch.resultsSummary2");
    346      return PluralForm.get(count, resultsSummaryString).replace("#1", count);
    347    }
    348 
    349    const searchResultsString = L10N.getStr("editor.searchResults1");
    350    return PluralForm.get(count, searchResultsString)
    351      .replace("#1", count)
    352      .replace("%d", matchIndex + 1);
    353  }
    354 
    355  shouldShowErrorEmoji() {
    356    const {
    357      query,
    358      results: { count },
    359    } = this.state;
    360    return !!query && !count;
    361  }
    362 
    363  render() {
    364    const { searchInFileEnabled } = this.props;
    365    const {
    366      results: { count },
    367    } = this.state;
    368 
    369    if (!searchInFileEnabled) {
    370      return div(null);
    371    }
    372    return div(
    373      {
    374        className: "search-bar",
    375      },
    376      React.createElement(SearchInput, {
    377        query: this.state.query,
    378        count,
    379        placeholder: L10N.getStr("sourceSearch.search.placeholder2"),
    380        summaryMsg: this.buildSummaryMsg(),
    381        isLoading: false,
    382        onChange: this.onChange,
    383        onFocus: this.onFocus,
    384        onBlur: this.onBlur,
    385        showErrorEmoji: this.shouldShowErrorEmoji(),
    386        onKeyDown: this.onKeyDown,
    387        onHistoryScroll: this.onHistoryScroll,
    388        handleNext: e => this.traverseResults(e, false),
    389        handlePrev: e => this.traverseResults(e, true),
    390        shouldFocus: this.state.inputFocused,
    391        showClose: true,
    392        showExcludePatterns: false,
    393        handleClose: this.closeSearch,
    394        showSearchModifiers: true,
    395        searchKey: searchKeys.FILE_SEARCH,
    396        onToggleSearchModifier: () => this.doSearch(this.state.query),
    397      })
    398    );
    399  }
    400 }
    401 
    402 SearchInFileBar.contextTypes = {
    403  shortcuts: PropTypes.object,
    404 };
    405 
    406 const mapStateToProps = state => {
    407  return {
    408    searchInFileEnabled: getActiveSearch(state) === "file",
    409    selectedSource: getSelectedSource(state),
    410    isPaused: getIsCurrentThreadPaused(state),
    411    selectedSourceTextContent: getSelectedSourceTextContent(state),
    412    modifiers: getSearchOptions(state, "file-search"),
    413  };
    414 };
    415 
    416 export default connect(mapStateToProps, {
    417  setFileSearchQuery: actions.setFileSearchQuery,
    418  setActiveSearch: actions.setActiveSearch,
    419  closeFileSearch: actions.closeFileSearch,
    420  querySearchWorker: actions.querySearchWorker,
    421  selectLocation: actions.selectLocation,
    422 })(SearchInFileBar);