settingsSync.ts 18.7 KB
Newer Older
1 2 3 4 5 6 7 8 9 10
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import * as objects from 'vs/base/common/objects';
import { Disposable } from 'vs/base/common/lifecycle';
import { IFileService, FileSystemProviderErrorCode, FileSystemProviderError, IFileContent } from 'vs/platform/files/common/files';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
S
Sandeep Somavarapu 已提交
11
import { IRemoteUserDataService, IUserData, RemoteUserDataError, RemoteUserDataErrorCode, ISynchroniser, SyncStatus, SETTINGS_PREVIEW_RESOURCE } from 'vs/workbench/services/userData/common/userData';
12
import { VSBuffer } from 'vs/base/common/buffer';
S
Sandeep Somavarapu 已提交
13
import { parse, findNodeAtLocation, parseTree, ParseError } from 'vs/base/common/json';
14 15 16 17 18 19 20 21 22
import { ITextModel } from 'vs/editor/common/model';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IModeService } from 'vs/editor/common/services/modeService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { localize } from 'vs/nls';
import { setProperty } from 'vs/base/common/jsonEdit';
import { Range } from 'vs/editor/common/core/range';
import { Selection } from 'vs/editor/common/core/selection';
import { EditOperation } from 'vs/editor/common/core/editOperation';
23
import { Emitter, Event } from 'vs/base/common/event';
24
import { ILogService } from 'vs/platform/log/common/log';
25
import { Position } from 'vs/editor/common/core/position';
26
import { IHistoryService } from 'vs/workbench/services/history/common/history';
S
Sandeep Somavarapu 已提交
27
import { CancelablePromise, createCancelablePromise, ThrottledDelayer } from 'vs/base/common/async';
28 29 30 31

interface ISyncPreviewResult {
	readonly fileContent: IFileContent | null;
	readonly remoteUserData: IUserData | null;
32 33
	readonly hasLocalChanged: boolean;
	readonly hasRemoteChanged: boolean;
34
	readonly hasConflicts: boolean;
35 36
}

S
Sandeep Somavarapu 已提交
37
export class SettingsSynchroniser extends Disposable implements ISynchroniser {
38 39 40 41

	private static LAST_SYNC_SETTINGS_STORAGE_KEY: string = 'LAST_SYNC_SETTINGS_CONTENTS';
	private static EXTERNAL_USER_DATA_SETTINGS_KEY: string = 'settings';

42
	private syncPreviewResultPromise: CancelablePromise<ISyncPreviewResult> | null = null;
43

44 45 46 47 48
	private _status: SyncStatus = SyncStatus.Idle;
	get status(): SyncStatus { return this._status; }
	private _onDidChangStatus: Emitter<SyncStatus> = this._register(new Emitter<SyncStatus>());
	readonly onDidChangeStatus: Event<SyncStatus> = this._onDidChangStatus.event;

S
Sandeep Somavarapu 已提交
49 50 51 52
	private readonly throttledDelayer: ThrottledDelayer<void>;
	private _onDidChangeLocal: Emitter<void> = this._register(new Emitter<void>());
	readonly onDidChangeLocal: Event<void> = this._onDidChangeLocal.event;

53 54 55 56 57 58 59
	constructor(
		@IFileService private readonly fileService: IFileService,
		@IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService,
		@IStorageService private readonly storageService: IStorageService,
		@IRemoteUserDataService private readonly remoteUserDataService: IRemoteUserDataService,
		@IModelService private readonly modelService: IModelService,
		@IModeService private readonly modeService: IModeService,
60
		@IEditorService private readonly editorService: IEditorService,
61 62
		@ILogService private readonly logService: ILogService,
		@IHistoryService private readonly historyService: IHistoryService,
63 64
	) {
		super();
S
Sandeep Somavarapu 已提交
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
		this.throttledDelayer = this._register(new ThrottledDelayer<void>(500));
		this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.workbenchEnvironmentService.settingsResource))(() => this.throttledDelayer.trigger(() => this.onDidChangeSettings())));
	}

	private async onDidChangeSettings(): Promise<void> {
		const localFileContent = await this.getLocalFileContent();
		const lastSyncData = this.getLastSyncUserData();
		if (localFileContent && lastSyncData) {
			if (localFileContent.value.toString() !== lastSyncData.content) {
				this._onDidChangeLocal.fire();
				return;
			}
		}
		if (!localFileContent || !lastSyncData) {
			this._onDidChangeLocal.fire();
			return;
		}
82 83
	}

