Merge from vscode a4177f50c475fc0fa278a78235e3bee9ffdec781 (#8649)

* Merge from vscode a4177f50c475fc0fa278a78235e3bee9ffdec781

* distro

* fix tests
This commit is contained in:
Anthony Dresser
2019-12-11 22:42:23 -08:00
committed by GitHub
parent 82974a2135
commit 4ba6a979ba
280 changed files with 10898 additions and 14231 deletions

View File

@@ -796,6 +796,7 @@ export interface IFilesConfiguration {
eol: string;
enableTrash: boolean;
hotExit: string;
preventSaveConflicts: boolean;
};
}

View File

@@ -484,15 +484,15 @@ export class DiskFileSystemProvider extends Disposable implements
//#region File Watching
private _onDidWatchErrorOccur: Emitter<string> = this._register(new Emitter<string>());
readonly onDidErrorOccur: Event<string> = this._onDidWatchErrorOccur.event;
private _onDidWatchErrorOccur = this._register(new Emitter<string>());
readonly onDidErrorOccur = this._onDidWatchErrorOccur.event;
private _onDidChangeFile = this._register(new Emitter<readonly IFileChange[]>());
get onDidChangeFile(): Event<readonly IFileChange[]> { return this._onDidChangeFile.event; }
readonly onDidChangeFile = this._onDidChangeFile.event;
private recursiveWatcher: WindowsWatcherService | UnixWatcherService | NsfwWatcherService | undefined;
private recursiveFoldersToWatch: { path: string, excludes: string[] }[] = [];
private recursiveWatchRequestDelayer: ThrottledDelayer<void> = this._register(new ThrottledDelayer<void>(0));
private recursiveWatchRequestDelayer = this._register(new ThrottledDelayer<void>(0));
private recursiveWatcherLogLevelListener: IDisposable | undefined;

View File

