All Downloads are FREE. Search and download functionalities are using the official Maven repository.

META-INF.dirigible.dev-tools.formatter.SourceFormatter.js Maven / Gradle / Ivy

There is a newer version: 10.6.27
Show newest version
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import * as Bindings from '../bindings/bindings.js';
import * as Common from '../common/common.js';
import * as SDK from '../sdk/sdk.js';
import * as TextUtils from '../text_utils/text_utils.js';
import * as Workspace from '../workspace/workspace.js';

import {FormatterInterface, FormatterSourceMapping} from './ScriptFormatter.js';  // eslint-disable-line no-unused-vars

export class SourceFormatData {
  /**
   * @param {!Workspace.UISourceCode.UISourceCode} originalSourceCode
   * @param {!Workspace.UISourceCode.UISourceCode} formattedSourceCode
   * @param {!FormatterSourceMapping} mapping
   */
  constructor(originalSourceCode, formattedSourceCode, mapping) {
    this.originalSourceCode = originalSourceCode;
    this.formattedSourceCode = formattedSourceCode;
    this.mapping = mapping;
  }

  originalPath() {
    return this.originalSourceCode.project().id() + ':' + this.originalSourceCode.url();
  }

  /**
   * @param {!Object} object
   * @return {?SourceFormatData}
   */
  static _for(object) {
    return object[SourceFormatData._formatDataSymbol];
  }
}

SourceFormatData._formatDataSymbol = Symbol('formatData');

export class SourceFormatter {
  constructor() {
    this._projectId = 'formatter:';
    this._project = new Bindings.ContentProviderBasedProject.ContentProviderBasedProject(
        Workspace.Workspace.WorkspaceImpl.instance(), this._projectId, Workspace.Workspace.projectTypes.Formatter,
        'formatter', true /* isServiceProject */);

    /** @type {!Map, formatData: ?SourceFormatData}>} */
    this._formattedSourceCodes = new Map();
    this._scriptMapping = new ScriptMapping();
    this._styleMapping = new StyleMapping();
    Workspace.Workspace.WorkspaceImpl.instance().addEventListener(
        Workspace.Workspace.Events.UISourceCodeRemoved, event => {
          this._onUISourceCodeRemoved(event);
        }, this);
  }

  /**
   * @param {!Common.EventTarget.EventTargetEvent} event
   */
  async _onUISourceCodeRemoved(event) {
    const uiSourceCode = /** @type {!Workspace.UISourceCode.UISourceCode} */ (event.data);
    const cacheEntry = this._formattedSourceCodes.get(uiSourceCode);
    if (cacheEntry && cacheEntry.formatData) {
      await this._discardFormatData(cacheEntry.formatData);
    }
    this._formattedSourceCodes.delete(uiSourceCode);
  }

  /**
   * @param {!Workspace.UISourceCode.UISourceCode} formattedUISourceCode
   * @return {!Promise}
   */
  async discardFormattedUISourceCode(formattedUISourceCode) {
    const formatData = SourceFormatData._for(formattedUISourceCode);
    if (!formatData) {
      return null;
    }
    await this._discardFormatData(formatData);
    this._formattedSourceCodes.delete(formatData.originalSourceCode);
    return formatData.originalSourceCode;
  }

  /**
   * @param {!SourceFormatData} formatData
   */
  async _discardFormatData(formatData) {
    delete formatData.formattedSourceCode[SourceFormatData._formatDataSymbol];
    await this._scriptMapping._setSourceMappingEnabled(formatData, false);
    this._styleMapping._setSourceMappingEnabled(formatData, false);
    this._project.removeFile(formatData.formattedSourceCode.url());
  }

  /**
   * @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode
   * @return {boolean}
   */
  hasFormatted(uiSourceCode) {
    return this._formattedSourceCodes.has(uiSourceCode);
  }

