| // 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. |
| |
| /** |
| * @unrestricted |
| */ |
| export default class PersistenceImpl extends Common.Object { |
| /** |
| * @param {!Workspace.Workspace} workspace |
| * @param {!Bindings.BreakpointManager} breakpointManager |
| */ |
| constructor(workspace, breakpointManager) { |
| super(); |
| this._workspace = workspace; |
| this._breakpointManager = breakpointManager; |
| /** @type {!Map<string, number>} */ |
| this._filePathPrefixesToBindingCount = new Map(); |
| |
| /** @type {!Platform.Multimap<!Workspace.UISourceCode, function()>} */ |
| this._subscribedBindingEventListeners = new Platform.Multimap(); |
| |
| const linkDecorator = new Persistence.PersistenceUtils.LinkDecorator(this); |
| Components.Linkifier.setLinkDecorator(linkDecorator); |
| |
| this._mapping = |
| new Persistence.Automapping(this._workspace, this._onStatusAdded.bind(this), this._onStatusRemoved.bind(this)); |
| } |
| |
| /** |
| * @param {function(!Workspace.UISourceCode):boolean} interceptor |
| */ |
| addNetworkInterceptor(interceptor) { |
| this._mapping.addNetworkInterceptor(interceptor); |
| } |
| |
| refreshAutomapping() { |
| this._mapping.scheduleRemap(); |
| } |
| |
| /** |
| * @param {!PersistenceBinding} binding |
| */ |
| addBinding(binding) { |
| this._innerAddBinding(binding); |
| } |
| |
| /** |
| * @param {!PersistenceBinding} binding |
| */ |
| addBindingForTest(binding) { |
| this._innerAddBinding(binding); |
| } |
| |
| /** |
| * @param {!PersistenceBinding} binding |
| */ |
| removeBinding(binding) { |
| this._innerRemoveBinding(binding); |
| } |
| |
| /** |
| * @param {!PersistenceBinding} binding |
| */ |
| removeBindingForTest(binding) { |
| this._innerRemoveBinding(binding); |
| } |
| |
| /** |
| * @param {!PersistenceBinding} binding |
| */ |
| _innerAddBinding(binding) { |
| binding.network[_binding] = binding; |
| binding.fileSystem[_binding] = 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()); |
| |
| 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); |
| } |
| |
| /** |
| * @param {!PersistenceBinding} binding |
| */ |
| _innerRemoveBinding(binding) { |
| if (binding.network[_binding] !== binding) { |
| return; |
| } |
| console.assert( |
| binding.network[_binding] === binding.fileSystem[_binding], |
| 'ERROR: inconsistent binding for networkURL ' + binding.network.url()); |
| |
| binding.network[_binding] = null; |
| binding.fileSystem[_binding] = null; |
| |
| 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()); |
| this._breakpointManager.copyBreakpoints(binding.network.url(), binding.fileSystem); |
| |
| this._notifyBindingEvent(binding.network); |
| this._notifyBindingEvent(binding.fileSystem); |
| this.dispatchEventToListeners(Events.BindingRemoved, binding); |
| } |
| |
| /** |
| * @param {!Persistence.AutomappingStatus} status |
| */ |
| _onStatusAdded(status) { |
| const binding = new PersistenceBinding(status.network, status.fileSystem); |
| status[_binding] = binding; |
| this._innerAddBinding(binding); |
| } |
| |
| /** |
| * @param {!Persistence.AutomappingStatus} status |
| */ |
| _onStatusRemoved(status) { |
| const binding = /** @type {!PersistenceBinding} */ (status[_binding]); |
| this._innerRemoveBinding(binding); |
| } |
| |
| /** |
| * @param {!Common.Event} event |
| */ |
| _onWorkingCopyChanged(event) { |
| const uiSourceCode = /** @type {!Workspace.UISourceCode} */ (event.data); |
| this._syncWorkingCopy(uiSourceCode); |
| } |
| |
| /** |
| * @param {!Workspace.UISourceCode} uiSourceCode |
| */ |
| _syncWorkingCopy(uiSourceCode) { |
| const binding = uiSourceCode[_binding]; |
| if (!binding || binding[_muteWorkingCopy]) { |
| return; |
| } |
| const other = binding.network === uiSourceCode ? binding.fileSystem : binding.network; |
| if (!uiSourceCode.isDirty()) { |
| binding[_muteWorkingCopy] = true; |
| other.resetWorkingCopy(); |
| binding[_muteWorkingCopy] = false; |
| this._contentSyncedForTest(); |
| return; |
| } |
| |
| const target = Bindings.NetworkProject.targetForUISourceCode(binding.network); |
| if (target.type() === SDK.Target.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()); |
| |
| /** |
| * @param {function():string} workingCopyGetter |
| * @this {PersistenceImpl} |
| */ |
| function setWorkingCopy(workingCopyGetter) { |
| binding[_muteWorkingCopy] = true; |
| other.setWorkingCopyGetter(workingCopyGetter); |
| binding[_muteWorkingCopy] = false; |
| this._contentSyncedForTest(); |
| } |
| } |
| |
| /** |
| * @param {!Common.Event} event |
| */ |
| _onWorkingCopyCommitted(event) { |
| const uiSourceCode = /** @type {!Workspace.UISourceCode} */ (event.data.uiSourceCode); |
| const newContent = /** @type {string} */ (event.data.content); |
| this.syncContent(uiSourceCode, newContent, event.data.encoded); |
| } |
| |
| /** |
| * @param {!Workspace.UISourceCode} uiSourceCode |
| * @param {string} newContent |
| * @param {boolean} encoded |
| */ |
| syncContent(uiSourceCode, newContent, encoded) { |
| const binding = uiSourceCode[_binding]; |
| if (!binding || binding[_muteCommit]) { |
| return; |
| } |
| const other = binding.network === uiSourceCode ? binding.fileSystem : binding.network; |
| const target = Bindings.NetworkProject.targetForUISourceCode(binding.network); |
| if (target.type() === SDK.Target.Type.Node) { |
| other.requestContent().then(currentContent => { |
| const nodeJSContent = PersistenceImpl.rewrapNodeJSContent(other, currentContent.content, newContent); |
| setContent.call(this, nodeJSContent); |
| }); |
| return; |
| } |
| setContent.call(this, newContent); |
| |
| /** |
| * @param {string} newContent |
| * @this {PersistenceImpl} |
| */ |
| function setContent(newContent) { |
| binding[_muteCommit] = true; |
| other.setContent(newContent, encoded); |
| binding[_muteCommit] = false; |
| this._contentSyncedForTest(); |
| } |
| } |
| |
| /** |
| * @param {!Workspace.UISourceCode} uiSourceCode |
| * @param {string} currentContent |
| * @param {string} newContent |
| * @return {string} |
| */ |
| static rewrapNodeJSContent(uiSourceCode, currentContent, newContent) { |
| if (uiSourceCode.project().type() === 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() { |
| } |
| |
| /** |
| * @param {!Workspace.UISourceCode} from |
| * @param {!Workspace.UISourceCode} to |
| */ |
| _moveBreakpoints(from, to) { |
| const breakpoints = this._breakpointManager.breakpointLocationsForUISourceCode(from).map( |
| breakpointLocation => breakpointLocation.breakpoint); |
| for (const breakpoint of breakpoints) { |
| breakpoint.remove(false /* keepInStorage */); |
| this._breakpointManager.setBreakpoint( |
| to, breakpoint.lineNumber(), breakpoint.columnNumber(), breakpoint.condition(), breakpoint.enabled()); |
| } |
| } |
| |
| /** |
| * @param {!Workspace.UISourceCode} uiSourceCode |
| * @return {boolean} |
| */ |
| hasUnsavedCommittedChanges(uiSourceCode) { |
| if (this._workspace.hasResourceContentTrackingExtensions()) { |
| return false; |
| } |
| if (uiSourceCode.project().canSetFileContent()) { |
| return false; |
| } |
| if (uiSourceCode[_binding]) { |
| return false; |
| } |
| return !!uiSourceCode.hasCommits(); |
| } |
| |
| /** |
| * @param {!Workspace.UISourceCode} uiSourceCode |
| * @return {?PersistenceBinding} |
| */ |
| binding(uiSourceCode) { |
| return uiSourceCode[_binding] || null; |
| } |
| |
| /** |
| * @param {!Workspace.UISourceCode} uiSourceCode |
| * @param {function()} listener |
| */ |
| subscribeForBindingEvent(uiSourceCode, listener) { |
| this._subscribedBindingEventListeners.set(uiSourceCode, listener); |
| } |
| |
| /** |
| * @param {!Workspace.UISourceCode} uiSourceCode |
| * @param {function()} listener |
| */ |
| unsubscribeFromBindingEvent(uiSourceCode, listener) { |
| this._subscribedBindingEventListeners.delete(uiSourceCode, listener); |
| } |
| |
| /** |
| * @param {!Workspace.UISourceCode} uiSourceCode |
| */ |
| _notifyBindingEvent(uiSourceCode) { |
| if (!this._subscribedBindingEventListeners.has(uiSourceCode)) { |
| return; |
| } |
| const listeners = Array.from(this._subscribedBindingEventListeners.get(uiSourceCode)); |
| for (const listener of listeners) { |
| listener.call(null); |
| } |
| } |
| |
| /** |
| * @param {!Workspace.UISourceCode} uiSourceCode |
| * @return {?Workspace.UISourceCode} |
| */ |
| fileSystem(uiSourceCode) { |
| const binding = this.binding(uiSourceCode); |
| return binding ? binding.fileSystem : null; |
| } |
| |
| /** |
| * @param {!Workspace.UISourceCode} uiSourceCode |
| * @return {?Workspace.UISourceCode} |
| */ |
| network(uiSourceCode) { |
| const binding = this.binding(uiSourceCode); |
| return binding ? binding.network : null; |
| } |
| |
| /** |
| * @param {string} filePath |
| */ |
| _addFilePathBindingPrefixes(filePath) { |
| let relative = ''; |
| for (const token of filePath.split('/')) { |
| relative += token + '/'; |
| const count = this._filePathPrefixesToBindingCount.get(relative) || 0; |
| this._filePathPrefixesToBindingCount.set(relative, count + 1); |
| } |
| } |
| |
| /** |
| * @param {string} filePath |
| */ |
| _removeFilePathBindingPrefixes(filePath) { |
| let relative = ''; |
| for (const token of filePath.split('/')) { |
| relative += token + '/'; |
| const count = this._filePathPrefixesToBindingCount.get(relative); |
| if (count === 1) { |
| this._filePathPrefixesToBindingCount.delete(relative); |
| } else { |
| this._filePathPrefixesToBindingCount.set(relative, count - 1); |
| } |
| } |
| } |
| |
| /** |
| * @param {string} filePath |
| * @return {boolean} |
| */ |
| filePathHasBindings(filePath) { |
| if (!filePath.endsWith('/')) { |
| filePath += '/'; |
| } |
| return this._filePathPrefixesToBindingCount.has(filePath); |
| } |
| } |
| |
| const _binding = Symbol('Persistence.Binding'); |
| const _muteCommit = Symbol('Persistence.MuteCommit'); |
| const _muteWorkingCopy = Symbol('Persistence.MuteWorkingCopy'); |
| const _NodePrefix = '(function (exports, require, module, __filename, __dirname) { '; |
| const _NodeSuffix = '\n});'; |
| const _NodeShebang = '#!/usr/bin/env node'; |
| |
| export const Events = { |
| BindingCreated: Symbol('BindingCreated'), |
| BindingRemoved: Symbol('BindingRemoved') |
| }; |
| |
| /** |
| * @unrestricted |
| */ |
| export class PathEncoder { |
| constructor() { |
| /** @type {!Common.CharacterIdMap<string>} */ |
| this._encoder = new Common.CharacterIdMap(); |
| } |
| |
| /** |
| * @param {string} path |
| * @return {string} |
| */ |
| encode(path) { |
| return path.split('/').map(token => this._encoder.toChar(token)).join(''); |
| } |
| |
| /** |
| * @param {string} path |
| * @return {string} |
| */ |
| decode(path) { |
| return path.split('').map(token => this._encoder.fromChar(token)).join('/'); |
| } |
| } |
| |
| /** |
| * @unrestricted |
| */ |
| export class PersistenceBinding { |
| /** |
| * @param {!Workspace.UISourceCode} network |
| * @param {!Workspace.UISourceCode} fileSystem |
| */ |
| constructor(network, fileSystem) { |
| this.network = network; |
| this.fileSystem = fileSystem; |
| } |
| } |
| |
| /* Legacy exported object */ |
| self.Persistence = self.Persistence || {}; |
| |
| /* Legacy exported object */ |
| Persistence = Persistence || {}; |
| |
| /** @constructor */ |
| Persistence.Persistence = PersistenceImpl; |
| |
| Persistence.Persistence.Events = Events; |
| Persistence.Persistence._NodeShebang = _NodeShebang; |
| Persistence.Persistence._NodePrefix = _NodePrefix; |
| Persistence.Persistence._NodeSuffix = _NodeSuffix; |
| |
| /** @constructor */ |
| Persistence.PathEncoder = PathEncoder; |
| |
| /** @constructor */ |
| Persistence.PersistenceBinding = PersistenceBinding; |
| |
| /** @type {!PersistenceImpl} */ |
| Persistence.persistence; |