chrome-devtools-frontend
Version:
Chrome DevTools UI
401 lines (337 loc) • 15.8 kB
text/typescript
// Copyright 2016 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 Host from '../../core/host/host.js';
import * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Components from '../../ui/legacy/components/utils/utils.js';
import * as Bindings from '../bindings/bindings.js';
import * as BreakpointManager from '../breakpoints/breakpoints.js';
import * as Workspace from '../workspace/workspace.js';
import {Automapping, type AutomappingStatus} from './Automapping.js';
import {LinkDecorator} from './PersistenceUtils.js';
let persistenceInstance: PersistenceImpl;
export class PersistenceImpl extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
private readonly workspace: Workspace.Workspace.WorkspaceImpl;
private readonly breakpointManager: BreakpointManager.BreakpointManager.BreakpointManager;
private readonly filePathPrefixesToBindingCount: FilePathPrefixesBindingCounts;
private subscribedBindingEventListeners:
Platform.MapUtilities.Multimap<Workspace.UISourceCode.UISourceCode, () => void>;
private readonly mapping: Automapping;
constructor(
workspace: Workspace.Workspace.WorkspaceImpl,
breakpointManager: BreakpointManager.BreakpointManager.BreakpointManager) {
super();
this.workspace = workspace;
this.breakpointManager = breakpointManager;
this.breakpointManager.addUpdateBindingsCallback(this.#setupBindings.bind(this));
this.filePathPrefixesToBindingCount = new FilePathPrefixesBindingCounts();
this.subscribedBindingEventListeners = new Platform.MapUtilities.Multimap();
const linkDecorator = new LinkDecorator(this);
Components.Linkifier.Linkifier.setLinkDecorator(linkDecorator);
this.mapping = new Automapping(this.workspace, this.onStatusAdded.bind(this), this.onStatusRemoved.bind(this));
}
static instance(opts: {
forceNew: boolean|null,
workspace: Workspace.Workspace.WorkspaceImpl|null,
breakpointManager: BreakpointManager.BreakpointManager.BreakpointManager|null,
} = {forceNew: null, workspace: null, breakpointManager: null}): PersistenceImpl {
const {forceNew, workspace, breakpointManager} = opts;
if (!persistenceInstance || forceNew) {
if (!workspace || !breakpointManager) {
throw new Error('Missing arguments for workspace');
}
persistenceInstance = new PersistenceImpl(workspace, breakpointManager);
}
return persistenceInstance;
}
addNetworkInterceptor(interceptor: (arg0: Workspace.UISourceCode.UISourceCode) => boolean): void {
this.mapping.addNetworkInterceptor(interceptor);
}
refreshAutomapping(): void {
this.mapping.scheduleRemap();
}
async addBinding(binding: PersistenceBinding): Promise<void> {
await this.innerAddBinding(binding);
}
async addBindingForTest(binding: PersistenceBinding): Promise<void> {
await this.innerAddBinding(binding);
}
async removeBinding(binding: PersistenceBinding): Promise<void> {
await this.innerRemoveBinding(binding);
}
async removeBindingForTest(binding: PersistenceBinding): Promise<void> {
await this.innerRemoveBinding(binding);
}
#setupBindings(networkUISourceCode: Workspace.UISourceCode.UISourceCode): Promise<void> {
if (networkUISourceCode.project().type() !== Workspace.Workspace.projectTypes.Network) {
return Promise.resolve();
}
return this.mapping.computeNetworkStatus(networkUISourceCode);
}
private async innerAddBinding(binding: PersistenceBinding): Promise<void> {
bindings.set(binding.network, binding);
bindings.set(binding.fileSystem, binding);
binding.fileSystem.forceLoadOnCheckContent();
binding.network.addEventListener(
Workspace.UISourceCode.Events.WorkingCopyCommitted, this.onWorkingCopyCommitted, this);
binding.fileSystem.addEventListener(
Workspace.UISourceCode.Events.WorkingCopyCommitted, this.onWorkingCopyCommitted, this);
binding.network.addEventListener(Workspace.UISourceCode.Events.WorkingCopyChanged, this.onWorkingCopyChanged, this);
binding.fileSystem.addEventListener(
Workspace.UISourceCode.Events.WorkingCopyChanged, this.onWorkingCopyChanged, this);
this.filePathPrefixesToBindingCount.add(binding.fileSystem.url());
await this.moveBreakpoints(binding.fileSystem, binding.network);
console.assert(!binding.fileSystem.isDirty() || !binding.network.isDirty());
if (binding.fileSystem.isDirty()) {
this.syncWorkingCopy(binding.fileSystem);
} else if (binding.network.isDirty()) {
this.syncWorkingCopy(binding.network);
} else if (binding.network.hasCommits() && binding.network.content() !== binding.fileSystem.content()) {
binding.network.setWorkingCopy(binding.network.content());
this.syncWorkingCopy(binding.network);
}
this.notifyBindingEvent(binding.network);
this.notifyBindingEvent(binding.fileSystem);
this.dispatchEventToListeners(Events.BindingCreated, binding);
}
private async innerRemoveBinding(binding: PersistenceBinding): Promise<void> {
if (bindings.get(binding.network) !== binding) {
return;
}
console.assert(
bindings.get(binding.network) === bindings.get(binding.fileSystem),
'ERROR: inconsistent binding for networkURL ' + binding.network.url());
bindings.delete(binding.network);
bindings.delete(binding.fileSystem);
binding.network.removeEventListener(
Workspace.UISourceCode.Events.WorkingCopyCommitted, this.onWorkingCopyCommitted, this);
binding.fileSystem.removeEventListener(
Workspace.UISourceCode.Events.WorkingCopyCommitted, this.onWorkingCopyCommitted, this);
binding.network.removeEventListener(
Workspace.UISourceCode.Events.WorkingCopyChanged, this.onWorkingCopyChanged, this);
binding.fileSystem.removeEventListener(
Workspace.UISourceCode.Events.WorkingCopyChanged, this.onWorkingCopyChanged, this);
this.filePathPrefixesToBindingCount.remove(binding.fileSystem.url());
await this.breakpointManager.copyBreakpoints(binding.network, binding.fileSystem);
this.notifyBindingEvent(binding.network);
this.notifyBindingEvent(binding.fileSystem);
this.dispatchEventToListeners(Events.BindingRemoved, binding);
}
private onStatusAdded(status: AutomappingStatus): Promise<void> {
const binding = new PersistenceBinding(status.network, status.fileSystem);
statusBindings.set(status, binding);
return this.innerAddBinding(binding);
}
private async onStatusRemoved(status: AutomappingStatus): Promise<void> {
const binding = statusBindings.get(status) as PersistenceBinding;
await this.innerRemoveBinding(binding);
}
private onWorkingCopyChanged(event: Common.EventTarget.EventTargetEvent<Workspace.UISourceCode.UISourceCode>): void {
const uiSourceCode = event.data;
this.syncWorkingCopy(uiSourceCode);
}
private syncWorkingCopy(uiSourceCode: Workspace.UISourceCode.UISourceCode): void {
const binding = bindings.get(uiSourceCode);
if (!binding || mutedWorkingCopies.has(binding)) {
return;
}
const other = binding.network === uiSourceCode ? binding.fileSystem : binding.network;
if (!uiSourceCode.isDirty()) {
mutedWorkingCopies.add(binding);
other.resetWorkingCopy();
mutedWorkingCopies.delete(binding);
this.contentSyncedForTest();
return;
}
const target = Bindings.NetworkProject.NetworkProject.targetForUISourceCode(binding.network);
if (target && target.type() === SDK.Target.Type.Node) {
const newContent = uiSourceCode.workingCopy();
void other.requestContent().then(() => {
const nodeJSContent = PersistenceImpl.rewrapNodeJSContent(other, other.workingCopy(), newContent);
setWorkingCopy.call(this, () => nodeJSContent);
});
return;
}
setWorkingCopy.call(this, () => uiSourceCode.workingCopy());
function setWorkingCopy(this: PersistenceImpl, workingCopyGetter: () => string): void {
if (binding) {
mutedWorkingCopies.add(binding);
}
other.setWorkingCopyGetter(workingCopyGetter);
if (binding) {
mutedWorkingCopies.delete(binding);
}
this.contentSyncedForTest();
}
}
private onWorkingCopyCommitted(
event: Common.EventTarget.EventTargetEvent<Workspace.UISourceCode.WorkingCopyCommitedEvent>): void {
const uiSourceCode = event.data.uiSourceCode;
const newContent = event.data.content;
this.syncContent(uiSourceCode, newContent, Boolean(event.data.encoded));
}
syncContent(uiSourceCode: Workspace.UISourceCode.UISourceCode, newContent: string, encoded: boolean): void {
const binding = bindings.get(uiSourceCode);
if (!binding || mutedCommits.has(binding)) {
return;
}
const other = binding.network === uiSourceCode ? binding.fileSystem : binding.network;
const target = Bindings.NetworkProject.NetworkProject.targetForUISourceCode(binding.network);
if (target && target.type() === SDK.Target.Type.Node) {
void other.requestContent().then(currentContent => {
const nodeJSContent = PersistenceImpl.rewrapNodeJSContent(other, currentContent.content || '', newContent);
setContent.call(this, nodeJSContent);
});
return;
}
setContent.call(this, newContent);
function setContent(this: PersistenceImpl, newContent: string): void {
if (binding) {
mutedCommits.add(binding);
}
other.setContent(newContent, encoded);
if (binding) {
mutedCommits.delete(binding);
}
this.contentSyncedForTest();
}
}
static rewrapNodeJSContent(
uiSourceCode: Workspace.UISourceCode.UISourceCode, currentContent: string, newContent: string): string {
if (uiSourceCode.project().type() === Workspace.Workspace.projectTypes.FileSystem) {
if (newContent.startsWith(NodePrefix) && newContent.endsWith(NodeSuffix)) {
newContent = newContent.substring(NodePrefix.length, newContent.length - NodeSuffix.length);
}
if (currentContent.startsWith(NodeShebang)) {
newContent = NodeShebang + newContent;
}
} else {
if (newContent.startsWith(NodeShebang)) {
newContent = newContent.substring(NodeShebang.length);
}
if (currentContent.startsWith(NodePrefix) && currentContent.endsWith(NodeSuffix)) {
newContent = NodePrefix + newContent + NodeSuffix;
}
}
return newContent;
}
private contentSyncedForTest(): void {
}
private async moveBreakpoints(from: Workspace.UISourceCode.UISourceCode, to: Workspace.UISourceCode.UISourceCode):
Promise<void> {
const breakpoints = this.breakpointManager.breakpointLocationsForUISourceCode(from).map(
breakpointLocation => breakpointLocation.breakpoint);
await Promise.all(breakpoints.map(async breakpoint => {
await breakpoint.remove(false /* keepInStorage */);
return this.breakpointManager.setBreakpoint(
to, breakpoint.lineNumber(), breakpoint.columnNumber(), breakpoint.condition(), breakpoint.enabled(),
breakpoint.isLogpoint(), BreakpointManager.BreakpointManager.BreakpointOrigin.OTHER);
}));
}
hasUnsavedCommittedChanges(uiSourceCode: Workspace.UISourceCode.UISourceCode): boolean {
if (this.workspace.hasResourceContentTrackingExtensions()) {
return false;
}
if (uiSourceCode.project().canSetFileContent()) {
return false;
}
if (bindings.has(uiSourceCode)) {
return false;
}
return Boolean(uiSourceCode.hasCommits());
}
binding(uiSourceCode: Workspace.UISourceCode.UISourceCode): PersistenceBinding|null {
return bindings.get(uiSourceCode) || null;
}
subscribeForBindingEvent(uiSourceCode: Workspace.UISourceCode.UISourceCode, listener: () => void): void {
this.subscribedBindingEventListeners.set(uiSourceCode, listener);
}
unsubscribeFromBindingEvent(uiSourceCode: Workspace.UISourceCode.UISourceCode, listener: () => void): void {
this.subscribedBindingEventListeners.delete(uiSourceCode, listener);
}
private notifyBindingEvent(uiSourceCode: Workspace.UISourceCode.UISourceCode): void {
if (!this.subscribedBindingEventListeners.has(uiSourceCode)) {
return;
}
const listeners = Array.from(this.subscribedBindingEventListeners.get(uiSourceCode));
for (const listener of listeners) {
listener.call(null);
}
}
fileSystem(uiSourceCode: Workspace.UISourceCode.UISourceCode): Workspace.UISourceCode.UISourceCode|null {
const binding = this.binding(uiSourceCode);
return binding ? binding.fileSystem : null;
}
network(uiSourceCode: Workspace.UISourceCode.UISourceCode): Workspace.UISourceCode.UISourceCode|null {
const binding = this.binding(uiSourceCode);
return binding ? binding.network : null;
}
filePathHasBindings(filePath: Platform.DevToolsPath.UrlString): boolean {
return this.filePathPrefixesToBindingCount.hasBindingPrefix(filePath);
}
}
class FilePathPrefixesBindingCounts {
private prefixCounts: Map<string, number>;
constructor() {
this.prefixCounts = new Map();
}
private getPlatformCanonicalFilePath(path: Platform.DevToolsPath.UrlString): Platform.DevToolsPath.UrlString {
return Host.Platform.isWin() ? Common.ParsedURL.ParsedURL.toLowerCase(path) : path;
}
add(filePath: Platform.DevToolsPath.UrlString): void {
filePath = this.getPlatformCanonicalFilePath(filePath);
let relative = '';
for (const token of filePath.split('/')) {
relative += token + '/';
const count = this.prefixCounts.get(relative) || 0;
this.prefixCounts.set(relative, count + 1);
}
}
remove(filePath: Platform.DevToolsPath.UrlString): void {
filePath = this.getPlatformCanonicalFilePath(filePath);
let relative = '';
for (const token of filePath.split('/')) {
relative += token + '/';
const count = this.prefixCounts.get(relative);
if (count === 1) {
this.prefixCounts.delete(relative);
} else if (count !== undefined) {
this.prefixCounts.set(relative, count - 1);
}
}
}
hasBindingPrefix(filePath: Platform.DevToolsPath.UrlString): boolean {
filePath = this.getPlatformCanonicalFilePath(filePath);
if (!filePath.endsWith('/')) {
filePath = Common.ParsedURL.ParsedURL.concatenate(filePath, '/');
}
return this.prefixCounts.has(filePath);
}
}
const bindings = new WeakMap<Workspace.UISourceCode.UISourceCode, PersistenceBinding>();
const statusBindings = new WeakMap<AutomappingStatus, PersistenceBinding>();
const mutedCommits = new WeakSet<PersistenceBinding>();
const mutedWorkingCopies = new WeakSet<PersistenceBinding>();
export const NodePrefix = '(function (exports, require, module, __filename, __dirname) { ';
export const NodeSuffix = '\n});';
export const NodeShebang = '#!/usr/bin/env node';
// TODO(crbug.com/1167717): Make this a const enum again
// eslint-disable-next-line rulesdir/const_enum
export enum Events {
BindingCreated = 'BindingCreated',
BindingRemoved = 'BindingRemoved',
}
export type EventTypes = {
[Events.BindingCreated]: PersistenceBinding,
[Events.BindingRemoved]: PersistenceBinding,
};
export class PersistenceBinding {
network: Workspace.UISourceCode.UISourceCode;
fileSystem: Workspace.UISourceCode.UISourceCode;
constructor(network: Workspace.UISourceCode.UISourceCode, fileSystem: Workspace.UISourceCode.UISourceCode) {
this.network = network;
this.fileSystem = fileSystem;
}
}