settingsSync.ts 18.2 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';
11
import { IRemoteUserDataService, IUserData, RemoteUserDataError, RemoteUserDataErrorCode, ISynchroniser, SyncStatus } from 'vs/workbench/services/userData/common/userData';
12
import { VSBuffer } from 'vs/base/common/buffer';
13
import { parse, findNodeAtLocation, parseTree } from 'vs/base/common/json';
14 15 16 17 18 19 20 21 22 23 24 25
import { ITextModelService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService';
import { URI } from 'vs/base/common/uri';
import { ITextModel } from 'vs/editor/common/model';
import { isEqual } from 'vs/base/common/resources';
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';
26
import { Emitter, Event } from 'vs/base/common/event';
27
import { ILogService } from 'vs/platform/log/common/log';
28
import { Position } from 'vs/editor/common/core/position';
29 30 31 32

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

39
export class SettingsSyncService extends Disposable implements ISynchroniser, ITextModelContentProvider {
S
Sandeep Somavarapu 已提交
40
	_serviceBrand: undefined;
41 42 43 44

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

45
	private readonly settingsPreviewResource: URI;
46 47 48

	private syncPreviewResultPromise: Promise<ISyncPreviewResult> | null = null;

49 50 51 52 53
	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;

54 55 56 57 58 59 60 61
	constructor(
		@IFileService private readonly fileService: IFileService,
		@IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService,
		@IStorageService private readonly storageService: IStorageService,
		@IRemoteUserDataService private readonly remoteUserDataService: IRemoteUserDataService,
		@ITextModelService private readonly textModelResolverService: ITextModelService,
		@IModelService private readonly modelService: IModelService,
		@IModeService private readonly modeService: IModeService,
62 63
		@IEditorService private readonly editorService: IEditorService,
		@ILogService private readonly logService: ILogService
64 65 66
	) {
		super();

67
		this.settingsPreviewResource = this.workbenchEnvironmentService.settingsResource.with({ scheme: 'vscode-settings-sync' });
68 69 70 71 72

		this.textModelResolverService.registerTextModelContentProvider('vscode-settings-sync', this);
	}

	provideTextContent(uri: URI): Promise<ITextModel> | null {
73 74
		if (isEqual(this.settingsPreviewResource, uri, false)) {
			return this.getPreview().then(({ settingsPreviewModel }) => settingsPreviewModel);
75 76 77 78
		}
		return null;
	}

79 80 81 82 83 84 85 86 87 88 89 90 91 92
	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);
93

S
Sandeep Somavarapu 已提交
94 95 96 97 98 99 100 101 102 103 104
		try {
			const result = await this.getPreview();
			if (result.hasConflicts) {
				this.setStatus(SyncStatus.HasConflicts);
				return false;
			}
			await this.apply();
			return true;
		} catch (e) {
			this.setStatus(SyncStatus.Idle);
			throw e;
105
		}
106
	}
107

108 109 110
	resolveConflicts(): void {
		if (this.status === SyncStatus.HasConflicts) {
			this.editorService.openEditor({
111 112
				resource: this.settingsPreviewResource,
				label: localize('settings preview', "Settings Preview"),
113 114 115 116 117 118 119
				options: {
					preserveFocus: false,
					pinned: true,
					revealIfVisible: true
				}
			});
		}
120 121
	}

