blob: f3ad2b1b7c1475b1ece4ca82fd87986b39e10a55 [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.
/**
* @implements {SDK.SDKModelObserver<!SDK.ResourceTreeModel>}
*/
export default class ResourceMapping {
/**
* @param {!SDK.TargetManager} targetManager
* @param {!Workspace.Workspace} workspace
*/
constructor(targetManager, workspace) {
this._workspace = workspace;
/** @type {!Map<!SDK.ResourceTreeModel, !ModelInfo>} */
this._modelToInfo = new Map();
targetManager.observeModels(SDK.ResourceTreeModel, this);
}
/**
* @override
* @param {!SDK.ResourceTreeModel} resourceTreeModel
*/
modelAdded(resourceTreeModel) {
const info = new ModelInfo(this._workspace, resourceTreeModel);
this._modelToInfo.set(resourceTreeModel, info);
}
/**
* @override
* @param {!SDK.ResourceTreeModel} resourceTreeModel
*/
modelRemoved(resourceTreeModel) {
const info = this._modelToInfo.get(resourceTreeModel);
info.dispose();
this._modelToInfo.delete(resourceTreeModel);
}
/**
* @param {!SDK.Target} target
* @return {?ModelInfo}
*/
_infoForTarget(target) {
const resourceTreeModel = target.model(SDK.ResourceTreeModel);
return resourceTreeModel ? this._modelToInfo.get(resourceTreeModel) : null;
}
/**
* @param {!SDK.CSSLocation} cssLocation
* @return {?Workspace.UILocation}
*/
cssLocationToUILocation(cssLocation) {
const header = cssLocation.header();
if (!header) {
return null;
}
const info = this._infoForTarget(cssLocation.cssModel().target());
if (!info) {
return null;
}
const uiSourceCode = info._project.uiSourceCodeForURL(cssLocation.url);
if (!uiSourceCode) {
return null;
}
const offset =
header[_offsetSymbol] || TextUtils.TextRange.createFromLocation(header.startLine, header.startColumn);
const lineNumber = cssLocation.lineNumber + offset.startLine - header.startLine;
let columnNumber = cssLocation.columnNumber;
if (cssLocation.lineNumber === header.startLine) {
columnNumber += offset.startColumn - header.startColumn;
}
return uiSourceCode.uiLocation(lineNumber, columnNumber);
}
/**
* @param {!SDK.DebuggerModel.Location} jsLocation
* @return {?Workspace.UILocation}
*/
jsLocationToUILocation(jsLocation) {
const script = jsLocation.script();
if (!script) {
return null;
}
const info = this._infoForTarget(jsLocation.debuggerModel.target());
if (!info) {
return null;
}
const uiSourceCode = info._project.uiSourceCodeForURL(script.sourceURL);
if (!uiSourceCode) {
return null;
}
const offset =
script[_offsetSymbol] || TextUtils.TextRange.createFromLocation(script.lineOffset, script.columnOffset);
const lineNumber = jsLocation.lineNumber + offset.startLine - script.lineOffset;
let columnNumber = jsLocation.columnNumber;
if (jsLocation.lineNumber === script.lineOffset) {
columnNumber += offset.startColumn - script.columnOffset;
}
return uiSourceCode.uiLocation(lineNumber, columnNumber);
}
/**
* @param {!Workspace.UISourceCode} uiSourceCode
* @param {number} lineNumber
* @param {number} columnNumber
* @return {!Array<!SDK.DebuggerModel.Location>}
*/
uiLocationToJSLocations(uiSourceCode, lineNumber, columnNumber) {
if (!uiSourceCode[_symbol]) {
return [];
}
const target = Bindings.NetworkProject.targetForUISourceCode(uiSourceCode);
if (!target) {
return [];
}
const debuggerModel = target.model(SDK.DebuggerModel);
if (!debuggerModel) {
return [];
}
const location = debuggerModel.createRawLocationByURL(uiSourceCode.url(), lineNumber, columnNumber);
if (location && location.script().containsLocation(lineNumber, columnNumber)) {
return [location];
}
return [];
}
/**
* @param {!Workspace.UILocation} uiLocation
* @return {!Array<!SDK.CSSLocation>}
*/
uiLocationToCSSLocations(uiLocation) {
if (!uiLocation.uiSourceCode[_symbol]) {
return [];
}
const target = Bindings.NetworkProject.targetForUISourceCode(uiLocation.uiSourceCode);
if (!target) {
return [];
}
const cssModel = target.model(SDK.CSSModel);
if (!cssModel) {
return [];
}
return cssModel.createRawLocationsByURL(
uiLocation.uiSourceCode.url(), uiLocation.lineNumber, uiLocation.columnNumber);
}
/**
* @param {!SDK.Target} target
*/
_resetForTest(target) {
const resourceTreeModel = target.model(SDK.ResourceTreeModel);
const info = resourceTreeModel ? this._modelToInfo.get(resourceTreeModel) : null;
if (info) {
info._resetForTest();
}
}
}
class ModelInfo {
/**
* @param {!Workspace.Workspace} workspace
* @param {!SDK.ResourceTreeModel} resourceTreeModel
*/
constructor(workspace, resourceTreeModel) {
const target = resourceTreeModel.target();
this._project = new Bindings.ContentProviderBasedProject(
workspace, 'resources:' + target.id(), Workspace.projectTypes.Network, '', false /* isServiceProject */);
Bindings.NetworkProject.setTargetForProject(this._project, target);
/** @type {!Map<string, !Binding>} */
this._bindings = new Map();
const cssModel = target.model(SDK.CSSModel);
this._cssModel = cssModel;
this._eventListeners = [
resourceTreeModel.addEventListener(SDK.ResourceTreeModel.Events.ResourceAdded, this._resourceAdded, this),
resourceTreeModel.addEventListener(SDK.ResourceTreeModel.Events.FrameWillNavigate, this._frameWillNavigate, this),
resourceTreeModel.addEventListener(SDK.ResourceTreeModel.Events.FrameDetached, this._frameDetached, this),
cssModel.addEventListener(SDK.CSSModel.Events.StyleSheetChanged, this._styleSheetChanged, this)
];
}
/**
* @param {!Common.Event} event
*/
_styleSheetChanged(event) {
const header = this._cssModel.styleSheetHeaderForId(event.data.styleSheetId);
if (!header || !header.isInline) {
return;
}
const binding = this._bindings.get(header.resourceURL());
if (!binding) {
return;
}
binding._styleSheetChanged(header, event.data.edit);
}
/**
* @param {!SDK.Resource} resource
*/
_acceptsResource(resource) {
const resourceType = resource.resourceType();
// Only load selected resource types from resources.
if (resourceType !== Common.resourceTypes.Image && resourceType !== Common.resourceTypes.Font &&
resourceType !== Common.resourceTypes.Document && resourceType !== Common.resourceTypes.Manifest) {
return false;
}
// Ignore non-images and non-fonts.
if (resourceType === Common.resourceTypes.Image && resource.mimeType && !resource.mimeType.startsWith('image')) {
return false;
}
if (resourceType === Common.resourceTypes.Font && resource.mimeType && !resource.mimeType.includes('font')) {
return false;
}
if ((resourceType === Common.resourceTypes.Image || resourceType === Common.resourceTypes.Font) &&
resource.contentURL().startsWith('data:')) {
return false;
}
return true;
}
/**
* @param {!Common.Event} event
*/
_resourceAdded(event) {
const resource = /** @type {!SDK.Resource} */ (event.data);
if (!this._acceptsResource(resource)) {
return;
}
let binding = this._bindings.get(resource.url);
if (!binding) {
binding = new Binding(this._project, resource);
this._bindings.set(resource.url, binding);
} else {
binding.addResource(resource);
}
}
/**
* @param {!SDK.ResourceTreeFrame} frame
*/
_removeFrameResources(frame) {
for (const resource of frame.resources()) {
if (!this._acceptsResource(resource)) {
continue;
}
const binding = this._bindings.get(resource.url);
if (binding._resources.size === 1) {
binding.dispose();
this._bindings.delete(resource.url);
} else {
binding.removeResource(resource);
}
}
}
/**
* @param {!Common.Event} event
*/
_frameWillNavigate(event) {
const frame = /** @type {!SDK.ResourceTreeFrame} */ (event.data);
this._removeFrameResources(frame);
}
/**
* @param {!Common.Event} event
*/
_frameDetached(event) {
const frame = /** @type {!SDK.ResourceTreeFrame} */ (event.data);
this._removeFrameResources(frame);
}
_resetForTest() {
for (const binding of this._bindings.valuesArray()) {
binding.dispose();
}
this._bindings.clear();
}
dispose() {
Common.EventTarget.removeEventListeners(this._eventListeners);
for (const binding of this._bindings.valuesArray()) {
binding.dispose();
}
this._bindings.clear();
this._project.removeProject();
}
}
/**
* @implements {Common.ContentProvider}
*/
class Binding {
/**
* @param {!Bindings.ContentProviderBasedProject} project
* @param {!SDK.Resource} resource
*/
constructor(project, resource) {
this._resources = new Set([resource]);
this._project = project;
this._uiSourceCode = this._project.createUISourceCode(resource.url, resource.contentType());
this._uiSourceCode[_symbol] = true;
Bindings.NetworkProject.setInitialFrameAttribution(this._uiSourceCode, resource.frameId);
this._project.addUISourceCodeWithProvider(
this._uiSourceCode, this, Bindings.resourceMetadata(resource), resource.mimeType);
/** @type {!Array<{stylesheet: !SDK.CSSStyleSheetHeader, edit: !SDK.CSSModel.Edit}>} */
this._edits = [];
}
/**
* @return {!Array<!SDK.CSSStyleSheetHeader>}
*/
_inlineStyles() {
const target = Bindings.NetworkProject.targetForUISourceCode(this._uiSourceCode);
const cssModel = target.model(SDK.CSSModel);
const stylesheets = [];
if (cssModel) {
for (const headerId of cssModel.styleSheetIdsForURL(this._uiSourceCode.url())) {
const header = cssModel.styleSheetHeaderForId(headerId);
if (header) {
stylesheets.push(header);
}
}
}
return stylesheets;
}
/**
* @return {!Array<!SDK.Script>}
*/
_inlineScripts() {
const target = Bindings.NetworkProject.targetForUISourceCode(this._uiSourceCode);
const debuggerModel = target.model(SDK.DebuggerModel);
if (!debuggerModel) {
return [];
}
return debuggerModel.scriptsForSourceURL(this._uiSourceCode.url());
}
/**
* @param {!SDK.CSSStyleSheetHeader} stylesheet
* @param {!SDK.CSSModel.Edit} edit
*/
async _styleSheetChanged(stylesheet, edit) {
this._edits.push({stylesheet, edit});
if (this._edits.length > 1) {
return;
} // There is already a _styleSheetChanged loop running
const {content} = await this._uiSourceCode.requestContent();
if (content !== null) {
this._innerStyleSheetChanged(content);
}
this._edits = [];
}
/**
* @param {string} content
*/
_innerStyleSheetChanged(content) {
const scripts = this._inlineScripts();
const styles = this._inlineStyles();
let text = new TextUtils.Text(content);
for (const data of this._edits) {
const edit = data.edit;
const stylesheet = data.stylesheet;
const startLocation = stylesheet[_offsetSymbol] ||
TextUtils.TextRange.createFromLocation(stylesheet.startLine, stylesheet.startColumn);
const oldRange = edit.oldRange.relativeFrom(startLocation.startLine, startLocation.startColumn);
const newRange = edit.newRange.relativeFrom(startLocation.startLine, startLocation.startColumn);
text = new TextUtils.Text(text.replaceRange(oldRange, edit.newText));
for (const script of scripts) {
const scriptOffset =
script[_offsetSymbol] || TextUtils.TextRange.createFromLocation(script.lineOffset, script.columnOffset);
if (!scriptOffset.follows(oldRange)) {
continue;
}
script[_offsetSymbol] = scriptOffset.rebaseAfterTextEdit(oldRange, newRange);
Bindings.debuggerWorkspaceBinding.updateLocations(script);
}
for (const style of styles) {
const styleOffset =
style[_offsetSymbol] || TextUtils.TextRange.createFromLocation(style.startLine, style.startColumn);
if (!styleOffset.follows(oldRange)) {
continue;
}
style[_offsetSymbol] = styleOffset.rebaseAfterTextEdit(oldRange, newRange);
Bindings.cssWorkspaceBinding.updateLocations(style);
}
}
this._uiSourceCode.addRevision(text.value());
}
/**
* @param {!SDK.Resource} resource
*/
addResource(resource) {
this._resources.add(resource);
Bindings.NetworkProject.addFrameAttribution(this._uiSourceCode, resource.frameId);
}
/**
* @param {!SDK.Resource} resource
*/
removeResource(resource) {
this._resources.delete(resource);
Bindings.NetworkProject.removeFrameAttribution(this._uiSourceCode, resource.frameId);
}
dispose() {
this._project.removeFile(this._uiSourceCode.url());
}
/**
* @override
* @return {string}
*/
contentURL() {
return this._resources.firstValue().contentURL();
}
/**
* @override
* @return {!Common.ResourceType}
*/
contentType() {
return this._resources.firstValue().contentType();
}
/**
* @override
* @return {!Promise<boolean>}
*/
contentEncoded() {
return this._resources.firstValue().contentEncoded();
}
/**
* @override
* @return {!Promise<!Common.DeferredContent>}
*/
requestContent() {
return this._resources.firstValue().requestContent();
}
/**
* @override
* @param {string} query
* @param {boolean} caseSensitive
* @param {boolean} isRegex
* @return {!Promise<!Array<!Common.ContentProvider.SearchMatch>>}
*/
searchInContent(query, caseSensitive, isRegex) {
return this._resources.firstValue().searchInContent(query, caseSensitive, isRegex);
}
}
export const _symbol = Symbol('Bindings.ResourceMapping._symbol');
export const _offsetSymbol = Symbol('Bindings.ResourceMapping._offsetSymbol');
/* Legacy exported object */
self.Bindings = self.Bindings || {};
/* Legacy exported object */
Bindings = Bindings || {};
/** @constructor */
Bindings.ResourceMapping = ResourceMapping;
Bindings.ResourceMapping._symbol = _symbol;
Bindings.ResourceMapping._offsetSymbol = _offsetSymbol;