UNPKG

14.9 kBPlain TextView Raw
1// Copyright 2016 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import * as Common from '../../core/common/common.js';
6import * as Platform from '../../core/platform/platform.js';
7import * as SDK from '../../core/sdk/sdk.js';
8import * as Components from '../../ui/legacy/components/utils/utils.js';
9import * as Bindings from '../bindings/bindings.js';
10import * as Workspace from '../workspace/workspace.js';
11
12import type {AutomappingStatus} from './Automapping.js';
13import {Automapping} from './Automapping.js';
14import {LinkDecorator} from './PersistenceUtils.js';
15
16let persistenceInstance: PersistenceImpl;
17
18export class PersistenceImpl extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
19 private readonly workspace: Workspace.Workspace.WorkspaceImpl;
20 private readonly breakpointManager: Bindings.BreakpointManager.BreakpointManager;
21 private readonly filePathPrefixesToBindingCount: Map<string, number>;
22 private subscribedBindingEventListeners:
23 Platform.MapUtilities.Multimap<Workspace.UISourceCode.UISourceCode, () => void>;
24 private readonly mapping: Automapping;
25
26 constructor(
27 workspace: Workspace.Workspace.WorkspaceImpl, breakpointManager: Bindings.BreakpointManager.BreakpointManager) {
28 super();
29 this.workspace = workspace;
30 this.breakpointManager = breakpointManager;
31 this.filePathPrefixesToBindingCount = new Map();
32
33 this.subscribedBindingEventListeners = new Platform.MapUtilities.Multimap();
34
35 const linkDecorator = new LinkDecorator(this);
36 Components.Linkifier.Linkifier.setLinkDecorator(linkDecorator);
37
38 this.mapping = new Automapping(this.workspace, this.onStatusAdded.bind(this), this.onStatusRemoved.bind(this));
39 }
40
41 static instance(opts: {
42 forceNew: boolean|null,
43 workspace: Workspace.Workspace.WorkspaceImpl|null,
44 breakpointManager: Bindings.BreakpointManager.BreakpointManager|null,
45 } = {forceNew: null, workspace: null, breakpointManager: null}): PersistenceImpl {
46 const {forceNew, workspace, breakpointManager} = opts;
47 if (!persistenceInstance || forceNew) {
48 if (!workspace || !breakpointManager) {
49 throw new Error('Missing arguments for workspace');
50 }
51 persistenceInstance = new PersistenceImpl(workspace, breakpointManager);
52 }
53
54 return persistenceInstance;
55 }
56
57 addNetworkInterceptor(interceptor: (arg0: Workspace.UISourceCode.UISourceCode) => boolean): void {
58 this.mapping.addNetworkInterceptor(interceptor);
59 }
60
61 refreshAutomapping(): void {
62 this.mapping.scheduleRemap();
63 }
64
65 async addBinding(binding: PersistenceBinding): Promise<void> {
66 await this.innerAddBinding(binding);
67 }
68
69 async addBindingForTest(binding: PersistenceBinding): Promise<void> {
70 await this.innerAddBinding(binding);
71 }
72
73 async removeBinding(binding: PersistenceBinding): Promise<void> {
74 await this.innerRemoveBinding(binding);
75 }
76
77 async removeBindingForTest(binding: PersistenceBinding): Promise<void> {
78 await this.innerRemoveBinding(binding);
79 }
80
81 private async innerAddBinding(binding: PersistenceBinding): Promise<void> {
82 bindings.set(binding.network, binding);
83 bindings.set(binding.fileSystem, binding);
84
85 binding.fileSystem.forceLoadOnCheckContent();
86
87 binding.network.addEventListener(
88 Workspace.UISourceCode.Events.WorkingCopyCommitted, this.onWorkingCopyCommitted, this);
89 binding.fileSystem.addEventListener(
90 Workspace.UISourceCode.Events.WorkingCopyCommitted, this.onWorkingCopyCommitted, this);
91 binding.network.addEventListener(Workspace.UISourceCode.Events.WorkingCopyChanged, this.onWorkingCopyChanged, this);
92 binding.fileSystem.addEventListener(
93 Workspace.UISourceCode.Events.WorkingCopyChanged, this.onWorkingCopyChanged, this);
94
95 this.addFilePathBindingPrefixes(binding.fileSystem.url());
96
97 await this.moveBreakpoints(binding.fileSystem, binding.network);
98
99 console.assert(!binding.fileSystem.isDirty() || !binding.network.isDirty());
100 if (binding.fileSystem.isDirty()) {
101 this.syncWorkingCopy(binding.fileSystem);
102 } else if (binding.network.isDirty()) {
103 this.syncWorkingCopy(binding.network);
104 } else if (binding.network.hasCommits() && binding.network.content() !== binding.fileSystem.content()) {
105 binding.network.setWorkingCopy(binding.network.content());
106 this.syncWorkingCopy(binding.network);
107 }
108
109 this.notifyBindingEvent(binding.network);
110 this.notifyBindingEvent(binding.fileSystem);
111 this.dispatchEventToListeners(Events.BindingCreated, binding);
112 }
113
114 private async innerRemoveBinding(binding: PersistenceBinding): Promise<void> {
115 if (bindings.get(binding.network) !== binding) {
116 return;
117 }
118 console.assert(
119 bindings.get(binding.network) === bindings.get(binding.fileSystem),
120 'ERROR: inconsistent binding for networkURL ' + binding.network.url());
121
122 bindings.delete(binding.network);
123 bindings.delete(binding.fileSystem);
124
125 binding.network.removeEventListener(
126 Workspace.UISourceCode.Events.WorkingCopyCommitted, this.onWorkingCopyCommitted, this);
127 binding.fileSystem.removeEventListener(
128 Workspace.UISourceCode.Events.WorkingCopyCommitted, this.onWorkingCopyCommitted, this);
129 binding.network.removeEventListener(
130 Workspace.UISourceCode.Events.WorkingCopyChanged, this.onWorkingCopyChanged, this);
131 binding.fileSystem.removeEventListener(
132 Workspace.UISourceCode.Events.WorkingCopyChanged, this.onWorkingCopyChanged, this);
133
134 this.removeFilePathBindingPrefixes(binding.fileSystem.url());
135 await this.breakpointManager.copyBreakpoints(binding.network.url(), binding.fileSystem);
136
137 this.notifyBindingEvent(binding.network);
138 this.notifyBindingEvent(binding.fileSystem);
139 this.dispatchEventToListeners(Events.BindingRemoved, binding);
140 }
141
142 private async onStatusAdded(status: AutomappingStatus): Promise<void> {
143 const binding = new PersistenceBinding(status.network, status.fileSystem);
144 statusBindings.set(status, binding);
145 await this.innerAddBinding(binding);
146 }
147
148 private async onStatusRemoved(status: AutomappingStatus): Promise<void> {
149 const binding = statusBindings.get(status) as PersistenceBinding;
150 await this.innerRemoveBinding(binding);
151 }
152
153 private onWorkingCopyChanged(event: Common.EventTarget.EventTargetEvent<Workspace.UISourceCode.UISourceCode>): void {
154 const uiSourceCode = event.data;
155 this.syncWorkingCopy(uiSourceCode);
156 }
157
158 private syncWorkingCopy(uiSourceCode: Workspace.UISourceCode.UISourceCode): void {
159 const binding = bindings.get(uiSourceCode);
160 if (!binding || mutedWorkingCopies.has(binding)) {
161 return;
162 }
163 const other = binding.network === uiSourceCode ? binding.fileSystem : binding.network;
164 if (!uiSourceCode.isDirty()) {
165 mutedWorkingCopies.add(binding);
166 other.resetWorkingCopy();
167 mutedWorkingCopies.delete(binding);
168 this.contentSyncedForTest();
169 return;
170 }
171
172 const target = Bindings.NetworkProject.NetworkProject.targetForUISourceCode(binding.network);
173 if (target && target.type() === SDK.Target.Type.Node) {
174 const newContent = uiSourceCode.workingCopy();
175 other.requestContent().then(() => {
176 const nodeJSContent = PersistenceImpl.rewrapNodeJSContent(other, other.workingCopy(), newContent);
177 setWorkingCopy.call(this, () => nodeJSContent);
178 });
179 return;
180 }
181
182 setWorkingCopy.call(this, () => uiSourceCode.workingCopy());
183
184 function setWorkingCopy(this: PersistenceImpl, workingCopyGetter: () => string): void {
185 if (binding) {
186 mutedWorkingCopies.add(binding);
187 }
188 other.setWorkingCopyGetter(workingCopyGetter);
189 if (binding) {
190 mutedWorkingCopies.delete(binding);
191 }
192 this.contentSyncedForTest();
193 }
194 }
195
196 private onWorkingCopyCommitted(
197 event: Common.EventTarget.EventTargetEvent<Workspace.UISourceCode.WorkingCopyCommitedEvent>): void {
198 const uiSourceCode = event.data.uiSourceCode;
199 const newContent = event.data.content;
200 this.syncContent(uiSourceCode, newContent, Boolean(event.data.encoded));
201 }
202
203 syncContent(uiSourceCode: Workspace.UISourceCode.UISourceCode, newContent: string, encoded: boolean): void {
204 const binding = bindings.get(uiSourceCode);
205 if (!binding || mutedCommits.has(binding)) {
206 return;
207 }
208 const other = binding.network === uiSourceCode ? binding.fileSystem : binding.network;
209 const target = Bindings.NetworkProject.NetworkProject.targetForUISourceCode(binding.network);
210 if (target && target.type() === SDK.Target.Type.Node) {
211 other.requestContent().then(currentContent => {
212 const nodeJSContent = PersistenceImpl.rewrapNodeJSContent(other, currentContent.content || '', newContent);
213 setContent.call(this, nodeJSContent);
214 });
215 return;
216 }
217 setContent.call(this, newContent);
218
219 function setContent(this: PersistenceImpl, newContent: string): void {
220 if (binding) {
221 mutedCommits.add(binding);
222 }
223 other.setContent(newContent, encoded);
224 if (binding) {
225 mutedCommits.delete(binding);
226 }
227 this.contentSyncedForTest();
228 }
229 }
230
231 static rewrapNodeJSContent(
232 uiSourceCode: Workspace.UISourceCode.UISourceCode, currentContent: string, newContent: string): string {
233 if (uiSourceCode.project().type() === Workspace.Workspace.projectTypes.FileSystem) {
234 if (newContent.startsWith(NodePrefix) && newContent.endsWith(NodeSuffix)) {
235 newContent = newContent.substring(NodePrefix.length, newContent.length - NodeSuffix.length);
236 }
237 if (currentContent.startsWith(NodeShebang)) {
238 newContent = NodeShebang + newContent;
239 }
240 } else {
241 if (newContent.startsWith(NodeShebang)) {
242 newContent = newContent.substring(NodeShebang.length);
243 }
244 if (currentContent.startsWith(NodePrefix) && currentContent.endsWith(NodeSuffix)) {
245 newContent = NodePrefix + newContent + NodeSuffix;
246 }
247 }
248 return newContent;
249 }
250
251 private contentSyncedForTest(): void {
252 }
253
254 private async moveBreakpoints(from: Workspace.UISourceCode.UISourceCode, to: Workspace.UISourceCode.UISourceCode):
255 Promise<void> {
256 const breakpoints = this.breakpointManager.breakpointLocationsForUISourceCode(from).map(
257 breakpointLocation => breakpointLocation.breakpoint);
258 await Promise.all(breakpoints.map(breakpoint => {
259 breakpoint.remove(false /* keepInStorage */);
260 return this.breakpointManager.setBreakpoint(
261 to, breakpoint.lineNumber(), breakpoint.columnNumber(), breakpoint.condition(), breakpoint.enabled());
262 }));
263 }
264
265 hasUnsavedCommittedChanges(uiSourceCode: Workspace.UISourceCode.UISourceCode): boolean {
266 if (this.workspace.hasResourceContentTrackingExtensions()) {
267 return false;
268 }
269 if (uiSourceCode.project().canSetFileContent()) {
270 return false;
271 }
272 if (bindings.has(uiSourceCode)) {
273 return false;
274 }
275 return Boolean(uiSourceCode.hasCommits());
276 }
277
278 binding(uiSourceCode: Workspace.UISourceCode.UISourceCode): PersistenceBinding|null {
279 return bindings.get(uiSourceCode) || null;
280 }
281
282 subscribeForBindingEvent(uiSourceCode: Workspace.UISourceCode.UISourceCode, listener: () => void): void {
283 this.subscribedBindingEventListeners.set(uiSourceCode, listener);
284 }
285
286 unsubscribeFromBindingEvent(uiSourceCode: Workspace.UISourceCode.UISourceCode, listener: () => void): void {
287 this.subscribedBindingEventListeners.delete(uiSourceCode, listener);
288 }
289
290 private notifyBindingEvent(uiSourceCode: Workspace.UISourceCode.UISourceCode): void {
291 if (!this.subscribedBindingEventListeners.has(uiSourceCode)) {
292 return;
293 }
294 const listeners = Array.from(this.subscribedBindingEventListeners.get(uiSourceCode));
295 for (const listener of listeners) {
296 listener.call(null);
297 }
298 }
299
300 fileSystem(uiSourceCode: Workspace.UISourceCode.UISourceCode): Workspace.UISourceCode.UISourceCode|null {
301 const binding = this.binding(uiSourceCode);
302 return binding ? binding.fileSystem : null;
303 }
304
305 network(uiSourceCode: Workspace.UISourceCode.UISourceCode): Workspace.UISourceCode.UISourceCode|null {
306 const binding = this.binding(uiSourceCode);
307 return binding ? binding.network : null;
308 }
309
310 private addFilePathBindingPrefixes(filePath: string): void {
311 let relative = '';
312 for (const token of filePath.split('/')) {
313 relative += token + '/';
314 const count = this.filePathPrefixesToBindingCount.get(relative) || 0;
315 this.filePathPrefixesToBindingCount.set(relative, count + 1);
316 }
317 }
318
319 private removeFilePathBindingPrefixes(filePath: string): void {
320 let relative = '';
321 for (const token of filePath.split('/')) {
322 relative += token + '/';
323 const count = this.filePathPrefixesToBindingCount.get(relative);
324 if (count === 1) {
325 this.filePathPrefixesToBindingCount.delete(relative);
326 } else if (count !== undefined) {
327 this.filePathPrefixesToBindingCount.set(relative, count - 1);
328 }
329 }
330 }
331
332 filePathHasBindings(filePath: string): boolean {
333 if (!filePath.endsWith('/')) {
334 filePath += '/';
335 }
336 return this.filePathPrefixesToBindingCount.has(filePath);
337 }
338}
339
340const bindings = new WeakMap<Workspace.UISourceCode.UISourceCode, PersistenceBinding>();
341const statusBindings = new WeakMap<AutomappingStatus, PersistenceBinding>();
342
343const mutedCommits = new WeakSet<PersistenceBinding>();
344
345const mutedWorkingCopies = new WeakSet<PersistenceBinding>();
346
347export const NodePrefix = '(function (exports, require, module, __filename, __dirname) { ';
348export const NodeSuffix = '\n});';
349export const NodeShebang = '#!/usr/bin/env node';
350
351// TODO(crbug.com/1167717): Make this a const enum again
352// eslint-disable-next-line rulesdir/const_enum
353export enum Events {
354 BindingCreated = 'BindingCreated',
355 BindingRemoved = 'BindingRemoved',
356}
357
358export type EventTypes = {
359 [Events.BindingCreated]: PersistenceBinding,
360 [Events.BindingRemoved]: PersistenceBinding,
361};
362
363export class PathEncoder {
364 private readonly encoder: Common.CharacterIdMap.CharacterIdMap<string>;
365 constructor() {
366 this.encoder = new Common.CharacterIdMap.CharacterIdMap();
367 }
368
369 encode(path: string): string {
370 return path.split('/').map(token => this.encoder.toChar(token)).join('');
371 }
372
373 decode(path: string): string {
374 return path.split('').map(token => this.encoder.fromChar(token)).join('/');
375 }
376}
377
378export class PersistenceBinding {
379 network: Workspace.UISourceCode.UISourceCode;
380 fileSystem: Workspace.UISourceCode.UISourceCode;
381 constructor(network: Workspace.UISourceCode.UISourceCode, fileSystem: Workspace.UISourceCode.UISourceCode) {
382 this.network = network;
383 this.fileSystem = fileSystem;
384 }
385}