122 123
	async apply(): Promise<void> {
		if (this.syncPreviewResultPromise) {
124
			const result = await this.syncPreviewResultPromise;
S
Sandeep Somavarapu 已提交
125
			let remoteUserData = result.remoteUserData;
126
			if (result.hasRemoteChanged) {
S
Sandeep Somavarapu 已提交
127 128
				remoteUserData = { version: result.remoteUserData ? result.remoteUserData.version + 1 : 1, content: result.settingsPreviewModel.getValue() };
				await this.writeToRemote(remoteUserData);
129 130 131
			}
			if (result.hasLocalChanged) {
				await this.writeToLocal(result.settingsPreviewModel.getValue(), result.fileContent);
132
			}
S
Sandeep Somavarapu 已提交
133 134
			if (remoteUserData) {
				this.updateLastSyncValue(remoteUserData);
135
			}
136
		}
137 138
		this.syncPreviewResultPromise = null;
		this.setStatus(SyncStatus.Idle);
139 140 141 142 143 144 145 146 147 148 149
	}

	private getPreview(): Promise<ISyncPreviewResult> {
		if (!this.syncPreviewResultPromise) {
			this.syncPreviewResultPromise = this.generatePreview();
		}
		return this.syncPreviewResultPromise;
	}

	private async generatePreview(): Promise<ISyncPreviewResult> {
		const remoteUserData = await this.remoteUserDataService.read(SettingsSyncService.EXTERNAL_USER_DATA_SETTINGS_KEY);
150
		// Get file content last to get the latest
151
		const fileContent = await this.getLocalFileContent();
152

153 154 155 156
		const settingsPreviewModel = this.modelService.getModel(this.settingsPreviewResource) || this.modelService.createModel('', this.modeService.create('jsonc'), this.settingsPreviewResource);
		let hasLocalChanged: boolean = false;
		let hasRemoteChanged: boolean = false;
		let hasConflicts: boolean = false;
157

158
		// First time sync to remote
159
		if (fileContent && !remoteUserData) {
160 161 162 163
			this.logService.trace('Settings Sync: Remote contents does not exist. So sync with settings file.');
			settingsPreviewModel.setValue(fileContent.value.toString());
			hasRemoteChanged = true;
			return { fileContent, remoteUserData, settingsPreviewModel, hasLocalChanged, hasRemoteChanged, hasConflicts };
164 165
		}

166
		// Settings file does not exist, so sync with remote contents.
167
		if (remoteUserData && !fileContent) {
168 169 170 171
			this.logService.trace('Settings Sync: Settings file does not exist. So sync with remote contents');
			settingsPreviewModel.setValue(remoteUserData.content);
			hasLocalChanged = true;
			return { fileContent, remoteUserData, settingsPreviewModel, hasLocalChanged, hasRemoteChanged, hasConflicts };
172 173 174 175 176 177 178 179 180 181
		}

		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) {
182 183 184
				this.logService.trace('Settings Sync: Settings file and remote contents are in sync.');
				settingsPreviewModel.setValue(localContent);
				return { fileContent, remoteUserData, settingsPreviewModel, hasLocalChanged, hasRemoteChanged, hasConflicts };
185 186
			}

187
			// First time Sync to Local
188
			if (!lastSyncData) {
189 190 191 192
				this.logService.trace('Settings Sync: Syncing remote contents with settings file for the first time.');
				hasLocalChanged = hasRemoteChanged = true;
				hasConflicts = await this.mergeContents(settingsPreviewModel, localContent, remoteContent, null);
				return { fileContent, remoteUserData, settingsPreviewModel, hasLocalChanged, hasRemoteChanged, hasConflicts };
193 194
			}