84 85 86 87 88 89 90 91 92 93 94 95 96 97
	private setStatus(status: SyncStatus): void {
		if (this._status !== status) {
			this._status = status;
			this._onDidChangStatus.fire(status);
		}
	}

	async sync(): Promise<boolean> {

		if (this.status !== SyncStatus.Idle) {
			return false;
		}

		this.setStatus(SyncStatus.Syncing);
98

S
Sandeep Somavarapu 已提交
99 100 101 102 103 104
		try {
			const result = await this.getPreview();
			if (result.hasConflicts) {
				this.setStatus(SyncStatus.HasConflicts);
				return false;
			}
105
			await this.apply();
S
Sandeep Somavarapu 已提交
106 107
			return true;
		} catch (e) {
S
Sandeep Somavarapu 已提交
108
			this.syncPreviewResultPromise = null;
S
Sandeep Somavarapu 已提交
109
			this.setStatus(SyncStatus.Idle);
S
Sandeep Somavarapu 已提交
110 111 112 113 114 115 116 117 118 119
			if (e instanceof RemoteUserDataError && e.code === RemoteUserDataErrorCode.Rejected) {
				// Rejected as there is a new remote version. Syncing again,
				this.logService.info('Failed to Synchronise settings as there is a new remote version available. Synchronising again...');
				return this.sync();
			}
			if (e instanceof FileSystemProviderError && e.code === FileSystemProviderErrorCode.FileExists) {
				// Rejected as there is a new local version. Syncing again.
				this.logService.info('Failed to Synchronise settings as there is a new local version available. Synchronising again...');
				return this.sync();
			}
S
Sandeep Somavarapu 已提交
120
			throw e;
121
		}
122
	}
123

S
Sandeep Somavarapu 已提交
124 125 126
	handleConflicts(): boolean {
		if (this.status !== SyncStatus.HasConflicts) {
			return false;
127
		}
S
Sandeep Somavarapu 已提交
128 129 130 131 132 133 134 135 136 137 138 139
		const resourceInput = {
			resource: SETTINGS_PREVIEW_RESOURCE,
			label: localize('Settings Conflicts', "Local ↔ Remote (Settings Conflicts)"),
			options: {
				preserveFocus: false,
				pinned: false,
				revealIfVisible: true,
			},
			mode: 'jsonc'
		};
		this.editorService.openEditor(resourceInput).then(() => this.historyService.remove(resourceInput));
		return true;
140 141
	}

142 143
	async continueSync(): Promise<boolean> {
		if (this.status !== SyncStatus.HasConflicts) {
S
Sandeep Somavarapu 已提交
144 145
			return false;
		}
146 147 148 149 150 151 152 153
		await this.apply();
		return true;
	}

	private async apply(): Promise<void> {
		if (!this.syncPreviewResultPromise) {
			return;
		}
S
Sandeep Somavarapu 已提交
154 155 156 157 158 159 160 161

		if (await this.fileService.exists(SETTINGS_PREVIEW_RESOURCE)) {
			const settingsPreivew = await this.fileService.readFile(SETTINGS_PREVIEW_RESOURCE);
			const content = settingsPreivew.value.toString();
			if (this.hasErrors(content)) {
				return Promise.reject(localize('errorInvalidSettings', "Unable to sync settings. Please resolve conflicts without any errors/warnings and try again."));
			}

S
Sandeep Somavarapu 已提交
162 163
			let { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged } = await this.syncPreviewResultPromise;
			if (hasRemoteChanged) {
S
Sandeep Somavarapu 已提交
164 165 166
				const ref = await this.writeToRemote(content, remoteUserData ? remoteUserData.ref : null);
				remoteUserData = { ref, content };
			}
S
Sandeep Somavarapu 已提交
167 168
			if (hasLocalChanged) {
				await this.writeToLocal(content, fileContent);
S
Sandeep Somavarapu 已提交
169 170 171 172
			}
			if (remoteUserData) {
				this.updateLastSyncValue(remoteUserData);
			}
S
Sandeep Somavarapu 已提交
173 174 175

			// Delete the preview
			await this.fileService.del(SETTINGS_PREVIEW_RESOURCE);
176
		}
S
Sandeep Somavarapu 已提交
177

178 179
		this.syncPreviewResultPromise = null;
		this.setStatus(SyncStatus.Idle);
180 181
	}

S
Sandeep Somavarapu 已提交
182 183 184 185 186 187
	private hasErrors(content: string): boolean {
		const parseErrors: ParseError[] = [];
		parse(content, parseErrors);
		return parseErrors.length > 0;
	}

