diff options
Diffstat (limited to 'devtools/client/debugger/src/workers/search')
8 files changed, 357 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/workers/search/get-matches.js b/devtools/client/debugger/src/workers/search/get-matches.js new file mode 100644 index 0000000000..947e75a7c1 --- /dev/null +++ b/devtools/client/debugger/src/workers/search/get-matches.js @@ -0,0 +1,49 @@ +/* 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 assert from "../../utils/assert"; +import buildQuery from "../../utils/build-query"; + +import type { SearchModifiers } from "../../types"; + +export default function getMatches( + query: string, + text: string, + modifiers: SearchModifiers +): Object[] { + if (!query || !text || !modifiers) { + return []; + } + const regexQuery = buildQuery(query, modifiers, { + isGlobal: true, + }); + const matchedLocations = []; + const lines = text.split("\n"); + for (let i = 0; i < lines.length; i++) { + let singleMatch; + const line = lines[i]; + while ((singleMatch = regexQuery.exec(line)) !== null) { + // Flow doesn't understand the test above. + if (!singleMatch) { + throw new Error("no singleMatch"); + } + + matchedLocations.push({ line: i, ch: singleMatch.index }); + + // When the match is an empty string the regexQuery.lastIndex will not + // change resulting in an infinite loop so we need to check for this and + // increment it manually in that case. See issue #7023 + if (singleMatch[0] === "") { + assert( + !regexQuery.unicode, + "lastIndex++ can cause issues in unicode mode" + ); + regexQuery.lastIndex++; + } + } + } + return matchedLocations; +} diff --git a/devtools/client/debugger/src/workers/search/index.js b/devtools/client/debugger/src/workers/search/index.js new file mode 100644 index 0000000000..39ec9fa25e --- /dev/null +++ b/devtools/client/debugger/src/workers/search/index.js @@ -0,0 +1,40 @@ +/* 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 { workerUtils } from "devtools-utils"; +const { WorkerDispatcher } = workerUtils; + +let startArgs; +let dispatcher; + +function getDispatcher() { + if (!dispatcher) { + dispatcher = new WorkerDispatcher(); + dispatcher.start(...startArgs); + } + + return dispatcher; +} + +export const start = (...args: any[]) => { + startArgs = args; +}; + +export const stop = () => { + if (dispatcher) { + dispatcher.stop(); + dispatcher = null; + startArgs = null; + } +}; + +export const getMatches = (...args: any[]) => { + return getDispatcher().invoke("getMatches", ...args); +}; + +export const findSourceMatches = (...args: any[]) => { + return getDispatcher().invoke("findSourceMatches", ...args); +}; diff --git a/devtools/client/debugger/src/workers/search/moz.build b/devtools/client/debugger/src/workers/search/moz.build new file mode 100644 index 0000000000..b7223ac81a --- /dev/null +++ b/devtools/client/debugger/src/workers/search/moz.build @@ -0,0 +1,10 @@ +# vim: set filetype=python: +# 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/. + +DIRS += [] + +CompiledModules( + "index.js", +) diff --git a/devtools/client/debugger/src/workers/search/project-search.js b/devtools/client/debugger/src/workers/search/project-search.js new file mode 100644 index 0000000000..c55f649cb4 --- /dev/null +++ b/devtools/client/debugger/src/workers/search/project-search.js @@ -0,0 +1,83 @@ +/* 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 + +// Maybe reuse file search's functions? + +import getMatches from "./get-matches"; + +import type { SourceId, TextSourceContent } from "../../types"; + +export function findSourceMatches( + sourceId: SourceId, + content: TextSourceContent, + queryText: string +): Object[] { + if (queryText == "") { + return []; + } + + const modifiers = { + caseSensitive: false, + regexMatch: false, + wholeWord: false, + }; + + const text = content.value; + const lines = text.split("\n"); + + return getMatches(queryText, text, modifiers).map(({ line, ch }) => { + const { value, matchIndex } = truncateLine(lines[line], ch); + return { + sourceId, + line: line + 1, + column: ch, + matchIndex, + match: queryText, + value, + }; + }); +} + +// This is used to find start of a word, so that cropped string look nice +const startRegex = /([ !@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?])/g; +// Similarly, find +const endRegex = new RegExp( + [ + "([ !@#$%^&*()_+-=[]{};':\"\\|,.<>/?])", + '[^ !@#$%^&*()_+-=[]{};\':"\\|,.<>/?]*$"/', + ].join("") +); + +function truncateLine(text: string, column: number) { + if (text.length < 100) { + return { + matchIndex: column, + value: text, + }; + } + + // Initially take 40 chars left to the match + const offset = Math.max(column - 40, 0); + // 400 characters should be enough to figure out the context of the match + const truncStr = text.slice(offset, column + 400); + let start = truncStr.search(startRegex); + let end = truncStr.search(endRegex); + + if (start > column) { + // No word separator found before the match, so we take all characters + // before the match + start = -1; + } + if (end < column) { + end = truncStr.length; + } + const value = truncStr.slice(start + 1, end); + + return { + matchIndex: column - start - offset - 1, + value, + }; +} diff --git a/devtools/client/debugger/src/workers/search/tests/__snapshots__/project-search.spec.js.snap b/devtools/client/debugger/src/workers/search/tests/__snapshots__/project-search.spec.js.snap new file mode 100644 index 0000000000..193e549b03 --- /dev/null +++ b/devtools/client/debugger/src/workers/search/tests/__snapshots__/project-search.spec.js.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`project search finds matches 1`] = ` +Array [ + Object { + "column": 11, + "line": 2, + "match": "foo", + "matchIndex": 11, + "sourceId": "bar.js", + "value": " function foo() {", + }, + Object { + "column": 4, + "line": 3, + "match": "foo", + "matchIndex": 4, + "sourceId": "bar.js", + "value": " foo();", + }, +] +`; diff --git a/devtools/client/debugger/src/workers/search/tests/get-matches.spec.js b/devtools/client/debugger/src/workers/search/tests/get-matches.spec.js new file mode 100644 index 0000000000..2ce9151fc4 --- /dev/null +++ b/devtools/client/debugger/src/workers/search/tests/get-matches.spec.js @@ -0,0 +1,101 @@ +/* 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 getMatches from "../get-matches"; + +describe("search", () => { + describe("getMatches", () => { + it("gets basic string match", () => { + const text = "the test string with test in it multiple times test."; + const query = "test"; + const matchLocations = getMatches(query, text, { + caseSensitive: true, + wholeWord: false, + regexMatch: false, + }); + expect(matchLocations).toHaveLength(3); + }); + + it("gets basic string match case-sensitive", () => { + const text = "the Test string with test in it multiple times test."; + const query = "Test"; + const matchLocations = getMatches(query, text, { + caseSensitive: true, + wholeWord: false, + regexMatch: false, + }); + expect(matchLocations).toHaveLength(1); + }); + + it("gets whole word string match", () => { + const text = "the test string test in it multiple times whoatestthe."; + const query = "test"; + const matchLocations = getMatches(query, text, { + caseSensitive: true, + wholeWord: true, + regexMatch: false, + }); + expect(matchLocations).toHaveLength(2); + }); + + it("gets regex match", () => { + const text = "the test string test in it multiple times whoatestthe."; + const query = "(\\w+)\\s+(\\w+)"; + const matchLocations = getMatches(query, text, { + caseSensitive: true, + wholeWord: false, + regexMatch: true, + }); + expect(matchLocations).toHaveLength(4); + }); + + it("it doesnt fail on empty data", () => { + const text = ""; + const query = ""; + const matchLocations = getMatches(query, text, { + caseSensitive: true, + wholeWord: false, + regexMatch: true, + }); + expect(matchLocations).toHaveLength(0); + }); + + it("fails gracefully when the line is too long", () => { + const text = Array(100002).join("x"); + const query = "query"; + const matchLocations = getMatches(query, text, { + caseSensitive: true, + wholeWord: false, + regexMatch: true, + }); + expect(matchLocations).toHaveLength(0); + }); + + // regression test for #6896 + it("doesn't crash on the regex 'a*'", () => { + const text = "abc"; + const query = "a*"; + const matchLocations = getMatches(query, text, { + caseSensitive: true, + wholeWord: false, + regexMatch: true, + }); + expect(matchLocations).toHaveLength(4); + }); + + // regression test for #6896 + it("doesn't crash on the regex '^'", () => { + const text = "012"; + const query = "^"; + const matchLocations = getMatches(query, text, { + caseSensitive: true, + wholeWord: false, + regexMatch: true, + }); + expect(matchLocations).toHaveLength(1); + }); + }); +}); diff --git a/devtools/client/debugger/src/workers/search/tests/project-search.spec.js b/devtools/client/debugger/src/workers/search/tests/project-search.spec.js new file mode 100644 index 0000000000..3374d316d9 --- /dev/null +++ b/devtools/client/debugger/src/workers/search/tests/project-search.spec.js @@ -0,0 +1,40 @@ +/* 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 { findSourceMatches } from "../project-search"; + +const text = ` + function foo() { + foo(); + } +`; + +describe("project search", () => { + const emptyResults = []; + + it("finds matches", () => { + const needle = "foo"; + const content = { + type: "text", + value: text, + contentType: undefined, + }; + + const matches = findSourceMatches("bar.js", content, needle); + expect(matches).toMatchSnapshot(); + }); + + it("finds no matches in source", () => { + const needle = "test"; + const content = { + type: "text", + value: text, + contentType: undefined, + }; + const matches = findSourceMatches("bar.js", content, needle); + expect(matches).toEqual(emptyResults); + }); +}); diff --git a/devtools/client/debugger/src/workers/search/worker.js b/devtools/client/debugger/src/workers/search/worker.js new file mode 100644 index 0000000000..bf3ddcf513 --- /dev/null +++ b/devtools/client/debugger/src/workers/search/worker.js @@ -0,0 +1,12 @@ +/* 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 getMatches from "./get-matches"; +import { findSourceMatches } from "./project-search"; +import { workerUtils } from "devtools-utils"; +const { workerHandler } = workerUtils; + +self.onmessage = workerHandler({ getMatches, findSourceMatches }); |