mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-09 21:28:43 +08:00
R=jacktfranklin@chromium.org No-Presubmit: True Bug: 1082789 Change-Id: Ic77884bf7e9b5dc74053915b3b10699368841b66 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2595268 Commit-Queue: Tim van der Lippe <tvanderlippe@chromium.org> Reviewed-by: Jack Franklin <jacktfranklin@chromium.org>
409 lines
16 KiB
JavaScript
409 lines
16 KiB
JavaScript
// 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
|
|
|
|
/** @type {!WeakMap<!Object, !SourceFormatData>} */
|
|
const objectToFormattingResult = new WeakMap();
|
|
|
|
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 objectToFormattingResult.get(object) || null;
|
|
}
|
|
}
|
|
|
|
/** @type {?SourceFormatter} */
|
|
let sourceFormatterInstance = null;
|
|
|
|
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<!Workspace.UISourceCode.UISourceCode, !{promise: !Promise<!SourceFormatData>, 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);
|
|
}
|
|
|
|
/**
|
|
* @return {!SourceFormatter}
|
|
*/
|
|
static instance() {
|
|
if (!sourceFormatterInstance) {
|
|
sourceFormatterInstance = new SourceFormatter();
|
|
}
|
|
return sourceFormatterInstance;
|
|
}
|
|
|
|
/**
|
|
* @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<?Workspace.UISourceCode.UISourceCode>}
|
|
*/
|
|
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) {
|
|
objectToFormattingResult.delete(formatData.formattedSourceCode);
|
|
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 = objectToFormattingResult.get(uiSourceCode);
|
|
if (!formatData) {
|
|
return uiSourceCode;
|
|
}
|
|
return formatData.originalSourceCode;
|
|
}
|
|
|
|
/**
|
|
* @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode
|
|
* @return {!Promise<!SourceFormatData>}
|
|
*/
|
|
async format(uiSourceCode) {
|
|
const cacheEntry = this._formattedSourceCodes.get(uiSourceCode);
|
|
if (cacheEntry) {
|
|
return cacheEntry.promise;
|
|
}
|
|
|
|
/**
|
|
* @type {!Promise<!SourceFormatData>}
|
|
*/
|
|
const resultPromise = new Promise(async resolve => {
|
|
const {content} = await uiSourceCode.requestContent();
|
|
|
|
FormatterInterface.format(
|
|
uiSourceCode.contentType(), uiSourceCode.mimeType(), content || '',
|
|
async (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.createUISourceCode(formattedURL, contentProvider.contentType());
|
|
const formatData = new SourceFormatData(uiSourceCode, formattedUISourceCode, formatterMapping);
|
|
objectToFormattingResult.set(formattedUISourceCode, formatData);
|
|
this._project.addUISourceCodeWithProvider(
|
|
formattedUISourceCode, contentProvider, /* metadata */ null, uiSourceCode.mimeType());
|
|
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());
|
|
}
|
|
|
|
resolve(formatData);
|
|
});
|
|
});
|
|
|
|
this._formattedSourceCodes.set(uiSourceCode, {promise: resultPromise, formatData: null});
|
|
|
|
return resultPromise;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @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 || !script) {
|
|
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 whether 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<!SDK.DebuggerModel.Location>}
|
|
*/
|
|
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 && Boolean(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 = /** @type {!Array<!SDK.DebuggerModel.Location>} */ (
|
|
scripts.map(script => script.rawLocation(originalLine, originalColumn)).filter(l => Boolean(l)));
|
|
console.assert(locations.every(l => l && Boolean(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) {
|
|
objectToFormattingResult.set(script, formatData);
|
|
}
|
|
} else {
|
|
for (const script of scripts) {
|
|
objectToFormattingResult.delete(script);
|
|
}
|
|
}
|
|
const updatePromises = scripts.map(
|
|
script => Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().updateLocations(script));
|
|
await Promise.all(updatePromises);
|
|
}
|
|
|
|
/**
|
|
* @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode
|
|
* @return {!Array<!SDK.Script.Script>}
|
|
*/
|
|
_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(!objectToFormattingResult.has(uiSourceCode));
|
|
const rawLocations = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance()
|
|
.uiLocationToRawLocationsForUnformattedJavaScript(uiSourceCode, 0, 0);
|
|
return /** @type {!Array<!SDK.Script.Script>} */ (
|
|
rawLocations.map(location => location.script()).filter(script => Boolean(script)));
|
|
}
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/** @type {!WeakMap<!Workspace.UISourceCode.UISourceCode, !Array<!SDK.CSSStyleSheetHeader.CSSStyleSheetHeader>>} */
|
|
const sourceCodeToHeaders = new WeakMap();
|
|
|
|
/**
|
|
* @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<!SDK.CSSModel.CSSLocation>}
|
|
*/
|
|
uiLocationToRawLocations(uiLocation) {
|
|
const formatData = SourceFormatData._for(uiLocation.uiSourceCode);
|
|
if (!formatData) {
|
|
return [];
|
|
}
|
|
const [originalLine, originalColumn] =
|
|
formatData.mapping.formattedToOriginal(uiLocation.lineNumber, uiLocation.columnNumber);
|
|
const allHeaders = sourceCodeToHeaders.get(formatData.originalSourceCode);
|
|
|
|
if (!allHeaders) {
|
|
return [];
|
|
}
|
|
|
|
const headers = allHeaders.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) {
|
|
sourceCodeToHeaders.set(original, headers);
|
|
headers.forEach(header => {
|
|
objectToFormattingResult.set(header, formatData);
|
|
});
|
|
} else {
|
|
sourceCodeToHeaders.delete(original);
|
|
headers.forEach(header => {
|
|
objectToFormattingResult.delete(header);
|
|
});
|
|
}
|
|
const updatePromises =
|
|
headers.map(header => Bindings.CSSWorkspaceBinding.CSSWorkspaceBinding.instance().updateLocations(header));
|
|
await Promise.all(updatePromises);
|
|
}
|
|
|
|
/**
|
|
* @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode
|
|
* @return {!Array<!SDK.CSSStyleSheetHeader.CSSStyleSheetHeader>}
|
|
*/
|
|
_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 /** @type {!Array<!SDK.CSSStyleSheetHeader.CSSStyleSheetHeader>} */ (
|
|
rawLocations.map(rawLocation => rawLocation.header()).filter(header => Boolean(header)));
|
|
}
|
|
return [];
|
|
}
|
|
}
|