188 189
	private getPreview(): Promise<ISyncPreviewResult> {
		if (!this.syncPreviewResultPromise) {
190
			this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview());
191 192 193 194 195
		}
		return this.syncPreviewResultPromise;
	}

	private async generatePreview(): Promise<ISyncPreviewResult> {
S
Sandeep Somavarapu 已提交
196
		const remoteUserData = await this.remoteUserDataService.read(SettingsSynchroniser.EXTERNAL_USER_DATA_SETTINGS_KEY);
197
		// Get file content last to get the latest
198
		const fileContent = await this.getLocalFileContent();
S
Sandeep Somavarapu 已提交
199
		const { settingsPreview, hasLocalChanged, hasRemoteChanged, hasConflicts } = this.computeChanges(fileContent, remoteUserData);
200
		if (hasLocalChanged || hasRemoteChanged) {
S
Sandeep Somavarapu 已提交
201
			await this.fileService.writeFile(SETTINGS_PREVIEW_RESOURCE, VSBuffer.fromString(settingsPreview));
202 203 204 205
		}
		return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts };
	}

S
Sandeep Somavarapu 已提交
206
	private computeChanges(fileContent: IFileContent | null, remoteUserData: IUserData | null): { settingsPreview: string, hasLocalChanged: boolean, hasRemoteChanged: boolean, hasConflicts: boolean } {
207

208 209 210
		let hasLocalChanged: boolean = false;
		let hasRemoteChanged: boolean = false;
		let hasConflicts: boolean = false;
211
		let settingsPreview: string = '';
212

213
		// First time sync to remote
214
		if (fileContent && !remoteUserData) {
215 216
			this.logService.trace('Settings Sync: Remote contents does not exist. So sync with settings file.');
			hasRemoteChanged = true;
217 218
			settingsPreview = fileContent.value.toString();
			return { settingsPreview, hasLocalChanged, hasRemoteChanged, hasConflicts };
219 220
		}

221
		// Settings file does not exist, so sync with remote contents.
222
		if (remoteUserData && !fileContent) {
223 224
			this.logService.trace('Settings Sync: Settings file does not exist. So sync with remote contents');
			hasLocalChanged = true;
225 226
			settingsPreview = remoteUserData.content;
			return { settingsPreview, hasLocalChanged, hasRemoteChanged, hasConflicts };
227 228 229 230 231 232
		}

		if (fileContent && remoteUserData) {
			const localContent: string = fileContent.value.toString();
			const remoteContent: string = remoteUserData.content;
			const lastSyncData = this.getLastSyncUserData();
233 234 235 236 237 238 239 240 241 242
			if (!lastSyncData // First time sync
				|| lastSyncData.content !== localContent // Local has moved forwarded
				|| lastSyncData.content !== remoteContent // Remote has moved forwarded
			) {
				this.logService.trace('Settings Sync: Merging remote contents with settings file.');
				const { settingsPreview, hasChanges, hasConflicts } = this.mergeContents(localContent, remoteContent, lastSyncData ? lastSyncData.content : null);
				if (hasChanges) {
					// Sync only if there are changes
					hasLocalChanged = settingsPreview !== localContent; // Local has changed
					hasRemoteChanged = settingsPreview !== remoteContent; // Remote has changed
243
					return { settingsPreview, hasLocalChanged, hasRemoteChanged, hasConflicts };
244 245 246 247
				}
			}
		}

248
		this.logService.trace('Settings Sync: No changes.');
249
		return { settingsPreview, hasLocalChanged, hasRemoteChanged, hasConflicts };
250 251 252

	}

253
	private mergeContents(localContent: string, remoteContent: string, lastSyncedContent: string | null): { settingsPreview: string, hasChanges: boolean; hasConflicts: boolean } {
254 255
		const local = parse(localContent);
		const remote = parse(remoteContent);
256
		const localToRemote = this.compare(local, remote);
257

258 259 260 261 262 263 264
		if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) {
			// No changes found between local and remote.
			return { settingsPreview: localContent, hasChanges: false, hasConflicts: false };
		}

		const settingsPreviewModel = this.modelService.createModel(localContent, this.modeService.create('jsonc'));
		const base = lastSyncedContent ? parse(lastSyncedContent) : null;
