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';
27
import { CancelablePromise, createCancelablePromise } 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;

49 50 51 52 53 54 55
	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,
56
		@IEditorService private readonly editorService: IEditorService,
57 58
		@ILogService private readonly logService: ILogService,
		@IHistoryService private readonly historyService: IHistoryService,
59 60 61 62
	) {
		super();
	}

63 64 65 66 67 68 69 70 71 72 73 74 75 76
	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);
77

S
Sandeep Somavarapu 已提交
78 79 80 81 82 83
		try {
			const result = await this.getPreview();
			if (result.hasConflicts) {
				this.setStatus(SyncStatus.HasConflicts);
				return false;
			}
84
			await this.apply();
S
Sandeep Somavarapu 已提交
85 86
			return true;
		} catch (e) {
S
Sandeep Somavarapu 已提交
87
			this.syncPreviewResultPromise = null;
S
Sandeep Somavarapu 已提交
88
			this.setStatus(SyncStatus.Idle);
S
Sandeep Somavarapu 已提交
89 90 91 92 93 94 95 96 97 98
			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 已提交
99
			throw e;
100
		}
101
	}
102

103 104 105 106 107 108 109 110 111
	async stopSync(): Promise<void> {
		await this.fileService.del(SETTINGS_PREVIEW_RESOURCE);
		if (this.syncPreviewResultPromise) {
			this.syncPreviewResultPromise.cancel();
			this.syncPreviewResultPromise = null;
			this.setStatus(SyncStatus.Idle);
		}
	}

S
Sandeep Somavarapu 已提交
112 113 114
	handleConflicts(): boolean {
		if (this.status !== SyncStatus.HasConflicts) {
			return false;
115
		}
S
Sandeep Somavarapu 已提交
116 117 118 119 120 121 122 123 124 125 126 127
		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;
128 129
	}

130 131
	async continueSync(): Promise<boolean> {
		if (this.status !== SyncStatus.HasConflicts) {
S
Sandeep Somavarapu 已提交
132 133
			return false;
		}
134 135 136 137 138 139 140 141 142 143 144 145 146
		await this.apply();
		return true;
	}

	private async apply(): Promise<void> {
		if (!this.syncPreviewResultPromise) {
			return;
		}
		const result = await this.syncPreviewResultPromise;
		let remoteUserData = result.remoteUserData;
		const settingsPreivew = await this.fileService.readFile(SETTINGS_PREVIEW_RESOURCE);
		const content = settingsPreivew.value.toString();

S
Sandeep Somavarapu 已提交
147
		if (this.hasErrors(content)) {
148 149 150 151 152 153 154 155 156 157 158
			return Promise.reject(localize('errorInvalidSettings', "Unable to sync settings. Please resolve conflicts without any errors/warnings and try again."));
		}
		if (result.hasRemoteChanged) {
			const ref = await this.writeToRemote(content, remoteUserData ? remoteUserData.ref : null);
			remoteUserData = { ref, content };
		}
		if (result.hasLocalChanged) {
			await this.writeToLocal(content, result.fileContent);
		}
		if (remoteUserData) {
			this.updateLastSyncValue(remoteUserData);
159
		}
160 161
		this.syncPreviewResultPromise = null;
		this.setStatus(SyncStatus.Idle);
162 163
	}

S
Sandeep Somavarapu 已提交
164 165 166 167 168 169
	private hasErrors(content: string): boolean {
		const parseErrors: ParseError[] = [];
		parse(content, parseErrors);
		return parseErrors.length > 0;
	}

