Source code

Revision control

Copy as Markdown

Other Tools

/* 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/>. */
"use strict";
const React = require("devtools/client/shared/vendor/react");
const { Component } = React;
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const { div } = require("devtools/client/shared/vendor/react-dom-factories");
const { PluralForm } = require("resource://devtools/shared/plural-form.js");
const { debounce } = require("resource://devtools/shared/debounce.js");
function getSearchShortcut(getString) {
return getString("sourceSearch.search.key2");
}
class FileSearchBar extends Component {
static get propTypes() {
return {
closeFileSearch: PropTypes.func.isRequired,
editor: PropTypes.object,
modifiers: PropTypes.object.isRequired,
searchInFileEnabled: PropTypes.bool.isRequired,
setCursorLocation: PropTypes.func.isRequired,
textContent: PropTypes.object,
setActiveSearch: PropTypes.func.isRequired,
querySearchWorker: PropTypes.func.isRequired,
shortcuts: PropTypes.object,
SearchInput: PropTypes.oneOfType([PropTypes.func, PropTypes.object])
.isRequired,
searchKey: PropTypes.string.isRequired,
shouldScroll: PropTypes.bool.isRequired,
scrollList: PropTypes.func.isRequired,
clearSearchEditor: PropTypes.func.isRequired,
find: PropTypes.func.isRequired,
findNext: PropTypes.func.isRequired,
findPrev: PropTypes.func.isRequired,
locale: PropTypes.object.isRequired,
};
}
constructor(props) {
super(props);
this.state = {
query: "",
selectedResultIndex: 0,
results: {
matches: [],
matchIndex: -1,
count: 0,
index: -1,
},
inputFocused: false,
};
}
componentDidMount() {
// overwrite this.doSearch with debounced version to
// reduce frequency of queries
this.doSearch = debounce(this.doSearch, 100);
const { shortcuts, locale } = this.props;
if (!shortcuts) {
return;
}
shortcuts.on(getSearchShortcut(locale.getStr), this.toggleSearch);
shortcuts.on("Escape", this.onEscape);
}
componentDidUpdate(prevProps) {
if (this.refs.resultList && this.refs.resultList.refs) {
this.props.scrollList(
this.refs.resultList.refs,
this.state.selectedResultIndex
);
}
const { query } = this.state;
const didEnableSearch =
!prevProps.searchInFileEnabled && this.props.searchInFileEnabled;
const didTextContentChange =
prevProps.textContent !== this.props.textContent;
if (
query &&
(didEnableSearch ||
(this.props.searchInFileEnabled && didTextContentChange))
) {
this.doSearch(query, this.props.shouldScroll);
}
}
componentWillUnmount() {
const { shortcuts, locale } = this.props;
if (shortcuts) {
shortcuts.off(getSearchShortcut(locale.getStr), this.toggleSearch);
shortcuts.off("Escape", this.onEscape);
}
if (this.doSearch?.cancel) {
this.doSearch.cancel();
}
}
onEscape = e => {
this.closeSearch(e);
};
clearSearch = () => {
const { editor } = this.props;
if (!editor) {
return;
}
editor.clearSearchMatches();
editor.removePositionContentMarker("active-selection-marker");
};
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 selectedText = editor.getSelectedText();
const query = selectedText || this.state.query;
if (query !== "") {
this.setState({ query, inputFocused: true });
this.doSearch(query);
} else {
this.setState({ query: "", inputFocused: true });
}
}
};
doSearch = async (query, shouldScroll = true) => {
const { editor, modifiers, textContent } = this.props;
if (!editor || !textContent || !modifiers) {
return;
}
const selectedContent = textContent;
const ctx = { editor, cm: editor.codeMirror };
if (!query) {
this.props.clearSearchEditor(ctx);
return;
}
let text;
if (selectedContent.type === "wasm") {
text = editor.renderWasmText(selectedContent).join("\n");
} else {
text = selectedContent.value;
}
const matches = await this.props.querySearchWorker(query, text, modifiers);
const results = this.props.find(ctx, query, true, modifiers, {
shouldScroll,
});
this.setSearchResults(results, matches, shouldScroll);
};
traverseResults = (e, reverse = false) => {
e.stopPropagation();
e.preventDefault();
const { editor } = this.props;
if (!editor) {
return;
}
const ctx = { editor, cm: editor.codeMirror };
const { modifiers, findPrev, findNext } = 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);
this.setSearchResults(results, matches, true);
}
};
/**
* Update the state with the results and matches from the search.
* This will also scroll to result's location in CodeMirror.
*
* @param {object} results
* @param {Array} matches
* @returns
*/
setSearchResults(results, matches, shouldScroll) {
if (!results) {
this.setState({
results: {
matches,
matchIndex: 0,
count: matches.length,
index: -1,
},
});
return;
}
const { ch, line } = results;
let matchContent = "";
const matchIndex = matches.findIndex(elm => {
if (elm.line === line && elm.ch === ch) {
matchContent = elm.match;
return true;
}
return false;
});
if (shouldScroll) {
this.props.setCursorLocation(line, ch, matchContent);
}
// Only change the selected location if we should scroll to it,
// otherwise we are most likely updating the search results while being paused
// and don't want to change the selected location from the current paused location
this.setState({
results: {
matches,
matchIndex,
count: matches.length,
index: ch,
},
});
}
onChange = e => {
this.setState({ query: e.target.value });
return this.doSearch(e.target.value);
};
onFocus = () => {
this.setState({ inputFocused: true });
};
onBlur = () => {
this.setState({ inputFocused: false });
};
onKeyDown = e => {
if (e.key !== "Enter" && e.key !== "F3") {
return;
}
e.preventDefault();
this.traverseResults(e, e.shiftKey);
};
onHistoryScroll = query => {
this.setState({ query });
this.doSearch(query);
};
// Renderers
buildSummaryMsg() {
const {
query,
results: { matchIndex, count, index },
} = this.state;
const { locale } = this.props;
if (query.trim() == "") {
return "";
}
if (count == 0) {
return locale.getStr("editor.noResultsFound");
}
if (index == -1) {
const resultsSummaryString = locale.getStr(
"sourceSearch.resultsSummary2"
);
return PluralForm.get(count, resultsSummaryString).replace("#1", count);
}
const searchResultsString = locale.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, searchKey, SearchInput, locale } = 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,
placeholder: locale.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,
onToggleSearchModifier: () => this.doSearch(this.state.query),
})
);
}
}
module.exports = FileSearchBar;