提交 b59d0cf5 编写于 作者: S Sandeep Somavarapu

improvements to settings sync

上级 0ebe7ec0
<svg aria-hidden="true" focusable="false" width="16" height="16" style="-ms-transform: rotate(360deg); -webkit-transform: rotate(360deg); transform: rotate(360deg);" preserveAspectRatio="xMidYMid meet" viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4 3H3V2h1v1zM3 5h1V4H3v1zm4 0L4 9h2v7h2V9h2L7 5zm4-5H1C.45 0 0 .45 0 1v12c0 .55.45 1 1 1h4v-1H1v-2h4v-1H2V1h9.02L11 10H9v1h2v2H9v1h2c.55 0 1-.45 1-1V1c0-.55-.45-1-1-1z" fill="#C5C5C5"/>
</svg>
<svg aria-hidden="true" focusable="false" width="16" height="16" style="-ms-transform: rotate(360deg); -webkit-transform: rotate(360deg); transform: rotate(360deg);" preserveAspectRatio="xMidYMid meet" viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4 3H3V2h1v1zM3 5h1V4H3v1zm4 0L4 9h2v7h2V9h2L7 5zm4-5H1C.45 0 0 .45 0 1v12c0 .55.45 1 1 1h4v-1H1v-2h4v-1H2V1h9.02L11 10H9v1h2v2H9v1h2c.55 0 1-.45 1-1V1c0-.55-.45-1-1-1z" fill="#626262"/>
</svg>
......@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { IUserDataSyncService, SyncStatus } from 'vs/workbench/services/userData/common/userData';
import { IUserDataSyncService, SyncStatus, USER_DATA_PREVIEW_SCHEME } from 'vs/workbench/services/userData/common/userData';
import { localize } from 'vs/nls';
import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
......@@ -18,9 +18,12 @@ 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';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { URI } from 'vs/base/common/uri';
import { registerAndGetAmdImageURL } from 'vs/base/common/amd';
import { ResourceContextKey } from 'vs/workbench/common/resources';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
const CONTEXT_SYNC_STATE = new RawContextKey<string>('syncStatus', SyncStatus.Uninitialized);
......@@ -71,6 +74,8 @@ class AutoSyncUserData extends Disposable implements IWorkbenchContribution {
}
const SYNC_PUSH_LIGHT_ICON_URI = URI.parse(registerAndGetAmdImageURL(`vs/workbench/contrib/userData/browser/media/sync-push-light.svg`));
const SYNC_PUSH_DARK_ICON_URI = URI.parse(registerAndGetAmdImageURL(`vs/workbench/contrib/userData/browser/media/sync-push-dark.svg`));
class SyncContribution extends Disposable implements IWorkbenchContribution {
private readonly syncEnablementContext: IContextKey<string>;
......@@ -82,7 +87,9 @@ class SyncContribution extends Disposable implements IWorkbenchContribution {
@IContextKeyService contextKeyService: IContextKeyService,
@IActivityService private readonly activityService: IActivityService,
@INotificationService private readonly notificationService: INotificationService,
@IConfigurationService private readonly configurationService: IConfigurationService
@IConfigurationService private readonly configurationService: IConfigurationService,
@IEditorService private readonly editorService: IEditorService,
@ITextFileService private readonly textFileService: ITextFileService,
) {
super();
this.syncEnablementContext = CONTEXT_SYNC_STATE.bindTo(contextKeyService);
......@@ -139,6 +146,30 @@ class SyncContribution extends Disposable implements IWorkbenchContribution {
this.configurationService.updateValue('userConfiguration.autoSync', false);
return this.userDataSyncService.stopSync();
}
private async continueSync(): Promise<void> {
// Get the preview editor
const editorInput = this.editorService.editors.filter(input => {
const resource = input.getResource();
return resource && resource.scheme === USER_DATA_PREVIEW_SCHEME;
})[0];
// Save the preview
if (editorInput && editorInput.isDirty()) {
await this.textFileService.save(editorInput.getResource()!);
}
try {
// Continue Sync
await this.userDataSyncService.continueSync();
} catch (error) {
this.notificationService.error(error);
return;
}
// Close the preview editor
if (editorInput) {
editorInput.dispose();
}
}
private registerActions(): void {
const startSyncMenuItem: IMenuItem = {
......@@ -187,6 +218,29 @@ class SyncContribution extends Disposable implements IWorkbenchContribution {
MenuRegistry.appendMenuItem(MenuId.GlobalActivity, resolveConflictsMenuItem);
MenuRegistry.appendMenuItem(MenuId.CommandPalette, resolveConflictsMenuItem);
const continueSyncCommandId = 'workbench.userData.actions.continueSync';
CommandsRegistry.registerCommand(continueSyncCommandId, () => this.continueSync());
MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
command: {
id: continueSyncCommandId,
title: localize('continue sync', "Sync: Continue")
},
when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.isEqualTo(SyncStatus.HasConflicts)),
});
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.Scheme.isEqualTo(USER_DATA_PREVIEW_SCHEME)),
});
CommandsRegistry.registerCommand('sync.synchronising', () => { });
MenuRegistry.appendMenuItem(MenuId.GlobalActivity, {
group: '5_sync',
......@@ -213,5 +267,3 @@ 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();
}
}
}
}
......@@ -29,7 +29,7 @@ export class TextResourcePropertiesService implements ITextResourcePropertiesSer
remoteAgentService.getEnvironment().then(remoteEnv => this.remoteEnvironment = remoteEnv);
}
getEOL(resource: URI, language?: string): string {
getEOL(resource?: URI, language?: string): string {
const filesConfiguration = this.configurationService.getValue<{ eol: string }>('files', { overrideIdentifier: language, resource });
if (filesConfiguration && filesConfiguration.eol && filesConfiguration.eol !== 'auto') {
return filesConfiguration.eol;
......@@ -38,12 +38,12 @@ export class TextResourcePropertiesService implements ITextResourcePropertiesSer
return os === OperatingSystem.Linux || os === OperatingSystem.Macintosh ? '\n' : '\r\n';
}
private getOS(resource: URI): OperatingSystem {
private getOS(resource?: URI): OperatingSystem {
let os = OS;
const remoteAuthority = this.environmentService.configuration.remoteAuthority;
if (remoteAuthority) {
if (resource.scheme !== Schemas.file) {
if (resource && resource.scheme !== Schemas.file) {
const osCacheKey = `resource.authority.os.${remoteAuthority}`;
os = this.remoteEnvironment ? this.remoteEnvironment.os : /* Get it from cache */ this.storageService.getNumber(osCacheKey, StorageScope.WORKSPACE, OS);
this.storageService.store(osCacheKey, os, StorageScope.WORKSPACE);
......
......@@ -11,7 +11,6 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag
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, 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';
import { IModeService } from 'vs/editor/common/services/modeService';
......@@ -25,7 +24,6 @@ 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 { IHistoryService } from 'vs/workbench/services/history/common/history';
import { isEqual } from 'vs/base/common/resources';
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
interface ISyncPreviewResult {
......@@ -83,7 +81,7 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser {
this.setStatus(SyncStatus.HasConflicts);
return false;
}
await this.apply(SETTINGS_PREVIEW_RESOURCE);
await this.apply();
return true;
} catch (e) {
this.syncPreviewResultPromise = null;
......@@ -129,35 +127,40 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser {
return true;
}
async apply(previewResource: URI): Promise<boolean> {
if (!isEqual(previewResource, SETTINGS_PREVIEW_RESOURCE, false)) {
async continueSync(): Promise<boolean> {
if (this.status !== SyncStatus.HasConflicts) {
return false;
}
if (this.syncPreviewResultPromise) {
const result = await this.syncPreviewResultPromise;
let remoteUserData = result.remoteUserData;
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 };
}
if (result.hasLocalChanged) {
await this.writeToLocal(content, result.fileContent);
}
if (remoteUserData) {
this.updateLastSyncValue(remoteUserData);
}
await this.apply();
return true;
}
private async apply(): Promise<void> {
if (!this.syncPreviewResultPromise) {
return;
}
const result = await this.syncPreviewResultPromise;
let remoteUserData = result.remoteUserData;
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 resolve conflicts without any errors/warnings and try again."));
}
if (result.hasRemoteChanged) {
const ref = await this.writeToRemote(content, remoteUserData ? remoteUserData.ref : null);
remoteUserData = { ref, content };
}
if (result.hasLocalChanged) {
await this.writeToLocal(content, result.fileContent);
}
if (remoteUserData) {
this.updateLastSyncValue(remoteUserData);
}
this.syncPreviewResultPromise = null;
this.setStatus(SyncStatus.Idle);
return true;
}
private getPreview(): Promise<ISyncPreviewResult> {
......@@ -225,8 +228,8 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser {
// Remote has moved forward
if (remoteUserData.ref !== lastSyncData.ref) {
this.logService.trace('Settings Sync: Remote contents have changed. Merge and Sync.');
hasRemoteChanged = true;
hasLocalChanged = lastSyncData.content !== localContent;
hasLocalChanged = true;
hasRemoteChanged = lastSyncData.content !== localContent;
const mergeResult = await this.mergeContents(localContent, remoteContent, lastSyncData.content);
return { settingsPreview: mergeResult.settingsPreview, hasLocalChanged, hasRemoteChanged, hasConflicts: mergeResult.hasConflicts };
}
......@@ -278,8 +281,8 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser {
const base = lastSyncedContent ? parse(lastSyncedContent) : null;
const settingsPreviewModel = this.modelService.createModel(localContent, this.modeService.create('jsonc'));
const baseToLocal = base ? this.compare(base, local) : { added: new Set<string>(), removed: new Set<string>(), updated: new Set<string>() };
const baseToRemote = base ? this.compare(base, remote) : { added: new Set<string>(), removed: new Set<string>(), updated: new Set<string>() };
const baseToLocal = base ? this.compare(base, local) : { added: Object.keys(local).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
const baseToRemote = base ? this.compare(base, remote) : { added: Object.keys(remote).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
const localToRemote = this.compare(local, remote);
const conflicts: Set<string> = new Set<string>();
......
......@@ -101,9 +101,9 @@ export interface ISynchroniser {
readonly status: SyncStatus;
readonly onDidChangeStatus: Event<SyncStatus>;
sync(): Promise<boolean>;
continueSync(): Promise<boolean>;
stopSync(): Promise<void>;
handleConflicts(): boolean;
apply(previewResource: URI): Promise<boolean>;
}
export const IUserDataSyncService = createDecorator<IUserDataSyncService>('IUserDataSyncService');
......
......@@ -9,7 +9,6 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
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';
......@@ -59,12 +58,12 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
}
}
async apply(previewResource: URI): Promise<boolean> {
async continueSync(): Promise<boolean> {
if (!this.remoteUserDataService.isEnabled()) {
throw new Error('Not enabled');
}
for (const synchroniser of this.synchronisers) {
if (await synchroniser.apply(previewResource)) {
if (await synchroniser.continueSync()) {
return true;
}
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册