/* 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 { connect } from "../../utils/connect";
import actions from "../../actions";
import { getEditor } from "../../utils/editor";
import { searchKeys } from "../../constants";
import { statusType } from "../../reducers/project-text-search";
import { getRelativePath } from "../../utils/sources-tree/utils";
import { getFormattedSourceId } from "../../utils/source";
import {
getProjectSearchResults,
getProjectSearchStatus,
getProjectSearchQuery,
getContext,
} from "../../selectors";
import SearchInput from "../shared/SearchInput";
import AccessibleImage from "../shared/AccessibleImage";
const { PluralForm } = require("devtools/shared/plural-form");
const classnames = require("devtools/client/shared/classnames.js");
const Tree = require("devtools/client/shared/components/Tree");
import "./ProjectSearch.css";
function getFilePath(item, index) {
return item.type === "RESULT"
? `${item.location.source.id}-${index || "$"}`
: `${item.location.source.id}-${item.location.line}-${
item.location.column
}-${index || "$"}`;
}
export class ProjectSearch extends Component {
constructor(props) {
super(props);
this.state = {
inputValue: this.props.query || "",
inputFocused: false,
focusedItem: null,
expanded: new Set(),
};
}
static get propTypes() {
return {
clearSearch: PropTypes.func.isRequired,
cx: PropTypes.object.isRequired,
doSearchForHighlight: PropTypes.func.isRequired,
query: PropTypes.string.isRequired,
results: PropTypes.array.isRequired,
searchSources: PropTypes.func.isRequired,
selectSpecificLocation: PropTypes.func.isRequired,
setActiveSearch: PropTypes.func.isRequired,
status: PropTypes.oneOf([
"INITIAL",
"FETCHING",
"CANCELED",
"DONE",
"ERROR",
]).isRequired,
modifiers: PropTypes.object,
toggleProjectSearchModifier: PropTypes.func,
};
}
componentDidMount() {
const { shortcuts } = this.context;
shortcuts.on("Enter", this.onEnterPress);
}
componentWillUnmount() {
const { shortcuts } = this.context;
shortcuts.off("Enter", this.onEnterPress);
}
componentDidUpdate(prevProps) {
// If the query changes in redux, also change it in the UI
if (prevProps.query !== this.props.query) {
this.setState({ inputValue: this.props.query });
}
}
doSearch(searchTerm) {
if (searchTerm) {
this.props.searchSources(this.props.cx, searchTerm);
}
}
selectMatchItem = matchItem => {
this.props.selectSpecificLocation(this.props.cx, matchItem.location);
this.props.doSearchForHighlight(
this.state.inputValue,
getEditor(),
matchItem.location.line,
matchItem.location.column
);
};
highlightMatches = lineMatch => {
const { value, matchIndex, match } = lineMatch;
const len = match.length;
return (
{value.slice(0, matchIndex)}
{value.substr(matchIndex, len)}
{value.slice(matchIndex + len, value.length)}
);
};
getResultCount = () =>
this.props.results.reduce((count, file) => count + file.matches.length, 0);
onKeyDown = e => {
if (e.key === "Escape") {
return;
}
e.stopPropagation();
this.setState({ focusedItem: null });
this.doSearch(this.state.inputValue);
};
onHistoryScroll = query => {
this.setState({ inputValue: query });
};
onEnterPress = () => {
// This is to select a match from the search result.
if (!this.state.focusedItem || this.state.inputFocused) {
return;
}
if (this.state.focusedItem.type === "MATCH") {
this.selectMatchItem(this.state.focusedItem);
}
};
onFocus = item => {
if (this.state.focusedItem !== item) {
this.setState({ focusedItem: item });
}
};
inputOnChange = e => {
const inputValue = e.target.value;
const { cx, clearSearch } = this.props;
this.setState({ inputValue });
if (inputValue === "") {
clearSearch(cx);
}
};
renderFile = (file, focused, expanded) => {
const matchesLength = file.matches.length;
const matches = ` (${matchesLength} match${matchesLength > 1 ? "es" : ""})`;
return (
{file.location.source.url
? getRelativePath(file.location.source.url)
: getFormattedSourceId(file.location.source.id)}
{matches}
);
};
renderMatch = (match, focused) => {
return (
setTimeout(() => this.selectMatchItem(match), 50)}
>
{match.location.line}
{this.highlightMatches(match)}
);
};
renderItem = (item, depth, focused, _, expanded) => {
if (item.type === "RESULT") {
return this.renderFile(item, focused, expanded);
}
return this.renderMatch(item, focused);
};
renderResults = () => {
const { status, results } = this.props;
if (!this.props.query) {
return null;
}
if (results.length) {
return (
results}
getChildren={file => file.matches || []}
itemHeight={24}
autoExpandAll={true}
autoExpandDepth={1}
autoExpandNodeChildrenLimit={100}
getParent={item => null}
getPath={getFilePath}
renderItem={this.renderItem}
focused={this.state.focusedItem}
onFocus={this.onFocus}
isExpanded={item => {
return this.state.expanded.has(item);
}}
onExpand={item => {
const { expanded } = this.state;
expanded.add(item);
this.setState({ expanded });
}}
onCollapse={item => {
const { expanded } = this.state;
expanded.delete(item);
this.setState({ expanded });
}}
getKey={getFilePath}
/>
);
}
const msg =
status === statusType.fetching
? L10N.getStr("loadingText")
: L10N.getStr("projectTextSearch.noResults");
return {msg}
;
};
renderSummary = () => {
if (this.props.query !== "") {
const resultsSummaryString = L10N.getStr("sourceSearch.resultsSummary2");
const count = this.getResultCount();
return PluralForm.get(count, resultsSummaryString).replace("#1", count);
}
return "";
};
shouldShowErrorEmoji() {
return !this.getResultCount() && this.props.status === statusType.done;
}
renderInput() {
const { status } = this.props;
return (
this.setState({ inputFocused: true })}
onBlur={() => this.setState({ inputFocused: false })}
onKeyDown={this.onKeyDown}
onHistoryScroll={this.onHistoryScroll}
showClose={false}
showExcludePatterns={true}
excludePatternsLabel={L10N.getStr(
"projectTextSearch.excludePatterns.label"
)}
excludePatternsPlaceholder={L10N.getStr(
"projectTextSearch.excludePatterns.placeholder"
)}
ref="searchInput"
showSearchModifiers={true}
searchKey={searchKeys.PROJECT_SEARCH}
onToggleSearchModifier={() => this.doSearch(this.state.inputValue)}
/>
);
}
render() {
return (
{this.renderInput()}
{this.renderResults()}
);
}
}
ProjectSearch.contextTypes = {
shortcuts: PropTypes.object,
};
const mapStateToProps = state => ({
cx: getContext(state),
results: getProjectSearchResults(state),
query: getProjectSearchQuery(state),
status: getProjectSearchStatus(state),
});
export default connect(mapStateToProps, {
searchSources: actions.searchSources,
clearSearch: actions.clearSearch,
selectSpecificLocation: actions.selectSpecificLocation,
setActiveSearch: actions.setActiveSearch,
doSearchForHighlight: actions.doSearchForHighlight,
})(ProjectSearch);