未验证 提交 e5687144 编写于 作者: S Sandeep Somavarapu 提交者: GitHub

Merge pull request #85894 from microsoft/sandy081/syncKeybindings

Enable synchronising keybindings
......@@ -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 } 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,6 +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 { UserKeybindingsResolverServiceClient } from 'vs/platform/userDataSync/common/keybindingsSyncIpc';
export interface ISharedProcessConfiguration {
readonly machineId: string;
......@@ -186,6 +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));
services.set(IUserKeybindingsResolverService, new UserKeybindingsResolverServiceClient(server.getChannel('userKeybindingsResolver', activeWindowRouter)));
services.set(IUserDataSyncStoreService, new SyncDescriptor(UserDataSyncStoreService));
services.set(IUserDataSyncService, new SyncDescriptor(UserDataSyncService));
registerConfiguration();
......
......@@ -127,6 +127,7 @@ export interface IEnvironmentService extends IUserHomeProvider {
// sync resources
userDataSyncLogResource: URI;
settingsSyncPreviewResource: URI;
keybindingsSyncPreviewResource: URI;
machineSettingsHome: URI;
machineSettingsResource: URI;
......
......@@ -114,6 +114,9 @@ export class EnvironmentService implements IEnvironmentService {
@memoize
get settingsSyncPreviewResource(): URI { return resources.joinPath(this.userRoamingDataHome, '.settings.json'); }
@memoize
get keybindingsSyncPreviewResource(): URI { return resources.joinPath(this.userRoamingDataHome, '.keybindings.json'); }
@memoize
get userDataSyncLogResource(): URI { return URI.file(path.join(this.logsPath, 'userDataSync.log')); }
......
/*---------------------------------------------------------------------------------------------
* 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 } 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<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 function merge(localContent: string, remoteContent: string, baseContent: string | null, normalizedKeys: IStringDictionary<string>): { mergeContent: string, hasChanges: boolean, hasConflicts: boolean } {
const local = <IUserFriendlyKeybinding[]>parse(localContent);
const remote = <IUserFriendlyKeybinding[]>parse(remoteContent);
const base = baseContent ? <IUserFriendlyKeybinding[]>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<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);
const eol = contentUtil.getEol(localContent);
let mergeContent = localContent;
// Removed commands in Remote
for (const command of values(commandsMergeResult.removed)) {
if (commandsMergeResult.conflicts.has(command)) {
continue;
}
mergeContent = removeKeybindings(mergeContent, eol, command);
}
// 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);
}
// 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<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, 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 = <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 updateKeybindings(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;
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { IFileService, FileSystemProviderErrorCode, FileSystemProviderError, IFileContent } from 'vs/platform/files/common/files';
import { IUserData, UserDataSyncStoreError, UserDataSyncStoreErrorCode, ISynchroniser, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, 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';
import { Emitter, Event } from 'vs/base/common/event';
import { CancelablePromise, createCancelablePromise, ThrottledDelayer } from 'vs/base/common/async';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { URI } from 'vs/base/common/uri';
import { joinPath } from 'vs/base/common/resources';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { CancellationToken } from 'vs/base/common/cancellation';
import { OS, OperatingSystem } from 'vs/base/common/platform';
import { isUndefined } from 'vs/base/common/types';
interface ISyncContent {
mac?: string;
linux?: string;
windows?: string;
all?: string;
}
interface ISyncPreviewResult {
readonly fileContent: IFileContent | null;
readonly remoteUserData: IUserData;
readonly hasLocalChanged: boolean;
readonly hasRemoteChanged: boolean;
readonly hasConflicts: boolean;
}
export class KeybindingsSynchroniser extends Disposable implements ISynchroniser {
private static EXTERNAL_USER_DATA_KEYBINDINGS_KEY: string = 'keybindings';
private syncPreviewResultPromise: CancelablePromise<ISyncPreviewResult> | null = null;
private _status: SyncStatus = SyncStatus.Idle;
get status(): SyncStatus { return this._status; }
private _onDidChangStatus: Emitter<SyncStatus> = this._register(new Emitter<SyncStatus>());
readonly onDidChangeStatus: Event<SyncStatus> = this._onDidChangStatus.event;
private readonly throttledDelayer: ThrottledDelayer<void>;
private _onDidChangeLocal: Emitter<void> = this._register(new Emitter<void>());
readonly onDidChangeLocal: Event<void> = this._onDidChangeLocal.event;
private readonly lastSyncKeybindingsResource: URI;
constructor(
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IFileService private readonly fileService: IFileService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IUserKeybindingsResolverService private readonly userKeybindingsResolverService: IUserKeybindingsResolverService,
) {
super();
this.lastSyncKeybindingsResource = joinPath(this.environmentService.userRoamingDataHome, '.lastSyncKeybindings.json');
this.throttledDelayer = this._register(new ThrottledDelayer<void>(500));
this._register(this.fileService.watch(this.environmentService.keybindingsResource));
this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.environmentService.keybindingsResource))(() => this.throttledDelayer.trigger(() => this.onDidChangeKeybindings())));
}
private async onDidChangeKeybindings(): Promise<void> {
const localFileContent = await this.getLocalContent();
const lastSyncData = await this.getLastSyncUserData();
if (localFileContent && lastSyncData) {
if (localFileContent.value.toString() !== lastSyncData.content) {
this._onDidChangeLocal.fire();
return;
}
}
if (!localFileContent || !lastSyncData) {
this._onDidChangeLocal.fire();
return;
}
}
private setStatus(status: SyncStatus): void {
if (this._status !== status) {
this._status = status;
this._onDidChangStatus.fire(status);
}
}
async sync(_continue?: boolean): Promise<boolean> {
if (!this.configurationService.getValue<boolean>('sync.enableKeybindings')) {
this.logService.trace('Keybindings: Skipping synchronizing keybindings as it is disabled.');
return false;
}
if (_continue) {
this.logService.info('Keybindings: Resumed synchronizing keybindings');
return this.continueSync();
}
if (this.status !== SyncStatus.Idle) {
this.logService.trace('Keybindings: Skipping synchronizing keybindings as it is running already.');
return false;
}
this.logService.trace('Keybindings: Started synchronizing keybindings...');
this.setStatus(SyncStatus.Syncing);
try {
const result = await this.getPreview();
if (result.hasConflicts) {
this.logService.info('Keybindings: Detected conflicts while synchronizing keybindings.');
this.setStatus(SyncStatus.HasConflicts);
return false;
}
await this.apply();
return true;
} catch (e) {
this.syncPreviewResultPromise = null;
this.setStatus(SyncStatus.Idle);
if (e instanceof UserDataSyncStoreError && e.code === UserDataSyncStoreErrorCode.Rejected) {
// Rejected as there is a new remote version. Syncing again,
this.logService.info('Keybindings: Failed to synchronise keybindings as there is a new remote version available. Synchronizing again...');
return this.sync();
}
if (e instanceof FileSystemProviderError && e.code === FileSystemProviderErrorCode.FileExists) {
// Rejected as there is a new local version. Syncing again.
this.logService.info('Keybindings: Failed to synchronise keybindings as there is a new local version available. Synchronizing again...');
return this.sync();
}
throw e;
}
}
stop(): void {
if (this.syncPreviewResultPromise) {
this.syncPreviewResultPromise.cancel();
this.syncPreviewResultPromise = null;
this.logService.info('Keybindings: Stopped synchronizing keybindings.');
}
this.fileService.del(this.environmentService.keybindingsSyncPreviewResource);
this.setStatus(SyncStatus.Idle);
}
private async continueSync(): Promise<boolean> {
if (this.status !== SyncStatus.HasConflicts) {
return false;
}
await this.apply();
return true;
}
private async apply(): Promise<void> {
if (!this.syncPreviewResultPromise) {
return;
}
if (await this.fileService.exists(this.environmentService.keybindingsSyncPreviewResource)) {
const keybindingsPreivew = await this.fileService.readFile(this.environmentService.keybindingsSyncPreviewResource);
const content = keybindingsPreivew.value.toString();
if (this.hasErrors(content)) {
const error = new Error(localize('errorInvalidKeybindings', "Unable to sync keybindings. Please resolve conflicts without any errors/warnings and try again."));
this.logService.error(error);
throw error;
}
let { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged } = await this.syncPreviewResultPromise;
if (!hasLocalChanged && !hasRemoteChanged) {
this.logService.trace('Keybindings: No changes found during synchronizing keybindings.');
}
if (hasLocalChanged) {
this.logService.info('Keybindings: Updating local keybindings');
await this.updateLocalContent(content, fileContent);
}
if (hasRemoteChanged) {
this.logService.info('Keybindings: Updating remote keybindings');
const remoteContents = this.updateSyncContent(content, remoteUserData.content);
const ref = await this.updateRemoteUserData(remoteContents, remoteUserData.ref);
remoteUserData = { ref, content: remoteContents };
}
if (remoteUserData.content) {
this.logService.info('Keybindings: Updating last synchronised keybindings');
const lastSyncContent = this.updateSyncContent(content, null);
await this.updateLastSyncUserData({ ref: remoteUserData.ref, content: lastSyncContent });
}
// Delete the preview
await this.fileService.del(this.environmentService.keybindingsSyncPreviewResource);
} else {
this.logService.trace('Keybindings: No changes found during synchronizing keybindings.');
}
this.logService.trace('Keybindings: Finised synchronizing keybindings.');
this.syncPreviewResultPromise = null;
this.setStatus(SyncStatus.Idle);
}
private hasErrors(content: string): boolean {
const parseErrors: ParseError[] = [];
parse(content, parseErrors, { allowEmptyContent: true, allowTrailingComma: true });
return parseErrors.length > 0;
}
private getPreview(): Promise<ISyncPreviewResult> {
if (!this.syncPreviewResultPromise) {
this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(token));
}
return this.syncPreviewResultPromise;
}
private async generatePreview(token: CancellationToken): Promise<ISyncPreviewResult> {
const lastSyncData = await this.getLastSyncUserData();
const lastSyncContent = lastSyncData && lastSyncData.content ? this.getKeybindingsContentFromSyncContent(lastSyncData.content) : null;
const remoteUserData = await this.getRemoteUserData(lastSyncData);
const remoteContent = remoteUserData.content ? this.getKeybindingsContentFromSyncContent(remoteUserData.content) : null;
// Get file content last to get the latest
const fileContent = await this.getLocalContent();
let hasLocalChanged: boolean = false;
let hasRemoteChanged: boolean = false;
let hasConflicts: boolean = false;
let previewContent = null;
if (remoteContent) {
const localContent: string = fileContent ? fileContent.value.toString() : '[]';
if (this.hasErrors(localContent)) {
this.logService.error('Keybindings: Unable to sync keybindings as there are errors/warning in keybindings file.');
return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts };
}
if (!lastSyncContent // First time sync
|| lastSyncContent !== localContent // Local has forwarded
|| lastSyncContent !== remoteContent // Remote has forwarded
) {
this.logService.trace('Keybindings: Merging remote keybindings with local keybindings...');
const keys = await this.userKeybindingsResolverService.resolveUserKeybindings(localContent, remoteContent, lastSyncContent);
const result = merge(localContent, remoteContent, lastSyncContent, keys);
// Sync only if there are changes
if (result.hasChanges) {
hasLocalChanged = result.mergeContent !== localContent;
hasRemoteChanged = result.mergeContent !== remoteContent;
hasConflicts = result.hasConflicts;
previewContent = result.mergeContent;
}
}
}
// First time syncing to remote
else if (fileContent) {
this.logService.info('Keybindings: Remote keybindings does not exist. Synchronizing keybindings for the first time.');
hasRemoteChanged = true;
previewContent = fileContent.value.toString();
}
if (previewContent && !token.isCancellationRequested) {
await this.fileService.writeFile(this.environmentService.keybindingsSyncPreviewResource, VSBuffer.fromString(previewContent));
}
return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts };
}
private async getLocalContent(): Promise<IFileContent | null> {
try {
return await this.fileService.readFile(this.environmentService.keybindingsResource);
} catch (error) {
return null;
}
}
private async updateLocalContent(newContent: string, oldContent: IFileContent | null): Promise<void> {
if (oldContent) {
// file exists already
await this.fileService.writeFile(this.environmentService.keybindingsResource, VSBuffer.fromString(newContent), oldContent);
} else {
// file does not exist
await this.fileService.createFile(this.environmentService.keybindingsResource, VSBuffer.fromString(newContent), { overwrite: false });
}
}
private async getLastSyncUserData(): Promise<IUserData | null> {
try {
const content = await this.fileService.readFile(this.lastSyncKeybindingsResource);
return JSON.parse(content.value.toString());
} catch (error) {
return null;
}
}
private async updateLastSyncUserData(remoteUserData: IUserData): Promise<void> {
await this.fileService.writeFile(this.lastSyncKeybindingsResource, VSBuffer.fromString(JSON.stringify(remoteUserData)));
}
private async getRemoteUserData(lastSyncData: IUserData | null): Promise<IUserData> {
return this.userDataSyncStoreService.read(KeybindingsSynchroniser.EXTERNAL_USER_DATA_KEYBINDINGS_KEY, lastSyncData);
}
private async updateRemoteUserData(content: string, ref: string | null): Promise<string> {
return this.userDataSyncStoreService.write(KeybindingsSynchroniser.EXTERNAL_USER_DATA_KEYBINDINGS_KEY, content, ref);
}
private getKeybindingsContentFromSyncContent(syncContent: string): string | null {
try {
const parsed = <ISyncContent>JSON.parse(syncContent);
if (!this.configurationService.getValue<boolean>('sync.keybindingsPerPlatform')) {
return isUndefined(parsed.all) ? null : parsed.all;
}
switch (OS) {
case OperatingSystem.Macintosh:
return isUndefined(parsed.mac) ? null : parsed.mac;
case OperatingSystem.Linux:
return isUndefined(parsed.linux) ? null : parsed.linux;
case OperatingSystem.Windows:
return isUndefined(parsed.windows) ? null : parsed.windows;
}
} catch (e) {
this.logService.error(e);
return null;
}
}
private updateSyncContent(keybindingsContent: string, syncContent: string | null): string {
let parsed: ISyncContent = {};
try {
parsed = JSON.parse(syncContent || '{}');
} catch (e) {
this.logService.error(e);
}
if (!this.configurationService.getValue<boolean>('sync.keybindingsPerPlatform')) {
parsed.all = keybindingsContent;
} else {
delete parsed.all;
}
switch (OS) {
case OperatingSystem.Macintosh:
parsed.mac = keybindingsContent;
break;
case OperatingSystem.Linux:
parsed.linux = keybindingsContent;
break;
case OperatingSystem.Windows:
parsed.windows = keybindingsContent;
break;
}
return JSON.stringify(parsed);
}
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { Event } from 'vs/base/common/event';
import { IUserKeybindingsResolverService } from 'vs/platform/userDataSync/common/userDataSync';
import { IStringDictionary } from 'vs/base/common/collections';
export class UserKeybindingsResolverServiceChannel implements IServerChannel {
constructor(private readonly service: IUserKeybindingsResolverService) { }
listen(_: unknown, event: string): Event<any> {
throw new Error(`Event not found: ${event}`);
}
call(context: any, command: string, args?: any): Promise<any> {
switch (command) {
case 'resolveUserKeybindings': return this.service.resolveUserKeybindings(args[0], args[1], args[2]);
}
throw new Error('Invalid call');
}
}
export class UserKeybindingsResolverServiceClient implements IUserKeybindingsResolverService {
_serviceBrand: undefined;
constructor(private readonly channel: IChannel) {
}
async resolveUserKeybindings(localContent: string, remoteContent: string, baseContent: string | null): Promise<IStringDictionary<string>> {
return this.channel.call('resolveUserKeybindings', [localContent, remoteContent, baseContent]);
}
}
......@@ -135,10 +135,9 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser {
}
private async continueSync(): Promise<boolean> {
if (this.status !== SyncStatus.HasConflicts) {
return false;
if (this.status === SyncStatus.HasConflicts) {
await this.apply();
}
await this.apply();
return true;
}
......@@ -218,8 +217,8 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser {
}
if (!lastSyncData // First time sync
|| lastSyncData.content !== localContent // Local has moved forwarded
|| lastSyncData.content !== remoteContent // Remote has moved forwarded
|| lastSyncData.content !== localContent // Local has forwarded
|| lastSyncData.content !== remoteContent // Remote has forwarded
) {
this.logService.trace('Settings: Merging remote settings with local settings...');
const result = await this.settingsMergeService.merge(localContent, remoteContent, lastSyncData ? lastSyncData.content : null, this.getIgnoredSettings());
......
......@@ -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';
......@@ -52,6 +53,18 @@ export function registerConfiguration(): IDisposable {
default: true,
scope: ConfigurationScope.APPLICATION,
},
'sync.enableKeybindings': {
type: 'boolean',
description: localize('sync.enableKeybindings', "Enable synchronizing keybindings."),
default: true,
scope: ConfigurationScope.APPLICATION,
},
'sync.keybindingsPerPlatform': {
type: 'boolean',
description: localize('sync.keybindingsPerPlatform', "Synchronize keybindings per platform."),
default: true,
scope: ConfigurationScope.APPLICATION,
},
'sync.ignoredExtensions': {
'type': 'array',
description: localize('sync.ignoredExtensions', "Configure extensions to be ignored while synchronizing."),
......@@ -132,6 +145,7 @@ export interface ISyncExtension {
export const enum SyncSource {
Settings = 1,
Keybindings,
Extensions
}
......@@ -173,6 +187,16 @@ export interface ISettingsMergeService {
}
export const IUserKeybindingsResolverService = createDecorator<IUserKeybindingsResolverService>('IUserKeybindingsResolverService');
export interface IUserKeybindingsResolverService {
_serviceBrand: undefined;
resolveUserKeybindings(localContent: string, remoteContent: string, baseContent: string | null): Promise<IStringDictionary<string>>;
}
export const IUserDataSyncLogService = createDecorator<IUserDataSyncLogService>('IUserDataSyncLogService');
export interface IUserDataSyncLogService extends ILogService {
......
......@@ -13,6 +13,7 @@ import { timeout } from 'vs/base/common/async';
import { ExtensionsSynchroniser } from 'vs/platform/userDataSync/common/extensionsSync';
import { IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { IAuthTokenService, AuthTokenStatus } from 'vs/platform/auth/common/auth';
import { KeybindingsSynchroniser } from 'vs/platform/userDataSync/common/keybindingsSync';
export class UserDataSyncService extends Disposable implements IUserDataSyncService {
......@@ -31,6 +32,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
get conflictsSource(): SyncSource | null { return this._conflictsSource; }
private readonly settingsSynchroniser: SettingsSynchroniser;
private readonly keybindingsSynchroniser: KeybindingsSynchroniser;
private readonly extensionsSynchroniser: ExtensionsSynchroniser;
constructor(
......@@ -40,8 +42,9 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
) {
super();
this.settingsSynchroniser = this._register(this.instantiationService.createInstance(SettingsSynchroniser));
this.keybindingsSynchroniser = this._register(this.instantiationService.createInstance(KeybindingsSynchroniser));
this.extensionsSynchroniser = this._register(this.instantiationService.createInstance(ExtensionsSynchroniser));
this.synchronisers = [this.settingsSynchroniser, this.extensionsSynchroniser];
this.synchronisers = [this.settingsSynchroniser, this.keybindingsSynchroniser, this.extensionsSynchroniser];
this.updateStatus();
if (this.userDataSyncStoreService.userDataSyncStore) {
......@@ -111,6 +114,9 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
if (source instanceof SettingsSynchroniser) {
return SyncSource.Settings;
}
if (source instanceof KeybindingsSynchroniser) {
return SyncSource.Keybindings;
}
}
return null;
}
......
......@@ -189,6 +189,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
const items = [{
id: 'sync.enableSettings',
label: localize('user settings', "User Settings")
}, {
id: 'sync.enableKeybindings',
label: localize('user keybindings', "User Keybindings")
}, {
id: 'sync.enableExtensions',
label: localize('extensions', "Extensions")
......@@ -251,13 +254,14 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
}
private getPreviewEditorInput(): IEditorInput | undefined {
return this.editorService.editors.filter(input => isEqual(input.getResource(), this.workbenchEnvironmentService.settingsSyncPreviewResource))[0];
return this.editorService.editors.filter(input => isEqual(input.getResource(), this.workbenchEnvironmentService.settingsSyncPreviewResource) || isEqual(input.getResource(), this.workbenchEnvironmentService.keybindingsSyncPreviewResource))[0];
}
private async handleConflicts(): Promise<void> {
if (this.userDataSyncService.conflictsSource === SyncSource.Settings) {
const conflictsResource = this.getConflictsResource();
if (conflictsResource) {
const resourceInput = {
resource: this.workbenchEnvironmentService.settingsSyncPreviewResource,
resource: conflictsResource,
options: {
preserveFocus: false,
pinned: false,
......@@ -279,6 +283,16 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
}
}
private getConflictsResource(): URI | null {
if (this.userDataSyncService.conflictsSource === SyncSource.Settings) {
return this.workbenchEnvironmentService.settingsSyncPreviewResource;
}
if (this.userDataSyncService.conflictsSource === SyncSource.Keybindings) {
return this.workbenchEnvironmentService.keybindingsSyncPreviewResource;
}
return null;
}
private registerActions(): void {
const startSyncMenuItem: IMenuItem = {
......@@ -380,6 +394,19 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
order: 1,
when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.isEqualTo(SyncStatus.HasConflicts), ResourceContextKey.Resource.isEqualTo(this.workbenchEnvironmentService.settingsSyncPreviewResource.toString())),
});
MenuRegistry.appendMenuItem(MenuId.EditorTitle, {
command: {
id: continueSyncCommandId,
title: localize('continue sync', "Sync: Continue"),
icon: {
light: SYNC_PUSH_LIGHT_ICON_URI,
dark: SYNC_PUSH_DARK_ICON_URI
}
},
group: 'navigation',
order: 1,
when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.isEqualTo(SyncStatus.HasConflicts), ResourceContextKey.Resource.isEqualTo(this.workbenchEnvironmentService.keybindingsSyncPreviewResource.toString())),
});
const signOutMenuItem: IMenuItem = {
group: '5_sync',
......
......@@ -4,19 +4,22 @@
*--------------------------------------------------------------------------------------------*/
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { ISettingsMergeService } 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 { UserKeybindingsResolverServiceChannel } from 'vs/platform/userDataSync/common/keybindingsSyncIpc';
class UserDataSyncServicesContribution implements IWorkbenchContribution {
constructor(
@ISettingsMergeService settingsMergeService: ISettingsMergeService,
@IUserKeybindingsResolverService keybindingsMergeService: IUserKeybindingsResolverService,
@ISharedProcessService sharedProcessService: ISharedProcessService,
) {
sharedProcessService.registerChannel('settingsMerge', new SettingsMergeChannel(settingsMergeService));
sharedProcessService.registerChannel('userKeybindingsResolver', new UserKeybindingsResolverServiceChannel(keybindingsMergeService));
}
}
......
......@@ -136,6 +136,9 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment
@memoize
get settingsSyncPreviewResource(): URI { return joinPath(this.userRoamingDataHome, '.settings.json'); }
@memoize
get keybindingsSyncPreviewResource(): URI { return joinPath(this.userRoamingDataHome, '.keybindings.json'); }
@memoize
get userDataSyncLogResource(): URI { return joinPath(this.options.logsPath, 'userDataSync.log'); }
......
/*---------------------------------------------------------------------------------------------
* 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<IStringDictionary<string>> {
const local = <IUserFriendlyKeybinding[]>parse(localContent);
const remote = <IUserFriendlyKeybinding[]>parse(remoteContent);
const base = baseContent ? <IUserFriendlyKeybinding[]>parse(baseContent) : null;
const keys: IStringDictionary<string> = {};
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);
......@@ -156,7 +156,7 @@ class SettingsMergeService implements ISettingsMergeService {
const remote = parse(remoteContent);
const remoteModel = this.modelService.createModel(localContent, this.modeService.create('jsonc'));
const ignored = ignoredSettings.reduce((set, key) => { set.add(key); return set; }, new Set<string>());
for (const key of Object.keys(ignoredSettings)) {
for (const key of ignoredSettings) {
if (ignored.has(key)) {
this.editSetting(remoteModel, key, undefined);
this.editSetting(remoteModel, key, remote[key]);
......
......@@ -80,6 +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/userDataSync/common/keybindingsMerge';
import 'vs/workbench/services/path/common/remotePathService';
import 'vs/workbench/services/remote/common/remoteExplorerService';
import 'vs/workbench/services/workingCopy/common/workingCopyService';
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册