Blame devtools/client/debugger/utils.js

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