summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/src/utils/source.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/debugger/src/utils/source.js')
-rw-r--r--devtools/client/debugger/src/utils/source.js573
1 files changed, 573 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..90459b6843
--- /dev/null
+++ b/devtools/client/debugger/src/utils/source.js
@@ -0,0 +1,573 @@
+/* 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
+
+/**
+ * Utils for working with Source URLs
+ * @module utils/source
+ */
+
+// $FlowIgnore
+const { getUnicodeUrl } = require("devtools/client/shared/unicode-url");
+
+import { isOriginalSource } from "../utils/source-maps";
+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 { getURL, getFileExtension } from "./sources-tree";
+import { features } from "./prefs";
+
+import type {
+ SourceId,
+ Source,
+ SourceActor,
+ SourceContent,
+ SourceLocation,
+ Thread,
+ URL,
+} from "../types";
+
+import { isFulfilled, type AsyncValue } from "./async-value";
+import type { Symbols, TabsSources } from "../reducers/types";
+
+type transformUrlCallback = string => string;
+
+export const sourceTypes = {
+ coffee: "coffeescript",
+ js: "javascript",
+ jsx: "react",
+ ts: "typescript",
+ tsx: "typescript",
+ vue: "vue",
+};
+
+const javascriptLikeExtensions = ["marko", "es6", "vue", "jsm"];
+
+function getPath(source: Source): Array<string> {
+ const { path } = getURL(source);
+ 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: ?Source) {
+ if (!source) {
+ return false;
+ }
+
+ if (!source.url) {
+ return false;
+ }
+
+ if (!features.originalBlackbox && isOriginalSource(source)) {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * 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: Source, content: SourceContent): boolean {
+ const extension = getFileExtension(source).toLowerCase();
+ const contentType = content.type === "wasm" ? null : content.contentType;
+ return (
+ javascriptLikeExtensions.includes(extension) ||
+ !!(contentType && contentType.includes("javascript"))
+ );
+}
+
+/**
+ * @memberof utils/source
+ * @static
+ */
+export function isPretty(source: Source): boolean {
+ return isPrettyURL(source.url);
+}
+
+export function isPrettyURL(url: URL): boolean {
+ return url ? url.endsWith(":formatted") : false;
+}
+
+export function isThirdParty(source: Source): boolean {
+ const { url } = source;
+ if (!source || !url) {
+ return false;
+ }
+
+ return url.includes("node_modules") || url.includes("bower_components");
+}
+
+/**
+ * @memberof utils/source
+ * @static
+ */
+export function getPrettySourceURL(url: ?URL): string {
+ if (!url) {
+ url = "";
+ }
+ return `${url}:formatted`;
+}
+
+/**
+ * @memberof utils/source
+ * @static
+ */
+export function getRawSourceURL(url: URL): string {
+ return url && url.endsWith(":formatted")
+ ? url.slice(0, -":formatted".length)
+ : url;
+}
+
+function resolveFileURL(
+ url: URL,
+ transformUrl: transformUrlCallback = initialUrl => initialUrl,
+ truncate: boolean = true
+): string {
+ url = getRawSourceURL(url || "");
+ const name = transformUrl(url);
+ if (!truncate) {
+ return name;
+ }
+ return endTruncateStr(name, 50);
+}
+
+export function getFormattedSourceId(id: string): string {
+ const firstIndex = id.indexOf("/");
+ const secondIndex = id.indexOf("/", firstIndex);
+ return `SOURCE${id.slice(firstIndex, secondIndex)}`;
+}
+
+/**
+ * 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: Source,
+ rawSourceURL: URL = getRawSourceURL(source.url)
+): string {
+ const { id } = source;
+ if (!rawSourceURL) {
+ return getFormattedSourceId(id);
+ }
+
+ const { filename } = getURL(source);
+ return getRawSourceURL(filename);
+}
+
+/**
+ * Provides a middle-trunated filename
+ *
+ * @memberof utils/source
+ * @static
+ */
+export function getTruncatedFileName(
+ source: Source,
+ querystring: string = "",
+ length: number = 30
+): string {
+ 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: Source,
+ sources: Source[] | TabsSources
+): string | void {
+ 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 == 0) {
+ 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: Source, truncate: boolean = true): string {
+ const { url, id } = source;
+ if (!url) {
+ return getFormattedSourceId(id);
+ }
+
+ return resolveFileURL(url, getUnicodeUrl, truncate);
+}
+
+const contentTypeModeMap = {
+ "text/javascript": { name: "javascript" },
+ "text/typescript": { name: "javascript", typescript: true },
+ "text/coffeescript": { name: "coffeescript" },
+ "text/typescript-jsx": {
+ name: "jsx",
+ base: { name: "javascript", typescript: true },
+ },
+ "text/jsx": { name: "jsx" },
+ "text/x-elm": { name: "elm" },
+ "text/x-clojure": { name: "clojure" },
+ "text/x-clojurescript": { name: "clojure" },
+ "text/wasm": { name: "text" },
+ "text/html": { name: "htmlmixed" },
+};
+
+export function getSourcePath(url: URL): string {
+ 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: SourceContent): number {
+ 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;
+}
+
+/**
+ *
+ * Checks if a source is minified based on some heuristics
+ * @param key
+ * @param text
+ * @return boolean
+ * @memberof utils/source
+ * @static
+ */
+
+/**
+ *
+ * Returns Code Mirror mode for source content type
+ * @param contentType
+ * @return String
+ * @memberof utils/source
+ * @static
+ */
+// eslint-disable-next-line complexity
+export function getMode(
+ source: Source,
+ content: SourceContent,
+ symbols?: Symbols
+): { name: string, base?: Object } {
+ const extension = getFileExtension(source);
+
+ if (content.type !== "text") {
+ return { name: "text" };
+ }
+
+ const { contentType, value: text } = content;
+
+ if (extension === "jsx" || (symbols && symbols.hasJsx)) {
+ if (symbols && symbols.hasTypes) {
+ return { name: "text/typescript-jsx" };
+ }
+ return { name: "jsx" };
+ }
+
+ if (symbols && symbols.hasTypes) {
+ if (symbols.hasJsx) {
+ return { name: "text/typescript-jsx" };
+ }
+
+ return { name: "text/typescript" };
+ }
+
+ const languageMimeMap = [
+ { ext: "c", mode: "text/x-csrc" },
+ { ext: "kt", mode: "text/x-kotlin" },
+ { ext: "cpp", mode: "text/x-c++src" },
+ { ext: "m", mode: "text/x-objectivec" },
+ { ext: "rs", mode: "text/x-rustsrc" },
+ { ext: "hx", mode: "text/x-haxe" },
+ ];
+
+ // check for C and other non JS languages
+ const result = languageMimeMap.find(({ ext }) => extension === ext);
+ if (result !== undefined) {
+ return { name: result.mode };
+ }
+
+ // if the url ends with a known Javascript-like URL, provide JavaScript mode.
+ // uses the first part of the URL to ignore query string
+ if (javascriptLikeExtensions.find(ext => ext === extension)) {
+ return { name: "javascript" };
+ }
+
+ // Use HTML mode for files in which the first non whitespace
+ // character is `<` regardless of extension.
+ const isHTMLLike = text.match(/^\s*</);
+ if (!contentType) {
+ if (isHTMLLike) {
+ return { name: "htmlmixed" };
+ }
+ return { name: "text" };
+ }
+
+ // // @flow or /* @flow */
+ if (text.match(/^\s*(\/\/ @flow|\/\* @flow \*\/)/)) {
+ return contentTypeModeMap["text/typescript"];
+ }
+
+ if (/script|elm|jsx|clojure|wasm|html/.test(contentType)) {
+ if (contentType in contentTypeModeMap) {
+ return contentTypeModeMap[contentType];
+ }
+
+ return contentTypeModeMap["text/javascript"];
+ }
+
+ if (isHTMLLike) {
+ return { name: "htmlmixed" };
+ }
+
+ return { name: "text" };
+}
+
+export function isInlineScript(source: SourceActor): boolean {
+ return source.introductionType === "scriptElement";
+}
+
+function getNthLine(str: string, lineNum: number) {
+ 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: SourceId,
+ asyncContent: AsyncValue<SourceContent> | null,
+ line: number
+ ) => {
+ 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: SourceId,
+ asyncContent: AsyncValue<SourceContent> | null,
+ location: SourceLocation
+): string {
+ const { column, line = 0 } = location;
+
+ const lineText = getLineText(sourceId, asyncContent, line);
+ return lineText.slice(column, column + 100).trim();
+}
+
+export function getSourceClassnames(
+ source: ?Object,
+ symbols: ?Symbols
+): string {
+ // Conditionals should be ordered by priority of icon!
+ const defaultClassName = "file";
+
+ if (!source || !source.url) {
+ return defaultClassName;
+ }
+
+ if (isPretty(source)) {
+ return "prettyPrint";
+ }
+
+ if (source.isBlackBoxed) {
+ return "blackBox";
+ }
+
+ if (symbols && !symbols.loading && symbols.framework) {
+ return symbols.framework.toLowerCase();
+ }
+
+ if (isUrlExtension(source.url)) {
+ return "extension";
+ }
+
+ return sourceTypes[getFileExtension(source)] || defaultClassName;
+}
+
+export function getRelativeUrl(source: Source, root: string): string {
+ const { group, path } = getURL(source);
+ if (!root) {
+ return path;
+ }
+
+ // + 1 removes the leading "/"
+ const url = group + path;
+ return url.slice(url.indexOf(root) + root.length + 1);
+}
+
+export function underRoot(
+ source: Source,
+ root: string,
+ threads: Array<Thread>
+): boolean {
+ // source.url doesn't include thread actor ID, so remove the thread actor ID from the root
+ threads.forEach(thread => {
+ if (root.includes(thread.actor)) {
+ root = root.slice(thread.actor.length + 1);
+ }
+ });
+
+ if (source.url && source.url.includes("chrome://")) {
+ const { group, path } = getURL(source);
+ return (group + path).includes(root);
+ }
+
+ return !!source.url && source.url.includes(root);
+}
+
+export function isOriginal(source: Source): boolean {
+ // Pretty-printed sources are given original IDs, so no need
+ // for any additional check
+ return isOriginalSource(source);
+}
+
+export function isGenerated(source: Source): boolean {
+ return !isOriginal(source);
+}
+
+export function getSourceQueryString(source: ?Source) {
+ if (!source) {
+ return;
+ }
+
+ return parseURL(getRawSourceURL(source.url)).search;
+}
+
+export function isUrlExtension(url: URL): boolean {
+ return url.includes("moz-extension:") || url.includes("chrome-extension");
+}
+
+export function isExtensionDirectoryPath(url: URL): ?boolean {
+ if (isUrlExtension(url)) {
+ const urlArr = url.replace(/\/+/g, "/").split("/");
+ let extensionIndex = urlArr.indexOf("moz-extension:");
+ if (extensionIndex === -1) {
+ extensionIndex = urlArr.indexOf("chrome-extension:");
+ }
+ return !urlArr[extensionIndex + 2];
+ }
+}
+
+export function getPlainUrl(url: URL): string {
+ const queryStart = url.indexOf("?");
+ return queryStart !== -1 ? url.slice(0, queryStart) : url;
+}