195 196
			// Remote has moved forward
			if (remoteUserData.version !== lastSyncData.version) {
197 198 199

				// Local content is same as last synced. So, sync with remote content.
				if (lastSyncData.content === localContent) {
200 201 202 203
					this.logService.trace('Settings Sync: Settings file has not changed from last time synced. So replace with remote contents.');
					settingsPreviewModel.setValue(remoteContent);
					hasLocalChanged = true;
					return { fileContent, remoteUserData, settingsPreviewModel, hasLocalChanged, hasRemoteChanged, hasConflicts };
204 205 206
				}

				// Local content is diverged from last synced. Required merge and sync.
207 208 209 210
				this.logService.trace('Settings Sync: Settings file is diverged from last time synced. Require merge and sync.');
				hasLocalChanged = hasRemoteChanged = true;
				hasConflicts = await this.mergeContents(settingsPreviewModel, localContent, remoteContent, lastSyncData.content);
				return { fileContent, remoteUserData, settingsPreviewModel, hasLocalChanged, hasRemoteChanged, hasConflicts };
211 212 213 214 215 216 217
			}

			// Remote data is same as last synced data
			if (lastSyncData.version === remoteUserData.version) {

				// Local contents are same as last synced data. No op.
				if (lastSyncData.content === localContent) {
218 219
					this.logService.trace('Settings Sync: Settings file and remote contents have not changed. So no sync needed.');
					return { fileContent, remoteUserData, settingsPreviewModel, hasLocalChanged, hasRemoteChanged, hasConflicts };
220 221 222
				}

				// New local contents. Sync with Local.
223 224 225 226
				this.logService.trace('Settings Sync: Remote contents have not changed. Settings file has changed. So sync with settings file.');
				settingsPreviewModel.setValue(localContent);
				hasRemoteChanged = true;
				return { fileContent, remoteUserData, settingsPreviewModel, hasLocalChanged, hasRemoteChanged, hasConflicts };
227 228 229 230
			}

		}

231
		return { fileContent, remoteUserData, settingsPreviewModel, hasLocalChanged, hasRemoteChanged, hasConflicts };
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253

	}

	private getLastSyncUserData(): IUserData | null {
		const lastSyncStorageContents = this.storageService.get(SettingsSyncService.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;
		}
	}

254 255 256
	private async mergeContents(settingsPreviewModel: ITextModel, localContent: string, remoteContent: string, lastSyncedContent: string | null): Promise<boolean> {
		const local = parse(localContent);
		const remote = parse(remoteContent);
257
		const base = lastSyncedContent ? parse(lastSyncedContent) : null;
258
		settingsPreviewModel.setValue(localContent);
259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275

		const baseToLocal = base ? this.compare(base, local) : { added: new Set<string>(), removed: new Set<string>(), updated: new Set<string>() };
		const baseToRemote = base ? this.compare(base, remote) : { added: new Set<string>(), removed: new Set<string>(), updated: new Set<string>() };
		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 已提交
276 277 278
			if (conflicts.has(key)) {
				continue;
			}
279 280 281 282
			// Got updated in local
			if (baseToLocal.updated.has(key)) {
				conflicts.add(key);
			} else {
283
				this.editSetting(settingsPreviewModel, key, undefined);
284 285 286 287 288
			}
		}

		// Added settings in Local
		for (const key of baseToLocal.added.keys()) {
S
Sandeep Somavarapu 已提交
289 290 291
			if (conflicts.has(key)) {
				continue;
			}
292 293 294 295 296 297 298 299 300 301 302
			// 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 已提交
303 304 305
			if (conflicts.has(key)) {
				continue;
			}
306 307 308 309 310 311 312
			// Got added in local
			if (baseToLocal.added.has(key)) {
				// Has different value
				if (localToRemote.updated.has(key)) {
					conflicts.add(key);
				}
			} else {
313
				this.editSetting(settingsPreviewModel, key, remote[key]);
314 315 316 317 318
			}
		}

		// Updated settings in Local
		for (const key of baseToLocal.updated.keys()) {
S
Sandeep Somavarapu 已提交
319 320 321
			if (conflicts.has(key)) {
				continue;
			}
322 323 324 325 326 327 328 329 330 331 332
			// 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 已提交
333 334 335
			if (conflicts.has(key)) {
				continue;
			}
336 337 338 339 340 341 342
			// Got updated in local
			if (baseToLocal.updated.has(key)) {
				// Has different value
				if (localToRemote.updated.has(key)) {
					conflicts.add(key);
				}
			} else {
343 344 345 346 347 348 349
				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 已提交
350 351
			const remoteEdit = setProperty(`{${settingsPreviewModel.getEOL()}\t${settingsPreviewModel.getEOL()}}`, [key], remote[key], { tabSize: 4, insertSpaces: false, eol: settingsPreviewModel.getEOL() })[0];
			const remoteContent = remoteEdit ? remoteEdit.content.substring(remoteEdit.offset + remoteEdit.length + 1) + settingsPreviewModel.getEOL() : '';
352
			if (valueNode) {
S
Sandeep Somavarapu 已提交
353
				// Updated in Local and Remote with different value
354 355 356 357
				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 已提交
358
					EditOperation.insert(new Position(valuePosition.lineNumber, settingsPreviewModel.getLineMaxColumn(valuePosition.lineNumber)), `${settingsPreviewModel.getEOL()}=======${settingsPreviewModel.getEOL()}${remoteContent}>>>>>>> remote`)
359 360
				];
				settingsPreviewModel.pushEditOperations([new Selection(keyPosition.lineNumber, keyPosition.column, keyPosition.lineNumber, keyPosition.column)], editOperations, () => []);
S
Sandeep Somavarapu 已提交
361 362 363 364 365 366 367
			} else {
				// Removed in Local, but updated in Remote
				const position = new Position(settingsPreviewModel.getLineCount() - 1, settingsPreviewModel.getLineMaxColumn(settingsPreviewModel.getLineCount() - 1));
				const editOperations = [
					EditOperation.insert(position, `${settingsPreviewModel.getEOL()}<<<<<<< local${ settingsPreviewModel.getEOL() }=======${ settingsPreviewModel.getEOL() }${ remoteContent }>>>>>>> remote`)
				];
				settingsPreviewModel.pushEditOperations([new Selection(position.lineNumber, position.column, position.lineNumber, position.column)], editOperations, () => []);
368 369
			}
		}
