/* 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 . */ // @flow import React, { Component } from "react"; import { connect } from "react-redux"; import fuzzyAldrin from "fuzzaldrin-plus"; import { basename } from "../utils/path"; import actions from "../actions"; import { getRelativeSources, getQuickOpenEnabled, getQuickOpenQuery, getQuickOpenType, getSelectedSource, getSymbols, getTabs, isSymbolsLoading } from "../selectors"; import { scrollList } from "../utils/result-list"; import { formatSymbols, parseLineColumn, formatShortcutResults, formatSources } from "../utils/quick-open"; import Modal from "./shared/Modal"; import SearchInput from "./shared/SearchInput"; import ResultList from "./shared/ResultList"; import type { FormattedSymbolDeclarations, QuickOpenResult } from "../utils/quick-open"; import type { Location, SourceRecord } from "../types"; import type { QuickOpenType } from "../reducers/quick-open"; import "./QuickOpenModal.css"; type Props = { enabled: boolean, sources: Array, selectedSource?: SourceRecord, query: string, searchType: QuickOpenType, symbols: FormattedSymbolDeclarations, symbolsLoading: boolean, tabs: string[], selectLocation: Location => void, setQuickOpenQuery: (query: string) => void, highlightLineRange: ({ start: number, end: number }) => void, closeQuickOpen: () => void, shortcutsModalEnabled: boolean, toggleShortcutsModal: () => void }; type State = { results: ?Array, selectedIndex: number }; type GotoLocationType = { sourceId?: string, line: number, column?: number }; function filter(values, query) { return fuzzyAldrin.filter(values, query, { key: "value", maxResults: 1000 }); } export class QuickOpenModal extends Component { constructor(props: Props) { super(props); this.state = { results: null, selectedIndex: 0 }; } componentDidMount() { const { query, shortcutsModalEnabled, toggleShortcutsModal } = this.props; this.updateResults(query); if (shortcutsModalEnabled) { toggleShortcutsModal(); } } componentDidUpdate(prevProps: Props) { const nowEnabled = !prevProps.enabled && this.props.enabled; const queryChanged = prevProps.query !== this.props.query; if (this.refs.resultList && this.refs.resultList.refs) { scrollList( this.refs.resultList.refs, this.state.selectedIndex, nowEnabled || !queryChanged ); } if (nowEnabled || queryChanged) { this.updateResults(this.props.query); } } closeModal = () => { this.props.closeQuickOpen(); }; dropGoto = (query: string) => { return query.split(":")[0]; }; searchSources = (query: string) => { const { sources } = this.props; const results = query == "" ? sources : filter(sources, this.dropGoto(query)); return this.setState({ results }); }; searchSymbols = (query: string) => { const { symbols: { functions, variables } } = this.props; let results = functions; if (this.isVariableQuery()) { results = variables; } else { results = results.filter(result => result.title !== "anonymous"); } if (query === "@" || query === "#") { return this.setState({ results }); } this.setState({ results: filter(results, query.slice(1)) }); }; searchShortcuts = (query: string) => { const results = formatShortcutResults(); if (query == "?") { this.setState({ results }); } else { this.setState({ results: filter(results, query.slice(1)) }); } }; showTopSources = () => { const { tabs, sources } = this.props; if (tabs.length > 0) { this.setState({ results: sources.filter(source => tabs.includes(source.url)) }); } else { this.setState({ results: sources.slice(0, 100) }); } }; updateResults = (query: string) => { if (this.isGotoQuery()) { return; } if (query == "" && !this.isShortcutQuery()) { return this.showTopSources(); } if (this.isSymbolSearch()) { return this.searchSymbols(query); } if (this.isShortcutQuery()) { return this.searchShortcuts(query); } return this.searchSources(query); }; setModifier = (item: QuickOpenResult) => { if (["@", "#", ":"].includes(item.id)) { this.props.setQuickOpenQuery(item.id); } }; selectResultItem = ( e: SyntheticEvent, item: ?QuickOpenResult ) => { if (item == null) { return; } if (this.isShortcutQuery()) { return this.setModifier(item); } if (this.isGotoSourceQuery()) { const location = parseLineColumn(this.props.query); return this.gotoLocation({ ...location, sourceId: item.id }); } if (this.isSymbolSearch()) { return this.gotoLocation({ line: item.location && item.location.start ? item.location.start.line : 0 }); } this.gotoLocation({ sourceId: item.id, line: 0 }); }; onSelectResultItem = (item: QuickOpenResult) => { const { selectLocation, selectedSource, highlightLineRange } = this.props; if (!this.isSymbolSearch() || selectedSource == null) { return; } if (this.isVariableQuery()) { const line = item.location && item.location.start ? item.location.start.line : 0; return selectLocation({ sourceId: selectedSource.get("id"), line, column: null }); } if (this.isFunctionQuery()) { return highlightLineRange({ ...(item.location != null ? { start: item.location.start.line, end: item.location.end.line } : {}), sourceId: selectedSource.get("id") }); } }; traverseResults = (e: SyntheticKeyboardEvent) => { const direction = e.key === "ArrowUp" ? -1 : 1; const { selectedIndex, results } = this.state; const resultCount = this.getResultCount(); const index = selectedIndex + direction; const nextIndex = (index + resultCount) % resultCount; this.setState({ selectedIndex: nextIndex }); if (results != null) { this.onSelectResultItem(results[nextIndex]); } }; gotoLocation = (location: ?GotoLocationType) => { const { selectLocation, selectedSource } = this.props; const selectedSourceId = selectedSource ? selectedSource.get("id") : ""; if (location != null) { const sourceId = location.sourceId ? location.sourceId : selectedSourceId; selectLocation({ sourceId, line: location.line, column: location.column || null }); this.closeModal(); } }; onChange = (e: SyntheticInputEvent) => { const { selectedSource, setQuickOpenQuery } = this.props; setQuickOpenQuery(e.target.value); const noSource = !selectedSource || !selectedSource.get("text"); if ((this.isSymbolSearch() && noSource) || this.isGotoQuery()) { return; } this.updateResults(e.target.value); }; onKeyDown = (e: SyntheticKeyboardEvent) => { const { enabled, query } = this.props; const { results, selectedIndex } = this.state; if (!this.isGotoQuery() && (!enabled || !results)) { return; } if (e.key === "Enter") { if (this.isGotoQuery()) { const location = parseLineColumn(query); return this.gotoLocation(location); } if (results) { if (this.isShortcutQuery()) { return this.setModifier(results[selectedIndex]); } return this.selectResultItem(e, results[selectedIndex]); } } if (e.key === "Tab") { return this.closeModal(); } if (["ArrowUp", "ArrowDown"].includes(e.key)) { e.preventDefault(); return this.traverseResults(e); } }; getResultCount = () => { const results = this.state.results; return results && results.length ? results.length : 0; }; // Query helpers isFunctionQuery = () => this.props.searchType === "functions"; isVariableQuery = () => this.props.searchType === "variables"; isSymbolSearch = () => this.isFunctionQuery() || this.isVariableQuery(); isGotoQuery = () => this.props.searchType === "goto"; isGotoSourceQuery = () => this.props.searchType === "gotoSource"; isShortcutQuery = () => this.props.searchType === "shortcuts"; isSourcesQuery = () => this.props.searchType === "sources"; isSourceSearch = () => this.isSourcesQuery() || this.isGotoSourceQuery(); /* eslint-disable react/no-danger */ renderHighlight = (candidateString: string, query: string, name: string) => { const options = { wrap: { tagOpen: '', tagClose: "" } }; const html = fuzzyAldrin.wrap(candidateString, query, options); return
; }; highlightMatching = (query: string, results: QuickOpenResult[]) => { let newQuery = query; if (newQuery === "") { return results; } newQuery = query.replace(/[@:#?]/gi, " "); return results.map(result => { return { ...result, title: this.renderHighlight(result.title, basename(newQuery), "title") }; }); }; shouldShowErrorEmoji() { const { query } = this.props; if (this.isGotoQuery()) { return !/^:\d*$/.test(query); } return !this.getResultCount() && !!query; } renderLoading = () => { const { symbolsLoading } = this.props; if ((this.isFunctionQuery() || this.isVariableQuery()) && symbolsLoading) { return (
{L10N.getStr("loadingText")}
); } }; render() { const { enabled, query } = this.props; const { selectedIndex, results } = this.state; if (!enabled) { return null; } const newResults = results && results.slice(0, 100); const items = this.highlightMatching(query, newResults || []); const expanded = !!items && items.length > 0; return ( {this.renderLoading()} {newResults && ( )} ); } } /* istanbul ignore next: ignoring testing of redux connection stuff */ function mapStateToProps(state) { const selectedSource = getSelectedSource(state); return { enabled: getQuickOpenEnabled(state), sources: formatSources(getRelativeSources(state), getTabs(state).toArray()), selectedSource, symbols: formatSymbols(getSymbols(state, selectedSource)), symbolsLoading: isSymbolsLoading(state, selectedSource), query: getQuickOpenQuery(state), searchType: getQuickOpenType(state), tabs: getTabs(state).toArray() }; } /* istanbul ignore next: ignoring testing of redux connection stuff */ export default connect(mapStateToProps, actions)(QuickOpenModal);