Merge from vscode a5cf1da01d5db3d2557132be8d30f89c38019f6c (#8525)

* Merge from vscode a5cf1da01d5db3d2557132be8d30f89c38019f6c

* remove files we don't want

* fix hygiene

* update distro

* update distro

* fix hygiene

* fix strict nulls

* distro

* distro

* fix tests

* fix tests

* add another edit

* fix viewlet icon

* fix azure dialog

* fix some padding

* fix more padding issues
This commit is contained in:
Anthony Dresser
2019-12-04 19:28:22 -08:00
committed by GitHub
parent a8818ab0df
commit f5ce7fb2a5
1507 changed files with 42813 additions and 27370 deletions

View File

@@ -0,0 +1,53 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { JSONPath } from 'vs/base/common/json';
import { setProperty } from 'vs/base/common/jsonEdit';
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
export function edit(content: string, originalPath: JSONPath, value: any, formattingOptions: FormattingOptions): string {
const edit = setProperty(content, originalPath, value, formattingOptions)[0];
if (edit) {
content = content.substring(0, edit.offset) + edit.content + content.substring(edit.offset + edit.length);
}
return content;
}
export function getLineStartOffset(content: string, eol: string, atOffset: number): number {
let lineStartingOffset = atOffset;
while (lineStartingOffset >= 0) {
if (content.charAt(lineStartingOffset) === eol.charAt(eol.length - 1)) {
if (eol.length === 1) {
return lineStartingOffset + 1;
}
}
lineStartingOffset--;
if (eol.length === 2) {
if (lineStartingOffset >= 0 && content.charAt(lineStartingOffset) === eol.charAt(0)) {
return lineStartingOffset + 2;
}
}
}
return 0;
}
export function getLineEndOffset(content: string, eol: string, atOffset: number): number {
let lineEndOffset = atOffset;
while (lineEndOffset >= 0) {
if (content.charAt(lineEndOffset) === eol.charAt(eol.length - 1)) {
if (eol.length === 1) {
return lineEndOffset;
}
}
lineEndOffset++;
if (eol.length === 2) {
if (lineEndOffset >= 0 && content.charAt(lineEndOffset) === eol.charAt(1)) {
return lineEndOffset;
}
}
}
return content.length - 1;
}

View File

@@ -69,11 +69,14 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
}
async sync(): Promise<boolean> {
if (!this.configurationService.getValue<boolean>('configurationSync.enableExtensions')) {
if (!this.configurationService.getValue<boolean>('sync.enableExtensions')) {
this.logService.trace('Extensions: Skipping synchronizing extensions as it is disabled.');
return false;
}
if (!this.extensionGalleryService.isEnabled()) {
this.logService.trace('Extensions: Skipping synchronizing extensions as gallery is disabled.');
return false;
}
if (this.status !== SyncStatus.Idle) {
this.logService.trace('Extensions: Skipping synchronizing extensions as it is running already.');
return false;
@@ -105,7 +108,7 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
return this.replaceQueue.queue(async () => {
const remoteData = await this.userDataSyncStoreService.read(ExtensionsSynchroniser.EXTERNAL_USER_DATA_EXTENSIONS_KEY, null);
const remoteExtensions: ISyncExtension[] = remoteData.content ? JSON.parse(remoteData.content) : [];
const ignoredExtensions = this.configurationService.getValue<string[]>('configurationSync.extensionsToIgnore') || [];
const ignoredExtensions = this.configurationService.getValue<string[]>('sync.ignoredExtensions') || [];
const removedExtensions = remoteExtensions.filter(e => !ignoredExtensions.some(id => areSameExtensions({ id }, e.identifier)) && areSameExtensions(e.identifier, identifier));
if (removedExtensions.length) {
for (const removedExtension of removedExtensions) {
@@ -159,11 +162,11 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser
* - 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): { added: ISyncExtension[], removed: IExtensionIdentifier[], updated: ISyncExtension[], remote: ISyncExtension[] | null } {
const ignoredExtensions = this.configurationService.getValue<string[]>('configurationSync.extensionsToIgnore') || [];
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.some(id => id.toLowerCase() === identifier.id.toLowerCase())) };
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>();

View File

@@ -0,0 +1,367 @@
/*---------------------------------------------------------------------------------------------
* 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 } from 'vs/base/common/json';
import { values, keys } from 'vs/base/common/map';
import { IUserFriendlyKeybinding } from 'vs/platform/keybinding/common/keybinding';
import { firstIndex as findFirstIndex, equals } from 'vs/base/common/arrays';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import * as contentUtil from 'vs/platform/userDataSync/common/content';
import { IStringDictionary } from 'vs/base/common/collections';
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
import { IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync';
interface ICompareResult {
added: Set<string>;
removed: Set<string>;
updated: Set<string>;
}
interface IMergeResult {
hasLocalForwarded: boolean;
hasRemoteForwarded: boolean;
added: Set<string>;
removed: Set<string>;
updated: Set<string>;
conflicts: Set<string>;
}
export async function merge(localContent: string, remoteContent: string, baseContent: string | null, formattingOptions: FormattingOptions, userDataSyncUtilService: IUserDataSyncUtilService): Promise<{ mergeContent: string, hasChanges: boolean, hasConflicts: boolean }> {
const local = <IUserFriendlyKeybinding[]>parse(localContent);
const remote = <IUserFriendlyKeybinding[]>parse(remoteContent);
const base = baseContent ? <IUserFriendlyKeybinding[]>parse(baseContent) : null;
const userbindings: string[] = [...local, ...remote, ...(base || [])].map(keybinding => keybinding.key);
const normalizedKeys = await userDataSyncUtilService.resolveUserBindings(userbindings);
let keybindingsMergeResult = computeMergeResultByKeybinding(local, remote, base, normalizedKeys);
if (!keybindingsMergeResult.hasLocalForwarded && !keybindingsMergeResult.hasRemoteForwarded) {
// No changes found between local and remote.
return { mergeContent: localContent, hasChanges: false, hasConflicts: false };
}
if (!keybindingsMergeResult.hasLocalForwarded && keybindingsMergeResult.hasRemoteForwarded) {
return { mergeContent: remoteContent, hasChanges: true, hasConflicts: false };
}
if (keybindingsMergeResult.hasLocalForwarded && !keybindingsMergeResult.hasRemoteForwarded) {
// Local has moved forward and remote has not. Return local.
return { mergeContent: localContent, hasChanges: true, hasConflicts: false };
}
// Both local and remote has moved forward.
const localByCommand = byCommand(local);
const remoteByCommand = byCommand(remote);
const baseByCommand = base ? byCommand(base) : null;
const localToRemoteByCommand = compareByCommand(localByCommand, remoteByCommand, normalizedKeys);
const baseToLocalByCommand = baseByCommand ? compareByCommand(baseByCommand, localByCommand, normalizedKeys) : { added: keys(localByCommand).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
const baseToRemoteByCommand = baseByCommand ? compareByCommand(baseByCommand, remoteByCommand, normalizedKeys) : { added: keys(remoteByCommand).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
const commandsMergeResult = computeMergeResult(localToRemoteByCommand, baseToLocalByCommand, baseToRemoteByCommand);
let mergeContent = localContent;
// Removed commands in Remote
for (const command of values(commandsMergeResult.removed)) {
if (commandsMergeResult.conflicts.has(command)) {
continue;
}
mergeContent = removeKeybindings(mergeContent, command, formattingOptions);
}
// Added commands in remote
for (const command of values(commandsMergeResult.added)) {
if (commandsMergeResult.conflicts.has(command)) {
continue;
}
const keybindings = remoteByCommand.get(command)!;
// Ignore negated commands
if (keybindings.some(keybinding => keybinding.command !== `-${command}` && keybindingsMergeResult.conflicts.has(normalizedKeys[keybinding.key]))) {
commandsMergeResult.conflicts.add(command);
continue;
}
mergeContent = addKeybindings(mergeContent, keybindings, formattingOptions);
}
// Updated commands in Remote
for (const command of values(commandsMergeResult.updated)) {
if (commandsMergeResult.conflicts.has(command)) {
continue;
}
const keybindings = remoteByCommand.get(command)!;
// Ignore negated commands
if (keybindings.some(keybinding => keybinding.command !== `-${command}` && keybindingsMergeResult.conflicts.has(normalizedKeys[keybinding.key]))) {
commandsMergeResult.conflicts.add(command);
continue;
}
mergeContent = updateKeybindings(mergeContent, command, keybindings, formattingOptions);
}
const hasConflicts = commandsMergeResult.conflicts.size > 0;
if (hasConflicts) {
mergeContent = `<<<<<<< local${formattingOptions.eol}`
+ mergeContent
+ `${formattingOptions.eol}=======${formattingOptions.eol}`
+ remoteContent
+ `${formattingOptions.eol}>>>>>>> remote`;
}
return { mergeContent, hasChanges: true, hasConflicts };
}
function computeMergeResult(localToRemote: ICompareResult, baseToLocal: ICompareResult, baseToRemote: ICompareResult): { added: Set<string>, removed: Set<string>, updated: Set<string>, conflicts: Set<string> } {
const added: Set<string> = new Set<string>();
const removed: Set<string> = new Set<string>();
const updated: Set<string> = new Set<string>();
const conflicts: Set<string> = new Set<string>();
// Removed keys in Local
for (const key of values(baseToLocal.removed)) {
// Got updated in remote
if (baseToRemote.updated.has(key)) {
conflicts.add(key);
}
}
// Removed keys 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 {
// remove the key
removed.add(key);
}
}
// Added keys 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 keys 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 {
added.add(key);
}
}
// Updated keys 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 keys 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 {
// updated key
updated.add(key);
}
}
return { added, removed, updated, conflicts };
}
function computeMergeResultByKeybinding(local: IUserFriendlyKeybinding[], remote: IUserFriendlyKeybinding[], base: IUserFriendlyKeybinding[] | null, normalizedKeys: IStringDictionary<string>): IMergeResult {
const empty = new Set<string>();
const localByKeybinding = byKeybinding(local, normalizedKeys);
const remoteByKeybinding = byKeybinding(remote, normalizedKeys);
const baseByKeybinding = base ? byKeybinding(base, normalizedKeys) : null;
const localToRemoteByKeybinding = compareByKeybinding(localByKeybinding, remoteByKeybinding);
if (localToRemoteByKeybinding.added.size === 0 && localToRemoteByKeybinding.removed.size === 0 && localToRemoteByKeybinding.updated.size === 0) {
return { hasLocalForwarded: false, hasRemoteForwarded: false, added: empty, removed: empty, updated: empty, conflicts: empty };
}
const baseToLocalByKeybinding = baseByKeybinding ? compareByKeybinding(baseByKeybinding, localByKeybinding) : { added: keys(localByKeybinding).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
if (baseToLocalByKeybinding.added.size === 0 && baseToLocalByKeybinding.removed.size === 0 && baseToLocalByKeybinding.updated.size === 0) {
// Remote has moved forward and local has not.
return { hasLocalForwarded: false, hasRemoteForwarded: true, added: empty, removed: empty, updated: empty, conflicts: empty };
}
const baseToRemoteByKeybinding = baseByKeybinding ? compareByKeybinding(baseByKeybinding, remoteByKeybinding) : { added: keys(remoteByKeybinding).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
if (baseToRemoteByKeybinding.added.size === 0 && baseToRemoteByKeybinding.removed.size === 0 && baseToRemoteByKeybinding.updated.size === 0) {
return { hasLocalForwarded: true, hasRemoteForwarded: false, added: empty, removed: empty, updated: empty, conflicts: empty };
}
const { added, removed, updated, conflicts } = computeMergeResult(localToRemoteByKeybinding, baseToLocalByKeybinding, baseToRemoteByKeybinding);
return { hasLocalForwarded: true, hasRemoteForwarded: true, added, removed, updated, conflicts };
}
function byKeybinding(keybindings: IUserFriendlyKeybinding[], keys: IStringDictionary<string>) {
const map: Map<string, IUserFriendlyKeybinding[]> = new Map<string, IUserFriendlyKeybinding[]>();
for (const keybinding of keybindings) {
const key = keys[keybinding.key];
let value = map.get(key);
if (!value) {
value = [];
map.set(key, value);
}
value.push(keybinding);
}
return map;
}
function byCommand(keybindings: IUserFriendlyKeybinding[]): Map<string, IUserFriendlyKeybinding[]> {
const map: Map<string, IUserFriendlyKeybinding[]> = new Map<string, IUserFriendlyKeybinding[]>();
for (const keybinding of keybindings) {
const command = keybinding.command[0] === '-' ? keybinding.command.substring(1) : keybinding.command;
let value = map.get(command);
if (!value) {
value = [];
map.set(command, value);
}
value.push(keybinding);
}
return map;
}
function compareByKeybinding(from: Map<string, IUserFriendlyKeybinding[]>, to: Map<string, IUserFriendlyKeybinding[]>): ICompareResult {
const fromKeys = keys(from);
const toKeys = keys(to);
const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
const updated: Set<string> = new Set<string>();
for (const key of fromKeys) {
if (removed.has(key)) {
continue;
}
const value1: IUserFriendlyKeybinding[] = from.get(key)!.map(keybinding => ({ ...keybinding, ...{ key } }));
const value2: IUserFriendlyKeybinding[] = to.get(key)!.map(keybinding => ({ ...keybinding, ...{ key } }));
if (!equals(value1, value2, (a, b) => isSameKeybinding(a, b))) {
updated.add(key);
}
}
return { added, removed, updated };
}
function compareByCommand(from: Map<string, IUserFriendlyKeybinding[]>, to: Map<string, IUserFriendlyKeybinding[]>, normalizedKeys: IStringDictionary<string>): ICompareResult {
const fromKeys = keys(from);
const toKeys = keys(to);
const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
const updated: Set<string> = new Set<string>();
for (const key of fromKeys) {
if (removed.has(key)) {
continue;
}
const value1: IUserFriendlyKeybinding[] = from.get(key)!.map(keybinding => ({ ...keybinding, ...{ key: normalizedKeys[keybinding.key] } }));
const value2: IUserFriendlyKeybinding[] = to.get(key)!.map(keybinding => ({ ...keybinding, ...{ key: normalizedKeys[keybinding.key] } }));
if (!areSameKeybindingsWithSameCommand(value1, value2)) {
updated.add(key);
}
}
return { added, removed, updated };
}
function areSameKeybindingsWithSameCommand(value1: IUserFriendlyKeybinding[], value2: IUserFriendlyKeybinding[]): boolean {
// Compare entries adding keybindings
if (!equals(value1.filter(({ command }) => command[0] !== '-'), value2.filter(({ command }) => command[0] !== '-'), (a, b) => isSameKeybinding(a, b))) {
return false;
}
// Compare entries removing keybindings
if (!equals(value1.filter(({ command }) => command[0] === '-'), value2.filter(({ command }) => command[0] === '-'), (a, b) => isSameKeybinding(a, b))) {
return false;
}
return true;
}
function isSameKeybinding(a: IUserFriendlyKeybinding, b: IUserFriendlyKeybinding): boolean {
if (a.command !== b.command) {
return false;
}
if (a.key !== b.key) {
return false;
}
const whenA = ContextKeyExpr.deserialize(a.when);
const whenB = ContextKeyExpr.deserialize(b.when);
if ((whenA && !whenB) || (!whenA && whenB)) {
return false;
}
if (whenA && whenB && !whenA.equals(whenB)) {
return false;
}
if (!objects.equals(a.args, b.args)) {
return false;
}
return true;
}
function addKeybindings(content: string, keybindings: IUserFriendlyKeybinding[], formattingOptions: FormattingOptions): string {
for (const keybinding of keybindings) {
content = contentUtil.edit(content, [-1], keybinding, formattingOptions);
}
return content;
}
function removeKeybindings(content: string, command: string, formattingOptions: FormattingOptions): string {
const keybindings = <IUserFriendlyKeybinding[]>parse(content);
for (let index = keybindings.length - 1; index >= 0; index--) {
if (keybindings[index].command === command || keybindings[index].command === `-${command}`) {
content = contentUtil.edit(content, [index], undefined, formattingOptions);
}
}
return content;
}
function updateKeybindings(content: string, command: string, keybindings: IUserFriendlyKeybinding[], formattingOptions: FormattingOptions): string {
const allKeybindings = <IUserFriendlyKeybinding[]>parse(content);
const location = findFirstIndex(allKeybindings, keybinding => keybinding.command === command || keybinding.command === `-${command}`);
// Remove all entries with this command
for (let index = allKeybindings.length - 1; index >= 0; index--) {
if (allKeybindings[index].command === command || allKeybindings[index].command === `-${command}`) {
content = contentUtil.edit(content, [index], undefined, formattingOptions);
}
}
// add all entries at the same location where the entry with this command was located.
for (let index = keybindings.length - 1; index >= 0; index--) {
content = contentUtil.edit(content, [location], keybindings[index], formattingOptions);
}
return content;
}

View File

@@ -0,0 +1,348 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { IFileService, FileSystemProviderErrorCode, FileSystemProviderError, IFileContent } from 'vs/platform/files/common/files';
import { IUserData, UserDataSyncStoreError, UserDataSyncStoreErrorCode, ISynchroniser, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync';
import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge';
import { VSBuffer } from 'vs/base/common/buffer';
import { parse, ParseError } from 'vs/base/common/json';
import { localize } from 'vs/nls';
import { Emitter, Event } from 'vs/base/common/event';
import { CancelablePromise, createCancelablePromise, ThrottledDelayer } from 'vs/base/common/async';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { URI } from 'vs/base/common/uri';
import { joinPath } from 'vs/base/common/resources';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { CancellationToken } from 'vs/base/common/cancellation';
import { OS, OperatingSystem } from 'vs/base/common/platform';
import { isUndefined } from 'vs/base/common/types';
interface ISyncContent {
mac?: string;
linux?: string;
windows?: string;
all?: string;
}
interface ISyncPreviewResult {
readonly fileContent: IFileContent | null;
readonly remoteUserData: IUserData;
readonly hasLocalChanged: boolean;
readonly hasRemoteChanged: boolean;
readonly hasConflicts: boolean;
}
export class KeybindingsSynchroniser extends Disposable implements ISynchroniser {
private static EXTERNAL_USER_DATA_KEYBINDINGS_KEY: string = 'keybindings';
private syncPreviewResultPromise: CancelablePromise<ISyncPreviewResult> | null = null;
private _status: SyncStatus = SyncStatus.Idle;
get status(): SyncStatus { return this._status; }
private _onDidChangStatus: Emitter<SyncStatus> = this._register(new Emitter<SyncStatus>());
readonly onDidChangeStatus: Event<SyncStatus> = this._onDidChangStatus.event;
private readonly throttledDelayer: ThrottledDelayer<void>;
private _onDidChangeLocal: Emitter<void> = this._register(new Emitter<void>());
readonly onDidChangeLocal: Event<void> = this._onDidChangeLocal.event;
private readonly lastSyncKeybindingsResource: URI;
constructor(
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IFileService private readonly fileService: IFileService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IUserDataSyncUtilService private readonly userDataSyncUtilService: IUserDataSyncUtilService,
) {
super();
this.lastSyncKeybindingsResource = joinPath(this.environmentService.userRoamingDataHome, '.lastSyncKeybindings.json');
this.throttledDelayer = this._register(new ThrottledDelayer<void>(500));
this._register(this.fileService.watch(this.environmentService.keybindingsResource));
this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.environmentService.keybindingsResource))(() => this.throttledDelayer.trigger(() => this.onDidChangeKeybindings())));
}
private async onDidChangeKeybindings(): Promise<void> {
const localFileContent = await this.getLocalContent();
const lastSyncData = await this.getLastSyncUserData();
if (localFileContent && lastSyncData) {
if (localFileContent.value.toString() !== lastSyncData.content) {
this._onDidChangeLocal.fire();
return;
}
}
if (!localFileContent || !lastSyncData) {
this._onDidChangeLocal.fire();
return;
}
}
private setStatus(status: SyncStatus): void {
if (this._status !== status) {
this._status = status;
this._onDidChangStatus.fire(status);
}
}
async sync(_continue?: boolean): Promise<boolean> {
if (!this.configurationService.getValue<boolean>('sync.enableKeybindings')) {
this.logService.trace('Keybindings: Skipping synchronizing keybindings as it is disabled.');
return false;
}
if (_continue) {
this.logService.info('Keybindings: Resumed synchronizing keybindings');
return this.continueSync();
}
if (this.status !== SyncStatus.Idle) {
this.logService.trace('Keybindings: Skipping synchronizing keybindings as it is running already.');
return false;
}
this.logService.trace('Keybindings: Started synchronizing keybindings...');
this.setStatus(SyncStatus.Syncing);
try {
const result = await this.getPreview();
if (result.hasConflicts) {
this.logService.info('Keybindings: Detected conflicts while synchronizing keybindings.');
this.setStatus(SyncStatus.HasConflicts);
return false;
}
await this.apply();
return true;
} catch (e) {
this.syncPreviewResultPromise = null;
this.setStatus(SyncStatus.Idle);
if (e instanceof UserDataSyncStoreError && e.code === UserDataSyncStoreErrorCode.Rejected) {
// Rejected as there is a new remote version. Syncing again,
this.logService.info('Keybindings: Failed to synchronise keybindings as there is a new remote version available. Synchronizing again...');
return this.sync();
}
if (e instanceof FileSystemProviderError && e.code === FileSystemProviderErrorCode.FileExists) {
// Rejected as there is a new local version. Syncing again.
this.logService.info('Keybindings: Failed to synchronise keybindings as there is a new local version available. Synchronizing again...');
return this.sync();
}
throw e;
}
}
stop(): void {
if (this.syncPreviewResultPromise) {
this.syncPreviewResultPromise.cancel();
this.syncPreviewResultPromise = null;
this.logService.info('Keybindings: Stopped synchronizing keybindings.');
}
this.fileService.del(this.environmentService.keybindingsSyncPreviewResource);
this.setStatus(SyncStatus.Idle);
}
private async continueSync(): Promise<boolean> {
if (this.status !== SyncStatus.HasConflicts) {
return false;
}
await this.apply();
return true;
}
private async apply(): Promise<void> {
if (!this.syncPreviewResultPromise) {
return;
}
if (await this.fileService.exists(this.environmentService.keybindingsSyncPreviewResource)) {
const keybindingsPreivew = await this.fileService.readFile(this.environmentService.keybindingsSyncPreviewResource);
const content = keybindingsPreivew.value.toString();
if (this.hasErrors(content)) {
const error = new Error(localize('errorInvalidKeybindings', "Unable to sync keybindings. Please resolve conflicts without any errors/warnings and try again."));
this.logService.error(error);
throw error;
}
let { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged } = await this.syncPreviewResultPromise;
if (!hasLocalChanged && !hasRemoteChanged) {
this.logService.trace('Keybindings: No changes found during synchronizing keybindings.');
}
if (hasLocalChanged) {
this.logService.info('Keybindings: Updating local keybindings');
await this.updateLocalContent(content, fileContent);
}
if (hasRemoteChanged) {
this.logService.info('Keybindings: Updating remote keybindings');
const remoteContents = this.updateSyncContent(content, remoteUserData.content);
const ref = await this.updateRemoteUserData(remoteContents, remoteUserData.ref);
remoteUserData = { ref, content: remoteContents };
}
if (remoteUserData.content) {
this.logService.info('Keybindings: Updating last synchronised keybindings');
const lastSyncContent = this.updateSyncContent(content, null);
await this.updateLastSyncUserData({ ref: remoteUserData.ref, content: lastSyncContent });
}
// Delete the preview
await this.fileService.del(this.environmentService.keybindingsSyncPreviewResource);
} else {
this.logService.trace('Keybindings: No changes found during synchronizing keybindings.');
}
this.logService.trace('Keybindings: Finised synchronizing keybindings.');
this.syncPreviewResultPromise = null;
this.setStatus(SyncStatus.Idle);
}
private hasErrors(content: string): boolean {
const parseErrors: ParseError[] = [];
parse(content, parseErrors, { allowEmptyContent: true, allowTrailingComma: true });
return parseErrors.length > 0;
}
private getPreview(): Promise<ISyncPreviewResult> {
if (!this.syncPreviewResultPromise) {
this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(token));
}
return this.syncPreviewResultPromise;
}
private async generatePreview(token: CancellationToken): Promise<ISyncPreviewResult> {
const lastSyncData = await this.getLastSyncUserData();
const lastSyncContent = lastSyncData && lastSyncData.content ? this.getKeybindingsContentFromSyncContent(lastSyncData.content) : null;
const remoteUserData = await this.getRemoteUserData(lastSyncData);
const remoteContent = remoteUserData.content ? this.getKeybindingsContentFromSyncContent(remoteUserData.content) : null;
// Get file content last to get the latest
const fileContent = await this.getLocalContent();
let hasLocalChanged: boolean = false;
let hasRemoteChanged: boolean = false;
let hasConflicts: boolean = false;
let previewContent = null;
if (remoteContent) {
const localContent: string = fileContent ? fileContent.value.toString() : '[]';
if (this.hasErrors(localContent)) {
this.logService.error('Keybindings: Unable to sync keybindings as there are errors/warning in keybindings file.');
return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts };
}
if (!lastSyncContent // First time sync
|| lastSyncContent !== localContent // Local has forwarded
|| 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 result = await merge(localContent, remoteContent, lastSyncContent, formattingOptions, this.userDataSyncUtilService);
// Sync only if there are changes
if (result.hasChanges) {
hasLocalChanged = result.mergeContent !== localContent;
hasRemoteChanged = result.mergeContent !== remoteContent;
hasConflicts = result.hasConflicts;
previewContent = result.mergeContent;
}
}
}
// First time syncing to remote
else if (fileContent) {
this.logService.info('Keybindings: Remote keybindings does not exist. Synchronizing keybindings for the first time.');
hasRemoteChanged = true;
previewContent = fileContent.value.toString();
}
if (previewContent && !token.isCancellationRequested) {
await this.fileService.writeFile(this.environmentService.keybindingsSyncPreviewResource, VSBuffer.fromString(previewContent));
}
return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts };
}
private async getLocalContent(): Promise<IFileContent | null> {
try {
return await this.fileService.readFile(this.environmentService.keybindingsResource);
} catch (error) {
return null;
}
}
private async updateLocalContent(newContent: string, oldContent: IFileContent | null): Promise<void> {
if (oldContent) {
// file exists already
await this.fileService.writeFile(this.environmentService.keybindingsResource, VSBuffer.fromString(newContent), oldContent);
} else {
// file does not exist
await this.fileService.createFile(this.environmentService.keybindingsResource, VSBuffer.fromString(newContent), { overwrite: false });
}
}
private async getLastSyncUserData(): Promise<IUserData | null> {
try {
const content = await this.fileService.readFile(this.lastSyncKeybindingsResource);
return JSON.parse(content.value.toString());
} catch (error) {
return null;
}
}
private async updateLastSyncUserData(remoteUserData: IUserData): Promise<void> {
await this.fileService.writeFile(this.lastSyncKeybindingsResource, VSBuffer.fromString(JSON.stringify(remoteUserData)));
}
private async getRemoteUserData(lastSyncData: IUserData | null): Promise<IUserData> {
return this.userDataSyncStoreService.read(KeybindingsSynchroniser.EXTERNAL_USER_DATA_KEYBINDINGS_KEY, lastSyncData);
}
private async updateRemoteUserData(content: string, ref: string | null): Promise<string> {
return this.userDataSyncStoreService.write(KeybindingsSynchroniser.EXTERNAL_USER_DATA_KEYBINDINGS_KEY, content, ref);
}
private getKeybindingsContentFromSyncContent(syncContent: string): string | null {
try {
const parsed = <ISyncContent>JSON.parse(syncContent);
if (!this.configurationService.getValue<boolean>('sync.keybindingsPerPlatform')) {
return isUndefined(parsed.all) ? null : parsed.all;
}
switch (OS) {
case OperatingSystem.Macintosh:
return isUndefined(parsed.mac) ? null : parsed.mac;
case OperatingSystem.Linux:
return isUndefined(parsed.linux) ? null : parsed.linux;
case OperatingSystem.Windows:
return isUndefined(parsed.windows) ? null : parsed.windows;
}
} catch (e) {
this.logService.error(e);
return null;
}
}
private updateSyncContent(keybindingsContent: string, syncContent: string | null): string {
let parsed: ISyncContent = {};
try {
parsed = JSON.parse(syncContent || '{}');
} catch (e) {
this.logService.error(e);
}
if (!this.configurationService.getValue<boolean>('sync.keybindingsPerPlatform')) {
parsed.all = keybindingsContent;
} else {
delete parsed.all;
}
switch (OS) {
case OperatingSystem.Macintosh:
parsed.mac = keybindingsContent;
break;
case OperatingSystem.Linux:
parsed.linux = keybindingsContent;
break;
case OperatingSystem.Windows:
parsed.windows = keybindingsContent;
break;
}
return JSON.stringify(parsed);
}
}

View File

@@ -0,0 +1,45 @@
/*---------------------------------------------------------------------------------------------
* 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

@@ -80,7 +80,7 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser {
}
async sync(_continue?: boolean): Promise<boolean> {
if (!this.configurationService.getValue<boolean>('configurationSync.enableSettings')) {
if (!this.configurationService.getValue<boolean>('sync.enableSettings')) {
this.logService.trace('Settings: Skipping synchronizing settings as it is disabled.');
return false;
}
@@ -135,10 +135,9 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser {
}
private async continueSync(): Promise<boolean> {
if (this.status !== SyncStatus.HasConflicts) {
return false;
if (this.status === SyncStatus.HasConflicts) {
await this.apply();
}
await this.apply();
return true;
}
@@ -153,7 +152,7 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser {
if (this.hasErrors(content)) {
const error = new Error(localize('errorInvalidSettings', "Unable to sync settings. Please resolve conflicts without any errors/warnings and try again."));
this.logService.error(error);
return Promise.reject(error);
throw error;
}
let { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged } = await this.syncPreviewResultPromise;
@@ -188,7 +187,7 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser {
private hasErrors(content: string): boolean {
const parseErrors: ParseError[] = [];
parse(content, parseErrors);
parse(content, parseErrors, { allowEmptyContent: true, allowTrailingComma: true });
return parseErrors.length > 0;
}
@@ -218,8 +217,8 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser {
}
if (!lastSyncData // First time sync
|| lastSyncData.content !== localContent // Local has moved forwarded
|| lastSyncData.content !== remoteContent // Remote has moved forwarded
|| lastSyncData.content !== localContent // Local has forwarded
|| 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());
@@ -248,13 +247,23 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser {
}
private getIgnoredSettings(settingsContent?: string): string[] {
const value: string[] = (settingsContent ? parse(settingsContent)['configurationSync.settingsToIgnore'] : this.configurationService.getValue<string[]>('configurationSync.settingsToIgnore')) || [];
let value: string[] = [];
if (settingsContent) {
const setting = parse(settingsContent);
if (setting) {
value = setting['sync.ignoredSettings'];
}
} else {
value = this.configurationService.getValue<string[]>('sync.ignoredSettings');
}
const added: string[] = [], removed: string[] = [];
for (const key of value) {
if (startsWith(key, '-')) {
removed.push(key.substring(1));
} else {
added.push(key);
if (Array.isArray(value)) {
for (const key of value) {
if (startsWith(key, '-')) {
removed.push(key.substring(1));
} else {
added.push(key);
}
}
}
return [...DEFAULT_IGNORED_SETTINGS, ...added].filter(setting => removed.indexOf(setting) === -1);

View File

@@ -0,0 +1,69 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IUserDataSyncService, SyncStatus, IUserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSync';
import { Disposable } from 'vs/base/common/lifecycle';
import { Event } from 'vs/base/common/event';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { timeout } from 'vs/base/common/async';
import { IAuthTokenService, AuthTokenStatus } from 'vs/platform/auth/common/auth';
export class UserDataAutoSync extends Disposable {
private enabled: boolean = false;
constructor(
@IConfigurationService private readonly configurationService: IConfigurationService,
@IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService,
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@IAuthTokenService private readonly authTokenService: IAuthTokenService,
) {
super();
this.updateEnablement(false);
this._register(Event.any<any>(authTokenService.onDidChangeStatus, userDataSyncService.onDidChangeStatus)(() => this.updateEnablement(true)));
this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('sync.enable'))(() => this.updateEnablement(true)));
}
private updateEnablement(stopIfDisabled: boolean): void {
const enabled = this.isSyncEnabled();
if (this.enabled === enabled) {
return;
}
this.enabled = enabled;
if (this.enabled) {
this.logService.info('Syncing configuration started');
this.sync(true);
return;
} else {
if (stopIfDisabled) {
this.userDataSyncService.stop();
this.logService.info('Syncing configuration stopped.');
}
}
}
protected async sync(loop: boolean): Promise<void> {
if (this.enabled) {
try {
await this.userDataSyncService.sync();
} catch (e) {
this.logService.error(e);
}
if (loop) {
await timeout(1000 * 60 * 5); // Loop sync for every 5 min.
this.sync(loop);
}
}
}
private isSyncEnabled(): boolean {
return this.configurationService.getValue<boolean>('sync.enable')
&& this.userDataSyncService.status !== SyncStatus.Uninitialized
&& this.authTokenService.status === AuthTokenStatus.SignedIn;
}
}

View File

@@ -14,61 +14,74 @@ import { IDisposable } from 'vs/base/common/lifecycle';
import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { ILogService } from 'vs/platform/log/common/log';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IStringDictionary } from 'vs/base/common/collections';
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
import { URI } from 'vs/base/common/uri';
const CONFIGURATION_SYNC_STORE_KEY = 'configurationSync.store';
export const DEFAULT_IGNORED_SETTINGS = [
'configurationSync.enable',
'configurationSync.enableSettings',
'configurationSync.enableExtensions',
CONFIGURATION_SYNC_STORE_KEY,
'sync.enable',
'sync.enableSettings',
'sync.enableExtensions',
];
export function registerConfiguration(): IDisposable {
const ignoredSettingsSchemaId = 'vscode://schemas/ignoredSettings';
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
configurationRegistry.registerConfiguration({
id: 'configurationSync',
id: 'sync',
order: 30,
title: localize('configurationSync', "Configuration Sync"),
title: localize('sync', "Sync"),
type: 'object',
properties: {
'configurationSync.enable': {
'sync.enable': {
type: 'boolean',
description: localize('configurationSync.enable', "When enabled, synchronises configuration that includes Settings and Extensions."),
default: true,
description: localize('sync.enable', "Enable synchronization."),
default: false,
scope: ConfigurationScope.APPLICATION
},
'configurationSync.enableSettings': {
'sync.enableSettings': {
type: 'boolean',
description: localize('configurationSync.enableSettings', "When enabled settings are synchronised while synchronizing configuration."),
description: localize('sync.enableSettings', "Enable synchronizing settings."),
default: true,
scope: ConfigurationScope.APPLICATION,
},
'configurationSync.enableExtensions': {
'sync.enableExtensions': {
type: 'boolean',
description: localize('configurationSync.enableExtensions', "When enabled extensions are synchronised while synchronizing configuration."),
description: localize('sync.enableExtensions', "Enable synchronizing extensions."),
default: true,
scope: ConfigurationScope.APPLICATION,
},
'configurationSync.extensionsToIgnore': {
'sync.enableKeybindings': {
type: 'boolean',
description: localize('sync.enableKeybindings', "Enable synchronizing keybindings."),
default: true,
scope: ConfigurationScope.APPLICATION,
},
'sync.keybindingsPerPlatform': {
type: 'boolean',
description: localize('sync.keybindingsPerPlatform', "Synchronize keybindings per platform."),
default: true,
scope: ConfigurationScope.APPLICATION,
},
'sync.ignoredExtensions': {
'type': 'array',
description: localize('configurationSync.extensionsToIgnore', "Configure extensions to be ignored while syncing."),
description: localize('sync.ignoredExtensions', "Configure extensions to be ignored while synchronizing."),
'default': [],
'scope': ConfigurationScope.APPLICATION,
uniqueItems: true
},
'configurationSync.settingsToIgnore': {
'sync.ignoredSettings': {
'type': 'array',
description: localize('configurationSync.settingsToIgnore', "Configure settings to be ignored while syncing. \nDefault Ignored Settings:\n\n{0}", DEFAULT_IGNORED_SETTINGS.sort().map(setting => `- ${setting}`).join('\n')),
description: localize('sync.ignoredSettings', "Configure settings to be ignored while synchronizing. \nDefault Ignored Settings:\n\n{0}", DEFAULT_IGNORED_SETTINGS.sort().map(setting => `- ${setting}`).join('\n')),
'default': [],
'scope': ConfigurationScope.APPLICATION,
$ref: ignoredSettingsSchemaId,
additionalProperties: true,
uniqueItems: true
},
'configurationSync.enableAuth': {
'type': 'boolean',
description: localize('configurationSync.enableAuth', "Enables authentication and requires VS Code restart when changed"),
'default': false,
'scope': ConfigurationScope.APPLICATION
}
}
});
@@ -104,12 +117,23 @@ export class UserDataSyncStoreError extends Error {
}
export interface IUserDataSyncStore {
url: string;
name: string;
account: string;
}
export function getUserDataSyncStore(configurationService: IConfigurationService): IUserDataSyncStore | undefined {
const value = configurationService.getValue<IUserDataSyncStore>(CONFIGURATION_SYNC_STORE_KEY);
return value && value.url && value.name && value.account ? value : undefined;
}
export const IUserDataSyncStoreService = createDecorator<IUserDataSyncStoreService>('IUserDataSyncStoreService');
export interface IUserDataSyncStoreService {
_serviceBrand: undefined;
readonly enabled: boolean;
readonly userDataSyncStore: IUserDataSyncStore | undefined;
read(key: string, oldValue: IUserData | null): Promise<IUserData>;
write(key: string, content: string, ref: string | null): Promise<string>;
@@ -123,6 +147,7 @@ export interface ISyncExtension {
export const enum SyncSource {
Settings = 1,
Keybindings,
Extensions
}
@@ -164,6 +189,18 @@ export interface ISettingsMergeService {
}
export const IUserDataSyncUtilService = createDecorator<IUserDataSyncUtilService>('IUserDataSyncUtilService');
export interface IUserDataSyncUtilService {
_serviceBrand: undefined;
resolveUserBindings(userbindings: string[]): Promise<IStringDictionary<string>>;
resolveFormattingOptions(resource: URI): Promise<FormattingOptions>;
}
export const IUserDataSyncLogService = createDecorator<IUserDataSyncLogService>('IUserDataSyncLogService');
export interface IUserDataSyncLogService extends ILogService {

View File

@@ -3,16 +3,15 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IUserDataSyncService, SyncStatus, ISynchroniser, IUserDataSyncStoreService, SyncSource, IUserDataSyncLogService, UserDataSyncStoreError, UserDataSyncStoreErrorCode } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserDataSyncService, SyncStatus, ISynchroniser, IUserDataSyncStoreService, SyncSource } from 'vs/platform/userDataSync/common/userDataSync';
import { Disposable } from 'vs/base/common/lifecycle';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync';
import { Emitter, Event } from 'vs/base/common/event';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { timeout } from 'vs/base/common/async';
import { ExtensionsSynchroniser } from 'vs/platform/userDataSync/common/extensionsSync';
import { IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { IAuthTokenService, AuthTokenStatus } from 'vs/platform/auth/common/auth';
import { KeybindingsSynchroniser } from 'vs/platform/userDataSync/common/keybindingsSync';
export class UserDataSyncService extends Disposable implements IUserDataSyncService {
@@ -31,6 +30,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
get conflictsSource(): SyncSource | null { return this._conflictsSource; }
private readonly settingsSynchroniser: SettingsSynchroniser;
private readonly keybindingsSynchroniser: KeybindingsSynchroniser;
private readonly extensionsSynchroniser: ExtensionsSynchroniser;
constructor(
@@ -40,20 +40,25 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
) {
super();
this.settingsSynchroniser = this._register(this.instantiationService.createInstance(SettingsSynchroniser));
this.keybindingsSynchroniser = this._register(this.instantiationService.createInstance(KeybindingsSynchroniser));
this.extensionsSynchroniser = this._register(this.instantiationService.createInstance(ExtensionsSynchroniser));
this.synchronisers = [this.settingsSynchroniser, this.extensionsSynchroniser];
this.synchronisers = [this.settingsSynchroniser, this.keybindingsSynchroniser, this.extensionsSynchroniser];
this.updateStatus();
this._register(Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeStatus, () => undefined)))(() => this.updateStatus()));
if (this.userDataSyncStoreService.userDataSyncStore) {
this._register(Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeStatus, () => undefined)))(() => this.updateStatus()));
this._register(authTokenService.onDidChangeStatus(() => this.onDidChangeAuthTokenStatus()));
}
this.onDidChangeLocal = Event.any(...this.synchronisers.map(s => s.onDidChangeLocal));
this._register(authTokenService.onDidChangeStatus(() => this.onDidChangeAuthTokenStatus()));
}
async sync(_continue?: boolean): Promise<boolean> {
if (!this.userDataSyncStoreService.enabled) {
if (!this.userDataSyncStoreService.userDataSyncStore) {
throw new Error('Not enabled');
}
if (this.authTokenService.status === AuthTokenStatus.Inactive) {
return Promise.reject('Not Authenticated. Please sign in to start sync.');
if (this.authTokenService.status === AuthTokenStatus.SignedOut) {
throw new Error('Not Authenticated. Please sign in to start sync.');
}
for (const synchroniser of this.synchronisers) {
if (!await synchroniser.sync(_continue)) {
@@ -64,7 +69,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
}
stop(): void {
if (!this.userDataSyncStoreService.enabled) {
if (!this.userDataSyncStoreService.userDataSyncStore) {
throw new Error('Not enabled');
}
for (const synchroniser of this.synchronisers) {
@@ -89,7 +94,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
}
private computeStatus(): SyncStatus {
if (!this.userDataSyncStoreService.enabled) {
if (!this.userDataSyncStoreService.userDataSyncStore) {
return SyncStatus.Uninitialized;
}
if (this.synchronisers.some(s => s.status === SyncStatus.HasConflicts)) {
@@ -107,83 +112,17 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
if (source instanceof SettingsSynchroniser) {
return SyncSource.Settings;
}
if (source instanceof KeybindingsSynchroniser) {
return SyncSource.Keybindings;
}
}
return null;
}
private onDidChangeAuthTokenStatus(): void {
if (this.authTokenService.status === AuthTokenStatus.Inactive) {
if (this.authTokenService.status === AuthTokenStatus.SignedOut) {
this.stop();
}
}
}
export class UserDataAutoSync extends Disposable {
private enabled: boolean = false;
constructor(
@IConfigurationService private readonly configurationService: IConfigurationService,
@IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService,
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@IAuthTokenService private readonly authTokenService: IAuthTokenService,
) {
super();
this.updateEnablement(false);
this._register(Event.any<any>(authTokenService.onDidChangeStatus, userDataSyncService.onDidChangeStatus)(() => this.updateEnablement(true)));
this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('configurationSync.enable'))(() => this.updateEnablement(true)));
// Sync immediately if there is a local change.
this._register(Event.debounce(this.userDataSyncService.onDidChangeLocal, () => undefined, 500)(() => this.sync(false)));
}
private updateEnablement(stopIfDisabled: boolean): void {
const enabled = this.isSyncEnabled();
if (this.enabled === enabled) {
return;
}
this.enabled = enabled;
if (this.enabled) {
this.logService.info('Syncing configuration started');
this.sync(true);
return;
} else {
if (stopIfDisabled) {
this.userDataSyncService.stop();
this.logService.info('Syncing configuration stopped.');
}
}
}
private async sync(loop: boolean): Promise<void> {
if (this.enabled) {
try {
await this.userDataSyncService.sync();
} catch (e) {
if (e instanceof UserDataSyncStoreError && e.code === UserDataSyncStoreErrorCode.Unauthroized) {
if (e instanceof UserDataSyncStoreError && e.code === UserDataSyncStoreErrorCode.Unauthroized && this.authTokenService.status === AuthTokenStatus.Disabled) {
this.logService.error('Sync failed because the server requires authorization. Please enable authorization.');
} else {
this.logService.error(e);
}
}
this.logService.error(e);
}
if (loop) {
await timeout(1000 * 5); // Loop sync for every 5s.
this.sync(loop);
}
}
}
private isSyncEnabled(): boolean {
return this.configurationService.getValue<boolean>('configurationSync.enable')
&& this.userDataSyncService.status !== SyncStatus.Uninitialized
&& this.authTokenService.status !== AuthTokenStatus.Inactive;
}
}

View File

@@ -4,35 +4,36 @@
*--------------------------------------------------------------------------------------------*/
import { Disposable, } from 'vs/base/common/lifecycle';
import { IUserData, IUserDataSyncStoreService, UserDataSyncStoreErrorCode, UserDataSyncStoreError } from 'vs/platform/userDataSync/common/userDataSync';
import { IProductService } from 'vs/platform/product/common/productService';
import { IUserData, IUserDataSyncStoreService, UserDataSyncStoreErrorCode, UserDataSyncStoreError, IUserDataSyncStore, getUserDataSyncStore } from 'vs/platform/userDataSync/common/userDataSync';
import { IRequestService, asText, isSuccess } from 'vs/platform/request/common/request';
import { URI } from 'vs/base/common/uri';
import { joinPath } from 'vs/base/common/resources';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IHeaders, IRequestOptions, IRequestContext } from 'vs/base/parts/request/common/request';
import { IAuthTokenService, AuthTokenStatus } from 'vs/platform/auth/common/auth';
import { IAuthTokenService } from 'vs/platform/auth/common/auth';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
export class UserDataSyncStoreService extends Disposable implements IUserDataSyncStoreService {
_serviceBrand: any;
get enabled(): boolean { return !!this.productService.settingsSyncStoreUrl; }
readonly userDataSyncStore: IUserDataSyncStore | undefined;
constructor(
@IProductService private readonly productService: IProductService,
@IConfigurationService configurationService: IConfigurationService,
@IRequestService private readonly requestService: IRequestService,
@IAuthTokenService private readonly authTokenService: IAuthTokenService,
) {
super();
this.userDataSyncStore = getUserDataSyncStore(configurationService);
}
async read(key: string, oldValue: IUserData | null): Promise<IUserData> {
if (!this.enabled) {
return Promise.reject(new Error('No settings sync store url configured.'));
if (!this.userDataSyncStore) {
throw new Error('No settings sync store url configured.');
}
const url = joinPath(URI.parse(this.productService.settingsSyncStoreUrl!), 'resource', key, 'latest').toString();
const url = joinPath(URI.parse(this.userDataSyncStore.url), 'resource', key, 'latest').toString();
const headers: IHeaders = {};
if (oldValue) {
headers['If-None-Match'] = oldValue.ref;
@@ -58,11 +59,11 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
}
async write(key: string, data: string, ref: string | null): Promise<string> {
if (!this.enabled) {
return Promise.reject(new Error('No settings sync store url configured.'));
if (!this.userDataSyncStore) {
throw new Error('No settings sync store url configured.');
}
const url = joinPath(URI.parse(this.productService.settingsSyncStoreUrl!), 'resource', key).toString();
const url = joinPath(URI.parse(this.userDataSyncStore.url), 'resource', key).toString();
const headers: IHeaders = { 'Content-Type': 'text/plain' };
if (ref) {
headers['If-Match'] = ref;
@@ -87,21 +88,17 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
}
private async request(options: IRequestOptions, token: CancellationToken): Promise<IRequestContext> {
if (this.authTokenService.status !== AuthTokenStatus.Disabled) {
const authToken = await this.authTokenService.getToken();
if (!authToken) {
return Promise.reject(new Error('No Auth Token Available.'));
}
options.headers = options.headers || {};
options.headers['authorization'] = `Bearer ${authToken}`;
const authToken = await this.authTokenService.getToken();
if (!authToken) {
throw new Error('No Auth Token Available.');
}
options.headers = options.headers || {};
options.headers['authorization'] = `Bearer ${authToken}`;
const context = await this.requestService.request(options, token);
if (context.res.statusCode === 401) {
if (this.authTokenService.status !== AuthTokenStatus.Disabled) {
this.authTokenService.refreshToken();
}
this.authTokenService.refreshToken();
// Throw Unauthorized Error
throw new UserDataSyncStoreError('Unauthorized', UserDataSyncStoreErrorCode.Unauthroized);
}

View File

@@ -0,0 +1,32 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IUserDataSyncService, IUserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSync';
import { Event } from 'vs/base/common/event';
import { IElectronService } from 'vs/platform/electron/node/electron';
import { UserDataAutoSync as BaseUserDataAutoSync } from 'vs/platform/userDataSync/common/userDataAutoSync';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IAuthTokenService } from 'vs/platform/auth/common/auth';
export class UserDataAutoSync extends BaseUserDataAutoSync {
constructor(
@IUserDataSyncService userDataSyncService: IUserDataSyncService,
@IElectronService electronService: IElectronService,
@IConfigurationService configurationService: IConfigurationService,
@IUserDataSyncLogService logService: IUserDataSyncLogService,
@IAuthTokenService authTokenService: IAuthTokenService,
) {
super(configurationService, userDataSyncService, logService, authTokenService);
// Sync immediately if there is a local change.
this._register(Event.debounce(Event.any<any>(
electronService.onWindowFocus,
electronService.onWindowOpen,
userDataSyncService.onDidChangeLocal
), () => undefined, 500)(() => this.sync(false)));
}
}

View File

@@ -0,0 +1,735 @@
/*---------------------------------------------------------------------------------------------
* 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 } 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';
suite('KeybindingsMerge - No Conflicts', () => {
test('merge when local and remote are same with one entry', async () => {
const localContent = stringify([{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }]);
const remoteContent = stringify([{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }]);
const actual = await mergeKeybindings(localContent, remoteContent, null);
assert.ok(!actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, localContent);
});
test('merge when local and remote are same with similar when contexts', async () => {
const localContent = stringify([{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }]);
const remoteContent = stringify([{ key: 'alt+c', command: 'a', when: '!editorReadonly && editorTextFocus' }]);
const actual = await mergeKeybindings(localContent, remoteContent, null);
assert.ok(!actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, localContent);
});
test('merge when local and remote has entries in different order', async () => {
const localContent = stringify([
{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+a', command: 'a', when: 'editorTextFocus' }
]);
const remoteContent = stringify([
{ key: 'alt+a', command: 'a', when: 'editorTextFocus' },
{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' }
]);
const actual = await mergeKeybindings(localContent, remoteContent, null);
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([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
{ key: 'cmd+c', command: 'b', args: { text: '`' } }
]);
const remoteContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
{ key: 'cmd+c', command: 'b', args: { text: '`' } }
]);
const actual = await mergeKeybindings(localContent, remoteContent, null);
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([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
{ key: 'cmd+c', command: 'b', args: { text: '`' } }
]);
const baseContent = stringify([
{ key: 'ctrl+c', command: 'e' },
{ key: 'shift+d', command: 'd', args: { text: '`' } }
]);
const remoteContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
{ key: 'cmd+c', command: 'b', args: { text: '`' } }
]);
const actual = await mergeKeybindings(localContent, remoteContent, baseContent);
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([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
{ key: 'cmd+c', command: 'b', args: { text: '`' } }
]);
const remoteContent = stringify([
{ key: 'cmd+c', command: 'b', args: { text: '`' } },
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
]);
const actual = await mergeKeybindings(localContent, remoteContent, null);
assert.ok(!actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, localContent);
});
test('merge when local and remote are same when remove entry is in different order', async () => {
const localContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
{ key: 'cmd+c', command: 'b', args: { text: '`' } }
]);
const remoteContent = stringify([
{ key: 'alt+d', command: '-a' },
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'cmd+c', command: 'b', args: { text: '`' } },
]);
const actual = await mergeKeybindings(localContent, remoteContent, null);
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([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
]);
const remoteContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
{ key: 'cmd+c', command: 'b', args: { text: '`' } },
]);
const actual = await mergeKeybindings(localContent, remoteContent, null);
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([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
]);
const remoteContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
{ key: 'cmd+c', command: 'b', args: { text: '`' } },
{ key: 'cmd+d', command: 'c' },
]);
const actual = await mergeKeybindings(localContent, remoteContent, null);
assert.ok(actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, remoteContent);
});
test('merge when multiple new entries are added to remote from base and local has not changed', async () => {
const localContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
]);
const remoteContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
{ key: 'cmd+c', command: 'b', args: { text: '`' } },
{ key: 'cmd+d', command: 'c' },
]);
const actual = await mergeKeybindings(localContent, remoteContent, localContent);
assert.ok(actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, remoteContent);
});
test('merge when an entry is removed from remote from base and local has not changed', async () => {
const localContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
{ key: 'cmd+c', command: 'b', args: { text: '`' } },
]);
const remoteContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
]);
const actual = await mergeKeybindings(localContent, remoteContent, localContent);
assert.ok(actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, remoteContent);
});
test('merge when an entry (same command) is removed from remote from base and local has not changed', async () => {
const localContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
]);
const remoteContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
]);
const actual = await mergeKeybindings(localContent, remoteContent, localContent);
assert.ok(actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, remoteContent);
});
test('merge when an entry is updated in remote from base and local has not changed', async () => {
const localContent = stringify([
{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' },
]);
const remoteContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
]);
const actual = await mergeKeybindings(localContent, remoteContent, localContent);
assert.ok(actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, remoteContent);
});
test('merge when a command with multiple entries is updated from remote from base and local has not changed', async () => {
const localContent = stringify([
{ key: 'shift+c', command: 'c' },
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: 'b' },
{ key: 'cmd+c', command: 'a' },
]);
const remoteContent = stringify([
{ key: 'shift+c', command: 'c' },
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: 'b' },
{ key: 'cmd+d', command: 'a' },
]);
const actual = await mergeKeybindings(localContent, remoteContent, localContent);
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([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'cmd+c', command: 'b', args: { text: '`' } },
{ key: 'alt+d', command: '-a' },
{ key: 'cmd+e', command: 'd' },
{ key: 'cmd+d', command: 'c', when: 'context1' },
]);
const remoteContent = stringify([
{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'cmd+e', command: 'd' },
{ key: 'alt+d', command: '-a' },
{ key: 'alt+f', command: 'f' },
{ key: 'alt+d', command: '-f' },
{ key: 'cmd+d', command: 'c', when: 'context1' },
{ key: 'cmd+c', command: '-c' },
]);
const actual = await mergeKeybindings(localContent, remoteContent, localContent);
assert.ok(actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, remoteContent);
});
test('merge when a new entry is added to local', async () => {
const localContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
{ key: 'cmd+c', command: 'b', args: { text: '`' } },
]);
const remoteContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
]);
const actual = await mergeKeybindings(localContent, remoteContent, null);
assert.ok(actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, localContent);
});
test('merge when multiple new entries are added to local', async () => {
const localContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
{ key: 'cmd+c', command: 'b', args: { text: '`' } },
{ key: 'cmd+d', command: 'c' },
]);
const remoteContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
]);
const actual = await mergeKeybindings(localContent, remoteContent, null);
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([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
{ key: 'cmd+c', command: 'b', args: { text: '`' } },
{ key: 'cmd+d', command: 'c' },
]);
const remoteContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
]);
const actual = await mergeKeybindings(localContent, remoteContent, remoteContent);
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([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
]);
const remoteContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
{ key: 'cmd+c', command: 'b', args: { text: '`' } },
]);
const actual = await mergeKeybindings(localContent, remoteContent, remoteContent);
assert.ok(actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, localContent);
});
test('merge when an entry (with same command) is removed from local from base and remote has not changed', async () => {
const localContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
]);
const remoteContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
]);
const actual = await mergeKeybindings(localContent, remoteContent, remoteContent);
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([
{ key: 'alt+d', command: 'a', when: 'editorTextFocus' },
]);
const remoteContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
]);
const actual = await mergeKeybindings(localContent, remoteContent, remoteContent);
assert.ok(actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, localContent);
});
test('merge when a command with multiple entries is updated from local from base and remote has not changed', async () => {
const localContent = stringify([
{ key: 'shift+c', command: 'c' },
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: 'b' },
{ key: 'cmd+c', command: 'a' },
]);
const remoteContent = stringify([
{ key: 'shift+c', command: 'c' },
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: 'b' },
{ key: 'cmd+d', command: 'a' },
]);
const actual = await mergeKeybindings(localContent, remoteContent, remoteContent);
assert.ok(actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, localContent);
});
test('merge when local has moved forwareded with multiple changes and remote stays with base', async () => {
const localContent = stringify([
{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'cmd+e', command: 'd' },
{ key: 'alt+d', command: '-a' },
{ key: 'alt+f', command: 'f' },
{ key: 'alt+d', command: '-f' },
{ key: 'cmd+d', command: 'c', when: 'context1' },
{ key: 'cmd+c', command: '-c' },
]);
const remoteContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'cmd+c', command: 'b', args: { text: '`' } },
{ key: 'alt+d', command: '-a' },
{ key: 'cmd+e', command: 'd' },
{ key: 'cmd+d', command: 'c', when: 'context1' },
]);
const expected = stringify([
{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'cmd+e', command: 'd' },
{ key: 'alt+d', command: '-a' },
{ key: 'alt+f', command: 'f' },
{ key: 'alt+d', command: '-f' },
{ key: 'cmd+d', command: 'c', when: 'context1' },
{ key: 'cmd+c', command: '-c' },
]);
const actual = await mergeKeybindings(localContent, remoteContent, remoteContent);
assert.ok(actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, expected);
});
test('merge when local and remote has moved forwareded with conflicts', async () => {
const baseContent = stringify([
{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'ctrl+c', command: '-a' },
{ key: 'cmd+e', command: 'd' },
{ key: 'alt+a', command: 'f' },
{ key: 'alt+d', command: '-f' },
{ key: 'cmd+d', command: 'c', when: 'context1' },
{ key: 'cmd+c', command: '-c' },
]);
const localContent = stringify([
{ key: 'alt+d', command: '-f' },
{ key: 'cmd+e', command: 'd' },
{ key: 'cmd+c', command: '-c' },
{ key: 'cmd+d', command: 'c', when: 'context1' },
{ key: 'alt+a', command: 'f' },
{ key: 'alt+e', command: 'e' },
]);
const remoteContent = stringify([
{ key: 'alt+a', command: 'f' },
{ key: 'cmd+c', command: '-c' },
{ key: 'cmd+d', command: 'd' },
{ key: 'alt+d', command: '-f' },
{ key: 'alt+c', command: 'c', when: 'context1' },
{ key: 'alt+g', command: 'g', when: 'context2' },
]);
const expected = stringify([
{ key: 'alt+d', command: '-f' },
{ key: 'cmd+d', command: 'd' },
{ key: 'cmd+c', command: '-c' },
{ key: 'alt+c', command: 'c', when: 'context1' },
{ key: 'alt+a', command: 'f' },
{ key: 'alt+e', command: 'e' },
{ key: 'alt+g', command: 'g', when: 'context2' },
]);
const actual = await mergeKeybindings(localContent, remoteContent, baseContent);
assert.ok(actual.hasChanges);
assert.ok(!actual.hasConflicts);
assert.equal(actual.mergeContent, expected);
});
});
suite.skip('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' }]);
const remoteContent = stringify([{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }]);
const actual = await mergeKeybindings(localContent, remoteContent, null);
assert.ok(actual.hasChanges);
assert.ok(actual.hasConflicts);
assert.equal(actual.mergeContent,
`<<<<<<< local
[
{
"key": "alt+d",
"command": "a",
"when": "editorTextFocus && !editorReadonly"
}
]
=======
[
{
"key": "alt+c",
"command": "a",
"when": "editorTextFocus && !editorReadonly"
}
]
>>>>>>> remote`);
});
test('merge when local and remote with different keybinding', async () => {
const localContent = stringify([
{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+a', command: '-a', when: 'editorTextFocus && !editorReadonly' }
]);
const remoteContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+a', command: '-a', when: 'editorTextFocus && !editorReadonly' }
]);
const actual = await mergeKeybindings(localContent, remoteContent, null);
assert.ok(actual.hasChanges);
assert.ok(actual.hasConflicts);
assert.equal(actual.mergeContent,
`<<<<<<< local
[
{
"key": "alt+d",
"command": "a",
"when": "editorTextFocus && !editorReadonly"
},
{
"key": "alt+a",
"command": "-a",
"when": "editorTextFocus && !editorReadonly"
}
]
=======
[
{
"key": "alt+c",
"command": "a",
"when": "editorTextFocus && !editorReadonly"
},
{
"key": "alt+a",
"command": "-a",
"when": "editorTextFocus && !editorReadonly"
}
]
>>>>>>> remote`);
});
test('merge when the entry is removed in local but updated in remote', async () => {
const baseContent = stringify([{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' }]);
const localContent = stringify([]);
const remoteContent = stringify([{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }]);
const actual = await mergeKeybindings(localContent, remoteContent, baseContent);
assert.ok(actual.hasChanges);
assert.ok(actual.hasConflicts);
assert.equal(actual.mergeContent,
`<<<<<<< local
[]
=======
[
{
"key": "alt+c",
"command": "a",
"when": "editorTextFocus && !editorReadonly"
}
]
>>>>>>> remote`);
});
test('merge when the entry is removed in local but updated in remote and a new entry is added in local', async () => {
const baseContent = stringify([{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' }]);
const localContent = stringify([{ key: 'alt+b', command: 'b' }]);
const remoteContent = stringify([{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }]);
const actual = await mergeKeybindings(localContent, remoteContent, baseContent);
assert.ok(actual.hasChanges);
assert.ok(actual.hasConflicts);
assert.equal(actual.mergeContent,
`<<<<<<< local
[
{
"key": "alt+b",
"command": "b"
}
]
=======
[
{
"key": "alt+c",
"command": "a",
"when": "editorTextFocus && !editorReadonly"
}
]
>>>>>>> remote`);
});
test('merge when the entry is removed in remote but updated in local', async () => {
const baseContent = stringify([{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' }]);
const localContent = stringify([{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }]);
const remoteContent = stringify([]);
const actual = await mergeKeybindings(localContent, remoteContent, baseContent);
assert.ok(actual.hasChanges);
assert.ok(actual.hasConflicts);
assert.equal(actual.mergeContent,
`<<<<<<< local
[
{
"key": "alt+c",
"command": "a",
"when": "editorTextFocus && !editorReadonly"
}
]
=======
[]
>>>>>>> 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([{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' }]);
const localContent = stringify([{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }]);
const remoteContent = stringify([{ key: 'alt+b', command: 'b' }]);
const actual = await mergeKeybindings(localContent, remoteContent, baseContent);
assert.ok(actual.hasChanges);
assert.ok(actual.hasConflicts);
assert.equal(actual.mergeContent,
`<<<<<<< local
[
{
"key": "alt+c",
"command": "a",
"when": "editorTextFocus && !editorReadonly"
},
{
"key": "alt+b",
"command": "b"
}
]
=======
[
{
"key": "alt+b",
"command": "b"
}
]
>>>>>>> remote`);
});
test('merge when local and remote has moved forwareded with conflicts', async () => {
const baseContent = stringify([
{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+c', command: '-a' },
{ key: 'cmd+e', command: 'd' },
{ key: 'alt+a', command: 'f' },
{ key: 'alt+d', command: '-f' },
{ key: 'cmd+d', command: 'c', when: 'context1' },
{ key: 'cmd+c', command: '-c' },
]);
const localContent = stringify([
{ key: 'alt+d', command: '-f' },
{ key: 'cmd+e', command: 'd' },
{ key: 'cmd+c', command: '-c' },
{ key: 'cmd+d', command: 'c', when: 'context1' },
{ key: 'alt+a', command: 'f' },
{ key: 'alt+e', command: 'e' },
]);
const remoteContent = stringify([
{ key: 'alt+a', command: 'f' },
{ key: 'cmd+c', command: '-c' },
{ key: 'cmd+d', command: 'd' },
{ key: 'alt+d', command: '-f' },
{ key: 'alt+c', command: 'c', when: 'context1' },
{ key: 'alt+g', command: 'g', when: 'context2' },
]);
const actual = await mergeKeybindings(localContent, remoteContent, baseContent);
assert.ok(actual.hasChanges);
assert.ok(actual.hasConflicts);
assert.equal(actual.mergeContent,
`<<<<<<< local
[
{
"key": "alt+d",
"command": "-f"
},
{
"key": "cmd+d",
"command": "d"
},
{
"key": "cmd+c",
"command": "-c"
},
{
"key": "cmd+d",
"command": "c",
"when": "context1"
},
{
"key": "alt+a",
"command": "f"
},
{
"key": "alt+e",
"command": "e"
},
{
"key": "alt+g",
"command": "g",
"when": "context2"
}
]
=======
[
{
"key": "alt+a",
"command": "f"
},
{
"key": "cmd+c",
"command": "-c"
},
{
"key": "cmd+d",
"command": "d"
},
{
"key": "alt+d",
"command": "-f"
},
{
"key": "alt+c",
"command": "c",
"when": "context1"
},
{
"key": "alt+g",
"command": "g",
"when": "context2"
}
]
>>>>>>> remote`);
});
});
async function mergeKeybindings(localContent: string, remoteContent: string, baseContent: string | null) {
const userDataSyncUtilService = new MockUserDataSyncUtilService();
const formattingOptions = await userDataSyncUtilService.resolveFormattingOptions();
return merge(localContent, remoteContent, baseContent, formattingOptions, userDataSyncUtilService);
}
function stringify(value: any): string {
return JSON.stringify(value, null, '\t');
}
class MockUserDataSyncUtilService implements IUserDataSyncUtilService {
_serviceBrand: any;
async resolveUserBindings(userbindings: string[]): Promise<IStringDictionary<string>> {
const keys: IStringDictionary<string> = {};
for (const keybinding of userbindings) {
keys[keybinding] = keybinding;
}
return keys;
}
async resolveFormattingOptions(file?: URI): Promise<FormattingOptions> {
return { eol: OS === OperatingSystem.Windows ? '\r\n' : '\n', insertSpaces: false, tabSize: 4 };
}
}