@@ -42,7 +42,7 @@ suite('Files', () => {
assert.strictEqual(true, r1.gotDeleted());
});
function testIsEqual(testMethod: (pA: string | undefined, pB: string, ignoreCase: boolean) => boolean): void {
function testIsEqual(testMethod: (pA: string, pB: string, ignoreCase: boolean) => boolean): void {
// corner cases
assert(testMethod('', '', true));
@@ -136,7 +136,7 @@ suite('Files', () => {
test('isEqualOrParent (ignorecase)', function () {
// same assertions apply as with isEqual()
testIsEqual(isEqualOrParent);
testIsEqual(isEqualOrParent); //
if (isWindows) {
assert(isEqualOrParent('c:\\some\\path', 'c:\\', true));
@@ -182,4 +182,4 @@ suite('Files', () => {
assert(!isEqualOrParent('foo/bar/test.ts', 'foo/BAR/test.', true));
}
});
});
});

View File

@@ -22,7 +22,7 @@ import { IEditorOptions } from 'vs/platform/editor/common/editor';
import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { Registry } from 'vs/platform/registry/common/platform';
import { attachListStyler, computeStyles, defaultListStyles, IColorMapping, attachStyler } from 'vs/platform/theme/common/styler';
import { attachListStyler, computeStyles, defaultListStyles, IColorMapping } from 'vs/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys';
import { ObjectTree, IObjectTreeOptions, ICompressibleTreeRenderer, CompressibleObjectTree, ICompressibleObjectTreeOptions } from 'vs/base/browser/ui/tree/objectTree';
@@ -287,7 +287,7 @@ export class WorkbenchList<T> extends List<T> {
this.disposables.add((listService as ListService).register(this));
if (options.overrideStyles) {
this.disposables.add(attachStyler(themeService, options.overrideStyles, this));
this.disposables.add(attachListStyler(this, themeService, options.overrideStyles));
}
this.disposables.add(this.onSelectionChange(() => {
@@ -368,7 +368,7 @@ export class WorkbenchPagedList<T> extends PagedList<T> {
this.disposables.add((listService as ListService).register(this));
if (options.overrideStyles) {
this.disposables.add(attachStyler(themeService, options.overrideStyles, this));
this.disposables.add(attachListStyler(this, themeService, options.overrideStyles));
}
this.registerListeners();
@@ -1044,7 +1044,7 @@ class WorkbenchTreeInternals<TInput, T, TFilterData> {
this.disposables.push(
this.contextKeyService,
(listService as ListService).register(tree),
overrideStyles ? attachStyler(themeService, overrideStyles, tree) : Disposable.None,
overrideStyles ? attachListStyler(tree, themeService, overrideStyles) : Disposable.None,
tree.onDidChangeSelection(() => {
const selection = tree.getSelection();
const focus = tree.getFocus();

View File

@@ -102,7 +102,7 @@ export interface IProductConfiguration {
readonly portable?: string;
readonly extensionKind?: { readonly [extensionId: string]: ExtensionKind | ExtensionKind[]; };
readonly extensionKind?: { readonly [extensionId: string]: ExtensionKind[]; };
readonly extensionAllowedProposedApi?: readonly string[];
readonly msftInternalDomains?: string[];

View File

@@ -11,8 +11,9 @@ export const ITunnelService = createDecorator<ITunnelService>('tunnelService');
export interface RemoteTunnel {
readonly tunnelRemotePort: number;
readonly tunnelRemoteHost: string;
readonly tunnelLocalPort: number;
readonly localAddress?: string;
readonly localAddress: string;
dispose(): void;
}

View File

@@ -5,11 +5,19 @@
import { ISignService } from 'vs/platform/sign/common/sign';
/* tslint:disable */
declare module vsda {
export class signer {
sign(arg: any): any;
}
}
export class SignService implements ISignService {
_serviceBrand: undefined;
private vsda(): Promise<typeof import('vsda')> {
return import('vsda');
private vsda(): Promise<typeof vsda> {
return new Promise((resolve, reject) => require(['vsda'], resolve, reject));
}
async sign(value: string): Promise<string> {
@@ -20,9 +28,8 @@ export class SignService implements ISignService {
return signer.sign(value);
}
} catch (e) {
console.error('signer.sign: ' + e);
// ignore errors silently
}
return value;
}
}

View File

@@ -11,16 +11,16 @@ import { isUndefined, isUndefinedOrNull } from 'vs/base/common/types';
import { IStateService } from 'vs/platform/state/node/state';
import { ILogService } from 'vs/platform/log/common/log';
type StorageDatebase = { [key: string]: any; };
type StorageDatabase = { [key: string]: any; };
export class FileStorage {
private _database: StorageDatebase | null = null;
private _database: StorageDatabase | null = null;
private lastFlushedSerializedDatabase: string | null = null;
constructor(private dbPath: string, private onError: (error: Error) => void) { }
private get database(): StorageDatebase {
private get database(): StorageDatabase {
if (!this._database) {
this._database = this.loadSync();
}
@@ -42,7 +42,7 @@ export class FileStorage {
this._database = database;
}
private loadSync(): StorageDatebase {
private loadSync(): StorageDatabase {
try {
this.lastFlushedSerializedDatabase = fs.readFileSync(this.dbPath).toString();
@@ -56,7 +56,7 @@ export class FileStorage {
}
}
private async loadAsync(): Promise<StorageDatebase> {
private async loadAsync(): Promise<StorageDatabase> {
try {
this.lastFlushedSerializedDatabase = (await readFile(this.dbPath)).toString();

View File

@@ -0,0 +1,169 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { values, keys } from 'vs/base/common/map';
import { ISyncExtension } from 'vs/platform/userDataSync/common/userDataSync';
import { IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { startsWith } from 'vs/base/common/strings';
export interface IMergeResult {
added: ISyncExtension[];
removed: IExtensionIdentifier[];
updated: ISyncExtension[];
remote: ISyncExtension[] | null;
}
export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISyncExtension[] | null, lastSyncExtensions: ISyncExtension[] | null, skippedExtensions: ISyncExtension[], ignoredExtensions: string[]): IMergeResult {
const added: ISyncExtension[] = [];
const removed: IExtensionIdentifier[] = [];
const updated: ISyncExtension[] = [];
if (!remoteExtensions) {
return {
added,
removed,
updated,
remote: localExtensions.filter(({ identifier }) => ignoredExtensions.every(id => id.toLowerCase() !== identifier.id.toLowerCase()))
};
}
const uuids: Map<string, string> = new Map<string, string>();
const addUUID = (identifier: IExtensionIdentifier) => { if (identifier.uuid) { uuids.set(identifier.id.toLowerCase(), identifier.uuid); } };
localExtensions.forEach(({ identifier }) => addUUID(identifier));
remoteExtensions.forEach(({ identifier }) => addUUID(identifier));
if (lastSyncExtensions) {
lastSyncExtensions.forEach(({ identifier }) => addUUID(identifier));
}
const addExtensionToMap = (map: Map<string, ISyncExtension>, extension: ISyncExtension) => {
const uuid = extension.identifier.uuid || uuids.get(extension.identifier.id.toLowerCase());
const key = uuid ? `uuid:${uuid}` : `id:${extension.identifier.id.toLowerCase()}`;
map.set(key, extension);
return map;
};
const localExtensionsMap = localExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>());
const remoteExtensionsMap = remoteExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>());
const newRemoteExtensionsMap = remoteExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>());
const lastSyncExtensionsMap = lastSyncExtensions ? lastSyncExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>()) : null;
const skippedExtensionsMap = skippedExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>());
const ignoredExtensionsSet = ignoredExtensions.reduce((set, id) => {
const uuid = uuids.get(id.toLowerCase());
return set.add(uuid ? `uuid:${uuid}` : `id:${id.toLowerCase()}`);
}, new Set<string>());
const localToRemote = compare(localExtensionsMap, remoteExtensionsMap, ignoredExtensionsSet);
if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) {
// No changes found between local and remote.
return { added: [], removed: [], updated: [], remote: null };
}
const baseToLocal = compare(lastSyncExtensionsMap, localExtensionsMap, ignoredExtensionsSet);
const baseToRemote = compare(lastSyncExtensionsMap, remoteExtensionsMap, ignoredExtensionsSet);
const massageSyncExtension = (extension: ISyncExtension, key: string): ISyncExtension => {
const massagedExtension: ISyncExtension = {
identifier: {
id: extension.identifier.id,
uuid: startsWith(key, 'uuid:') ? key.substring('uuid:'.length) : undefined
},
enabled: extension.enabled,
};
if (extension.version) {
massagedExtension.version = extension.version;
}
return massagedExtension;
};
// Remotely removed extension.
for (const key of values(baseToRemote.removed)) {
const e = localExtensionsMap.get(key);
if (e) {
removed.push(e.identifier);
}
}
// Remotely added extension
for (const key of values(baseToRemote.added)) {
// Got added in local
if (baseToLocal.added.has(key)) {
// Is different from local to remote
if (localToRemote.updated.has(key)) {
updated.push(massageSyncExtension(remoteExtensionsMap.get(key)!, key));
}
} else {
// Add to local
added.push(massageSyncExtension(remoteExtensionsMap.get(key)!, key));
}
}
// Remotely updated extensions
for (const key of values(baseToRemote.updated)) {
// If updated in local
if (baseToLocal.updated.has(key)) {
// Is different from local to remote
if (localToRemote.updated.has(key)) {
// update it in local
updated.push(massageSyncExtension(remoteExtensionsMap.get(key)!, key));
}
}
}
// Locally added extensions
for (const key of values(baseToLocal.added)) {
// Not there in remote
if (!baseToRemote.added.has(key)) {
newRemoteExtensionsMap.set(key, massageSyncExtension(localExtensionsMap.get(key)!, key));
}
}
// Locally updated extensions
for (const key of values(baseToLocal.updated)) {
// If removed in remote
if (baseToRemote.removed.has(key)) {
continue;
}
// If not updated in remote
if (!baseToRemote.updated.has(key)) {
newRemoteExtensionsMap.set(key, massageSyncExtension(localExtensionsMap.get(key)!, key));
}
}
// Locally removed extensions
for (const key of values(baseToLocal.removed)) {
// If not skipped and not updated in remote
if (!skippedExtensionsMap.has(key) && !baseToRemote.updated.has(key)) {
newRemoteExtensionsMap.delete(key);
}
}
const remoteChanges = compare(remoteExtensionsMap, newRemoteExtensionsMap, new Set<string>());
const remote = remoteChanges.added.size > 0 || remoteChanges.updated.size > 0 || remoteChanges.removed.size > 0 ? values(newRemoteExtensionsMap) : null;
return { added, removed, updated, remote };
}
function compare(from: Map<string, ISyncExtension> | null, to: Map<string, ISyncExtension>, ignoredExtensions: Set<string>): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
const fromKeys = from ? keys(from).filter(key => !ignoredExtensions.has(key)) : [];
const toKeys = keys(to).filter(key => !ignoredExtensions.has(key));
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) {
if (removed.has(key)) {
continue;
}
const fromExtension = from!.get(key)!;
const toExtension = to.get(key);
if (!toExtension
|| fromExtension.enabled !== toExtension.enabled
|| fromExtension.version !== toExtension.version
) {
updated.add(key);
}
}
return { added, removed, updated };
}

View File

@@ -13,12 +13,11 @@ import { joinPath } from 'vs/base/common/resources';
import { IExtensionManagementService, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { keys, values } from 'vs/base/common/map';
import { startsWith } from 'vs/base/common/strings';
import { IFileService } from 'vs/platform/files/common/files';
import { Queue } from 'vs/base/common/async';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { localize } from 'vs/nls';
import { merge } from 'vs/platform/userDataSync/common/extensionsMerge';
export interface ISyncPreviewResult {
readonly added: ISyncExtension[];
@@ -135,8 +134,14 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
const localExtensions = await this.getLocalExtensions();
this.logService.trace('Extensions: Merging remote extensions with local extensions...');
const { added, removed, updated, remote } = this.merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions);
if (remoteExtensions) {
this.logService.trace('Extensions: Merging remote extensions with local extensions...');
} else {
this.logService.info('Extensions: Remote extensions does not exist. Synchronizing extensions for the first time.');
}
const ignoredExtensions = this.configurationService.getValue<string[]>('sync.ignoredExtensions') || [];
const { added, removed, updated, remote } = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, ignoredExtensions);
if (!added.length && !removed.length && !updated.length && !remote) {
this.logService.trace('Extensions: No changes found during synchronizing extensions.');
@@ -162,160 +167,6 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
}
}
/**
* Merge Strategy:
* - If remote does not exist, merge with local (First time sync)
* - Overwrite local with remote changes. Removed, Added, Updated.
* - Update remote with those local extension which are newly added or updated or removed and untouched in remote.
*/
private merge(localExtensions: ISyncExtension[], remoteExtensions: ISyncExtension[] | null, lastSyncExtensions: ISyncExtension[] | null, skippedExtensions: ISyncExtension[]): { added: ISyncExtension[], removed: IExtensionIdentifier[], updated: ISyncExtension[], remote: ISyncExtension[] | null } {
const ignoredExtensions = this.configurationService.getValue<string[]>('sync.ignoredExtensions') || [];
// First time sync
if (!remoteExtensions) {
this.logService.info('Extensions: Remote extensions does not exist. Synchronizing extensions for the first time.');
return { added: [], removed: [], updated: [], remote: localExtensions.filter(({ identifier }) => ignoredExtensions.every(id => id.toLowerCase() !== identifier.id.toLowerCase())) };
}
const uuids: Map<string, string> = new Map<string, string>();
const addUUID = (identifier: IExtensionIdentifier) => { if (identifier.uuid) { uuids.set(identifier.id.toLowerCase(), identifier.uuid); } };
localExtensions.forEach(({ identifier }) => addUUID(identifier));
remoteExtensions.forEach(({ identifier }) => addUUID(identifier));
if (lastSyncExtensions) {
lastSyncExtensions.forEach(({ identifier }) => addUUID(identifier));
}
const addExtensionToMap = (map: Map<string, ISyncExtension>, extension: ISyncExtension) => {
const uuid = extension.identifier.uuid || uuids.get(extension.identifier.id.toLowerCase());
const key = uuid ? `uuid:${uuid}` : `id:${extension.identifier.id.toLowerCase()}`;
map.set(key, extension);
return map;
};
const localExtensionsMap = localExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>());
const remoteExtensionsMap = remoteExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>());
const newRemoteExtensionsMap = remoteExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>());
const lastSyncExtensionsMap = lastSyncExtensions ? lastSyncExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>()) : null;
const skippedExtensionsMap = skippedExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>());
const ignoredExtensionsSet = ignoredExtensions.reduce((set, id) => {
const uuid = uuids.get(id.toLowerCase());
return set.add(uuid ? `uuid:${uuid}` : `id:${id.toLowerCase()}`);
}, new Set<string>());
const localToRemote = this.compare(localExtensionsMap, remoteExtensionsMap, ignoredExtensionsSet);
if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) {
// No changes found between local and remote.
return { added: [], removed: [], updated: [], remote: null };
}
const added: ISyncExtension[] = [];
const removed: IExtensionIdentifier[] = [];
const updated: ISyncExtension[] = [];
const baseToLocal = lastSyncExtensionsMap ? this.compare(lastSyncExtensionsMap, localExtensionsMap, ignoredExtensionsSet) : { added: keys(localExtensionsMap).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
const baseToRemote = lastSyncExtensionsMap ? this.compare(lastSyncExtensionsMap, remoteExtensionsMap, ignoredExtensionsSet) : { added: keys(remoteExtensionsMap).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
const massageSyncExtension = (extension: ISyncExtension, key: string): ISyncExtension => {
return {
identifier: {
id: extension.identifier.id,
uuid: startsWith(key, 'uuid:') ? key.substring('uuid:'.length) : undefined
},
enabled: extension.enabled,
version: extension.version
};
};
// Remotely removed extension.
for (const key of values(baseToRemote.removed)) {
const e = localExtensionsMap.get(key);
if (e) {
removed.push(e.identifier);
}
}
// Remotely added extension
for (const key of values(baseToRemote.added)) {
// Got added in local
if (baseToLocal.added.has(key)) {
// Is different from local to remote
if (localToRemote.updated.has(key)) {
updated.push(massageSyncExtension(remoteExtensionsMap.get(key)!, key));
}
} else {
// Add to local
added.push(massageSyncExtension(remoteExtensionsMap.get(key)!, key));
}
}
// Remotely updated extensions
for (const key of values(baseToRemote.updated)) {
// If updated in local
if (baseToLocal.updated.has(key)) {
// Is different from local to remote
if (localToRemote.updated.has(key)) {
// update it in local
updated.push(massageSyncExtension(remoteExtensionsMap.get(key)!, key));
}
}
}
// Locally added extensions
for (const key of values(baseToLocal.added)) {
// Not there in remote
if (!baseToRemote.added.has(key)) {
newRemoteExtensionsMap.set(key, massageSyncExtension(localExtensionsMap.get(key)!, key));
}
}
// Locally updated extensions
for (const key of values(baseToLocal.updated)) {
// If removed in remote
if (baseToRemote.removed.has(key)) {
continue;
}
// If not updated in remote
if (!baseToRemote.updated.has(key)) {
newRemoteExtensionsMap.set(key, massageSyncExtension(localExtensionsMap.get(key)!, key));
}
}
// Locally removed extensions
for (const key of values(baseToLocal.removed)) {
// If not skipped and not updated in remote
if (!skippedExtensionsMap.has(key) && !baseToRemote.updated.has(key)) {
newRemoteExtensionsMap.delete(key);
}
}
const remoteChanges = this.compare(remoteExtensionsMap, newRemoteExtensionsMap, new Set<string>());
const remote = remoteChanges.added.size > 0 || remoteChanges.updated.size > 0 || remoteChanges.removed.size > 0 ? values(newRemoteExtensionsMap) : null;
return { added, removed, updated, remote };
}
private compare(from: Map<string, ISyncExtension>, to: Map<string, ISyncExtension>, ignoredExtensions: Set<string>): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
const fromKeys = keys(from).filter(key => !ignoredExtensions.has(key));
const toKeys = keys(to).filter(key => !ignoredExtensions.has(key));
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) {
if (removed.has(key)) {
continue;
}
const fromExtension = from.get(key)!;
const toExtension = to.get(key);
if (!toExtension
|| fromExtension.enabled !== toExtension.enabled
|| fromExtension.version !== toExtension.version
) {
updated.add(key);
}
}
return { added, removed, updated };
}
private async updateLocalExtensions(added: ISyncExtension[], removed: IExtensionIdentifier[], updated: ISyncExtension[], skippedExtensions: ISyncExtension[]): Promise<ISyncExtension[]> {
const removeFromSkipped: IExtensionIdentifier[] = [];
const addToSkipped: ISyncExtension[] = [];

View File

@@ -19,6 +19,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { CancellationToken } from 'vs/base/common/cancellation';
import { OS, OperatingSystem } from 'vs/base/common/platform';
import { isUndefined } from 'vs/base/common/types';
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
interface ISyncContent {
mac?: string;
@@ -217,7 +218,7 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser
|| lastSyncContent !== remoteContent // Remote has forwarded
) {
this.logService.trace('Keybindings: Merging remote keybindings with local keybindings...');
const formattingOptions = await this.userDataSyncUtilService.resolveFormattingOptions(this.environmentService.keybindingsResource);
const formattingOptions = await this.getFormattingOptions();
const result = await merge(localContent, remoteContent, lastSyncContent, formattingOptions, this.userDataSyncUtilService);
// Sync only if there are changes
if (result.hasChanges) {
@@ -243,6 +244,14 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser
return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts };
}
private _formattingOptions: Promise<FormattingOptions> | undefined = undefined;
private getFormattingOptions(): Promise<FormattingOptions> {
if (!this._formattingOptions) {
this._formattingOptions = this.userDataSyncUtilService.resolveFormattingOptions(this.environmentService.keybindingsResource);
}
return this._formattingOptions;
}
private async getLocalContent(): Promise<IFileContent | null> {
try {
return await this.fileService.readFile(this.environmentService.keybindingsResource);

View File

@@ -1,45 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { Event } from 'vs/base/common/event';
import { IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync';
import { IStringDictionary } from 'vs/base/common/collections';
import { URI } from 'vs/base/common/uri';
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
export class UserDataSycnUtilServiceChannel implements IServerChannel {
constructor(private readonly service: IUserDataSyncUtilService) { }
listen(_: unknown, event: string): Event<any> {
throw new Error(`Event not found: ${event}`);
}
call(context: any, command: string, args?: any): Promise<any> {
switch (command) {
case 'resolveUserKeybindings': return this.service.resolveUserBindings(args[0]);
case 'resolveFormattingOptions': return this.service.resolveFormattingOptions(URI.revive(args[0]));
}
throw new Error('Invalid call');
}
}
export class UserDataSyncUtilServiceClient implements IUserDataSyncUtilService {
_serviceBrand: undefined;
constructor(private readonly channel: IChannel) {
}
async resolveUserBindings(userbindings: string[]): Promise<IStringDictionary<string>> {
return this.channel.call('resolveUserKeybindings', [userbindings]);
}
async resolveFormattingOptions(file: URI): Promise<FormattingOptions> {
return this.channel.call('resolveFormattingOptions', [file]);
}
}

View File

@@ -0,0 +1,191 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as objects from 'vs/base/common/objects';
import { parse, findNodeAtLocation, parseTree, Node } from 'vs/base/common/json';
import { setProperty } from 'vs/base/common/jsonEdit';
import { values } from 'vs/base/common/map';
import { IStringDictionary } from 'vs/base/common/collections';
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
import * as contentUtil from 'vs/platform/userDataSync/common/content';
export function computeRemoteContent(localContent: string, remoteContent: string, ignoredSettings: string[], formattingOptions: FormattingOptions): string {
if (ignoredSettings.length) {
const remote = parse(remoteContent);
const ignored = ignoredSettings.reduce((set, key) => { set.add(key); return set; }, new Set<string>());
for (const key of ignoredSettings) {
if (ignored.has(key)) {
localContent = contentUtil.edit(localContent, [key], remote[key], formattingOptions);
}
}
}
return localContent;
}
export function merge(localContent: string, remoteContent: string, baseContent: string | null, ignoredSettings: string[], formattingOptions: FormattingOptions): { mergeContent: string, hasChanges: boolean, hasConflicts: boolean } {
const local = parse(localContent);
const remote = parse(remoteContent);
const base = baseContent ? parse(baseContent) : null;
const ignored = ignoredSettings.reduce((set, key) => { set.add(key); return set; }, new Set<string>());
const localToRemote = compare(local, remote, ignored);
if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) {
// No changes found between local and remote.
return { mergeContent: localContent, hasChanges: false, hasConflicts: false };
}
const conflicts: Set<string> = new Set<string>();
const baseToLocal = base ? compare(base, local, ignored) : { 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 ? compare(base, remote, ignored) : { added: Object.keys(remote).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
let mergeContent = localContent;
// Removed settings in Local
for (const key of values(baseToLocal.removed)) {
// Got updated in remote
if (baseToRemote.updated.has(key)) {
conflicts.add(key);
}
}
// Removed settings in Remote
for (const key of values(baseToRemote.removed)) {
if (conflicts.has(key)) {
continue;
}
// Got updated in local
if (baseToLocal.updated.has(key)) {
conflicts.add(key);
} else {
mergeContent = contentUtil.edit(mergeContent, [key], undefined, formattingOptions);
}
}
// Added settings in Local
for (const key of values(baseToLocal.added)) {
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 values(baseToRemote.added)) {
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 {
mergeContent = contentUtil.edit(mergeContent, [key], remote[key], formattingOptions);
}
}
// Updated settings in Local
for (const key of values(baseToLocal.updated)) {
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 values(baseToRemote.updated)) {
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 {
mergeContent = contentUtil.edit(mergeContent, [key], remote[key], formattingOptions);
}
}
if (conflicts.size > 0) {
const conflictNodes: { key: string, node: Node | undefined }[] = [];
const tree = parseTree(mergeContent);
const eol = formattingOptions.eol!;
for (const key of values(conflicts)) {
const node = findNodeAtLocation(tree, [key]);
conflictNodes.push({ key, node });
}
conflictNodes.sort((a, b) => {
if (a.node && b.node) {
return b.node.offset - a.node.offset;
}
return a.node ? 1 : -1;
});
const lastNode = tree.children ? tree.children[tree.children.length - 1] : undefined;
for (const { key, node } of conflictNodes) {
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 (node) {
// Updated in Local and Remote with different value
const localStartOffset = contentUtil.getLineStartOffset(mergeContent, eol, node.parent!.offset);
const localEndOffset = contentUtil.getLineEndOffset(mergeContent, eol, node.offset + node.length);
mergeContent = mergeContent.substring(0, localStartOffset)
+ `<<<<<<< local${eol}`
+ mergeContent.substring(localStartOffset, localEndOffset)
+ `${eol}=======${eol}${remoteContent}>>>>>>> remote`
+ mergeContent.substring(localEndOffset);
} else {
// Removed in Local, but updated in Remote
if (lastNode) {
const localStartOffset = contentUtil.getLineEndOffset(mergeContent, eol, lastNode.offset + lastNode.length);
mergeContent = mergeContent.substring(0, localStartOffset)
+ `${eol}<<<<<<< local${eol}=======${eol}${remoteContent}>>>>>>> remote`
+ mergeContent.substring(localStartOffset);
} else {
const localStartOffset = tree.offset + 1;
mergeContent = mergeContent.substring(0, localStartOffset)
+ `${eol}<<<<<<< local${eol}=======${eol}${remoteContent}>>>>>>> remote${eol}`
+ mergeContent.substring(localStartOffset);
}
}
}
}
return { mergeContent, hasChanges: true, hasConflicts: conflicts.size > 0 };
}
function compare(from: IStringDictionary<any>, to: IStringDictionary<any>, ignored: Set<string>): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
const fromKeys = Object.keys(from).filter(key => !ignored.has(key));
const toKeys = Object.keys(to).filter(key => !ignored.has(key));
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) {
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 };
}

View File

@@ -5,7 +5,7 @@
import { Disposable } from 'vs/base/common/lifecycle';
import { IFileService, FileSystemProviderErrorCode, FileSystemProviderError, IFileContent } from 'vs/platform/files/common/files';
import { IUserData, UserDataSyncStoreError, UserDataSyncStoreErrorCode, ISynchroniser, SyncStatus, ISettingsMergeService, IUserDataSyncStoreService, DEFAULT_IGNORED_SETTINGS, IUserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserData, UserDataSyncStoreError, UserDataSyncStoreErrorCode, ISynchroniser, SyncStatus, IUserDataSyncStoreService, DEFAULT_IGNORED_SETTINGS, IUserDataSyncLogService, IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync';
import { VSBuffer } from 'vs/base/common/buffer';
import { parse, ParseError } from 'vs/base/common/json';
import { localize } from 'vs/nls';
@@ -17,6 +17,8 @@ import { joinPath, dirname } from 'vs/base/common/resources';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { startsWith } from 'vs/base/common/strings';
import { CancellationToken } from 'vs/base/common/cancellation';
import { computeRemoteContent, merge } from 'vs/platform/userDataSync/common/settingsMerge';
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
interface ISyncPreviewResult {
readonly fileContent: IFileContent | null;
@@ -46,8 +48,8 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser {
@IFileService private readonly fileService: IFileService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
@ISettingsMergeService private readonly settingsMergeService: ISettingsMergeService,
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@IUserDataSyncUtilService private readonly userDataSyncUtilService: IUserDataSyncUtilService,
@IConfigurationService private readonly configurationService: IConfigurationService,
) {
super();
@@ -148,7 +150,8 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser {
await this.writeToLocal(content, fileContent);
}
if (hasRemoteChanged) {
const remoteContent = remoteUserData.content ? await this.settingsMergeService.computeRemoteContent(content, remoteUserData.content, this.getIgnoredSettings(content)) : content;
const formatUtils = await this.getFormattingOptions();
const remoteContent = remoteUserData.content ? computeRemoteContent(content, remoteUserData.content, this.getIgnoredSettings(content), formatUtils) : content;
this.logService.info('Settings: Updating remote settings');
const ref = await this.writeToRemote(remoteContent, remoteUserData.ref);
remoteUserData = { ref, content };
@@ -205,7 +208,8 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser {
|| lastSyncData.content !== remoteContent // Remote has forwarded
) {
this.logService.trace('Settings: Merging remote settings with local settings...');
const result = await this.settingsMergeService.merge(localContent, remoteContent, lastSyncData ? lastSyncData.content : null, this.getIgnoredSettings());
const formatUtils = await this.getFormattingOptions();
const result = merge(localContent, remoteContent, lastSyncData ? lastSyncData.content : null, this.getIgnoredSettings(), formatUtils);
// Sync only if there are changes
if (result.hasChanges) {
hasLocalChanged = result.mergeContent !== localContent;
@@ -230,6 +234,14 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser {
return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts };
}
private _formattingOptions: Promise<FormattingOptions> | undefined = undefined;
private getFormattingOptions(): Promise<FormattingOptions> {
if (!this._formattingOptions) {
this._formattingOptions = this.userDataSyncUtilService.resolveFormattingOptions(this.environmentService.settingsResource);
}
return this._formattingOptions;
}
private getIgnoredSettings(settingsContent?: string): string[] {
let value: string[] = [];
if (settingsContent) {

View File

@@ -1,42 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { Event } from 'vs/base/common/event';
import { ISettingsMergeService } from 'vs/platform/userDataSync/common/userDataSync';
export class SettingsMergeChannel implements IServerChannel {
constructor(private readonly service: ISettingsMergeService) { }
listen(_: unknown, event: string): Event<any> {
throw new Error(`Event not found: ${event}`);
}
call(context: any, command: string, args?: any): Promise<any> {
switch (command) {
case 'merge': return this.service.merge(args[0], args[1], args[2], args[3]);
case 'computeRemoteContent': return this.service.computeRemoteContent(args[0], args[1], args[2]);
}
throw new Error('Invalid call');
}
}
export class SettingsMergeChannelClient implements ISettingsMergeService {
_serviceBrand: undefined;
constructor(private readonly channel: IChannel) {
}
merge(localContent: string, remoteContent: string, baseContent: string | null, ignoredSettings: string[]): Promise<{ mergeContent: string, hasChanges: boolean, hasConflicts: boolean }> {
return this.channel.call('merge', [localContent, remoteContent, baseContent, ignoredSettings]);
}
computeRemoteContent(localContent: string, remoteContent: string, ignoredSettings: string[]): Promise<string> {
return this.channel.call('computeRemoteContent', [localContent, remoteContent, ignoredSettings]);
}
}

View File

@@ -177,18 +177,6 @@ export interface IUserDataSyncService extends ISynchroniser {
removeExtension(identifier: IExtensionIdentifier): Promise<void>;
}
export const ISettingsMergeService = createDecorator<ISettingsMergeService>('ISettingsMergeService');
export interface ISettingsMergeService {
_serviceBrand: undefined;
merge(localContent: string, remoteContent: string, baseContent: string | null, ignoredSettings: string[]): Promise<{ mergeContent: string, hasChanges: boolean, hasConflicts: boolean }>;
computeRemoteContent(localContent: string, remoteContent: string, ignoredSettings: string[]): Promise<string>;
}
export const IUserDataSyncUtilService = createDecorator<IUserDataSyncUtilService>('IUserDataSyncUtilService');
export interface IUserDataSyncUtilService {

View File

@@ -3,9 +3,12 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc';
import { Event } from 'vs/base/common/event';
import { IUserDataSyncService } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserDataSyncService, IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync';
import { URI } from 'vs/base/common/uri';
import { IStringDictionary } from 'vs/base/common/collections';
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
export class UserDataSyncChannel implements IServerChannel {
@@ -30,3 +33,38 @@ export class UserDataSyncChannel implements IServerChannel {
throw new Error('Invalid call');
}
}
export class UserDataSycnUtilServiceChannel implements IServerChannel {
constructor(private readonly service: IUserDataSyncUtilService) { }
listen(_: unknown, event: string): Event<any> {
throw new Error(`Event not found: ${event}`);
}
call(context: any, command: string, args?: any): Promise<any> {
switch (command) {
case 'resolveUserKeybindings': return this.service.resolveUserBindings(args[0]);
case 'resolveFormattingOptions': return this.service.resolveFormattingOptions(URI.revive(args[0]));
}
throw new Error('Invalid call');
}
}
export class UserDataSyncUtilServiceClient implements IUserDataSyncUtilService {
_serviceBrand: undefined;
constructor(private readonly channel: IChannel) {
}
async resolveUserBindings(userbindings: string[]): Promise<IStringDictionary<string>> {
return this.channel.call('resolveUserKeybindings', [userbindings]);
}
async resolveFormattingOptions(file: URI): Promise<FormattingOptions> {
return this.channel.call('resolveFormattingOptions', [file]);
}
}

View File

@@ -0,0 +1,500 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { ISyncExtension } from 'vs/platform/userDataSync/common/userDataSync';
import { merge } from 'vs/platform/userDataSync/common/extensionsMerge';
suite('ExtensionsMerge - No Conflicts', () => {
test('merge returns local extension if remote does not exist', async () => {
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
];
const actual = merge(localExtensions, null, null, [], []);
assert.deepEqual(actual.added, []);
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.updated, []);
assert.deepEqual(actual.remote, localExtensions);
});
test('merge returns local extension if remote does not exist with ignored extensions', async () => {
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
];
const expected: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
];
const actual = merge(localExtensions, null, null, [], ['a']);
assert.deepEqual(actual.added, []);
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.updated, []);
assert.deepEqual(actual.remote, expected);
});
test('merge returns local extension if remote does not exist with ignored extensions (ignore case)', async () => {
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
];
const expected: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
];
const actual = merge(localExtensions, null, null, [], ['A']);
assert.deepEqual(actual.added, []);
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.updated, []);
assert.deepEqual(actual.remote, expected);
});
test('merge returns local extension if remote does not exist with skipped extensions', async () => {
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
];
const skippedExtension: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
];
const expected: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
];
const actual = merge(localExtensions, null, null, skippedExtension, []);
assert.deepEqual(actual.added, []);
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.updated, []);
assert.deepEqual(actual.remote, expected);
});
test('merge returns local extension if remote does not exist with skipped and ignored extensions', async () => {
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
];
const skippedExtension: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
];
const expected: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
];
const actual = merge(localExtensions, null, null, skippedExtension, ['a']);
assert.deepEqual(actual.added, []);
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.updated, []);
assert.deepEqual(actual.remote, expected);
});
test('merge local and remote extensions when there is no base', async () => {
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
];
const expected: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
];
const actual = merge(localExtensions, remoteExtensions, null, [], []);
assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, enabled: true }, { identifier: { id: 'c', uuid: 'c' }, enabled: true }]);
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.updated, []);
assert.deepEqual(actual.remote, expected);
});
test('merge local and remote extensions when there is no base and with ignored extensions', async () => {
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
];
const expected: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
];
const actual = merge(localExtensions, remoteExtensions, null, [], ['a']);
assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, enabled: true }, { identifier: { id: 'c', uuid: 'c' }, enabled: true }]);
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.updated, []);
assert.deepEqual(actual.remote, expected);
});
test('merge local and remote extensions when remote is moved forwarded', async () => {
const baseExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
];
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []);
assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, enabled: true }, { identifier: { id: 'c', uuid: 'c' }, enabled: true }]);
assert.deepEqual(actual.removed, [{ id: 'a', uuid: 'a' }, { id: 'd', uuid: 'd' }]);
assert.deepEqual(actual.updated, []);
assert.equal(actual.remote, null);
});
test('merge local and remote extensions when remote moved forwarded with ignored extensions', async () => {
const baseExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
];
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], ['a']);
assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, enabled: true }, { identifier: { id: 'c', uuid: 'c' }, enabled: true }]);
assert.deepEqual(actual.removed, [{ id: 'd', uuid: 'd' }]);
assert.deepEqual(actual.updated, []);
assert.equal(actual.remote, null);
});
test('merge local and remote extensions when remote is moved forwarded with skipped extensions', async () => {
const baseExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
];
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
];
const skippedExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, []);
assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, enabled: true }, { identifier: { id: 'c', uuid: 'c' }, enabled: true }]);
assert.deepEqual(actual.removed, [{ id: 'd', uuid: 'd' }]);
assert.deepEqual(actual.updated, []);
assert.equal(actual.remote, null);
});
test('merge local and remote extensions when remote is moved forwarded with skipped and ignored extensions', async () => {
const baseExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
];
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
];
const skippedExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, ['b']);
assert.deepEqual(actual.added, [{ identifier: { id: 'c', uuid: 'c' }, enabled: true }]);
assert.deepEqual(actual.removed, [{ id: 'd', uuid: 'd' }]);
assert.deepEqual(actual.updated, []);
assert.equal(actual.remote, null);
});
test('merge local and remote extensions when local is moved forwarded', async () => {
const baseExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
];
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []);
assert.deepEqual(actual.added, []);
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.updated, []);
assert.deepEqual(actual.remote, localExtensions);
});
test('merge local and remote extensions when local is moved forwarded with ignored settings', async () => {
const baseExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
];
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], ['b']);
assert.deepEqual(actual.added, []);
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.updated, []);
assert.deepEqual(actual.remote, [
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
]);
});
test('merge local and remote extensions when local is moved forwarded with skipped extensions', async () => {
const baseExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
];
const skippedExtensions: ISyncExtension[] = [
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
];
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
];
const expected: ISyncExtension[] = [
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, []);
assert.deepEqual(actual.added, []);
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.updated, []);
assert.deepEqual(actual.remote, expected);
});
test('merge local and remote extensions when local is moved forwarded with skipped and ignored extensions', async () => {
const baseExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
];
const skippedExtensions: ISyncExtension[] = [
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
];
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
];
const expected: ISyncExtension[] = [
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, ['c']);
assert.deepEqual(actual.added, []);
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.updated, []);
assert.deepEqual(actual.remote, expected);
});
test('merge local and remote extensions when both moved forwarded', async () => {
const baseExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
];
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'e', uuid: 'e' }, enabled: true },
];
const expected: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'e', uuid: 'e' }, enabled: true },
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []);
assert.deepEqual(actual.added, [{ identifier: { id: 'e', uuid: 'e' }, enabled: true }]);
assert.deepEqual(actual.removed, [{ id: 'a', uuid: 'a' }]);
assert.deepEqual(actual.updated, []);
assert.deepEqual(actual.remote, expected);
});
test('merge local and remote extensions when both moved forwarded with ignored extensions', async () => {
const baseExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
];
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'e', uuid: 'e' }, enabled: true },
];
const expected: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'e', uuid: 'e' }, enabled: true },
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], ['a', 'e']);
assert.deepEqual(actual.added, []);
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.updated, []);
assert.deepEqual(actual.remote, expected);
});
test('merge local and remote extensions when both moved forwarded with skipped extensions', async () => {
const baseExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
];
const skippedExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
];
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'e', uuid: 'e' }, enabled: true },
];
const expected: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'e', uuid: 'e' }, enabled: true },
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, []);
assert.deepEqual(actual.added, [{ identifier: { id: 'e', uuid: 'e' }, enabled: true }]);
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.updated, []);
assert.deepEqual(actual.remote, expected);
});
test('merge local and remote extensions when both moved forwarded with skipped and ignoredextensions', async () => {
const baseExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
];
const skippedExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
];
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'e', uuid: 'e' }, enabled: true },
];
const expected: ISyncExtension[] = [
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'e', uuid: 'e' }, enabled: true },
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
];
const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, ['e']);
assert.deepEqual(actual.added, []);
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.updated, []);
assert.deepEqual(actual.remote, expected);
});
test('merge when remote extension has no uuid and different extension id case', async () => {
const localExtensions: ISyncExtension[] = [
{ identifier: { id: 'a', uuid: 'a' }, enabled: true },
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
];
const remoteExtensions: ISyncExtension[] = [
{ identifier: { id: 'A' }, enabled: true },
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
];
const expected: ISyncExtension[] = [
{ identifier: { id: 'A' }, enabled: true },
{ identifier: { id: 'd', uuid: 'd' }, enabled: true },
{ identifier: { id: 'b', uuid: 'b' }, enabled: true },
{ identifier: { id: 'c', uuid: 'c' }, enabled: true },
];
const actual = merge(localExtensions, remoteExtensions, null, [], []);
assert.deepEqual(actual.added, [{ identifier: { id: 'd', uuid: 'd' }, enabled: true }]);
assert.deepEqual(actual.removed, []);
assert.deepEqual(actual.updated, []);
assert.deepEqual(actual.remote, expected);
});
});