  /**
   * @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode
   * @return {!Workspace.UISourceCode.UISourceCode}
   */
  getOriginalUISourceCode(uiSourceCode) {
    const formatData =
        /** @type {?SourceFormatData} */ (uiSourceCode[SourceFormatData._formatDataSymbol]);
    if (!formatData) {
      return uiSourceCode;
    }
    return formatData.originalSourceCode;
  }

  /**
   * @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode
   * @return {!Promise}
   */
  async format(uiSourceCode) {
    const cacheEntry = this._formattedSourceCodes.get(uiSourceCode);
    if (cacheEntry) {
      return cacheEntry.promise;
    }

    let fulfillFormatPromise;
    const resultPromise = new Promise(fulfill => {
      fulfillFormatPromise = fulfill;
    });
    this._formattedSourceCodes.set(uiSourceCode, {promise: resultPromise, formatData: null});
    const {content} = await uiSourceCode.requestContent();
    // ------------ ASYNC ------------
    FormatterInterface.format(
        uiSourceCode.contentType(), uiSourceCode.mimeType(), content || '', formatDone.bind(this));
    return resultPromise;

    /**
     * @this SourceFormatter
     * @param {string} formattedContent
     * @param {!FormatterSourceMapping} formatterMapping
     */
    async function formatDone(formattedContent, formatterMapping) {
      const cacheEntry = this._formattedSourceCodes.get(uiSourceCode);
      if (!cacheEntry || cacheEntry.promise !== resultPromise) {
        return;
      }
      let formattedURL;
      let count = 0;
      let suffix = '';
      do {
        formattedURL = `${uiSourceCode.url()}:formatted${suffix}`;
        suffix = `:${count++}`;
      } while (this._project.uiSourceCodeForURL(formattedURL));
      const contentProvider = TextUtils.StaticContentProvider.StaticContentProvider.fromString(
          formattedURL, uiSourceCode.contentType(), formattedContent);
      const formattedUISourceCode =
          this._project.addContentProvider(formattedURL, contentProvider, uiSourceCode.mimeType());
      const formatData = new SourceFormatData(uiSourceCode, formattedUISourceCode, formatterMapping);
      formattedUISourceCode[SourceFormatData._formatDataSymbol] = formatData;
      await this._scriptMapping._setSourceMappingEnabled(formatData, true);
      await this._styleMapping._setSourceMappingEnabled(formatData, true);
      cacheEntry.formatData = formatData;

      for (const decoration of uiSourceCode.allDecorations()) {
        const range = decoration.range();
        const startLocation = formatterMapping.originalToFormatted(range.startLine, range.startColumn);
        const endLocation = formatterMapping.originalToFormatted(range.endLine, range.endColumn);

        formattedUISourceCode.addDecoration(
            new TextUtils.TextRange.TextRange(startLocation[0], startLocation[1], endLocation[0], endLocation[1]),
            /** @type {string} */ (decoration.type()), decoration.data());
      }

      fulfillFormatPromise(formatData);
    }
  }
}

/**
 * @implements {Bindings.DebuggerWorkspaceBinding.DebuggerSourceMapping}
 */
class ScriptMapping {
  constructor() {
    Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().addSourceMapping(this);
  }

