src/vs/platform/userDataSync/common/extensionsMerge.ts

428 LOC · 384 covered · 44 uncovered · 137 ranges · 689 concepts · 64 introducers · 421 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- extensionsMerge.ts ×8
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { IStringDictionary } from '../../../base/common/collections.js';
7 > import { deepClone, equals } from '../../../base/common/objects.js';
8 > import * as semver from '../../../base/common/semver/semver.js';
9 > import { assertReturnsDefined } from '../../../base/common/types.js';
10 > import { IExtensionIdentifier } from '../../extensions/common/extensions.js';
11 > import { ILocalSyncExtension, IRemoteSyncExtension, ISyncExtension } from './userDataSync.js';
12 >
13 > export interface IMergeResult {
14 > readonly local: { added: ISyncExtension[]; removed: IExtensionIdentifier[]; updated: ISyncExtension[] };
15 > readonly remote: { added: ISyncExtension[]; removed: ISyncExtension[]; updated: ISyncExtension[]; all: ISyncExtension[] } | null;
16 > }
17 >
18 > export function merge(localExtensions: ILocalSyncExtension[], remoteExtensions: IRemoteSyncExtension[] | null, lastSyncExtensions: IRemoteSyncExtension[] | null, skippedExtensions: ISyncExtension[], ignoredExtensions: string[], lastSyncBuiltinExtensions: IExtensionIdentifier[] | null): IMergeResult {
19 > const added: ISyncExtension[] = []; extensionsMerge.ts ×8
20 > const removed: IExtensionIdentifier[] = [];
21 > const updated: ISyncExtension[] = [];
22 >
23 > if (!remoteExtensions) {
24 > const remote = localExtensions.filter(({ identifier }) => ignoredExtensions.every(id => id.toLowerCase() !== identifier.id.toLowerCase())); extensionsMerge.ts ×2
25 > return {
26 > local: {
27 > added,
28 > removed,
29 > updated,
30 > },
31 > remote: remote.length > 0 ? {
32 > added: remote, extensionsMerge.ts ×1
33 > updated: [],
34 > removed: [],
35 > all: remote
36 > } : null extensionsMerge.ts ×2
37 > };
38 > }
40 > localExtensions = localExtensions.map(massageIncomingExtension) as ILocalSyncExtension[];
41 > remoteExtensions = remoteExtensions.map(massageIncomingExtension);
42 > lastSyncExtensions = lastSyncExtensions ? lastSyncExtensions.map(massageIncomingExtension) : null; extensionsMerge.ts ×8
43 >
44 > const uuids: Map<string, string> = new Map<string, string>();
45 > const addUUID = (identifier: IExtensionIdentifier) => { if (identifier.uuid) { uuids.set(identifier.id.toLowerCase(), identifier.uuid); } };
46 > localExtensions.forEach(({ identifier }) => addUUID(identifier));
47 > remoteExtensions.forEach(({ identifier }) => addUUID(identifier));
48 > lastSyncExtensions?.forEach(({ identifier }) => addUUID(identifier));
49 > skippedExtensions?.forEach(({ identifier }) => addUUID(identifier));
50 > lastSyncBuiltinExtensions?.forEach(identifier => addUUID(identifier));
51 >
52 > const getKey = (extension: ISyncExtension): string => {
53 > const uuid = extension.identifier.uuid || uuids.get(extension.identifier.id.toLowerCase()); extensionsMerge.ts ×9
54 > return uuid ? `uuid:${uuid}` : `id:${extension.identifier.id.toLowerCase()}`;
55 > };
56 > const addExtensionToMap = (map: Map<string, ISyncExtension>, extension: ISyncExtension) => { extensionsMerge.ts ×8
57 > map.set(getKey(extension), extension); extensionsMerge.ts ×9
58 > return map;
59 > };
60 > const localExtensionsMap: Map<string, ISyncExtension> = localExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>()); extensionsMerge.ts ×8
61 > const remoteExtensionsMap = remoteExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>());
62 > const newRemoteExtensionsMap = remoteExtensions.reduce((map: Map<string, ISyncExtension>, extension: ISyncExtension) => addExtensionToMap(map, deepClone(extension)), new Map<string, ISyncExtension>());
63 > const lastSyncExtensionsMap = lastSyncExtensions ? lastSyncExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>()) : null;
64 > const skippedExtensionsMap = skippedExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>());
65 > const ignoredExtensionsSet = ignoredExtensions.reduce((set, id) => {
66 > const uuid = uuids.get(id.toLowerCase()); extensionsMerge.ts ×1
67 > return set.add(uuid ? `uuid:${uuid}` : `id:${id.toLowerCase()}`);
68 > }, new Set<string>()); extensionsMerge.ts ×8
69 > const lastSyncBuiltinExtensionsSet = lastSyncBuiltinExtensions ? lastSyncBuiltinExtensions.reduce((set, { id, uuid }) => {
70 > uuid = uuid ?? uuids.get(id.toLowerCase()); extensionsMerge.ts ×1
71 > return set.add(uuid ? `uuid:${uuid}` : `id:${id.toLowerCase()}`);
72 > }, new Set<string>()) : null; extensionsMerge.ts ×8
73 >
74 > const localToRemote = compare(localExtensionsMap, remoteExtensionsMap, ignoredExtensionsSet, false);
75 > if (localToRemote.added.size > 0 || localToRemote.removed.size > 0 || localToRemote.updated.size > 0) {
77 > const baseToLocal = compare(lastSyncExtensionsMap, localExtensionsMap, ignoredExtensionsSet, false);
78 > const baseToRemote = compare(lastSyncExtensionsMap, remoteExtensionsMap, ignoredExtensionsSet, true);
79 >
80 > const merge = (key: string, localExtension: ISyncExtension, remoteExtension: ISyncExtension, preferred: ISyncExtension): ISyncExtension => {
81 > let pinned: boolean | undefined, version: string | undefined, preRelease: boolean | undefined; extensionsMerge.ts ×10
82 > if (localExtension.installed) {
83 > pinned = preferred.pinned; extensionsMerge.ts ×1
84 > preRelease = preferred.preRelease;
85 > if (pinned) {
86 > version = preferred.version; extensionsMerge.ts ×1
87 > }
88 > } else { extensionsMerge.ts ×10
89 > pinned = remoteExtension.pinned; extensionsMerge.ts ×2
90 > preRelease = remoteExtension.preRelease;
91 > if (pinned) {
92 > version = remoteExtension.version; extensionsMerge.ts ×1
93 > }
95 > if (pinned === undefined /* from older client*/) { extensionsMerge.ts ×10
96 pinned = localExtension.pinned;
97 if (pinned) {
98 version = localExtension.version;
99 }
100 }
101 > if (preRelease === undefined /* from older client*/) { extensionsMerge.ts ×10
102 preRelease = localExtension.preRelease;
103 }
104 > return { extensionsMerge.ts ×10
105 > ...preferred,
106 > installed: localExtension.installed || remoteExtension.installed,
107 > pinned,
108 > preRelease,
109 > version: version ?? (remoteExtension.version && (!localExtension.installed || semver.gt(remoteExtension.version, localExtension.version)) ? remoteExtension.version : localExtension.version),
110 > state: mergeExtensionState(localExtension, remoteExtension, lastSyncExtensionsMap?.get(key)),
111 > };
112 > };
114 > // Remotely removed extension => exist in base and does not in remote
115 > for (const key of baseToRemote.removed.values()) {
116 > const localExtension = localExtensionsMap.get(key); extensionsMerge.ts ×3
117 > if (!localExtension) {
118 > continue; extensionsMerge.ts ×2
119 > }
121 > const baseExtension = assertReturnsDefined(lastSyncExtensionsMap?.get(key)); extensionsMerge.ts ×3
122 > const wasAnInstalledExtensionDuringLastSync = lastSyncBuiltinExtensionsSet && !lastSyncBuiltinExtensionsSet.has(key) && baseExtension.installed;
123 > if (localExtension.installed && wasAnInstalledExtensionDuringLastSync /* It is an installed extension now and during last sync */) {
124 > // Installed extension is removed from remote. Remove it from local. extensionsMerge.ts ×1
125 > removed.push(localExtension.identifier);
126 > } else { extensionsMerge.ts ×2
127 > // Add to remote: It is a builtin extenision or got installed after last sync extensionsMerge.ts ×1
128 > newRemoteExtensionsMap.set(key, localExtension);
129 > }
131 > }
133 > // Remotely added extension => does not exist in base and exist in remote
134 > for (const key of baseToRemote.added.values()) {
135 > const remoteExtension = assertReturnsDefined(remoteExtensionsMap.get(key)); extensionsMerge.ts ×3
136 > const localExtension = localExtensionsMap.get(key);
137 >
138 > // Also exist in local
139 > if (localExtension) {
140 > // Is different from local to remote extensionsMerge.ts ×2
141 > if (localToRemote.updated.has(key)) {
142 > const mergedExtension = merge(key, localExtension, remoteExtension, remoteExtension); extensionsMerge.ts ×2
143 > // Update locally only when the extension has changes in properties other than installed poperty
144 > if (!areSame(localExtension, remoteExtension, false, false)) {
145 > updated.push(massageOutgoingExtension(mergedExtension, key)); extensionsMerge.ts ×1
146 > }
147 > newRemoteExtensionsMap.set(key, mergedExtension); extensionsMerge.ts ×2
148 > }
149 > } else { extensionsMerge.ts ×3
150 > // Add only if the extension is an installed extension extensionsMerge.ts ×2
151 > if (remoteExtension.installed) {
152 > added.push(massageOutgoingExtension(remoteExtension, key)); extensionsMerge.ts ×1
153 > }
157 > // Remotely updated extension => exist in base and remote
158 > for (const key of baseToRemote.updated.values()) {
159 > const remoteExtension = assertReturnsDefined(remoteExtensionsMap.get(key)); extensionsMerge.ts ×2
160 > const baseExtension = assertReturnsDefined(lastSyncExtensionsMap?.get(key));
161 > const localExtension = localExtensionsMap.get(key);
162 >
163 > // Also exist in local
164 > if (localExtension) {
165 > const wasAnInstalledExtensionDuringLastSync = lastSyncBuiltinExtensionsSet && !lastSyncBuiltinExtensionsSet.has(key) && baseExtension.installed; extensionsMerge.ts ×3
166 > if (wasAnInstalledExtensionDuringLastSync && localExtension.installed && !remoteExtension.installed) {
167 > // Remove it locally if it is installed locally and not remotely extensionsMerge.ts ×1
168 > removed.push(localExtension.identifier);
169 > } else { extensionsMerge.ts ×3
170 > // Update in local always extensionsMerge.ts ×1
171 > const mergedExtension = merge(key, localExtension, remoteExtension, remoteExtension);
172 > updated.push(massageOutgoingExtension(mergedExtension, key));
173 > newRemoteExtensionsMap.set(key, mergedExtension);
174 > }
176 > // Add it locally if does not exist locally and installed remotely extensionsMerge.ts ×2
177 > else if (remoteExtension.installed) {
178 > added.push(massageOutgoingExtension(remoteExtension, key));
179 > }
181 > }
183 > // Locally added extension => does not exist in base and exist in local
184 > for (const key of baseToLocal.added.values()) {
185 > // If added in remote (already handled) extensionsMerge.ts ×1
186 > if (baseToRemote.added.has(key)) {
187 > continue; extensionsMerge.ts ×2
188 > }
189 > newRemoteExtensionsMap.set(key, assertReturnsDefined(localExtensionsMap.get(key))); extensionsMerge.ts ×1
190 > }
192 > // Locally updated extension => exist in base and local
193 > for (const key of baseToLocal.updated.values()) {
194 > // If removed in remote (already handled) extensionsMerge.ts ×1
195 > if (baseToRemote.removed.has(key)) {
196 > continue; extensionsMerge.ts ×1
197 > }
198 > // If updated in remote (already handled) extensionsMerge.ts ×1
199 > if (baseToRemote.updated.has(key)) {
200 > continue; extensionsMerge.ts ×1
201 > }
202 > const localExtension = assertReturnsDefined(localExtensionsMap.get(key)); extensionsMerge.ts ×1
203 > const remoteExtension = assertReturnsDefined(remoteExtensionsMap.get(key));
204 > // Update remotely
205 > newRemoteExtensionsMap.set(key, merge(key, localExtension, remoteExtension, localExtension));
206 > }
208 > // Locally removed extensions => exist in base and does not exist in local
209 > for (const key of baseToLocal.removed.values()) {
210 > // If updated in remote (already handled) extensionsMerge.ts ×2
211 > if (baseToRemote.updated.has(key)) {
212 > continue; extensionsMerge.ts ×2
213 > }
214 > // If removed in remote (already handled) extensionsMerge.ts ×1
215 > if (baseToRemote.removed.has(key)) {
216 > continue; extensionsMerge.ts ×2
217 > }
218 > // Skipped extensionsMerge.ts ×1
219 > if (skippedExtensionsMap.has(key)) {
220 > continue; extensionsMerge.ts ×1
221 > }
222 > // Skip if it is a builtin extension extensionsMerge.ts ×1
223 > if (!assertReturnsDefined(remoteExtensionsMap.get(key)).installed) {
224 > continue; extensionsMerge.ts ×1
225 > }
226 > // Skip if last sync builtin extensions set is not available extensionsMerge.ts ×2
227 > if (!lastSyncBuiltinExtensionsSet) {
228 continue;
229 }
230 > // Skip if it was a builtin extension during last sync extensionsMerge.ts ×2
231 > if (lastSyncBuiltinExtensionsSet.has(key) || !assertReturnsDefined(lastSyncExtensionsMap?.get(key)).installed) { extensionsMerge.ts ×2
232 > continue; extensionsMerge.ts ×1
233 > }
234 > newRemoteExtensionsMap.delete(key); extensionsMerge.ts ×1
235 > }
238 > const remote: ISyncExtension[] = [];
239 > const remoteChanges = compare(remoteExtensionsMap, newRemoteExtensionsMap, new Set<string>(), true);
240 > const hasRemoteChanges = remoteChanges.added.size > 0 || remoteChanges.updated.size > 0 || remoteChanges.removed.size > 0; extensionsMerge.ts ×8
241 > if (hasRemoteChanges) {
242 > newRemoteExtensionsMap.forEach((value, key) => remote.push(massageOutgoingExtension(value, key))); extensionsMerge.ts ×2
243 > }
245 > return {
246 > local: { added, removed, updated },
247 > remote: hasRemoteChanges ? {
248 > added: [...remoteChanges.added].map(id => newRemoteExtensionsMap.get(id)!), extensionsMerge.ts ×2
249 > updated: [...remoteChanges.updated].map(id => newRemoteExtensionsMap.get(id)!),
250 > removed: [...remoteChanges.removed].map(id => remoteExtensionsMap.get(id)!),
251 > all: remote
252 > } : null extensionsMerge.ts ×8
253 > };
254 > }
256 > function compare(from: Map<string, ISyncExtension> | null, to: Map<string, ISyncExtension>, ignoredExtensions: Set<string>, checkVersionProperty: boolean): { added: Set<string>; removed: Set<string>; updated: Set<string> } { extensionsMerge.ts ×9
257 > const fromKeys = from ? [...from.keys()].filter(key => !ignoredExtensions.has(key)) : [];
258 > const toKeys = [...to.keys()].filter(key => !ignoredExtensions.has(key));
259 > const added = toKeys.filter(key => !fromKeys.includes(key)).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
260 > const removed = fromKeys.filter(key => !toKeys.includes(key)).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
261 > const updated: Set<string> = new Set<string>();
262 >
263 > for (const key of fromKeys) {
264 > if (removed.has(key)) {
265 > continue; extensionsMerge.ts ×1
266 > }
267 > const fromExtension = from!.get(key)!; extensionsMerge.ts ×6
268 > const toExtension = to.get(key);
269 > if (!toExtension || !areSame(fromExtension, toExtension, checkVersionProperty, true)) { extensionsMerge.ts ×9
270 > updated.add(key); extensionsMerge.ts ×1
271 > }
273 >
274 > return { added, removed, updated };
275 > }
277 > function areSame(fromExtension: ISyncExtension, toExtension: ISyncExtension, checkVersionProperty: boolean, checkInstalledProperty: boolean): boolean { extensionsMerge.ts ×6
278 > if (fromExtension.disabled !== toExtension.disabled) {
279 > /* extension enablement changed */ extensionsMerge.ts ×1
280 > return false;
281 > }
283 > if (!!fromExtension.isApplicationScoped !== !!toExtension.isApplicationScoped) {
284 > /* extension application scope has changed */ extensionsMerge.ts ×1
285 > return false;
286 > }
288 > if (checkInstalledProperty && fromExtension.installed !== toExtension.installed) {
289 > /* extension installed property changed */ extensionsMerge.ts ×1
290 > return false;
291 > }
293 > if (fromExtension.installed && toExtension.installed) { extensionsMerge.ts ×6
295 > if (fromExtension.preRelease !== toExtension.preRelease) {
296 > /* installed extension's pre-release version changed */ extensionsMerge.ts ×1
297 > return false;
298 > }
300 > if (fromExtension.pinned !== toExtension.pinned) {
301 > /* installed extension's pinning changed */ extensionsMerge.ts ×1
302 > return false;
303 > }
305 > if (toExtension.pinned && fromExtension.version !== toExtension.version) {
306 > /* installed extension's pinned version changed */ extensionsMerge.ts ×1
307 > return false;
308 > }
311 > if (!isSameExtensionState(fromExtension.state, toExtension.state)) {
312 > /* extension state changed */ extensionsMerge.ts ×1
313 > return false;
314 > }
316 > if ((checkVersionProperty && fromExtension.version !== toExtension.version)) { extensionsMerge.ts ×6
317 /* extension version changed */
318 return false;
319 }
321 > return true;
322 > }
324 > function mergeExtensionState(localExtension: ISyncExtension, remoteExtension: ISyncExtension, lastSyncExtension: ISyncExtension | undefined): IStringDictionary<any> | undefined { extensionsMerge.ts ×10
325 > const localState = localExtension.state;
326 > const remoteState = remoteExtension.state;
327 > const baseState = lastSyncExtension?.state;
328 >
329 > // If remote extension has no version, use local state
330 > if (!remoteExtension.version) {
331 return localState;
332 }
334 > // If local state exists and local extension is latest then use local state
335 > if (localState && semver.gt(localExtension.version, remoteExtension.version)) {
336 return localState;
337 }
338 > // If remote state exists and remote extension is latest, use remote state extensionsMerge.ts ×10
339 > if (remoteState && semver.gt(remoteExtension.version, localExtension.version)) {
340 return remoteState;
341 }
343 >
344 > /* Remote and local are on same version */
345 >
346 > // If local state is not yet set, use remote state
347 > if (!localState) {
348 > return remoteState;
349 > }
350 // If remote state is not yet set, use local state
351 if (!remoteState) {
352 return localState;
353 }
354
355 const mergedState: IStringDictionary<any> = deepClone(localState);
356 > const baseToRemote = baseState ? compareExtensionState(baseState, remoteState) : { added: Object.keys(remoteState).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() }; extensionsMerge.ts ×10
357 > const baseToLocal = baseState ? compareExtensionState(baseState, localState) : { added: Object.keys(localState).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
358 > // Added/Updated in remote
359 > for (const key of [...baseToRemote.added.values(), ...baseToRemote.updated.values()]) {
360 mergedState[key] = remoteState[key];
361 }
362 // Removed in remote
363 for (const key of baseToRemote.removed.values()) {
364 // Not updated in local
365 if (!baseToLocal.updated.has(key)) {
366 delete mergedState[key];
367 }
368 }
369 return mergedState;
370 }
372 > function compareExtensionState(from: IStringDictionary<any>, to: IStringDictionary<any>): { added: Set<string>; removed: Set<string>; updated: Set<string> } { extensionsMerge.ts ×5
373 > const fromKeys = Object.keys(from);
374 > const toKeys = Object.keys(to);
375 > const added = toKeys.filter(key => !fromKeys.includes(key)).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
376 > const removed = fromKeys.filter(key => !toKeys.includes(key)).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
377 > const updated: Set<string> = new Set<string>();
378 >
379 > for (const key of fromKeys) {
380 if (removed.has(key)) {
381 continue;
382 }
383 const value1 = from[key];
384 const value2 = to[key];
385 if (!equals(value1, value2)) {
386 updated.add(key);
387 }
388 }
390 > return { added, removed, updated };
391 > }
393 > function isSameExtensionState(a: IStringDictionary<any> = {}, b: IStringDictionary<any> = {}): boolean { extensionsMerge.ts ×5
394 > const { added, removed, updated } = compareExtensionState(a, b);
395 > return added.size === 0 && removed.size === 0 && updated.size === 0;
396 > }
398 > // massage incoming extension - add optional properties
399 > function massageIncomingExtension(extension: ISyncExtension): ISyncExtension { extensionsMerge.ts ×9
400 > return { ...extension, ...{ disabled: !!extension.disabled, installed: !!extension.installed } };
401 > }
403 > // massage outgoing extension - remove optional properties
404 > function massageOutgoingExtension(extension: ISyncExtension, key: string): ISyncExtension { extensionsMerge.ts ×5
405 > const massagedExtension: ISyncExtension = {
406 > ...extension,
407 > identifier: {
408 > id: extension.identifier.id,
409 > uuid: key.startsWith('uuid:') ? key.substring('uuid:'.length) : undefined
410 > },
411 > /* set following always so that to differentiate with older clients */
412 > preRelease: !!extension.preRelease,
413 > pinned: !!extension.pinned,
414 > };
415 > if (!extension.disabled) {
416 > delete massagedExtension.disabled; extensionsMerge.ts ×1
417 > }
418 > if (!extension.installed) { extensionsMerge.ts ×5
419 > delete massagedExtension.installed; extensionsMerge.ts ×1
420 > }
421 > if (!extension.state) { extensionsMerge.ts ×5
422 > delete massagedExtension.state; extensionsMerge.ts ×1
423 > }
424 > if (!extension.isApplicationScoped) { extensionsMerge.ts ×5
425 > delete massagedExtension.isApplicationScoped; extensionsMerge.ts ×1
426 > }
427 > return massagedExtension; extensionsMerge.ts ×5
428 > }