summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/src/components/Editor/SearchInFileBar.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/debugger/src/components/Editor/SearchInFileBar.js')
-rw-r--r--devtools/client/debugger/src/components/Editor/SearchInFileBar.js368
1 files changed, 368 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/components/Editor/SearchInFileBar.js b/devtools/client/debugger/src/components/Editor/SearchInFileBar.js
new file mode 100644
index 0000000000..a3491a3fef
--- /dev/null
+++ b/devtools/client/debugger/src/components/Editor/SearchInFileBar.js
@@ -0,0 +1,368 @@
+/* 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 PropTypes from "devtools/client/shared/vendor/react-prop-types";
+import React, { Component } from "devtools/client/shared/vendor/react";
+import { div } from "devtools/client/shared/vendor/react-dom-factories";
+import { connect } from "devtools/client/shared/vendor/react-redux";
+import actions from "../../actions/index";
+import {
+ getActiveSearch,
+ getSelectedSource,
+ getSelectedSourceTextContent,
+ getSearchOptions,
+} from "../../selectors/index";
+
+import { searchKeys } from "../../constants";
+import { scrollList } from "../../utils/result-list";
+
+import SearchInput from "../shared/SearchInput";
+
+const { PluralForm } = require("resource://devtools/shared/plural-form.js");
+const { debounce } = require("resource://devtools/shared/debounce.js");
+import { renderWasmText } from "../../utils/wasm";
+import {
+ clearSearch,
+ find,
+ findNext,
+ findPrev,
+ removeOverlay,
+} from "../../utils/editor/index";
+import { isFulfilled } from "../../utils/async-value";
+
+function getSearchShortcut() {
+ return L10N.getStr("sourceSearch.search.key2");
+}
+
+class SearchInFileBar extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ query: "",
+ selectedResultIndex: 0,
+ results: {
+ matches: [],
+ matchIndex: -1,
+ count: 0,
+ index: -1,
+ },
+ inputFocused: false,
+ };
+ }
+
+ static get propTypes() {
+ return {
+ closeFileSearch: PropTypes.func.isRequired,
+ editor: PropTypes.object,
+ modifiers: PropTypes.object.isRequired,
+ searchInFileEnabled: PropTypes.bool.isRequired,
+ selectedSourceTextContent: PropTypes.bool.isRequired,
+ selectedSource: PropTypes.object.isRequired,
+ setActiveSearch: PropTypes.func.isRequired,
+ querySearchWorker: PropTypes.func.isRequired,
+ };
+ }
+
+ componentWillUnmount() {
+ const { shortcuts } = this.context;
+
+ shortcuts.off(getSearchShortcut(), this.toggleSearch);
+ shortcuts.off("Escape", this.onEscape);
+
+ this.doSearch.cancel();
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ const { query } = this.state;
+ // If a new source is selected update the file search results
+ if (
+ this.props.selectedSource &&
+ nextProps.selectedSource !== this.props.selectedSource &&
+ this.props.searchInFileEnabled &&
+ query
+ ) {
+ this.doSearch(query, false);
+ }
+ }
+
+ componentDidMount() {
+ // overwrite this.doSearch with debounced version to
+ // reduce frequency of queries
+ this.doSearch = debounce(this.doSearch, 100);
+ const { shortcuts } = this.context;
+
+ shortcuts.on(getSearchShortcut(), this.toggleSearch);
+ shortcuts.on("Escape", this.onEscape);
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ if (this.refs.resultList && this.refs.resultList.refs) {
+ scrollList(this.refs.resultList.refs, this.state.selectedResultIndex);
+ }
+ }
+
+ onEscape = e => {
+ this.closeSearch(e);
+ };
+
+ clearSearch = () => {
+ const { editor: ed } = this.props;
+ if (ed) {
+ const ctx = { ed, cm: ed.codeMirror };
+ removeOverlay(ctx, this.state.query);
+ }
+ };
+
+ closeSearch = e => {
+ const { closeFileSearch, editor, searchInFileEnabled } = this.props;
+ this.clearSearch();
+ if (editor && searchInFileEnabled) {
+ closeFileSearch();
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ this.setState({ inputFocused: false });
+ };
+
+ toggleSearch = e => {
+ e.stopPropagation();
+ e.preventDefault();
+ const { editor, searchInFileEnabled, setActiveSearch } = this.props;
+
+ // Set inputFocused to false, so that search query is highlighted whenever search shortcut is used, even if the input already has focus.
+ this.setState({ inputFocused: false });
+
+ if (!searchInFileEnabled) {
+ setActiveSearch("file");
+ }
+
+ if (searchInFileEnabled && editor) {
+ const query = editor.codeMirror.getSelection() || this.state.query;
+
+ if (query !== "") {
+ this.setState({ query, inputFocused: true });
+ this.doSearch(query);
+ } else {
+ this.setState({ query: "", inputFocused: true });
+ }
+ }
+ };
+
+ doSearch = async (query, focusFirstResult = true) => {
+ const { editor, modifiers, selectedSourceTextContent } = this.props;
+ if (
+ !editor ||
+ !selectedSourceTextContent ||
+ !isFulfilled(selectedSourceTextContent) ||
+ !modifiers
+ ) {
+ return;
+ }
+ const selectedContent = selectedSourceTextContent.value;
+
+ const ctx = { ed: editor, cm: editor.codeMirror };
+
+ if (!query) {
+ clearSearch(ctx.cm, query);
+ return;
+ }
+
+ let text;
+ if (selectedContent.type === "wasm") {
+ text = renderWasmText(this.props.selectedSource.id, selectedContent).join(
+ "\n"
+ );
+ } else {
+ text = selectedContent.value;
+ }
+
+ const matches = await this.props.querySearchWorker(query, text, modifiers);
+
+ const res = find(ctx, query, true, modifiers, focusFirstResult);
+ if (!res) {
+ return;
+ }
+
+ const { ch, line } = res;
+
+ const matchIndex = matches.findIndex(
+ elm => elm.line === line && elm.ch === ch
+ );
+ this.setState({
+ results: {
+ matches,
+ matchIndex,
+ count: matches.length,
+ index: ch,
+ },
+ });
+ };
+
+ traverseResults = (e, reverse = false) => {
+ e.stopPropagation();
+ e.preventDefault();
+ const { editor } = this.props;
+
+ if (!editor) {
+ return;
+ }
+
+ const ctx = { ed: editor, cm: editor.codeMirror };
+
+ const { modifiers } = this.props;
+ const { query } = this.state;
+ const { matches } = this.state.results;
+
+ if (query === "" && !this.props.searchInFileEnabled) {
+ this.props.setActiveSearch("file");
+ }
+
+ if (modifiers) {
+ const findArgs = [ctx, query, true, modifiers];
+ const results = reverse ? findPrev(...findArgs) : findNext(...findArgs);
+
+ if (!results) {
+ return;
+ }
+ const { ch, line } = results;
+ const matchIndex = matches.findIndex(
+ elm => elm.line === line && elm.ch === ch
+ );
+ this.setState({
+ results: {
+ matches,
+ matchIndex,
+ count: matches.length,
+ index: ch,
+ },
+ });
+ }
+ };
+
+ // Handlers
+
+ onChange = e => {
+ this.setState({ query: e.target.value });
+
+ return this.doSearch(e.target.value);
+ };
+
+ onFocus = e => {
+ this.setState({ inputFocused: true });
+ };
+
+ onBlur = e => {
+ this.setState({ inputFocused: false });
+ };
+
+ onKeyDown = e => {
+ if (e.key !== "Enter" && e.key !== "F3") {
+ return;
+ }
+
+ this.traverseResults(e, e.shiftKey);
+ e.preventDefault();
+ this.doSearch(e.target.value);
+ };
+
+ onHistoryScroll = query => {
+ this.setState({ query });
+ this.doSearch(query);
+ };
+
+ // Renderers
+ buildSummaryMsg() {
+ const {
+ query,
+ results: { matchIndex, count, index },
+ } = this.state;
+
+ if (query.trim() == "") {
+ return "";
+ }
+
+ if (count == 0) {
+ return L10N.getStr("editor.noResultsFound");
+ }
+
+ if (index == -1) {
+ const resultsSummaryString = L10N.getStr("sourceSearch.resultsSummary1");
+ return PluralForm.get(count, resultsSummaryString).replace("#1", count);
+ }
+
+ const searchResultsString = L10N.getStr("editor.searchResults1");
+ return PluralForm.get(count, searchResultsString)
+ .replace("#1", count)
+ .replace("%d", matchIndex + 1);
+ }
+
+ shouldShowErrorEmoji() {
+ const {
+ query,
+ results: { count },
+ } = this.state;
+ return !!query && !count;
+ }
+
+ render() {
+ const { searchInFileEnabled } = this.props;
+ const {
+ results: { count },
+ } = this.state;
+
+ if (!searchInFileEnabled) {
+ return div(null);
+ }
+ return div(
+ {
+ className: "search-bar",
+ },
+ React.createElement(SearchInput, {
+ query: this.state.query,
+ count: count,
+ placeholder: L10N.getStr("sourceSearch.search.placeholder2"),
+ summaryMsg: this.buildSummaryMsg(),
+ isLoading: false,
+ onChange: this.onChange,
+ onFocus: this.onFocus,
+ onBlur: this.onBlur,
+ showErrorEmoji: this.shouldShowErrorEmoji(),
+ onKeyDown: this.onKeyDown,
+ onHistoryScroll: this.onHistoryScroll,
+ handleNext: e => this.traverseResults(e, false),
+ handlePrev: e => this.traverseResults(e, true),
+ shouldFocus: this.state.inputFocused,
+ showClose: true,
+ showExcludePatterns: false,
+ handleClose: this.closeSearch,
+ showSearchModifiers: true,
+ searchKey: searchKeys.FILE_SEARCH,
+ onToggleSearchModifier: () => this.doSearch(this.state.query),
+ })
+ );
+ }
+}
+
+SearchInFileBar.contextTypes = {
+ shortcuts: PropTypes.object,
+};
+
+const mapStateToProps = (state, p) => {
+ const selectedSource = getSelectedSource(state);
+
+ return {
+ searchInFileEnabled: getActiveSearch(state) === "file",
+ selectedSource,
+ selectedSourceTextContent: getSelectedSourceTextContent(state),
+ modifiers: getSearchOptions(state, "file-search"),
+ };
+};
+
+export default connect(mapStateToProps, {
+ setFileSearchQuery: actions.setFileSearchQuery,
+ setActiveSearch: actions.setActiveSearch,
+ closeFileSearch: actions.closeFileSearch,
+ querySearchWorker: actions.querySearchWorker,
+})(SearchInFileBar);