  /**
   * @override
   * @param {!SDK.DebuggerModel.Location} rawLocation
   * @return {?Workspace.UISourceCode.UILocation}
   */
  rawLocationToUILocation(rawLocation) {
    const script = rawLocation.script();
    const formatData = script && SourceFormatData._for(script);
    if (!formatData) {
      return null;
    }
    if (script.isInlineScriptWithSourceURL()) {
      // Inline scripts with #sourceURL= have lineEndings wrt. the inline script (and not wrt. the containing document),
      // but `rawLocation` will always use locations wrt. the containing document, because that is what the back-end is
      // sending. This is a hack, because what we are really doing here is deciding the location based on /how/ the
      // script is displayed, which is really something this layer cannot and should not have to decide: The
      // SourceFormatter should not have to know wether a script is displayed inline (in its containing document) or
      // stand-alone.
      const [relativeLineNumber, relativeColumnNumber] = script.toRelativeLocation(rawLocation);
      const [formattedLineNumber, formattedColumnNumber] =
          formatData.mapping.originalToFormatted(relativeLineNumber, relativeColumnNumber);
      return formatData.formattedSourceCode.uiLocation(formattedLineNumber, formattedColumnNumber);
    }
    // Here we either have an inline script without a #sourceURL= or a stand-alone script. For stand-alone scripts, no
    // translation must be applied. For inline scripts, also no translation must be applied, because the line-endings
    // tables in the mapping are the same as in the containing document.
    const [lineNumber, columnNumber] =
        formatData.mapping.originalToFormatted(rawLocation.lineNumber, rawLocation.columnNumber || 0);
    return formatData.formattedSourceCode.uiLocation(lineNumber, columnNumber);
  }

  /**
   * @override
   * @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode
   * @param {number} lineNumber
   * @param {number} columnNumber
   * @return {!Array}
   */
  uiLocationToRawLocations(uiSourceCode, lineNumber, columnNumber) {
    const formatData = SourceFormatData._for(uiSourceCode);
    if (!formatData) {
      return [];
    }
    const [originalLine, originalColumn] = formatData.mapping.formattedToOriginal(lineNumber, columnNumber);
    if (formatData.originalSourceCode.contentType().isScript()) {
      // Here we have a script that is displayed on its own (i.e. it has a dedicated uiSourceCode). This means it is
      // either a stand-alone script or an inline script with a #sourceURL= and in both cases we can just forward the
      // question to the original (unformatted) source code.
      const rawLocations = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance()
                               .uiLocationToRawLocationsForUnformattedJavaScript(
                                   formatData.originalSourceCode, originalLine, originalColumn);
      console.assert(rawLocations.every(l => l && !!l.script()));
      return rawLocations;
    }
    if (formatData.originalSourceCode.contentType() === Common.ResourceType.resourceTypes.Document) {
      const target = Bindings.NetworkProject.NetworkProject.targetForUISourceCode(formatData.originalSourceCode);
      const debuggerModel = target && target.model(SDK.DebuggerModel.DebuggerModel);
      if (debuggerModel) {
        const scripts = debuggerModel.scriptsForSourceURL(formatData.originalSourceCode.url())
                            .filter(script => script.isInlineScript() && !script.hasSourceURL);
        // Here we have an inline script, which was formatted together with the containing document, so we must not
        // translate locations as they are relative to the start of the document.
        const locations = scripts.map(script => script.rawLocation(originalLine, originalColumn)).filter(l => !!l);
        console.assert(locations.every(l => l && !!l.script()));
        return locations;
      }
    }
    return [];
  }

  /**
   * @param {!SourceFormatData} formatData
   * @param {boolean} enabled
   */
  async _setSourceMappingEnabled(formatData, enabled) {
    const scripts = this._scriptsForUISourceCode(formatData.originalSourceCode);
    if (!scripts.length) {
      return;
    }
    if (enabled) {
      for (const script of scripts) {
        script[SourceFormatData._formatDataSymbol] = formatData;
      }
    } else {
      for (const script of scripts) {
        delete script[SourceFormatData._formatDataSymbol];
      }
    }
    const updatePromises = scripts.map(
        script => Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().updateLocations(script));
    await Promise.all(updatePromises);
  }

