blob: 527fe64ac756e2cc6a432314eefce405a4a6feca [file] [log] [blame]
// 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.
Sources.SourceFormatData = class {
/**
* @param {!Workspace.UISourceCode} originalSourceCode
* @param {!Workspace.UISourceCode} formattedSourceCode
* @param {!Formatter.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 {?Sources.SourceFormatData}
*/
static _for(object) {
return object[Sources.SourceFormatData._formatDataSymbol];
}
};
Sources.SourceFormatData._formatDataSymbol = Symbol('formatData');
Sources.SourceFormatter = class {
constructor() {
this._projectId = 'formatter:';
this._project = new Bindings.ContentProviderBasedProject(
Workspace.workspace, this._projectId, Workspace.projectTypes.Formatter, 'formatter',
true /* isServiceProject */);
/** @type {!Map<!Workspace.UISourceCode, !{promise: !Promise<!Sources.SourceFormatData>, formatData: ?Sources.SourceFormatData}>} */
this._formattedSourceCodes = new Map();
this._scriptMapping = new Sources.SourceFormatter.ScriptMapping();
this._styleMapping = new Sources.SourceFormatter.StyleMapping();
Workspace.workspace.addEventListener(
Workspace.Workspace.Events.UISourceCodeRemoved, this._onUISourceCodeRemoved, this);
}
/**
* @param {!Common.Event} event
*/
_onUISourceCodeRemoved(event) {
const uiSourceCode = /** @type {!Workspace.UISourceCode} */ (event.data);
const cacheEntry = this._formattedSourceCodes.get(uiSourceCode);
if (cacheEntry && cacheEntry.formatData)
this._discardFormatData(cacheEntry.formatData);
this._formattedSourceCodes.remove(uiSourceCode);
}
/**
* @param {!Workspace.UISourceCode} formattedUISourceCode
* @return {?Workspace.UISourceCode}
*/
discardFormattedUISourceCode(formattedUISourceCode) {
const formatData = Sources.SourceFormatData._for(formattedUISourceCode);
if (!formatData)
return null;
this._discardFormatData(formatData);
this._formattedSourceCodes.remove(formatData.originalSourceCode);
return formatData.originalSourceCode;
}
/**
* @param {!Sources.SourceFormatData} formatData
*/
_discardFormatData(formatData) {
delete formatData.formattedSourceCode[Sources.SourceFormatData._formatDataSymbol];
this._scriptMapping._setSourceMappingEnabled(formatData, false);
this._styleMapping._setSourceMappingEnabled(formatData, false);
this._project.removeFile(formatData.formattedSourceCode.url());
}
/**
* @param {!Workspace.UISourceCode} uiSourceCode
* @return {boolean}
*/
hasFormatted(uiSourceCode) {
return this._formattedSourceCodes.has(uiSourceCode);
}
/**
* @param {!Workspace.UISourceCode} uiSourceCode
* @return {!Promise<!Sources.SourceFormatData>}
*/
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 ------------
Formatter.Formatter.format(
uiSourceCode.contentType(), uiSourceCode.mimeType(), content || '', formatDone.bind(this));
return resultPromise;
/**
* @this Sources.SourceFormatter
* @param {string} formattedContent
* @param {!Formatter.FormatterSourceMapping} formatterMapping
*/
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 =
Common.StaticContentProvider.fromString(formattedURL, uiSourceCode.contentType(), formattedContent);
const formattedUISourceCode =
this._project.addContentProvider(formattedURL, contentProvider, uiSourceCode.mimeType());
const formatData = new Sources.SourceFormatData(uiSourceCode, formattedUISourceCode, formatterMapping);
formattedUISourceCode[Sources.SourceFormatData._formatDataSymbol] = formatData;
this._scriptMapping._setSourceMappingEnabled(formatData, true);
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(startLocation[0], startLocation[1], endLocation[0], endLocation[1]),
/** @type {string} */ (decoration.type()), decoration.data());
}
fulfillFormatPromise(formatData);
}
}
};
/**
* @implements {Bindings.DebuggerSourceMapping}
*/
Sources.SourceFormatter.ScriptMapping = class {
constructor() {
Bindings.debuggerWorkspaceBinding.addSourceMapping(this);
}
/**
* @override
* @param {!SDK.DebuggerModel.Location} rawLocation
* @return {?Workspace.UILocation}
*/
rawLocationToUILocation(rawLocation) {
const script = rawLocation.script();
const formatData = script && Sources.SourceFormatData._for(script);
if (!formatData)
return null;
const lineNumber = rawLocation.lineNumber;
const columnNumber = rawLocation.columnNumber || 0;
const formattedLocation = formatData.mapping.originalToFormatted(lineNumber, columnNumber);
return formatData.formattedSourceCode.uiLocation(formattedLocation[0], formattedLocation[1]);
}
/**
* @override
* @param {!Workspace.UISourceCode} uiSourceCode
* @param {number} lineNumber
* @param {number} columnNumber
* @return {?SDK.DebuggerModel.Location}
*/
uiLocationToRawLocation(uiSourceCode, lineNumber, columnNumber) {
const formatData = Sources.SourceFormatData._for(uiSourceCode);
if (!formatData)
return null;
const originalLocation = formatData.mapping.formattedToOriginal(lineNumber, columnNumber);
const scripts = this._scriptsForUISourceCode(formatData.originalSourceCode);
if (!scripts.length)
return null;
return scripts[0].debuggerModel.createRawLocation(scripts[0], originalLocation[0], originalLocation[1]);
}
/**
* @param {!Sources.SourceFormatData} formatData
* @param {boolean} enabled
*/
_setSourceMappingEnabled(formatData, enabled) {
const scripts = this._scriptsForUISourceCode(formatData.originalSourceCode);
if (!scripts.length)
return;
if (enabled) {
for (const script of scripts)
script[Sources.SourceFormatData._formatDataSymbol] = formatData;
} else {
for (const script of scripts)
delete script[Sources.SourceFormatData._formatDataSymbol];
}
for (const script of scripts)
Bindings.debuggerWorkspaceBinding.updateLocations(script);
}
/**
* @param {!Workspace.UISourceCode} uiSourceCode
* @return {!Array<!SDK.Script>}
*/
_scriptsForUISourceCode(uiSourceCode) {
if (uiSourceCode.contentType() === Common.resourceTypes.Document) {
const target = Bindings.NetworkProject.targetForUISourceCode(uiSourceCode);
const debuggerModel = target && target.model(SDK.DebuggerModel);
if (debuggerModel) {
const scripts = debuggerModel.scriptsForSourceURL(uiSourceCode.url())
.filter(script => script.isInlineScript() && !script.hasSourceURL);
return scripts;
}
}
if (uiSourceCode.contentType().isScript()) {
const rawLocation = Bindings.debuggerWorkspaceBinding.uiLocationToRawLocation(uiSourceCode, 0, 0);
if (rawLocation)
return [rawLocation.script()];
}
return [];
}
};
/**
* @implements {Bindings.CSSWorkspaceBinding.SourceMapping}
*/
Sources.SourceFormatter.StyleMapping = class {
constructor() {
Bindings.cssWorkspaceBinding.addSourceMapping(this);
this._headersSymbol = Symbol('Sources.SourceFormatter.StyleMapping._headersSymbol');
}
/**
* @override
* @param {!SDK.CSSLocation} rawLocation
* @return {?Workspace.UILocation}
*/
rawLocationToUILocation(rawLocation) {
const styleHeader = rawLocation.header();
const formatData = styleHeader && Sources.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.UILocation} uiLocation
* @return {!Array<!SDK.CSSLocation>}
*/
uiLocationToRawLocations(uiLocation) {
const formatData = Sources.SourceFormatData._for(uiLocation.uiSourceCode);
if (!formatData)
return [];
const originalLocation = formatData.mapping.formattedToOriginal(uiLocation.lineNumber, uiLocation.columnNumber);
const headers = formatData.originalSourceCode[this._headersSymbol];
return headers.map(header => new SDK.CSSLocation(header, originalLocation[0], originalLocation[1]));
}
/**
* @param {!Sources.SourceFormatData} formatData
* @param {boolean} enable
*/
_setSourceMappingEnabled(formatData, enable) {
const original = formatData.originalSourceCode;
const rawLocations = Bindings.cssWorkspaceBinding.uiLocationToRawLocations(original.uiLocation(0, 0));
const headers = rawLocations.map(rawLocation => rawLocation.header()).filter(header => !!header);
if (!headers.length)
return;
if (enable) {
original[this._headersSymbol] = headers;
headers.forEach(header => header[Sources.SourceFormatData._formatDataSymbol] = formatData);
} else {
original[this._headersSymbol] = null;
headers.forEach(header => delete header[Sources.SourceFormatData._formatDataSymbol]);
}
headers.forEach(header => Bindings.cssWorkspaceBinding.updateLocations(header));
}
};
Sources.sourceFormatter = new Sources.SourceFormatter();