265 266
		const baseToLocal = base ? this.compare(base, local) : { added: Object.keys(local).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
		const baseToRemote = base ? this.compare(base, remote) : { added: Object.keys(remote).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
267 268 269 270 271 272 273 274 275 276 277 278 279

		const conflicts: Set<string> = new Set<string>();

		// Removed settings in Local
		for (const key of baseToLocal.removed.keys()) {
			// Got updated in remote
			if (baseToRemote.updated.has(key)) {
				conflicts.add(key);
			}
		}

		// Removed settings in Remote
		for (const key of baseToRemote.removed.keys()) {
S
Sandeep Somavarapu 已提交
280 281 282
			if (conflicts.has(key)) {
				continue;
			}
283 284 285 286
			// Got updated in local
			if (baseToLocal.updated.has(key)) {
				conflicts.add(key);
			} else {
287
				this.editSetting(settingsPreviewModel, key, undefined);
288 289 290 291 292
			}
		}

		// Added settings in Local
		for (const key of baseToLocal.added.keys()) {
S
Sandeep Somavarapu 已提交
293 294 295
			if (conflicts.has(key)) {
				continue;
			}
296 297 298 299 300 301 302 303 304 305 306
			// Got added in remote
			if (baseToRemote.added.has(key)) {
				// Has different value
				if (localToRemote.updated.has(key)) {
					conflicts.add(key);
				}
			}
		}

		// Added settings in remote
		for (const key of baseToRemote.added.keys()) {
S
Sandeep Somavarapu 已提交
307 308 309
			if (conflicts.has(key)) {
				continue;
			}
310 311 312 313 314 315 316
			// Got added in local
			if (baseToLocal.added.has(key)) {
				// Has different value
				if (localToRemote.updated.has(key)) {
					conflicts.add(key);
				}
			} else {
317
				this.editSetting(settingsPreviewModel, key, remote[key]);
318 319 320 321 322
			}
		}

		// Updated settings in Local
		for (const key of baseToLocal.updated.keys()) {
S
Sandeep Somavarapu 已提交
323 324 325
			if (conflicts.has(key)) {
				continue;
			}
326 327 328 329 330 331 332 333 334 335 336
			// Got updated in remote
			if (baseToRemote.updated.has(key)) {
				// Has different value
				if (localToRemote.updated.has(key)) {
					conflicts.add(key);
				}
			}
		}

		// Updated settings in Remote
		for (const key of baseToRemote.updated.keys()) {
S
Sandeep Somavarapu 已提交
337 338 339
			if (conflicts.has(key)) {
				continue;
			}
340 341 342 343 344 345 346
			// Got updated in local
			if (baseToLocal.updated.has(key)) {
				// Has different value
				if (localToRemote.updated.has(key)) {
					conflicts.add(key);
				}
			} else {
347 348 349 350 351 352 353
				this.editSetting(settingsPreviewModel, key, remote[key]);
			}
		}

		for (const key of conflicts.keys()) {
			const tree = parseTree(settingsPreviewModel.getValue());
			const valueNode = findNodeAtLocation(tree, [key]);
S
Sandeep Somavarapu 已提交
354
			const remoteEdit = setProperty(`{${settingsPreviewModel.getEOL()}\t${settingsPreviewModel.getEOL()}}`, [key], remote[key], { tabSize: 4, insertSpaces: false, eol: settingsPreviewModel.getEOL() })[0];
355
			const remoteContent = remoteEdit ? `${remoteEdit.content.substring(remoteEdit.offset + remoteEdit.length + 1)},${settingsPreviewModel.getEOL()}` : '';
356
			if (valueNode) {
S
Sandeep Somavarapu 已提交
357
				// Updated in Local and Remote with different value
358 359 360 361
				const keyPosition = settingsPreviewModel.getPositionAt(valueNode.parent!.offset);
				const valuePosition = settingsPreviewModel.getPositionAt(valueNode.offset + valueNode.length);
				const editOperations = [
					EditOperation.insert(new Position(keyPosition.lineNumber - 1, settingsPreviewModel.getLineMaxColumn(keyPosition.lineNumber - 1)), `${settingsPreviewModel.getEOL()}<<<<<<< local`),
S
Sandeep Somavarapu 已提交
362
					EditOperation.insert(new Position(valuePosition.lineNumber, settingsPreviewModel.getLineMaxColumn(valuePosition.lineNumber)), `${settingsPreviewModel.getEOL()}=======${settingsPreviewModel.getEOL()}${remoteContent}>>>>>>> remote`)
363 364
				];
				settingsPreviewModel.pushEditOperations([new Selection(keyPosition.lineNumber, keyPosition.column, keyPosition.lineNumber, keyPosition.column)], editOperations, () => []);
S
Sandeep Somavarapu 已提交
365 366 367 368
			} else {
				// Removed in Local, but updated in Remote
				const position = new Position(settingsPreviewModel.getLineCount() - 1, settingsPreviewModel.getLineMaxColumn(settingsPreviewModel.getLineCount() - 1));
				const editOperations = [
S
Sandeep Somavarapu 已提交
369
					EditOperation.insert(position, `${settingsPreviewModel.getEOL()}<<<<<<< local${settingsPreviewModel.getEOL()}=======${settingsPreviewModel.getEOL()}${remoteContent}>>>>>>> remote`)
S
Sandeep Somavarapu 已提交
370 371
				];
				settingsPreviewModel.pushEditOperations([new Selection(position.lineNumber, position.column, position.lineNumber, position.column)], editOperations, () => []);
372 373
			}
		}
374

375
		return { settingsPreview: settingsPreviewModel.getValue(), hasChanges: true, hasConflicts: conflicts.size > 0 };
376 377 378 379 380 381 382 383 384 385
	}

	private compare(from: { [key: string]: any }, to: { [key: string]: any }): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
		const fromKeys = Object.keys(from);
		const toKeys = Object.keys(to);
		const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
		const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
		const updated: Set<string> = new Set<string>();

		for (const key of fromKeys) {
386 387 388
			if (removed.has(key)) {
				continue;
			}
389 390 391 392 393 394 395 396 397 398
			const value1 = from[key];
			const value2 = to[key];
			if (!objects.equals(value1, value2)) {
				updated.add(key);
			}
		}

		return { added, removed, updated };
	}

