提交 7be71103 编写于 作者: S Sandeep Somavarapu

remove text model and move to platform

上级 39464526
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { OperatingSystem, OS } from 'vs/base/common/platform';
import { JSONPath } from 'vs/base/common/json';
import { setProperty } from 'vs/base/common/jsonEdit';
export function edit(content: string, eol: string, originalPath: JSONPath, value: any): string {
const edit = setProperty(content, originalPath, value, { tabSize: 4, insertSpaces: false, eol })[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;
}
export function getEol(content: string): string {
if (content.indexOf('\r\n') !== -1) {
return '\r\n';
}
if (content.indexOf('\n') !== -1) {
return '\n';
}
return OS === OperatingSystem.Linux || OS === OperatingSystem.Macintosh ? '\n' : '\r\n';
}
/*---------------------------------------------------------------------------------------------
* 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, findNodeAtLocation, parseTree } 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';
export function mergeKeybindings(localContent: string, remoteContent: string, baseContent: string | null): { mergeContent: string, hasChanges: boolean, hasConflicts: boolean } {
const local = <IUserFriendlyKeybinding[]>parse(localContent);
const remote = <IUserFriendlyKeybinding[]>parse(remoteContent);
const base = baseContent ? <IUserFriendlyKeybinding[]>parse(baseContent) : null;
const byCommand = (keybindings: 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;
};
const localByCommand = byCommand(local);
const remoteByCommand = byCommand(remote);
const baseByCommand = base ? byCommand(base) : null;
const localToRemote = compare(localByCommand, remoteByCommand);
if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) {
// No changes found between local and remote.
return { mergeContent: localContent, hasChanges: false, hasConflicts: false };
}
const conflictCommands: Set<string> = new Set<string>();
const baseToLocal = baseByCommand ? compare(baseByCommand, localByCommand) : { added: keys(localByCommand).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
const baseToRemote = baseByCommand ? compare(baseByCommand, remoteByCommand) : { added: keys(remoteByCommand).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
const eol = contentUtil.getEol(localContent);
let mergeContent = localContent;
// Removed commands in Local
for (const command of values(baseToLocal.removed)) {
// Got updated in remote
if (baseToRemote.updated.has(command)) {
conflictCommands.add(command);
}
}
// Removed commands in Remote
for (const command of values(baseToRemote.removed)) {
if (conflictCommands.has(command)) {
continue;
}
// Got updated in local
if (baseToLocal.updated.has(command)) {
conflictCommands.add(command);
} else {
// remove the command
mergeContent = removeKeybindings(mergeContent, eol, command);
}
}
// Added commands in Local
for (const command of values(baseToLocal.added)) {
if (conflictCommands.has(command)) {
continue;
}
// Got added in remote
if (baseToRemote.added.has(command)) {
// Has different value
if (localToRemote.updated.has(command)) {
conflictCommands.add(command);
}
}
}
// Added commands in remote
for (const command of values(baseToRemote.added)) {
if (conflictCommands.has(command)) {
continue;
}
// Got added in local
if (baseToLocal.added.has(command)) {
// Has different value
if (localToRemote.updated.has(command)) {
conflictCommands.add(command);
}
} else {
mergeContent = addKeybinding(mergeContent, eol, command, remoteByCommand.get(command)!);
}
}
// Updated commands in Local
for (const command of values(baseToLocal.updated)) {
if (conflictCommands.has(command)) {
continue;
}
// Got updated in remote
if (baseToRemote.updated.has(command)) {
// Has different value
if (localToRemote.updated.has(command)) {
conflictCommands.add(command);
}
}
}
// Updated commands in Remote
for (const command of values(baseToRemote.updated)) {
if (conflictCommands.has(command)) {
continue;
}
// Got updated in local
if (baseToLocal.updated.has(command)) {
// Has different value
if (localToRemote.updated.has(command)) {
conflictCommands.add(command);
}
} else {
// update the command
mergeContent = updateKeybinding(mergeContent, eol, command, remoteByCommand.get(command)!);
}
}
const conflicts: { command: string, local: IUserFriendlyKeybinding[] | undefined, remote: IUserFriendlyKeybinding[] | undefined, firstIndex: number }[] = [];
for (const command of values(conflictCommands)) {
const local = localByCommand.get(command);
const remote = remoteByCommand.get(command);
mergeContent = updateKeybinding(mergeContent, eol, command, [...local || [], ...remote || []]);
conflicts.push({ command, local, remote, firstIndex: -1 });
}
const allKeybindings = <IUserFriendlyKeybinding[]>parse(mergeContent);
for (const conflict of conflicts) {
conflict.firstIndex = findFirstIndex(allKeybindings, keybinding => keybinding.command === conflict.command || keybinding.command === `-${conflict.command}`);
}
// Sort reverse so that conflicts content is added from last
conflicts.sort((a, b) => b.firstIndex - a.firstIndex);
const tree = parseTree(mergeContent);
for (const { firstIndex, local, remote } of conflicts) {
const firstNode = findNodeAtLocation(tree, [firstIndex])!;
const startLocalOffset = contentUtil.getLineStartOffset(mergeContent, eol, firstNode.offset) - eol.length;
let endLocalOffset = startLocalOffset;
let remoteOffset = endLocalOffset;
if (local) {
const lastLocalValueNode = findNodeAtLocation(tree, [firstIndex + local.length - 1])!;
endLocalOffset = contentUtil.getLineEndOffset(mergeContent, eol, lastLocalValueNode.offset + lastLocalValueNode.length);
}
if (remote) {
const lastRemoteValueNode = findNodeAtLocation(tree, [firstIndex + (local ? local.length : 0) + remote.length - 1])!;
remoteOffset = contentUtil.getLineEndOffset(mergeContent, eol, lastRemoteValueNode.offset + lastRemoteValueNode.length);
}
mergeContent = mergeContent.substring(0, startLocalOffset)
+ `${eol}<<<<<<< local`
+ mergeContent.substring(startLocalOffset, endLocalOffset)
+ `${eol}=======`
+ mergeContent.substring(endLocalOffset, remoteOffset)
+ `${eol}>>>>>>> remote`
+ mergeContent.substring(remoteOffset);
}
return { mergeContent, hasChanges: true, hasConflicts: conflicts.length > 0 };
}
function compare(from: Map<string, IUserFriendlyKeybinding[]>, to: Map<string, IUserFriendlyKeybinding[]>): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
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)!;
const value2: IUserFriendlyKeybinding[] = to.get(key)!;
if (!areSameKeybindings(value1, value2)) {
updated.add(key);
}
}
return { added, removed, updated };
}
function areSameKeybindings(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 addKeybinding(content: string, eol: string, command: 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 = <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, eol, [index], undefined);
}
}
return content;
}
function updateKeybinding(content: string, eol: string, command: string, keybindings: IUserFriendlyKeybinding[]): 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, 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;
}
......@@ -4,34 +4,29 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { workbenchInstantiationService } from 'vs/workbench/test/workbenchTestServices';
import { KeybindingsMergeService } from 'vs/workbench/services/keybinding/common/keybindingsMerge';
let testObject: KeybindingsMergeService;
suiteSetup(() => testObject = workbenchInstantiationService().createInstance(KeybindingsMergeService));
import { mergeKeybindings } from 'vs/platform/userDataSync/common/keybindingsMerge';
suite('KeybindingsMerge - No Conflicts', () => {
test('merge when local and remote are same with one entry', async () => {
test('merge when local and remote are same with one entry', () => {
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);
});
test('merge when local and remote are same with similar when contexts', async () => {
test('merge when local and remote are same with similar when contexts', () => {
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);
});
test('merge when local and remote are same with multiple entries', async () => {
test('merge when local and remote are same with multiple entries', () => {
const localContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
......@@ -42,13 +37,13 @@ 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);
});
test('merge when local and remote are same with different base content', async () => {
test('merge when local and remote are same with different base content', () => {
const localContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
......@@ -63,13 +58,13 @@ 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);
});
test('merge when local and remote are same with multiple entries in different order', async () => {
test('merge when local and remote are same with multiple entries in different order', () => {
const localContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
......@@ -80,13 +75,13 @@ 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);
});
test('merge when local and remote are same when remove entry is in different order', async () => {
test('merge when local and remote are same when remove entry is in different order', () => {
const localContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
......@@ -97,13 +92,13 @@ 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);
});
test('merge when a new entry is added to remote', async () => {
test('merge when a new entry is added to remote', () => {
const localContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
......@@ -113,13 +108,13 @@ 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);
});
test('merge when multiple new entries are added to remote', async () => {
test('merge when multiple new entries are added to remote', () => {
const localContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
......@@ -130,13 +125,13 @@ 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);
});
test('merge when multiple new entries are added to remote from base and local has not changed', async () => {
test('merge when multiple new entries are added to remote from base and local has not changed', () => {
const localContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
......@@ -147,13 +142,13 @@ 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);
});
test('merge when an entry is removed from remote from base and local has not changed', async () => {
test('merge when an entry is removed from remote from base and local has not changed', () => {
const localContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
......@@ -163,13 +158,13 @@ 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);
});
test('merge when an entry (same command) is removed from remote from base and local has not changed', async () => {
test('merge when an entry (same command) is removed from remote from base and local has not changed', () => {
const localContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
......@@ -177,26 +172,26 @@ 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);
});
test('merge when an entry is updated in remote from base and local has not changed', async () => {
test('merge when an entry is updated in remote from base and local has not changed', () => {
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, localContent);
const actual = 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 () => {
test('merge when a command with multiple entries is updated from remote from base and local has not changed', () => {
const localContent = stringify([
{ key: 'shift+c', command: 'c' },
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
......@@ -215,13 +210,13 @@ suite('KeybindingsMerge - No Conflicts', () => {
{ key: 'cmd+d', command: 'a' },
{ key: 'alt+d', command: 'b' },
]);
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, expected);
});
test('merge when remote has moved forwareded with multiple changes and local stays with base', async () => {
test('merge when remote has moved forwareded with multiple changes and local stays with base', () => {
const localContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'cmd+c', command: 'b', args: { text: '`' } },
......@@ -247,13 +242,13 @@ suite('KeybindingsMerge - No Conflicts', () => {
{ key: 'alt+f', command: 'f' },
{ key: 'alt+d', command: '-f' },
]);
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, expected);
});
test('merge when a new entry is added to local', async () => {
test('merge when a new entry is added to local', () => {
const localContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
......@@ -263,13 +258,13 @@ 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);
});
test('merge when multiple new entries are added to local', async () => {
test('merge when multiple new entries are added to local', () => {
const localContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
......@@ -280,13 +275,13 @@ 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);
});
test('merge when multiple new entries are added to local from base and remote is not changed', async () => {
test('merge when multiple new entries are added to local from base and remote is not changed', () => {
const localContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
......@@ -297,13 +292,13 @@ 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);
});
test('merge when an entry is removed from local from base and remote has not changed', async () => {
test('merge when an entry is removed from local from base and remote has not changed', () => {
const localContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+d', command: '-a' },
......@@ -313,13 +308,13 @@ 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);
});
test('merge when an entry (with same command) is removed from local from base and remote has not changed', async () => {
test('merge when an entry (with same command) is removed from local from base and remote has not changed', () => {
const localContent = stringify([
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
]);
......@@ -327,26 +322,26 @@ 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);
});
test('merge when an entry is updated in local from base and remote has not changed', async () => {
test('merge when an entry is updated in local from base and remote has not changed', () => {
const localContent = stringify([
{ key: 'alt+d', command: 'a', when: 'editorTextFocus' },
]);
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);
});
test('merge when a command with multiple entries is updated from local from base and remote has not changed', async () => {
test('merge when a command with multiple entries is updated from local from base and remote has not changed', () => {
const localContent = stringify([
{ key: 'shift+c', command: 'c' },
{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' },
......@@ -359,13 +354,13 @@ 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);
});
test('merge when local has moved forwareded with multiple changes and remote stays with base', async () => {
test('merge when local has moved forwareded with multiple changes and remote stays with base', () => {
const localContent = stringify([
{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'cmd+e', command: 'd' },
......@@ -391,13 +386,13 @@ 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);
});
test('merge when local and remote has moved forwareded with no conflicts', async () => {
test('merge when local and remote has moved forwareded with no conflicts', () => {
const baseContent = stringify([
{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+c', command: '-a' },
......@@ -432,7 +427,7 @@ suite('KeybindingsMerge - No Conflicts', () => {
{ key: 'alt+e', command: 'e' },
{ 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, expected);
......@@ -442,10 +437,10 @@ suite('KeybindingsMerge - No Conflicts', () => {
suite('KeybindingsMerge - Conflicts', () => {
test('merge when local and remote with one entry but different value', async () => {
test('merge when local and remote with one entry but different value', () => {
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,
......@@ -466,7 +461,7 @@ suite('KeybindingsMerge - Conflicts', () => {
]`);
});
test('merge when local and remote with different keybinding', async () => {
test('merge when local and remote with different keybinding', () => {
const localContent = stringify([
{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+a', command: '-a', when: 'editorTextFocus && !editorReadonly' }
......@@ -475,7 +470,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,
......@@ -506,7 +501,7 @@ suite('KeybindingsMerge - Conflicts', () => {
]`);
});
test('merge when local and remote has entries in different order', async () => {
test('merge when local and remote has entries in different order', () => {
const localContent = stringify([
{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' },
{ key: 'alt+a', command: 'a', when: 'editorTextFocus' }
......@@ -515,7 +510,7 @@ suite('KeybindingsMerge - 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,
......@@ -546,11 +541,13 @@ suite('KeybindingsMerge - Conflicts', () => {
]`);
});
test('merge when the entry is removed in local but updated in remote', async () => {
test('merge when the entry is removed in local but updated in remote', () => {
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);
//'[\n<<<<<<< local\n\n=======\t{\n\t\t"key": "alt+c",\n\t\t"command": "a",\n\t\t"when": "editorTextFocus && !editorReadonly"\n\t}\n>>>>>>> remote\n]'
//'[\n<<<<<<< local\n=======\n\t{\n\t\t"key": "alt+c",\n\t\t"command": "a",\n\t\t"when": "editorTextFocus && !editorReadonly"\n\t}\n>>>>>>> remote\n]'
assert.ok(actual.hasChanges);
assert.ok(actual.hasConflicts);
assert.equal(actual.mergeContent,
......@@ -566,11 +563,11 @@ suite('KeybindingsMerge - Conflicts', () => {
]`);
});
test('merge when the entry is removed in local but updated in remote and a new entry is added in local', async () => {
test('merge when the entry is removed in local but updated in remote and a new entry is added in local', () => {
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,
......
/*---------------------------------------------------------------------------------------------
* 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, findNodeAtLocation, parseTree, JSONPath } from 'vs/base/common/json';
import { setProperty } from 'vs/base/common/jsonEdit';
import { IModelService } from 'vs/editor/common/services/modelService';
import { Position } from 'vs/editor/common/core/position';
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 { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IKeybindingsMergeService } from 'vs/platform/userDataSync/common/userDataSync';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
export class KeybindingsMergeService implements IKeybindingsMergeService {
_serviceBrand: undefined;
constructor(
@IModelService private readonly modelService: IModelService
) { }
async merge(localContent: string, remoteContent: string, baseContent: string | null): 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 byCommand = (keybindings: 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;
};
const localByCommand = byCommand(local);
const remoteByCommand = byCommand(remote);
const baseByCommand = base ? byCommand(base) : null;
const localToRemote = this.compare(localByCommand, remoteByCommand);
if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) {
// No changes found between local and remote.
return { mergeContent: localContent, hasChanges: false, hasConflicts: false };
}
const conflictCommands: Set<string> = new Set<string>();
const baseToLocal = baseByCommand ? this.compare(baseByCommand, localByCommand) : { added: keys(localByCommand).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
const baseToRemote = baseByCommand ? this.compare(baseByCommand, remoteByCommand) : { added: keys(remoteByCommand).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
let mergeContent = localContent;
const eol = this.modelService.createModel(mergeContent, null).getEOL();
// Removed commands in Local
for (const command of values(baseToLocal.removed)) {
// Got updated in remote
if (baseToRemote.updated.has(command)) {
conflictCommands.add(command);
}
}
// Removed commands in Remote
for (const command of values(baseToRemote.removed)) {
if (conflictCommands.has(command)) {
continue;
}
// Got updated in local
if (baseToLocal.updated.has(command)) {
conflictCommands.add(command);
} else {
// remove the command
mergeContent = this.removeKeybindings(mergeContent, eol, command);
}
}
// Added commands in Local
for (const command of values(baseToLocal.added)) {
if (conflictCommands.has(command)) {
continue;
}
// Got added in remote
if (baseToRemote.added.has(command)) {
// Has different value
if (localToRemote.updated.has(command)) {
conflictCommands.add(command);
}
}
}
// Added commands in remote
for (const command of values(baseToRemote.added)) {
if (conflictCommands.has(command)) {
continue;
}
// Got added in local
if (baseToLocal.added.has(command)) {
// Has different value
if (localToRemote.updated.has(command)) {
conflictCommands.add(command);
}
} else {
mergeContent = this.addKeybinding(mergeContent, eol, command, remoteByCommand.get(command)!);
}
}
// Updated commands in Local
for (const command of values(baseToLocal.updated)) {
if (conflictCommands.has(command)) {
continue;
}
// Got updated in remote
if (baseToRemote.updated.has(command)) {
// Has different value
if (localToRemote.updated.has(command)) {
conflictCommands.add(command);
}
}
}
// Updated commands in Remote
for (const command of values(baseToRemote.updated)) {
if (conflictCommands.has(command)) {
continue;
}
// Got updated in local
if (baseToLocal.updated.has(command)) {
// Has different value
if (localToRemote.updated.has(command)) {
conflictCommands.add(command);
}
} else {
// update the command
mergeContent = this.updateKeybinding(mergeContent, eol, command, remoteByCommand.get(command)!);
}
}
const conflicts: { command: string, local: IUserFriendlyKeybinding[] | undefined, remote: IUserFriendlyKeybinding[] | undefined, firstIndex: number }[] = [];
for (const command of values(conflictCommands)) {
const local = localByCommand.get(command);
const remote = remoteByCommand.get(command);
mergeContent = this.updateKeybinding(mergeContent, eol, command, [...local || [], ...remote || []]);
conflicts.push({ command, local, remote, firstIndex: -1 });
}
const allKeybindings = <IUserFriendlyKeybinding[]>parse(mergeContent);
for (const conflict of conflicts) {
conflict.firstIndex = findFirstIndex(allKeybindings, keybinding => keybinding.command === conflict.command || keybinding.command === `-${conflict.command}`);
}
// Sort reverse so that conflicts content is added from last
conflicts.sort((a, b) => b.firstIndex - a.firstIndex);
const model = this.modelService.createModel(mergeContent, null);
const tree = parseTree(mergeContent);
for (const { firstIndex, local, remote } of conflicts) {
const firstNode = findNodeAtLocation(tree, [firstIndex])!;
const fistNodePosition = model.getPositionAt(firstNode.offset);
const startLocalOffset = model.getOffsetAt(new Position(fistNodePosition.lineNumber - 1, model.getLineMaxColumn(fistNodePosition.lineNumber - 1)));
let endLocalOffset = startLocalOffset;
let remoteOffset = endLocalOffset;
if (local) {
const lastLocalValueNode = findNodeAtLocation(tree, [firstIndex + local.length - 1])!;
const lastLocalValueEndPosition = model.getPositionAt(lastLocalValueNode.offset + lastLocalValueNode.length);
endLocalOffset = model.getOffsetAt(new Position(lastLocalValueEndPosition.lineNumber, model.getLineMaxColumn(lastLocalValueEndPosition.lineNumber)));
}
if (remote) {
const lastRemoteValueNode = findNodeAtLocation(tree, [firstIndex + (local ? local.length : 0) + remote.length - 1])!;
const lastRemoteValueEndPosition = model.getPositionAt(lastRemoteValueNode.offset + lastRemoteValueNode.length);
remoteOffset = model.getOffsetAt(new Position(lastRemoteValueEndPosition.lineNumber, model.getLineMaxColumn(lastRemoteValueEndPosition.lineNumber)));
}
mergeContent = mergeContent.substring(0, startLocalOffset)
+ `${eol}<<<<<<< local`
+ mergeContent.substring(startLocalOffset, endLocalOffset)
+ `${eol}=======`
+ mergeContent.substring(endLocalOffset, remoteOffset)
+ `${eol}>>>>>>> remote`
+ mergeContent.substring(remoteOffset);
}
return { mergeContent, hasChanges: true, hasConflicts: conflicts.length > 0 };
}
private compare(from: Map<string, IUserFriendlyKeybinding[]>, to: Map<string, IUserFriendlyKeybinding[]>): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
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)!;
const value2: IUserFriendlyKeybinding[] = to.get(key)!;
if (!this.areSameKeybindings(value1, value2)) {
updated.add(key);
}
}
return { added, removed, updated };
}
private areSameKeybindings(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 addKeybinding(content: string, eol: string, command: string, keybindings: IUserFriendlyKeybinding[]): string {
for (const keybinding of keybindings) {
content = this.edit(content, eol, [-1], keybinding);
}
return content;
}
private removeKeybindings(content: string, eol: string, command: string): 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 = this.edit(content, eol, [index], undefined);
}
}
return content;
}
private updateKeybinding(content: string, eol: string, command: string, keybindings: IUserFriendlyKeybinding[]): 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 = this.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 = this.edit(content, eol, [location], keybindings[index]);
}
return content;
}
private edit(content: string, eol: string, originalPath: JSONPath, value: any): string {
const edit = setProperty(content, originalPath, value, { tabSize: 4, insertSpaces: false, eol })[0];
if (edit) {
content = content.substring(0, edit.offset) + edit.content + content.substring(edit.offset + edit.length);
}
return content;
}
}
registerSingleton(IKeybindingsMergeService, KeybindingsMergeService);
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册