170 171
	private getPreview(): Promise<ISyncPreviewResult> {
		if (!this.syncPreviewResultPromise) {
172
			this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview());
173 174 175 176 177
		}
		return this.syncPreviewResultPromise;
	}

	private async generatePreview(): Promise<ISyncPreviewResult> {
S
Sandeep Somavarapu 已提交
178
		const remoteUserData = await this.remoteUserDataService.read(SettingsSynchroniser.EXTERNAL_USER_DATA_SETTINGS_KEY);
179
		// Get file content last to get the latest
180
		const fileContent = await this.getLocalFileContent();
181 182
		const { settingsPreview, hasLocalChanged, hasRemoteChanged, hasConflicts } = await this.computeChanges(fileContent, remoteUserData);
		if (hasLocalChanged || hasRemoteChanged) {
S
Sandeep Somavarapu 已提交
183
			await this.fileService.writeFile(SETTINGS_PREVIEW_RESOURCE, VSBuffer.fromString(settingsPreview));
184 185 186 187 188
		}
		return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts };
	}

	private async computeChanges(fileContent: IFileContent | null, remoteUserData: IUserData | null): Promise<{ settingsPreview: string, hasLocalChanged: boolean, hasRemoteChanged: boolean, hasConflicts: boolean }> {
189

190 191 192
		let hasLocalChanged: boolean = false;
		let hasRemoteChanged: boolean = false;
		let hasConflicts: boolean = false;
193
		let settingsPreview: string = '';
194

195
		// First time sync to remote
196
		if (fileContent && !remoteUserData) {
197 198
			this.logService.trace('Settings Sync: Remote contents does not exist. So sync with settings file.');
			hasRemoteChanged = true;
199 200
			settingsPreview = fileContent.value.toString();
			return { settingsPreview, hasLocalChanged, hasRemoteChanged, hasConflicts };
201 202
		}

203
		// Settings file does not exist, so sync with remote contents.
204
		if (remoteUserData && !fileContent) {
205 206
			this.logService.trace('Settings Sync: Settings file does not exist. So sync with remote contents');
			hasLocalChanged = true;
207 208
			settingsPreview = remoteUserData.content;
			return { settingsPreview, hasLocalChanged, hasRemoteChanged, hasConflicts };
209 210 211 212 213 214 215 216 217 218
		}

		if (fileContent && remoteUserData) {

			const localContent: string = fileContent.value.toString();
			const remoteContent: string = remoteUserData.content;
			const lastSyncData = this.getLastSyncUserData();

			// Already in Sync.
			if (localContent === remoteUserData.content) {
219
				this.logService.trace('Settings Sync: Settings file and remote contents are in sync.');
220 221
				settingsPreview = localContent;
				return { settingsPreview, hasLocalChanged, hasRemoteChanged, hasConflicts };
222 223
			}

224
			// First time Sync to Local
225
			if (!lastSyncData) {
226 227
				this.logService.trace('Settings Sync: Syncing remote contents with settings file for the first time.');
				hasLocalChanged = hasRemoteChanged = true;
228 229
				const mergeResult = await this.mergeContents(localContent, remoteContent, null);
				return { settingsPreview: mergeResult.settingsPreview, hasLocalChanged, hasRemoteChanged, hasConflicts: mergeResult.hasConflicts };
230 231
			}

232
			// Remote has moved forward
S
Sandeep Somavarapu 已提交
233
			if (remoteUserData.ref !== lastSyncData.ref) {
234
				this.logService.trace('Settings Sync: Remote contents have changed. Merge and Sync.');
235 236
				hasLocalChanged = true;
				hasRemoteChanged = lastSyncData.content !== localContent;
237 238
				const mergeResult = await this.mergeContents(localContent, remoteContent, lastSyncData.content);
				return { settingsPreview: mergeResult.settingsPreview, hasLocalChanged, hasRemoteChanged, hasConflicts: mergeResult.hasConflicts };
239 240 241
			}

			// Remote data is same as last synced data
S
Sandeep Somavarapu 已提交
242
			if (lastSyncData.ref === remoteUserData.ref) {
243 244 245

				// Local contents are same as last synced data. No op.
				if (lastSyncData.content === localContent) {
246
					this.logService.trace('Settings Sync: Settings file and remote contents have not changed. So no sync needed.');
247
					return { settingsPreview, hasLocalChanged, hasRemoteChanged, hasConflicts };
248 249 250
				}

				// New local contents. Sync with Local.
251 252
				this.logService.trace('Settings Sync: Remote contents have not changed. Settings file has changed. So sync with settings file.');
				hasRemoteChanged = true;
253 254
				settingsPreview = localContent;
				return { settingsPreview, hasLocalChanged, hasRemoteChanged, hasConflicts };
255 256 257 258
			}

		}

259
		return { settingsPreview, hasLocalChanged, hasRemoteChanged, hasConflicts };
260 261 262 263

	}

	private getLastSyncUserData(): IUserData | null {
S
Sandeep Somavarapu 已提交
264
		const lastSyncStorageContents = this.storageService.get(SettingsSynchroniser.LAST_SYNC_SETTINGS_STORAGE_KEY, StorageScope.GLOBAL, undefined);
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
		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;
		}
	}

282
	private async mergeContents(localContent: string, remoteContent: string, lastSyncedContent: string | null): Promise<{ settingsPreview: string, hasConflicts: boolean }> {
283 284
		const local = parse(localContent);
		const remote = parse(remoteContent);
285
		const base = lastSyncedContent ? parse(lastSyncedContent) : null;
286
		const settingsPreviewModel = this.modelService.createModel(localContent, this.modeService.create('jsonc'));
287

288 289
		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>() };
290 291 292 293 294 295 296 297 298 299 300 301 302 303
		const localToRemote = this.compare(local, remote);

		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 已提交
304 305 306
			if (conflicts.has(key)) {
				continue;
			}
