diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 3d35a5af3df..e4729ebdb3e 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -92,3 +92,13 @@ export const IUserDataSyncService = createDecorator('IUser export interface IUserDataSyncService extends ISynchroniser { _serviceBrand: any; } + +export const ISettingsMergeService = createDecorator('ISettingsMergeService'); + +export interface ISettingsMergeService { + + _serviceBrand: undefined; + + merge(localContent: string, remoteContent: string, baseContent: string | null): Promise; + +} diff --git a/src/vs/workbench/services/userData/common/settingsMergeService.ts b/src/vs/workbench/services/userData/common/settingsMergeService.ts new file mode 100644 index 00000000000..835e1a7179b --- /dev/null +++ b/src/vs/workbench/services/userData/common/settingsMergeService.ts @@ -0,0 +1,206 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { values } from 'vs/base/common/map'; +import { parse, findNodeAtLocation, parseTree } from 'vs/base/common/json'; +import { EditOperation } from 'vs/editor/common/core/editOperation'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { ITextModel } from 'vs/editor/common/model'; +import { setProperty } from 'vs/base/common/jsonEdit'; +import { Range } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { Position } from 'vs/editor/common/core/position'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { ISettingsMergeService } from 'vs/platform/userDataSync/common/userDataSync'; + +class SettingsMergeService implements ISettingsMergeService { + + _serviceBrand: undefined; + + constructor( + @IModelService private readonly modelService: IModelService, + @IModeService private readonly modeService: IModeService + ) { } + + async merge(localContent: string, remoteContent: string, baseContent: string | null): Promise { + const local = parse(localContent); + const remote = parse(remoteContent); + const base = baseContent ? parse(baseContent) : null; + const { changes, conflicts } = this.getChanges(local, remote, base); + + if (!changes.length && !conflicts.length) { + return localContent; + } + + const settingsPreviewModel = this.modelService.createModel(localContent, this.modeService.create('jsonc')); + for (const change of changes) { + this.editSetting(settingsPreviewModel, change.key, change.value); + } + for (const key of conflicts) { + const tree = parseTree(settingsPreviewModel.getValue()); + const valueNode = findNodeAtLocation(tree, [key]); + const eol = settingsPreviewModel.getEOL(); + const remoteEdit = setProperty(`{${eol}\t${eol}}`, [key], remote[key], { tabSize: 4, insertSpaces: false, eol: eol })[0]; + const remoteContent = remoteEdit ? `${remoteEdit.content.substring(remoteEdit.offset + remoteEdit.length + 1)},${eol}` : ''; + if (valueNode) { + // Updated in Local and Remote with different value + 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)), `${eol}<<<<<<< local`), + EditOperation.insert(new Position(valuePosition.lineNumber, settingsPreviewModel.getLineMaxColumn(valuePosition.lineNumber)), `${eol}=======${eol}${remoteContent}>>>>>>> remote`) + ]; + settingsPreviewModel.pushEditOperations([new Selection(keyPosition.lineNumber, keyPosition.column, keyPosition.lineNumber, keyPosition.column)], editOperations, () => []); + } 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, `${eol}<<<<<<< local${eol}=======${eol}${remoteContent}>>>>>>> remote`) + ]; + settingsPreviewModel.pushEditOperations([new Selection(position.lineNumber, position.column, position.lineNumber, position.column)], editOperations, () => []); + } + } + return settingsPreviewModel.getValue(); + } + + private editSetting(model: ITextModel, key: string, value: any | undefined): void { + const insertSpaces = false; + const tabSize = 4; + const eol = model.getEOL(); + 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], () => []); + } + } + } + + private getChanges(local: { [key: string]: any }, remote: { [key: string]: any }, base: { [key: string]: any } | null): { changes: { key: string; value: any | undefined; }[], conflicts: string[] } { + const localToRemote = this.compare(local, remote); + if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) { + // No changes found between local and remote. + return { changes: [], conflicts: [] }; + } + + const changes: { key: string, value: any | undefined }[] = []; + const conflicts: Set = new Set(); + const baseToLocal = base ? this.compare(base, local) : { added: Object.keys(local).reduce((r, k) => { r.add(k); return r; }, new Set()), removed: new Set(), updated: new Set() }; + const baseToRemote = base ? this.compare(base, remote) : { added: Object.keys(remote).reduce((r, k) => { r.add(k); return r; }, new Set()), removed: new Set(), updated: new Set() }; + + // 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()) { + if (conflicts.has(key)) { + continue; + } + // Got updated in local + if (baseToLocal.updated.has(key)) { + conflicts.add(key); + } else { + changes.push({ key, value: undefined }); + } + } + + // Added settings in Local + for (const key of baseToLocal.added.keys()) { + if (conflicts.has(key)) { + continue; + } + // 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()) { + if (conflicts.has(key)) { + continue; + } + // Got added in local + if (baseToLocal.added.has(key)) { + // Has different value + if (localToRemote.updated.has(key)) { + conflicts.add(key); + } + } else { + changes.push({ key, value: remote[key] }); + } + } + + // Updated settings in Local + for (const key of baseToLocal.updated.keys()) { + if (conflicts.has(key)) { + continue; + } + // 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()) { + if (conflicts.has(key)) { + continue; + } + // Got updated in local + if (baseToLocal.updated.has(key)) { + // Has different value + if (localToRemote.updated.has(key)) { + conflicts.add(key); + } + } else { + changes.push({ key, value: remote[key] }); + } + } + + return { changes, conflicts: values(conflicts) }; + } + + private compare(from: { [key: string]: any }, to: { [key: string]: any }): { added: Set, removed: Set, updated: Set } { + 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()); + const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set()); + const updated: Set = new Set(); + + for (const key of fromKeys) { + if (removed.has(key)) { + continue; + } + const value1 = from[key]; + const value2 = to[key]; + if (!objects.equals(value1, value2)) { + updated.add(key); + } + } + + return { added, removed, updated }; + } + +} + +registerSingleton(ISettingsMergeService, SettingsMergeService); diff --git a/src/vs/workbench/services/userData/common/settingsSync.ts b/src/vs/workbench/services/userData/common/settingsSync.ts index e8c13d8955a..9cd6edc227e 100644 --- a/src/vs/workbench/services/userData/common/settingsSync.ts +++ b/src/vs/workbench/services/userData/common/settingsSync.ts @@ -3,29 +3,20 @@ * 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'; -import { IUserData, UserDataSyncStoreError, UserDataSyncStoreErrorCode, ISynchroniser, SyncStatus, SETTINGS_PREVIEW_RESOURCE } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserData, UserDataSyncStoreError, UserDataSyncStoreErrorCode, ISynchroniser, SyncStatus, SETTINGS_PREVIEW_RESOURCE, ISettingsMergeService } from 'vs/platform/userDataSync/common/userDataSync'; import { IUserDataSyncStoreService } from 'vs/workbench/services/userData/common/userData'; import { VSBuffer } from 'vs/base/common/buffer'; -import { parse, findNodeAtLocation, parseTree, ParseError } from 'vs/base/common/json'; -import { ITextModel } from 'vs/editor/common/model'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { IModeService } from 'vs/editor/common/services/modeService'; +import { parse, ParseError } from 'vs/base/common/json'; 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'; import { Emitter, Event } from 'vs/base/common/event'; import { ILogService } from 'vs/platform/log/common/log'; -import { Position } from 'vs/editor/common/core/position'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { CancelablePromise, createCancelablePromise, ThrottledDelayer } from 'vs/base/common/async'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; interface ISyncPreviewResult { readonly fileContent: IFileContent | null; @@ -53,18 +44,17 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { constructor( @IFileService private readonly fileService: IFileService, - @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, @IStorageService private readonly storageService: IStorageService, @IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService, - @IModelService private readonly modelService: IModelService, - @IModeService private readonly modeService: IModeService, @IEditorService private readonly editorService: IEditorService, + @ISettingsMergeService private readonly settingsMergeService: ISettingsMergeService, @ILogService private readonly logService: ILogService, @IHistoryService private readonly historyService: IHistoryService, ) { super(); this.throttledDelayer = this._register(new ThrottledDelayer(500)); - this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.workbenchEnvironmentService.settingsResource))(() => this.throttledDelayer.trigger(() => this.onDidChangeSettings()))); + this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.environmentService.settingsResource))(() => this.throttledDelayer.trigger(() => this.onDidChangeSettings()))); } private async onDidChangeSettings(): Promise { @@ -197,34 +187,24 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { const remoteUserData = await this.userDataSyncStoreService.read(SettingsSynchroniser.EXTERNAL_USER_DATA_SETTINGS_KEY); // Get file content last to get the latest const fileContent = await this.getLocalFileContent(); - const { settingsPreview, hasLocalChanged, hasRemoteChanged, hasConflicts } = this.computeChanges(fileContent, remoteUserData); - if (hasLocalChanged || hasRemoteChanged) { - await this.fileService.writeFile(SETTINGS_PREVIEW_RESOURCE, VSBuffer.fromString(settingsPreview)); - } - return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts }; - } - - private computeChanges(fileContent: IFileContent | null, remoteUserData: IUserData | null): { settingsPreview: string, hasLocalChanged: boolean, hasRemoteChanged: boolean, hasConflicts: boolean } { - let hasLocalChanged: boolean = false; let hasRemoteChanged: boolean = false; let hasConflicts: boolean = false; - let settingsPreview: string = ''; // First time sync to remote if (fileContent && !remoteUserData) { this.logService.trace('Settings Sync: Remote contents does not exist. So sync with settings file.'); hasRemoteChanged = true; - settingsPreview = fileContent.value.toString(); - return { settingsPreview, hasLocalChanged, hasRemoteChanged, hasConflicts }; + await this.fileService.writeFile(SETTINGS_PREVIEW_RESOURCE, VSBuffer.fromString(fileContent.value.toString())); + return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts }; } // Settings file does not exist, so sync with remote contents. if (remoteUserData && !fileContent) { this.logService.trace('Settings Sync: Settings file does not exist. So sync with remote contents'); hasLocalChanged = true; - settingsPreview = remoteUserData.content; - return { settingsPreview, hasLocalChanged, hasRemoteChanged, hasConflicts }; + await this.fileService.writeFile(SETTINGS_PREVIEW_RESOURCE, VSBuffer.fromString(remoteUserData.content)); + return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts }; } if (fileContent && remoteUserData) { @@ -236,182 +216,20 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { || 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) { + const mergeContent = await this.settingsMergeService.merge(localContent, remoteContent, lastSyncData ? lastSyncData.content : null); + hasLocalChanged = mergeContent !== localContent; + hasRemoteChanged = mergeContent !== remoteContent; + if (hasLocalChanged || hasRemoteChanged) { // Sync only if there are changes - hasLocalChanged = settingsPreview !== localContent; // Local has changed - hasRemoteChanged = settingsPreview !== remoteContent; // Remote has changed - return { settingsPreview, hasLocalChanged, hasRemoteChanged, hasConflicts }; + hasConflicts = this.hasErrors(mergeContent); + await this.fileService.writeFile(SETTINGS_PREVIEW_RESOURCE, VSBuffer.fromString(mergeContent)); + return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts }; } } } this.logService.trace('Settings Sync: No changes.'); - return { settingsPreview, hasLocalChanged, hasRemoteChanged, hasConflicts }; - - } - - private mergeContents(localContent: string, remoteContent: string, lastSyncedContent: string | null): { settingsPreview: string, hasChanges: boolean; hasConflicts: boolean } { - const local = parse(localContent); - const remote = parse(remoteContent); - const localToRemote = this.compare(local, remote); - - 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; - const baseToLocal = base ? this.compare(base, local) : { added: Object.keys(local).reduce((r, k) => { r.add(k); return r; }, new Set()), removed: new Set(), updated: new Set() }; - const baseToRemote = base ? this.compare(base, remote) : { added: Object.keys(remote).reduce((r, k) => { r.add(k); return r; }, new Set()), removed: new Set(), updated: new Set() }; - - const conflicts: Set = new Set(); - - // 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()) { - if (conflicts.has(key)) { - continue; - } - // Got updated in local - if (baseToLocal.updated.has(key)) { - conflicts.add(key); - } else { - this.editSetting(settingsPreviewModel, key, undefined); - } - } - - // Added settings in Local - for (const key of baseToLocal.added.keys()) { - if (conflicts.has(key)) { - continue; - } - // 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()) { - if (conflicts.has(key)) { - continue; - } - // Got added in local - if (baseToLocal.added.has(key)) { - // Has different value - if (localToRemote.updated.has(key)) { - conflicts.add(key); - } - } else { - this.editSetting(settingsPreviewModel, key, remote[key]); - } - } - - // Updated settings in Local - for (const key of baseToLocal.updated.keys()) { - if (conflicts.has(key)) { - continue; - } - // 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()) { - if (conflicts.has(key)) { - continue; - } - // Got updated in local - if (baseToLocal.updated.has(key)) { - // Has different value - if (localToRemote.updated.has(key)) { - conflicts.add(key); - } - } else { - this.editSetting(settingsPreviewModel, key, remote[key]); - } - } - - for (const key of conflicts.keys()) { - const tree = parseTree(settingsPreviewModel.getValue()); - const valueNode = findNodeAtLocation(tree, [key]); - 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()}` : ''; - if (valueNode) { - // Updated in Local and Remote with different value - 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`), - EditOperation.insert(new Position(valuePosition.lineNumber, settingsPreviewModel.getLineMaxColumn(valuePosition.lineNumber)), `${settingsPreviewModel.getEOL()}=======${settingsPreviewModel.getEOL()}${remoteContent}>>>>>>> remote`) - ]; - settingsPreviewModel.pushEditOperations([new Selection(keyPosition.lineNumber, keyPosition.column, keyPosition.lineNumber, keyPosition.column)], editOperations, () => []); - } 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, () => []); - } - } - - return { settingsPreview: settingsPreviewModel.getValue(), hasChanges: true, hasConflicts: conflicts.size > 0 }; - } - - private compare(from: { [key: string]: any }, to: { [key: string]: any }): { added: Set, removed: Set, updated: Set } { - 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()); - const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set()); - const updated: Set = new Set(); - - for (const key of fromKeys) { - if (removed.has(key)) { - continue; - } - const value1 = from[key]; - const value2 = to[key]; - if (!objects.equals(value1, value2)) { - updated.add(key); - } - } - - return { added, removed, updated }; - } - - private editSetting(model: ITextModel, key: string, value: any | undefined): void { - const insertSpaces = false; - const tabSize = 4; - const eol = model.getEOL(); - 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], () => []); - } - } + return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts }; } private getLastSyncUserData(): IUserData | null { @@ -424,7 +242,7 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { private async getLocalFileContent(): Promise { try { - return await this.fileService.readFile(this.workbenchEnvironmentService.settingsResource); + return await this.fileService.readFile(this.environmentService.settingsResource); } catch (error) { if (error instanceof FileSystemProviderError && error.code !== FileSystemProviderErrorCode.FileNotFound) { return null; @@ -440,10 +258,10 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { private async writeToLocal(newContent: string, oldContent: IFileContent | null): Promise { if (oldContent) { // file exists already - await this.fileService.writeFile(this.workbenchEnvironmentService.settingsResource, VSBuffer.fromString(newContent), oldContent); + await this.fileService.writeFile(this.environmentService.settingsResource, VSBuffer.fromString(newContent), oldContent); } else { // file does not exist - await this.fileService.createFile(this.workbenchEnvironmentService.settingsResource, VSBuffer.fromString(newContent), { overwrite: false }); + await this.fileService.createFile(this.environmentService.settingsResource, VSBuffer.fromString(newContent), { overwrite: false }); } } diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 5a5fdc3b2d9..94dd9df5881 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -81,6 +81,7 @@ import 'vs/workbench/services/notification/common/notificationService'; import 'vs/workbench/services/extensions/common/staticExtensions'; import 'vs/workbench/services/userData/common/userDataSyncStoreService'; import 'vs/workbench/services/userData/common/userDataSyncService'; +import 'vs/workbench/services/userData/common/settingsMergeService'; import 'vs/workbench/services/workspace/browser/workspaceEditingService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions';