blob: 2ee030eccd4dfda754e667948c8642167d0bf41a [file] [log] [blame]
// 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;