mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-25 09:35:37 -05:00
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:
53
src/vs/platform/userDataSync/common/content.ts
Normal file
53
src/vs/platform/userDataSync/common/content.ts
Normal 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;
|
||||
}
|
||||
@@ -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>();
|
||||
|
||||
367
src/vs/platform/userDataSync/common/keybindingsMerge.ts
Normal file
367
src/vs/platform/userDataSync/common/keybindingsMerge.ts
Normal 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;
|
||||
}
|
||||
348
src/vs/platform/userDataSync/common/keybindingsSync.ts
Normal file
348
src/vs/platform/userDataSync/common/keybindingsSync.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
45
src/vs/platform/userDataSync/common/keybindingsSyncIpc.ts
Normal file
45
src/vs/platform/userDataSync/common/keybindingsSyncIpc.ts
Normal 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]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
69
src/vs/platform/userDataSync/common/userDataAutoSync.ts
Normal file
69
src/vs/platform/userDataSync/common/userDataAutoSync.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user