Skip to content

Commit

Permalink
Accept and sync changes
Browse files Browse the repository at this point in the history
  • Loading branch information
Sandeep Somavarapu authored and Sandeep Somavarapu committed Sep 12, 2019
1 parent 2b5da56 commit 889c7a8
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { FalseContext } from 'vs/platform/contextkey/common/contextkeys';
import { IActivityService, IBadge, NumberBadge, ProgressBadge } from 'vs/workbench/services/activity/common/activity';
import { GLOBAL_ACTIVITY_ID } from 'vs/workbench/common/activity';
import { timeout } from 'vs/base/common/async';
import { registerEditorContribution } from 'vs/editor/browser/editorExtensions';
import { AcceptChangesController } from 'vs/workbench/contrib/userData/browser/userDataPreviewEditorContribution';

const CONTEXT_SYNC_STATE = new RawContextKey<string>('syncStatus', SyncStatus.Uninitialized);

Expand Down Expand Up @@ -129,7 +131,7 @@ class SyncContribution extends Disposable implements IWorkbenchContribution {
when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), ContextKeyExpr.has('config.userConfiguration.autoSync')),
});

CommandsRegistry.registerCommand('sync.resolveConflicts', serviceAccessor => serviceAccessor.get(IUserDataSyncService).resolveConflicts());
CommandsRegistry.registerCommand('sync.resolveConflicts', serviceAccessor => serviceAccessor.get(IUserDataSyncService).handleConflicts());
MenuRegistry.appendMenuItem(MenuId.GlobalActivity, {
group: '5_sync',
command: {
Expand Down Expand Up @@ -174,3 +176,5 @@ class SyncContribution extends Disposable implements IWorkbenchContribution {
const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
workbenchRegistry.registerWorkbenchContribution(SyncContribution, LifecyclePhase.Starting);
workbenchRegistry.registerWorkbenchContribution(AutoSyncUserData, LifecyclePhase.Eventually);

registerEditorContribution(AcceptChangesController);
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import * as editorCommon from 'vs/editor/common/editorCommon';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { FloatingClickWidget } from 'vs/workbench/browser/parts/editor/editorWidgets';
import { localize } from 'vs/nls';
import { IUserDataSyncService, SETTINGS_PREVIEW_RESOURCE } from 'vs/workbench/services/userData/common/userData';
import { isEqual } from 'vs/base/common/resources';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { EditorOption } from 'vs/editor/common/config/editorOptions';

export class AcceptChangesController extends Disposable implements editorCommon.IEditorContribution {

private static readonly ID = 'editor.contrib.sync.acceptChanges';

static get(editor: ICodeEditor): AcceptChangesController {
return editor.getContribution<AcceptChangesController>(AcceptChangesController.ID);
}

private readonly acceptChangesWidgetRenderer: MutableDisposable<AcceptChangesWidgetRenderer>;

constructor(
private editor: ICodeEditor,
@IInstantiationService private readonly instantiationService: IInstantiationService
) {
super();

this.acceptChangesWidgetRenderer = this._register(new MutableDisposable<AcceptChangesWidgetRenderer>());
this._register(this.editor.onDidChangeModel(() => this.update()));
this.update();
}

getId(): string {
return AcceptChangesController.ID;
}

private update(): void {
if (this.isInterestingEditorModel()) {
if (!this.acceptChangesWidgetRenderer.value) {
this.acceptChangesWidgetRenderer.value = this.instantiationService.createInstance(AcceptChangesWidgetRenderer, this.editor);
}
} else {
this.acceptChangesWidgetRenderer.clear();
}
}

private isInterestingEditorModel(): boolean {
const model = this.editor.getModel();
if (!model) {
return false;
}
return isEqual(model.uri, SETTINGS_PREVIEW_RESOURCE, false);
}
}

export class AcceptChangesWidgetRenderer extends Disposable {

constructor(
private readonly editor: ICodeEditor,
@IInstantiationService instantiationService: IInstantiationService,
@IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService,
@IEditorService private readonly editorService: IEditorService,
@ITextFileService private readonly textFileService: ITextFileService,
@INotificationService private readonly notificationService: INotificationService
) {
super();

const floatingClickWidget = this._register(instantiationService.createInstance(FloatingClickWidget, editor, localize('Accept', "Accept & Sync"), null));
this._register(floatingClickWidget.onClick(() => this.acceptChanges()));
floatingClickWidget.render();
}

private async acceptChanges(): Promise<void> {
// Do not accept if editor is readonly
if (this.editor.getOption(EditorOption.readOnly)) {
return;
}

const model = this.editor.getModel();
if (model) {
// Disable updating
this.editor.updateOptions({ readOnly: true });
// Save the preview
await this.textFileService.save(model.uri);

try {
// Apply Preview
await this.userDataSyncService.apply(model.uri);
} catch (error) {
this.notificationService.error(error);
// Enable updating
this.editor.updateOptions({ readOnly: false });
return;
}

// Close all preview editors
const editorInputs = this.editorService.editors.filter(input => isEqual(input.getResource(), model.uri));
for (const input of editorInputs) {
input.dispose();
}
}

}
}
69 changes: 37 additions & 32 deletions src/vs/workbench/services/userData/common/settingsSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import { Disposable } from 'vs/base/common/lifecycle';
import { IFileService, FileSystemProviderErrorCode, FileSystemProviderError, IFileContent } from 'vs/platform/files/common/files';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IRemoteUserDataService, IUserData, RemoteUserDataError, RemoteUserDataErrorCode, ISynchroniser, SyncStatus } from 'vs/workbench/services/userData/common/userData';
import { IRemoteUserDataService, IUserData, RemoteUserDataError, RemoteUserDataErrorCode, ISynchroniser, SyncStatus, SETTINGS_PREVIEW_RESOURCE } from 'vs/workbench/services/userData/common/userData';
import { VSBuffer } from 'vs/base/common/buffer';
import { parse, findNodeAtLocation, parseTree } from 'vs/base/common/json';
import { parse, findNodeAtLocation, parseTree, ParseError } from 'vs/base/common/json';
import { URI } from 'vs/base/common/uri';
import { ITextModel } from 'vs/editor/common/model';
import { IModelService } from 'vs/editor/common/services/modelService';
Expand All @@ -24,8 +24,8 @@ import { EditOperation } from 'vs/editor/common/core/editOperation';
import { Emitter, Event } from 'vs/base/common/event';
import { ILogService } from 'vs/platform/log/common/log';
import { Position } from 'vs/editor/common/core/position';
import { InMemoryFileSystemProvider } from 'vs/workbench/services/userData/common/inMemoryUserDataProvider';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
import { isEqual } from 'vs/base/common/resources';

interface ISyncPreviewResult {
readonly fileContent: IFileContent | null;
Expand All @@ -35,14 +35,11 @@ interface ISyncPreviewResult {
readonly hasConflicts: boolean;
}

export class SettingsSyncService extends Disposable implements ISynchroniser {
_serviceBrand: undefined;
export class SettingsSynchroniser extends Disposable implements ISynchroniser {

private static LAST_SYNC_SETTINGS_STORAGE_KEY: string = 'LAST_SYNC_SETTINGS_CONTENTS';
private static EXTERNAL_USER_DATA_SETTINGS_KEY: string = 'settings';

private readonly settingsPreviewResource: URI;

private syncPreviewResultPromise: Promise<ISyncPreviewResult> | null = null;

private _status: SyncStatus = SyncStatus.Idle;
Expand All @@ -62,10 +59,6 @@ export class SettingsSyncService extends Disposable implements ISynchroniser {
@IHistoryService private readonly historyService: IHistoryService,
) {
super();

const settingsPreviewScheme = 'vscode-in-memory';
this.settingsPreviewResource = URI.file('Settings-Preview').with({ scheme: settingsPreviewScheme });
this.fileService.registerProvider(settingsPreviewScheme, new InMemoryFileSystemProvider());
}

private setStatus(status: SyncStatus): void {
Expand All @@ -89,7 +82,7 @@ export class SettingsSyncService extends Disposable implements ISynchroniser {
this.setStatus(SyncStatus.HasConflicts);
return false;
}
await this.apply();
await this.apply(SETTINGS_PREVIEW_RESOURCE);
return true;
} catch (e) {
this.syncPreviewResultPromise = null;
Expand All @@ -108,28 +101,39 @@ export class SettingsSyncService extends Disposable implements ISynchroniser {
}
}

resolveConflicts(): void {
if (this.status === SyncStatus.HasConflicts) {
const resourceInput = {
resource: this.settingsPreviewResource,
label: localize('settings preview', "Settings Preview"),
options: {
preserveFocus: false,
pinned: false,
revealIfVisible: true,
},
mode: 'jsonc'
};
this.editorService.openEditor(resourceInput).then(() => this.historyService.remove(resourceInput));
handleConflicts(): boolean {
if (this.status !== SyncStatus.HasConflicts) {
return false;
}
const resourceInput = {
resource: SETTINGS_PREVIEW_RESOURCE,
label: localize('Settings Conflicts', "Local ↔ Remote (Settings Conflicts)"),
options: {
preserveFocus: false,
pinned: false,
revealIfVisible: true,
},
mode: 'jsonc'
};
this.editorService.openEditor(resourceInput).then(() => this.historyService.remove(resourceInput));
return true;
}

async apply(): Promise<void> {
async apply(previewResource: URI): Promise<boolean> {
if (!isEqual(previewResource, SETTINGS_PREVIEW_RESOURCE, false)) {
return false;
}
if (this.syncPreviewResultPromise) {
const result = await this.syncPreviewResultPromise;
let remoteUserData = result.remoteUserData;
const settingsPreivew = await this.fileService.readFile(this.settingsPreviewResource);
const settingsPreivew = await this.fileService.readFile(SETTINGS_PREVIEW_RESOURCE);
const content = settingsPreivew.value.toString();

const parseErrors: ParseError[] = [];
parse(content, parseErrors);
if (parseErrors.length > 0) {
return Promise.reject(localize('errorInvalidSettings', "Unable to sync settings. Please fix errors/warnings in it and try again."));
}
if (result.hasRemoteChanged) {
const ref = await this.writeToRemote(content, remoteUserData ? remoteUserData.ref : null);
remoteUserData = { ref, content };
Expand All @@ -143,6 +147,7 @@ export class SettingsSyncService extends Disposable implements ISynchroniser {
}
this.syncPreviewResultPromise = null;
this.setStatus(SyncStatus.Idle);
return true;
}

private getPreview(): Promise<ISyncPreviewResult> {
Expand All @@ -153,12 +158,12 @@ export class SettingsSyncService extends Disposable implements ISynchroniser {
}

private async generatePreview(): Promise<ISyncPreviewResult> {
const remoteUserData = await this.remoteUserDataService.read(SettingsSyncService.EXTERNAL_USER_DATA_SETTINGS_KEY);
const remoteUserData = await this.remoteUserDataService.read(SettingsSynchroniser.EXTERNAL_USER_DATA_SETTINGS_KEY);
// Get file content last to get the latest
const fileContent = await this.getLocalFileContent();
const { settingsPreview, hasLocalChanged, hasRemoteChanged, hasConflicts } = await this.computeChanges(fileContent, remoteUserData);
if (hasLocalChanged || hasRemoteChanged) {
await this.fileService.writeFile(this.settingsPreviewResource, VSBuffer.fromString(settingsPreview));
await this.fileService.writeFile(SETTINGS_PREVIEW_RESOURCE, VSBuffer.fromString(settingsPreview));
}
return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts };
}
Expand Down Expand Up @@ -248,7 +253,7 @@ export class SettingsSyncService extends Disposable implements ISynchroniser {
}

private getLastSyncUserData(): IUserData | null {
const lastSyncStorageContents = this.storageService.get(SettingsSyncService.LAST_SYNC_SETTINGS_STORAGE_KEY, StorageScope.GLOBAL, undefined);
const lastSyncStorageContents = this.storageService.get(SettingsSynchroniser.LAST_SYNC_SETTINGS_STORAGE_KEY, StorageScope.GLOBAL, undefined);
if (lastSyncStorageContents) {
return JSON.parse(lastSyncStorageContents);
}
Expand Down Expand Up @@ -425,7 +430,7 @@ export class SettingsSyncService extends Disposable implements ISynchroniser {
}

private async writeToRemote(content: string, ref: string | null): Promise<string> {
return this.remoteUserDataService.write(SettingsSyncService.EXTERNAL_USER_DATA_SETTINGS_KEY, content, ref);
return this.remoteUserDataService.write(SettingsSynchroniser.EXTERNAL_USER_DATA_SETTINGS_KEY, content, ref);
}

private async writeToLocal(newContent: string, oldContent: IFileContent | null): Promise<void> {
Expand All @@ -439,7 +444,7 @@ export class SettingsSyncService extends Disposable implements ISynchroniser {
}

private updateLastSyncValue(remoteUserData: IUserData): void {
this.storageService.store(SettingsSyncService.LAST_SYNC_SETTINGS_STORAGE_KEY, JSON.stringify(remoteUserData), StorageScope.GLOBAL);
this.storageService.store(SettingsSynchroniser.LAST_SYNC_SETTINGS_STORAGE_KEY, JSON.stringify(remoteUserData), StorageScope.GLOBAL);
}

}
14 changes: 7 additions & 7 deletions src/vs/workbench/services/userData/common/userData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';

export interface IUserData {
ref: string;
Expand Down Expand Up @@ -93,20 +94,19 @@ export enum SyncStatus {
HasConflicts = 'hasConflicts',
}

export const USER_DATA_PREVIEW_SCHEME = 'vscode-userdata-preview';
export const SETTINGS_PREVIEW_RESOURCE = URI.file('Settings-Preview').with({ scheme: USER_DATA_PREVIEW_SCHEME });

export interface ISynchroniser {
readonly status: SyncStatus;
readonly onDidChangeStatus: Event<SyncStatus>;
sync(): Promise<boolean>;
resolveConflicts(): void;
apply(): void;
handleConflicts(): boolean;
apply(previewResource: URI): Promise<boolean>;
}

export const IUserDataSyncService = createDecorator<IUserDataSyncService>('IUserDataSyncService');

export interface IUserDataSyncService {
export interface IUserDataSyncService extends ISynchroniser {
_serviceBrand: any;
readonly status: SyncStatus;
readonly onDidChangeStatus: Event<SyncStatus>;
sync(): Promise<void>;
resolveConflicts(): void;
}
Loading

0 comments on commit 889c7a8

Please sign in to comment.