UNPKG

chrome-devtools-frontend

Version:
379 lines (317 loc) • 14.6 kB
// 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. /* eslint-disable rulesdir/no_underscored_properties */ import * as Bindings from '../bindings/bindings.js'; import * as Common from '../common/common.js'; import * as Components from '../components/components.js'; import * as Platform from '../platform/platform.js'; import * as SDK from '../sdk/sdk.js'; import * as Workspace from '../workspace/workspace.js'; import {Automapping, AutomappingStatus} from './Automapping.js'; // eslint-disable-line no-unused-vars import {LinkDecorator} from './PersistenceUtils.js'; let persistenceInstance: PersistenceImpl; export class PersistenceImpl extends Common.ObjectWrapper.ObjectWrapper { _workspace: Workspace.Workspace.WorkspaceImpl; _breakpointManager: Bindings.BreakpointManager.BreakpointManager; _filePathPrefixesToBindingCount: Map<string, number>; _subscribedBindingEventListeners: Platform.MapUtilities.Multimap<Workspace.UISourceCode.UISourceCode, () => void>; _mapping: Automapping; constructor( workspace: Workspace.Workspace.WorkspaceImpl, breakpointManager: Bindings.BreakpointManager.BreakpointManager) { super(); this._workspace = workspace; this._breakpointManager = breakpointManager; this._filePathPrefixesToBindingCount = new Map(); 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: Bindings.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); } 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._addFilePathBindingPrefixes(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); } 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._removeFilePathBindingPrefixes(binding.fileSystem.url()); await this._breakpointManager.copyBreakpoints(binding.network.url(), binding.fileSystem); this._notifyBindingEvent(binding.network); this._notifyBindingEvent(binding.fileSystem); this.dispatchEventToListeners(Events.BindingRemoved, binding); } async _onStatusAdded(status: AutomappingStatus): Promise<void> { const binding = new PersistenceBinding(status.network, status.fileSystem); statusBindings.set(status, binding); await this._innerAddBinding(binding); } async _onStatusRemoved(status: AutomappingStatus): Promise<void> { const binding = statusBindings.get(status) as PersistenceBinding; await this._innerRemoveBinding(binding); } _onWorkingCopyChanged(event: Common.EventTarget.EventTargetEvent): void { const uiSourceCode = event.data as Workspace.UISourceCode.UISourceCode; this._syncWorkingCopy(uiSourceCode); } _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.SDKModel.Type.Node) { const newContent = uiSourceCode.workingCopy(); 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(); } } _onWorkingCopyCommitted(event: Common.EventTarget.EventTargetEvent): void { const uiSourceCode = event.data.uiSourceCode as Workspace.UISourceCode.UISourceCode; const newContent = event.data.content as string; this.syncContent(uiSourceCode, newContent, 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.SDKModel.Type.Node) { 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; } _contentSyncedForTest(): void { } 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(breakpoint => { breakpoint.remove(false /* keepInStorage */); return this._breakpointManager.setBreakpoint( to, breakpoint.lineNumber(), breakpoint.columnNumber(), breakpoint.condition(), breakpoint.enabled()); })); } 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); } _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; } _addFilePathBindingPrefixes(filePath: string): void { let relative = ''; for (const token of filePath.split('/')) { relative += token + '/'; const count = this._filePathPrefixesToBindingCount.get(relative) || 0; this._filePathPrefixesToBindingCount.set(relative, count + 1); } } _removeFilePathBindingPrefixes(filePath: string): void { let relative = ''; for (const token of filePath.split('/')) { relative += token + '/'; const count = this._filePathPrefixesToBindingCount.get(relative); if (count === 1) { this._filePathPrefixesToBindingCount.delete(relative); } else if (count !== undefined) { this._filePathPrefixesToBindingCount.set(relative, count - 1); } } } filePathHasBindings(filePath: string): boolean { if (!filePath.endsWith('/')) { filePath += '/'; } return this._filePathPrefixesToBindingCount.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'; export const Events = { BindingCreated: Symbol('BindingCreated'), BindingRemoved: Symbol('BindingRemoved'), }; export class PathEncoder { _encoder: Common.CharacterIdMap.CharacterIdMap<string>; constructor() { this._encoder = new Common.CharacterIdMap.CharacterIdMap(); } encode(path: string): string { return path.split('/').map(token => this._encoder.toChar(token)).join(''); } decode(path: string): string { return path.split('').map(token => this._encoder.fromChar(token)).join('/'); } } 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; } }