399
	private editSetting(model: ITextModel, key: string, value: any | undefined): void {
400 401
		const insertSpaces = false;
		const tabSize = 4;
402
		const eol = model.getEOL();
403 404 405 406 407 408 409 410 411 412
		const edit = setProperty(model.getValue(), [key], value, { tabSize, insertSpaces, eol })[0];
		if (edit) {
			const startPosition = model.getPositionAt(edit.offset);
			const endPosition = model.getPositionAt(edit.offset + edit.length);
			const range = new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column);
			let currentText = model.getValueInRange(range);
			if (edit.content !== currentText) {
				const editOperation = currentText ? EditOperation.replace(range, edit.content) : EditOperation.insert(startPosition, edit.content);
				model.pushEditOperations([new Selection(startPosition.lineNumber, startPosition.column, startPosition.lineNumber, startPosition.column)], [editOperation], () => []);
			}
413 414 415
		}
	}

S
Sandeep Somavarapu 已提交
416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434
	private getLastSyncUserData(): IUserData | null {
		const lastSyncStorageContents = this.storageService.get(SettingsSynchroniser.LAST_SYNC_SETTINGS_STORAGE_KEY, StorageScope.GLOBAL, undefined);
		if (lastSyncStorageContents) {
			return JSON.parse(lastSyncStorageContents);
		}
		return null;
	}

	private async getLocalFileContent(): Promise<IFileContent | null> {
		try {
			return await this.fileService.readFile(this.workbenchEnvironmentService.settingsResource);
		} catch (error) {
			if (error instanceof FileSystemProviderError && error.code !== FileSystemProviderErrorCode.FileNotFound) {
				return null;
			}
			throw error;
		}
	}

S
Sandeep Somavarapu 已提交
435
	private async writeToRemote(content: string, ref: string | null): Promise<string> {
S
Sandeep Somavarapu 已提交
436
		return this.remoteUserDataService.write(SettingsSynchroniser.EXTERNAL_USER_DATA_SETTINGS_KEY, content, ref);
437 438
	}

439
	private async writeToLocal(newContent: string, oldContent: IFileContent | null): Promise<void> {
440
		if (oldContent) {
S
Sandeep Somavarapu 已提交
441 442
			// file exists already
			await this.fileService.writeFile(this.workbenchEnvironmentService.settingsResource, VSBuffer.fromString(newContent), oldContent);
443
		} else {
S
Sandeep Somavarapu 已提交
444 445
			// file does not exist
			await this.fileService.createFile(this.workbenchEnvironmentService.settingsResource, VSBuffer.fromString(newContent), { overwrite: false });
446
		}
447 448 449
	}

	private updateLastSyncValue(remoteUserData: IUserData): void {
S
Sandeep Somavarapu 已提交
450 451 452 453
		const lastSyncUserData = this.getLastSyncUserData();
		if (lastSyncUserData && lastSyncUserData.ref === remoteUserData.ref) {
			return;
		}
S
Sandeep Somavarapu 已提交
454
		this.storageService.store(SettingsSynchroniser.LAST_SYNC_SETTINGS_STORAGE_KEY, JSON.stringify(remoteUserData), StorageScope.GLOBAL);
455 456 457
	}

}