View File

@@ -6,7 +6,6 @@
import * as assert from 'assert';
import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge';
import { IStringDictionary } from 'vs/base/common/collections';
import { OperatingSystem, OS } from 'vs/base/common/platform';
import { IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync';
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
import { URI } from 'vs/base/common/uri';
@@ -441,7 +440,7 @@ suite('KeybindingsMerge - No Conflicts', () => {
});
suite.skip('KeybindingsMerge - Conflicts', () => {
suite('KeybindingsMerge - Conflicts', () => {
test('merge when local and remote with one entry but different value', async () => {
const localContent = stringify([{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' }]);
@@ -730,6 +729,6 @@ class MockUserDataSyncUtilService implements IUserDataSyncUtilService {
}
async resolveFormattingOptions(file?: URI): Promise<FormattingOptions> {
return { eol: OS === OperatingSystem.Windows ? '\r\n' : '\n', insertSpaces: false, tabSize: 4 };
return { eol: '\n', insertSpaces: false, tabSize: 4 };
}
}

View File

@@ -0,0 +1,547 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { merge, computeRemoteContent } from 'vs/platform/userDataSync/common/settingsMerge';
const formattingOptions = { eol: '\n', insertSpaces: false, tabSize: 4 };
suite('SettingsMerge - No Conflicts', () => {
test('merge when local and remote are same with one entry', async () => {
const localContent = stringify({ 'a': 1 });
const remoteContent = stringify({ 'a': 1 });
const actual = merge(localContent, remoteContent, null, [], formattingOptions);
assert.ok(!actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, localContent);
});
test('merge when local and remote are same with multiple entries', async () => {
const localContent = stringify({
'a': 1,
'b': 2
});
const remoteContent = stringify({
'a': 1,
'b': 2
});
const actual = merge(localContent, remoteContent, null, [], formattingOptions);
assert.ok(!actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, localContent);
});
test('merge when local and remote are same with multiple entries in different order', async () => {
const localContent = stringify({
'b': 2,
'a': 1,
});
const remoteContent = stringify({
'a': 1,
'b': 2
});
const actual = merge(localContent, remoteContent, null, [], formattingOptions);
assert.ok(!actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, localContent);
});
test('merge when local and remote are same with different base content', async () => {
const localContent = stringify({
'b': 2,
'a': 1,
});
const baseContent = stringify({
'a': 2,
'b': 1
});
const remoteContent = stringify({
'a': 1,
'b': 2
});
const actual = merge(localContent, remoteContent, baseContent, [], formattingOptions);
assert.ok(!actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, localContent);
});
test('merge when a new entry is added to remote', async () => {
const localContent = stringify({
'a': 1,
});
const remoteContent = stringify({
'a': 1,
'b': 2
});
const actual = merge(localContent, remoteContent, null, [], formattingOptions);
assert.ok(actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, remoteContent);
});
test('merge when multiple new entries are added to remote', async () => {
const localContent = stringify({
'a': 1,
});
const remoteContent = stringify({
'b': 2,
'a': 1,
'c': 3,
});
const expected = stringify({
'a': 1,
'b': 2,
'c': 3,
});
const actual = merge(localContent, remoteContent, null, [], formattingOptions);
assert.ok(actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, expected);
});
test('merge when multiple new entries are added to remote from base and local has not changed', async () => {
const localContent = stringify({
'a': 1,
});
const remoteContent = stringify({
'b': 2,
'a': 1,
'c': 3,
});
const expected = stringify({
'a': 1,
'b': 2,
'c': 3,
});
const actual = merge(localContent, remoteContent, localContent, [], formattingOptions);
assert.ok(actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, expected);
});
test('merge when an entry is removed from remote from base and local has not changed', async () => {
const localContent = stringify({
'a': 1,
'b': 2,
});
const remoteContent = stringify({
'a': 1,
});
const actual = merge(localContent, remoteContent, localContent, [], formattingOptions);
assert.ok(actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, remoteContent);
});
test('merge when all entries are removed from base and local has not changed', async () => {
const localContent = stringify({
'a': 1,
});
const remoteContent = stringify({});
const actual = merge(localContent, remoteContent, localContent, [], formattingOptions);
assert.ok(actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.deepEqual(JSON.parse(actual.mergeContent), {});
});
test('merge when an entry is updated in remote from base and local has not changed', async () => {
const localContent = stringify({
'a': 1,
});
const remoteContent = stringify({
'a': 2
});
const actual = merge(localContent, remoteContent, localContent, [], formattingOptions);
assert.ok(actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, remoteContent);
});
test('merge when remote has moved forwareded with multiple changes and local stays with base', async () => {
const localContent = stringify({
'a': 1,
});
const remoteContent = stringify({
'a': 2,
'b': 1,
'c': 3,
'd': 4,
});
const actual = merge(localContent, remoteContent, localContent, [], formattingOptions);
assert.ok(actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, remoteContent);
});
test('merge when a new entries are added to local', async () => {
const localContent = stringify({
'a': 1,
'b': 2,
'c': 3,
'd': 4,
});
const remoteContent = stringify({
'a': 1,
});
const actual = merge(localContent, remoteContent, null, [], formattingOptions);
assert.ok(actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, localContent);
});
test('merge when multiple new entries are added to local from base and remote is not changed', async () => {
const localContent = stringify({
'a': 2,
'b': 1,
'c': 3,
'd': 4,
});
const remoteContent = stringify({
'a': 1,
});
const actual = merge(localContent, remoteContent, remoteContent, [], formattingOptions);
assert.ok(actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, localContent);
});
test('merge when an entry is removed from local from base and remote has not changed', async () => {
const localContent = stringify({
'a': 1,
'c': 2
});
const remoteContent = stringify({
'a': 2,
'b': 1,
'c': 3,
'd': 4,
});
const actual = merge(localContent, remoteContent, remoteContent, [], formattingOptions);
assert.ok(actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, localContent);
});
test('merge when an entry is updated in local from base and remote has not changed', async () => {
const localContent = stringify({
'a': 1,
'c': 2
});
const remoteContent = stringify({
'a': 2,
'c': 2,
});
const actual = merge(localContent, remoteContent, remoteContent, [], formattingOptions);
assert.ok(actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, localContent);
});
test('merge when local has moved forwarded with multiple changes and remote stays with base', async () => {
const localContent = stringify({
'a': 2,
'b': 1,
'c': 3,
'd': 4,
});
const remoteContent = stringify({
'a': 1,
});
const actual = merge(localContent, remoteContent, remoteContent, [], formattingOptions);
assert.ok(actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, localContent);
});
});
suite('SettingsMerge - Conflicts', () => {
test('merge when local and remote with one entry but different value', async () => {
const localContent = stringify({
'a': 1
});
const remoteContent = stringify({
'a': 2
});
const actual = merge(localContent, remoteContent, null, [], formattingOptions);
assert.ok(actual.hasChanges);
assert.ok(actual.hasConflicts);
assert.equal(actual.mergeContent,
`{
<<<<<<< local
"a": 1
=======
"a": 2,
>>>>>>> remote
}`);
});
test('merge when the entry is removed in remote but updated in local and a new entry is added in remote', async () => {
const baseContent = stringify({
'a': 1
});
const localContent = stringify({
'a': 2
});
const remoteContent = stringify({
'b': 2
});
const actual = merge(localContent, remoteContent, baseContent, [], formattingOptions);
assert.ok(actual.hasChanges);
assert.ok(actual.hasConflicts);
assert.equal(actual.mergeContent,
`{
<<<<<<< local
"a": 2,
=======
>>>>>>> remote
"b": 2
}`);
});
test('merge with single entry and local is empty', async () => {
const baseContent = stringify({
'a': 1
});
const localContent = stringify({});
const remoteContent = stringify({
'a': 2
});
const actual = merge(localContent, remoteContent, baseContent, [], formattingOptions);
assert.ok(actual.hasChanges);
assert.ok(actual.hasConflicts);
assert.equal(actual.mergeContent,
`{
<<<<<<< local
=======
"a": 2,
>>>>>>> remote
}`);
});
test('merge when local and remote has moved forwareded with conflicts', async () => {
const baseContent = stringify({
'a': 1,
'b': 2,
'c': 3,
'd': 4,
});
const localContent = stringify({
'a': 2,
'c': 3,
'd': 5,
'e': 4,
'f': 1,
});
const remoteContent = stringify({
'b': 3,
'c': 3,
'd': 6,
'e': 5,
});
const actual = merge(localContent, remoteContent, baseContent, [], formattingOptions);
assert.ok(actual.hasChanges);
assert.ok(actual.hasConflicts);
assert.equal(actual.mergeContent,
`{
<<<<<<< local
"a": 2,
=======
>>>>>>> remote
"c": 3,
<<<<<<< local
"d": 5,
=======
"d": 6,
>>>>>>> remote
<<<<<<< local
"e": 4,
=======
"e": 5,
>>>>>>> remote
"f": 1
<<<<<<< local
=======
"b": 3,
>>>>>>> remote
}`);
});
});
suite('SettingsMerge - Ignored Settings', () => {
test('ignored setting is not merged when changed in local and remote', async () => {
const localContent = stringify({ 'a': 1 });
const remoteContent = stringify({ 'a': 2 });
const actual = merge(localContent, remoteContent, null, ['a'], formattingOptions);
assert.ok(!actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, localContent);
});
test('ignored setting is not merged when changed in local and remote from base', async () => {
const baseContent = stringify({ 'a': 0 });
const localContent = stringify({ 'a': 1 });
const remoteContent = stringify({ 'a': 2 });
const actual = merge(localContent, remoteContent, baseContent, ['a'], formattingOptions);
assert.ok(!actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, localContent);
});
test('ignored setting is not merged when added in remote', async () => {
const localContent = stringify({});
const remoteContent = stringify({ 'a': 1 });
const actual = merge(localContent, remoteContent, null, ['a'], formattingOptions);
assert.ok(!actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, localContent);
});
test('ignored setting is not merged when added in remote from base', async () => {
const localContent = stringify({ 'b': 2 });
const remoteContent = stringify({ 'a': 1, 'b': 2 });
const actual = merge(localContent, remoteContent, localContent, ['a'], formattingOptions);
assert.ok(!actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, localContent);
});
test('ignored setting is not merged when removed in remote', async () => {
const localContent = stringify({ 'a': 1 });
const remoteContent = stringify({});
const actual = merge(localContent, remoteContent, null, ['a'], formattingOptions);
assert.ok(!actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, localContent);
});
test('ignored setting is not merged when removed in remote from base', async () => {
const localContent = stringify({ 'a': 2 });
const remoteContent = stringify({});
const actual = merge(localContent, remoteContent, localContent, ['a'], formattingOptions);
assert.ok(!actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, localContent);
});
test('ignored setting is not merged with other changes without conflicts', async () => {
const baseContent = stringify({
'a': 2,
'b': 2,
'c': 3,
'd': 4,
'e': 5,
});
const localContent = stringify({
'a': 1,
'b': 2,
'c': 3,
});
const remoteContent = stringify({
'a': 3,
'b': 3,
'd': 4,
'e': 6,
});
const expectedContent = stringify({
'a': 1,
'b': 3,
});
const actual = merge(localContent, remoteContent, baseContent, ['a', 'e'], formattingOptions);
assert.ok(actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, expectedContent);
});
test('ignored setting is not merged with other changes conflicts', async () => {
const baseContent = stringify({
'a': 2,
'b': 2,
'c': 3,
'd': 4,
'e': 5,
});
const localContent = stringify({
'a': 1,
'b': 4,
'c': 3,
'd': 5,
});
const remoteContent = stringify({
'a': 3,
'b': 3,
'e': 6,
});
const actual = merge(localContent, remoteContent, baseContent, ['a', 'e'], formattingOptions);
//'{\n\t"a": 1,\n\n<<<<<<< local\t"b": 4,\n=======\n\t"b": 3,\n>>>>>>> remote'
//'{\n\t"a": 1,\n<<<<<<< local\n\t"b": 4,\n=======\n\t"b": 3,\n>>>>>>> remote\n<<<<<<< local\n\t"d": 5\n=======\n>>>>>>> remote\n}'
assert.ok(actual.hasChanges);
assert.ok(actual.hasChanges);
assert.ok(actual.hasConflicts);
assert.equal(actual.mergeContent,
`{
"a": 1,
<<<<<<< local
"b": 4,
=======
"b": 3,
>>>>>>> remote
<<<<<<< local
"d": 5
=======
>>>>>>> remote
}`);
});
});
suite('SettingsMerge - Compute Remote Content', () => {
test('local content is returned when there are no ignored settings', async () => {
const localContent = stringify({
'a': 1,
'b': 2,
'c': 3,
});
const remoteContent = stringify({
'a': 3,
'b': 3,
'd': 4,
'e': 6,
});
const actual = computeRemoteContent(localContent, remoteContent, [], formattingOptions);
assert.equal(actual, localContent);
});
test('ignored settings are not updated from remote content', async () => {
const localContent = stringify({
'a': 1,
'b': 2,
'c': 3,
});
const remoteContent = stringify({
'a': 3,
'b': 3,
'd': 4,
'e': 6,
});
const expected = stringify({
'a': 3,
'b': 2,
'c': 3,
});
const actual = computeRemoteContent(localContent, remoteContent, ['a'], formattingOptions);
assert.equal(actual, expected);
});
});
function stringify(value: any): string {
return JSON.stringify(value, null, '\t');
}

View File

@@ -91,7 +91,7 @@ export interface IWindowSettings {
titleBarStyle: 'native' | 'custom';
autoDetectHighContrast: boolean;
menuBarVisibility: MenuBarVisibility;
newWindowDimensions: 'default' | 'inherit' | 'maximized' | 'fullscreen';
newWindowDimensions: 'default' | 'inherit' | 'offset' | 'maximized' | 'fullscreen';
nativeTabs: boolean;
nativeFullScreen: boolean;
enableMenuBarMnemonics: boolean;

View File

@@ -1381,7 +1381,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
// Window state is not from a previous session: only allow fullscreen if we inherit it or user wants fullscreen
let allowFullscreen: boolean;
if (state.hasDefaultState) {
allowFullscreen = (windowConfig?.newWindowDimensions && ['fullscreen', 'inherit'].indexOf(windowConfig.newWindowDimensions) >= 0);
allowFullscreen = (windowConfig?.newWindowDimensions && ['fullscreen', 'inherit', 'offset'].indexOf(windowConfig.newWindowDimensions) >= 0);
}
// Window state is from a previous session: only allow fullscreen when we got updated or user wants to restore
@@ -1576,7 +1576,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
} else if (windowConfig.newWindowDimensions === 'fullscreen') {
state.mode = WindowMode.Fullscreen;
ensureNoOverlap = false;
} else if (windowConfig.newWindowDimensions === 'inherit' && lastActive) {
} else if ((windowConfig.newWindowDimensions === 'inherit' || windowConfig.newWindowDimensions === 'offset') && lastActive) {
const lastActiveState = lastActive.serializeWindowState();
if (lastActiveState.mode === WindowMode.Fullscreen) {
state.mode = WindowMode.Fullscreen; // only take mode (fixes https://github.com/Microsoft/vscode/issues/19331)
@@ -1584,7 +1584,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
state = lastActiveState;
}
ensureNoOverlap = false;
ensureNoOverlap = state.mode !== WindowMode.Fullscreen && windowConfig.newWindowDimensions === 'offset';
}
}