  /**
   * @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode
   * @return {!Array}
   */
  _scriptsForUISourceCode(uiSourceCode) {
    if (uiSourceCode.contentType() === Common.ResourceType.resourceTypes.Document) {
      const target = Bindings.NetworkProject.NetworkProject.targetForUISourceCode(uiSourceCode);
      const debuggerModel = target && target.model(SDK.DebuggerModel.DebuggerModel);
      if (debuggerModel) {
        const scripts = debuggerModel.scriptsForSourceURL(uiSourceCode.url())
                            .filter(script => script.isInlineScript() && !script.hasSourceURL);
        return scripts;
      }
    }
    if (uiSourceCode.contentType().isScript()) {
      console.assert(
          !uiSourceCode[SourceFormatData._formatDataSymbol] ||
          uiSourceCode[SourceFormatData._formatDataSymbol] === uiSourceCode);
      const rawLocations = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance()
                               .uiLocationToRawLocationsForUnformattedJavaScript(uiSourceCode, 0, 0);
      return rawLocations.map(location => location.script()).filter(script => !!script);
    }
    return [];
  }
}

/**
 * @implements {Bindings.CSSWorkspaceBinding.SourceMapping}
 */
class StyleMapping {
  constructor() {
    Bindings.CSSWorkspaceBinding.CSSWorkspaceBinding.instance().addSourceMapping(this);
    this._headersSymbol = Symbol('Formatter.SourceFormatter.StyleMapping._headersSymbol');
  }

  /**
   * @override
   * @param {!SDK.CSSModel.CSSLocation} rawLocation
   * @return {?Workspace.UISourceCode.UILocation}
   */
  rawLocationToUILocation(rawLocation) {
    const styleHeader = rawLocation.header();
    const formatData = styleHeader && SourceFormatData._for(styleHeader);
    if (!formatData) {
      return null;
    }
    const formattedLocation =
        formatData.mapping.originalToFormatted(rawLocation.lineNumber, rawLocation.columnNumber || 0);
    return formatData.formattedSourceCode.uiLocation(formattedLocation[0], formattedLocation[1]);
  }

  /**
   * @override
   * @param {!Workspace.UISourceCode.UILocation} uiLocation
   * @return {!Array}
   */
  uiLocationToRawLocations(uiLocation) {
    const formatData = SourceFormatData._for(uiLocation.uiSourceCode);
    if (!formatData) {
      return [];
    }
    const [originalLine, originalColumn] =
        formatData.mapping.formattedToOriginal(uiLocation.lineNumber, uiLocation.columnNumber);
    const headers = formatData.originalSourceCode[this._headersSymbol].filter(
        header => header.containsLocation(originalLine, originalColumn));
    return headers.map(header => new SDK.CSSModel.CSSLocation(header, originalLine, originalColumn));
  }

  /**
   * @param {!SourceFormatData} formatData
   * @param {boolean} enable
   */
  async _setSourceMappingEnabled(formatData, enable) {
    const original = formatData.originalSourceCode;
    const headers = this._headersForUISourceCode(original);
    if (enable) {
      original[this._headersSymbol] = headers;
      headers.forEach(header => header[SourceFormatData._formatDataSymbol] = formatData);
    } else {
      original[this._headersSymbol] = null;
      headers.forEach(header => delete header[SourceFormatData._formatDataSymbol]);
    }
    const updatePromises =
        headers.map(header => Bindings.CSSWorkspaceBinding.CSSWorkspaceBinding.instance().updateLocations(header));
    await Promise.all(updatePromises);
  }

  /**
   * @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode
   * @return {!Array}
   */
  _headersForUISourceCode(uiSourceCode) {
    if (uiSourceCode.contentType() === Common.ResourceType.resourceTypes.Document) {
      const target = Bindings.NetworkProject.NetworkProject.targetForUISourceCode(uiSourceCode);
      const cssModel = target && target.model(SDK.CSSModel.CSSModel);
      if (cssModel) {
        return cssModel.headersForSourceURL(uiSourceCode.url())
            .filter(header => header.isInline && !header.hasSourceURL);
      }
    } else if (uiSourceCode.contentType().isStyleSheet()) {
      const rawLocations = Bindings.CSSWorkspaceBinding.CSSWorkspaceBinding.instance().uiLocationToRawLocations(
          uiSourceCode.uiLocation(0, 0));
      return rawLocations.map(rawLocation => rawLocation.header()).filter(header => !!header);
    }
    return [];
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy