summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/src/utils/source.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/client/debugger/src/utils/source.js536
1 files changed, 536 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/utils/source.js b/devtools/client/debugger/src/utils/source.js
new file mode 100644
index 0000000000..245caa67a9
--- /dev/null
+++ b/devtools/client/debugger/src/utils/source.js
@@ -0,0 +1,536 @@
+/* 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/>. */
+
+/**
+ * Utils for working with Source URLs
+ * @module utils/source
+ */
+
+const { getUnicodeUrl } = require("devtools/client/shared/unicode-url");
+const {
+ micromatch,
+} = require("devtools/client/shared/vendor/micromatch/micromatch.js");
+
+import { getRelativePath } from "../utils/sources-tree/utils";
+import { endTruncateStr } from "./utils";
+import { truncateMiddleText } from "../utils/text";
+import { parse as parseURL } from "../utils/url";
+import { memoizeLast } from "../utils/memoizeLast";
+import { renderWasmText } from "./wasm";
+import { toEditorLine } from "./editor";
+export { isMinified } from "./isMinified";
+
+import { isFulfilled } from "./async-value";
+
+export const sourceTypes = {
+ coffee: "coffeescript",
+ js: "javascript",
+ jsx: "react",
+ ts: "typescript",
+ tsx: "typescript",
+ vue: "vue",
+};
+
+export const javascriptLikeExtensions = new Set(["marko", "es6", "vue", "jsm"]);
+
+function getPath(source) {
+ const { path } = source.displayURL;
+ let lastIndex = path.lastIndexOf("/");
+ let nextToLastIndex = path.lastIndexOf("/", lastIndex - 1);
+
+ const result = [];
+ do {
+ result.push(path.slice(nextToLastIndex + 1, lastIndex));
+ lastIndex = nextToLastIndex;
+ nextToLastIndex = path.lastIndexOf("/", lastIndex - 1);
+ } while (lastIndex !== nextToLastIndex);
+
+ result.push("");
+
+ return result;
+}
+
+export function shouldBlackbox(source) {
+ if (!source) {
+ return false;
+ }
+
+ if (!source.url) {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Checks if the frame is within a line ranges which are blackboxed
+ * in the source.
+ *
+ * @param {Object} frame
+ * The current frame
+ * @param {Object} blackboxedRanges
+ * The currently blackboxedRanges for all the sources.
+ * @param {Boolean} isFrameBlackBoxed
+ * If the frame is within the blackboxed range
+ * or not.
+ */
+export function isFrameBlackBoxed(frame, blackboxedRanges) {
+ return (
+ frame.source &&
+ !!blackboxedRanges[frame.source.url] &&
+ (!blackboxedRanges[frame.source.url].length ||
+ !!findBlackBoxRange(frame.source, blackboxedRanges, {
+ start: frame.location.line,
+ end: frame.location.line,
+ }))
+ );
+}
+
+/**
+ * Checks if a blackbox range exist for the line range.
+ * That is if any start and end lines overlap any of the
+ * blackbox ranges
+ *
+ * @param {Object} source
+ * The current selected source
+ * @param {Object} blackboxedRanges
+ * The store of blackboxedRanges
+ * @param {Object} lineRange
+ * The start/end line range `{ start: <Number>, end: <Number> }`
+ * @return {Object} blackboxRange
+ * The first matching blackbox range that all or part of the
+ * specified lineRange sits within.
+ */
+export function findBlackBoxRange(source, blackboxedRanges, lineRange) {
+ const ranges = blackboxedRanges[source.url];
+ if (!ranges || !ranges.length) {
+ return null;
+ }
+
+ return ranges.find(
+ range =>
+ (lineRange.start >= range.start.line &&
+ lineRange.start <= range.end.line) ||
+ (lineRange.end >= range.start.line && lineRange.end <= range.end.line)
+ );
+}
+
+/**
+ * Checks if a source line is blackboxed
+ * @param {Array} ranges - Line ranges that are blackboxed
+ * @param {Number} line
+ * @param {Boolean} isSourceOnIgnoreList - is the line in a source that is on
+ * the sourcemap ignore lists then the line is blackboxed.
+ * @returns boolean
+ */
+export function isLineBlackboxed(ranges, line, isSourceOnIgnoreList) {
+ if (isSourceOnIgnoreList) {
+ return true;
+ }
+
+ if (!ranges) {
+ return false;
+ }
+ // If the whole source is ignored , then the line is
+ // ignored.
+ if (!ranges.length) {
+ return true;
+ }
+ return !!ranges.find(
+ range => line >= range.start.line && line <= range.end.line
+ );
+}
+
+/**
+ * Returns true if the specified url and/or content type are specific to
+ * javascript files.
+ *
+ * @return boolean
+ * True if the source is likely javascript.
+ *
+ * @memberof utils/source
+ * @static
+ */
+export function isJavaScript(source, content) {
+ const extension = source.displayURL.fileExtension;
+ const contentType = content.type === "wasm" ? null : content.contentType;
+ return (
+ javascriptLikeExtensions.has(extension) ||
+ !!(contentType && contentType.includes("javascript"))
+ );
+}
+
+/**
+ * @memberof utils/source
+ * @static
+ */
+export function isPretty(source) {
+ return isPrettyURL(source.url);
+}
+
+export function isPrettyURL(url) {
+ return url ? url.endsWith(":formatted") : false;
+}
+
+/**
+ * @memberof utils/source
+ * @static
+ */
+export function getPrettySourceURL(url) {
+ if (!url) {
+ url = "";
+ }
+ return `${url}:formatted`;
+}
+
+/**
+ * @memberof utils/source
+ * @static
+ */
+export function getRawSourceURL(url) {
+ return url && url.endsWith(":formatted")
+ ? url.slice(0, -":formatted".length)
+ : url;
+}
+
+function resolveFileURL(
+ url,
+ transformUrl = initialUrl => initialUrl,
+ truncate = true
+) {
+ url = getRawSourceURL(url || "");
+ const name = transformUrl(url);
+ if (!truncate) {
+ return name;
+ }
+ return endTruncateStr(name, 50);
+}
+
+export function getFormattedSourceId(id) {
+ return id.substring(id.lastIndexOf("/") + 1);
+}
+
+/**
+ * Gets a readable filename from a source URL for display purposes.
+ * If the source does not have a URL, the source ID will be returned instead.
+ *
+ * @memberof utils/source
+ * @static
+ */
+export function getFilename(
+ source,
+ rawSourceURL = getRawSourceURL(source.url)
+) {
+ const { id } = source;
+ if (!rawSourceURL) {
+ return getFormattedSourceId(id);
+ }
+
+ const { filename } = source.displayURL;
+ return getRawSourceURL(filename);
+}
+
+/**
+ * Provides a middle-trunated filename
+ *
+ * @memberof utils/source
+ * @static
+ */
+export function getTruncatedFileName(source, querystring = "", length = 30) {
+ return truncateMiddleText(`${getFilename(source)}${querystring}`, length);
+}
+
+/* Gets path for files with same filename for editor tabs, breakpoints, etc.
+ * Pass the source, and list of other sources
+ *
+ * @memberof utils/source
+ * @static
+ */
+
+export function getDisplayPath(mySource, sources) {
+ const rawSourceURL = getRawSourceURL(mySource.url);
+ const filename = getFilename(mySource, rawSourceURL);
+
+ // Find sources that have the same filename, but different paths
+ // as the original source
+ const similarSources = sources.filter(source => {
+ const rawSource = getRawSourceURL(source.url);
+ return (
+ rawSourceURL != rawSource && filename == getFilename(source, rawSource)
+ );
+ });
+
+ if (!similarSources.length) {
+ return undefined;
+ }
+
+ // get an array of source path directories e.g. ['a/b/c.html'] => [['b', 'a']]
+ const paths = new Array(similarSources.length + 1);
+
+ paths[0] = getPath(mySource);
+ for (let i = 0; i < similarSources.length; ++i) {
+ paths[i + 1] = getPath(similarSources[i]);
+ }
+
+ // create an array of similar path directories and one dis-similar directory
+ // for example [`a/b/c.html`, `a1/b/c.html`] => ['b', 'a']
+ // where 'b' is the similar directory and 'a' is the dis-similar directory.
+ let displayPath = "";
+ for (let i = 0; i < paths[0].length; i++) {
+ let similar = false;
+ for (let k = 1; k < paths.length; ++k) {
+ if (paths[k][i] === paths[0][i]) {
+ similar = true;
+ break;
+ }
+ }
+
+ displayPath = paths[0][i] + (i !== 0 ? "/" : "") + displayPath;
+
+ if (!similar) {
+ break;
+ }
+ }
+
+ return displayPath;
+}
+
+/**
+ * Gets a readable source URL for display purposes.
+ * If the source does not have a URL, the source ID will be returned instead.
+ *
+ * @memberof utils/source
+ * @static
+ */
+export function getFileURL(source, truncate = true) {
+ const { url, id } = source;
+ if (!url) {
+ return getFormattedSourceId(id);
+ }
+
+ return resolveFileURL(url, getUnicodeUrl, truncate);
+}
+
+export function getSourcePath(url) {
+ if (!url) {
+ return "";
+ }
+
+ const { path, href } = parseURL(url);
+ // for URLs like "about:home" the path is null so we pass the full href
+ return path || href;
+}
+
+/**
+ * Returns amount of lines in the source. If source is a WebAssembly binary,
+ * the function returns amount of bytes.
+ */
+export function getSourceLineCount(content) {
+ if (content.type === "wasm") {
+ const { binary } = content.value;
+ return binary.length;
+ }
+
+ let count = 0;
+
+ for (let i = 0; i < content.value.length; ++i) {
+ if (content.value[i] === "\n") {
+ ++count;
+ }
+ }
+
+ return count + 1;
+}
+
+export function isInlineScript(source) {
+ return source.introductionType === "scriptElement";
+}
+
+function getNthLine(str, lineNum) {
+ let startIndex = -1;
+
+ let newLinesFound = 0;
+ while (newLinesFound < lineNum) {
+ const nextIndex = str.indexOf("\n", startIndex + 1);
+ if (nextIndex === -1) {
+ return null;
+ }
+ startIndex = nextIndex;
+ newLinesFound++;
+ }
+ const endIndex = str.indexOf("\n", startIndex + 1);
+ if (endIndex === -1) {
+ return str.slice(startIndex + 1);
+ }
+
+ return str.slice(startIndex + 1, endIndex);
+}
+
+export const getLineText = memoizeLast((sourceId, asyncContent, line) => {
+ if (!asyncContent || !isFulfilled(asyncContent)) {
+ return "";
+ }
+
+ const content = asyncContent.value;
+
+ if (content.type === "wasm") {
+ const editorLine = toEditorLine(sourceId, line);
+ const lines = renderWasmText(sourceId, content);
+ return lines[editorLine] || "";
+ }
+
+ const lineText = getNthLine(content.value, line - 1);
+ return lineText || "";
+});
+
+export function getTextAtPosition(sourceId, asyncContent, location) {
+ const { column, line = 0 } = location;
+
+ const lineText = getLineText(sourceId, asyncContent, line);
+ return lineText.slice(column, column + 100).trim();
+}
+
+/**
+ * Compute the CSS classname string to use for the icon of a given source.
+ *
+ * @param {Object} source
+ * The reducer source object.
+ * @param {Object} symbols
+ * The reducer symbol object for the given source.
+ * @param {Boolean} isBlackBoxed
+ * To be set to true, when the given source is blackboxed.
+ * @param {Boolean} hasPrettyTab
+ * To be set to true, if the given source isn't the pretty printed one,
+ * but another tab for that source is opened pretty printed.
+ * @return String
+ * The classname to use.
+ */
+export function getSourceClassnames(
+ source,
+ symbols,
+ isBlackBoxed,
+ hasPrettyTab = false
+) {
+ // Conditionals should be ordered by priority of icon!
+ const defaultClassName = "file";
+
+ if (!source || !source.url) {
+ return defaultClassName;
+ }
+
+ // In the SourceTree, we don't show the pretty printed sources,
+ // but still want to show the pretty print icon when a pretty printed tab
+ // for the current source is opened.
+ if (isPretty(source) || hasPrettyTab) {
+ return "prettyPrint";
+ }
+
+ if (isBlackBoxed) {
+ return "blackBox";
+ }
+
+ if (symbols && symbols.framework) {
+ return symbols.framework.toLowerCase();
+ }
+
+ if (isUrlExtension(source.url)) {
+ return "extension";
+ }
+
+ return sourceTypes[source.displayURL.fileExtension] || defaultClassName;
+}
+
+export function getRelativeUrl(source, root) {
+ const { group, path } = source.displayURL;
+ if (!root) {
+ return path;
+ }
+
+ // + 1 removes the leading "/"
+ const url = group + path;
+ return url.slice(url.indexOf(root) + root.length + 1);
+}
+
+/**
+ * source.url doesn't include thread actor ID, so before calling underRoot(), the thread actor ID
+ * must be removed from the root, which this function handles.
+ * @param {string} root The root url to be cleaned
+ * @param {Set<Thread>} threads The list of threads
+ * @returns {string} The root url with thread actor IDs removed
+ */
+export function removeThreadActorId(root, threads) {
+ threads.forEach(thread => {
+ if (root.includes(thread.actor)) {
+ root = root.slice(thread.actor.length + 1);
+ }
+ });
+ return root;
+}
+
+/**
+ * Checks if the source is descendant of the root identified by the
+ * root url specified. The root might likely be projectDirectoryRoot which
+ * is a defined by a pref that allows users restrict the source tree to
+ * a subset of sources.
+ *
+ * @param {Object} source
+ * The source object
+ * @param {String} rootUrlWithoutThreadActor
+ * The url for the root node, without the thread actor ID. This can be obtained
+ * by calling removeThreadActorId()
+ */
+export function isDescendantOfRoot(source, rootUrlWithoutThreadActor) {
+ if (source.url && source.url.includes("chrome://")) {
+ const { group, path } = source.displayURL;
+ return (group + path).includes(rootUrlWithoutThreadActor);
+ }
+
+ return !!source.url && source.url.includes(rootUrlWithoutThreadActor);
+}
+
+export function isGenerated(source) {
+ return !source.isOriginal;
+}
+
+export function getSourceQueryString(source) {
+ if (!source) {
+ return "";
+ }
+
+ return parseURL(getRawSourceURL(source.url)).search;
+}
+
+export function isUrlExtension(url) {
+ return url.includes("moz-extension:") || url.includes("chrome-extension");
+}
+
+/**
+* Checks that source url matches one of the glob patterns
+*
+* @param {Object} source
+* @param {String} excludePatterns
+ String of comma-seperated glob patterns
+* @return {return} Boolean value specifies if the string matches any
+ of the patterns.
+*/
+export function matchesGlobPatterns(source, excludePatterns) {
+ if (!excludePatterns) {
+ return false;
+ }
+ const patterns = excludePatterns
+ .split(",")
+ .map(pattern => pattern.trim())
+ .filter(pattern => pattern !== "");
+
+ if (!patterns.length) {
+ return false;
+ }
+
+ return micromatch.contains(
+ // Makes sure we format the url or id exactly the way its displayed in the search ui,
+ // as user wil usually create glob patterns based on what is seen in the ui.
+ source.url ? getRelativePath(source.url) : getFormattedSourceId(source.id),
+ patterns
+ );
+}