From 02bd95c9066294b8f387a57203172c7000333acb Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 28 Nov 2019 20:20:35 +0100 Subject: [PATCH] refactor keybindings merge --- .../sharedProcess/sharedProcessMain.ts | 7 +- .../userDataSync/common/keybindingsMerge.ts | 374 +++++++++++++++++ .../userDataSync/common/keybindingsSync.ts | 8 +- .../userDataSync/common/keybindingsSyncIpc.ts | 15 +- .../userDataSync/common/userDataSync.ts | 7 +- .../test/common}/keybindingsMerge.test.ts | 94 ++--- .../userDataSync.contribution.ts | 8 +- .../keybinding/common/keybindingsMerge.ts | 392 ------------------ .../userDataSync/common/keybindingsMerge.ts | 32 ++ src/vs/workbench/workbench.common.main.ts | 2 +- 10 files changed, 477 insertions(+), 462 deletions(-) create mode 100644 src/vs/platform/userDataSync/common/keybindingsMerge.ts rename src/vs/{workbench/services/keybinding/test/electron-browser => platform/userDataSync/test/common}/keybindingsMerge.test.ts (87%) delete mode 100644 src/vs/workbench/services/keybinding/common/keybindingsMerge.ts create mode 100644 src/vs/workbench/services/userDataSync/common/keybindingsMerge.ts diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 60766bcc4a5..822ba16de3c 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -50,7 +50,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { DiskFileSystemProvider } from 'vs/platform/files/electron-browser/diskFileSystemProvider'; import { Schemas } from 'vs/base/common/network'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IUserDataSyncService, IUserDataSyncStoreService, ISettingsMergeService, registerConfiguration, IUserDataSyncLogService, IKeybindingsMergeService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, IUserDataSyncStoreService, ISettingsMergeService, registerConfiguration, IUserDataSyncLogService, IUserKeybindingsResolverService } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncService, UserDataAutoSync } from 'vs/platform/userDataSync/common/userDataSyncService'; import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { UserDataSyncChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc'; @@ -63,7 +63,7 @@ import { AuthTokenService } from 'vs/platform/auth/electron-browser/authTokenSer import { AuthTokenChannel } from 'vs/platform/auth/common/authTokenIpc'; import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; import { KeytarCredentialsService } from 'vs/platform/credentials/node/credentialsService'; -import { KeybindingsMergeChannelClient } from 'vs/platform/userDataSync/common/keybindingsSyncIpc'; +import { UserKeybindingsResolverServiceClient } from 'vs/platform/userDataSync/common/keybindingsSyncIpc'; export interface ISharedProcessConfiguration { readonly machineId: string; @@ -187,8 +187,7 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat services.set(IUserDataSyncLogService, new SyncDescriptor(UserDataSyncLogService)); const settingsMergeChannel = server.getChannel('settingsMerge', activeWindowRouter); services.set(ISettingsMergeService, new SettingsMergeChannelClient(settingsMergeChannel)); - const keybindingsMergeChannel = server.getChannel('keybindingsMerge', activeWindowRouter); - services.set(IKeybindingsMergeService, new KeybindingsMergeChannelClient(keybindingsMergeChannel)); + services.set(IUserKeybindingsResolverService, new UserKeybindingsResolverServiceClient(server.getChannel('userKeybindingsResolver', activeWindowRouter))); services.set(IUserDataSyncStoreService, new SyncDescriptor(UserDataSyncStoreService)); services.set(IUserDataSyncService, new SyncDescriptor(UserDataSyncService)); registerConfiguration(); diff --git a/src/vs/platform/userDataSync/common/keybindingsMerge.ts b/src/vs/platform/userDataSync/common/keybindingsMerge.ts new file mode 100644 index 00000000000..84c6a15062f --- /dev/null +++ b/src/vs/platform/userDataSync/common/keybindingsMerge.ts @@ -0,0 +1,374 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as objects from 'vs/base/common/objects'; +import { 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'; + +interface ICompareResult { + added: Set; + removed: Set; + updated: Set; +} + +interface IMergeResult { + hasLocalForwarded: boolean; + hasRemoteForwarded: boolean; + added: Set; + removed: Set; + updated: Set; + conflicts: Set; +} + +export function merge(localContent: string, remoteContent: string, baseContent: string | null, normalizedKeys: IStringDictionary): { mergeContent: string, hasChanges: boolean, hasConflicts: boolean } { + const local = parse(localContent); + const remote = parse(remoteContent); + const base = baseContent ? parse(baseContent) : null; + + 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()), removed: new Set(), updated: new Set() }; + const baseToRemoteByCommand = baseByCommand ? compareByCommand(baseByCommand, remoteByCommand, normalizedKeys) : { added: keys(remoteByCommand).reduce((r, k) => { r.add(k); return r; }, new Set()), removed: new Set(), updated: new Set() }; + + const commandsMergeResult = computeMergeResult(localToRemoteByCommand, baseToLocalByCommand, baseToRemoteByCommand); + const eol = contentUtil.getEol(localContent); + let mergeContent = localContent; + let mergeContentChanged = false; + + // Removed commands in Remote + for (const command of values(commandsMergeResult.removed)) { + if (commandsMergeResult.conflicts.has(command)) { + continue; + } + mergeContent = removeKeybindings(mergeContent, eol, command); + mergeContentChanged = true; + } + + if (mergeContentChanged) { + keybindingsMergeResult = computeMergeResultByKeybinding(local, remote, parse(mergeContent), normalizedKeys); + } + mergeContentChanged = false; + // 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, eol, keybindings); + mergeContentChanged = true; + } + + if (mergeContentChanged) { + keybindingsMergeResult = computeMergeResultByKeybinding(local, remote, parse(mergeContent), normalizedKeys); + } + // 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, eol, command, keybindings); + } + + const hasConflicts = commandsMergeResult.conflicts.size > 0; + if (hasConflicts) { + mergeContent = `<<<<<<< local${eol}` + + mergeContent + + `${eol}=======${eol}` + + remoteContent + + `${eol}>>>>>>> remote`; + } + + return { mergeContent, hasChanges: true, hasConflicts }; +} + +function computeMergeResult(localToRemote: ICompareResult, baseToLocal: ICompareResult, baseToRemote: ICompareResult): { added: Set, removed: Set, updated: Set, conflicts: Set } { + const added: Set = new Set(); + const removed: Set = new Set(); + const updated: Set = new Set(); + const conflicts: Set = new Set(); + + // 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): IMergeResult { + const empty = new Set(); + 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()), removed: new Set(), updated: new Set() }; + 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()), removed: new Set(), updated: new Set() }; + 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) { + const map: Map = new Map(); + 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 { + const map: Map = new Map(); + 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, to: Map): 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()); + const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set()); + const updated: Set = new Set(); + + for (const key of fromKeys) { + if (removed.has(key)) { + continue; + } + const value1: 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, to: Map, normalizedKeys: IStringDictionary): 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()); + const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set()); + const updated: Set = new Set(); + + for (const key of fromKeys) { + if (removed.has(key)) { + continue; + } + const value1: 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, eol: string, keybindings: IUserFriendlyKeybinding[]): string { + for (const keybinding of keybindings) { + content = contentUtil.edit(content, eol, [-1], keybinding); + } + return content; +} + +function removeKeybindings(content: string, eol: string, command: string): string { + const keybindings = parse(content); + for (let index = keybindings.length - 1; index >= 0; index--) { + if (keybindings[index].command === command || keybindings[index].command === `-${command}`) { + content = contentUtil.edit(content, eol, [index], undefined); + } + } + return content; +} + +function updateKeybindings(content: string, eol: string, command: string, keybindings: IUserFriendlyKeybinding[]): string { + const allKeybindings = 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, eol, [index], undefined); + } + } + // 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, eol, [location], keybindings[index]); + } + return content; +} diff --git a/src/vs/platform/userDataSync/common/keybindingsSync.ts b/src/vs/platform/userDataSync/common/keybindingsSync.ts index 4da5fadbe44..da6c85b1ab7 100644 --- a/src/vs/platform/userDataSync/common/keybindingsSync.ts +++ b/src/vs/platform/userDataSync/common/keybindingsSync.ts @@ -5,7 +5,8 @@ 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, IKeybindingsMergeService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserData, UserDataSyncStoreError, UserDataSyncStoreErrorCode, ISynchroniser, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserKeybindingsResolverService } 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'; @@ -48,7 +49,7 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser @IConfigurationService private readonly configurationService: IConfigurationService, @IFileService private readonly fileService: IFileService, @IEnvironmentService private readonly environmentService: IEnvironmentService, - @IKeybindingsMergeService private readonly keybindingsMergeService: IKeybindingsMergeService, + @IUserKeybindingsResolverService private readonly userKeybindingsResolverService: IUserKeybindingsResolverService, ) { super(); this.lastSyncKeybindingsResource = joinPath(this.environmentService.userRoamingDataHome, '.lastSyncKeybindings.json'); @@ -220,7 +221,8 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser || lastSyncData.content !== remoteContent // Remote has forwarded ) { this.logService.trace('Keybindings: Merging remote keybindings with local keybindings...'); - const result = await this.keybindingsMergeService.merge(localContent, remoteContent, lastSyncData ? lastSyncData.content : null); + const keys = await this.userKeybindingsResolverService.resolveUserKeybindings(localContent, remoteContent, lastSyncData ? lastSyncData.content : null); + const result = merge(localContent, remoteContent, lastSyncData ? lastSyncData.content : null, keys); // Sync only if there are changes if (result.hasChanges) { hasLocalChanged = result.mergeContent !== localContent; diff --git a/src/vs/platform/userDataSync/common/keybindingsSyncIpc.ts b/src/vs/platform/userDataSync/common/keybindingsSyncIpc.ts index 0bfa0de8f6a..c4c852604a9 100644 --- a/src/vs/platform/userDataSync/common/keybindingsSyncIpc.ts +++ b/src/vs/platform/userDataSync/common/keybindingsSyncIpc.ts @@ -5,11 +5,12 @@ import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; import { Event } from 'vs/base/common/event'; -import { IKeybindingsMergeService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserKeybindingsResolverService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IStringDictionary } from 'vs/base/common/collections'; -export class KeybindingsMergeChannel implements IServerChannel { +export class UserKeybindingsResolverServiceChannel implements IServerChannel { - constructor(private readonly service: IKeybindingsMergeService) { } + constructor(private readonly service: IUserKeybindingsResolverService) { } listen(_: unknown, event: string): Event { throw new Error(`Event not found: ${event}`); @@ -17,21 +18,21 @@ export class KeybindingsMergeChannel implements IServerChannel { call(context: any, command: string, args?: any): Promise { switch (command) { - case 'merge': return this.service.merge(args[0], args[1], args[2]); + case 'resolveUserKeybindings': return this.service.resolveUserKeybindings(args[0], args[1], args[2]); } throw new Error('Invalid call'); } } -export class KeybindingsMergeChannelClient implements IKeybindingsMergeService { +export class UserKeybindingsResolverServiceClient implements IUserKeybindingsResolverService { _serviceBrand: undefined; constructor(private readonly channel: IChannel) { } - merge(localContent: string, remoteContent: string, baseContent: string | null): Promise<{ mergeContent: string, hasChanges: boolean, hasConflicts: boolean }> { - return this.channel.call('merge', [localContent, remoteContent, baseContent]); + async resolveUserKeybindings(localContent: string, remoteContent: string, baseContent: string | null): Promise> { + return this.channel.call('resolveUserKeybindings', [localContent, remoteContent, baseContent]); } } diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index c17f0f7746d..660cae2d320 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -15,6 +15,7 @@ import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/plat 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'; const CONFIGURATION_SYNC_STORE_KEY = 'configurationSync.store'; @@ -180,13 +181,13 @@ export interface ISettingsMergeService { } -export const IKeybindingsMergeService = createDecorator('IKeybindingsMergeService'); +export const IUserKeybindingsResolverService = createDecorator('IUserKeybindingsResolverService'); -export interface IKeybindingsMergeService { +export interface IUserKeybindingsResolverService { _serviceBrand: undefined; - merge(localContent: string, remoteContent: string, baseContent: string | null): Promise<{ mergeContent: string, hasChanges: boolean, hasConflicts: boolean }>; + resolveUserKeybindings(localContent: string, remoteContent: string, baseContent: string | null): Promise>; } diff --git a/src/vs/workbench/services/keybinding/test/electron-browser/keybindingsMerge.test.ts b/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts similarity index 87% rename from src/vs/workbench/services/keybinding/test/electron-browser/keybindingsMerge.test.ts rename to src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts index 9529d3150a8..e3ba6d50ee7 100644 --- a/src/vs/workbench/services/keybinding/test/electron-browser/keybindingsMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts @@ -4,30 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { OS } from 'vs/base/common/platform'; -import { KeybindingsMergeService } from 'vs/workbench/services/keybinding/common/keybindingsMerge'; -import { MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; -import { ResolvedKeybinding, ChordKeybinding, SimpleKeybinding } from 'vs/base/common/keyCodes'; -import { KeybindingParser } from 'vs/base/common/keybindingParser'; -import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding'; - -let testObject: KeybindingsMergeService; - -suiteSetup(() => { - testObject = new KeybindingsMergeService(new class extends MockKeybindingService { - resolveUserBinding(userBinding: string): ResolvedKeybinding[] { - const parts = KeybindingParser.parseUserBinding(userBinding); - return [new USLayoutResolvedKeybinding(new ChordKeybinding(parts), OS)]; - } - }); -}); +import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { IUserFriendlyKeybinding } from 'vs/platform/keybinding/common/keybinding'; +import { parse } from 'vs/base/common/json'; 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 testObject.merge(localContent, remoteContent, null); + const actual = mergeKeybindings(localContent, remoteContent, null); assert.ok(!actual.hasChanges); assert.ok(!actual.hasConflicts); assert.equal(actual.mergeContent, localContent); @@ -36,7 +23,7 @@ suite('KeybindingsMerge - No Conflicts', () => { 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 testObject.merge(localContent, remoteContent, null); + const actual = mergeKeybindings(localContent, remoteContent, null); assert.ok(!actual.hasChanges); assert.ok(!actual.hasConflicts); assert.equal(actual.mergeContent, localContent); @@ -51,7 +38,7 @@ suite('KeybindingsMerge - No Conflicts', () => { { key: 'alt+a', command: 'a', when: 'editorTextFocus' }, { key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' } ]); - const actual = await testObject.merge(localContent, remoteContent, null); + const actual = mergeKeybindings(localContent, remoteContent, null); assert.ok(!actual.hasChanges); assert.ok(!actual.hasConflicts); assert.equal(actual.mergeContent, localContent); @@ -68,7 +55,7 @@ suite('KeybindingsMerge - No Conflicts', () => { { key: 'alt+d', command: '-a' }, { key: 'cmd+c', command: 'b', args: { text: '`' } } ]); - const actual = await testObject.merge(localContent, remoteContent, null); + const actual = mergeKeybindings(localContent, remoteContent, null); assert.ok(!actual.hasChanges); assert.ok(!actual.hasConflicts); assert.equal(actual.mergeContent, localContent); @@ -89,7 +76,7 @@ suite('KeybindingsMerge - No Conflicts', () => { { key: 'alt+d', command: '-a' }, { key: 'cmd+c', command: 'b', args: { text: '`' } } ]); - const actual = await testObject.merge(localContent, remoteContent, baseContent); + const actual = mergeKeybindings(localContent, remoteContent, baseContent); assert.ok(!actual.hasChanges); assert.ok(!actual.hasConflicts); assert.equal(actual.mergeContent, localContent); @@ -106,7 +93,7 @@ suite('KeybindingsMerge - No Conflicts', () => { { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, { key: 'alt+d', command: '-a' }, ]); - const actual = await testObject.merge(localContent, remoteContent, null); + const actual = mergeKeybindings(localContent, remoteContent, null); assert.ok(!actual.hasChanges); assert.ok(!actual.hasConflicts); assert.equal(actual.mergeContent, localContent); @@ -123,7 +110,7 @@ suite('KeybindingsMerge - No Conflicts', () => { { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, { key: 'cmd+c', command: 'b', args: { text: '`' } }, ]); - const actual = await testObject.merge(localContent, remoteContent, null); + const actual = mergeKeybindings(localContent, remoteContent, null); assert.ok(!actual.hasChanges); assert.ok(!actual.hasConflicts); assert.equal(actual.mergeContent, localContent); @@ -139,7 +126,7 @@ suite('KeybindingsMerge - No Conflicts', () => { { key: 'alt+d', command: '-a' }, { key: 'cmd+c', command: 'b', args: { text: '`' } }, ]); - const actual = await testObject.merge(localContent, remoteContent, null); + const actual = mergeKeybindings(localContent, remoteContent, null); assert.ok(actual.hasChanges); assert.ok(!actual.hasConflicts); assert.equal(actual.mergeContent, remoteContent); @@ -156,7 +143,7 @@ suite('KeybindingsMerge - No Conflicts', () => { { key: 'cmd+c', command: 'b', args: { text: '`' } }, { key: 'cmd+d', command: 'c' }, ]); - const actual = await testObject.merge(localContent, remoteContent, null); + const actual = mergeKeybindings(localContent, remoteContent, null); assert.ok(actual.hasChanges); assert.ok(!actual.hasConflicts); assert.equal(actual.mergeContent, remoteContent); @@ -173,7 +160,7 @@ suite('KeybindingsMerge - No Conflicts', () => { { key: 'cmd+c', command: 'b', args: { text: '`' } }, { key: 'cmd+d', command: 'c' }, ]); - const actual = await testObject.merge(localContent, remoteContent, localContent); + const actual = mergeKeybindings(localContent, remoteContent, localContent); assert.ok(actual.hasChanges); assert.ok(!actual.hasConflicts); assert.equal(actual.mergeContent, remoteContent); @@ -189,7 +176,7 @@ suite('KeybindingsMerge - No Conflicts', () => { { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, { key: 'alt+d', command: '-a' }, ]); - const actual = await testObject.merge(localContent, remoteContent, localContent); + const actual = mergeKeybindings(localContent, remoteContent, localContent); assert.ok(actual.hasChanges); assert.ok(!actual.hasConflicts); assert.equal(actual.mergeContent, remoteContent); @@ -203,7 +190,7 @@ suite('KeybindingsMerge - No Conflicts', () => { const remoteContent = stringify([ { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, ]); - const actual = await testObject.merge(localContent, remoteContent, localContent); + const actual = mergeKeybindings(localContent, remoteContent, localContent); assert.ok(actual.hasChanges); assert.ok(!actual.hasConflicts); assert.equal(actual.mergeContent, remoteContent); @@ -216,7 +203,7 @@ suite('KeybindingsMerge - No Conflicts', () => { const remoteContent = stringify([ { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, ]); - const actual = await testObject.merge(localContent, remoteContent, localContent); + const actual = mergeKeybindings(localContent, remoteContent, localContent); assert.ok(actual.hasChanges); assert.ok(!actual.hasConflicts); assert.equal(actual.mergeContent, remoteContent); @@ -235,7 +222,7 @@ suite('KeybindingsMerge - No Conflicts', () => { { key: 'alt+d', command: 'b' }, { key: 'cmd+d', command: 'a' }, ]); - const actual = await testObject.merge(localContent, remoteContent, localContent); + const actual = mergeKeybindings(localContent, remoteContent, localContent); assert.ok(actual.hasChanges); assert.ok(!actual.hasConflicts); assert.equal(actual.mergeContent, remoteContent); @@ -258,7 +245,7 @@ suite('KeybindingsMerge - No Conflicts', () => { { key: 'cmd+d', command: 'c', when: 'context1' }, { key: 'cmd+c', command: '-c' }, ]); - const actual = await testObject.merge(localContent, remoteContent, localContent); + const actual = mergeKeybindings(localContent, remoteContent, localContent); assert.ok(actual.hasChanges); assert.ok(!actual.hasConflicts); assert.equal(actual.mergeContent, remoteContent); @@ -274,7 +261,7 @@ suite('KeybindingsMerge - No Conflicts', () => { { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, { key: 'alt+d', command: '-a' }, ]); - const actual = await testObject.merge(localContent, remoteContent, null); + const actual = mergeKeybindings(localContent, remoteContent, null); assert.ok(actual.hasChanges); assert.ok(!actual.hasConflicts); assert.equal(actual.mergeContent, localContent); @@ -291,7 +278,7 @@ suite('KeybindingsMerge - No Conflicts', () => { { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, { key: 'alt+d', command: '-a' }, ]); - const actual = await testObject.merge(localContent, remoteContent, null); + const actual = mergeKeybindings(localContent, remoteContent, null); assert.ok(actual.hasChanges); assert.ok(!actual.hasConflicts); assert.equal(actual.mergeContent, localContent); @@ -308,7 +295,7 @@ suite('KeybindingsMerge - No Conflicts', () => { { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, { key: 'alt+d', command: '-a' }, ]); - const actual = await testObject.merge(localContent, remoteContent, remoteContent); + const actual = mergeKeybindings(localContent, remoteContent, remoteContent); assert.ok(actual.hasChanges); assert.ok(!actual.hasConflicts); assert.equal(actual.mergeContent, localContent); @@ -324,7 +311,7 @@ suite('KeybindingsMerge - No Conflicts', () => { { key: 'alt+d', command: '-a' }, { key: 'cmd+c', command: 'b', args: { text: '`' } }, ]); - const actual = await testObject.merge(localContent, remoteContent, remoteContent); + const actual = mergeKeybindings(localContent, remoteContent, remoteContent); assert.ok(actual.hasChanges); assert.ok(!actual.hasConflicts); assert.equal(actual.mergeContent, localContent); @@ -338,7 +325,7 @@ suite('KeybindingsMerge - No Conflicts', () => { { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, { key: 'alt+d', command: '-a' }, ]); - const actual = await testObject.merge(localContent, remoteContent, remoteContent); + const actual = mergeKeybindings(localContent, remoteContent, remoteContent); assert.ok(actual.hasChanges); assert.ok(!actual.hasConflicts); assert.equal(actual.mergeContent, localContent); @@ -351,7 +338,7 @@ suite('KeybindingsMerge - No Conflicts', () => { const remoteContent = stringify([ { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, ]); - const actual = await testObject.merge(localContent, remoteContent, remoteContent); + const actual = mergeKeybindings(localContent, remoteContent, remoteContent); assert.ok(actual.hasChanges); assert.ok(!actual.hasConflicts); assert.equal(actual.mergeContent, localContent); @@ -370,7 +357,7 @@ suite('KeybindingsMerge - No Conflicts', () => { { key: 'alt+d', command: 'b' }, { key: 'cmd+d', command: 'a' }, ]); - const actual = await testObject.merge(localContent, remoteContent, remoteContent); + const actual = mergeKeybindings(localContent, remoteContent, remoteContent); assert.ok(actual.hasChanges); assert.ok(!actual.hasConflicts); assert.equal(actual.mergeContent, localContent); @@ -402,7 +389,7 @@ suite('KeybindingsMerge - No Conflicts', () => { { key: 'cmd+d', command: 'c', when: 'context1' }, { key: 'cmd+c', command: '-c' }, ]); - const actual = await testObject.merge(localContent, remoteContent, remoteContent); + const actual = mergeKeybindings(localContent, remoteContent, remoteContent); assert.ok(actual.hasChanges); assert.ok(!actual.hasConflicts); assert.equal(actual.mergeContent, expected); @@ -445,7 +432,7 @@ suite('KeybindingsMerge - No Conflicts', () => { ]); //'[\n\t{\n\t\t"key": "alt+d",\n\t\t"command": "-f"\n\t},\n\t{\n\t\t"key": "cmd+d",\n\t\t"command": "d"\n\t},\n\t{\n\t\t"key": "cmd+c",\n\t\t"command": "-c"\n\t},\n\t{\n\t\t"key": "cmd+d",\n\t\t"command": "c",\n\t\t"when": "context1"\n\t},\n\t{\n\t\t"key": "alt+a",\n\t\t"command": "f"\n\t},\n\t{\n\t\t"key": "alt+e",\n\t\t"command": "e"\n\t},\n\t{\n\t\t"key": "alt+g",\n\t\t"command": "g",\n\t\t"when": "context2"\n\t}\n]' //'[\n\t{\n\t\t"key": "alt+d",\n\t\t"command": "-f"\n\t},\n\t{\n\t\t"key": "cmd+d",\n\t\t"command": "d"\n\t},\n\t{\n\t\t"key": "cmd+c",\n\t\t"command": "-c"\n\t},\n\t{\n\t\t"key": "alt+c",\n\t\t"command": "c",\n\t\t"when": "context1"\n\t},\n\t{\n\t\t"key": "alt+a",\n\t\t"command": "f"\n\t},\n\t{\n\t\t"key": "alt+e",\n\t\t"command": "e"\n\t},\n\t{\n\t\t"key": "alt+g",\n\t\t"command": "g",\n\t\t"when": "context2"\n\t}\n]' - const actual = await testObject.merge(localContent, remoteContent, baseContent); + const actual = mergeKeybindings(localContent, remoteContent, baseContent); assert.ok(actual.hasChanges); assert.ok(!actual.hasConflicts); assert.equal(actual.mergeContent, expected); @@ -458,7 +445,7 @@ suite('KeybindingsMerge - Conflicts', () => { test('merge when local and remote with one entry but different value', async () => { const localContent = stringify([{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' }]); const remoteContent = stringify([{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }]); - const actual = await testObject.merge(localContent, remoteContent, null); + const actual = mergeKeybindings(localContent, remoteContent, null); assert.ok(actual.hasChanges); assert.ok(actual.hasConflicts); assert.equal(actual.mergeContent, @@ -490,7 +477,7 @@ suite('KeybindingsMerge - Conflicts', () => { { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, { key: 'alt+a', command: '-a', when: 'editorTextFocus && !editorReadonly' } ]); - const actual = await testObject.merge(localContent, remoteContent, null); + const actual = mergeKeybindings(localContent, remoteContent, null); assert.ok(actual.hasChanges); assert.ok(actual.hasConflicts); assert.equal(actual.mergeContent, @@ -527,7 +514,7 @@ suite('KeybindingsMerge - Conflicts', () => { 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 testObject.merge(localContent, remoteContent, baseContent); + const actual = mergeKeybindings(localContent, remoteContent, baseContent); assert.ok(actual.hasChanges); assert.ok(actual.hasConflicts); assert.equal(actual.mergeContent, @@ -548,7 +535,7 @@ suite('KeybindingsMerge - Conflicts', () => { 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 testObject.merge(localContent, remoteContent, baseContent); + const actual = mergeKeybindings(localContent, remoteContent, baseContent); assert.ok(actual.hasChanges); assert.ok(actual.hasConflicts); assert.equal(actual.mergeContent, @@ -574,7 +561,7 @@ suite('KeybindingsMerge - Conflicts', () => { 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 testObject.merge(localContent, remoteContent, baseContent); + const actual = mergeKeybindings(localContent, remoteContent, baseContent); assert.ok(actual.hasChanges); assert.ok(actual.hasConflicts); assert.equal(actual.mergeContent, @@ -595,7 +582,7 @@ suite('KeybindingsMerge - Conflicts', () => { 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 testObject.merge(localContent, remoteContent, baseContent); + const actual = mergeKeybindings(localContent, remoteContent, baseContent); assert.ok(actual.hasChanges); assert.ok(actual.hasConflicts); assert.equal(actual.mergeContent, @@ -647,7 +634,7 @@ suite('KeybindingsMerge - Conflicts', () => { { key: 'alt+c', command: 'c', when: 'context1' }, { key: 'alt+g', command: 'g', when: 'context2' }, ]); - const actual = await testObject.merge(localContent, remoteContent, baseContent); + const actual = mergeKeybindings(localContent, remoteContent, baseContent); assert.ok(actual.hasChanges); assert.ok(actual.hasConflicts); assert.equal(actual.mergeContent, @@ -718,6 +705,17 @@ suite('KeybindingsMerge - Conflicts', () => { }); +function mergeKeybindings(localContent: string, remoteContent: string, baseContent: string | null) { + const local = parse(localContent); + const remote = parse(remoteContent); + const base = baseContent ? parse(baseContent) : null; + const keys: IStringDictionary = {}; + for (const keybinding of [...local, ...remote, ...(base || [])]) { + keys[keybinding.key] = keybinding.key; + } + return merge(localContent, remoteContent, baseContent, keys); +} + function stringify(value: any): any { return JSON.stringify(value, null, '\t'); } diff --git a/src/vs/workbench/contrib/userDataSync/electron-browser/userDataSync.contribution.ts b/src/vs/workbench/contrib/userDataSync/electron-browser/userDataSync.contribution.ts index 9a07beb2623..3c94e35f321 100644 --- a/src/vs/workbench/contrib/userDataSync/electron-browser/userDataSync.contribution.ts +++ b/src/vs/workbench/contrib/userDataSync/electron-browser/userDataSync.contribution.ts @@ -4,22 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { ISettingsMergeService, IKeybindingsMergeService } from 'vs/platform/userDataSync/common/userDataSync'; +import { ISettingsMergeService, IUserKeybindingsResolverService } from 'vs/platform/userDataSync/common/userDataSync'; import { Registry } from 'vs/platform/registry/common/platform'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { SettingsMergeChannel } from 'vs/platform/userDataSync/common/settingsSyncIpc'; -import { KeybindingsMergeChannel } from 'vs/platform/userDataSync/common/keybindingsSyncIpc'; +import { UserKeybindingsResolverServiceChannel } from 'vs/platform/userDataSync/common/keybindingsSyncIpc'; class UserDataSyncServicesContribution implements IWorkbenchContribution { constructor( @ISettingsMergeService settingsMergeService: ISettingsMergeService, - @IKeybindingsMergeService keybindingsMergeService: IKeybindingsMergeService, + @IUserKeybindingsResolverService keybindingsMergeService: IUserKeybindingsResolverService, @ISharedProcessService sharedProcessService: ISharedProcessService, ) { sharedProcessService.registerChannel('settingsMerge', new SettingsMergeChannel(settingsMergeService)); - sharedProcessService.registerChannel('keybindingsMerge', new KeybindingsMergeChannel(keybindingsMergeService)); + sharedProcessService.registerChannel('userKeybindingsResolver', new UserKeybindingsResolverServiceChannel(keybindingsMergeService)); } } diff --git a/src/vs/workbench/services/keybinding/common/keybindingsMerge.ts b/src/vs/workbench/services/keybinding/common/keybindingsMerge.ts deleted file mode 100644 index 0f3da528a2b..00000000000 --- a/src/vs/workbench/services/keybinding/common/keybindingsMerge.ts +++ /dev/null @@ -1,392 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as objects from 'vs/base/common/objects'; -import { parse } from 'vs/base/common/json'; -import { values, keys } from 'vs/base/common/map'; -import { IUserFriendlyKeybinding, IKeybindingService } 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 { IKeybindingsMergeService } from 'vs/platform/userDataSync/common/userDataSync'; - -interface ICompareResult { - added: Set; - removed: Set; - updated: Set; -} - -interface IMergeResult { - hasLocalForwarded: boolean; - hasRemoteForwarded: boolean; - added: Set; - removed: Set; - updated: Set; - conflicts: Set; -} - -export class KeybindingsMergeService implements IKeybindingsMergeService { - - _serviceBrand: undefined; - - constructor( - @IKeybindingService private readonly keybindingsService: IKeybindingService - ) { } - - public async merge(localContent: string, remoteContent: string, baseContent: string | null): Promise<{ mergeContent: string, hasChanges: boolean, hasConflicts: boolean }> { - const local = parse(localContent); - const remote = parse(remoteContent); - const base = baseContent ? parse(baseContent) : null; - const normalizedKeys = this.getNormalizedKeys(local, remote, base); - - let keybindingsMergeResult = this.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 = this.byCommand(local); - const remoteByCommand = this.byCommand(remote); - const baseByCommand = base ? this.byCommand(base) : null; - const localToRemoteByCommand = this.compareByCommand(localByCommand, remoteByCommand, normalizedKeys); - const baseToLocalByCommand = baseByCommand ? this.compareByCommand(baseByCommand, localByCommand, normalizedKeys) : { added: keys(localByCommand).reduce((r, k) => { r.add(k); return r; }, new Set()), removed: new Set(), updated: new Set() }; - const baseToRemoteByCommand = baseByCommand ? this.compareByCommand(baseByCommand, remoteByCommand, normalizedKeys) : { added: keys(remoteByCommand).reduce((r, k) => { r.add(k); return r; }, new Set()), removed: new Set(), updated: new Set() }; - - const commandsMergeResult = this.computeMergeResult(localToRemoteByCommand, baseToLocalByCommand, baseToRemoteByCommand); - const eol = contentUtil.getEol(localContent); - let mergeContent = localContent; - let mergeContentChanged = false; - - // Removed commands in Remote - for (const command of values(commandsMergeResult.removed)) { - if (commandsMergeResult.conflicts.has(command)) { - continue; - } - mergeContent = this.removeKeybindings(mergeContent, eol, command); - mergeContentChanged = true; - } - - if (mergeContentChanged) { - keybindingsMergeResult = this.computeMergeResultByKeybinding(local, remote, parse(mergeContent), normalizedKeys); - } - mergeContentChanged = false; - // 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.get(keybinding.key)!))) { - commandsMergeResult.conflicts.add(command); - continue; - } - mergeContent = this.addKeybindings(mergeContent, eol, keybindings); - mergeContentChanged = true; - } - - if (mergeContentChanged) { - keybindingsMergeResult = this.computeMergeResultByKeybinding(local, remote, parse(mergeContent), normalizedKeys); - } - // 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.get(keybinding.key)!))) { - commandsMergeResult.conflicts.add(command); - continue; - } - mergeContent = this.updateKeybindings(mergeContent, eol, command, keybindings); - } - - const hasConflicts = commandsMergeResult.conflicts.size > 0; - if (hasConflicts) { - mergeContent = `<<<<<<< local${eol}` - + mergeContent - + `${eol}=======${eol}` - + remoteContent - + `${eol}>>>>>>> remote`; - } - - return { mergeContent, hasChanges: true, hasConflicts }; - } - - private computeMergeResult(localToRemote: ICompareResult, baseToLocal: ICompareResult, baseToRemote: ICompareResult): { added: Set, removed: Set, updated: Set, conflicts: Set } { - const added: Set = new Set(); - const removed: Set = new Set(); - const updated: Set = new Set(); - const conflicts: Set = new Set(); - - // 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 }; - } - - private getNormalizedKeys(local: IUserFriendlyKeybinding[], remote: IUserFriendlyKeybinding[], base: IUserFriendlyKeybinding[] | null): Map { - const keys = new Map(); - for (const keybinding of [...local, ...remote, ...(base || [])]) { - keys.set(keybinding.key, this.keybindingsService.resolveUserBinding(keybinding.key).map(part => part.getUserSettingsLabel()).join(' ')); - } - return keys; - } - - private computeMergeResultByKeybinding(local: IUserFriendlyKeybinding[], remote: IUserFriendlyKeybinding[], base: IUserFriendlyKeybinding[] | null, normalizedKeys: Map): IMergeResult { - const empty = new Set(); - const localByKeybinding = this.byKeybinding(local, normalizedKeys); - const remoteByKeybinding = this.byKeybinding(remote, normalizedKeys); - const baseByKeybinding = base ? this.byKeybinding(base, normalizedKeys) : null; - - const localToRemoteByKeybinding = this.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 ? this.compareByKeybinding(baseByKeybinding, localByKeybinding) : { added: keys(localByKeybinding).reduce((r, k) => { r.add(k); return r; }, new Set()), removed: new Set(), updated: new Set() }; - 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 ? this.compareByKeybinding(baseByKeybinding, remoteByKeybinding) : { added: keys(remoteByKeybinding).reduce((r, k) => { r.add(k); return r; }, new Set()), removed: new Set(), updated: new Set() }; - 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 } = this.computeMergeResult(localToRemoteByKeybinding, baseToLocalByKeybinding, baseToRemoteByKeybinding); - return { hasLocalForwarded: true, hasRemoteForwarded: true, added, removed, updated, conflicts }; - } - - private byKeybinding(keybindings: IUserFriendlyKeybinding[], keys: Map) { - const map: Map = new Map(); - for (const keybinding of keybindings) { - const key = keys.get(keybinding.key)!; - let value = map.get(key); - if (!value) { - value = []; - map.set(key, value); - } - value.push(keybinding); - - } - return map; - } - - private byCommand(keybindings: IUserFriendlyKeybinding[]): Map { - const map: Map = new Map(); - 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; - } - - - private compareByKeybinding(from: Map, to: Map): 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()); - const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set()); - const updated: Set = new Set(); - - for (const key of fromKeys) { - if (removed.has(key)) { - continue; - } - const value1: IUserFriendlyKeybinding[] = from.get(key)!.map(keybinding => ({ ...keybinding, ...{ key } })); - const value2: IUserFriendlyKeybinding[] = to.get(key)!.map(keybinding => ({ ...keybinding, ...{ key } })); - if (!equals(value1, value2, (a, b) => this.isSameKeybinding(a, b))) { - updated.add(key); - } - } - - return { added, removed, updated }; - } - - private compareByCommand(from: Map, to: Map, normalizedKeys: Map): 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()); - const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set()); - const updated: Set = new Set(); - - for (const key of fromKeys) { - if (removed.has(key)) { - continue; - } - const value1: IUserFriendlyKeybinding[] = from.get(key)!.map(keybinding => ({ ...keybinding, ...{ key: normalizedKeys.get(keybinding.key)! } })); - const value2: IUserFriendlyKeybinding[] = to.get(key)!.map(keybinding => ({ ...keybinding, ...{ key: normalizedKeys.get(keybinding.key)! } })); - if (!this.areSameKeybindingsWithSameCommand(value1, value2)) { - updated.add(key); - } - } - - return { added, removed, updated }; - } - - private areSameKeybindingsWithSameCommand(value1: IUserFriendlyKeybinding[], value2: IUserFriendlyKeybinding[]): boolean { - // Compare entries adding keybindings - if (!equals(value1.filter(({ command }) => command[0] !== '-'), value2.filter(({ command }) => command[0] !== '-'), (a, b) => this.isSameKeybinding(a, b))) { - return false; - } - // Compare entries removing keybindings - if (!equals(value1.filter(({ command }) => command[0] === '-'), value2.filter(({ command }) => command[0] === '-'), (a, b) => this.isSameKeybinding(a, b))) { - return false; - } - return true; - } - - private 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; - } - - private addKeybindings(content: string, eol: string, keybindings: IUserFriendlyKeybinding[]): string { - for (const keybinding of keybindings) { - content = contentUtil.edit(content, eol, [-1], keybinding); - } - return content; - } - - private removeKeybindings(content: string, eol: string, command: string): string { - const keybindings = parse(content); - for (let index = keybindings.length - 1; index >= 0; index--) { - if (keybindings[index].command === command || keybindings[index].command === `-${command}`) { - content = contentUtil.edit(content, eol, [index], undefined); - } - } - return content; - } - - private updateKeybindings(content: string, eol: string, command: string, keybindings: IUserFriendlyKeybinding[]): string { - const allKeybindings = 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, eol, [index], undefined); - } - } - // 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, eol, [location], keybindings[index]); - } - return content; - } -} diff --git a/src/vs/workbench/services/userDataSync/common/keybindingsMerge.ts b/src/vs/workbench/services/userDataSync/common/keybindingsMerge.ts new file mode 100644 index 00000000000..d263bfa18d5 --- /dev/null +++ b/src/vs/workbench/services/userDataSync/common/keybindingsMerge.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { parse } from 'vs/base/common/json'; +import { IUserFriendlyKeybinding, IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IUserKeybindingsResolverService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; + +export class UserKeybindingsResolverService implements IUserKeybindingsResolverService { + + _serviceBrand: undefined; + + constructor( + @IKeybindingService private readonly keybindingsService: IKeybindingService + ) { } + + public async resolveUserKeybindings(localContent: string, remoteContent: string, baseContent: string | null): Promise> { + const local = parse(localContent); + const remote = parse(remoteContent); + const base = baseContent ? parse(baseContent) : null; + const keys: IStringDictionary = {}; + for (const keybinding of [...local, ...remote, ...(base || [])]) { + keys[keybinding.key] = this.keybindingsService.resolveUserBinding(keybinding.key).map(part => part.getUserSettingsLabel()).join(' '); + } + return keys; + } +} + +registerSingleton(IUserKeybindingsResolverService, UserKeybindingsResolverService); diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index ebc12c69d14..316b630b15a 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -80,7 +80,7 @@ import 'vs/workbench/services/extensionManagement/common/extensionEnablementServ import 'vs/workbench/services/notification/common/notificationService'; import 'vs/workbench/services/extensions/common/staticExtensions'; import 'vs/workbench/services/userDataSync/common/settingsMergeService'; -import 'vs/workbench/services/keybinding/common/keybindingsMerge'; +import 'vs/workbench/services/userDataSync/common/keybindingsMerge'; import 'vs/workbench/services/path/common/remotePathService'; import 'vs/workbench/services/remote/common/remoteExplorerService'; import 'vs/workbench/services/workingCopy/common/workingCopyService';