mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-09 13:18:29 +08:00
R=jacktfranklin@chromium.org Bug: 1082789 Change-Id: I100a53f7abd5d35565a3062769273e61c00cbafd Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2612902 Auto-Submit: Tim van der Lippe <tvanderlippe@chromium.org> Commit-Queue: Jack Franklin <jacktfranklin@chromium.org> Reviewed-by: Jack Franklin <jacktfranklin@chromium.org>
540 lines
17 KiB
JavaScript
540 lines
17 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 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 {ContentProviderBasedProject} from './ContentProviderBasedProject.js';
|
|
import {CSSWorkspaceBinding} from './CSSWorkspaceBinding.js';
|
|
import {DebuggerWorkspaceBinding} from './DebuggerWorkspaceBinding.js';
|
|
import {NetworkProject} from './NetworkProject.js';
|
|
import {resourceMetadata} from './ResourceUtils.js';
|
|
|
|
/**
|
|
* @type {!ResourceMapping}
|
|
*/
|
|
let resourceMappingInstance;
|
|
|
|
/** @type WeakMap<!SDK.CSSStyleSheetHeader.CSSStyleSheetHeader, !TextUtils.TextRange.TextRange> */
|
|
const styleSheetOffsetMap = new WeakMap();
|
|
/** @type WeakMap<!SDK.Script.Script, !TextUtils.TextRange.TextRange> */
|
|
const scriptOffsetMap = new WeakMap();
|
|
/** @type WeakSet<!Workspace.UISourceCode.UISourceCode> */
|
|
const boundUISourceCodes = new WeakSet();
|
|
|
|
/**
|
|
* @implements {SDK.SDKModel.SDKModelObserver<!SDK.ResourceTreeModel.ResourceTreeModel>}
|
|
*/
|
|
export class ResourceMapping {
|
|
/**
|
|
* @private
|
|
* @param {!SDK.SDKModel.TargetManager} targetManager
|
|
* @param {!Workspace.Workspace.WorkspaceImpl} workspace
|
|
*/
|
|
constructor(targetManager, workspace) {
|
|
this._workspace = workspace;
|
|
/** @type {!Map<!SDK.ResourceTreeModel.ResourceTreeModel, !ModelInfo>} */
|
|
this._modelToInfo = new Map();
|
|
targetManager.observeModels(SDK.ResourceTreeModel.ResourceTreeModel, this);
|
|
}
|
|
|
|
/**
|
|
* @param {{forceNew: ?boolean, targetManager: ?SDK.SDKModel.TargetManager, workspace: ?Workspace.Workspace.WorkspaceImpl}} opts
|
|
*/
|
|
static instance(opts = {forceNew: null, targetManager: null, workspace: null}) {
|
|
const {forceNew, targetManager, workspace} = opts;
|
|
if (!resourceMappingInstance || forceNew) {
|
|
if (!targetManager || !workspace) {
|
|
throw new Error(
|
|
`Unable to create ResourceMapping: targetManager and workspace must be provided: ${new Error().stack}`);
|
|
}
|
|
|
|
resourceMappingInstance = new ResourceMapping(targetManager, workspace);
|
|
}
|
|
|
|
return resourceMappingInstance;
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {!SDK.ResourceTreeModel.ResourceTreeModel} resourceTreeModel
|
|
*/
|
|
modelAdded(resourceTreeModel) {
|
|
const info = new ModelInfo(this._workspace, resourceTreeModel);
|
|
this._modelToInfo.set(resourceTreeModel, info);
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {!SDK.ResourceTreeModel.ResourceTreeModel} resourceTreeModel
|
|
*/
|
|
modelRemoved(resourceTreeModel) {
|
|
const info = this._modelToInfo.get(resourceTreeModel);
|
|
if (info) {
|
|
info.dispose();
|
|
this._modelToInfo.delete(resourceTreeModel);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!SDK.SDKModel.Target} target
|
|
* @return {?ModelInfo}
|
|
*/
|
|
_infoForTarget(target) {
|
|
const resourceTreeModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel);
|
|
return resourceTreeModel ? this._modelToInfo.get(resourceTreeModel) || null : null;
|
|
}
|
|
|
|
/**
|
|
* @param {!SDK.CSSModel.CSSLocation} cssLocation
|
|
* @return {?Workspace.UISourceCode.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 = styleSheetOffsetMap.get(header) ||
|
|
TextUtils.TextRange.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.UISourceCode.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 = scriptOffsetMap.get(script) ||
|
|
TextUtils.TextRange.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} uiSourceCode
|
|
* @param {number} lineNumber
|
|
* @param {number} columnNumber
|
|
* @return {!Array<!SDK.DebuggerModel.Location>}
|
|
*/
|
|
uiLocationToJSLocations(uiSourceCode, lineNumber, columnNumber) {
|
|
if (!boundUISourceCodes.has(uiSourceCode)) {
|
|
return [];
|
|
}
|
|
const target = NetworkProject.targetForUISourceCode(uiSourceCode);
|
|
if (!target) {
|
|
return [];
|
|
}
|
|
const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel);
|
|
if (!debuggerModel) {
|
|
return [];
|
|
}
|
|
const location = debuggerModel.createRawLocationByURL(uiSourceCode.url(), lineNumber, columnNumber);
|
|
if (location) {
|
|
const script = location.script();
|
|
if (script && script.containsLocation(lineNumber, columnNumber)) {
|
|
return [location];
|
|
}
|
|
}
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* @param {!Workspace.UISourceCode.UILocation} uiLocation
|
|
* @return {!Array<!SDK.CSSModel.CSSLocation>}
|
|
*/
|
|
uiLocationToCSSLocations(uiLocation) {
|
|
if (!boundUISourceCodes.has(uiLocation.uiSourceCode)) {
|
|
return [];
|
|
}
|
|
const target = NetworkProject.targetForUISourceCode(uiLocation.uiSourceCode);
|
|
if (!target) {
|
|
return [];
|
|
}
|
|
const cssModel = target.model(SDK.CSSModel.CSSModel);
|
|
if (!cssModel) {
|
|
return [];
|
|
}
|
|
return cssModel.createRawLocationsByURL(
|
|
uiLocation.uiSourceCode.url(), uiLocation.lineNumber, uiLocation.columnNumber);
|
|
}
|
|
|
|
/**
|
|
* @param {!SDK.SDKModel.Target} target
|
|
*/
|
|
_resetForTest(target) {
|
|
const resourceTreeModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel);
|
|
const info = resourceTreeModel ? this._modelToInfo.get(resourceTreeModel) : null;
|
|
if (info) {
|
|
info._resetForTest();
|
|
}
|
|
}
|
|
}
|
|
|
|
class ModelInfo {
|
|
/**
|
|
* @param {!Workspace.Workspace.WorkspaceImpl} workspace
|
|
* @param {!SDK.ResourceTreeModel.ResourceTreeModel} resourceTreeModel
|
|
*/
|
|
constructor(workspace, resourceTreeModel) {
|
|
const target = resourceTreeModel.target();
|
|
this._project = new ContentProviderBasedProject(
|
|
workspace, 'resources:' + target.id(), Workspace.Workspace.projectTypes.Network, '',
|
|
false /* isServiceProject */);
|
|
NetworkProject.setTargetForProject(this._project, target);
|
|
|
|
/** @type {!Map<string, !Binding>} */
|
|
this._bindings = new Map();
|
|
|
|
const cssModel = target.model(SDK.CSSModel.CSSModel);
|
|
console.assert(Boolean(cssModel));
|
|
this._cssModel = /** @type {!SDK.CSSModel.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),
|
|
this._cssModel.addEventListener(
|
|
SDK.CSSModel.Events.StyleSheetChanged,
|
|
event => {
|
|
this._styleSheetChanged(event);
|
|
},
|
|
this)
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param {!Common.EventTarget.EventTargetEvent} event
|
|
*/
|
|
async _styleSheetChanged(event) {
|
|
const header = this._cssModel.styleSheetHeaderForId(event.data.styleSheetId);
|
|
if (!header || !header.isInline || (header.isInline && header.isMutable)) {
|
|
return;
|
|
}
|
|
const binding = this._bindings.get(header.resourceURL());
|
|
if (!binding) {
|
|
return;
|
|
}
|
|
await binding._styleSheetChanged(header, event.data.edit);
|
|
}
|
|
|
|
/**
|
|
* @param {!SDK.Resource.Resource} resource
|
|
*/
|
|
_acceptsResource(resource) {
|
|
const resourceType = resource.resourceType();
|
|
// Only load selected resource types from resources.
|
|
if (resourceType !== Common.ResourceType.resourceTypes.Image &&
|
|
resourceType !== Common.ResourceType.resourceTypes.Font &&
|
|
resourceType !== Common.ResourceType.resourceTypes.Document &&
|
|
resourceType !== Common.ResourceType.resourceTypes.Manifest) {
|
|
return false;
|
|
}
|
|
|
|
// Ignore non-images and non-fonts.
|
|
if (resourceType === Common.ResourceType.resourceTypes.Image && resource.mimeType &&
|
|
!resource.mimeType.startsWith('image')) {
|
|
return false;
|
|
}
|
|
if (resourceType === Common.ResourceType.resourceTypes.Font && resource.mimeType &&
|
|
!resource.mimeType.includes('font')) {
|
|
return false;
|
|
}
|
|
if ((resourceType === Common.ResourceType.resourceTypes.Image ||
|
|
resourceType === Common.ResourceType.resourceTypes.Font) &&
|
|
resource.contentURL().startsWith('data:')) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param {!Common.EventTarget.EventTargetEvent} event
|
|
*/
|
|
_resourceAdded(event) {
|
|
const resource = /** @type {!SDK.Resource.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.ResourceTreeModel.ResourceTreeFrame} frame
|
|
*/
|
|
_removeFrameResources(frame) {
|
|
for (const resource of frame.resources()) {
|
|
if (!this._acceptsResource(resource)) {
|
|
continue;
|
|
}
|
|
const binding = this._bindings.get(resource.url);
|
|
if (!binding) {
|
|
continue;
|
|
}
|
|
if (binding._resources.size === 1) {
|
|
binding.dispose();
|
|
this._bindings.delete(resource.url);
|
|
} else {
|
|
binding.removeResource(resource);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!Common.EventTarget.EventTargetEvent} event
|
|
*/
|
|
_frameWillNavigate(event) {
|
|
const frame = /** @type {!SDK.ResourceTreeModel.ResourceTreeFrame} */ (event.data);
|
|
this._removeFrameResources(frame);
|
|
}
|
|
|
|
/**
|
|
* @param {!Common.EventTarget.EventTargetEvent} event
|
|
*/
|
|
_frameDetached(event) {
|
|
const frame = /** @type {!SDK.ResourceTreeModel.ResourceTreeFrame} */ (event.data);
|
|
this._removeFrameResources(frame);
|
|
}
|
|
|
|
_resetForTest() {
|
|
for (const binding of this._bindings.values()) {
|
|
binding.dispose();
|
|
}
|
|
this._bindings.clear();
|
|
}
|
|
|
|
dispose() {
|
|
Common.EventTarget.EventTarget.removeEventListeners(this._eventListeners);
|
|
for (const binding of this._bindings.values()) {
|
|
binding.dispose();
|
|
}
|
|
this._bindings.clear();
|
|
this._project.removeProject();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @implements {TextUtils.ContentProvider.ContentProvider}
|
|
*/
|
|
class Binding {
|
|
/**
|
|
* @param {!ContentProviderBasedProject} project
|
|
* @param {!SDK.Resource.Resource} resource
|
|
*/
|
|
constructor(project, resource) {
|
|
this._resources = new Set([resource]);
|
|
this._project = project;
|
|
this._uiSourceCode = this._project.createUISourceCode(resource.url, resource.contentType());
|
|
boundUISourceCodes.add(this._uiSourceCode);
|
|
NetworkProject.setInitialFrameAttribution(this._uiSourceCode, resource.frameId);
|
|
this._project.addUISourceCodeWithProvider(this._uiSourceCode, this, resourceMetadata(resource), resource.mimeType);
|
|
/** @type {!Array<{stylesheet: !SDK.CSSStyleSheetHeader.CSSStyleSheetHeader, edit: ?SDK.CSSModel.Edit}>} */
|
|
this._edits = [];
|
|
}
|
|
|
|
/**
|
|
* @return {!Array<!SDK.CSSStyleSheetHeader.CSSStyleSheetHeader>}
|
|
*/
|
|
_inlineStyles() {
|
|
const target = NetworkProject.targetForUISourceCode(this._uiSourceCode);
|
|
/** @type {!Array<!SDK.CSSStyleSheetHeader.CSSStyleSheetHeader>} */
|
|
const stylesheets = [];
|
|
if (!target) {
|
|
return stylesheets;
|
|
}
|
|
const cssModel = target.model(SDK.CSSModel.CSSModel);
|
|
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.Script>}
|
|
*/
|
|
_inlineScripts() {
|
|
const target = NetworkProject.targetForUISourceCode(this._uiSourceCode);
|
|
if (!target) {
|
|
return [];
|
|
}
|
|
const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel);
|
|
if (!debuggerModel) {
|
|
return [];
|
|
}
|
|
return debuggerModel.scriptsForSourceURL(this._uiSourceCode.url());
|
|
}
|
|
|
|
/**
|
|
* @param {!SDK.CSSStyleSheetHeader.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) {
|
|
await this._innerStyleSheetChanged(content);
|
|
}
|
|
this._edits = [];
|
|
}
|
|
|
|
/**
|
|
* @param {string} content
|
|
*/
|
|
async _innerStyleSheetChanged(content) {
|
|
const scripts = this._inlineScripts();
|
|
const styles = this._inlineStyles();
|
|
let text = new TextUtils.Text.Text(content);
|
|
for (const data of this._edits) {
|
|
const edit = data.edit;
|
|
if (!edit) {
|
|
continue;
|
|
}
|
|
const stylesheet = data.stylesheet;
|
|
const startLocation = styleSheetOffsetMap.get(stylesheet) ||
|
|
TextUtils.TextRange.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(text.replaceRange(oldRange, edit.newText));
|
|
const updatePromises = [];
|
|
for (const script of scripts) {
|
|
const scriptOffset = scriptOffsetMap.get(script) ||
|
|
TextUtils.TextRange.TextRange.createFromLocation(script.lineOffset, script.columnOffset);
|
|
if (!scriptOffset.follows(oldRange)) {
|
|
continue;
|
|
}
|
|
scriptOffsetMap.set(script, scriptOffset.rebaseAfterTextEdit(oldRange, newRange));
|
|
updatePromises.push(DebuggerWorkspaceBinding.instance().updateLocations(script));
|
|
}
|
|
for (const style of styles) {
|
|
const styleOffset = styleSheetOffsetMap.get(style) ||
|
|
TextUtils.TextRange.TextRange.createFromLocation(style.startLine, style.startColumn);
|
|
if (!styleOffset.follows(oldRange)) {
|
|
continue;
|
|
}
|
|
styleSheetOffsetMap.set(style, styleOffset.rebaseAfterTextEdit(oldRange, newRange));
|
|
updatePromises.push(CSSWorkspaceBinding.instance().updateLocations(style));
|
|
}
|
|
await Promise.all(updatePromises);
|
|
}
|
|
this._uiSourceCode.addRevision(text.value());
|
|
}
|
|
|
|
/**
|
|
* @param {!SDK.Resource.Resource} resource
|
|
*/
|
|
addResource(resource) {
|
|
this._resources.add(resource);
|
|
NetworkProject.addFrameAttribution(this._uiSourceCode, resource.frameId);
|
|
}
|
|
|
|
/**
|
|
* @param {!SDK.Resource.Resource} resource
|
|
*/
|
|
removeResource(resource) {
|
|
this._resources.delete(resource);
|
|
NetworkProject.removeFrameAttribution(this._uiSourceCode, resource.frameId);
|
|
}
|
|
|
|
dispose() {
|
|
this._project.removeFile(this._uiSourceCode.url());
|
|
}
|
|
|
|
/**
|
|
* @return {!SDK.Resource.Resource}
|
|
*/
|
|
_firstResource() {
|
|
console.assert(this._resources.size > 0);
|
|
return this._resources.values().next().value;
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @return {string}
|
|
*/
|
|
contentURL() {
|
|
return this._firstResource().contentURL();
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @return {!Common.ResourceType.ResourceType}
|
|
*/
|
|
contentType() {
|
|
return this._firstResource().contentType();
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @return {!Promise<boolean>}
|
|
*/
|
|
contentEncoded() {
|
|
return this._firstResource().contentEncoded();
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @return {!Promise<!TextUtils.ContentProvider.DeferredContent>}
|
|
*/
|
|
requestContent() {
|
|
return this._firstResource().requestContent();
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {string} query
|
|
* @param {boolean} caseSensitive
|
|
* @param {boolean} isRegex
|
|
* @return {!Promise<!Array<!TextUtils.ContentProvider.SearchMatch>>}
|
|
*/
|
|
searchInContent(query, caseSensitive, isRegex) {
|
|
return this._firstResource().searchInContent(query, caseSensitive, isRegex);
|
|
}
|
|
}
|