summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/src/utils/editor/source-search.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/debugger/src/utils/editor/source-search.js')
-rw-r--r--devtools/client/debugger/src/utils/editor/source-search.js350
1 files changed, 350 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/utils/editor/source-search.js b/devtools/client/debugger/src/utils/editor/source-search.js
new file mode 100644
index 0000000000..044ed5b3fa
--- /dev/null
+++ b/devtools/client/debugger/src/utils/editor/source-search.js
@@ -0,0 +1,350 @@
+/* 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/>. */
+
+// @flow
+
+import buildQuery from "../build-query";
+
+import type { SearchModifiers } from "../../types";
+
+/**
+ * @memberof utils/source-search
+ * @static
+ */
+function getSearchCursor(cm, query: string, pos, modifiers: SearchModifiers) {
+ const regexQuery = buildQuery(query, modifiers, { isGlobal: true });
+ return cm.getSearchCursor(regexQuery, pos);
+}
+
+/**
+ * @memberof utils/source-search
+ * @static
+ */
+function SearchState(): void {
+ this.posFrom = this.posTo = this.query = null;
+ this.overlay = null;
+ this.results = [];
+}
+
+/**
+ * @memberof utils/source-search
+ * @static
+ */
+function getSearchState(cm: any, query: string) {
+ const state = cm.state.search || (cm.state.search = new SearchState());
+ return state;
+}
+
+function isWhitespace(query): boolean {
+ return !query.match(/\S/);
+}
+
+/**
+ * This returns a mode object used by CoeMirror's addOverlay function
+ * to parse and style tokens in the file.
+ * The mode object contains a tokenizer function (token) which takes
+ * a character stream as input, advances it a character at a time,
+ * and returns style(s) for that token. For more details see
+ * https://codemirror.net/doc/manual.html#modeapi
+ *
+ * Also the token function code is mainly based of work done
+ * by the chrome devtools team. Thanks guys! :)
+ *
+ * @memberof utils/source-search
+ * @static
+ */
+function searchOverlay(query, modifiers) {
+ const regexQuery = buildQuery(query, modifiers, {
+ ignoreSpaces: true,
+ // regex must be global for the overlay
+ isGlobal: true,
+ });
+
+ return {
+ token: function(stream, state) {
+ // set the last index to be the current stream position
+ // this acts as an offset
+ regexQuery.lastIndex = stream.pos;
+ const match = regexQuery.exec(stream.string);
+ if (match && match.index === stream.pos) {
+ // if we have a match at the current stream position
+ // set the class for a match
+ stream.pos += match[0].length || 1;
+ return "highlight highlight-full";
+ } else if (match) {
+ // if we have a match somewhere in the line, go to that point in the
+ // stream
+ stream.pos = match.index;
+ } else {
+ // if we have no matches in this line, skip to the end of the line
+ stream.skipToEnd();
+ }
+ },
+ };
+}
+
+/**
+ * @memberof utils/source-search
+ * @static
+ */
+function updateOverlay(cm, state, query, modifiers): void {
+ cm.removeOverlay(state.overlay);
+ state.overlay = searchOverlay(query, modifiers);
+ cm.addOverlay(state.overlay, { opaque: false });
+}
+
+function updateCursor(cm, state, keepSelection): void {
+ state.posTo = cm.getCursor("anchor");
+ state.posFrom = cm.getCursor("head");
+
+ if (!keepSelection) {
+ state.posTo = { line: 0, ch: 0 };
+ state.posFrom = { line: 0, ch: 0 };
+ }
+}
+
+export function getMatchIndex(
+ count: number,
+ currentIndex: number,
+ rev: boolean
+): number {
+ if (!rev) {
+ if (currentIndex == count - 1) {
+ return 0;
+ }
+
+ return currentIndex + 1;
+ }
+
+ if (currentIndex == 0) {
+ return count - 1;
+ }
+
+ return currentIndex - 1;
+}
+
+/**
+ * If there's a saved search, selects the next results.
+ * Otherwise, creates a new search and selects the first
+ * result.
+ *
+ * @memberof utils/source-search
+ * @static
+ */
+function doSearch(
+ ctx,
+ rev,
+ query,
+ keepSelection,
+ modifiers: SearchModifiers,
+ focusFirstResult?: boolean = true
+) {
+ const { cm, ed } = ctx;
+ if (!cm) {
+ return;
+ }
+ const defaultIndex = { line: -1, ch: -1 };
+
+ return cm.operation(function() {
+ if (!query || isWhitespace(query)) {
+ clearSearch(cm, query);
+ return;
+ }
+
+ const state = getSearchState(cm, query);
+ const isNewQuery = state.query !== query;
+ state.query = query;
+
+ updateOverlay(cm, state, query, modifiers);
+ updateCursor(cm, state, keepSelection);
+ const searchLocation = searchNext(ctx, rev, query, isNewQuery, modifiers);
+
+ // We don't want to jump the editor
+ // when we're selecting text
+ if (!cm.state.selectingText && searchLocation && focusFirstResult) {
+ ed.alignLine(searchLocation.from.line, "center");
+ cm.setSelection(searchLocation.from, searchLocation.to);
+ }
+
+ return searchLocation ? searchLocation.from : defaultIndex;
+ });
+}
+
+export function searchSourceForHighlight(
+ ctx: Object,
+ rev: boolean,
+ query: string,
+ keepSelection: boolean,
+ modifiers: SearchModifiers,
+ line: number,
+ ch: number
+) {
+ const { cm } = ctx;
+ if (!cm) {
+ return;
+ }
+
+ return cm.operation(function() {
+ const state = getSearchState(cm, query);
+ const isNewQuery = state.query !== query;
+ state.query = query;
+
+ updateOverlay(cm, state, query, modifiers);
+ updateCursor(cm, state, keepSelection);
+ findNextOnLine(ctx, rev, query, isNewQuery, modifiers, line, ch);
+ });
+}
+
+function getCursorPos(newQuery, rev, state) {
+ if (newQuery) {
+ return rev ? state.posFrom : state.posTo;
+ }
+
+ return rev ? state.posTo : state.posFrom;
+}
+
+/**
+ * Selects the next result of a saved search.
+ *
+ * @memberof utils/source-search
+ * @static
+ */
+function searchNext(ctx, rev, query, newQuery, modifiers) {
+ const { cm } = ctx;
+ let nextMatch;
+ cm.operation(function() {
+ const state = getSearchState(cm, query);
+ const pos = getCursorPos(newQuery, rev, state);
+
+ if (!state.query) {
+ return;
+ }
+
+ let cursor = getSearchCursor(cm, state.query, pos, modifiers);
+
+ const location = rev
+ ? { line: cm.lastLine(), ch: null }
+ : { line: cm.firstLine(), ch: 0 };
+
+ if (!cursor.find(rev) && state.query) {
+ cursor = getSearchCursor(cm, state.query, location, modifiers);
+ if (!cursor.find(rev)) {
+ return;
+ }
+ }
+
+ nextMatch = { from: cursor.from(), to: cursor.to() };
+ });
+
+ return nextMatch;
+}
+
+function findNextOnLine(ctx, rev, query, newQuery, modifiers, line, ch): void {
+ const { cm, ed } = ctx;
+ cm.operation(function() {
+ const pos = { line: line - 1, ch };
+ let cursor = getSearchCursor(cm, query, pos, modifiers);
+
+ if (!cursor.find(rev) && query) {
+ cursor = getSearchCursor(cm, query, pos, modifiers);
+ if (!cursor.find(rev)) {
+ return;
+ }
+ }
+
+ // We don't want to jump the editor
+ // when we're selecting text
+ if (!cm.state.selectingText) {
+ ed.alignLine(cursor.from().line, "center");
+ cm.setSelection(cursor.from(), cursor.to());
+ }
+ });
+}
+
+/**
+ * Remove overlay.
+ *
+ * @memberof utils/source-search
+ * @static
+ */
+export function removeOverlay(ctx: any, query: string): void {
+ const state = getSearchState(ctx.cm, query);
+ ctx.cm.removeOverlay(state.overlay);
+ const { line, ch } = ctx.cm.getCursor();
+ ctx.cm.doc.setSelection({ line, ch }, { line, ch }, { scroll: false });
+}
+
+/**
+ * Clears the currently saved search.
+ *
+ * @memberof utils/source-search
+ * @static
+ */
+export function clearSearch(cm: any, query: string): void {
+ const state = getSearchState(cm, query);
+
+ state.results = [];
+
+ if (!state.query) {
+ return;
+ }
+ cm.removeOverlay(state.overlay);
+ state.query = null;
+}
+
+/**
+ * Starts a new search.
+ *
+ * @memberof utils/source-search
+ * @static
+ */
+export function find(
+ ctx: any,
+ query: string,
+ keepSelection: boolean,
+ modifiers: SearchModifiers,
+ focusFirstResult?: boolean
+) {
+ clearSearch(ctx.cm, query);
+ return doSearch(
+ ctx,
+ false,
+ query,
+ keepSelection,
+ modifiers,
+ focusFirstResult
+ );
+}
+
+/**
+ * Finds the next item based on the currently saved search.
+ *
+ * @memberof utils/source-search
+ * @static
+ */
+export function findNext(
+ ctx: any,
+ query: string,
+ keepSelection: boolean,
+ modifiers: SearchModifiers
+) {
+ return doSearch(ctx, false, query, keepSelection, modifiers);
+}
+
+/**
+ * Finds the previous item based on the currently saved search.
+ *
+ * @memberof utils/source-search
+ * @static
+ */
+export function findPrev(
+ ctx: any,
+ query: string,
+ keepSelection: boolean,
+ modifiers: SearchModifiers
+) {
+ return doSearch(ctx, true, query, keepSelection, modifiers);
+}
+
+export { buildQuery };