370 371

		return conflicts.size > 0;
372 373 374 375 376 377 378 379 380 381
	}

	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) {
382 383 384
			if (removed.has(key)) {
				continue;
			}
385 386 387 388 389 390 391 392 393 394
			const value1 = from[key];
			const value2 = to[key];
			if (!objects.equals(value1, value2)) {
				updated.add(key);
			}
		}

		return { added, removed, updated };
	}

395
	private editSetting(model: ITextModel, key: string, value: any | undefined): void {
396 397
		const insertSpaces = false;
		const tabSize = 4;
398
		const eol = model.getEOL();
399 400 401 402 403 404 405 406 407 408
		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], () => []);
			}
409 410 411 412 413 414 415
		}
	}

	private async writeToRemote(userData: IUserData): Promise<void> {
		try {
			await this.remoteUserDataService.write(SettingsSyncService.EXTERNAL_USER_DATA_SETTINGS_KEY, userData.version, userData.content);
		} catch (e) {
416
			if (e instanceof RemoteUserDataError && e.code === RemoteUserDataErrorCode.VersionExists) {
417
				// Rejected as there is a new version. Sync again
418
				await this.sync();
419 420 421 422 423 424
			}
			// An unknown error
			throw e;
		}
	}

425
	private async writeToLocal(newContent: string, oldContent: IFileContent | null): Promise<void> {
426 427 428 429 430 431 432 433 434 435 436 437 438 439
		if (oldContent) {
			try {
				// file exists before
				await this.fileService.writeFile(this.workbenchEnvironmentService.settingsResource, VSBuffer.fromString(newContent), oldContent);
			} catch (error) {
				// Error to check for dirty to sync again
				throw error;
			}
		} else {
			try {
				// file does not exist before
				await this.fileService.createFile(this.workbenchEnvironmentService.settingsResource, VSBuffer.fromString(newContent), { overwrite: false });
			} catch (error) {
				if (error instanceof FileSystemProviderError && error.code === FileSystemProviderErrorCode.FileExists) {
440
					await this.sync();
441 442 443 444 445
				}
				throw error;
			}
		}

446 447 448
	}

	private updateLastSyncValue(remoteUserData: IUserData): void {
449 450 451 452
		this.storageService.store(SettingsSyncService.LAST_SYNC_SETTINGS_STORAGE_KEY, JSON.stringify(remoteUserData), StorageScope.GLOBAL);
	}

}