307 308 309 310
			// Got updated in local
			if (baseToLocal.updated.has(key)) {
				conflicts.add(key);
			} else {
311
				this.editSetting(settingsPreviewModel, key, undefined);
312 313 314 315 316
			}
		}

		// Added settings in Local
		for (const key of baseToLocal.added.keys()) {
S
Sandeep Somavarapu 已提交
317 318 319
			if (conflicts.has(key)) {
				continue;
			}
320 321 322 323 324 325 326 327 328 329 330
			// 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 已提交
331 332 333
			if (conflicts.has(key)) {
				continue;
			}
334 335 336 337 338 339 340
			// Got added in local
			if (baseToLocal.added.has(key)) {
				// Has different value
				if (localToRemote.updated.has(key)) {
					conflicts.add(key);
				}
			} else {
341
				this.editSetting(settingsPreviewModel, key, remote[key]);
342 343 344 345 346
			}
		}

		// Updated settings in Local
		for (const key of baseToLocal.updated.keys()) {
S
Sandeep Somavarapu 已提交
347 348 349
			if (conflicts.has(key)) {
				continue;
			}
350 351 352 353 354 355 356 357 358 359 360
			// 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 已提交
361 362 363
			if (conflicts.has(key)) {
				continue;
			}
364 365 366 367 368 369 370
			// Got updated in local
			if (baseToLocal.updated.has(key)) {
				// Has different value
				if (localToRemote.updated.has(key)) {
					conflicts.add(key);
				}
			} else {
371 372 373 374 375 376 377
				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 已提交
378
			const remoteEdit = setProperty(`{${settingsPreviewModel.getEOL()}\t${settingsPreviewModel.getEOL()}}`, [key], remote[key], { tabSize: 4, insertSpaces: false, eol: settingsPreviewModel.getEOL() })[0];
379
			const remoteContent = remoteEdit ? `${remoteEdit.content.substring(remoteEdit.offset + remoteEdit.length + 1)},${settingsPreviewModel.getEOL()}` : '';
380
			if (valueNode) {
S
Sandeep Somavarapu 已提交
381
				// Updated in Local and Remote with different value
382 383 384 385
				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 已提交
386
					EditOperation.insert(new Position(valuePosition.lineNumber, settingsPreviewModel.getLineMaxColumn(valuePosition.lineNumber)), `${settingsPreviewModel.getEOL()}=======${settingsPreviewModel.getEOL()}${remoteContent}>>>>>>> remote`)
387 388
				];
				settingsPreviewModel.pushEditOperations([new Selection(keyPosition.lineNumber, keyPosition.column, keyPosition.lineNumber, keyPosition.column)], editOperations, () => []);
S
Sandeep Somavarapu 已提交
389 390 391 392
			} 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 已提交
393
					EditOperation.insert(position, `${settingsPreviewModel.getEOL()}<<<<<<< local${settingsPreviewModel.getEOL()}=======${settingsPreviewModel.getEOL()}${remoteContent}>>>>>>> remote`)
S
Sandeep Somavarapu 已提交
394 395
				];
				settingsPreviewModel.pushEditOperations([new Selection(position.lineNumber, position.column, position.lineNumber, position.column)], editOperations, () => []);
396 397
			}
		}
398

399
		return { settingsPreview: settingsPreviewModel.getValue(), hasConflicts: conflicts.size > 0 };
400 401 402 403 404 405 406 407 408 409
	}

	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) {
410 411 412
			if (removed.has(key)) {
				continue;
			}
413 414 415 416 417 418 419 420 421 422
			const value1 = from[key];
			const value2 = to[key];
			if (!objects.equals(value1, value2)) {
				updated.add(key);
			}
		}

		return { added, removed, updated };
	}

423
	private editSetting(model: ITextModel, key: string, value: any | undefined): void {
424 425
		const insertSpaces = false;
		const tabSize = 4;
426
		const eol = model.getEOL();
427 428 429 430 431 432 433 434 435 436
		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], () => []);
			}
437 438 439
		}
	}

S
Sandeep Somavarapu 已提交
440
	private async writeToRemote(content: string, ref: string | null): Promise<string> {
S
Sandeep Somavarapu 已提交
441
		return this.remoteUserDataService.write(SettingsSynchroniser.EXTERNAL_USER_DATA_SETTINGS_KEY, content, ref);
442 443
	}

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

	private updateLastSyncValue(remoteUserData: IUserData): void {
S
Sandeep Somavarapu 已提交
455
		this.storageService.store(SettingsSynchroniser.LAST_SYNC_SETTINGS_STORAGE_KEY, JSON.stringify(remoteUserData), StorageScope.GLOBAL);
456 457 458
	}

}