提交 889c7a87 编写于 作者: S Sandeep Somavarapu

Accept and sync changes

上级 2b5da561
......@@ -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);
......@@ -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: {
......@@ -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);
/*---------------------------------------------------------------------------------------------
* 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();
}
}
}
}
......@@ -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';
......@@ -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;
......@@ -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;
......@@ -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 {
......@@ -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;
......@@ -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 };
......@@ -143,6 +147,7 @@ export class SettingsSyncService extends Disposable implements ISynchroniser {
}
this.syncPreviewResultPromise = null;
this.setStatus(SyncStatus.Idle);
return true;
}
private getPreview(): Promise<ISyncPreviewResult> {
......@@ -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 };
}
......@@ -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);
}
......@@ -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> {
......@@ -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);
}
}
......@@ -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;
......@@ -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;
}
......@@ -3,12 +3,15 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IUserDataSyncService, SyncStatus, IRemoteUserDataService, ISynchroniser } from 'vs/workbench/services/userData/common/userData';
import { IUserDataSyncService, SyncStatus, IRemoteUserDataService, ISynchroniser, USER_DATA_PREVIEW_SCHEME } from 'vs/workbench/services/userData/common/userData';
import { Disposable } from 'vs/base/common/lifecycle';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { SettingsSyncService as SettingsSynchroniser } from 'vs/workbench/services/userData/common/settingsSync';
import { SettingsSynchroniser } from 'vs/workbench/services/userData/common/settingsSync';
import { Emitter, Event } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
import { IFileService } from 'vs/platform/files/common/files';
import { InMemoryFileSystemProvider } from 'vs/workbench/services/userData/common/inMemoryUserDataProvider';
export class UserDataSyncService extends Disposable implements IUserDataSyncService {
......@@ -22,10 +25,12 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
readonly onDidChangeStatus: Event<SyncStatus> = this._onDidChangStatus.event;
constructor(
@IFileService fileService: IFileService,
@IRemoteUserDataService private readonly remoteUserDataService: IRemoteUserDataService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
) {
super();
this._register(fileService.registerProvider(USER_DATA_PREVIEW_SCHEME, new InMemoryFileSystemProvider()));
this.synchronisers = [
this.instantiationService.createInstance(SettingsSynchroniser)
];
......@@ -33,22 +38,40 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
this._register(Event.any(this.remoteUserDataService.onDidChangeEnablement, ...this.synchronisers.map(s => Event.map(s.onDidChangeStatus, () => undefined)))(() => this.updateStatus()));
}
async sync(): Promise<void> {
async sync(): Promise<boolean> {
if (!this.remoteUserDataService.isEnabled()) {
throw new Error('Not enabled');
}
for (const synchroniser of this.synchronisers) {
if (!await synchroniser.sync()) {
return;
return false;
}
}
return true;
}
resolveConflicts(): void {
const synchroniserWithConflicts = this.synchronisers.filter(s => s.status === SyncStatus.HasConflicts)[0];
if (synchroniserWithConflicts) {
synchroniserWithConflicts.resolveConflicts();
async apply(previewResource: URI): Promise<boolean> {
if (!this.remoteUserDataService.isEnabled()) {
throw new Error('Not enabled');
}
for (const synchroniser of this.synchronisers) {
if (await synchroniser.apply(previewResource)) {
return true;
}
}
return false;
}
handleConflicts(): boolean {
if (!this.remoteUserDataService.isEnabled()) {
throw new Error('Not enabled');
}
for (const synchroniser of this.synchronisers) {
if (synchroniser.handleConflicts()) {
return true;
}
}
return false;
}
private updateStatus(): void {
......
......@@ -240,6 +240,6 @@ import 'vs/workbench/contrib/experiments/browser/experiments.contribution';
import 'vs/workbench/contrib/feedback/browser/feedback.contribution';
// User Data
import 'vs/workbench/contrib/userData/common/userData.contribution';
import 'vs/workbench/contrib/userData/browser/userData.contribution';
//#endregion
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册