chrome-devtools-frontend
Version:
Chrome DevTools UI
322 lines (286 loc) • 14.3 kB
text/typescript
// 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 Common from '../../core/common/common.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Bindings from '../bindings/bindings.js';
import * as TextUtils from '../text_utils/text_utils.js';
import * as Workspace from '../workspace/workspace.js';
import type {FormatterSourceMapping} from './ScriptFormatter.js';
import {format} from './ScriptFormatter.js';
const objectToFormattingResult = new WeakMap<Object, SourceFormatData>();
export class SourceFormatData {
originalSourceCode: Workspace.UISourceCode.UISourceCode;
formattedSourceCode: Workspace.UISourceCode.UISourceCode;
mapping: FormatterSourceMapping;
constructor(
originalSourceCode: Workspace.UISourceCode.UISourceCode, formattedSourceCode: Workspace.UISourceCode.UISourceCode,
mapping: FormatterSourceMapping) {
this.originalSourceCode = originalSourceCode;
this.formattedSourceCode = formattedSourceCode;
this.mapping = mapping;
}
originalPath(): string {
return this.originalSourceCode.project().id() + ':' + this.originalSourceCode.url();
}
static for(object: Object): SourceFormatData|null {
return objectToFormattingResult.get(object) || null;
}
}
let sourceFormatterInstance: SourceFormatter|null = null;
export class SourceFormatter {
private readonly projectId: string;
private readonly project: Bindings.ContentProviderBasedProject.ContentProviderBasedProject;
private readonly formattedSourceCodes: Map<Workspace.UISourceCode.UISourceCode, {
promise: Promise<SourceFormatData>,
formatData: SourceFormatData|null,
}>;
private readonly scriptMapping: ScriptMapping;
private readonly styleMapping: StyleMapping;
constructor() {
this.projectId = 'formatter:';
this.project = new Bindings.ContentProviderBasedProject.ContentProviderBasedProject(
Workspace.Workspace.WorkspaceImpl.instance(), this.projectId, Workspace.Workspace.projectTypes.Formatter,
'formatter', true /* isServiceProject */);
this.formattedSourceCodes = new Map();
this.scriptMapping = new ScriptMapping();
this.styleMapping = new StyleMapping();
Workspace.Workspace.WorkspaceImpl.instance().addEventListener(
Workspace.Workspace.Events.UISourceCodeRemoved, event => {
void this.onUISourceCodeRemoved(event);
}, this);
}
static instance({forceNew = false}: {forceNew?: boolean} = {}): SourceFormatter {
if (!sourceFormatterInstance || forceNew) {
sourceFormatterInstance = new SourceFormatter();
}
return sourceFormatterInstance;
}
private async onUISourceCodeRemoved(event: Common.EventTarget.EventTargetEvent<Workspace.UISourceCode.UISourceCode>):
Promise<void> {
const uiSourceCode = event.data;
const cacheEntry = this.formattedSourceCodes.get(uiSourceCode);
if (cacheEntry && cacheEntry.formatData) {
await this.discardFormatData(cacheEntry.formatData);
}
this.formattedSourceCodes.delete(uiSourceCode);
}
async discardFormattedUISourceCode(formattedUISourceCode: Workspace.UISourceCode.UISourceCode):
Promise<Workspace.UISourceCode.UISourceCode|null> {
const formatData = SourceFormatData.for(formattedUISourceCode);
if (!formatData) {
return null;
}
await this.discardFormatData(formatData);
this.formattedSourceCodes.delete(formatData.originalSourceCode);
return formatData.originalSourceCode;
}
private async discardFormatData(formatData: SourceFormatData): Promise<void> {
objectToFormattingResult.delete(formatData.formattedSourceCode);
await this.scriptMapping.setSourceMappingEnabled(formatData, false);
void this.styleMapping.setSourceMappingEnabled(formatData, false);
this.project.removeFile(formatData.formattedSourceCode.url());
}
hasFormatted(uiSourceCode: Workspace.UISourceCode.UISourceCode): boolean {
return this.formattedSourceCodes.has(uiSourceCode);
}
getOriginalUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode): Workspace.UISourceCode.UISourceCode {
const formatData = objectToFormattingResult.get(uiSourceCode);
if (!formatData) {
return uiSourceCode;
}
return formatData.originalSourceCode;
}
async format(uiSourceCode: Workspace.UISourceCode.UISourceCode): Promise<SourceFormatData> {
const cacheEntry = this.formattedSourceCodes.get(uiSourceCode);
if (cacheEntry) {
return cacheEntry.promise;
}
const resultPromise = new Promise<SourceFormatData>(async (resolve, reject) => {
const {content} = await uiSourceCode.requestContent();
try {
const {formattedContent, formattedMapping} =
await format(uiSourceCode.contentType(), uiSourceCode.mimeType(), content || '');
const cacheEntry = this.formattedSourceCodes.get(uiSourceCode);
if (!cacheEntry || cacheEntry.promise !== resultPromise) {
return;
}
let formattedURL;
let count = 0;
let suffix = '';
do {
formattedURL = Common.ParsedURL.ParsedURL.concatenate(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, formattedMapping);
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;
resolve(formatData);
} catch (e) {
reject(e);
}
});
this.formattedSourceCodes.set(uiSourceCode, {promise: resultPromise, formatData: null});
return resultPromise;
}
}
class ScriptMapping implements Bindings.DebuggerWorkspaceBinding.DebuggerSourceMapping {
constructor() {
Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().addSourceMapping(this);
}
rawLocationToUILocation(rawLocation: SDK.DebuggerModel.Location): Workspace.UISourceCode.UILocation|null {
const script = rawLocation.script();
const formatData = script && SourceFormatData.for(script);
if (!formatData || !script) {
return null;
}
const [lineNumber, columnNumber] =
formatData.mapping.originalToFormatted(rawLocation.lineNumber, rawLocation.columnNumber || 0);
return formatData.formattedSourceCode.uiLocation(lineNumber, columnNumber);
}
uiLocationToRawLocations(uiSourceCode: Workspace.UISourceCode.UISourceCode, lineNumber: number, columnNumber: number):
SDK.DebuggerModel.Location[] {
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 =
(scripts.map(script => script.rawLocation(originalLine, originalColumn)).filter(l => Boolean(l)) as
SDK.DebuggerModel.Location[]);
console.assert(locations.every(l => l && Boolean(l.script())));
return locations;
}
}
return [];
}
async setSourceMappingEnabled(formatData: SourceFormatData, enabled: boolean): Promise<void> {
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);
}
private scriptsForUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode): SDK.Script.Script[] {
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 rawLocations.map(location => location.script()).filter(script => Boolean(script)) as SDK.Script.Script[];
}
return [];
}
}
const sourceCodeToHeaders =
new WeakMap<Workspace.UISourceCode.UISourceCode, SDK.CSSStyleSheetHeader.CSSStyleSheetHeader[]>();
class StyleMapping implements Bindings.CSSWorkspaceBinding.SourceMapping {
private readonly headersSymbol: symbol;
constructor() {
Bindings.CSSWorkspaceBinding.CSSWorkspaceBinding.instance().addSourceMapping(this);
this.headersSymbol = Symbol('Formatter.SourceFormatter.StyleMapping._headersSymbol');
}
rawLocationToUILocation(rawLocation: SDK.CSSModel.CSSLocation): Workspace.UISourceCode.UILocation|null {
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]);
}
uiLocationToRawLocations(uiLocation: Workspace.UISourceCode.UILocation): SDK.CSSModel.CSSLocation[] {
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));
}
async setSourceMappingEnabled(formatData: SourceFormatData, enable: boolean): Promise<void> {
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);
}
private headersForUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode):
SDK.CSSStyleSheetHeader.CSSStyleSheetHeader[] {
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 => Boolean(header)) as
SDK.CSSStyleSheetHeader.CSSStyleSheetHeader[];
}
return [];
}
}