mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-30 21:55:31 +00:00
379 lines
11 KiB
JavaScript
379 lines
11 KiB
JavaScript
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
|
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
|
/* 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/. */
|
|
/* globals document, window */
|
|
/* import-globals-from ./debugger-controller.js */
|
|
"use strict";
|
|
|
|
// Maps known URLs to friendly source group names and put them at the
|
|
// bottom of source list.
|
|
var KNOWN_SOURCE_GROUPS = {
|
|
"Add-on SDK": "resource://gre/modules/commonjs/",
|
|
};
|
|
|
|
KNOWN_SOURCE_GROUPS[L10N.getStr("anonymousSourcesLabel")] = "anonymous";
|
|
|
|
var XULUtils = {
|
|
/**
|
|
* Create <command> elements within `commandset` with event handlers
|
|
* bound to the `command` event
|
|
*
|
|
* @param commandset HTML Element
|
|
* A <commandset> element
|
|
* @param commands Object
|
|
* An object where keys specify <command> ids and values
|
|
* specify event handlers to be bound on the `command` event
|
|
*/
|
|
addCommands: function (commandset, commands) {
|
|
Object.keys(commands).forEach(name => {
|
|
let node = document.createElement("command");
|
|
node.id = name;
|
|
// XXX bug 371900: the command element must have an oncommand
|
|
// attribute as a string set by `setAttribute` for keys to use it
|
|
node.setAttribute("oncommand", " ");
|
|
node.addEventListener("command", commands[name]);
|
|
commandset.appendChild(node);
|
|
});
|
|
}
|
|
};
|
|
|
|
// Used to detect minification for automatic pretty printing
|
|
const SAMPLE_SIZE = 50; // no of lines
|
|
const INDENT_COUNT_THRESHOLD = 5; // percentage
|
|
const CHARACTER_LIMIT = 250; // line character limit
|
|
|
|
/**
|
|
* Utility functions for handling sources.
|
|
*/
|
|
var SourceUtils = {
|
|
_labelsCache: new Map(), // Can't use WeakMaps because keys are strings.
|
|
_groupsCache: new Map(),
|
|
_minifiedCache: new Map(),
|
|
|
|
/**
|
|
* Returns true if the specified url and/or content type are specific to
|
|
* javascript files.
|
|
*
|
|
* @return boolean
|
|
* True if the source is likely javascript.
|
|
*/
|
|
isJavaScript: function (aUrl, aContentType = "") {
|
|
return (aUrl && /\.jsm?$/.test(this.trimUrlQuery(aUrl))) ||
|
|
aContentType.includes("javascript");
|
|
},
|
|
|
|
/**
|
|
* Determines if the source text is minified by using
|
|
* the percentage indented of a subset of lines
|
|
*
|
|
* @return object
|
|
* A promise that resolves to true if source text is minified.
|
|
*/
|
|
isMinified: function (key, text) {
|
|
if (this._minifiedCache.has(key)) {
|
|
return this._minifiedCache.get(key);
|
|
}
|
|
|
|
let isMinified;
|
|
let lineEndIndex = 0;
|
|
let lineStartIndex = 0;
|
|
let lines = 0;
|
|
let indentCount = 0;
|
|
let overCharLimit = false;
|
|
|
|
// Strip comments.
|
|
text = text.replace(/\/\*[\S\s]*?\*\/|\/\/(.+|\n)/g, "");
|
|
|
|
while (lines++ < SAMPLE_SIZE) {
|
|
lineEndIndex = text.indexOf("\n", lineStartIndex);
|
|
if (lineEndIndex == -1) {
|
|
break;
|
|
}
|
|
if (/^\s+/.test(text.slice(lineStartIndex, lineEndIndex))) {
|
|
indentCount++;
|
|
}
|
|
// For files with no indents but are not minified.
|
|
if ((lineEndIndex - lineStartIndex) > CHARACTER_LIMIT) {
|
|
overCharLimit = true;
|
|
break;
|
|
}
|
|
lineStartIndex = lineEndIndex + 1;
|
|
}
|
|
|
|
isMinified =
|
|
((indentCount / lines) * 100) < INDENT_COUNT_THRESHOLD || overCharLimit;
|
|
|
|
this._minifiedCache.set(key, isMinified);
|
|
return isMinified;
|
|
},
|
|
|
|
/**
|
|
* Clears the labels, groups and minify cache, populated by methods like
|
|
* SourceUtils.getSourceLabel or Source Utils.getSourceGroup.
|
|
* This should be done every time the content location changes.
|
|
*/
|
|
clearCache: function () {
|
|
this._labelsCache.clear();
|
|
this._groupsCache.clear();
|
|
this._minifiedCache.clear();
|
|
},
|
|
|
|
/**
|
|
* Gets a unique, simplified label from a source url.
|
|
*
|
|
* @param string aUrl
|
|
* The source url.
|
|
* @return string
|
|
* The simplified label.
|
|
*/
|
|
getSourceLabel: function (aUrl) {
|
|
let cachedLabel = this._labelsCache.get(aUrl);
|
|
if (cachedLabel) {
|
|
return cachedLabel;
|
|
}
|
|
|
|
let sourceLabel = null;
|
|
|
|
for (let name of Object.keys(KNOWN_SOURCE_GROUPS)) {
|
|
if (aUrl.startsWith(KNOWN_SOURCE_GROUPS[name])) {
|
|
sourceLabel = aUrl.substring(KNOWN_SOURCE_GROUPS[name].length);
|
|
}
|
|
}
|
|
|
|
if (!sourceLabel) {
|
|
sourceLabel = this.trimUrl(aUrl);
|
|
}
|
|
|
|
let unicodeLabel = NetworkHelper.convertToUnicode(unescape(sourceLabel));
|
|
this._labelsCache.set(aUrl, unicodeLabel);
|
|
return unicodeLabel;
|
|
},
|
|
|
|
/**
|
|
* Gets as much information as possible about the hostname and directory paths
|
|
* of an url to create a short url group identifier.
|
|
*
|
|
* @param string aUrl
|
|
* The source url.
|
|
* @return string
|
|
* The simplified group.
|
|
*/
|
|
getSourceGroup: function (aUrl) {
|
|
let cachedGroup = this._groupsCache.get(aUrl);
|
|
if (cachedGroup) {
|
|
return cachedGroup;
|
|
}
|
|
|
|
try {
|
|
// Use an nsIURL to parse all the url path parts.
|
|
var uri = Services.io.newURI(aUrl).QueryInterface(Ci.nsIURL);
|
|
} catch (e) {
|
|
// This doesn't look like a url, or nsIURL can't handle it.
|
|
return "";
|
|
}
|
|
|
|
let groupLabel = uri.prePath;
|
|
|
|
for (let name of Object.keys(KNOWN_SOURCE_GROUPS)) {
|
|
if (aUrl.startsWith(KNOWN_SOURCE_GROUPS[name])) {
|
|
groupLabel = name;
|
|
}
|
|
}
|
|
|
|
let unicodeLabel = NetworkHelper.convertToUnicode(unescape(groupLabel));
|
|
this._groupsCache.set(aUrl, unicodeLabel);
|
|
return unicodeLabel;
|
|
},
|
|
|
|
/**
|
|
* Trims the url by shortening it if it exceeds a certain length, adding an
|
|
* ellipsis at the end.
|
|
*
|
|
* @param string aUrl
|
|
* The source url.
|
|
* @param number aLength [optional]
|
|
* The expected source url length.
|
|
* @param number aSection [optional]
|
|
* The section to trim. Supported values: "start", "center", "end"
|
|
* @return string
|
|
* The shortened url.
|
|
*/
|
|
trimUrlLength: function (aUrl, aLength, aSection) {
|
|
aLength = aLength || SOURCE_URL_DEFAULT_MAX_LENGTH;
|
|
aSection = aSection || "end";
|
|
|
|
if (aUrl.length > aLength) {
|
|
switch (aSection) {
|
|
case "start":
|
|
return ELLIPSIS + aUrl.slice(-aLength);
|
|
break;
|
|
case "center":
|
|
return aUrl.substr(0, aLength / 2 - 1) + ELLIPSIS + aUrl.slice(-aLength / 2 + 1);
|
|
break;
|
|
case "end":
|
|
return aUrl.substr(0, aLength) + ELLIPSIS;
|
|
break;
|
|
}
|
|
}
|
|
return aUrl;
|
|
},
|
|
|
|
/**
|
|
* Trims the query part or reference identifier of a url string, if necessary.
|
|
*
|
|
* @param string aUrl
|
|
* The source url.
|
|
* @return string
|
|
* The shortened url.
|
|
*/
|
|
trimUrlQuery: function (aUrl) {
|
|
let length = aUrl.length;
|
|
let q1 = aUrl.indexOf("?");
|
|
let q2 = aUrl.indexOf("&");
|
|
let q3 = aUrl.indexOf("#");
|
|
let q = Math.min(q1 != -1 ? q1 : length,
|
|
q2 != -1 ? q2 : length,
|
|
q3 != -1 ? q3 : length);
|
|
|
|
return aUrl.slice(0, q);
|
|
},
|
|
|
|
/**
|
|
* Trims as much as possible from a url, while keeping the label unique
|
|
* in the sources container.
|
|
*
|
|
* @param string | nsIURL aUrl
|
|
* The source url.
|
|
* @param string aLabel [optional]
|
|
* The resulting label at each step.
|
|
* @param number aSeq [optional]
|
|
* The current iteration step.
|
|
* @return string
|
|
* The resulting label at the final step.
|
|
*/
|
|
trimUrl: function (aUrl, aLabel, aSeq) {
|
|
if (!(aUrl instanceof Ci.nsIURL)) {
|
|
try {
|
|
// Use an nsIURL to parse all the url path parts.
|
|
aUrl = Services.io.newURI(aUrl).QueryInterface(Ci.nsIURL);
|
|
} catch (e) {
|
|
// This doesn't look like a url, or nsIURL can't handle it.
|
|
return aUrl;
|
|
}
|
|
}
|
|
if (!aSeq) {
|
|
let name = aUrl.fileName;
|
|
if (name) {
|
|
// This is a regular file url, get only the file name (contains the
|
|
// base name and extension if available).
|
|
|
|
// If this url contains an invalid query, unfortunately nsIURL thinks
|
|
// it's part of the file extension. It must be removed.
|
|
aLabel = aUrl.fileName.replace(/\&.*/, "");
|
|
} else {
|
|
// This is not a file url, hence there is no base name, nor extension.
|
|
// Proceed using other available information.
|
|
aLabel = "";
|
|
}
|
|
aSeq = 1;
|
|
}
|
|
|
|
// If we have a label and it doesn't only contain a query...
|
|
if (aLabel && aLabel.indexOf("?") != 0) {
|
|
// A page may contain multiple requests to the same url but with different
|
|
// queries. It is *not* redundant to show each one.
|
|
if (!DebuggerView.Sources.getItemForAttachment(e => e.label == aLabel)) {
|
|
return aLabel;
|
|
}
|
|
}
|
|
|
|
// Append the url query.
|
|
if (aSeq == 1) {
|
|
let query = aUrl.query;
|
|
if (query) {
|
|
return this.trimUrl(aUrl, aLabel + "?" + query, aSeq + 1);
|
|
}
|
|
aSeq++;
|
|
}
|
|
// Append the url reference.
|
|
if (aSeq == 2) {
|
|
let ref = aUrl.ref;
|
|
if (ref) {
|
|
return this.trimUrl(aUrl, aLabel + "#" + aUrl.ref, aSeq + 1);
|
|
}
|
|
aSeq++;
|
|
}
|
|
// Prepend the url directory.
|
|
if (aSeq == 3) {
|
|
let dir = aUrl.directory;
|
|
if (dir) {
|
|
return this.trimUrl(aUrl, dir.replace(/^\//, "") + aLabel, aSeq + 1);
|
|
}
|
|
aSeq++;
|
|
}
|
|
// Prepend the hostname and port number.
|
|
if (aSeq == 4) {
|
|
let host;
|
|
try {
|
|
// Bug 1261860: jar: URLs throw when accessing `hostPost`
|
|
host = aUrl.hostPort;
|
|
} catch (e) {}
|
|
if (host) {
|
|
return this.trimUrl(aUrl, host + "/" + aLabel, aSeq + 1);
|
|
}
|
|
aSeq++;
|
|
}
|
|
// Use the whole url spec but ignoring the reference.
|
|
if (aSeq == 5) {
|
|
return this.trimUrl(aUrl, aUrl.specIgnoringRef, aSeq + 1);
|
|
}
|
|
// Give up.
|
|
return aUrl.spec;
|
|
},
|
|
|
|
parseSource: function (aDebuggerView, aParser) {
|
|
let editor = aDebuggerView.editor;
|
|
|
|
let contents = editor.getText();
|
|
let location = aDebuggerView.Sources.selectedValue;
|
|
let parsedSource = aParser.get(contents, location);
|
|
|
|
return parsedSource;
|
|
},
|
|
|
|
findIdentifier: function (aEditor, parsedSource, x, y) {
|
|
let editor = aEditor;
|
|
|
|
// Calculate the editor's line and column at the current x and y coords.
|
|
let hoveredPos = editor.getPositionFromCoords({ left: x, top: y });
|
|
let hoveredOffset = editor.getOffset(hoveredPos);
|
|
let hoveredLine = hoveredPos.line;
|
|
let hoveredColumn = hoveredPos.ch;
|
|
|
|
let scriptInfo = parsedSource.getScriptInfo(hoveredOffset);
|
|
|
|
// If the script length is negative, we're not hovering JS source code.
|
|
if (scriptInfo.length == -1) {
|
|
return;
|
|
}
|
|
|
|
// Using the script offset, determine the actual line and column inside the
|
|
// script, to use when finding identifiers.
|
|
let scriptStart = editor.getPosition(scriptInfo.start);
|
|
let scriptLineOffset = scriptStart.line;
|
|
let scriptColumnOffset = (hoveredLine == scriptStart.line ? scriptStart.ch : 0);
|
|
|
|
let scriptLine = hoveredLine - scriptLineOffset;
|
|
let scriptColumn = hoveredColumn - scriptColumnOffset;
|
|
let identifierInfo = parsedSource.getIdentifierAt({
|
|
line: scriptLine + 1,
|
|
column: scriptColumn,
|
|
scriptIndex: scriptInfo.index
|
|
});
|
|
|
|
return identifierInfo;
|
|
}
|
|
};
|