1 |
|
2 |
|
3 |
|
4 |
|
5 | import * as Common from '../../core/common/common.js';
|
6 | import * as i18n from '../../core/i18n/i18n.js';
|
7 | import * as Platform from '../../core/platform/platform.js';
|
8 | import * as SDK from '../../core/sdk/sdk.js';
|
9 | import * as Bindings from '../bindings/bindings.js';
|
10 | import * as Workspace from '../workspace/workspace.js';
|
11 |
|
12 | import type {FileSystem} from './FileSystemWorkspaceBinding.js';
|
13 | import {FileSystemWorkspaceBinding} from './FileSystemWorkspaceBinding.js';
|
14 | import {PathEncoder, PersistenceImpl} from './PersistenceImpl.js';
|
15 |
|
16 | const UIStrings = {
|
17 | |
18 |
|
19 |
|
20 |
|
21 | theAttemptToBindSInTheWorkspace: 'The attempt to bind "{PH1}" in the workspace failed as this URI is malformed.',
|
22 | };
|
23 | const str_ = i18n.i18n.registerUIStrings('models/persistence/Automapping.ts', UIStrings);
|
24 | const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
|
25 |
|
26 | export class Automapping {
|
27 | private readonly workspace: Workspace.Workspace.WorkspaceImpl;
|
28 | private readonly onStatusAdded: (arg0: AutomappingStatus) => Promise<void>;
|
29 | private readonly onStatusRemoved: (arg0: AutomappingStatus) => Promise<void>;
|
30 | private readonly statuses: Set<AutomappingStatus>;
|
31 | private readonly fileSystemUISourceCodes: Map<string, Workspace.UISourceCode.UISourceCode>;
|
32 | private readonly sweepThrottler: Common.Throttler.Throttler;
|
33 | private readonly sourceCodeToProcessingPromiseMap: WeakMap<Workspace.UISourceCode.UISourceCode, Promise<void>>;
|
34 | private readonly sourceCodeToAutoMappingStatusMap: WeakMap<Workspace.UISourceCode.UISourceCode, AutomappingStatus>;
|
35 | private readonly sourceCodeToMetadataMap:
|
36 | WeakMap<Workspace.UISourceCode.UISourceCode, Workspace.UISourceCode.UISourceCodeMetadata|null>;
|
37 | private readonly filesIndex: FilePathIndex;
|
38 | private readonly projectFoldersIndex: FolderIndex;
|
39 | private readonly activeFoldersIndex: FolderIndex;
|
40 | private readonly interceptors: ((arg0: Workspace.UISourceCode.UISourceCode) => boolean)[];
|
41 | constructor(
|
42 | workspace: Workspace.Workspace.WorkspaceImpl, onStatusAdded: (arg0: AutomappingStatus) => Promise<void>,
|
43 | onStatusRemoved: (arg0: AutomappingStatus) => Promise<void>) {
|
44 | this.workspace = workspace;
|
45 |
|
46 | this.onStatusAdded = onStatusAdded;
|
47 | this.onStatusRemoved = onStatusRemoved;
|
48 | this.statuses = new Set();
|
49 |
|
50 | this.fileSystemUISourceCodes = new Map();
|
51 | this.sweepThrottler = new Common.Throttler.Throttler(100);
|
52 |
|
53 | this.sourceCodeToProcessingPromiseMap = new WeakMap();
|
54 | this.sourceCodeToAutoMappingStatusMap = new WeakMap();
|
55 | this.sourceCodeToMetadataMap = new WeakMap();
|
56 |
|
57 | const pathEncoder = new PathEncoder();
|
58 | this.filesIndex = new FilePathIndex(pathEncoder);
|
59 | this.projectFoldersIndex = new FolderIndex(pathEncoder);
|
60 | this.activeFoldersIndex = new FolderIndex(pathEncoder);
|
61 |
|
62 | this.interceptors = [];
|
63 |
|
64 | this.workspace.addEventListener(
|
65 | Workspace.Workspace.Events.UISourceCodeAdded, event => this.onUISourceCodeAdded(event.data));
|
66 | this.workspace.addEventListener(
|
67 | Workspace.Workspace.Events.UISourceCodeRemoved, event => this.onUISourceCodeRemoved(event.data));
|
68 | this.workspace.addEventListener(Workspace.Workspace.Events.UISourceCodeRenamed, this.onUISourceCodeRenamed, this);
|
69 | this.workspace.addEventListener(
|
70 | Workspace.Workspace.Events.ProjectAdded, event => this.onProjectAdded(event.data), this);
|
71 | this.workspace.addEventListener(
|
72 | Workspace.Workspace.Events.ProjectRemoved, event => this.onProjectRemoved(event.data), this);
|
73 |
|
74 | for (const fileSystem of workspace.projects()) {
|
75 | this.onProjectAdded(fileSystem);
|
76 | }
|
77 | for (const uiSourceCode of workspace.uiSourceCodes()) {
|
78 | this.onUISourceCodeAdded(uiSourceCode);
|
79 | }
|
80 | }
|
81 |
|
82 | addNetworkInterceptor(interceptor: (arg0: Workspace.UISourceCode.UISourceCode) => boolean): void {
|
83 | this.interceptors.push(interceptor);
|
84 | this.scheduleRemap();
|
85 | }
|
86 |
|
87 | scheduleRemap(): void {
|
88 | for (const status of this.statuses.values()) {
|
89 | this.clearNetworkStatus(status.network);
|
90 | }
|
91 | this.scheduleSweep();
|
92 | }
|
93 |
|
94 | private scheduleSweep(): void {
|
95 | this.sweepThrottler.schedule(sweepUnmapped.bind(this));
|
96 |
|
97 | function sweepUnmapped(this: Automapping): Promise<void> {
|
98 | const networkProjects = this.workspace.projectsForType(Workspace.Workspace.projectTypes.Network);
|
99 | for (const networkProject of networkProjects) {
|
100 | for (const uiSourceCode of networkProject.uiSourceCodes()) {
|
101 | this.computeNetworkStatus(uiSourceCode);
|
102 | }
|
103 | }
|
104 | this.onSweepHappenedForTest();
|
105 | return Promise.resolve();
|
106 | }
|
107 | }
|
108 |
|
109 | private onSweepHappenedForTest(): void {
|
110 | }
|
111 |
|
112 | private onProjectRemoved(project: Workspace.Workspace.Project): void {
|
113 | for (const uiSourceCode of project.uiSourceCodes()) {
|
114 | this.onUISourceCodeRemoved(uiSourceCode);
|
115 | }
|
116 | if (project.type() !== Workspace.Workspace.projectTypes.FileSystem) {
|
117 | return;
|
118 | }
|
119 | const fileSystem = project as FileSystem;
|
120 | for (const gitFolder of fileSystem.initialGitFolders()) {
|
121 | this.projectFoldersIndex.removeFolder(gitFolder);
|
122 | }
|
123 | this.projectFoldersIndex.removeFolder(fileSystem.fileSystemPath());
|
124 | this.scheduleRemap();
|
125 | }
|
126 |
|
127 | private onProjectAdded(project: Workspace.Workspace.Project): void {
|
128 | if (project.type() !== Workspace.Workspace.projectTypes.FileSystem) {
|
129 | return;
|
130 | }
|
131 | const fileSystem = project as FileSystem;
|
132 | for (const gitFolder of fileSystem.initialGitFolders()) {
|
133 | this.projectFoldersIndex.addFolder(gitFolder);
|
134 | }
|
135 | this.projectFoldersIndex.addFolder(fileSystem.fileSystemPath());
|
136 | project.uiSourceCodes().forEach(this.onUISourceCodeAdded.bind(this));
|
137 | this.scheduleRemap();
|
138 | }
|
139 |
|
140 | private onUISourceCodeAdded(uiSourceCode: Workspace.UISourceCode.UISourceCode): void {
|
141 | const project = uiSourceCode.project();
|
142 | if (project.type() === Workspace.Workspace.projectTypes.FileSystem) {
|
143 | if (!FileSystemWorkspaceBinding.fileSystemSupportsAutomapping(project)) {
|
144 | return;
|
145 | }
|
146 | this.filesIndex.addPath(uiSourceCode.url());
|
147 | this.fileSystemUISourceCodes.set(uiSourceCode.url(), uiSourceCode);
|
148 | this.scheduleSweep();
|
149 | } else if (project.type() === Workspace.Workspace.projectTypes.Network) {
|
150 | this.computeNetworkStatus(uiSourceCode);
|
151 | }
|
152 | }
|
153 |
|
154 | private onUISourceCodeRemoved(uiSourceCode: Workspace.UISourceCode.UISourceCode): void {
|
155 | if (uiSourceCode.project().type() === Workspace.Workspace.projectTypes.FileSystem) {
|
156 | this.filesIndex.removePath(uiSourceCode.url());
|
157 | this.fileSystemUISourceCodes.delete(uiSourceCode.url());
|
158 | const status = this.sourceCodeToAutoMappingStatusMap.get(uiSourceCode);
|
159 | if (status) {
|
160 | this.clearNetworkStatus(status.network);
|
161 | }
|
162 | } else if (uiSourceCode.project().type() === Workspace.Workspace.projectTypes.Network) {
|
163 | this.clearNetworkStatus(uiSourceCode);
|
164 | }
|
165 | }
|
166 |
|
167 | private onUISourceCodeRenamed(
|
168 | event: Common.EventTarget.EventTargetEvent<Workspace.Workspace.UISourceCodeRenamedEvent>): void {
|
169 | const {uiSourceCode, oldURL} = event.data;
|
170 | if (uiSourceCode.project().type() !== Workspace.Workspace.projectTypes.FileSystem) {
|
171 | return;
|
172 | }
|
173 |
|
174 | this.filesIndex.removePath(oldURL);
|
175 | this.fileSystemUISourceCodes.delete(oldURL);
|
176 | const status = this.sourceCodeToAutoMappingStatusMap.get(uiSourceCode);
|
177 | if (status) {
|
178 | this.clearNetworkStatus(status.network);
|
179 | }
|
180 |
|
181 | this.filesIndex.addPath(uiSourceCode.url());
|
182 | this.fileSystemUISourceCodes.set(uiSourceCode.url(), uiSourceCode);
|
183 | this.scheduleSweep();
|
184 | }
|
185 |
|
186 | private computeNetworkStatus(networkSourceCode: Workspace.UISourceCode.UISourceCode): void {
|
187 | if (this.sourceCodeToProcessingPromiseMap.has(networkSourceCode) ||
|
188 | this.sourceCodeToAutoMappingStatusMap.has(networkSourceCode)) {
|
189 | return;
|
190 | }
|
191 | if (this.interceptors.some(interceptor => interceptor(networkSourceCode))) {
|
192 | return;
|
193 | }
|
194 | if (networkSourceCode.url().startsWith('wasm:
|
195 | return;
|
196 | }
|
197 | const createBindingPromise =
|
198 | this.createBinding(networkSourceCode).then(validateStatus.bind(this)).then(onStatus.bind(this));
|
199 | this.sourceCodeToProcessingPromiseMap.set(networkSourceCode, createBindingPromise);
|
200 |
|
201 | async function validateStatus(this: Automapping, status: AutomappingStatus|null): Promise<AutomappingStatus|null> {
|
202 | if (!status) {
|
203 | return null;
|
204 | }
|
205 | if (this.sourceCodeToProcessingPromiseMap.get(networkSourceCode) !== createBindingPromise) {
|
206 | return null;
|
207 | }
|
208 | if (status.network.contentType().isFromSourceMap() || !status.fileSystem.contentType().isTextType()) {
|
209 | return status;
|
210 | }
|
211 |
|
212 |
|
213 |
|
214 |
|
215 |
|
216 |
|
217 |
|
218 |
|
219 |
|
220 |
|
221 |
|
222 |
|
223 |
|
224 |
|
225 |
|
226 |
|
227 |
|
228 |
|
229 | if (status.fileSystem.isDirty() && (status.network.isDirty() || status.network.hasCommits())) {
|
230 | return null;
|
231 | }
|
232 |
|
233 | const [fileSystemContent, networkContent] = await Promise.all(
|
234 | [status.fileSystem.requestContent(), status.network.project().requestFileContent(status.network)]);
|
235 | if (fileSystemContent.content === null || networkContent === null) {
|
236 | return null;
|
237 | }
|
238 |
|
239 | if (this.sourceCodeToProcessingPromiseMap.get(networkSourceCode) !== createBindingPromise) {
|
240 | return null;
|
241 | }
|
242 |
|
243 | const target = Bindings.NetworkProject.NetworkProject.targetForUISourceCode(status.network);
|
244 | let isValid = false;
|
245 | const fileContent = fileSystemContent.content;
|
246 | if (target && target.type() === SDK.Target.Type.Node) {
|
247 | if (networkContent.content) {
|
248 | const rewrappedNetworkContent =
|
249 | PersistenceImpl.rewrapNodeJSContent(status.fileSystem, fileContent, networkContent.content);
|
250 | isValid = fileContent === rewrappedNetworkContent;
|
251 | }
|
252 | } else {
|
253 | if (networkContent.content) {
|
254 |
|
255 | isValid = fileContent.trimRight() === networkContent.content.trimRight();
|
256 | }
|
257 | }
|
258 | if (!isValid) {
|
259 | this.prevalidationFailedForTest(status);
|
260 | return null;
|
261 | }
|
262 | return status;
|
263 | }
|
264 |
|
265 | function onStatus(this: Automapping, status: AutomappingStatus|null): void {
|
266 | if (this.sourceCodeToProcessingPromiseMap.get(networkSourceCode) !== createBindingPromise) {
|
267 | return;
|
268 | }
|
269 | this.sourceCodeToProcessingPromiseMap.delete(networkSourceCode);
|
270 | if (!status) {
|
271 | this.onBindingFailedForTest();
|
272 | return;
|
273 | }
|
274 |
|
275 | if (this.sourceCodeToAutoMappingStatusMap.has(status.network) ||
|
276 | this.sourceCodeToAutoMappingStatusMap.has(status.fileSystem)) {
|
277 | return;
|
278 | }
|
279 |
|
280 | this.statuses.add(status);
|
281 | this.sourceCodeToAutoMappingStatusMap.set(status.network, status);
|
282 | this.sourceCodeToAutoMappingStatusMap.set(status.fileSystem, status);
|
283 | if (status.exactMatch) {
|
284 | const projectFolder = this.projectFoldersIndex.closestParentFolder(status.fileSystem.url());
|
285 | const newFolderAdded = projectFolder ? this.activeFoldersIndex.addFolder(projectFolder) : false;
|
286 | if (newFolderAdded) {
|
287 | this.scheduleSweep();
|
288 | }
|
289 | }
|
290 | this.onStatusAdded.call(null, status);
|
291 | }
|
292 | }
|
293 |
|
294 | private prevalidationFailedForTest(_binding: AutomappingStatus): void {
|
295 | }
|
296 |
|
297 | private onBindingFailedForTest(): void {
|
298 | }
|
299 |
|
300 | private clearNetworkStatus(networkSourceCode: Workspace.UISourceCode.UISourceCode): void {
|
301 | if (this.sourceCodeToProcessingPromiseMap.has(networkSourceCode)) {
|
302 | this.sourceCodeToProcessingPromiseMap.delete(networkSourceCode);
|
303 | return;
|
304 | }
|
305 | const status = this.sourceCodeToAutoMappingStatusMap.get(networkSourceCode);
|
306 | if (!status) {
|
307 | return;
|
308 | }
|
309 |
|
310 | this.statuses.delete(status);
|
311 | this.sourceCodeToAutoMappingStatusMap.delete(status.network);
|
312 | this.sourceCodeToAutoMappingStatusMap.delete(status.fileSystem);
|
313 | if (status.exactMatch) {
|
314 | const projectFolder = this.projectFoldersIndex.closestParentFolder(status.fileSystem.url());
|
315 | if (projectFolder) {
|
316 | this.activeFoldersIndex.removeFolder(projectFolder);
|
317 | }
|
318 | }
|
319 | this.onStatusRemoved.call(null, status);
|
320 | }
|
321 |
|
322 | private createBinding(networkSourceCode: Workspace.UISourceCode.UISourceCode): Promise<AutomappingStatus|null> {
|
323 | const url = networkSourceCode.url();
|
324 | if (url.startsWith('file:
|
325 | const decodedUrl = sanitizeSourceUrl(url);
|
326 | if (!decodedUrl) {
|
327 | return Promise.resolve(null as AutomappingStatus | null);
|
328 | }
|
329 | const fileSourceCode = this.fileSystemUISourceCodes.get(decodedUrl);
|
330 | const status = fileSourceCode ? new AutomappingStatus(networkSourceCode, fileSourceCode, false) : null;
|
331 | return Promise.resolve(status);
|
332 | }
|
333 |
|
334 | let networkPath = Common.ParsedURL.ParsedURL.extractPath(url);
|
335 | if (networkPath === null) {
|
336 | return Promise.resolve(null as AutomappingStatus | null);
|
337 | }
|
338 |
|
339 | if (networkPath.endsWith('/')) {
|
340 | networkPath += 'index.html';
|
341 | }
|
342 |
|
343 | const urlDecodedNetworkPath = sanitizeSourceUrl(networkPath);
|
344 | if (!urlDecodedNetworkPath) {
|
345 | return Promise.resolve(null as AutomappingStatus | null);
|
346 | }
|
347 |
|
348 | const similarFiles =
|
349 | this.filesIndex.similarFiles(urlDecodedNetworkPath).map(path => this.fileSystemUISourceCodes.get(path)) as
|
350 | Workspace.UISourceCode.UISourceCode[];
|
351 | if (!similarFiles.length) {
|
352 | return Promise.resolve(null as AutomappingStatus | null);
|
353 | }
|
354 |
|
355 | return this.pullMetadatas(similarFiles.concat(networkSourceCode)).then(onMetadatas.bind(this));
|
356 |
|
357 | function sanitizeSourceUrl(url: string): string|null {
|
358 | try {
|
359 | const decodedUrl = decodeURI(url);
|
360 | return decodedUrl;
|
361 | } catch (error) {
|
362 | Common.Console.Console.instance().error(i18nString(UIStrings.theAttemptToBindSInTheWorkspace, {PH1: url}));
|
363 | return null;
|
364 | }
|
365 | }
|
366 |
|
367 | function onMetadatas(this: Automapping): AutomappingStatus|null {
|
368 | const activeFiles =
|
369 | similarFiles.filter(
|
370 | file => Boolean(file) && Boolean(this.activeFoldersIndex.closestParentFolder(file.url()))) as
|
371 | Workspace.UISourceCode.UISourceCode[];
|
372 | const networkMetadata = this.sourceCodeToMetadataMap.get(networkSourceCode);
|
373 | if (!networkMetadata || (!networkMetadata.modificationTime && typeof networkMetadata.contentSize !== 'number')) {
|
374 |
|
375 | if (activeFiles.length !== 1) {
|
376 | return null;
|
377 | }
|
378 | return new AutomappingStatus(networkSourceCode, activeFiles[0], false);
|
379 | }
|
380 |
|
381 |
|
382 | let exactMatches = this.filterWithMetadata(activeFiles, networkMetadata);
|
383 | if (!exactMatches.length) {
|
384 | exactMatches = this.filterWithMetadata(similarFiles, networkMetadata);
|
385 | }
|
386 | if (exactMatches.length !== 1) {
|
387 | return null;
|
388 | }
|
389 | return new AutomappingStatus(networkSourceCode, exactMatches[0], true);
|
390 | }
|
391 | }
|
392 |
|
393 | private async pullMetadatas(uiSourceCodes: Workspace.UISourceCode.UISourceCode[]): Promise<void> {
|
394 | await Promise.all(uiSourceCodes.map(async file => {
|
395 | this.sourceCodeToMetadataMap.set(file, await file.requestMetadata());
|
396 | }));
|
397 | }
|
398 |
|
399 | private filterWithMetadata(
|
400 | files: Workspace.UISourceCode.UISourceCode[],
|
401 | networkMetadata: Workspace.UISourceCode.UISourceCodeMetadata): Workspace.UISourceCode.UISourceCode[] {
|
402 | return files.filter(file => {
|
403 | const fileMetadata = this.sourceCodeToMetadataMap.get(file);
|
404 | if (!fileMetadata) {
|
405 | return false;
|
406 | }
|
407 |
|
408 | const timeMatches = !networkMetadata.modificationTime || !fileMetadata.modificationTime ||
|
409 | Math.abs(networkMetadata.modificationTime.getTime() - fileMetadata.modificationTime.getTime()) < 1000;
|
410 | const contentMatches = !networkMetadata.contentSize || fileMetadata.contentSize === networkMetadata.contentSize;
|
411 | return timeMatches && contentMatches;
|
412 | });
|
413 | }
|
414 | }
|
415 |
|
416 | class FilePathIndex {
|
417 | private readonly encoder: PathEncoder;
|
418 | private readonly reversedIndex: Common.Trie.Trie;
|
419 | constructor(encoder: PathEncoder) {
|
420 | this.encoder = encoder;
|
421 | this.reversedIndex = new Common.Trie.Trie();
|
422 | }
|
423 |
|
424 | addPath(path: string): void {
|
425 | const encodedPath = this.encoder.encode(path);
|
426 | this.reversedIndex.add(Platform.StringUtilities.reverse(encodedPath));
|
427 | }
|
428 |
|
429 | removePath(path: string): void {
|
430 | const encodedPath = this.encoder.encode(path);
|
431 | this.reversedIndex.remove(Platform.StringUtilities.reverse(encodedPath));
|
432 | }
|
433 |
|
434 | similarFiles(networkPath: string): string[] {
|
435 | const encodedPath = this.encoder.encode(networkPath);
|
436 | const reversedEncodedPath = Platform.StringUtilities.reverse(encodedPath);
|
437 | const longestCommonPrefix = this.reversedIndex.longestPrefix(reversedEncodedPath, false);
|
438 | if (!longestCommonPrefix) {
|
439 | return [];
|
440 | }
|
441 | return this.reversedIndex.words(longestCommonPrefix)
|
442 | .map(encodedPath => this.encoder.decode(Platform.StringUtilities.reverse(encodedPath)));
|
443 | }
|
444 | }
|
445 |
|
446 | class FolderIndex {
|
447 | private readonly encoder: PathEncoder;
|
448 | private readonly index: Common.Trie.Trie;
|
449 | private readonly folderCount: Map<string, number>;
|
450 | constructor(encoder: PathEncoder) {
|
451 | this.encoder = encoder;
|
452 | this.index = new Common.Trie.Trie();
|
453 | this.folderCount = new Map();
|
454 | }
|
455 |
|
456 | addFolder(path: string): boolean {
|
457 | if (path.endsWith('/')) {
|
458 | path = path.substring(0, path.length - 1);
|
459 | }
|
460 | const encodedPath = this.encoder.encode(path);
|
461 | this.index.add(encodedPath);
|
462 | const count = this.folderCount.get(encodedPath) || 0;
|
463 | this.folderCount.set(encodedPath, count + 1);
|
464 | return count === 0;
|
465 | }
|
466 |
|
467 | removeFolder(path: string): boolean {
|
468 | if (path.endsWith('/')) {
|
469 | path = path.substring(0, path.length - 1);
|
470 | }
|
471 | const encodedPath = this.encoder.encode(path);
|
472 | const count = this.folderCount.get(encodedPath) || 0;
|
473 | if (!count) {
|
474 | return false;
|
475 | }
|
476 | if (count > 1) {
|
477 | this.folderCount.set(encodedPath, count - 1);
|
478 | return false;
|
479 | }
|
480 | this.index.remove(encodedPath);
|
481 | this.folderCount.delete(encodedPath);
|
482 | return true;
|
483 | }
|
484 |
|
485 | closestParentFolder(path: string): string {
|
486 | const encodedPath = this.encoder.encode(path);
|
487 | const commonPrefix = this.index.longestPrefix(encodedPath, true);
|
488 | return this.encoder.decode(commonPrefix);
|
489 | }
|
490 | }
|
491 |
|
492 | export class AutomappingStatus {
|
493 | network: Workspace.UISourceCode.UISourceCode;
|
494 | fileSystem: Workspace.UISourceCode.UISourceCode;
|
495 | exactMatch: boolean;
|
496 | constructor(
|
497 | network: Workspace.UISourceCode.UISourceCode, fileSystem: Workspace.UISourceCode.UISourceCode,
|
498 | exactMatch: boolean) {
|
499 | this.network = network;
|
500 | this.fileSystem = fileSystem;
|
501 | this.exactMatch = exactMatch;
|
502 | }
|
503 | }
|
504 |
|
\ | No newline at end of file |