1 |
|
2 |
|
3 |
|
4 |
|
5 | import * as Common from '../../core/common/common.js';
|
6 | import * as Platform from '../../core/platform/platform.js';
|
7 | import * as SDK from '../../core/sdk/sdk.js';
|
8 | import * as Components from '../../ui/legacy/components/utils/utils.js';
|
9 | import * as Bindings from '../bindings/bindings.js';
|
10 | import * as Workspace from '../workspace/workspace.js';
|
11 |
|
12 | import type {AutomappingStatus} from './Automapping.js';
|
13 | import {Automapping} from './Automapping.js';
|
14 | import {LinkDecorator} from './PersistenceUtils.js';
|
15 |
|
16 | let persistenceInstance: PersistenceImpl;
|
17 |
|
18 | export 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 );
|
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 |
|
340 | const bindings = new WeakMap<Workspace.UISourceCode.UISourceCode, PersistenceBinding>();
|
341 | const statusBindings = new WeakMap<AutomappingStatus, PersistenceBinding>();
|
342 |
|
343 | const mutedCommits = new WeakSet<PersistenceBinding>();
|
344 |
|
345 | const mutedWorkingCopies = new WeakSet<PersistenceBinding>();
|
346 |
|
347 | export const NodePrefix = '(function (exports, require, module, __filename, __dirname) { ';
|
348 | export const NodeSuffix = '\n});';
|
349 | export const NodeShebang = '#!/usr/bin/env node';
|
350 |
|
351 |
|
352 |
|
353 | export enum Events {
|
354 | BindingCreated = 'BindingCreated',
|
355 | BindingRemoved = 'BindingRemoved',
|
356 | }
|
357 |
|
358 | export type EventTypes = {
|
359 | [Events.BindingCreated]: PersistenceBinding,
|
360 | [Events.BindingRemoved]: PersistenceBinding,
|
361 | };
|
362 |
|
363 | export 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 |
|
378 | export 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 | }
|