diff --git a/src/vs/workbench/contrib/userData/browser/media/sync-push-dark.svg b/src/vs/workbench/contrib/userData/browser/media/sync-push-dark.svg new file mode 100644 index 0000000000000..72695bb2e5b13 --- /dev/null +++ b/src/vs/workbench/contrib/userData/browser/media/sync-push-dark.svg @@ -0,0 +1,3 @@ + diff --git a/src/vs/workbench/contrib/userData/browser/media/sync-push-light.svg b/src/vs/workbench/contrib/userData/browser/media/sync-push-light.svg new file mode 100644 index 0000000000000..82cbddecbc01c --- /dev/null +++ b/src/vs/workbench/contrib/userData/browser/media/sync-push-light.svg @@ -0,0 +1,3 @@ + diff --git a/src/vs/workbench/contrib/userData/browser/userData.contribution.ts b/src/vs/workbench/contrib/userData/browser/userData.contribution.ts index c0c80c1f4ea27..8bd7f3cf0212b 100644 --- a/src/vs/workbench/contrib/userData/browser/userData.contribution.ts +++ b/src/vs/workbench/contrib/userData/browser/userData.contribution.ts @@ -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('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; @@ -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 { + // 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(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(SyncContribution, LifecyclePhase.Starting); workbenchRegistry.registerWorkbenchContribution(AutoSyncUserData, LifecyclePhase.Eventually); - -registerEditorContribution(AcceptChangesController); diff --git a/src/vs/workbench/contrib/userData/browser/userDataPreviewEditorContribution.ts b/src/vs/workbench/contrib/userData/browser/userDataPreviewEditorContribution.ts deleted file mode 100644 index 5ef756c569181..0000000000000 --- a/src/vs/workbench/contrib/userData/browser/userDataPreviewEditorContribution.ts +++ /dev/null @@ -1,111 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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.ID); - } - - private readonly acceptChangesWidgetRenderer: MutableDisposable; - - constructor( - private editor: ICodeEditor, - @IInstantiationService private readonly instantiationService: IInstantiationService - ) { - super(); - - this.acceptChangesWidgetRenderer = this._register(new MutableDisposable()); - 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 { - // 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(); - } - } - - } -} diff --git a/src/vs/workbench/services/textfile/common/textResourcePropertiesService.ts b/src/vs/workbench/services/textfile/common/textResourcePropertiesService.ts index 940794852e208..95495464e6dcd 100644 --- a/src/vs/workbench/services/textfile/common/textResourcePropertiesService.ts +++ b/src/vs/workbench/services/textfile/common/textResourcePropertiesService.ts @@ -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); diff --git a/src/vs/workbench/services/userData/common/settingsSync.ts b/src/vs/workbench/services/userData/common/settingsSync.ts index 7cce436e7ef3f..7ab74c4e7d1c5 100644 --- a/src/vs/workbench/services/userData/common/settingsSync.ts +++ b/src/vs/workbench/services/userData/common/settingsSync.ts @@ -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 { - if (!isEqual(previewResource, SETTINGS_PREVIEW_RESOURCE, false)) { + async continueSync(): Promise { + 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 { + 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 { @@ -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(), removed: new Set(), updated: new Set() }; - const baseToRemote = base ? this.compare(base, remote) : { added: new Set(), removed: new Set(), updated: new Set() }; + const baseToLocal = base ? this.compare(base, local) : { added: Object.keys(local).reduce((r, k) => { r.add(k); return r; }, new Set()), removed: new Set(), updated: new Set() }; + const baseToRemote = base ? this.compare(base, remote) : { added: Object.keys(remote).reduce((r, k) => { r.add(k); return r; }, new Set()), removed: new Set(), updated: new Set() }; const localToRemote = this.compare(local, remote); const conflicts: Set = new Set(); diff --git a/src/vs/workbench/services/userData/common/userData.ts b/src/vs/workbench/services/userData/common/userData.ts index 470d2b4386210..b7969cf03729d 100644 --- a/src/vs/workbench/services/userData/common/userData.ts +++ b/src/vs/workbench/services/userData/common/userData.ts @@ -101,9 +101,9 @@ export interface ISynchroniser { readonly status: SyncStatus; readonly onDidChangeStatus: Event; sync(): Promise; + continueSync(): Promise; stopSync(): Promise; handleConflicts(): boolean; - apply(previewResource: URI): Promise; } export const IUserDataSyncService = createDecorator('IUserDataSyncService'); diff --git a/src/vs/workbench/services/userData/common/userDataSyncService.ts b/src/vs/workbench/services/userData/common/userDataSyncService.ts index 8fb5a98fa3ef4..e034d2a45f9d4 100644 --- a/src/vs/workbench/services/userData/common/userDataSyncService.ts +++ b/src/vs/workbench/services/userData/common/userDataSyncService.ts @@ -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 { + async continueSync(): Promise { 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; } }