提交 63afb956 编写于 作者: S Sandeep Somavarapu

Enable keybindings synchronization

上级 7be71103
......@@ -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')); }
......
......@@ -5,7 +5,7 @@
import { Disposable } from 'vs/base/common/lifecycle';
import { IFileService, FileSystemProviderErrorCode, FileSystemProviderError, IFileContent } from 'vs/platform/files/common/files';
import { IUserData, UserDataSyncStoreError, UserDataSyncStoreErrorCode, ISynchroniser, SyncStatus, ISettingsMergeService, IUserDataSyncStoreService, DEFAULT_IGNORED_SETTINGS, IUserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserData, UserDataSyncStoreError, UserDataSyncStoreErrorCode, ISynchroniser, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSync';
import { VSBuffer } from 'vs/base/common/buffer';
import { parse, ParseError } from 'vs/base/common/json';
import { localize } from 'vs/nls';
......@@ -15,14 +15,23 @@ 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 { startsWith } from 'vs/base/common/strings';
import { CancellationToken } from 'vs/base/common/cancellation';
import { mergeKeybindings } from 'vs/platform/userDataSync/common/keybindingsMerge';
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>());
......@@ -69,12 +78,17 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser
}
}
async sync(): Promise<boolean> {
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;
......@@ -83,6 +97,152 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser
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.writeToLocal(content, fileContent);
}
if (hasRemoteChanged) {
this.logService.info('Keybindings: Updating remote keybindings');
const ref = await this.writeToRemote(content, remoteUserData.ref);
remoteUserData = { ref, content };
}
if (remoteUserData.content) {
this.logService.info('Keybindings: Updating last synchronised keybindings');
await this.updateLastSyncValue(remoteUserData);
}
// 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 remoteUserData = await this.userDataSyncStoreService.read(KeybindingsSynchroniser.EXTERNAL_USER_DATA_KEYBINDINGS_KEY, lastSyncData);
const remoteContent: string | null = remoteUserData.content;
// Get file content last to get the latest
const fileContent = await this.getLocalFileContent();
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 (!lastSyncData // First time sync
|| lastSyncData.content !== localContent // Local has forwarded
|| lastSyncData.content !== remoteContent // Remote has forwarded
) {
this.logService.trace('Keybindings: Merging remote keybindings with local keybindings...');
const result = mergeKeybindings(localContent, remoteContent, lastSyncData ? lastSyncData.content : null);
// 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 getLastSyncUserData(): Promise<IUserData | null> {
......@@ -96,7 +256,7 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser
private async getLocalFileContent(): Promise<IFileContent | null> {
try {
return await this.fileService.readFile(this.environmentService.settingsResource);
return await this.fileService.readFile(this.environmentService.keybindingsResource);
} catch (error) {
return null;
}
......@@ -109,14 +269,14 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser
private async writeToLocal(newContent: string, oldContent: IFileContent | null): Promise<void> {
if (oldContent) {
// file exists already
await this.fileService.writeFile(this.environmentService.settingsResource, VSBuffer.fromString(newContent), oldContent);
await this.fileService.writeFile(this.environmentService.keybindingsResource, VSBuffer.fromString(newContent), oldContent);
} else {
// file does not exist
await this.fileService.createFile(this.environmentService.settingsResource, VSBuffer.fromString(newContent), { overwrite: false });
await this.fileService.createFile(this.environmentService.keybindingsResource, VSBuffer.fromString(newContent), { overwrite: false });
}
}
private async updateLastSyncValue(remoteUserData: IUserData): Promise<void> {
await this.fileService.writeFile(this.lastSyncSettingsResource, VSBuffer.fromString(JSON.stringify(remoteUserData)));
await this.fileService.writeFile(this.lastSyncKeybindingsResource, VSBuffer.fromString(JSON.stringify(remoteUserData)));
}
}
......@@ -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;
}
......
......@@ -52,6 +52,12 @@ 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.ignoredExtensions': {
'type': 'array',
description: localize('sync.ignoredExtensions', "Configure extensions to be ignored while synchronizing."),
......@@ -132,6 +138,7 @@ export interface ISyncExtension {
export const enum SyncSource {
Settings = 1,
Keybindings,
Extensions
}
......
......@@ -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"),
iconLocation: {
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',
......
......@@ -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'); }
......
......@@ -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]);
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册