chrome-devtools-frontend
Version:
Chrome DevTools UI
542 lines (491 loc) • 19.9 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 type * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/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';
const styleSheetRangeMap = new WeakMap<SDK.CSSStyleSheetHeader.CSSStyleSheetHeader, TextUtils.TextRange.TextRange>();
const scriptRangeMap = new WeakMap<SDK.Script.Script, TextUtils.TextRange.TextRange>();
const boundUISourceCodes = new WeakSet<Workspace.UISourceCode.UISourceCode>();
function computeScriptRange(script: SDK.Script.Script): TextUtils.TextRange.TextRange {
return new TextUtils.TextRange.TextRange(script.lineOffset, script.columnOffset, script.endLine, script.endColumn);
}
function computeStyleSheetRange(header: SDK.CSSStyleSheetHeader.CSSStyleSheetHeader): TextUtils.TextRange.TextRange {
return new TextUtils.TextRange.TextRange(header.startLine, header.startColumn, header.endLine, header.endColumn);
}
export class ResourceMapping implements SDK.TargetManager.SDKModelObserver<SDK.ResourceTreeModel.ResourceTreeModel> {
readonly workspace: Workspace.Workspace.WorkspaceImpl;
readonly #modelToInfo: Map<SDK.ResourceTreeModel.ResourceTreeModel, ModelInfo>;
constructor(targetManager: SDK.TargetManager.TargetManager, workspace: Workspace.Workspace.WorkspaceImpl) {
this.workspace = workspace;
this.#modelToInfo = new Map();
targetManager.observeModels(SDK.ResourceTreeModel.ResourceTreeModel, this);
}
modelAdded(resourceTreeModel: SDK.ResourceTreeModel.ResourceTreeModel): void {
const info = new ModelInfo(this.workspace, resourceTreeModel);
this.#modelToInfo.set(resourceTreeModel, info);
}
modelRemoved(resourceTreeModel: SDK.ResourceTreeModel.ResourceTreeModel): void {
const info = this.#modelToInfo.get(resourceTreeModel);
if (info) {
info.dispose();
this.#modelToInfo.delete(resourceTreeModel);
}
}
private infoForTarget(target: SDK.Target.Target): ModelInfo|null {
const resourceTreeModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel);
return resourceTreeModel ? this.#modelToInfo.get(resourceTreeModel) || null : null;
}
uiSourceCodeForScript(script: SDK.Script.Script): Workspace.UISourceCode.UISourceCode|null {
const info = this.infoForTarget(script.debuggerModel.target());
if (!info) {
return null;
}
const project = info.getProject();
const uiSourceCode = project.uiSourceCodeForURL(script.sourceURL);
return uiSourceCode;
}
cssLocationToUILocation(cssLocation: SDK.CSSModel.CSSLocation): Workspace.UISourceCode.UILocation|null {
const header = cssLocation.header();
if (!header) {
return null;
}
const info = this.infoForTarget(cssLocation.cssModel().target());
if (!info) {
return null;
}
const uiSourceCode = info.getProject().uiSourceCodeForURL(cssLocation.url);
if (!uiSourceCode) {
return null;
}
const offset = styleSheetRangeMap.get(header) ?? computeStyleSheetRange(header);
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);
}
jsLocationToUILocation(jsLocation: SDK.DebuggerModel.Location): Workspace.UISourceCode.UILocation|null {
const script = jsLocation.script();
if (!script) {
return null;
}
const info = this.infoForTarget(jsLocation.debuggerModel.target());
if (!info) {
return null;
}
const embedderName = script.embedderName();
if (!embedderName) {
return null;
}
const uiSourceCode = info.getProject().uiSourceCodeForURL(embedderName);
if (!uiSourceCode) {
return null;
}
const {startLine, startColumn} = scriptRangeMap.get(script) ?? computeScriptRange(script);
let {lineNumber, columnNumber} = jsLocation;
if (lineNumber === script.lineOffset) {
columnNumber += startColumn - script.columnOffset;
}
lineNumber += startLine - script.lineOffset;
if (script.hasSourceURL) {
if (lineNumber === 0) {
columnNumber += script.columnOffset;
}
lineNumber += script.lineOffset;
}
return uiSourceCode.uiLocation(lineNumber, columnNumber);
}
uiLocationToJSLocations(uiSourceCode: Workspace.UISourceCode.UISourceCode, lineNumber: number, columnNumber: number):
SDK.DebuggerModel.Location[] {
if (!boundUISourceCodes.has(uiSourceCode)) {
return [];
}
const target = NetworkProject.targetForUISourceCode(uiSourceCode);
if (!target) {
return [];
}
const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel);
if (!debuggerModel) {
return [];
}
const locations = [];
for (const script of debuggerModel.scripts()) {
if (script.embedderName() !== uiSourceCode.url()) {
continue;
}
const range = scriptRangeMap.get(script) ?? computeScriptRange(script);
if (!range.containsLocation(lineNumber, columnNumber)) {
continue;
}
let scriptLineNumber = lineNumber;
let scriptColumnNumber = columnNumber;
if (script.hasSourceURL) {
scriptLineNumber -= range.startLine;
if (scriptLineNumber === 0) {
scriptColumnNumber -= range.startColumn;
}
}
locations.push(debuggerModel.createRawLocation(script, scriptLineNumber, scriptColumnNumber));
}
return locations;
}
uiLocationRangeToJSLocationRanges(
uiSourceCode: Workspace.UISourceCode.UISourceCode,
textRange: TextUtils.TextRange.TextRange): SDK.DebuggerModel.LocationRange[]|null {
if (!boundUISourceCodes.has(uiSourceCode)) {
return null;
}
const target = NetworkProject.targetForUISourceCode(uiSourceCode);
if (!target) {
return null;
}
const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel);
if (!debuggerModel) {
return null;
}
const ranges = [];
for (const script of debuggerModel.scripts()) {
if (script.embedderName() !== uiSourceCode.url()) {
continue;
}
const scriptTextRange = scriptRangeMap.get(script) ?? computeScriptRange(script);
const range = scriptTextRange.intersection(textRange);
if (range.isEmpty()) {
continue;
}
let {startLine, startColumn, endLine, endColumn} = range;
if (script.hasSourceURL) {
startLine -= range.startLine;
if (startLine === 0) {
startColumn -= range.startColumn;
}
endLine -= range.startLine;
if (endLine === 0) {
endColumn -= range.startColumn;
}
}
const start = debuggerModel.createRawLocation(script, startLine, startColumn);
const end = debuggerModel.createRawLocation(script, endLine, endColumn);
ranges.push({start, end});
}
return ranges;
}
getMappedLines(uiSourceCode: Workspace.UISourceCode.UISourceCode): Set<number>|null {
if (!boundUISourceCodes.has(uiSourceCode)) {
return null;
}
const target = NetworkProject.targetForUISourceCode(uiSourceCode);
if (!target) {
return null;
}
const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel);
if (!debuggerModel) {
return null;
}
const mappedLines = new Set<number>();
for (const script of debuggerModel.scripts()) {
if (script.embedderName() !== uiSourceCode.url()) {
continue;
}
const {startLine, endLine} = scriptRangeMap.get(script) ?? computeScriptRange(script);
for (let line = startLine; line <= endLine; ++line) {
mappedLines.add(line);
}
}
return mappedLines;
}
uiLocationToCSSLocations(uiLocation: Workspace.UISourceCode.UILocation): SDK.CSSModel.CSSLocation[] {
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);
}
resetForTest(target: SDK.Target.Target): void {
const resourceTreeModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel);
const info = resourceTreeModel ? this.#modelToInfo.get(resourceTreeModel) : null;
if (info) {
info.resetForTest();
}
}
}
class ModelInfo {
project: ContentProviderBasedProject;
readonly #bindings: Map<string, Binding>;
readonly #cssModel: SDK.CSSModel.CSSModel;
readonly #eventListeners: Common.EventTarget.EventDescriptor[];
constructor(
workspace: Workspace.Workspace.WorkspaceImpl, resourceTreeModel: SDK.ResourceTreeModel.ResourceTreeModel) {
const target = resourceTreeModel.target();
this.project = new ContentProviderBasedProject(
workspace, 'resources:' + target.id(), Workspace.Workspace.projectTypes.Network, '',
false /* isServiceProject */);
NetworkProject.setTargetForProject(this.project, target);
this.#bindings = new Map();
const cssModel = target.model(SDK.CSSModel.CSSModel);
console.assert(Boolean(cssModel));
this.#cssModel = (cssModel as SDK.CSSModel.CSSModel);
for (const frame of resourceTreeModel.frames()) {
for (const resource of frame.getResourcesMap().values()) {
this.addResource(resource);
}
}
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 => {
void this.styleSheetChanged(event);
},
this),
];
}
private async styleSheetChanged(event: Common.EventTarget.EventTargetEvent<SDK.CSSModel.StyleSheetChangedEvent>):
Promise<void> {
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 || null);
}
private acceptsResource(resource: SDK.Resource.Resource): boolean {
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;
}
private resourceAdded(event: Common.EventTarget.EventTargetEvent<SDK.Resource.Resource>): void {
this.addResource(event.data);
}
private addResource(resource: SDK.Resource.Resource): void {
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);
}
}
private removeFrameResources(frame: SDK.ResourceTreeModel.ResourceTreeFrame): void {
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);
}
}
}
private frameWillNavigate(event: Common.EventTarget.EventTargetEvent<SDK.ResourceTreeModel.ResourceTreeFrame>): void {
this.removeFrameResources(event.data);
}
private frameDetached(
event: Common.EventTarget.EventTargetEvent<{frame: SDK.ResourceTreeModel.ResourceTreeFrame, isSwap: boolean}>):
void {
this.removeFrameResources(event.data.frame);
}
resetForTest(): void {
for (const binding of this.#bindings.values()) {
binding.dispose();
}
this.#bindings.clear();
}
dispose(): void {
Common.EventTarget.removeEventListeners(this.#eventListeners);
for (const binding of this.#bindings.values()) {
binding.dispose();
}
this.#bindings.clear();
this.project.removeProject();
}
getProject(): ContentProviderBasedProject {
return this.project;
}
}
class Binding implements TextUtils.ContentProvider.ContentProvider {
readonly resources: Set<SDK.Resource.Resource>;
readonly #project: ContentProviderBasedProject;
readonly #uiSourceCode: Workspace.UISourceCode.UISourceCode;
#edits: {
stylesheet: SDK.CSSStyleSheetHeader.CSSStyleSheetHeader,
edit: SDK.CSSModel.Edit|null,
}[];
constructor(project: ContentProviderBasedProject, resource: SDK.Resource.Resource) {
this.resources = new Set([resource]);
this.#project = project;
this.#uiSourceCode = this.#project.createUISourceCode(resource.url, resource.contentType());
boundUISourceCodes.add(this.#uiSourceCode);
if (resource.frameId) {
NetworkProject.setInitialFrameAttribution(this.#uiSourceCode, resource.frameId);
}
this.#project.addUISourceCodeWithProvider(this.#uiSourceCode, this, resourceMetadata(resource), resource.mimeType);
this.#edits = [];
void Promise.all([
...this.inlineScripts().map(script => DebuggerWorkspaceBinding.instance().updateLocations(script)),
...this.inlineStyles().map(style => CSSWorkspaceBinding.instance().updateLocations(style)),
]);
}
private inlineStyles(): SDK.CSSStyleSheetHeader.CSSStyleSheetHeader[] {
const target = NetworkProject.targetForUISourceCode(this.#uiSourceCode);
const stylesheets: SDK.CSSStyleSheetHeader.CSSStyleSheetHeader[] = [];
if (!target) {
return stylesheets;
}
const cssModel = target.model(SDK.CSSModel.CSSModel);
if (cssModel) {
for (const headerId of cssModel.getStyleSheetIdsForURL(this.#uiSourceCode.url())) {
const header = cssModel.styleSheetHeaderForId(headerId);
if (header) {
stylesheets.push(header);
}
}
}
return stylesheets;
}
private inlineScripts(): SDK.Script.Script[] {
const target = NetworkProject.targetForUISourceCode(this.#uiSourceCode);
if (!target) {
return [];
}
const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel);
if (!debuggerModel) {
return [];
}
return debuggerModel.scripts().filter(script => script.embedderName() === this.#uiSourceCode.url());
}
async styleSheetChanged(stylesheet: SDK.CSSStyleSheetHeader.CSSStyleSheetHeader, edit: SDK.CSSModel.Edit|null):
Promise<void> {
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 = [];
}
private async innerStyleSheetChanged(content: string): Promise<void> {
const scripts = this.inlineScripts();
const styles = this.inlineStyles();
let text: TextUtils.Text.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 = styleSheetRangeMap.get(stylesheet) ?? computeStyleSheetRange(stylesheet);
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 range = scriptRangeMap.get(script) ?? computeScriptRange(script);
if (!range.follows(oldRange)) {
continue;
}
scriptRangeMap.set(script, range.rebaseAfterTextEdit(oldRange, newRange));
updatePromises.push(DebuggerWorkspaceBinding.instance().updateLocations(script));
}
for (const style of styles) {
const range = styleSheetRangeMap.get(style) ?? computeStyleSheetRange(style);
if (!range.follows(oldRange)) {
continue;
}
styleSheetRangeMap.set(style, range.rebaseAfterTextEdit(oldRange, newRange));
updatePromises.push(CSSWorkspaceBinding.instance().updateLocations(style));
}
await Promise.all(updatePromises);
}
this.#uiSourceCode.addRevision(text.value());
}
addResource(resource: SDK.Resource.Resource): void {
this.resources.add(resource);
if (resource.frameId) {
NetworkProject.addFrameAttribution(this.#uiSourceCode, resource.frameId);
}
}
removeResource(resource: SDK.Resource.Resource): void {
this.resources.delete(resource);
if (resource.frameId) {
NetworkProject.removeFrameAttribution(this.#uiSourceCode, resource.frameId);
}
}
dispose(): void {
this.#project.removeUISourceCode(this.#uiSourceCode.url());
void Promise.all([
...this.inlineScripts().map(script => DebuggerWorkspaceBinding.instance().updateLocations(script)),
...this.inlineStyles().map(style => CSSWorkspaceBinding.instance().updateLocations(style)),
]);
}
private firstResource(): SDK.Resource.Resource {
console.assert(this.resources.size > 0);
return this.resources.values().next().value;
}
contentURL(): Platform.DevToolsPath.UrlString {
return this.firstResource().contentURL();
}
contentType(): Common.ResourceType.ResourceType {
return this.firstResource().contentType();
}
requestContent(): Promise<TextUtils.ContentProvider.DeferredContent> {
return this.firstResource().requestContent();
}
searchInContent(query: string, caseSensitive: boolean, isRegex: boolean):
Promise<TextUtils.ContentProvider.SearchMatch[]> {
return this.firstResource().searchInContent(query, caseSensitive, isRegex);
}
}