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

391 LOC · 297 covered · 94 uncovered · 76 ranges · 585 concepts · 21 introducers · 348 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 > /*--------------------------------------------------------------------------------------------- abstractSynchronizer.ts ×49
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 { isNonEmptyArray } from '../../../base/common/arrays.js';
7 > import { VSBuffer } from '../../../base/common/buffer.js';
8 > import { CancellationToken } from '../../../base/common/cancellation.js';
9 > import { Event } from '../../../base/common/event.js';
10 > import { parse } from '../../../base/common/json.js';
11 > import { OperatingSystem, OS } from '../../../base/common/platform.js';
12 > import { isUndefined } from '../../../base/common/types.js';
13 > import { URI } from '../../../base/common/uri.js';
14 > import { localize } from '../../../nls.js';
15 > import { IConfigurationService } from '../../configuration/common/configuration.js';
16 > import { IEnvironmentService } from '../../environment/common/environment.js';
17 > import { FileOperationError, FileOperationResult, IFileService } from '../../files/common/files.js';
18 > import { ILogService } from '../../log/common/log.js';
19 > import { IStorageService } from '../../storage/common/storage.js';
20 > import { ITelemetryService } from '../../telemetry/common/telemetry.js';
21 > import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js';
22 > import { IUserDataProfile, IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js';
23 > import { AbstractInitializer, AbstractJsonFileSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } from './abstractSynchronizer.js';
24 > import { merge } from './keybindingsMerge.js';
25 > import { Change, IRemoteUserData, IUserDataSyncLocalStoreService, IUserDataSyncConfiguration, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, IUserDataSyncUtilService, SyncResource, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME, CONFIG_SYNC_KEYBINDINGS_PER_PLATFORM } from './userDataSync.js';
26 >
27 > interface ISyncContent {
28 > mac?: string;
29 > linux?: string;
30 > windows?: string;
31 > all?: string;
32 > }
33 >
34 > interface IKeybindingsResourcePreview extends IFileResourcePreview {
35 > previewResult: IMergeResult;
36 > }
37 >
38 > interface ILastSyncUserData extends IRemoteUserData {
39 > platformSpecific?: boolean;
40 > }
41 >
42 > export function getKeybindingsContentFromSyncContent(syncContent: string, platformSpecific: boolean, logService: ILogService): string | null {
44 > const parsed = <ISyncContent>JSON.parse(syncContent);
45 > if (!platformSpecific) {
46 return isUndefined(parsed.all) ? null : parsed.all;
47 }
48 > switch (OS) { keybindingsSync.ts ×5
49 > case OperatingSystem.Macintosh:
50 return isUndefined(parsed.mac) ? null : parsed.mac;
51 > case OperatingSystem.Linux: keybindingsSync.ts ×5
52 > return isUndefined(parsed.linux) ? null : parsed.linux;
53 > case OperatingSystem.Windows:
54 return isUndefined(parsed.windows) ? null : parsed.windows;
56 > } catch (e) {
57 logService.error(e);
58 return null;
59 }
62 > export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implements IUserDataSynchroniser {
63 >
64 > /* Version 2: Change settings from `sync.${setting}` to `settingsSync.{setting}` */
65 > protected readonly version: number = 2;
66 > private readonly previewResource: URI = this.extUri.joinPath(this.syncPreviewFolder, 'keybindings.json');
67 > private readonly baseResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'base' });
68 > private readonly localResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' });
69 > private readonly remoteResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' });
70 > private readonly acceptedResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' });
71 >
72 > constructor(
73 > profile: IUserDataProfile, userDataSyncService.ts ×19
74 > collection: string | undefined,
75 > @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
76 > @IUserDataSyncLocalStoreService userDataSyncLocalStoreService: IUserDataSyncLocalStoreService,
77 > @IUserDataSyncLogService logService: IUserDataSyncLogService,
78 > @IConfigurationService configurationService: IConfigurationService,
79 > @IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService,
80 > @IFileService fileService: IFileService,
81 > @IEnvironmentService environmentService: IEnvironmentService,
82 > @IStorageService storageService: IStorageService,
83 > @IUserDataSyncUtilService userDataSyncUtilService: IUserDataSyncUtilService,
84 > @ITelemetryService telemetryService: ITelemetryService,
85 > @IUriIdentityService uriIdentityService: IUriIdentityService,
86 > ) {
87 > super(profile.keybindingsResource, { syncResource: SyncResource.Keybindings, profile }, collection, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncLocalStoreService, userDataSyncEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService, uriIdentityService);
88 > this._register(Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('settingsSync.keybindingsPerPlatform'))(() => this.triggerLocalChange()));
89 > }
91 > protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, isRemoteDataFromCurrentMachine: boolean, userDataSyncConfiguration: IUserDataSyncConfiguration): Promise<IKeybindingsResourcePreview[]> {
92 > const remoteContent = remoteUserData.syncData ? getKeybindingsContentFromSyncContent(remoteUserData.syncData.content, userDataSyncConfiguration.keybindingsPerPlatform ?? this.syncKeybindingsPerPlatform(), this.logService) : null; keybindingsSync.ts ×7
93 >
94 > // Use remote data as last sync data if last sync data does not exist and remote data is from same machine
95 > lastSyncUserData = lastSyncUserData === null && isRemoteDataFromCurrentMachine ? remoteUserData : lastSyncUserData;
96 > const lastSyncContent: string | null = lastSyncUserData ? this.getKeybindingsContentFromLastSyncUserData(lastSyncUserData) : null;
97 >
98 > // Get file content last to get the latest
99 > const fileContent = await this.getLocalFileContent();
100 > const formattingOptions = await this.getFormattingOptions();
101 >
102 > let mergedContent: string | null = null;
103 > let hasLocalChanged: boolean = false;
104 > let hasRemoteChanged: boolean = false;
105 > let hasConflicts: boolean = false;
106 >
107 > if (remoteContent) {
108 > let localContent: string = fileContent ? fileContent.value.toString() : '[]'; keybindingsSync.ts ×4
109 > localContent = localContent || '[]';
110 > if (this.hasErrors(localContent, true)) {
111 throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync keybindings because the content in the file is not valid. Please open the file and correct it."), UserDataSyncErrorCode.LocalInvalidContent, this.resource);
112 }
114 > if (!lastSyncContent // First time sync
115 > || lastSyncContent !== localContent // Local has forwarded keybindingsSync.ts ×3
116 > || lastSyncContent !== remoteContent // Remote has forwarded keybindingsSync.ts ×1
118 > this.logService.trace(`${this.syncResourceLogLabel}: Merging remote keybindings with local keybindings...`); keybindingsSync.ts ×2
119 > const result = await merge(localContent, remoteContent, lastSyncContent, formattingOptions, this.userDataSyncUtilService);
120 > // Sync only if there are changes
121 > if (result.hasChanges) {
122 > mergedContent = result.mergeContent; keybindingsSync.ts ×1
123 > hasConflicts = result.hasConflicts;
124 > hasLocalChanged = hasConflicts || result.mergeContent !== localContent;
125 > hasRemoteChanged = hasConflicts || result.mergeContent !== remoteContent;
126 > }
130 > // First time syncing to remote
131 > else if (fileContent) {
132 > this.logService.trace(`${this.syncResourceLogLabel}: Remote keybindings does not exist. Synchronizing keybindings for the first time.`); keybindingsSync.ts ×6
133 > mergedContent = fileContent.value.toString();
134 > hasRemoteChanged = true;
135 > }
137 > const previewResult: IMergeResult = {
138 > content: hasConflicts ? lastSyncContent : mergedContent,
139 > localChange: hasLocalChanged ? fileContent ? Change.Modified : Change.Added : Change.None,
140 > remoteChange: hasRemoteChanged ? Change.Modified : Change.None,
141 > hasConflicts
142 > };
143 >
144 > const localContent = fileContent ? fileContent.value.toString() : null;
145 > return [{
146 > fileContent,
147 >
148 > baseResource: this.baseResource,
149 > baseContent: lastSyncContent,
150 >
151 > localResource: this.localResource,
152 > localContent,
153 > localChange: previewResult.localChange,
154 >
155 > remoteResource: this.remoteResource,
156 > remoteContent,
157 > remoteChange: previewResult.remoteChange,
158 >
159 > previewResource: this.previewResource,
160 > previewResult,
161 > acceptedResource: this.acceptedResource,
162 > }];
163 >
164 > }
166 > protected async hasRemoteChanged(lastSyncUserData: IRemoteUserData): Promise<boolean> {
167 > const lastSyncContent = this.getKeybindingsContentFromLastSyncUserData(lastSyncUserData); globalStateSync.ts ×2
168 > if (lastSyncContent === null) {
169 return true;
170 }
172 > const fileContent = await this.getLocalFileContent();
173 > const localContent: string = fileContent ? fileContent.value.toString() : '';
174 > const formattingOptions = await this.getFormattingOptions();
175 > const result = await merge(localContent || '[]', lastSyncContent, lastSyncContent, formattingOptions, this.userDataSyncUtilService);
176 > return result.hasConflicts || result.mergeContent !== lastSyncContent;
177 > }
179 > protected async getMergeResult(resourcePreview: IKeybindingsResourcePreview, token: CancellationToken): Promise<IMergeResult> {
180 > return resourcePreview.previewResult; keybindingsSync.ts ×6
181 > }
183 > protected async getAcceptResult(resourcePreview: IKeybindingsResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise<IAcceptResult> {
185 > /* Accept local resource */
186 > if (this.extUri.isEqual(resource, this.localResource)) {
187 return {
188 content: resourcePreview.fileContent ? resourcePreview.fileContent.value.toString() : null,
189 localChange: Change.None,
190 remoteChange: Change.Modified,
191 };
192 }
194 > /* Accept remote resource */
195 > if (this.extUri.isEqual(resource, this.remoteResource)) {
196 > return { keybindingsSync.ts ×4
197 > content: resourcePreview.remoteContent,
198 > localChange: Change.Modified,
199 > remoteChange: Change.None,
200 > };
201 > }
203 > /* Accept preview resource */
204 > if (this.extUri.isEqual(resource, this.previewResource)) {
205 > if (content === undefined) {
206 > return {
207 > content: resourcePreview.previewResult.content,
208 > localChange: resourcePreview.previewResult.localChange,
209 > remoteChange: resourcePreview.previewResult.remoteChange,
210 > };
211 > } else {
212 return {
213 content,
214 localChange: Change.Modified,
215 remoteChange: Change.Modified,
216 };
217 }
219
220 throw new Error(`Invalid Resource: ${resource.toString()}`);
223 > protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: [IKeybindingsResourcePreview, IAcceptResult][], force: boolean): Promise<void> {
224 > const { fileContent } = resourcePreviews[0][0]; keybindingsSync.ts ×7
225 > let { content, localChange, remoteChange } = resourcePreviews[0][1];
226 >
227 > if (localChange === Change.None && remoteChange === Change.None) {
228 > this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing keybindings.`); keybindingsSync.ts ×1
229 > }
231 > if (content !== null) {
232 > content = content.trim(); keybindingsSync.ts ×6
233 > content = content || '[]';
234 > if (this.hasErrors(content, true)) {
235 > throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync keybindings because the content in the file is not valid. Please open the file and correct it."), UserDataSyncErrorCode.LocalInvalidContent, this.resource); keybindingsSync.ts ×1
236 > }
239 > if (localChange !== Change.None) {
240 > this.logService.trace(`${this.syncResourceLogLabel}: Updating local keybindings...`); keybindingsSync.ts ×2
241 > if (fileContent) {
242 > await this.backupLocal(this.toSyncContent(fileContent.value.toString())); keybindingsSync.ts ×1
243 > }
244 > await this.updateLocalFileContent(content || '[]', fileContent, force); keybindingsSync.ts ×2
245 > this.logService.info(`${this.syncResourceLogLabel}: Updated local keybindings`);
246 > }
248 > if (remoteChange !== Change.None) {
249 > this.logService.trace(`${this.syncResourceLogLabel}: Updating remote keybindings...`); keybindingsSync.ts ×6
250 > const remoteContents = this.toSyncContent(content || '[]', remoteUserData.syncData?.content);
251 > remoteUserData = await this.updateRemoteUserData(remoteContents, force ? null : remoteUserData.ref);
252 > this.logService.info(`${this.syncResourceLogLabel}: Updated remote keybindings`);
253 > }
255 > // Delete the preview
256 > try {
257 > await this.fileService.del(this.previewResource);
258 > } catch (e) { /* ignore */ }
259 >
260 > if (lastSyncUserData?.ref !== remoteUserData.ref) { keybindingsSync.ts ×7
261 > this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized keybindings...`); keybindingsSync.ts ×5
262 > await this.updateLastSyncUserData(remoteUserData, { platformSpecific: this.syncKeybindingsPerPlatform() });
263 > this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized keybindings`);
264 > }
266 > }
268 > async hasLocalData(): Promise<boolean> {
269 try {
270 const localFileContent = await this.getLocalFileContent();
271 if (localFileContent) {
272 const keybindings = parse(localFileContent.value.toString());
273 if (isNonEmptyArray(keybindings)) {
274 return true;
275 }
276 }
277 } catch (error) {
278 if ((<FileOperationError>error).fileOperationResult !== FileOperationResult.FILE_NOT_FOUND) {
279 return true;
280 }
281 }
282 return false;
283 }
285 > async resolveContent(uri: URI): Promise<string | null> {
286 > if (this.extUri.isEqual(this.remoteResource, uri) keybindingsSync.ts ×4
287 || this.extUri.isEqual(this.baseResource, uri)
288 || this.extUri.isEqual(this.localResource, uri)
289 || this.extUri.isEqual(this.acceptedResource, uri)
291 > return this.resolvePreviewContent(uri);
292 > }
293 return null;
296 > private getKeybindingsContentFromLastSyncUserData(lastSyncUserData: ILastSyncUserData): string | null {
297 > if (!lastSyncUserData.syncData) { keybindingsSync.ts ×3
298 > return null; keybindingsSync.ts ×1
299 > }
301 > // Return null if there is a change in platform specific property from last time sync.
302 > if (lastSyncUserData.platformSpecific !== undefined && lastSyncUserData.platformSpecific !== this.syncKeybindingsPerPlatform()) { keybindingsSync.ts ×3
303 return null;
304 }
306 > return getKeybindingsContentFromSyncContent(lastSyncUserData.syncData.content, this.syncKeybindingsPerPlatform(), this.logService);
309 > private toSyncContent(keybindingsContent: string, syncContent?: string): string {
310 > let parsed: ISyncContent = {}; keybindingsSync.ts ×6
311 > try {
312 > parsed = JSON.parse(syncContent || '{}');
313 > } catch (e) {
314 this.logService.error(e);
315 }
316 > if (this.syncKeybindingsPerPlatform()) { keybindingsSync.ts ×6
317 > delete parsed.all;
318 > } else {
319 parsed.all = keybindingsContent;
320 }
321 > switch (OS) { keybindingsSync.ts ×6
322 > case OperatingSystem.Macintosh:
323 parsed.mac = keybindingsContent;
324 break;
325 > case OperatingSystem.Linux: keybindingsSync.ts ×6
326 > parsed.linux = keybindingsContent;
327 > break;
328 > case OperatingSystem.Windows:
329 parsed.windows = keybindingsContent;
330 break;
332 > return JSON.stringify(parsed);
333 > }
335 > private syncKeybindingsPerPlatform(): boolean {
336 > return !!this.configurationService.getValue(CONFIG_SYNC_KEYBINDINGS_PER_PLATFORM); keybindingsSync.ts ×5
337 > }
339 > }
340 >
341 > export class KeybindingsInitializer extends AbstractInitializer {
342 >
343 > constructor(
344 @IFileService fileService: IFileService,
345 @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService,
346 @IEnvironmentService environmentService: IEnvironmentService,
347 @IUserDataSyncLogService logService: IUserDataSyncLogService,
348 @IStorageService storageService: IStorageService,
349 @IUriIdentityService uriIdentityService: IUriIdentityService,
350 ) {
351 super(SyncResource.Keybindings, userDataProfilesService, environmentService, logService, fileService, storageService, uriIdentityService);
352 }
354 > protected async doInitialize(remoteUserData: IRemoteUserData): Promise<void> {
355 const keybindingsContent = remoteUserData.syncData ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null;
356 if (!keybindingsContent) {
357 this.logService.info('Skipping initializing keybindings because remote keybindings does not exist.');
358 return;
359 }
360
361 const isEmpty = await this.isEmpty();
362 if (!isEmpty) {
363 this.logService.info('Skipping initializing keybindings because local keybindings exist.');
364 return;
365 }
366
367 await this.fileService.writeFile(this.userDataProfilesService.defaultProfile.keybindingsResource, VSBuffer.fromString(keybindingsContent));
368
369 await this.updateLastSyncUserData(remoteUserData);
370 }
372 > private async isEmpty(): Promise<boolean> {
373 try {
374 const fileContent = await this.fileService.readFile(this.userDataProfilesService.defaultProfile.settingsResource);
375 const keybindings = parse(fileContent.value.toString());
376 return !isNonEmptyArray(keybindings);
377 } catch (error) {
378 return (<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND;
379 }
380 }
382 > private getKeybindingsContentFromSyncContent(syncContent: string): string | null {
383 try {
384 return getKeybindingsContentFromSyncContent(syncContent, true, this.logService);
385 } catch (e) {
386 this.logService.error(e);
387 return null;
388 }
389 }
391 > }