Files
azuredatastudio/src/vs/platform/userDataSync/common/keybindingsMerge.ts
Anthony Dresser f5ce7fb2a5 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
2019-12-04 19:28:22 -08:00

368 lines
15 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* 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;
}