Merge from vscode 79a1f5a5ca0c6c53db617aa1fa5a2396d2caebe2

This commit is contained in:
ADS Merger
2020-05-31 19:47:51 +00:00
parent 84492049e8
commit 28be33cfea
913 changed files with 28242 additions and 15549 deletions

View File

@@ -11,6 +11,8 @@ import { IHostService } from 'vs/workbench/services/host/browser/host';
import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { UserDataSyncTrigger } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger';
import { IProductService } from 'vs/platform/product/common/productService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
export class UserDataAutoSyncService extends BaseUserDataAutoSyncService {
@@ -22,8 +24,10 @@ export class UserDataAutoSyncService extends BaseUserDataAutoSyncService {
@IInstantiationService instantiationService: IInstantiationService,
@IHostService hostService: IHostService,
@ITelemetryService telemetryService: ITelemetryService,
@IProductService productService: IProductService,
@IConfigurationService configurationService: IConfigurationService,
) {
super(userDataSyncEnablementService, userDataSyncService, logService, authTokenService, telemetryService);
super(userDataSyncEnablementService, userDataSyncService, logService, authTokenService, telemetryService, productService, configurationService);
this._register(Event.debounce<string, string[]>(Event.any<string>(
Event.map(hostService.onDidChangeFocus, () => 'windowFocus'),

View File

@@ -3,12 +3,41 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { Registry } from 'vs/platform/registry/common/platform';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { UserDataSyncWorkbenchContribution } from 'vs/workbench/contrib/userDataSync/browser/userDataSync';
import { UserDataSyncViewContribution } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncView';
import { IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode } from 'vs/platform/userDataSync/common/userDataSync';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { Disposable } from 'vs/base/common/lifecycle';
import { localize } from 'vs/nls';
import { isWeb } from 'vs/base/common/platform';
class UserDataSyncReportIssueContribution extends Disposable implements IWorkbenchContribution {
constructor(
@IUserDataAutoSyncService userDataAutoSyncService: IUserDataAutoSyncService,
@INotificationService private readonly notificationService: INotificationService,
) {
super();
this._register(userDataAutoSyncService.onError(error => this.onAutoSyncError(error)));
}
private onAutoSyncError(error: UserDataSyncError): void {
switch (error.code) {
case UserDataSyncErrorCode.LocalTooManyRequests:
this.notificationService.notify({
severity: Severity.Error,
message: localize('too many requests', "Turned off syncing preferences on this device because it is making too many requests."),
});
return;
}
}
}
const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
workbenchRegistry.registerWorkbenchContribution(UserDataSyncWorkbenchContribution, LifecyclePhase.Ready);
workbenchRegistry.registerWorkbenchContribution(UserDataSyncViewContribution, LifecyclePhase.Ready);
if (isWeb) {
workbenchRegistry.registerWorkbenchContribution(UserDataSyncReportIssueContribution, LifecyclePhase.Ready);
}

View File

@@ -5,7 +5,7 @@
import { Action } from 'vs/base/common/actions';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { canceled, isPromiseCanceledError } from 'vs/base/common/errors';
import { isPromiseCanceledError } from 'vs/base/common/errors';
import { Event } from 'vs/base/common/event';
import { Disposable, DisposableStore, dispose, MutableDisposable, toDisposable, IDisposable } from 'vs/base/common/lifecycle';
import { isEqual, basename } from 'vs/base/common/resources';
@@ -28,8 +28,8 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import {
CONTEXT_SYNC_STATE, IUserDataAutoSyncService, IUserDataSyncService, registerConfiguration,
SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME, IUserDataSyncEnablementService, CONTEXT_SYNC_ENABLEMENT,
IUserDataAutoSyncService, IUserDataSyncService, registerConfiguration,
SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME, IUserDataSyncEnablementService,
SyncResourceConflicts, Conflict, getSyncResourceFromLocalPreview
} from 'vs/platform/userDataSync/common/userDataSync';
import { FloatingClickWidget } from 'vs/workbench/browser/parts/editor/editorWidgets';
@@ -48,40 +48,30 @@ import { IProductService } from 'vs/platform/product/common/productService';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService';
import { UserDataSyncAccounts, AccountStatus } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncAccount';
import { Registry } from 'vs/platform/registry/common/platform';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { Codicon } from 'vs/base/common/codicons';
import { ViewContainerLocation, IViewContainersRegistry, Extensions, ViewContainer } from 'vs/workbench/common/views';
import { UserDataSyncViewPaneContainer, UserDataSyncDataViews } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncViews';
import { IUserDataSyncWorkbenchService, CONTEXT_ENABLE_VIEWS, getSyncAreaLabel, AccountStatus, CONTEXT_SYNC_STATE, CONTEXT_SYNC_ENABLEMENT, CONTEXT_ACCOUNT_STATE, CONFIGURE_SYNC_COMMAND_ID, ENABLE_SYNC_VIEWS_COMMAND_ID, SHOW_SYNC_LOG_COMMAND_ID } from 'vs/workbench/services/userDataSync/common/userDataSync';
const CONTEXT_CONFLICTS_SOURCES = new RawContextKey<string>('conflictsSources', '');
type ConfigureSyncQuickPickItem = { id: SyncResource, label: string, description?: string };
function getSyncAreaLabel(source: SyncResource): string {
switch (source) {
case SyncResource.Settings: return localize('settings', "Settings");
case SyncResource.Keybindings: return localize('keybindings', "Keyboard Shortcuts");
case SyncResource.Snippets: return localize('snippets', "User Snippets");
case SyncResource.Extensions: return localize('extensions', "Extensions");
case SyncResource.GlobalState: return localize('ui state label', "UI State");
}
}
type SyncConflictsClassification = {
source: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
action?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
};
type FirstTimeSyncClassification = {
action: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
};
const turnOnSyncCommand = { id: 'workbench.userData.actions.syncStart', title: localize('turn on sync with category', "Preferences Sync: Turn On...") };
const stopSyncCommand = { id: 'workbench.userData.actions.stopSync', title: localize('stop sync', "Preferences Sync: Turn Off") };
const resolveSettingsConflictsCommand = { id: 'workbench.userData.actions.resolveSettingsConflicts', title: localize('showConflicts', "Preferences Sync: Show Settings Conflicts") };
const resolveKeybindingsConflictsCommand = { id: 'workbench.userData.actions.resolveKeybindingsConflicts', title: localize('showKeybindingsConflicts', "Preferences Sync: Show Keybindings Conflicts") };
const resolveSnippetsConflictsCommand = { id: 'workbench.userData.actions.resolveSnippetsConflicts', title: localize('showSnippetsConflicts', "Preferences Sync: Show User Snippets Conflicts") };
const configureSyncCommand = { id: 'workbench.userData.actions.configureSync', title: localize('configure sync', "Preferences Sync: Configure...") };
const showSyncActivityCommand = { id: 'workbench.userData.actions.showSyncActivity', title: localize('show sync log', "Preferences Sync: Show Log") };
const turnOnSyncCommand = { id: 'workbench.userDataSync.actions.turnOn', title: localize('turn on sync with category', "Preferences Sync: Turn On...") };
const turnOffSyncCommand = { id: 'workbench.userDataSync.actions.turnOff', title: localize('stop sync', "Preferences Sync: Turn Off") };
const configureSyncCommand = { id: CONFIGURE_SYNC_COMMAND_ID, title: localize('configure sync', "Preferences Sync: Configure...") };
const resolveSettingsConflictsCommand = { id: 'workbench.userDataSync.actions.resolveSettingsConflicts', title: localize('showConflicts', "Preferences Sync: Show Settings Conflicts") };
const resolveKeybindingsConflictsCommand = { id: 'workbench.userDataSync.actions.resolveKeybindingsConflicts', title: localize('showKeybindingsConflicts', "Preferences Sync: Show Keybindings Conflicts") };
const resolveSnippetsConflictsCommand = { id: 'workbench.userDataSync.actions.resolveSnippetsConflicts', title: localize('showSnippetsConflicts', "Preferences Sync: Show User Snippets Conflicts") };
const syncNowCommand = {
id: 'workbench.userData.actions.syncNow',
id: 'workbench.userDataSync.actions.syncNow',
title: localize('sync now', "Preferences Sync: Sync Now"),
description(userDataSyncService: IUserDataSyncService): string | undefined {
if (userDataSyncService.status === SyncStatus.Syncing) {
@@ -93,25 +83,22 @@ const syncNowCommand = {
return undefined;
}
};
const showSyncSettingsCommand = { id: 'workbench.userData.actions.syncSettings', title: localize('sync settings', "Preferences Sync: Show Settings"), };
const showSyncSettingsCommand = { id: 'workbench.userDataSync.actions.settings', title: localize('sync settings', "Preferences Sync: Show Settings"), };
const CONTEXT_TURNING_ON_STATE = new RawContextKey<false>('userDataSyncTurningOn', false);
export const CONTEXT_ACCOUNT_STATE = new RawContextKey<string>('userDataSyncAccountStatus', AccountStatus.Uninitialized);
export class UserDataSyncWorkbenchContribution extends Disposable implements IWorkbenchContribution {
private readonly turningOnSyncContext: IContextKey<boolean>;
private readonly syncEnablementContext: IContextKey<boolean>;
private readonly syncStatusContext: IContextKey<string>;
private readonly accountStatusContext: IContextKey<string>;
private readonly conflictsSources: IContextKey<string>;
private readonly viewsEnablementContext: IContextKey<boolean>;
private readonly userDataSyncAccounts: UserDataSyncAccounts;
private readonly badgeDisposable = this._register(new MutableDisposable());
constructor(
@IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService,
@IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService,
@IUserDataSyncWorkbenchService private readonly userDataSyncWorkbenchService: IUserDataSyncWorkbenchService,
@IContextKeyService contextKeyService: IContextKeyService,
@IActivityService private readonly activityService: IActivityService,
@INotificationService private readonly notificationService: INotificationService,
@@ -119,7 +106,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
@IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService,
@IDialogService private readonly dialogService: IDialogService,
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IInstantiationService instantiationService: IInstantiationService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IOutputService private readonly outputService: IOutputService,
@IAuthenticationTokenService readonly authTokenService: IAuthenticationTokenService,
@IUserDataAutoSyncService userDataAutoSyncService: IUserDataAutoSyncService,
@@ -134,45 +121,32 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
super();
this.turningOnSyncContext = CONTEXT_TURNING_ON_STATE.bindTo(contextKeyService);
this.syncEnablementContext = CONTEXT_SYNC_ENABLEMENT.bindTo(contextKeyService);
this.syncStatusContext = CONTEXT_SYNC_STATE.bindTo(contextKeyService);
this.accountStatusContext = CONTEXT_ACCOUNT_STATE.bindTo(contextKeyService);
this.conflictsSources = CONTEXT_CONFLICTS_SOURCES.bindTo(contextKeyService);
this.viewsEnablementContext = CONTEXT_ENABLE_VIEWS.bindTo(contextKeyService);
this.userDataSyncAccounts = instantiationService.createInstance(UserDataSyncAccounts);
if (this.userDataSyncAccounts.authenticationProviders.length) {
if (this.userDataSyncWorkbenchService.authenticationProviders.length) {
registerConfiguration();
this.onDidChangeSyncStatus(this.userDataSyncService.status);
this.updateBadge();
this.onDidChangeConflicts(this.userDataSyncService.conflicts);
this.onDidChangeEnablement(this.userDataSyncEnablementService.isEnabled());
this.onDidChangeAccountStatus(this.userDataSyncAccounts.status);
this._register(Event.debounce(userDataSyncService.onDidChangeStatus, () => undefined, 500)(() => this.onDidChangeSyncStatus(this.userDataSyncService.status)));
this._register(Event.any(
Event.debounce(userDataSyncService.onDidChangeStatus, () => undefined, 500),
this.userDataSyncEnablementService.onDidChangeEnablement,
this.userDataSyncWorkbenchService.onDidChangeAccountStatus
)(() => this.updateBadge()));
this._register(userDataSyncService.onDidChangeConflicts(() => this.onDidChangeConflicts(this.userDataSyncService.conflicts)));
this._register(userDataSyncService.onSyncErrors(errors => this.onSyncErrors(errors)));
this._register(this.userDataSyncEnablementService.onDidChangeEnablement(enabled => this.onDidChangeEnablement(enabled)));
this._register(userDataSyncService.onSyncErrors(errors => this.onSynchronizerErrors(errors)));
this._register(userDataAutoSyncService.onError(error => this.onAutoSyncError(error)));
this._register(this.userDataSyncAccounts.onDidChangeStatus(status => this.onDidChangeAccountStatus(status)));
this._register(this.userDataSyncAccounts.onDidSignOut(() => this.doTurnOff(false)));
this.registerActions();
this.registerViews();
textModelResolverService.registerTextModelContentProvider(USER_DATA_SYNC_SCHEME, instantiationService.createInstance(UserDataRemoteContentProvider));
registerEditorContribution(AcceptChangesContribution.ID, AcceptChangesContribution);
}
}
private onDidChangeAccountStatus(status: AccountStatus): void {
this.accountStatusContext.set(status);
this.updateBadge();
}
private onDidChangeSyncStatus(status: SyncStatus) {
this.syncStatusContext.set(status);
this.updateBadge();
}
private readonly conflictsDisposables = new Map<SyncResource, IDisposable>();
private onDidChangeConflicts(conflicts: SyncResourceConflicts[]) {
this.updateBadge();
@@ -261,8 +235,11 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
try {
for (const conflict of conflicts) {
const modelRef = await this.textModelResolverService.createModelReference(conflict.remote);
await this.userDataSyncService.acceptConflict(conflict.remote, modelRef.object.textEditorModel.getValue());
modelRef.dispose();
try {
await this.userDataSyncService.acceptConflict(conflict.remote, modelRef.object.textEditorModel.getValue());
} finally {
modelRef.dispose();
}
}
} catch (e) {
this.notificationService.error(e);
@@ -273,20 +250,18 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
try {
for (const conflict of conflicts) {
const modelRef = await this.textModelResolverService.createModelReference(conflict.local);
await this.userDataSyncService.acceptConflict(conflict.local, modelRef.object.textEditorModel.getValue());
modelRef.dispose();
try {
await this.userDataSyncService.acceptConflict(conflict.local, modelRef.object.textEditorModel.getValue());
} finally {
modelRef.dispose();
}
}
} catch (e) {
this.notificationService.error(e);
}
}
private onDidChangeEnablement(enabled: boolean) {
this.syncEnablementContext.set(enabled);
this.updateBadge();
}
private onAutoSyncError(error: UserDataSyncError): void {
private onAutoSyncError(error: UserDataSyncError): boolean {
switch (error.code) {
case UserDataSyncErrorCode.TurnedOff:
case UserDataSyncErrorCode.SessionExpired:
@@ -297,7 +272,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
primary: [new Action('turn on sync', localize('turn on sync', "Turn on Preferences Sync..."), undefined, true, () => this.turnOn())]
}
});
return;
return true;
case UserDataSyncErrorCode.TooLarge:
if (error.resource === SyncResource.Keybindings || error.resource === SyncResource.Settings) {
this.disableSync(error.resource);
@@ -311,19 +286,21 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
}
});
}
return;
return true;
case UserDataSyncErrorCode.Incompatible:
case UserDataSyncErrorCode.UpgradeRequired:
this.disableSync();
this.notificationService.notify({
severity: Severity.Error,
message: localize('error incompatible', "Turned off sync because local data is incompatible with the data in the cloud. Please update {0} and turn on sync to continue syncing.", this.productService.nameLong),
message: localize('error upgrade required', "Turned off sync because the current version ({0}, {1}) of {2} is not compatible with the Preferences Sync Service. Please update and turn on sync to continue syncing.", this.productService.version, this.productService.commit, this.productService.nameLong),
});
return;
return true;
}
return false;
}
private readonly invalidContentErrorDisposables = new Map<SyncResource, IDisposable>();
private onSyncErrors(errors: [SyncResource, UserDataSyncError][]): void {
private onSynchronizerErrors(errors: [SyncResource, UserDataSyncError][]): void {
if (errors.length) {
for (const [source, error] of errors) {
switch (error.code) {
@@ -379,7 +356,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
let clazz: string | undefined;
let priority: number | undefined = undefined;
if (this.userDataSyncService.status !== SyncStatus.Uninitialized && this.userDataSyncEnablementService.isEnabled() && this.userDataSyncAccounts.status === AccountStatus.Unavailable) {
if (this.userDataSyncService.status !== SyncStatus.Uninitialized && this.userDataSyncEnablementService.isEnabled() && this.userDataSyncWorkbenchService.accountStatus === AccountStatus.Unavailable) {
badge = new NumberBadge(1, () => localize('sign in to sync preferences', "Sign in to Sync Preferences"));
} else if (this.userDataSyncService.conflicts.length) {
badge = new NumberBadge(this.userDataSyncService.conflicts.reduce((result, syncResourceConflict) => { return result + syncResourceConflict.conflicts.length; }, 0), () => localize('has conflicts', "Preferences Sync: Conflicts Detected"));
@@ -415,8 +392,16 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
if (!turnOn) {
return;
}
await this.doTurnOn();
await this.userDataSyncWorkbenchService.turnOn();
this.storageService.store('sync.donotAskPreviewConfirmation', true, StorageScope.GLOBAL);
} catch (e) {
if (isPromiseCanceledError(e)) {
return;
}
if (e instanceof UserDataSyncError && this.onAutoSyncError(e)) {
return;
}
this.notificationService.error(localize('turn on failed', "Error while starting Sync: {0}", toErrorMessage(e)));
} finally {
this.turningOnSync = false;
}
@@ -450,13 +435,13 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
quickPick.title = localize('Preferences Sync Title', "Preferences Sync");
quickPick.ok = false;
quickPick.customButton = true;
if (this.userDataSyncAccounts.all.length) {
if (this.userDataSyncWorkbenchService.all.length) {
quickPick.customLabel = localize('turn on', "Turn On");
} else {
const orTerm = localize({ key: 'or', comment: ['Here is the context where it is used - Sign in with your A or B or C account to synchronize your data across devices.'] }, "or");
const displayName = this.userDataSyncAccounts.authenticationProviders.length === 1
? this.authenticationService.getDisplayName(this.userDataSyncAccounts.authenticationProviders[0].id)
: this.userDataSyncAccounts.authenticationProviders.map(({ id }) => this.authenticationService.getDisplayName(id)).join(` ${orTerm} `);
const displayName = this.userDataSyncWorkbenchService.authenticationProviders.length === 1
? this.authenticationService.getDisplayName(this.userDataSyncWorkbenchService.authenticationProviders[0].id)
: this.userDataSyncWorkbenchService.authenticationProviders.map(({ id }) => this.authenticationService.getDisplayName(id)).join(` ${orTerm} `);
quickPick.description = localize('sign in and turn on sync detail', "Sign in with your {0} account to synchronize your data across devices.", displayName);
quickPick.customLabel = localize('sign in and turn on sync', "Sign in & Turn on");
}
@@ -487,22 +472,6 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
});
}
private async doTurnOn(): Promise<void> {
const picked = await this.userDataSyncAccounts.pick();
if (!picked) {
throw canceled();
}
// User did not pick an account or login failed
if (this.userDataSyncAccounts.status !== AccountStatus.Available) {
throw new Error(localize('no account', "No account available"));
}
await this.handleFirstTimeSync();
this.userDataSyncEnablementService.setEnablement(true);
this.notificationService.info(localize('sync turned on', "Preferences sync is turned on"));
}
private getConfigureSyncQuickPickItems(): ConfigureSyncQuickPickItem[] {
return [{
id: SyncResource.Settings,
@@ -559,38 +528,6 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
});
}
private async handleFirstTimeSync(): Promise<void> {
const isFirstSyncWithMerge = await this.userDataSyncService.isFirstTimeSyncWithMerge();
if (!isFirstSyncWithMerge) {
return;
}
const result = await this.dialogService.show(
Severity.Info,
localize('firs time sync', "Sync"),
[
localize('merge', "Merge"),
localize('cancel', "Cancel"),
localize('replace', "Replace Local"),
],
{
cancelId: 1,
detail: localize('first time sync detail', "It looks like this is the first time sync is set up.\nWould you like to merge or replace with the data from the cloud?"),
}
);
switch (result.choice) {
case 0:
this.telemetryService.publicLog2<{ action: string }, FirstTimeSyncClassification>('sync/firstTimeSync', { action: 'merge' });
break;
case 1:
this.telemetryService.publicLog2<{ action: string }, FirstTimeSyncClassification>('sync/firstTimeSync', { action: 'cancelled' });
throw canceled();
case 2:
this.telemetryService.publicLog2<{ action: string }, FirstTimeSyncClassification>('sync/firstTimeSync', { action: 'replace-local' });
await this.userDataSyncService.pull();
break;
}
}
private async turnOff(): Promise<void> {
const result = await this.dialogService.confirm({
type: 'info',
@@ -602,26 +539,10 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
}
});
if (result.confirmed) {
return this.doTurnOff(!!result.checkboxChecked);
return this.userDataSyncWorkbenchService.turnoff(!!result.checkboxChecked);
}
}
private async doTurnOff(turnOffEveryWhere: boolean): Promise<void> {
if (!this.userDataSyncEnablementService.isEnabled()) {
return;
}
if (!this.userDataSyncEnablementService.canToggleEnablement()) {
return;
}
if (turnOffEveryWhere) {
this.telemetryService.publicLog2('sync/turnOffEveryWhere');
await this.userDataSyncService.reset();
} else {
await this.userDataSyncService.resetLocal();
}
this.disableSync();
}
private disableSync(source?: SyncResource): void {
if (source === undefined) {
this.userDataSyncEnablementService.setEnablement(false);
@@ -694,24 +615,18 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
this.registerShowSettingsConflictsAction();
this.registerShowKeybindingsConflictsAction();
this.registerShowSnippetsConflictsAction();
this.registerSyncStatusAction();
this.registerEnableSyncViewsAction();
this.registerManageSyncAction();
this.registerSyncNowAction();
this.registerConfigureSyncAction();
this.registerShowActivityAction();
this.registerShowSettingsAction();
this.registerShowLogAction();
}
private registerTurnOnSyncAction(): void {
const turnOnSyncWhenContext = ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_ENABLEMENT.toNegated(), CONTEXT_ACCOUNT_STATE.notEqualsTo(AccountStatus.Uninitialized), CONTEXT_TURNING_ON_STATE.negate());
CommandsRegistry.registerCommand(turnOnSyncCommand.id, async () => {
try {
await this.turnOn();
} catch (e) {
if (!isPromiseCanceledError(e)) {
this.notificationService.error(localize('turn on failed', "Error while starting Sync: {0}", toErrorMessage(e)));
}
}
});
CommandsRegistry.registerCommand(turnOnSyncCommand.id, () => this.turnOn());
MenuRegistry.appendMenuItem(MenuId.GlobalActivity, {
group: '5_sync',
command: {
@@ -786,7 +701,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
}
async run(): Promise<any> {
try {
await that.userDataSyncAccounts.pick();
await that.userDataSyncWorkbenchService.pickAccount();
} catch (e) {
that.notificationService.error(e);
}
@@ -886,14 +801,13 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
}));
}
private registerSyncStatusAction(): void {
private registerManageSyncAction(): void {
const that = this;
const when = ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, CONTEXT_ACCOUNT_STATE.isEqualTo(AccountStatus.Available), CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized));
this.registerSyncNowAction();
this._register(registerAction2(class SyncStatusAction extends Action2 {
constructor() {
super({
id: 'workbench.userData.actions.syncStatus',
id: 'workbench.userDataSync.actions.manage',
title: localize('sync is on', "Preferences Sync is On"),
menu: [
{
@@ -941,13 +855,11 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
items.push({ type: 'separator' });
}
items.push({ id: configureSyncCommand.id, label: configureSyncCommand.title });
items.push({ id: showSyncSettingsCommand.id, label: showSyncSettingsCommand.title });
items.push({ id: showSyncActivityCommand.id, label: showSyncActivityCommand.title });
items.push({ type: 'separator' });
items.push({ id: syncNowCommand.id, label: syncNowCommand.title, description: syncNowCommand.description(that.userDataSyncService) });
if (that.userDataSyncEnablementService.canToggleEnablement()) {
const account = that.userDataSyncAccounts.current;
items.push({ id: stopSyncCommand.id, label: stopSyncCommand.title, description: account ? `${account.accountName} (${that.authenticationService.getDisplayName(account.authenticationProviderId)})` : undefined });
const account = that.userDataSyncWorkbenchService.current;
items.push({ id: turnOffSyncCommand.id, label: turnOffSyncCommand.title, description: account ? `${account.accountName} (${that.authenticationService.getDisplayName(account.authenticationProviderId)})` : undefined });
}
quickPick.items = items;
disposables.add(quickPick.onDidAccept(() => {
@@ -966,6 +878,23 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
}));
}
private registerEnableSyncViewsAction(): void {
const that = this;
const when = ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, CONTEXT_ACCOUNT_STATE.isEqualTo(AccountStatus.Available), CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized));
this._register(registerAction2(class SyncStatusAction extends Action2 {
constructor() {
super({
id: ENABLE_SYNC_VIEWS_COMMAND_ID,
title: ENABLE_SYNC_VIEWS_COMMAND_ID,
precondition: when
});
}
run(accessor: ServicesAccessor): any {
that.viewsEnablementContext.set(true);
}
}));
}
private registerSyncNowAction(): void {
const that = this;
this._register(registerAction2(class SyncNowAction extends Action2 {
@@ -973,9 +902,14 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
super({
id: syncNowCommand.id,
title: syncNowCommand.title,
menu: {
id: MenuId.CommandPalette,
when: ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, CONTEXT_ACCOUNT_STATE.isEqualTo(AccountStatus.Available), CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized))
}
});
}
run(): Promise<any> {
run(accessor: ServicesAccessor): Promise<any> {
accessor.get(ITelemetryService).publicLog2(`sync/actions/${syncNowCommand.id}`);
return that.userDataSyncService.sync();
}
}));
@@ -986,8 +920,8 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
this._register(registerAction2(class StopSyncAction extends Action2 {
constructor() {
super({
id: stopSyncCommand.id,
title: stopSyncCommand.title,
id: turnOffSyncCommand.id,
title: turnOffSyncCommand.title,
menu: {
id: MenuId.CommandPalette,
when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_ENABLEMENT),
@@ -1008,28 +942,29 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
private registerConfigureSyncAction(): void {
const that = this;
this._register(registerAction2(class ShowSyncActivityAction extends Action2 {
const when = ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_ENABLEMENT);
this._register(registerAction2(class ConfigureSyncAction extends Action2 {
constructor() {
super({
id: configureSyncCommand.id,
title: configureSyncCommand.title,
menu: {
id: MenuId.CommandPalette,
when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_ENABLEMENT),
},
when
}
});
}
run(): any { return that.configureSyncOptions(); }
}));
}
private registerShowActivityAction(): void {
private registerShowLogAction(): void {
const that = this;
this._register(registerAction2(class ShowSyncActivityAction extends Action2 {
constructor() {
super({
id: showSyncActivityCommand.id,
title: showSyncActivityCommand.title,
id: SHOW_SYNC_LOG_COMMAND_ID,
title: localize('show sync log title', "Preferences Sync: Show Log"),
menu: {
id: MenuId.CommandPalette,
when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized)),
@@ -1058,6 +993,30 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
}));
}
private registerViews(): void {
const container = this.registerViewContainer();
this.registerDataViews(container);
}
private registerViewContainer(): ViewContainer {
const viewContainerId = 'workbench.view.sync';
return Registry.as<IViewContainersRegistry>(Extensions.ViewContainersRegistry).registerViewContainer(
{
id: viewContainerId,
name: localize('sync preferences', "Preferences Sync"),
ctorDescriptor: new SyncDescriptor(
UserDataSyncViewPaneContainer,
[viewContainerId]
),
icon: Codicon.sync.classNames,
hideIfEmpty: true,
}, ViewContainerLocation.Sidebar);
}
private registerDataViews(container: ViewContainer): void {
this._register(this.instantiationService.createInstance(UserDataSyncDataViews, container));
}
}
class UserDataRemoteContentProvider implements ITextModelContentProvider {

View File

@@ -1,341 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService';
import { IQuickInputService, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication';
import { IProductService } from 'vs/platform/product/common/productService';
import { IStorageService, StorageScope, IWorkspaceStorageChangeEvent } from 'vs/platform/storage/common/storage';
import { localize } from 'vs/nls';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { AuthenticationSession, AuthenticationSessionsChangeEvent } from 'vs/editor/common/modes';
import { Event, Emitter } from 'vs/base/common/event';
import { getUserDataSyncStore, IUserDataSyncEnablementService, IAuthenticationProvider, isAuthenticationProvider } from 'vs/platform/userDataSync/common/userDataSync';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { values } from 'vs/base/common/map';
import { ILogService } from 'vs/platform/log/common/log';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { flatten } from 'vs/base/common/arrays';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
type UserAccountClassification = {
id: { classification: 'EndUserPseudonymizedInformation', purpose: 'BusinessInsight' };
};
type UserAccountEvent = {
id: string;
};
type AccountQuickPickItem = { label: string, authenticationProvider: IAuthenticationProvider, account?: UserDataSyncAccount, description?: string };
export class UserDataSyncAccount {
constructor(readonly authenticationProviderId: string, private readonly session: AuthenticationSession) { }
get sessionId(): string { return this.session.id; }
get accountName(): string { return this.session.account.displayName; }
get accountId(): string { return this.session.account.id; }
getToken(): Thenable<string> { return this.session.getAccessToken(); }
}
export const enum AccountStatus {
Uninitialized = 'uninitialized',
Unavailable = 'unavailable',
Available = 'available',
}
export class UserDataSyncAccounts extends Disposable {
private static DONOT_USE_WORKBENCH_SESSION_STORAGE_KEY = 'userDataSyncAccount.donotUseWorkbenchSession';
private static CACHED_SESSION_STORAGE_KEY = 'userDataSyncAccountPreference';
_serviceBrand: any;
readonly authenticationProviders: IAuthenticationProvider[];
private _status: AccountStatus = AccountStatus.Uninitialized;
get status(): AccountStatus { return this._status; }
private readonly _onDidChangeStatus = this._register(new Emitter<AccountStatus>());
readonly onDidChangeStatus = this._onDidChangeStatus.event;
private readonly _onDidSignOut = this._register(new Emitter<void>());
readonly onDidSignOut = this._onDidSignOut.event;
private _all: Map<string, UserDataSyncAccount[]> = new Map<string, UserDataSyncAccount[]>();
get all(): UserDataSyncAccount[] { return flatten(values(this._all)); }
get current(): UserDataSyncAccount | undefined { return this.all.filter(account => this.isCurrentAccount(account))[0]; }
constructor(
@IAuthenticationService private readonly authenticationService: IAuthenticationService,
@IAuthenticationTokenService private readonly authenticationTokenService: IAuthenticationTokenService,
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IStorageService private readonly storageService: IStorageService,
@IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@ILogService private readonly logService: ILogService,
@IProductService productService: IProductService,
@IConfigurationService configurationService: IConfigurationService,
@IExtensionService extensionService: IExtensionService,
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
) {
super();
this.authenticationProviders = getUserDataSyncStore(productService, configurationService)?.authenticationProviders || [];
if (this.authenticationProviders.length) {
extensionService.whenInstalledExtensionsRegistered().then(() => {
if (this.authenticationProviders.every(({ id }) => authenticationService.isAuthenticationProviderRegistered(id))) {
this.initialize();
} else {
const disposable = this.authenticationService.onDidRegisterAuthenticationProvider(() => {
if (this.authenticationProviders.every(({ id }) => authenticationService.isAuthenticationProviderRegistered(id))) {
disposable.dispose();
this.initialize();
}
});
}
});
}
}
private async initialize(): Promise<void> {
if (this.currentSessionId === undefined && this.useWorkbenchSessionId && this.environmentService.options?.authenticationSessionId) {
this.currentSessionId = this.environmentService.options.authenticationSessionId;
this.useWorkbenchSessionId = false;
}
await this.update();
this._register(
Event.any(
Event.filter(
Event.any(
this.authenticationService.onDidRegisterAuthenticationProvider,
this.authenticationService.onDidUnregisterAuthenticationProvider,
), authenticationProviderId => this.isSupportedAuthenticationProviderId(authenticationProviderId)),
this.authenticationTokenService.onTokenFailed)
(() => this.update()));
this._register(Event.filter(this.authenticationService.onDidChangeSessions, e => this.isSupportedAuthenticationProviderId(e.providerId))(({ event }) => this.onDidChangeSessions(event)));
this._register(this.storageService.onDidChangeStorage(e => this.onDidChangeStorage(e)));
}
private isSupportedAuthenticationProviderId(authenticationProviderId: string): boolean {
return this.authenticationProviders.some(({ id }) => id === authenticationProviderId);
}
private async update(): Promise<void> {
const allAccounts: Map<string, UserDataSyncAccount[]> = new Map<string, UserDataSyncAccount[]>();
for (const { id } of this.authenticationProviders) {
const accounts = await this.getAccounts(id);
allAccounts.set(id, accounts);
}
this._all = allAccounts;
const current = this.current;
await this.updateToken(current);
this.updateStatus(current);
}
private async getAccounts(authenticationProviderId: string): Promise<UserDataSyncAccount[]> {
let accounts: Map<string, UserDataSyncAccount> = new Map<string, UserDataSyncAccount>();
let currentAccount: UserDataSyncAccount | null = null;
const sessions = await this.authenticationService.getSessions(authenticationProviderId) || [];
for (const session of sessions) {
const account: UserDataSyncAccount = new UserDataSyncAccount(authenticationProviderId, session);
accounts.set(account.accountName, account);
if (this.isCurrentAccount(account)) {
currentAccount = account;
}
}
if (currentAccount) {
// Always use current account if available
accounts.set(currentAccount.accountName, currentAccount);
}
return values(accounts);
}
private async updateToken(current: UserDataSyncAccount | undefined): Promise<void> {
let value: { token: string, authenticationProviderId: string } | undefined = undefined;
if (current) {
try {
this.logService.trace('Preferences Sync: Updating the token for the account', current.accountName);
const token = await current.getToken();
this.logService.trace('Preferences Sync: Token updated for the account', current.accountName);
value = { token, authenticationProviderId: current.authenticationProviderId };
} catch (e) {
this.logService.error(e);
}
}
await this.authenticationTokenService.setToken(value);
}
private updateStatus(current: UserDataSyncAccount | undefined): void {
// set status
const status: AccountStatus = current ? AccountStatus.Available : AccountStatus.Unavailable;
if (this._status !== status) {
const previous = this._status;
this.logService.debug('Sync account status changed', previous, status);
if (previous === AccountStatus.Available && status === AccountStatus.Unavailable) {
this._onDidSignOut.fire();
}
this._status = status;
this._onDidChangeStatus.fire(status);
}
}
private isCurrentAccount(account: UserDataSyncAccount): boolean {
return account.sessionId === this.currentSessionId;
}
async pick(): Promise<boolean> {
const result = await this.doPick();
if (!result) {
return false;
}
let sessionId: string, accountName: string, accountId: string;
if (isAuthenticationProvider(result)) {
const session = await this.authenticationService.login(result.id, result.scopes);
sessionId = session.id;
accountName = session.account.displayName;
accountId = session.account.id;
} else {
sessionId = result.sessionId;
accountName = result.accountName;
accountId = result.accountId;
}
await this.switch(sessionId, accountName, accountId);
return true;
}
private async doPick(): Promise<UserDataSyncAccount | IAuthenticationProvider | undefined> {
if (this.authenticationProviders.length === 0) {
return undefined;
}
await this.update();
// Single auth provider and no accounts available
if (this.authenticationProviders.length === 1 && !this.all.length) {
return this.authenticationProviders[0];
}
return new Promise<UserDataSyncAccount | IAuthenticationProvider | undefined>(async (c, e) => {
let result: UserDataSyncAccount | IAuthenticationProvider | undefined;
const disposables: DisposableStore = new DisposableStore();
const quickPick = this.quickInputService.createQuickPick<AccountQuickPickItem>();
disposables.add(quickPick);
quickPick.title = localize('pick an account', "Preferences Sync");
quickPick.ok = false;
quickPick.placeholder = localize('choose account placeholder', "Select an account");
quickPick.ignoreFocusOut = true;
quickPick.items = this.createQuickpickItems();
disposables.add(quickPick.onDidAccept(() => {
result = quickPick.selectedItems[0]?.account ? quickPick.selectedItems[0]?.account : quickPick.selectedItems[0]?.authenticationProvider;
quickPick.hide();
}));
disposables.add(quickPick.onDidHide(() => {
disposables.dispose();
c(result);
}));
quickPick.show();
});
}
private createQuickpickItems(): (AccountQuickPickItem | IQuickPickSeparator)[] {
const quickPickItems: (AccountQuickPickItem | IQuickPickSeparator)[] = [];
// Signed in Accounts
if (this.all.length) {
const authenticationProviders = [...this.authenticationProviders].sort(({ id }) => id === this.current?.authenticationProviderId ? -1 : 1);
quickPickItems.push({ type: 'separator', label: localize('signed in', "Signed in") });
for (const authenticationProvider of authenticationProviders) {
const accounts = (this._all.get(authenticationProvider.id) || []).sort(({ sessionId }) => sessionId === this.current?.sessionId ? -1 : 1);
const providerName = this.authenticationService.getDisplayName(authenticationProvider.id);
for (const account of accounts) {
quickPickItems.push({
label: `${account.accountName} (${providerName})`,
description: account.sessionId === this.current?.sessionId ? localize('last used', "Last Used with Sync") : undefined,
account,
authenticationProvider,
});
}
}
quickPickItems.push({ type: 'separator', label: localize('others', "Others") });
}
// Account proviers
for (const authenticationProvider of this.authenticationProviders) {
const providerName = this.authenticationService.getDisplayName(authenticationProvider.id);
quickPickItems.push({ label: localize('sign in using account', "Sign in with {0}", providerName), authenticationProvider });
}
return quickPickItems;
}
private async switch(sessionId: string, accountName: string, accountId: string): Promise<void> {
const currentAccount = this.current;
if (this.userDataSyncEnablementService.isEnabled() && (currentAccount && currentAccount.accountName !== accountName)) {
// accounts are switched while sync is enabled.
}
this.currentSessionId = sessionId;
this.telemetryService.publicLog2<UserAccountEvent, UserAccountClassification>('sync.userAccount', { id: accountId });
await this.update();
}
private onDidChangeSessions(e: AuthenticationSessionsChangeEvent): void {
if (this.currentSessionId && e.removed.includes(this.currentSessionId)) {
this.currentSessionId = undefined;
}
this.update();
}
private onDidChangeStorage(e: IWorkspaceStorageChangeEvent): void {
if (e.key === UserDataSyncAccounts.CACHED_SESSION_STORAGE_KEY && e.scope === StorageScope.GLOBAL
&& this.currentSessionId !== this.getStoredCachedSessionId() /* This checks if current window changed the value or not */) {
this._cachedCurrentSessionId = null;
this.update();
}
}
private _cachedCurrentSessionId: string | undefined | null = null;
private get currentSessionId(): string | undefined {
if (this._cachedCurrentSessionId === null) {
this._cachedCurrentSessionId = this.getStoredCachedSessionId();
}
return this._cachedCurrentSessionId;
}
private set currentSessionId(cachedSessionId: string | undefined) {
if (this._cachedCurrentSessionId !== cachedSessionId) {
this._cachedCurrentSessionId = cachedSessionId;
if (cachedSessionId === undefined) {
this.storageService.remove(UserDataSyncAccounts.CACHED_SESSION_STORAGE_KEY, StorageScope.GLOBAL);
} else {
this.storageService.store(UserDataSyncAccounts.CACHED_SESSION_STORAGE_KEY, cachedSessionId, StorageScope.GLOBAL);
}
}
}
private getStoredCachedSessionId(): string | undefined {
return this.storageService.get(UserDataSyncAccounts.CACHED_SESSION_STORAGE_KEY, StorageScope.GLOBAL);
}
private get useWorkbenchSessionId(): boolean {
return !this.storageService.getBoolean(UserDataSyncAccounts.DONOT_USE_WORKBENCH_SESSION_STORAGE_KEY, StorageScope.GLOBAL, false);
}
private set useWorkbenchSessionId(useWorkbenchSession: boolean) {
this.storageService.store(UserDataSyncAccounts.DONOT_USE_WORKBENCH_SESSION_STORAGE_KEY, !useWorkbenchSession, StorageScope.GLOBAL);
}
}

View File

@@ -12,7 +12,6 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/
import { VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions';
import { IEditorInput } from 'vs/workbench/common/editor';
import { IViewsService } from 'vs/workbench/common/views';
import { VIEW_CONTAINER_ID } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncView';
export class UserDataSyncTrigger extends Disposable {
@@ -29,7 +28,7 @@ export class UserDataSyncTrigger extends Disposable {
Event.filter(
Event.any<string | undefined>(
Event.map(editorService.onDidActiveEditorChange, () => this.getUserDataEditorInputSource(editorService.activeEditor)),
Event.map(Event.filter(viewsService.onDidChangeViewContainerVisibility, e => [VIEWLET_ID, VIEW_CONTAINER_ID].includes(e.id) && e.visible), e => e.id)
Event.map(Event.filter(viewsService.onDidChangeViewContainerVisibility, e => e.id === VIEWLET_ID && e.visible), e => e.id)
), source => source !== undefined)(source => this._onDidTriggerSync.fire(source!)));
}

View File

@@ -1,225 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { Registry } from 'vs/platform/registry/common/platform';
import { IViewsRegistry, Extensions, ITreeViewDescriptor, ITreeViewDataProvider, ITreeItem, TreeItemCollapsibleState, IViewsService, TreeViewItemHandleArg, IViewContainersRegistry, ViewContainerLocation, ViewContainer, IViewDescriptorService } from 'vs/workbench/common/views';
import { localize } from 'vs/nls';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { TreeViewPane, TreeView } from 'vs/workbench/browser/parts/views/treeView';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { ALL_SYNC_RESOURCES, SyncResource, IUserDataSyncService, ISyncResourceHandle, CONTEXT_SYNC_STATE, SyncStatus } from 'vs/platform/userDataSync/common/userDataSync';
import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions';
import { IContextKeyService, RawContextKey, ContextKeyExpr, ContextKeyEqualsExpr, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { URI } from 'vs/base/common/uri';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { FolderThemeIcon } from 'vs/platform/theme/common/themeService';
import { fromNow } from 'vs/base/common/date';
import { pad, uppercaseFirstLetter } from 'vs/base/common/strings';
import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer';
import { Codicon } from 'vs/base/common/codicons';
import { CONTEXT_ACCOUNT_STATE } from 'vs/workbench/contrib/userDataSync/browser/userDataSync';
import { AccountStatus } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncAccount';
export const VIEW_CONTAINER_ID = 'workbench.view.sync';
const CONTEXT_ENABLE_VIEWS = new RawContextKey<boolean>(`showUserDataSyncViews`, false);
export class UserDataSyncViewContribution implements IWorkbenchContribution {
private readonly viewsEnablementContext: IContextKey<boolean>;
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService,
) {
const container = this.registerSyncViewContainer();
this.viewsEnablementContext = CONTEXT_ENABLE_VIEWS.bindTo(this.contextKeyService);
this.registerView(container, true);
this.registerView(container, false);
}
private registerSyncViewContainer(): ViewContainer {
return Registry.as<IViewContainersRegistry>(Extensions.ViewContainersRegistry).registerViewContainer(
{
id: VIEW_CONTAINER_ID,
name: localize('sync preferences', "Preferences Sync"),
ctorDescriptor: new SyncDescriptor(
ViewPaneContainer,
[VIEW_CONTAINER_ID, { mergeViewWithContainerWhenSingleView: true }]
),
icon: Codicon.sync.classNames,
hideIfEmpty: true,
}, ViewContainerLocation.Sidebar);
}
private registerView(container: ViewContainer, remote: boolean): void {
const that = this;
const id = `workbench.views.sync.${remote ? 'remote' : 'local'}DataView`;
const name = remote ? localize('remote title', "Remote Data") : localize('local title', "Local Backup");
const treeView = this.instantiationService.createInstance(TreeView, id, name);
treeView.showCollapseAllAction = true;
treeView.showRefreshAction = true;
const disposable = treeView.onDidChangeVisibility(visible => {
if (visible && !treeView.dataProvider) {
disposable.dispose();
treeView.dataProvider = new UserDataSyncHistoryViewDataProvider(remote, this.userDataSyncService);
}
});
const viewsRegistry = Registry.as<IViewsRegistry>(Extensions.ViewsRegistry);
viewsRegistry.registerViews([<ITreeViewDescriptor>{
id,
name,
ctorDescriptor: new SyncDescriptor(TreeViewPane),
when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_ACCOUNT_STATE.isEqualTo(AccountStatus.Available), CONTEXT_ENABLE_VIEWS),
canToggleVisibility: true,
canMoveView: true,
treeView,
collapsed: false,
order: 100,
}], container);
registerAction2(class extends Action2 {
constructor() {
super({
id: `workbench.actions.showSync${remote ? 'Remote' : 'Local'}DataView`,
title: remote ?
{ value: localize('workbench.action.showSyncRemoteBackup', "Show Remote Data"), original: `Show Remote Data` }
: { value: localize('workbench.action.showSyncLocalBackup', "Show Local Backup"), original: `Show Local Backup` },
category: { value: localize('sync preferences', "Preferences Sync"), original: `Preferences Sync` },
menu: {
id: MenuId.CommandPalette,
when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_ACCOUNT_STATE.isEqualTo(AccountStatus.Available)),
},
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const viewDescriptorService = accessor.get(IViewDescriptorService);
const viewsService = accessor.get(IViewsService);
that.viewsEnablementContext.set(true);
const viewContainer = viewDescriptorService.getViewContainerByViewId(id);
if (viewContainer) {
const model = viewDescriptorService.getViewContainerModel(viewContainer);
if (model.activeViewDescriptors.some(viewDescriptor => viewDescriptor.id === id)) {
viewsService.openView(id, true);
} else {
const disposable = model.onDidChangeActiveViewDescriptors(e => {
if (e.added.some(viewDescriptor => viewDescriptor.id === id)) {
disposable.dispose();
viewsService.openView(id, true);
}
});
}
}
}
});
this.registerActions(id);
}
private registerActions(viewId: string) {
registerAction2(class extends Action2 {
constructor() {
super({
id: `workbench.actions.sync.resolveResource`,
title: localize('workbench.actions.sync.resolveResourceRef', "Show raw JSON sync data"),
menu: {
id: MenuId.ViewItemContext,
when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', viewId), ContextKeyExpr.regex('viewItem', /sync-resource-.*/i))
},
});
}
async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise<void> {
const editorService = accessor.get(IEditorService);
await editorService.openEditor({ resource: URI.parse(handle.$treeItemHandle) });
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: `workbench.actions.sync.commpareWithLocal`,
title: localize('workbench.actions.sync.commpareWithLocal', "Open Changes"),
});
}
async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise<void> {
const editorService = accessor.get(IEditorService);
const { resource, comparableResource } = <{ resource: string, comparableResource?: string }>JSON.parse(handle.$treeItemHandle);
if (comparableResource) {
await editorService.openEditor({
leftResource: URI.parse(resource),
rightResource: URI.parse(comparableResource),
options: {
preserveFocus: true,
revealIfVisible: true,
},
});
} else {
await editorService.openEditor({ resource: URI.parse(resource) });
}
}
});
}
}
interface SyncResourceTreeItem extends ITreeItem {
resource: SyncResource;
resourceHandle: ISyncResourceHandle;
}
class UserDataSyncHistoryViewDataProvider implements ITreeViewDataProvider {
constructor(private readonly remote: boolean, private userDataSyncService: IUserDataSyncService) { }
async getChildren(element?: ITreeItem): Promise<ITreeItem[]> {
if (!element) {
return ALL_SYNC_RESOURCES.map(resourceKey => ({
handle: resourceKey,
collapsibleState: TreeItemCollapsibleState.Collapsed,
label: { label: uppercaseFirstLetter(resourceKey) },
themeIcon: FolderThemeIcon,
}));
}
const resourceKey = ALL_SYNC_RESOURCES.filter(key => key === element.handle)[0] as SyncResource;
if (resourceKey) {
const refHandles = this.remote ? await this.userDataSyncService.getRemoteSyncResourceHandles(resourceKey) : await this.userDataSyncService.getLocalSyncResourceHandles(resourceKey);
return refHandles.map(({ uri, created }) => {
return <SyncResourceTreeItem>{
handle: uri.toString(),
collapsibleState: TreeItemCollapsibleState.Collapsed,
label: { label: label(new Date(created)) },
description: fromNow(created, true),
resourceUri: uri,
resource: resourceKey,
resourceHandle: { uri, created },
contextValue: `sync-resource-${resourceKey}`
};
});
}
if ((<SyncResourceTreeItem>element).resourceHandle) {
const associatedResources = await this.userDataSyncService.getAssociatedResources((<SyncResourceTreeItem>element).resource, (<SyncResourceTreeItem>element).resourceHandle);
return associatedResources.map(({ resource, comparableResource }) => {
const handle = JSON.stringify({ resource: resource.toString(), comparableResource: comparableResource?.toString() });
return {
handle,
collapsibleState: TreeItemCollapsibleState.None,
resourceUri: resource,
command: { id: `workbench.actions.sync.commpareWithLocal`, title: '', arguments: [<TreeViewItemHandleArg>{ $treeViewId: '', $treeItemHandle: handle }] },
contextValue: `sync-associatedResource-${(<SyncResourceTreeItem>element).resource}`
};
});
}
return [];
}
}
function label(date: Date): string {
return date.toLocaleDateString() +
' ' + pad(date.getHours(), 2) +
':' + pad(date.getMinutes(), 2) +
':' + pad(date.getSeconds(), 2);
}

View File

@@ -0,0 +1,483 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Registry } from 'vs/platform/registry/common/platform';
import { IViewsRegistry, Extensions, ITreeViewDescriptor, ITreeViewDataProvider, ITreeItem, TreeItemCollapsibleState, IViewsService, TreeViewItemHandleArg, ViewContainer, IViewDescriptorService } from 'vs/workbench/common/views';
import { localize } from 'vs/nls';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { TreeViewPane, TreeView } from 'vs/workbench/browser/parts/views/treeView';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { ALL_SYNC_RESOURCES, SyncResource, IUserDataSyncService, ISyncResourceHandle, SyncStatus, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync';
import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions';
import { ContextKeyExpr, ContextKeyEqualsExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { URI } from 'vs/base/common/uri';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { FolderThemeIcon, IThemeService } from 'vs/platform/theme/common/themeService';
import { fromNow } from 'vs/base/common/date';
import { pad } from 'vs/base/common/strings';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { Codicon } from 'vs/base/common/codicons';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IAction, Action } from 'vs/base/common/actions';
import { IUserDataSyncWorkbenchService, CONTEXT_SYNC_STATE, getSyncAreaLabel, CONTEXT_ACCOUNT_STATE, AccountStatus, CONTEXT_ENABLE_VIEWS, SHOW_SYNC_LOG_COMMAND_ID, CONFIGURE_SYNC_COMMAND_ID, ENABLE_SYNC_VIEWS_COMMAND_ID } from 'vs/workbench/services/userDataSync/common/userDataSync';
import { IUserDataSyncMachinesService, IUserDataSyncMachine } from 'vs/platform/userDataSync/common/userDataSyncMachines';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
export class UserDataSyncViewPaneContainer extends ViewPaneContainer {
constructor(
containerId: string,
@ICommandService private readonly commandService: ICommandService,
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
@ITelemetryService telemetryService: ITelemetryService,
@IInstantiationService instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService,
@IConfigurationService configurationService: IConfigurationService,
@IStorageService storageService: IStorageService,
@IWorkspaceContextService contextService: IWorkspaceContextService,
@IContextMenuService contextMenuService: IContextMenuService,
@IExtensionService extensionService: IExtensionService,
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
) {
super(containerId, { mergeViewWithContainerWhenSingleView: false }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService);
}
getActions(): IAction[] {
return [
new Action(SHOW_SYNC_LOG_COMMAND_ID, localize('showLog', "Show Log"), Codicon.output.classNames, true, async () => this.commandService.executeCommand(SHOW_SYNC_LOG_COMMAND_ID)),
new Action(CONFIGURE_SYNC_COMMAND_ID, localize('configure', "Configure..."), Codicon.settingsGear.classNames, true, async () => this.commandService.executeCommand(CONFIGURE_SYNC_COMMAND_ID)),
];
}
}
export class UserDataSyncDataViews extends Disposable {
constructor(
container: ViewContainer,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService,
@IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IUserDataSyncMachinesService private readonly userDataSyncMachinesService: IUserDataSyncMachinesService,
) {
super();
this.registerViews(container);
}
private registerViews(container: ViewContainer): void {
const remoteView = this.registerDataView(container, true, true);
this.registerRemoteViewActions(remoteView);
this.registerDataView(container, false, false);
this.registerMachinesView(container);
}
private registerDataView(container: ViewContainer, remote: boolean, showByDefault: boolean): TreeView {
const id = `workbench.views.sync.${remote ? 'remote' : 'local'}DataView`;
const showByDefaultContext = new RawContextKey<boolean>(id, showByDefault);
const viewEnablementContext = showByDefaultContext.bindTo(this.contextKeyService);
const name = remote ? localize('remote title', "Synced Data") : localize('local title', "Local Backup");
const treeView = this.instantiationService.createInstance(TreeView, id, name);
treeView.showCollapseAllAction = true;
treeView.showRefreshAction = true;
const disposable = treeView.onDidChangeVisibility(visible => {
if (visible && !treeView.dataProvider) {
disposable.dispose();
treeView.dataProvider = remote ? new RemoteUserDataSyncHistoryViewDataProvider(this.userDataSyncService, this.userDataSyncEnablementService, this.userDataSyncMachinesService)
: new LocalUserDataSyncHistoryViewDataProvider(this.userDataSyncService, this.userDataSyncEnablementService);
}
});
this._register(Event.any(this.userDataSyncEnablementService.onDidChangeResourceEnablement, this.userDataSyncEnablementService.onDidChangeEnablement)(() => treeView.refresh()));
const viewsRegistry = Registry.as<IViewsRegistry>(Extensions.ViewsRegistry);
viewsRegistry.registerViews([<ITreeViewDescriptor>{
id,
name,
ctorDescriptor: new SyncDescriptor(TreeViewPane),
when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_ACCOUNT_STATE.isEqualTo(AccountStatus.Available), CONTEXT_ENABLE_VIEWS, showByDefaultContext),
canToggleVisibility: true,
canMoveView: true,
treeView,
collapsed: false,
order: 100,
}], container);
registerAction2(class extends Action2 {
constructor() {
super({
id: `workbench.actions.showSync${remote ? 'Remote' : 'Local'}DataView`,
title: remote ?
{ value: localize('workbench.action.showSyncRemoteBackup', "Show Synced Data"), original: `Show Synced Data` }
: { value: localize('workbench.action.showSyncLocalBackup', "Show Local Backup"), original: `Show Local Backup` },
category: { value: localize('sync preferences', "Preferences Sync"), original: `Preferences Sync` },
menu: {
id: MenuId.CommandPalette,
when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_ACCOUNT_STATE.isEqualTo(AccountStatus.Available)),
},
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const viewDescriptorService = accessor.get(IViewDescriptorService);
const viewsService = accessor.get(IViewsService);
const commandService = accessor.get(ICommandService);
await commandService.executeCommand(ENABLE_SYNC_VIEWS_COMMAND_ID);
viewEnablementContext.set(true);
const viewContainer = viewDescriptorService.getViewContainerByViewId(id);
if (viewContainer) {
const model = viewDescriptorService.getViewContainerModel(viewContainer);
if (model.activeViewDescriptors.some(viewDescriptor => viewDescriptor.id === id)) {
viewsService.openView(id, true);
} else {
const disposable = model.onDidChangeActiveViewDescriptors(e => {
if (e.added.some(viewDescriptor => viewDescriptor.id === id)) {
disposable.dispose();
viewsService.openView(id, true);
}
});
}
}
}
});
this.registerDataViewActions(id);
return treeView;
}
private registerMachinesView(container: ViewContainer): void {
const that = this;
const id = `workbench.views.sync.machines`;
const name = localize('synced machines', "Synced Machines");
const treeView = this.instantiationService.createInstance(TreeView, id, name);
treeView.showRefreshAction = true;
const disposable = treeView.onDidChangeVisibility(visible => {
if (visible && !treeView.dataProvider) {
disposable.dispose();
treeView.dataProvider = new UserDataSyncMachinesViewDataProvider(treeView, this.userDataSyncMachinesService);
}
});
this._register(Event.any(this.userDataSyncEnablementService.onDidChangeResourceEnablement, this.userDataSyncEnablementService.onDidChangeEnablement)(() => treeView.refresh()));
const viewsRegistry = Registry.as<IViewsRegistry>(Extensions.ViewsRegistry);
viewsRegistry.registerViews([<ITreeViewDescriptor>{
id,
name,
ctorDescriptor: new SyncDescriptor(TreeViewPane),
when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_ACCOUNT_STATE.isEqualTo(AccountStatus.Available), CONTEXT_ENABLE_VIEWS),
canToggleVisibility: true,
canMoveView: true,
treeView,
collapsed: false,
order: 200,
}], container);
registerAction2(class extends Action2 {
constructor() {
super({
id: `workbench.actions.sync.editCurrentMachineName`,
title: localize('workbench.actions.sync.editCurrentMachineName', "Edit Name"),
icon: Codicon.edit,
menu: {
id: MenuId.ViewItemContext,
when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', id), ContextKeyEqualsExpr.create('viewItem', 'sync-machine')),
group: 'inline',
},
});
}
async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise<void> {
const quickInputService = accessor.get(IQuickInputService);
const inputBox = quickInputService.createInputBox();
inputBox.placeholder = localize('placeholder', "Enter the name of the machine");
inputBox.show();
return new Promise((c, e) => {
inputBox.onDidAccept(async () => {
const name = inputBox.value;
inputBox.dispose();
if (name) {
try {
await that.userDataSyncMachinesService.renameMachine(handle.$treeItemHandle, name);
await treeView.refresh();
c();
} catch (error) {
e(error);
return;
}
}
});
});
}
});
}
private registerDataViewActions(viewId: string) {
registerAction2(class extends Action2 {
constructor() {
super({
id: `workbench.actions.sync.resolveResource`,
title: localize('workbench.actions.sync.resolveResourceRef', "Show raw JSON sync data"),
menu: {
id: MenuId.ViewItemContext,
when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', viewId), ContextKeyExpr.regex('viewItem', /sync-resource-.*/i))
},
});
}
async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise<void> {
const { resource } = <{ resource: string }>JSON.parse(handle.$treeItemHandle);
const editorService = accessor.get(IEditorService);
await editorService.openEditor({ resource: URI.parse(resource) });
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: `workbench.actions.sync.replaceCurrent`,
title: localize('workbench.actions.sync.replaceCurrent', "Download..."),
icon: { id: 'codicon/cloud-download' },
menu: {
id: MenuId.ViewItemContext,
when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', viewId), ContextKeyExpr.regex('viewItem', /sync-resource-.*/i)),
group: 'inline',
},
});
}
async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise<void> {
const dialogService = accessor.get(IDialogService);
const userDataSyncService = accessor.get(IUserDataSyncService);
const { resource, syncResource } = <{ resource: string, syncResource: SyncResource }>JSON.parse(handle.$treeItemHandle);
const result = await dialogService.confirm({
message: localize('confirm replace', "Would you like to replace your current {0} with selected?", getSyncAreaLabel(syncResource)),
type: 'info',
title: localize('preferences sync', "Preferences Sync")
});
if (result.confirmed) {
return userDataSyncService.replace(URI.parse(resource));
}
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: `workbench.actions.sync.commpareWithLocal`,
title: localize('workbench.actions.sync.commpareWithLocal', "Open Changes"),
});
}
async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise<void> {
const editorService = accessor.get(IEditorService);
const { resource, comparableResource } = <{ resource: string, comparableResource?: string }>JSON.parse(handle.$treeItemHandle);
if (comparableResource) {
await editorService.openEditor({
leftResource: URI.parse(resource),
rightResource: URI.parse(comparableResource),
options: {
preserveFocus: true,
revealIfVisible: true,
},
});
} else {
await editorService.openEditor({ resource: URI.parse(resource) });
}
}
});
}
private registerRemoteViewActions(view: TreeView) {
this.registerResetAction(view);
}
private registerResetAction(view: TreeView) {
registerAction2(class extends Action2 {
constructor() {
super({
id: `workbench.actions.syncData.reset`,
title: localize('workbench.actions.syncData.reset', "Reset Synced Data"),
menu: {
id: MenuId.ViewTitle,
when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', view.id)),
},
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const dialogService = accessor.get(IDialogService);
const userDataSyncWorkbenchService = accessor.get(IUserDataSyncWorkbenchService);
const result = await dialogService.confirm({
message: localize('reset', "This will clear your synced data from the cloud and stop sync on all your devices."),
title: localize('reset title', "Reset Synced Data"),
type: 'info',
primaryButton: localize('reset button', "Reset"),
});
if (result.confirmed) {
await userDataSyncWorkbenchService.turnoff(true);
await view.refresh();
}
}
});
}
}
interface SyncResourceTreeItem extends ITreeItem {
resource: SyncResource;
resourceHandle: ISyncResourceHandle;
}
abstract class UserDataSyncHistoryViewDataProvider implements ITreeViewDataProvider {
constructor(
protected readonly userDataSyncService: IUserDataSyncService,
private readonly userDataSyncEnablementService: IUserDataSyncEnablementService,
) { }
async getChildren(element?: ITreeItem): Promise<ITreeItem[]> {
if (!element) {
return this.getRoots();
}
const syncResource = ALL_SYNC_RESOURCES.filter(key => key === element.handle)[0] as SyncResource;
if (syncResource) {
return this.getChildrenForSyncResource(syncResource);
}
if ((<SyncResourceTreeItem>element).resourceHandle) {
return this.getChildrenForSyncResourceTreeItem(<SyncResourceTreeItem>element);
}
return [];
}
protected async getRoots(): Promise<ITreeItem[]> {
return ALL_SYNC_RESOURCES.map(resourceKey => ({
handle: resourceKey,
collapsibleState: TreeItemCollapsibleState.Collapsed,
label: { label: getSyncAreaLabel(resourceKey) },
description: !this.userDataSyncEnablementService.isEnabled() || this.userDataSyncEnablementService.isResourceEnabled(resourceKey) ? undefined : localize('not syncing', "Not syncing"),
themeIcon: FolderThemeIcon,
contextValue: resourceKey
}));
}
protected async getChildrenForSyncResource(syncResource: SyncResource): Promise<ITreeItem[]> {
const refHandles = await this.getSyncResourceHandles(syncResource);
return refHandles.map(({ uri, created }) => {
const handle = JSON.stringify({ resource: uri.toString(), syncResource });
return <SyncResourceTreeItem>{
handle,
collapsibleState: TreeItemCollapsibleState.Collapsed,
label: { label: label(new Date(created)) },
description: fromNow(created, true),
resourceUri: uri,
resource: syncResource,
resourceHandle: { uri, created },
contextValue: `sync-resource-${syncResource}`
};
});
}
protected async getChildrenForSyncResourceTreeItem(element: SyncResourceTreeItem): Promise<ITreeItem[]> {
const associatedResources = await this.userDataSyncService.getAssociatedResources((<SyncResourceTreeItem>element).resource, (<SyncResourceTreeItem>element).resourceHandle);
return associatedResources.map(({ resource, comparableResource }) => {
const handle = JSON.stringify({ resource: resource.toString(), comparableResource: comparableResource?.toString() });
return {
handle,
collapsibleState: TreeItemCollapsibleState.None,
resourceUri: resource,
command: { id: `workbench.actions.sync.commpareWithLocal`, title: '', arguments: [<TreeViewItemHandleArg>{ $treeViewId: '', $treeItemHandle: handle }] },
contextValue: `sync-associatedResource-${(<SyncResourceTreeItem>element).resource}`
};
});
}
protected abstract getSyncResourceHandles(syncResource: SyncResource): Promise<ISyncResourceHandle[]>;
}
class LocalUserDataSyncHistoryViewDataProvider extends UserDataSyncHistoryViewDataProvider {
protected getSyncResourceHandles(syncResource: SyncResource): Promise<ISyncResourceHandle[]> {
return this.userDataSyncService.getLocalSyncResourceHandles(syncResource);
}
}
class RemoteUserDataSyncHistoryViewDataProvider extends UserDataSyncHistoryViewDataProvider {
private machinesPromise: Promise<IUserDataSyncMachine[]> | undefined;
constructor(
userDataSyncService: IUserDataSyncService,
userDataSyncEnablementService: IUserDataSyncEnablementService,
private readonly userDataSyncMachinesService: IUserDataSyncMachinesService,
) {
super(userDataSyncService, userDataSyncEnablementService);
}
async getChildren(element?: ITreeItem): Promise<ITreeItem[]> {
if (!element) {
this.machinesPromise = undefined;
}
return super.getChildren(element);
}
private getMachines(): Promise<IUserDataSyncMachine[]> {
if (this.machinesPromise === undefined) {
this.machinesPromise = this.userDataSyncMachinesService.getMachines();
}
return this.machinesPromise;
}
protected getSyncResourceHandles(syncResource: SyncResource): Promise<ISyncResourceHandle[]> {
return this.userDataSyncService.getRemoteSyncResourceHandles(syncResource);
}
protected async getChildrenForSyncResourceTreeItem(element: SyncResourceTreeItem): Promise<ITreeItem[]> {
const children = await super.getChildrenForSyncResourceTreeItem(element);
const machineId = await this.userDataSyncService.getMachineId(element.resource, element.resourceHandle);
if (machineId) {
const machines = await this.getMachines();
const machine = machines.find(({ id }) => id === machineId);
children.push({
handle: machineId,
label: { label: machine?.name || machineId },
collapsibleState: TreeItemCollapsibleState.None,
themeIcon: Codicon.vm,
});
}
return children;
}
}
class UserDataSyncMachinesViewDataProvider implements ITreeViewDataProvider {
constructor(
private readonly treeView: TreeView,
private readonly userDataSyncMachinesService: IUserDataSyncMachinesService,
) { }
async getChildren(): Promise<ITreeItem[]> {
let machines = await this.userDataSyncMachinesService.getMachines();
machines = machines.filter(m => !m.disabled).sort((m1, m2) => m1.isCurrent ? -1 : 1);
this.treeView.message = machines.length ? undefined : localize('no machines', "No Machines");
return machines.map(({ id, name, isCurrent }) => ({
handle: id,
collapsibleState: TreeItemCollapsibleState.None,
label: { label: name },
description: isCurrent ? localize({ key: 'current', comment: ['Current machine'] }, "Current") : undefined,
themeIcon: Codicon.vm,
contextValue: 'sync-machine'
}));
}
}
function label(date: Date): string {
return date.toLocaleDateString() +
' ' + pad(date.getHours(), 2) +
':' + pad(date.getMinutes(), 2) +
':' + pad(date.getSeconds(), 2);
}

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { IUserDataSyncUtilService, CONTEXT_SYNC_STATE, SyncStatus } from 'vs/platform/userDataSync/common/userDataSync';
import { IUserDataSyncUtilService, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync';
import { Registry } from 'vs/platform/registry/common/platform';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService';
@@ -14,7 +14,13 @@ import { localize } from 'vs/nls';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IFileService } from 'vs/platform/files/common/files';
import { IElectronService } from 'vs/platform/electron/node/electron';
import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { Action } from 'vs/base/common/actions';
import { IWorkbenchIssueService } from 'vs/workbench/contrib/issue/electron-browser/issue';
import { Disposable } from 'vs/base/common/lifecycle';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { CONTEXT_SYNC_STATE, SHOW_SYNC_LOG_COMMAND_ID } from 'vs/workbench/services/userDataSync/common/userDataSync';
class UserDataSyncServicesContribution implements IWorkbenchContribution {
@@ -26,8 +32,39 @@ class UserDataSyncServicesContribution implements IWorkbenchContribution {
}
}
class UserDataSyncReportIssueContribution extends Disposable implements IWorkbenchContribution {
constructor(
@IUserDataAutoSyncService userDataAutoSyncService: IUserDataAutoSyncService,
@INotificationService private readonly notificationService: INotificationService,
@IWorkbenchIssueService private readonly workbenchIssueService: IWorkbenchIssueService,
@ICommandService private readonly commandService: ICommandService,
) {
super();
this._register(userDataAutoSyncService.onError(error => this.onAutoSyncError(error)));
}
private onAutoSyncError(error: UserDataSyncError): void {
switch (error.code) {
case UserDataSyncErrorCode.LocalTooManyRequests:
this.notificationService.notify({
severity: Severity.Error,
message: localize('too many requests', "Turned off syncing preferences on this device because it is making too many requests. Please report an issue by providing the sync logs."),
actions: {
primary: [
new Action('Show Sync Logs', localize('show sync logs', "Show Log"), undefined, true, () => this.commandService.executeCommand(SHOW_SYNC_LOG_COMMAND_ID)),
new Action('Report Issue', localize('report issue', "Report Issue"), undefined, true, () => this.workbenchIssueService.openReporter())
]
}
});
return;
}
}
}
const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
workbenchRegistry.registerWorkbenchContribution(UserDataSyncServicesContribution, LifecyclePhase.Starting);
workbenchRegistry.registerWorkbenchContribution(UserDataSyncReportIssueContribution, LifecyclePhase.Restored);
registerAction2(class OpenSyncBackupsFolder extends Action2 {
constructor() {