diff --git a/src/vs/platform/files/common/inMemoryFilesystemProvider.ts b/src/vs/platform/files/common/inMemoryFilesystemProvider.ts index 5ee8e5366b6..615948123ad 100644 --- a/src/vs/platform/files/common/inMemoryFilesystemProvider.ts +++ b/src/vs/platform/files/common/inMemoryFilesystemProvider.ts @@ -52,10 +52,20 @@ export type Entry = File | Directory; export class InMemoryFileSystemProvider extends Disposable implements IFileSystemProviderWithFileReadWriteCapability { - readonly capabilities: FileSystemProviderCapabilities = - FileSystemProviderCapabilities.FileReadWrite - | FileSystemProviderCapabilities.PathCaseSensitive; - readonly onDidChangeCapabilities: Event = Event.None; + private _onDidChangeCapabilities = this._register(new Emitter()); + readonly onDidChangeCapabilities = this._onDidChangeCapabilities.event; + + private _capabilities = FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.PathCaseSensitive; + get capabilities(): FileSystemProviderCapabilities { return this._capabilities; } + + setReadOnly(readonly: boolean) { + const isReadonly = !!(this._capabilities & FileSystemProviderCapabilities.Readonly); + if (readonly !== isReadonly) { + this._capabilities = readonly ? FileSystemProviderCapabilities.Readonly | FileSystemProviderCapabilities.PathCaseSensitive | FileSystemProviderCapabilities.FileReadWrite + : FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.PathCaseSensitive; + this._onDidChangeCapabilities.fire(); + } + } root = new Directory(''); diff --git a/src/vs/platform/userDataProfile/electron-sandbox/userDataProfileStorageService.ts b/src/vs/platform/userDataProfile/electron-sandbox/userDataProfileStorageService.ts index b0d9c5082fd..f000b71599d 100644 --- a/src/vs/platform/userDataProfile/electron-sandbox/userDataProfileStorageService.ts +++ b/src/vs/platform/userDataProfile/electron-sandbox/userDataProfileStorageService.ts @@ -12,6 +12,7 @@ import { AbstractUserDataProfileStorageService, IProfileStorageChanges, IUserDat import { isProfileUsingDefaultStorage, IStorageService } from 'vs/platform/storage/common/storage'; import { ApplicationStorageDatabaseClient, ProfileStorageDatabaseClient } from 'vs/platform/storage/common/storageIpc'; import { IUserDataProfile, IUserDataProfilesService, reviveProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; export class UserDataProfileStorageService extends AbstractUserDataProfileStorageService implements IUserDataProfileStorageService { @@ -50,3 +51,5 @@ export class UserDataProfileStorageService extends AbstractUserDataProfileStorag return isProfileUsingDefaultStorage(profile) ? new ApplicationStorageDatabaseClient(storageChannel) : new ProfileStorageDatabaseClient(storageChannel, profile); } } + +registerSingleton(IUserDataProfileStorageService, UserDataProfileStorageService, InstantiationType.Delayed); diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index f1c508f2e79..2efbf6f249b 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -43,8 +43,6 @@ import { StringSHA1 } from 'vs/base/common/hash'; import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; import { GestureEvent } from 'vs/base/browser/touch'; import { IPaneCompositePart, IPaneCompositeSelectorPart } from 'vs/workbench/browser/parts/paneCompositePart'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { Extensions, IProfileStorageRegistry } from 'vs/workbench/services/userDataProfile/common/userDataProfileStorageRegistry'; import { IUserDataProfileService, PROFILES_TTILE } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; @@ -149,14 +147,6 @@ export class ActivitybarPart extends Part implements IPaneCompositeSelectorPart this.registerListeners(); - Registry.as(Extensions.ProfileStorageRegistry) - .registerKeys([{ - key: ActivitybarPart.PINNED_VIEW_CONTAINERS, - description: localize('pinned view containers', "Activity bar entries visibility customizations") - }, { - key: AccountsActivityActionViewItem.ACCOUNTS_VISIBILITY_PREFERENCE_KEY, - description: localize('accounts visibility key', "Accounts entry visibility customization in the activity bar.") - }]); } private createCompositeBar() { diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index 513e653171a..9fbb998f5f3 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -44,7 +44,6 @@ import { IPaneCompositePart, IPaneCompositeSelectorPart } from 'vs/workbench/bro import { IPartOptions } from 'vs/workbench/browser/part'; import { StringSHA1 } from 'vs/base/common/hash'; import { URI } from 'vs/base/common/uri'; -import { Extensions, IProfileStorageRegistry } from 'vs/workbench/services/userDataProfile/common/userDataProfileStorageRegistry'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -210,12 +209,6 @@ export abstract class BasePanelPart extends CompositePart impleme // Global Panel Actions this.globalActions = this._register(this.instantiationService.createInstance(CompositeMenuActions, partId === Parts.PANEL_PART ? MenuId.PanelTitle : MenuId.AuxiliaryBarTitle, undefined, undefined)); this._register(this.globalActions.onDidChange(() => this.updateGlobalToolbarActions())); - - Registry.as(Extensions.ProfileStorageRegistry) - .registerKeys([{ - key: this.pinnedPanelsKey, - description: localize('pinned view containers', "Panel entries visibility customizations") - }]); } protected abstract getActivityHoverOptions(): IActivityHoverOptions; diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts b/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts index 1f5f723f8f0..bf44fea999d 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts @@ -8,9 +8,6 @@ import { isStatusbarEntryLocation, IStatusbarEntryLocation, StatusbarAlignment } import { hide, show, isAncestor } from 'vs/base/browser/dom'; import { IStorageService, StorageScope, IStorageValueChangeEvent, StorageTarget } from 'vs/platform/storage/common/storage'; import { Emitter } from 'vs/base/common/event'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { Extensions, IProfileStorageRegistry } from 'vs/workbench/services/userDataProfile/common/userDataProfileStorageRegistry'; -import { localize } from 'vs/nls'; export interface IStatusbarEntryPriority { @@ -68,11 +65,6 @@ export class StatusbarViewModel extends Disposable { this.restoreState(); this.registerListeners(); - Registry.as(Extensions.ProfileStorageRegistry) - .registerKeys([{ - key: StatusbarViewModel.HIDDEN_ENTRIES_KEY, - description: localize('statusbar.hidden', "Status bar entries visibility customizations"), - }]); } private restoreState(): void { diff --git a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts index 86fb1f94e9b..6b83ad251b3 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts @@ -11,7 +11,7 @@ import { workbenchInstantiationService, TestServiceAccessor, getLastResolvedFile import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IEditorFactoryRegistry, Verbosity, EditorExtensions, EditorInputCapabilities } from 'vs/workbench/common/editor'; import { EncodingMode, TextFileOperationError, TextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; -import { FileOperationResult, FileOperationError, NotModifiedSinceFileOperationError, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; +import { FileOperationResult, FileOperationError, NotModifiedSinceFileOperationError } from 'vs/platform/files/common/files'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { timeout } from 'vs/base/common/async'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; @@ -127,11 +127,10 @@ suite('Files - FileEditorInput', () => { test('reports as readonly with readonly file scheme', async function () { - class ReadonlyInMemoryFileSystemProvider extends InMemoryFileSystemProvider { - override readonly capabilities: FileSystemProviderCapabilities = FileSystemProviderCapabilities.Readonly; - } + const inMemoryFilesystemProvider = new InMemoryFileSystemProvider(); + inMemoryFilesystemProvider.setReadOnly(true); - const disposable = accessor.fileService.registerProvider('someTestingReadonlyScheme', new ReadonlyInMemoryFileSystemProvider()); + const disposable = accessor.fileService.registerProvider('someTestingReadonlyScheme', inMemoryFilesystemProvider); try { const input = createFileInput(toResource.call(this, '/foo/bar/file.js').with({ scheme: 'someTestingReadonlyScheme' })); diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts index 546d3277dd6..d5b8b668319 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts @@ -19,18 +19,17 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { RenameProfileAction } from 'vs/workbench/contrib/userDataProfile/browser/userDataProfileActions'; import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { CURRENT_PROFILE_CONTEXT, HAS_PROFILES_CONTEXT, isUserDataProfileTemplate, IS_CURRENT_PROFILE_TRANSIENT_CONTEXT, IUserDataProfileImportExportService, IUserDataProfileManagementService, IUserDataProfileService, IUserDataProfileTemplate, ManageProfilesSubMenu, PROFILES_CATEGORY, PROFILES_ENABLEMENT_CONTEXT, PROFILES_TTILE, PROFILE_EXTENSION, PROFILE_FILTER } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { CURRENT_PROFILE_CONTEXT, HAS_PROFILES_CONTEXT, isUserDataProfileTemplate, IS_CURRENT_PROFILE_TRANSIENT_CONTEXT, IS_PROFILE_IMPORT_EXPORT_IN_PROGRESS_CONTEXT, IUserDataProfileImportExportService, IUserDataProfileManagementService, IUserDataProfileService, IUserDataProfileTemplate, ManageProfilesSubMenu, PROFILES_CATEGORY, PROFILES_ENABLEMENT_CONTEXT, PROFILES_TTILE, PROFILE_FILTER } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { charCount } from 'vs/base/common/strings'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { joinPath } from 'vs/base/common/resources'; import { Codicon } from 'vs/base/common/codicons'; import { IFileService } from 'vs/platform/files/common/files'; import { asJson, asText, IRequestService } from 'vs/platform/request/common/request'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { URI } from 'vs/base/common/uri'; export class UserDataProfilesWorkbenchContribution extends Disposable implements IWorkbenchContribution { @@ -262,6 +261,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements original: `Export (${that.userDataProfileService.currentProfile.name})...` }, category: PROFILES_CATEGORY, + precondition: IS_PROFILE_IMPORT_EXPORT_IN_PROGRESS_CONTEXT.toNegated(), menu: [ { id: ManageProfilesSubMenu, @@ -276,25 +276,8 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements } async run(accessor: ServicesAccessor) { - const textFileService = accessor.get(ITextFileService); - const fileDialogService = accessor.get(IFileDialogService); const userDataProfileImportExportService = accessor.get(IUserDataProfileImportExportService); - const notificationService = accessor.get(INotificationService); - - const profileLocation = await fileDialogService.showSaveDialog({ - title: localize('export profile dialog', "Save Profile"), - filters: PROFILE_FILTER, - defaultUri: joinPath(await fileDialogService.defaultFilePath(), `profile.${PROFILE_EXTENSION}`), - }); - - if (!profileLocation) { - return; - } - - const profile = await userDataProfileImportExportService.exportProfile({ skipComments: true }); - await textFileService.create([{ resource: profileLocation, value: JSON.stringify(profile), options: { overwrite: true } }]); - - notificationService.info(localize('export success', "{0}: Exported successfully.", PROFILES_CATEGORY.value)); + return userDataProfileImportExportService.exportProfile(); } })); disposables.add(MenuRegistry.appendMenuItem(MenuId.MenubarShare, { @@ -323,6 +306,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements }, category: PROFILES_CATEGORY, f1: true, + precondition: IS_PROFILE_IMPORT_EXPORT_IN_PROGRESS_CONTEXT.toNegated(), menu: [ { id: ManageProfilesSubMenu, @@ -358,11 +342,11 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements const disposables = new DisposableStore(); const quickPick = disposables.add(quickInputService.createQuickPick()); const updateQuickPickItems = (value?: string) => { - const selectFromFileItem: IQuickPickItem = { label: isSettingProfilesEnabled ? localize('select from file', "Select Profile template file") : localize('import from file', "Import from profile file") }; - quickPick.items = value ? [{ label: isSettingProfilesEnabled ? localize('select from url', "Create from template URL") : localize('import from url', "Import from URL"), description: quickPick.value }, selectFromFileItem] : [selectFromFileItem]; + const selectFromFileItem: IQuickPickItem = { label: localize('import from file', "Import from profile file") }; + quickPick.items = value ? [{ label: localize('import from url', "Import from URL"), description: quickPick.value }, selectFromFileItem] : [selectFromFileItem]; }; - quickPick.title = isSettingProfilesEnabled ? localize('create from profile template quick pick title', "Create from Profile Template") : localize('import profile quick pick title', "Import Settings from a Profile"); - quickPick.placeholder = isSettingProfilesEnabled ? localize('create from profile template placeholder', "Provide a template URL or Select a template file") : localize('import profile placeholder', "Provide profile URL or select profile file to import"); + quickPick.title = localize('import profile quick pick title', "Import Profile"); + quickPick.placeholder = localize('import profile placeholder', "Provide profile URL or select profile file to import"); quickPick.ignoreFocusOut = true; disposables.add(quickPick.onDidChangeValue(updateQuickPickItems)); updateQuickPickItems(); @@ -371,11 +355,14 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements disposables.add(quickPick.onDidAccept(async () => { try { quickPick.hide(); - const profile = quickPick.selectedItems[0].description ? await this.getProfileFromURL(quickPick.value, requestService) : await this.getProfileFromFileSystem(fileDialogService, fileService); - if (profile) { - if (isSettingProfilesEnabled) { + if (isSettingProfilesEnabled) { + const profile = quickPick.selectedItems[0].description ? URI.parse(quickPick.value) : await this.getProfileUriFromFileSystem(fileDialogService); + if (profile) { await userDataProfileImportExportService.importProfile(profile); - } else { + } + } else { + const profile = quickPick.selectedItems[0].description ? await this.getProfileFromURL(quickPick.value, requestService) : await this.getProfileFromFileSystem(fileDialogService, fileService); + if (profile) { await userDataProfileImportExportService.setProfile(profile); } } @@ -387,7 +374,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements quickPick.show(); } - private async getProfileFromFileSystem(fileDialogService: IFileDialogService, fileService: IFileService): Promise { + private async getProfileUriFromFileSystem(fileDialogService: IFileDialogService): Promise { const profileLocation = await fileDialogService.showOpenDialog({ canSelectFolders: false, canSelectFiles: true, @@ -398,7 +385,15 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements if (!profileLocation) { return null; } - const content = (await fileService.readFile(profileLocation[0])).value.toString(); + return profileLocation[0]; + } + + private async getProfileFromFileSystem(fileDialogService: IFileDialogService, fileService: IFileService): Promise { + const profileLocation = await this.getProfileUriFromFileSystem(fileDialogService); + if (!profileLocation) { + return null; + } + const content = (await fileService.readFile(profileLocation)).value.toString(); const parsed = JSON.parse(content); return isUserDataProfileTemplate(parsed) ? parsed : null; } diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts index 6d2bf5f714b..5485b67400b 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts @@ -68,22 +68,25 @@ export class NativeExtensionManagementService extends ExtensionManagementChannel override async install(vsix: URI, options?: InstallVSIXOptions): Promise { const { location, cleanup } = await this.downloadVsix(vsix); try { - return await super.install(location, { ...options, profileLocation: this.userDataProfileService.currentProfile.extensionsResource }); + options = options?.profileLocation ? options : { ...options, profileLocation: this.userDataProfileService.currentProfile.extensionsResource }; + return await super.install(location, options); } finally { await cleanup(); } } override installFromGallery(extension: IGalleryExtension, installOptions?: InstallOptions): Promise { - return super.installFromGallery(extension, { ...installOptions, profileLocation: this.userDataProfileService.currentProfile.extensionsResource }); + installOptions = installOptions?.profileLocation ? installOptions : { ...installOptions, profileLocation: this.userDataProfileService.currentProfile.extensionsResource }; + return super.installFromGallery(extension, installOptions); } override uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise { - return super.uninstall(extension, { ...options, profileLocation: this.userDataProfileService.currentProfile.extensionsResource }); + options = options?.profileLocation ? options : { ...options, profileLocation: this.userDataProfileService.currentProfile.extensionsResource }; + return super.uninstall(extension, options); } - override getInstalled(type: ExtensionType | null = null): Promise { - return super.getInstalled(type, this.userDataProfileService.currentProfile.extensionsResource); + override getInstalled(type: ExtensionType | null = null, profileLocation: URI = this.userDataProfileService.currentProfile.extensionsResource): Promise { + return super.getInstalled(type, profileLocation); } private async downloadVsix(vsix: URI): Promise<{ location: URI; cleanup: () => Promise }> { diff --git a/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts b/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts new file mode 100644 index 00000000000..81e9272808f --- /dev/null +++ b/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts @@ -0,0 +1,224 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; +import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService'; +import { IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGlobalExtensionEnablementService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { ExtensionType } from 'vs/platform/extensions/common/extensions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IUserDataProfileStorageService } from 'vs/platform/userDataProfile/common/userDataProfileStorageService'; +import { ITreeItemCheckboxState, TreeItemCollapsibleState } from 'vs/workbench/common/views'; +import { IProfileResource, IProfileResourceChildTreeItem, IProfileResourceTreeItem } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; + +interface IProfileExtension { + identifier: IExtensionIdentifier; + displayName?: string; + preRelease?: boolean; + disabled?: boolean; +} + +export class ExtensionsResource implements IProfileResource { + + constructor( + @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, + @IUserDataProfileStorageService private readonly userDataProfileStorageService: IUserDataProfileStorageService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @ILogService private readonly logService: ILogService, + ) { + } + + async getContent(profile: IUserDataProfile, exclude?: string[]): Promise { + const extensions = await this.getLocalExtensions(profile); + return JSON.stringify(exclude?.length ? extensions.filter(e => !exclude.includes(e.identifier.id.toLowerCase())) : extensions); + } + + async apply(content: string, profile: IUserDataProfile): Promise { + return this.withProfileScopedServices(profile, async (extensionEnablementService) => { + const profileExtensions: IProfileExtension[] = await this.getProfileExtensions(content); + const installedExtensions = await this.extensionManagementService.getInstalled(undefined, profile.extensionsResource); + const extensionsToEnableOrDisable: { extension: ILocalExtension; enable: boolean }[] = []; + const extensionsToInstall: IProfileExtension[] = []; + for (const e of profileExtensions) { + const isDisabled = extensionEnablementService.getDisabledExtensions().some(disabledExtension => areSameExtensions(disabledExtension, e.identifier)); + const installedExtension = installedExtensions.find(installed => areSameExtensions(installed.identifier, e.identifier)); + if (!installedExtension || installedExtension.preRelease !== e.preRelease) { + extensionsToInstall.push(e); + } + if (installedExtension && isDisabled !== !!e.disabled) { + extensionsToEnableOrDisable.push({ extension: installedExtension, enable: !!e.disabled }); + } + } + const extensionsToUninstall: ILocalExtension[] = installedExtensions.filter(extension => extension.type === ExtensionType.User && !profileExtensions.some(({ identifier }) => areSameExtensions(identifier, extension.identifier))); + for (const { extension, enable } of extensionsToEnableOrDisable) { + if (enable) { + await extensionEnablementService.enableExtension(extension.identifier); + } else { + await extensionEnablementService.disableExtension(extension.identifier); + } + } + if (extensionsToInstall.length) { + const galleryExtensions = await this.extensionGalleryService.getExtensions(extensionsToInstall.map(e => ({ ...e.identifier, hasPreRelease: e.preRelease })), CancellationToken.None); + await Promise.all(extensionsToInstall.map(async e => { + const extension = galleryExtensions.find(galleryExtension => areSameExtensions(galleryExtension.identifier, e.identifier)); + if (!extension) { + return; + } + if (await this.extensionManagementService.canInstall(extension)) { + await this.extensionManagementService.installFromGallery(extension, { isMachineScoped: false, donotIncludePackAndDependencies: true, installPreReleaseVersion: e.preRelease, profileLocation: profile.extensionsResource } /* set isMachineScoped value to prevent install and sync dialog in web */); + } else { + this.logService.info(`Profile: Skipped installing extension because it cannot be installed.`, extension.displayName || extension.identifier.id); + } + })); + } + if (extensionsToUninstall.length) { + await Promise.all(extensionsToUninstall.map(e => this.extensionManagementService.uninstall(e))); + } + }); + } + + async getLocalExtensions(profile: IUserDataProfile): Promise { + return this.withProfileScopedServices(profile, async (extensionEnablementService) => { + const result: Array = []; + const installedExtensions = await this.extensionManagementService.getInstalled(undefined, profile.extensionsResource); + const disabledExtensions = extensionEnablementService.getDisabledExtensions(); + for (const extension of installedExtensions) { + const { identifier, preRelease } = extension; + const disabled = disabledExtensions.some(disabledExtension => areSameExtensions(disabledExtension, identifier)); + if (extension.type === ExtensionType.System && !disabled) { + // skip enabled system extensions + continue; + } + if (extension.type === ExtensionType.User) { + if (!extension.identifier.uuid) { + // skip user extensions without uuid + continue; + } + if (disabled && !extension.isBuiltin) { + // skip user disabled extensions + continue; + } + } + const profileExtension: IProfileExtension = { identifier, displayName: extension.manifest.displayName }; + if (disabled) { + profileExtension.disabled = true; + } + if (preRelease) { + profileExtension.preRelease = true; + } + result.push(profileExtension); + } + return result; + }); + } + + async getProfileExtensions(content: string): Promise { + return JSON.parse(content); + } + + private async withProfileScopedServices(profile: IUserDataProfile, fn: (extensionEnablementService: IGlobalExtensionEnablementService) => Promise): Promise { + return this.userDataProfileStorageService.withProfileScopedStorageService(profile, + async storageService => { + const disposables = new DisposableStore(); + const instantiationService = this.instantiationService.createChild(new ServiceCollection([IStorageService, storageService])); + const extensionEnablementService = disposables.add(instantiationService.createInstance(GlobalExtensionEnablementService)); + try { + return await fn(extensionEnablementService); + } finally { + disposables.dispose(); + } + }); + } +} + +export class ExtensionsResourceExportTreeItem implements IProfileResourceTreeItem { + + readonly handle = this.profile.extensionsResource.toString(); + readonly label = { label: localize('extensions', "Extensions") }; + readonly collapsibleState = TreeItemCollapsibleState.Expanded; + checkbox: ITreeItemCheckboxState = { isChecked: true }; + + private readonly excludedExtensions = new Set(); + + constructor( + private readonly profile: IUserDataProfile, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { } + + async getChildren(): Promise { + const extensions = await this.instantiationService.createInstance(ExtensionsResource).getLocalExtensions(this.profile); + const that = this; + return extensions.map(e => ({ + handle: e.identifier.id.toLowerCase(), + parent: this, + label: { label: e.displayName || e.identifier.id }, + description: e.disabled ? localize('disabled', "Disabled") : undefined, + collapsibleState: TreeItemCollapsibleState.None, + checkbox: { + get isChecked() { return !that.excludedExtensions.has(e.identifier.id.toLowerCase()); }, + set isChecked(value: boolean) { + if (value) { + that.excludedExtensions.delete(e.identifier.id.toLowerCase()); + } else { + that.excludedExtensions.add(e.identifier.id.toLowerCase()); + } + } + }, + command: { + id: 'extension.open', + title: '', + arguments: [e.identifier.id] + } + })); + } + + async hasContent(): Promise { + const extensions = await this.instantiationService.createInstance(ExtensionsResource).getLocalExtensions(this.profile); + return extensions.length > 0; + } + + async getContent(): Promise { + return this.instantiationService.createInstance(ExtensionsResource).getContent(this.profile, [...this.excludedExtensions.values()]); + } + +} + +export class ExtensionsResourceImportTreeItem implements IProfileResourceTreeItem { + + readonly handle = 'extensions'; + readonly label = { label: localize('extensions', "Extensions") }; + readonly collapsibleState = TreeItemCollapsibleState.Expanded; + + constructor( + private readonly content: string, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { } + + async getChildren(): Promise { + const extensions = await this.instantiationService.createInstance(ExtensionsResource).getProfileExtensions(this.content); + return extensions.map(e => ({ + handle: e.identifier.id.toLowerCase(), + parent: this, + label: { label: e.displayName || e.identifier.id }, + description: e.disabled ? localize('disabled', "Disabled") : undefined, + collapsibleState: TreeItemCollapsibleState.None, + command: { + id: 'extension.open', + title: '', + arguments: [e.identifier.id] + } + })); + } + +} + + diff --git a/src/vs/workbench/services/userDataProfile/browser/globalStateResource.ts b/src/vs/workbench/services/userDataProfile/browser/globalStateResource.ts new file mode 100644 index 00000000000..526a46ce468 --- /dev/null +++ b/src/vs/workbench/services/userDataProfile/browser/globalStateResource.ts @@ -0,0 +1,116 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IStringDictionary } from 'vs/base/common/collections'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IUserDataProfileStorageService } from 'vs/platform/userDataProfile/common/userDataProfileStorageService'; +import { API_OPEN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; +import { ITreeItemCheckboxState, TreeItemCollapsibleState } from 'vs/workbench/common/views'; +import { IProfileResource, IProfileResourceTreeItem } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; + +interface IGlobalState { + storage: IStringDictionary; +} + +export class GlobalStateResource implements IProfileResource { + + constructor( + @IStorageService private readonly storageService: IStorageService, + @IUserDataProfileStorageService private readonly userDataProfileStorageService: IUserDataProfileStorageService, + @ILogService private readonly logService: ILogService, + ) { + } + + async getContent(profile: IUserDataProfile): Promise { + const globalState = await this.getGlobalState(profile); + return JSON.stringify(globalState); + } + + async apply(content: string, profile: IUserDataProfile): Promise { + const globalState: IGlobalState = JSON.parse(content); + await this.writeGlobalState(globalState, profile); + } + + async getGlobalState(profile: IUserDataProfile): Promise { + const storage: IStringDictionary = {}; + const storageData = await this.userDataProfileStorageService.readStorageData(profile); + for (const [key, value] of storageData) { + if (value.value !== undefined && value.target === StorageTarget.USER) { + storage[key] = value.value; + } + } + return { storage }; + } + + private async writeGlobalState(globalState: IGlobalState, profile: IUserDataProfile): Promise { + const storageKeys = Object.keys(globalState.storage); + if (storageKeys.length) { + const updatedStorage = new Map(); + const nonProfileKeys = [ + // Do not include application scope user target keys because they also include default profile user target keys + ...this.storageService.keys(StorageScope.APPLICATION, StorageTarget.MACHINE), + ...this.storageService.keys(StorageScope.WORKSPACE, StorageTarget.USER), + ...this.storageService.keys(StorageScope.WORKSPACE, StorageTarget.MACHINE), + ]; + for (const key of storageKeys) { + if (nonProfileKeys.includes(key)) { + this.logService.info(`Profile: Ignoring global state key '${key}' because it is not a profile key.`); + } else { + updatedStorage.set(key, globalState.storage[key]); + } + } + await this.userDataProfileStorageService.updateStorageData(profile, updatedStorage, StorageTarget.USER); + } + } +} + +export class GlobalStateResourceExportTreeItem implements IProfileResourceTreeItem { + + readonly handle = this.profile.globalStorageHome.toString(); + readonly label = { label: localize('globalState', "UI State") }; + readonly collapsibleState = TreeItemCollapsibleState.None; + checkbox: ITreeItemCheckboxState = { isChecked: true }; + + constructor( + private readonly profile: IUserDataProfile, + @IInstantiationService private readonly instantiationService: IInstantiationService + ) { } + + async getChildren(): Promise { return undefined; } + + async hasContent(): Promise { + const globalState = await this.instantiationService.createInstance(GlobalStateResource).getGlobalState(this.profile); + return Object.keys(globalState.storage).length > 0; + } + + async getContent(): Promise { + return this.instantiationService.createInstance(GlobalStateResource).getContent(this.profile); + } + +} + +export class GlobalStateResourceImportTreeItem implements IProfileResourceTreeItem { + + readonly handle = 'globalState'; + readonly label = { label: localize('globalState', "UI State") }; + readonly collapsibleState = TreeItemCollapsibleState.None; + readonly command = { + id: API_OPEN_EDITOR_COMMAND_ID, + title: '', + arguments: [this.resource, undefined, undefined] + }; + + constructor(private readonly resource: URI) { } + + async getChildren(): Promise { return undefined; } +} + + + diff --git a/src/vs/workbench/services/userDataProfile/browser/keybindingsResource.ts b/src/vs/workbench/services/userDataProfile/browser/keybindingsResource.ts new file mode 100644 index 00000000000..a2707695411 --- /dev/null +++ b/src/vs/workbench/services/userDataProfile/browser/keybindingsResource.ts @@ -0,0 +1,93 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from 'vs/base/common/buffer'; +import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IProfileResource, IProfileResourceTreeItem } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { platform, Platform } from 'vs/base/common/platform'; +import { ITreeItemCheckboxState, TreeItemCollapsibleState } from 'vs/workbench/common/views'; +import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { API_OPEN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { localize } from 'vs/nls'; + +interface IKeybindingsResourceContent { + platform: Platform; + keybindings: string | null; +} + +export class KeybindingsResource implements IProfileResource { + + constructor( + @IFileService private readonly fileService: IFileService, + @ILogService private readonly logService: ILogService, + ) { + } + + async getContent(profile: IUserDataProfile): Promise { + const keybindingsContent = await this.getKeybindingsResourceContent(profile); + return JSON.stringify(keybindingsContent); + } + + async getKeybindingsResourceContent(profile: IUserDataProfile): Promise { + const keybindings = await this.getKeybindingsContent(profile); + return { keybindings, platform }; + } + + async apply(content: string, profile: IUserDataProfile): Promise { + const keybindingsContent: IKeybindingsResourceContent = JSON.parse(content); + if (keybindingsContent.keybindings === null) { + this.logService.info(`Profile: No keybindings to apply...`); + return; + } + await this.fileService.writeFile(profile.keybindingsResource, VSBuffer.fromString(keybindingsContent.keybindings)); + } + + private async getKeybindingsContent(profile: IUserDataProfile): Promise { + try { + const content = await this.fileService.readFile(profile.keybindingsResource); + return content.value.toString(); + } catch (error) { + // File not found + if (error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) { + return null; + } else { + throw error; + } + } + } + +} + +export class KeybindingsResourceTreeItem implements IProfileResourceTreeItem { + + readonly handle = this.profile.keybindingsResource.toString(); + readonly label = { label: localize('keybindings', "Keyboard Shortcuts") }; + readonly collapsibleState = TreeItemCollapsibleState.None; + checkbox: ITreeItemCheckboxState | undefined = { isChecked: true }; + readonly command = { + id: API_OPEN_EDITOR_COMMAND_ID, + title: '', + arguments: [this.profile.keybindingsResource, undefined, undefined] + }; + + constructor( + private readonly profile: IUserDataProfile, + @IInstantiationService private readonly instantiationService: IInstantiationService + ) { } + + async getChildren(): Promise { return undefined; } + + async hasContent(): Promise { + const keybindingsContent = await this.instantiationService.createInstance(KeybindingsResource).getKeybindingsResourceContent(this.profile); + return keybindingsContent.keybindings !== null; + } + + async getContent(): Promise { + return this.instantiationService.createInstance(KeybindingsResource).getContent(this.profile); + } + +} diff --git a/src/vs/workbench/services/userDataProfile/browser/settingsResource.ts b/src/vs/workbench/services/userDataProfile/browser/settingsResource.ts new file mode 100644 index 00000000000..cdb12f8f329 --- /dev/null +++ b/src/vs/workbench/services/userDataProfile/browser/settingsResource.ts @@ -0,0 +1,112 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from 'vs/base/common/buffer'; +import { ConfigurationScope, Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IProfileResource, IProfileResourceTreeItem } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { updateIgnoredSettings } from 'vs/platform/userDataSync/common/settingsMerge'; +import { IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync'; +import { ITreeItemCheckboxState, TreeItemCollapsibleState } from 'vs/workbench/common/views'; +import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { API_OPEN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { localize } from 'vs/nls'; + +interface ISettingsContent { + settings: string | null; +} + +export class SettingsResource implements IProfileResource { + + constructor( + @IFileService private readonly fileService: IFileService, + @IUserDataSyncUtilService private readonly userDataSyncUtilService: IUserDataSyncUtilService, + @ILogService private readonly logService: ILogService, + ) { + } + + async getContent(profile: IUserDataProfile): Promise { + const settingsContent = await this.getSettingsContent(profile); + return JSON.stringify(settingsContent); + } + + async getSettingsContent(profile: IUserDataProfile): Promise { + const localContent = await this.getLocalFileContent(profile); + if (localContent === null) { + return { settings: null }; + } else { + const ignoredSettings = this.getIgnoredSettings(); + const formattingOptions = await this.userDataSyncUtilService.resolveFormattingOptions(profile.settingsResource); + const settings = updateIgnoredSettings(localContent || '{}', '{}', ignoredSettings, formattingOptions); + return { settings }; + } + } + + async apply(content: string, profile: IUserDataProfile): Promise { + const settingsContent: ISettingsContent = JSON.parse(content); + if (settingsContent.settings === null) { + this.logService.info(`Profile: No settings to apply...`); + return; + } + const localSettingsContent = await this.getLocalFileContent(profile); + const formattingOptions = await this.userDataSyncUtilService.resolveFormattingOptions(profile.settingsResource); + const contentToUpdate = updateIgnoredSettings(settingsContent.settings, localSettingsContent || '{}', this.getIgnoredSettings(), formattingOptions); + await this.fileService.writeFile(profile.settingsResource, VSBuffer.fromString(contentToUpdate)); + } + + private getIgnoredSettings(): string[] { + const allSettings = Registry.as(Extensions.Configuration).getConfigurationProperties(); + const ignoredSettings = Object.keys(allSettings).filter(key => allSettings[key]?.scope === ConfigurationScope.MACHINE || allSettings[key]?.scope === ConfigurationScope.MACHINE_OVERRIDABLE); + return ignoredSettings; + } + + private async getLocalFileContent(profile: IUserDataProfile): Promise { + try { + const content = await this.fileService.readFile(profile.settingsResource); + return content.value.toString(); + } catch (error) { + // File not found + if (error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) { + return null; + } else { + throw error; + } + } + } + +} + +export class SettingsResourceTreeItem implements IProfileResourceTreeItem { + + readonly handle = this.profile.settingsResource.toString(); + readonly label = { label: localize('settings', "Settings") }; + readonly collapsibleState = TreeItemCollapsibleState.None; + checkbox: ITreeItemCheckboxState | undefined = { isChecked: true }; + readonly command = { + id: API_OPEN_EDITOR_COMMAND_ID, + title: '', + arguments: [this.profile.settingsResource, undefined, undefined] + }; + + constructor( + private readonly profile: IUserDataProfile, + @IInstantiationService private readonly instantiationService: IInstantiationService + ) { } + + async getChildren(): Promise { return undefined; } + + async hasContent(): Promise { + const settingsContent = await this.instantiationService.createInstance(SettingsResource).getSettingsContent(this.profile); + return settingsContent.settings !== null; + } + + async getContent(): Promise { + return this.instantiationService.createInstance(SettingsResource).getContent(this.profile); + } + +} diff --git a/src/vs/workbench/services/userDataProfile/browser/snippetsResource.ts b/src/vs/workbench/services/userDataProfile/browser/snippetsResource.ts new file mode 100644 index 00000000000..2cca57fa599 --- /dev/null +++ b/src/vs/workbench/services/userDataProfile/browser/snippetsResource.ts @@ -0,0 +1,131 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from 'vs/base/common/buffer'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { ResourceSet } from 'vs/base/common/map'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { FileOperationError, FileOperationResult, IFileService, IFileStat } from 'vs/platform/files/common/files'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { API_OPEN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; +import { ITreeItemCheckboxState, TreeItemCollapsibleState } from 'vs/workbench/common/views'; +import { IProfileResource, IProfileResourceChildTreeItem, IProfileResourceTreeItem } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; + +interface ISnippetsContent { + snippets: IStringDictionary; +} + +export class SnippetsResource implements IProfileResource { + + constructor( + @IFileService private readonly fileService: IFileService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + ) { + } + + async getContent(profile: IUserDataProfile, excluded?: ResourceSet): Promise { + const snippets = await this.getSnippets(profile, excluded); + return JSON.stringify({ snippets }); + } + + async apply(content: string, profile: IUserDataProfile): Promise { + const snippetsContent: ISnippetsContent = JSON.parse(content); + for (const key in snippetsContent.snippets) { + const resource = this.uriIdentityService.extUri.joinPath(profile.snippetsHome, key); + await this.fileService.writeFile(resource, VSBuffer.fromString(snippetsContent.snippets[key])); + } + } + + private async getSnippets(profile: IUserDataProfile, excluded?: ResourceSet): Promise> { + const snippets: IStringDictionary = {}; + const snippetsResources = await this.getSnippetsResources(profile, excluded); + for (const resource of snippetsResources) { + const key = this.uriIdentityService.extUri.relativePath(profile.snippetsHome, resource)!; + const content = await this.fileService.readFile(resource); + snippets[key] = content.value.toString(); + } + return snippets; + } + + async getSnippetsResources(profile: IUserDataProfile, excluded?: ResourceSet): Promise { + const snippets: URI[] = []; + let stat: IFileStat; + try { + stat = await this.fileService.resolve(profile.snippetsHome); + } catch (e) { + // No snippets + if (e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) { + return snippets; + } else { + throw e; + } + } + for (const { resource } of stat.children || []) { + if (excluded?.has(resource)) { + continue; + } + const extension = this.uriIdentityService.extUri.extname(resource); + if (extension === '.json' || extension === '.code-snippets') { + snippets.push(resource); + } + } + return snippets; + } +} + +export class SnippetsResourceTreeItem implements IProfileResourceTreeItem { + + readonly handle = this.profile.snippetsHome.toString(); + readonly label = { label: localize('snippets', "Snippets") }; + readonly collapsibleState = TreeItemCollapsibleState.Collapsed; + checkbox: ITreeItemCheckboxState | undefined = { isChecked: true }; + + private readonly excludedSnippets = new ResourceSet(); + + constructor( + private readonly profile: IUserDataProfile, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { } + + async getChildren(): Promise { + const snippetsResources = await this.instantiationService.createInstance(SnippetsResource).getSnippetsResources(this.profile); + const that = this; + return snippetsResources.map(resource => ({ + handle: resource.toString(), + parent: that, + resourceUri: resource, + collapsibleState: TreeItemCollapsibleState.None, + checkbox: that.checkbox ? { + get isChecked() { return !that.excludedSnippets.has(resource); }, + set isChecked(value: boolean) { + if (value) { + that.excludedSnippets.delete(resource); + } else { + that.excludedSnippets.add(resource); + } + } + } : undefined, + command: { + id: API_OPEN_EDITOR_COMMAND_ID, + title: '', + arguments: [resource, undefined, undefined] + } + })); + } + + async hasContent(): Promise { + const snippetsResources = await this.instantiationService.createInstance(SnippetsResource).getSnippetsResources(this.profile); + return snippetsResources.length > 0; + } + + async getContent(): Promise { + return this.instantiationService.createInstance(SnippetsResource).getContent(this.profile, this.excludedSnippets); + } + +} + diff --git a/src/vs/workbench/services/userDataProfile/browser/tasksResource.ts b/src/vs/workbench/services/userDataProfile/browser/tasksResource.ts new file mode 100644 index 00000000000..3929f989fc8 --- /dev/null +++ b/src/vs/workbench/services/userDataProfile/browser/tasksResource.ts @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from 'vs/base/common/buffer'; +import { localize } from 'vs/nls'; +import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { API_OPEN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; +import { ITreeItemCheckboxState, TreeItemCollapsibleState } from 'vs/workbench/common/views'; +import { IProfileResource, IProfileResourceTreeItem } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; + +interface ITasksResourceContent { + tasks: string | null; +} + +export class TasksResource implements IProfileResource { + + constructor( + @IFileService private readonly fileService: IFileService, + @ILogService private readonly logService: ILogService, + ) { + } + + async getContent(profile: IUserDataProfile): Promise { + const tasksContent = await this.getTasksResourceContent(profile); + return JSON.stringify(tasksContent); + } + + async getTasksResourceContent(profile: IUserDataProfile): Promise { + const tasksContent = await this.getTasksContent(profile); + return { tasks: tasksContent }; + } + + async apply(content: string, profile: IUserDataProfile): Promise { + const tasksContent: ITasksResourceContent = JSON.parse(content); + if (!tasksContent.tasks) { + this.logService.info(`Profile: No tasks to apply...`); + return; + } + await this.fileService.writeFile(profile.tasksResource, VSBuffer.fromString(tasksContent.tasks)); + } + + private async getTasksContent(profile: IUserDataProfile): Promise { + try { + const content = await this.fileService.readFile(profile.tasksResource); + return content.value.toString(); + } catch (error) { + // File not found + if (error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) { + return null; + } else { + throw error; + } + } + } + +} + +export class TasksResourceTreeItem implements IProfileResourceTreeItem { + + readonly handle = this.profile.tasksResource.toString(); + readonly label = { label: localize('tasks', "User Tasks") }; + readonly collapsibleState = TreeItemCollapsibleState.None; + checkbox: ITreeItemCheckboxState | undefined = { isChecked: true }; + readonly command = { + id: API_OPEN_EDITOR_COMMAND_ID, + title: '', + arguments: [this.profile.tasksResource, undefined, undefined] + }; + + constructor( + private readonly profile: IUserDataProfile, + @IInstantiationService private readonly instantiationService: IInstantiationService + ) { } + + async getChildren(): Promise { return undefined; } + + async hasContent(): Promise { + const tasksContent = await this.instantiationService.createInstance(TasksResource).getTasksResourceContent(this.profile); + return tasksContent.tasks !== null; + } + + async getContent(): Promise { + return this.instantiationService.createInstance(TasksResource).getContent(this.profile); + } + +} diff --git a/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts b/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts new file mode 100644 index 00000000000..2f2f0731ff3 --- /dev/null +++ b/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts @@ -0,0 +1,699 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import * as DOM from 'vs/base/browser/dom'; +import { IUserDataProfileImportExportService, PROFILE_FILTER, PROFILE_EXTENSION, IUserDataProfileContentHandler, IS_PROFILE_IMPORT_EXPORT_IN_PROGRESS_CONTEXT, PROFILES_TTILE, defaultUserDataProfileIcon, IUserDataProfileService, IProfileResourceTreeItem, IProfileResourceChildTreeItem, PROFILES_CATEGORY, isUserDataProfileTemplate, IUserDataProfileManagementService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IFileService } from 'vs/platform/files/common/files'; +import { URI } from 'vs/base/common/uri'; +import { Extensions, ITreeItem, ITreeViewDataProvider, ITreeViewDescriptor, IViewContainersRegistry, IViewDescriptorService, IViewsRegistry, IViewsService, TreeItemCollapsibleState, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views'; +import { IUserDataProfile, IUserDataProfilesService, toUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ILogService } from 'vs/platform/log/common/log'; +import { TreeView, TreeViewPane } from 'vs/workbench/browser/parts/views/treeView'; +import { SettingsResource, SettingsResourceTreeItem } from 'vs/workbench/services/userDataProfile/browser/settingsResource'; +import { KeybindingsResource, KeybindingsResourceTreeItem } from 'vs/workbench/services/userDataProfile/browser/keybindingsResource'; +import { SnippetsResource, SnippetsResourceTreeItem } from 'vs/workbench/services/userDataProfile/browser/snippetsResource'; +import { TasksResource, TasksResourceTreeItem } from 'vs/workbench/services/userDataProfile/browser/tasksResource'; +import { ExtensionsResource, ExtensionsResourceExportTreeItem, ExtensionsResourceImportTreeItem } from 'vs/workbench/services/userDataProfile/browser/extensionsResource'; +import { GlobalStateResource, GlobalStateResourceExportTreeItem, GlobalStateResourceImportTreeItem } from 'vs/workbench/services/userDataProfile/browser/globalStateResource'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { generateUuid } from 'vs/base/common/uuid'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { EditorsOrder } from 'vs/workbench/common/editor'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { joinPath } from 'vs/base/common/resources'; + +interface IUserDataProfileTemplate { + readonly name: string; + readonly shortName?: string; + readonly settings?: string; + readonly keybindings?: string; + readonly tasks?: string; + readonly snippets?: string; + readonly globalState?: string; + readonly extensions?: string; +} + +export class UserDataProfileImportExportService extends Disposable implements IUserDataProfileImportExportService { + + readonly _serviceBrand: undefined; + + private profileContentHandlers = new Map(); + private readonly isProfileImportExportInProgressContextKey: IContextKey; + + private readonly viewContainer: ViewContainer; + private readonly fileUserDataProfileContentHandler: IUserDataProfileContentHandler; + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, + @IViewsService private readonly viewsService: IViewsService, + @IEditorService private readonly editorService: IEditorService, + @IContextKeyService contextKeyService: IContextKeyService, + @IFileService private readonly fileService: IFileService, + @IUserDataProfileManagementService private readonly userDataProfileManagementService: IUserDataProfileManagementService, + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, + @IExtensionService private readonly extensionService: IExtensionService, + @IQuickInputService private readonly quickInputService: IQuickInputService, + @INotificationService private readonly notificationService: INotificationService, + @IProgressService private readonly progressService: IProgressService, + @IDialogService private readonly dialogService: IDialogService, + @ILogService private readonly logService: ILogService, + ) { + super(); + this.registerProfileContentHandler(this.fileUserDataProfileContentHandler = instantiationService.createInstance(FileUserDataProfileContentHandler)); + this.isProfileImportExportInProgressContextKey = IS_PROFILE_IMPORT_EXPORT_IN_PROGRESS_CONTEXT.bindTo(contextKeyService); + + this.viewContainer = Registry.as(Extensions.ViewContainersRegistry).registerViewContainer( + { + id: 'userDataProfiles', + title: PROFILES_TTILE, + ctorDescriptor: new SyncDescriptor( + ViewPaneContainer, + ['userDataProfiles', { mergeViewWithContainerWhenSingleView: true }] + ), + icon: defaultUserDataProfileIcon, + hideIfEmpty: true, + }, ViewContainerLocation.Sidebar); + } + + registerProfileContentHandler(profileContentHandler: IUserDataProfileContentHandler): void { + if (this.profileContentHandlers.has(profileContentHandler.id)) { + throw new Error(`Profile content handler with id '${profileContentHandler.id}' already registered.`); + } + this.profileContentHandlers.set(profileContentHandler.id, profileContentHandler); + } + + async exportProfile(): Promise { + if (this.isProfileImportExportInProgressContextKey.get()) { + this.logService.warn('Profile import/export already in progress.'); + return; + } + + this.isProfileImportExportInProgressContextKey.set(true); + const disposables = new DisposableStore(); + + try { + disposables.add(toDisposable(() => this.isProfileImportExportInProgressContextKey.set(false))); + const userDataProfilesData = disposables.add(this.instantiationService.createInstance(UserDataProfileExportData, this.userDataProfileService.currentProfile)); + const exportProfile = await this.showProfilePreviewView(`workbench.views.profiles.export.preview`, localize('export profile preview', "Export"), userDataProfilesData); + if (exportProfile) { + const profileContent = await userDataProfilesData.getContent(); + const resource = await this.saveProfileContent(profileContent); + if (resource) { + this.notificationService.info(localize('export success', "{0}: Exported successfully.", PROFILES_CATEGORY.value)); + } + } + } finally { + disposables.dispose(); + } + } + + async importProfile(uri: URI): Promise { + if (this.isProfileImportExportInProgressContextKey.get()) { + this.logService.warn('Profile import/export already in progress.'); + return; + } + + this.isProfileImportExportInProgressContextKey.set(true); + const disposables = new DisposableStore(); + disposables.add(toDisposable(() => this.isProfileImportExportInProgressContextKey.set(false))); + + try { + const profileContent = await this.resolveProfileContent(uri); + if (profileContent === null) { + return; + } + const profileTemplate: IUserDataProfileTemplate = JSON.parse(profileContent); + if (!isUserDataProfileTemplate(profileTemplate)) { + this.notificationService.error('Invalid profile content.'); + return; + } + const userDataProfilesData = disposables.add(this.instantiationService.createInstance(UserDataProfileImportData, profileTemplate)); + const importProfile = await this.showProfilePreviewView(`workbench.views.profiles.import.preview`, localize('import profile preview', "Import"), userDataProfilesData); + if (!importProfile) { + return; + } + const profile = await this.getProfileToImport(profileTemplate); + if (!profile) { + return; + } + await this.progressService.withProgress({ + location: ProgressLocation.Notification, + title: localize('profiles.importing', "{0}: Importing...", PROFILES_CATEGORY.value), + }, async progress => { + if (profileTemplate.settings) { + await this.instantiationService.createInstance(SettingsResource).apply(profileTemplate.settings, profile); + } + if (profileTemplate.keybindings) { + await this.instantiationService.createInstance(KeybindingsResource).apply(profileTemplate.keybindings, profile); + } + if (profileTemplate.tasks) { + await this.instantiationService.createInstance(TasksResource).apply(profileTemplate.tasks, profile); + } + if (profileTemplate.snippets) { + await this.instantiationService.createInstance(SnippetsResource).apply(profileTemplate.snippets, profile); + } + if (profileTemplate.globalState) { + await this.instantiationService.createInstance(GlobalStateResource).apply(profileTemplate.globalState, profile); + } + if (profileTemplate.extensions) { + await this.instantiationService.createInstance(ExtensionsResource).apply(profileTemplate.extensions, profile); + } + await this.userDataProfileManagementService.switchProfile(profile); + }); + + this.notificationService.info(localize('imported profile', "{0}: Imported successfully.", PROFILES_CATEGORY.value)); + } finally { + disposables.dispose(); + } + } + + private async saveProfileContent(content: string): Promise { + const profileContentHandler = await this.pickProfileContentHandler(); + if (!profileContentHandler) { + return null; + } + const resource = await profileContentHandler.saveProfile(content); + return resource; + } + + private async resolveProfileContent(resource: URI): Promise { + if (await this.fileService.canHandleResource(resource)) { + return this.fileUserDataProfileContentHandler.readProfile(resource); + } + await this.extensionService.activateByEvent(`onProfile:import:${resource.authority}`); + const profileContentHandler = this.profileContentHandlers.get(resource.authority); + return profileContentHandler?.readProfile(resource) ?? null; + } + + private async pickProfileContentHandler(): Promise { + if (this.profileContentHandlers.size === 1) { + return this.profileContentHandlers.values().next().value; + } + await this.extensionService.activateByEvent('onProfile:export'); + return undefined; + } + + private async getProfileToImport(profileTemplate: IUserDataProfileTemplate): Promise { + const profile = this.userDataProfilesService.profiles.find(p => p.name === profileTemplate.name); + if (profile) { + const confirmation = await this.dialogService.confirm({ + type: 'info', + message: localize('profile already exists', "Profile with name '{0}' already exists. Do you want to overwrite it?", profileTemplate.name), + primaryButton: localize('overwrite', "Overwrite"), + secondaryButton: localize('create new', "Create New Profile"), + }); + if (confirmation.confirmed) { + return profile; + } + const name = await this.quickInputService.input({ + placeHolder: localize('name', "Profile name"), + title: localize('create new', "Create New Profile"), + validateInput: async (value: string) => { + if (this.userDataProfilesService.profiles.some(p => p.name === value)) { + return localize('profileExists', "Profile with name {0} already exists.", value); + } + return undefined; + } + }); + if (!name) { + return undefined; + } + return this.userDataProfilesService.createNamedProfile(name); + } else { + return this.userDataProfilesService.createNamedProfile(profileTemplate.name, { shortName: profileTemplate.shortName }); + } + } + + private async showProfilePreviewView(id: string, name: string, userDataProfilesData: UserDataProfileTreeViewData): Promise { + const disposables = new DisposableStore(); + const viewsRegistry = Registry.as(Extensions.ViewsRegistry); + const treeView = disposables.add(this.instantiationService.createInstance(TreeView, id, name)); + treeView.showRefreshAction = true; + let onConfirm: (() => void) | undefined, onCancel: (() => void) | undefined; + const exportPreviewConfirmPomise = new Promise((c, e) => { onConfirm = c; onCancel = e; }); + const descriptor: ITreeViewDescriptor = { + id, + name, + ctorDescriptor: new SyncDescriptor(UserDataProfileExportViewPane, [userDataProfilesData, name, onConfirm, onCancel]), + canToggleVisibility: false, + canMoveView: false, + treeView, + collapsed: false, + }; + + try { + viewsRegistry.registerViews([descriptor], this.viewContainer); + await this.viewsService.openView(id, true); + await exportPreviewConfirmPomise; + return true; + } catch { + return false; + } finally { + viewsRegistry.deregisterViews([descriptor], this.viewContainer); + disposables.dispose(); + this.closeAllImportExportPreviewEditors().then(null, onUnexpectedError); + } + } + + private async closeAllImportExportPreviewEditors(): Promise { + const editorsToColse = this.editorService.getEditors(EditorsOrder.SEQUENTIAL).filter(({ editor }) => editor.resource?.scheme === USER_DATA_PROFILE_IMPORT_EXPORT_PREVIEW_SCHEME); + if (editorsToColse.length) { + await this.editorService.closeEditors(editorsToColse); + } + } + + async setProfile(profile: IUserDataProfileTemplate): Promise { + await this.progressService.withProgress({ + location: ProgressLocation.Notification, + title: localize('profiles.applying', "{0}: Applying...", PROFILES_CATEGORY.value), + }, async progress => { + if (profile.settings) { + await this.instantiationService.createInstance(SettingsResource).apply(profile.settings, this.userDataProfileService.currentProfile); + } + if (profile.globalState) { + await this.instantiationService.createInstance(GlobalStateResource).apply(profile.globalState, this.userDataProfileService.currentProfile); + } + if (profile.extensions) { + await this.instantiationService.createInstance(ExtensionsResource).apply(profile.extensions, this.userDataProfileService.currentProfile); + } + }); + this.notificationService.info(localize('applied profile', "{0}: Applied successfully.", PROFILES_CATEGORY.value)); + } + +} + +class FileUserDataProfileContentHandler implements IUserDataProfileContentHandler { + + readonly id = 'file'; + readonly name = localize('file', "File"); + + constructor( + @IFileDialogService private readonly fileDialogService: IFileDialogService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @IFileService private readonly fileService: IFileService, + @ITextFileService private readonly textFileService: ITextFileService, + ) { } + + async saveProfile(content: string): Promise { + const profileLocation = await this.fileDialogService.showSaveDialog({ + title: localize('export profile dialog', "Save Profile"), + filters: PROFILE_FILTER, + defaultUri: this.uriIdentityService.extUri.joinPath(await this.fileDialogService.defaultFilePath(), `profile.${PROFILE_EXTENSION}`), + }); + if (!profileLocation) { + return null; + } + await this.textFileService.create([{ resource: profileLocation, value: content, options: { overwrite: true } }]); + return profileLocation; + } + + async readProfile(uri: URI): Promise { + return (await this.fileService.readFile(uri)).value.toString(); + } + + async selectProfile(): Promise { + const profileLocation = await this.fileDialogService.showOpenDialog({ + canSelectFolders: false, + canSelectFiles: true, + canSelectMany: false, + filters: PROFILE_FILTER, + title: localize('select profile', "Select Profile"), + }); + return profileLocation ? profileLocation[0] : null; + } + + +} + +class UserDataProfileExportViewPane extends TreeViewPane { + + private buttonsContainer!: HTMLElement; + private confirmButton!: Button; + private cancelButton!: Button; + private dimension: DOM.Dimension | undefined; + private totalTreeItemsCount: number = 0; + + constructor( + private readonly userDataProfileData: UserDataProfileTreeViewData, + private readonly confirmLabel: string, + private readonly onConfirm: () => void, + private readonly onCancel: () => void, + options: IViewletViewOptions, + @IKeybindingService keybindingService: IKeybindingService, + @IContextMenuService contextMenuService: IContextMenuService, + @IConfigurationService configurationService: IConfigurationService, + @IContextKeyService contextKeyService: IContextKeyService, + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + @IInstantiationService instantiationService: IInstantiationService, + @IOpenerService openerService: IOpenerService, + @IThemeService themeService: IThemeService, + @ITelemetryService telemetryService: ITelemetryService, + @INotificationService notificationService: INotificationService, + ) { + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService, notificationService); + } + + + protected override renderTreeView(container: HTMLElement): void { + this.treeView.dataProvider = this.userDataProfileData; + super.renderTreeView(DOM.append(container, DOM.$(''))); + this.createButtons(container); + this._register(this.treeView.onDidChangeCheckboxState(items => { + this.treeView.refresh(this.userDataProfileData.onDidChangeCheckboxState(items)); + this.updateConfirmButtonEnablement(); + })); + this.userDataProfileData.getExpandedItemsCount().then(count => { + this.totalTreeItemsCount = count; + if (this.dimension) { + this.layoutTreeView(this.dimension.height, this.dimension.width); + } + }); + } + + private createButtons(container: HTMLElement): void { + this.buttonsContainer = DOM.append(container, DOM.$('.manual-sync-buttons-container')); + + this.confirmButton = this._register(new Button(this.buttonsContainer, { ...defaultButtonStyles })); + this.confirmButton.label = this.confirmLabel; + this._register(this.confirmButton.onDidClick(() => this.onConfirm())); + + this.cancelButton = this._register(new Button(this.buttonsContainer, { secondary: true, ...defaultButtonStyles })); + this.cancelButton.label = localize('cancel', "Cancel"); + this._register(this.cancelButton.onDidClick(() => this.onCancel())); + } + + + protected override layoutTreeView(height: number, width: number): void { + this.dimension = new DOM.Dimension(width, height); + const buttonContainerHeight = 78; + this.buttonsContainer.style.height = `${buttonContainerHeight}px`; + this.buttonsContainer.style.width = `${width}px`; + + super.layoutTreeView(Math.min(height - buttonContainerHeight, 22 * (this.totalTreeItemsCount || 12)), width); + } + + private updateConfirmButtonEnablement(): void { + this.confirmButton.enabled = this.userDataProfileData.isEnabled(); + } + +} + +const USER_DATA_PROFILE_IMPORT_EXPORT_PREVIEW_SCHEME = 'userdataprofileexportpreview'; + +abstract class UserDataProfileTreeViewData extends Disposable implements ITreeViewDataProvider { + + async getExpandedItemsCount(): Promise { + const roots = await this.getRoots(); + const children = await Promise.all(roots.map(async root => { + if (root.collapsibleState === TreeItemCollapsibleState.Expanded) { + const children = await root.getChildren(); + return children ?? []; + } + return []; + })); + return roots.length + children.flat().length; + } + + private rootsPromise: Promise | undefined; + async getChildren(element?: ITreeItem): Promise { + if (element) { + return (element).getChildren(); + } else { + this.rootsPromise = undefined; + return this.getRoots(); + } + } + + private getRoots(): Promise { + if (!this.rootsPromise) { + this.rootsPromise = this.fetchRoots(); + } + return this.rootsPromise; + } + + abstract isEnabled(): boolean; + abstract onDidChangeCheckboxState(items: ITreeItem[]): ITreeItem[]; + protected abstract fetchRoots(): Promise; +} + +class UserDataProfileExportData extends UserDataProfileTreeViewData implements ITreeViewDataProvider { + + private settingsResourceTreeItem: SettingsResourceTreeItem | undefined; + private keybindingsResourceTreeItem: KeybindingsResourceTreeItem | undefined; + private tasksResourceTreeItem: TasksResourceTreeItem | undefined; + private snippetsResourceTreeItem: SnippetsResourceTreeItem | undefined; + private extensionsResourceTreeItem: ExtensionsResourceExportTreeItem | undefined; + private globalStateResourceTreeItem: GlobalStateResourceExportTreeItem | undefined; + + private readonly disposables = this._register(new DisposableStore()); + + constructor( + private readonly profile: IUserDataProfile, + @IFileService private readonly fileService: IFileService, + @IInstantiationService private readonly instantiationService: IInstantiationService + ) { + super(); + } + + onDidChangeCheckboxState(items: ITreeItem[]): ITreeItem[] { + const toRefresh: ITreeItem[] = []; + for (const item of items) { + if (item.children) { + for (const child of item.children) { + if (child.checkbox) { + child.checkbox.isChecked = !!item.checkbox?.isChecked; + } + } + toRefresh.push(item); + } else { + const parent = (item).parent; + if (item.checkbox?.isChecked && parent?.checkbox) { + parent.checkbox.isChecked = true; + toRefresh.push(parent); + } + } + } + return items; + } + + protected async fetchRoots(): Promise { + this.disposables.clear(); + this.disposables.add(this.fileService.registerProvider(USER_DATA_PROFILE_IMPORT_EXPORT_PREVIEW_SCHEME, this._register(new InMemoryFileSystemProvider()))); + const roots: IProfileResourceTreeItem[] = []; + const exportPreviewProfle = this.createExportPreviewProfile(this.profile); + + const settingsResource = this.instantiationService.createInstance(SettingsResource); + const settingsContent = await settingsResource.getContent(this.profile); + await settingsResource.apply(settingsContent, exportPreviewProfle); + this.settingsResourceTreeItem = this.instantiationService.createInstance(SettingsResourceTreeItem, exportPreviewProfle); + if (await this.settingsResourceTreeItem.hasContent()) { + roots.push(this.settingsResourceTreeItem); + } + + const keybindingsResource = this.instantiationService.createInstance(KeybindingsResource); + const keybindingsContent = await keybindingsResource.getContent(this.profile); + await keybindingsResource.apply(keybindingsContent, exportPreviewProfle); + this.keybindingsResourceTreeItem = this.instantiationService.createInstance(KeybindingsResourceTreeItem, exportPreviewProfle); + if (await this.keybindingsResourceTreeItem.hasContent()) { + roots.push(this.keybindingsResourceTreeItem); + } + + const tasksResource = this.instantiationService.createInstance(TasksResource); + const tasksContent = await tasksResource.getContent(this.profile); + await tasksResource.apply(tasksContent, exportPreviewProfle); + this.tasksResourceTreeItem = this.instantiationService.createInstance(TasksResourceTreeItem, exportPreviewProfle); + if (await this.tasksResourceTreeItem.hasContent()) { + roots.push(this.tasksResourceTreeItem); + } + + const snippetsResource = this.instantiationService.createInstance(SnippetsResource); + const snippetsContent = await snippetsResource.getContent(this.profile); + await snippetsResource.apply(snippetsContent, exportPreviewProfle); + this.snippetsResourceTreeItem = this.instantiationService.createInstance(SnippetsResourceTreeItem, exportPreviewProfle); + if (await this.snippetsResourceTreeItem.hasContent()) { + roots.push(this.snippetsResourceTreeItem); + } + + this.globalStateResourceTreeItem = this.instantiationService.createInstance(GlobalStateResourceExportTreeItem, exportPreviewProfle); + if (await this.globalStateResourceTreeItem.hasContent()) { + roots.push(this.globalStateResourceTreeItem); + } + + this.extensionsResourceTreeItem = this.instantiationService.createInstance(ExtensionsResourceExportTreeItem, exportPreviewProfle); + if (await this.extensionsResourceTreeItem.hasContent()) { + roots.push(this.extensionsResourceTreeItem); + } + + return roots; + } + + private createExportPreviewProfile(profile: IUserDataProfile): IUserDataProfile { + return { + id: profile.id, + name: profile.name, + location: profile.location, + isDefault: profile.isDefault, + shortName: profile.shortName, + globalStorageHome: profile.globalStorageHome, + settingsResource: profile.settingsResource.with({ scheme: USER_DATA_PROFILE_IMPORT_EXPORT_PREVIEW_SCHEME }), + keybindingsResource: profile.keybindingsResource.with({ scheme: USER_DATA_PROFILE_IMPORT_EXPORT_PREVIEW_SCHEME }), + tasksResource: profile.tasksResource.with({ scheme: USER_DATA_PROFILE_IMPORT_EXPORT_PREVIEW_SCHEME }), + snippetsHome: profile.snippetsHome.with({ scheme: USER_DATA_PROFILE_IMPORT_EXPORT_PREVIEW_SCHEME }), + extensionsResource: profile.extensionsResource, + useDefaultFlags: profile.useDefaultFlags, + isTransient: profile.isTransient + }; + } + + async getContent(): Promise { + const settings = this.settingsResourceTreeItem?.checkbox?.isChecked ? await this.settingsResourceTreeItem.getContent() : undefined; + const keybindings = this.keybindingsResourceTreeItem?.checkbox?.isChecked ? await this.keybindingsResourceTreeItem.getContent() : undefined; + const tasks = this.tasksResourceTreeItem?.checkbox?.isChecked ? await this.tasksResourceTreeItem.getContent() : undefined; + const snippets = this.snippetsResourceTreeItem?.checkbox?.isChecked ? await this.snippetsResourceTreeItem.getContent() : undefined; + const extensions = this.extensionsResourceTreeItem?.checkbox?.isChecked ? await this.extensionsResourceTreeItem.getContent() : undefined; + const globalState = this.globalStateResourceTreeItem?.checkbox?.isChecked ? await this.globalStateResourceTreeItem.getContent() : undefined; + const profile: IUserDataProfileTemplate = { + name: this.profile.name, + shortName: this.profile.shortName, + settings, + keybindings, + tasks, + snippets, + extensions, + globalState + }; + return JSON.stringify(profile); + } + + isEnabled(): boolean { + return !!this.settingsResourceTreeItem?.checkbox?.isChecked + || !!this.keybindingsResourceTreeItem?.checkbox?.isChecked + || !!this.tasksResourceTreeItem?.checkbox?.isChecked + || !!this.snippetsResourceTreeItem?.checkbox?.isChecked + || !!this.extensionsResourceTreeItem?.checkbox?.isChecked + || !!this.globalStateResourceTreeItem?.checkbox?.isChecked; + } + +} + +class UserDataProfileImportData extends UserDataProfileTreeViewData implements ITreeViewDataProvider { + + private settingsResourceTreeItem: SettingsResourceTreeItem | undefined; + private keybindingsResourceTreeItem: KeybindingsResourceTreeItem | undefined; + private tasksResourceTreeItem: TasksResourceTreeItem | undefined; + private snippetsResourceTreeItem: SnippetsResourceTreeItem | undefined; + private extensionsResourceTreeItem: ExtensionsResourceImportTreeItem | undefined; + private globalStateResourceTreeItem: GlobalStateResourceImportTreeItem | undefined; + + private readonly disposables = this._register(new DisposableStore()); + + constructor( + private readonly profile: IUserDataProfileTemplate, + @IFileService private readonly fileService: IFileService, + @IInstantiationService private readonly instantiationService: IInstantiationService + ) { + super(); + } + + onDidChangeCheckboxState(items: ITreeItem[]): ITreeItem[] { + return items; + } + + protected async fetchRoots(): Promise { + this.disposables.clear(); + + const inMemoryProvider = this._register(new InMemoryFileSystemProvider()); + this.disposables.add(this.fileService.registerProvider(USER_DATA_PROFILE_IMPORT_EXPORT_PREVIEW_SCHEME, inMemoryProvider)); + const roots: IProfileResourceTreeItem[] = []; + const importPreviewProfle = toUserDataProfile(generateUuid(), this.profile.name, URI.file('/root').with({ scheme: USER_DATA_PROFILE_IMPORT_EXPORT_PREVIEW_SCHEME })); + + this.settingsResourceTreeItem = undefined; + if (this.profile.settings) { + const settingsResource = this.instantiationService.createInstance(SettingsResource); + await settingsResource.apply(this.profile.settings, importPreviewProfle); + this.settingsResourceTreeItem = this.instantiationService.createInstance(SettingsResourceTreeItem, importPreviewProfle); + this.settingsResourceTreeItem.checkbox = undefined; + roots.push(this.settingsResourceTreeItem); + } + + this.keybindingsResourceTreeItem = undefined; + if (this.profile.keybindings) { + const keybindingsResource = this.instantiationService.createInstance(KeybindingsResource); + await keybindingsResource.apply(this.profile.keybindings, importPreviewProfle); + this.keybindingsResourceTreeItem = this.instantiationService.createInstance(KeybindingsResourceTreeItem, importPreviewProfle); + this.keybindingsResourceTreeItem.checkbox = undefined; + roots.push(this.keybindingsResourceTreeItem); + } + + this.tasksResourceTreeItem = undefined; + if (this.profile.tasks) { + const tasksResource = this.instantiationService.createInstance(TasksResource); + await tasksResource.apply(this.profile.tasks, importPreviewProfle); + this.tasksResourceTreeItem = this.instantiationService.createInstance(TasksResourceTreeItem, importPreviewProfle); + this.tasksResourceTreeItem.checkbox = undefined; + roots.push(this.tasksResourceTreeItem); + } + + this.snippetsResourceTreeItem = undefined; + if (this.profile.snippets) { + const snippetsResource = this.instantiationService.createInstance(SnippetsResource); + await snippetsResource.apply(this.profile.snippets, importPreviewProfle); + this.snippetsResourceTreeItem = this.instantiationService.createInstance(SnippetsResourceTreeItem, importPreviewProfle); + this.snippetsResourceTreeItem.checkbox = undefined; + roots.push(this.snippetsResourceTreeItem); + } + + this.globalStateResourceTreeItem = undefined; + if (this.profile.globalState) { + const globalStateResource = joinPath(importPreviewProfle.globalStorageHome, 'globalState.json'); + await this.fileService.writeFile(globalStateResource, VSBuffer.fromString(JSON.stringify(JSON.parse(this.profile.globalState), null, '\t'))); + this.globalStateResourceTreeItem = this.instantiationService.createInstance(GlobalStateResourceImportTreeItem, globalStateResource); + roots.push(this.globalStateResourceTreeItem); + } + + this.extensionsResourceTreeItem = undefined; + if (this.profile.extensions) { + this.extensionsResourceTreeItem = this.instantiationService.createInstance(ExtensionsResourceImportTreeItem, this.profile.extensions); + roots.push(this.extensionsResourceTreeItem); + } + + inMemoryProvider.setReadOnly(true); + + return roots; + } + + isEnabled(): boolean { + return true; + } + +} + +registerSingleton(IUserDataProfileImportExportService, UserDataProfileImportExportService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/userDataProfile/common/extensionsResource.ts b/src/vs/workbench/services/userDataProfile/common/extensionsResource.ts deleted file mode 100644 index 101a537d12e..00000000000 --- a/src/vs/workbench/services/userDataProfile/common/extensionsResource.ts +++ /dev/null @@ -1,102 +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 { CancellationToken } from 'vs/base/common/cancellation'; -import { IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { ExtensionType } from 'vs/platform/extensions/common/extensions'; -import { ILogService } from 'vs/platform/log/common/log'; -import { EnablementState, IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { IProfileResource } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; - -interface IProfileExtension { - identifier: IExtensionIdentifier; - preRelease?: boolean; - disabled?: boolean; -} - -export class ExtensionsResource implements IProfileResource { - - constructor( - @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, - @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, - @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, - @ILogService private readonly logService: ILogService, - ) { - } - - async getContent(): Promise { - const extensions = await this.getLocalExtensions(); - return JSON.stringify(extensions); - } - - async apply(content: string): Promise { - const profileExtensions: IProfileExtension[] = JSON.parse(content); - const installedExtensions = await this.extensionManagementService.getInstalled(); - const extensionsToEnableOrDisable: { extension: ILocalExtension; enablementState: EnablementState }[] = []; - const extensionsToInstall: IProfileExtension[] = []; - for (const e of profileExtensions) { - const installedExtension = installedExtensions.find(installed => areSameExtensions(installed.identifier, e.identifier)); - if (!installedExtension || installedExtension.preRelease !== e.preRelease) { - extensionsToInstall.push(e); - } - if (installedExtension && this.extensionEnablementService.isEnabled(installedExtension) !== !e.disabled) { - extensionsToEnableOrDisable.push({ extension: installedExtension, enablementState: e.disabled ? EnablementState.DisabledGlobally : EnablementState.EnabledGlobally }); - } - } - const extensionsToUninstall: ILocalExtension[] = installedExtensions.filter(extension => extension.type === ExtensionType.User && !profileExtensions.some(({ identifier }) => areSameExtensions(identifier, extension.identifier))); - for (const { extension, enablementState } of extensionsToEnableOrDisable) { - this.logService.trace(`Profile: Updating extension enablement...`, extension.identifier.id); - await this.extensionEnablementService.setEnablement([extension], enablementState); - this.logService.info(`Profile: Updated extension enablement`, extension.identifier.id); - } - if (extensionsToInstall.length) { - const galleryExtensions = await this.extensionGalleryService.getExtensions(extensionsToInstall.map(e => ({ ...e.identifier, hasPreRelease: e.preRelease })), CancellationToken.None); - await Promise.all(extensionsToInstall.map(async e => { - const extension = galleryExtensions.find(galleryExtension => areSameExtensions(galleryExtension.identifier, e.identifier)); - if (!extension) { - return; - } - if (await this.extensionManagementService.canInstall(extension)) { - this.logService.trace(`Profile: Installing extension...`, e.identifier.id, extension.version); - await this.extensionManagementService.installFromGallery(extension, { isMachineScoped: false, donotIncludePackAndDependencies: true, installPreReleaseVersion: e.preRelease } /* set isMachineScoped value to prevent install and sync dialog in web */); - this.logService.info(`Profile: Installed extension.`, e.identifier.id, extension.version); - } else { - this.logService.info(`Profile: Skipped installing extension because it cannot be installed.`, extension.displayName || extension.identifier.id); - } - })); - } - if (extensionsToUninstall.length) { - await Promise.all(extensionsToUninstall.map(e => this.extensionManagementService.uninstall(e))); - } - } - - private async getLocalExtensions(): Promise { - const result: IProfileExtension[] = []; - const installedExtensions = await this.extensionManagementService.getInstalled(undefined); - for (const extension of installedExtensions) { - const { identifier, preRelease } = extension; - const enablementState = this.extensionEnablementService.getEnablementState(extension); - const disabled = !this.extensionEnablementService.isEnabledEnablementState(enablementState); - if (!disabled && extension.type === ExtensionType.System) { - // skip enabled system extensions - continue; - } - if (disabled && enablementState !== EnablementState.DisabledGlobally && enablementState !== EnablementState.DisabledWorkspace) { - //skip extensions that are not disabled by user - continue; - } - const profileExtension: IProfileExtension = { identifier }; - if (disabled) { - profileExtension.disabled = true; - } - if (preRelease) { - profileExtension.preRelease = true; - } - result.push(profileExtension); - } - return result; - } -} diff --git a/src/vs/workbench/services/userDataProfile/common/globalStateResource.ts b/src/vs/workbench/services/userDataProfile/common/globalStateResource.ts deleted file mode 100644 index d9f439a9ad6..00000000000 --- a/src/vs/workbench/services/userDataProfile/common/globalStateResource.ts +++ /dev/null @@ -1,64 +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 { IStringDictionary } from 'vs/base/common/collections'; -import { ILogService } from 'vs/platform/log/common/log'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { IProfileResource } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; -import { Extensions, IProfileStorageRegistry } from 'vs/workbench/services/userDataProfile/common/userDataProfileStorageRegistry'; - -interface IGlobalState { - storage: IStringDictionary; -} - -export class GlobalStateResource implements IProfileResource { - - constructor( - @IStorageService private readonly storageService: IStorageService, - @ILogService private readonly logService: ILogService, - ) { - } - - async getContent(): Promise { - const globalState = await this.getLocalGlobalState(); - return JSON.stringify(globalState); - } - - async apply(content: string): Promise { - const globalState: IGlobalState = JSON.parse(content); - await this.writeLocalGlobalState(globalState); - } - - private async getLocalGlobalState(): Promise { - const storage: IStringDictionary = {}; - for (const { key } of Registry.as(Extensions.ProfileStorageRegistry).all) { - const value = this.storageService.get(key, StorageScope.PROFILE); - if (value) { - storage[key] = value; - } - } - return { storage }; - } - - private async writeLocalGlobalState(globalState: IGlobalState): Promise { - const profileKeys: string[] = Object.keys(globalState.storage); - const updatedStorage: IStringDictionary = globalState.storage; - for (const { key } of Registry.as(Extensions.ProfileStorageRegistry).all) { - if (!profileKeys.includes(key)) { - // Remove the key if it does not exist in the profile - updatedStorage[key] = undefined; - } - } - const updatedStorageKeys: string[] = Object.keys(updatedStorage); - if (updatedStorageKeys.length) { - this.logService.trace(`Profile: Updating global state...`); - for (const key of updatedStorageKeys) { - this.storageService.store(key, globalState.storage[key], StorageScope.PROFILE, StorageTarget.USER); - } - this.logService.info(`Profile: Updated global state`, updatedStorageKeys); - } - } -} diff --git a/src/vs/workbench/services/userDataProfile/common/settingsResource.ts b/src/vs/workbench/services/userDataProfile/common/settingsResource.ts deleted file mode 100644 index 88b1b25733e..00000000000 --- a/src/vs/workbench/services/userDataProfile/common/settingsResource.ts +++ /dev/null @@ -1,66 +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 { VSBuffer } from 'vs/base/common/buffer'; -import { ConfigurationScope, Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; -import { IFileService } from 'vs/platform/files/common/files'; -import { ILogService } from 'vs/platform/log/common/log'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { IUserDataProfileService, IProfileResource, ProfileCreationOptions } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; -import { removeComments, updateIgnoredSettings } from 'vs/platform/userDataSync/common/settingsMerge'; -import { IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync'; - -interface ISettingsContent { - settings: string; -} - -export class SettingsResource implements IProfileResource { - - constructor( - @IFileService private readonly fileService: IFileService, - @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, - @IUserDataSyncUtilService private readonly userDataSyncUtilService: IUserDataSyncUtilService, - @ILogService private readonly logService: ILogService, - ) { - } - - async getContent(options?: ProfileCreationOptions): Promise { - const ignoredSettings = this.getIgnoredSettings(); - const formattingOptions = await this.userDataSyncUtilService.resolveFormattingOptions(this.userDataProfileService.currentProfile.settingsResource); - const localContent = await this.getLocalFileContent(); - let settings = updateIgnoredSettings(localContent || '{}', '{}', ignoredSettings, formattingOptions); - if (options?.skipComments) { - settings = removeComments(settings, formattingOptions); - } - const settingsContent: ISettingsContent = { settings }; - return JSON.stringify(settingsContent); - } - - async apply(content: string): Promise { - const settingsContent: ISettingsContent = JSON.parse(content); - this.logService.trace(`Profile: Applying settings...`); - const localSettingsContent = await this.getLocalFileContent(); - const formattingOptions = await this.userDataSyncUtilService.resolveFormattingOptions(this.userDataProfileService.currentProfile.settingsResource); - const contentToUpdate = updateIgnoredSettings(settingsContent.settings, localSettingsContent || '{}', this.getIgnoredSettings(), formattingOptions); - await this.fileService.writeFile(this.userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString(contentToUpdate)); - this.logService.info(`Profile: Applied settings`); - } - - private getIgnoredSettings(): string[] { - const allSettings = Registry.as(Extensions.Configuration).getConfigurationProperties(); - const ignoredSettings = Object.keys(allSettings).filter(key => allSettings[key]?.scope === ConfigurationScope.MACHINE || allSettings[key]?.scope === ConfigurationScope.MACHINE_OVERRIDABLE); - return ignoredSettings; - } - - private async getLocalFileContent(): Promise { - try { - const content = await this.fileService.readFile(this.userDataProfileService.currentProfile.settingsResource); - return content.value.toString(); - } catch (error) { - return null; - } - } - -} diff --git a/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts b/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts index 0109ce3c737..63b9032ad85 100644 --- a/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts +++ b/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts @@ -10,6 +10,10 @@ import { MenuId } from 'vs/platform/actions/common/actions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IUserDataProfile, IUserDataProfileOptions, IUserDataProfileUpdateOptions } from 'vs/platform/userDataProfile/common/userDataProfile'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { URI } from 'vs/base/common/uri'; +import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { Codicon } from 'vs/base/common/codicons'; +import { ITreeItem } from 'vs/workbench/common/views'; export interface DidChangeUserDataProfileEvent { readonly preserveData: boolean; @@ -42,6 +46,9 @@ export interface IUserDataProfileManagementService { export interface IUserDataProfileTemplate { readonly settings?: string; + readonly keybindings?: string; + readonly tasks?: string; + readonly snippets?: string; readonly globalState?: string; readonly extensions?: string; } @@ -61,16 +68,36 @@ export const IUserDataProfileImportExportService = createDecorator; - importProfile(profile: IUserDataProfileTemplate): Promise; + registerProfileContentHandler(profileContentHandler: IUserDataProfileContentHandler): void; + + exportProfile(): Promise; + importProfile(uri: URI): Promise; setProfile(profile: IUserDataProfileTemplate): Promise; } export interface IProfileResource { - getContent(): Promise; - apply(content: string): Promise; + getContent(profile: IUserDataProfile): Promise; + apply(content: string, profile: IUserDataProfile): Promise; } +export interface IProfileResourceTreeItem extends ITreeItem { + getChildren(): Promise; +} + +export interface IProfileResourceChildTreeItem extends ITreeItem { + parent: IProfileResourceTreeItem; +} + +export interface IUserDataProfileContentHandler { + readonly id: string; + readonly name: string; + readonly description?: string; + saveProfile(content: string): Promise; + readProfile(uri: URI): Promise; +} + +export const defaultUserDataProfileIcon = registerIcon('defaultProfile-icon', Codicon.settings, localize('defaultProfileIcon', 'Icon for Default Profile.')); + export const ManageProfilesSubMenu = new MenuId('Profiles'); export const MANAGE_PROFILES_ACTION_ID = 'workbench.profiles.actions.manage'; export const PROFILES_TTILE = { value: localize('profiles', "Profiles"), original: 'Profiles' }; @@ -81,3 +108,4 @@ export const PROFILES_ENABLEMENT_CONTEXT = new RawContextKey('profiles. export const CURRENT_PROFILE_CONTEXT = new RawContextKey('currentProfile', ''); export const IS_CURRENT_PROFILE_TRANSIENT_CONTEXT = new RawContextKey('isCurrentProfileTransient', false); export const HAS_PROFILES_CONTEXT = new RawContextKey('hasProfiles', false); +export const IS_PROFILE_IMPORT_EXPORT_IN_PROGRESS_CONTEXT = new RawContextKey('isProfileImportExportInProgress', false); diff --git a/src/vs/workbench/services/userDataProfile/common/userDataProfileImportExportService.ts b/src/vs/workbench/services/userDataProfile/common/userDataProfileImportExportService.ts deleted file mode 100644 index bbeb2e00a12..00000000000 --- a/src/vs/workbench/services/userDataProfile/common/userDataProfileImportExportService.ts +++ /dev/null @@ -1,96 +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 { localize } from 'vs/nls'; -import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; -import { ExtensionsResource } from 'vs/workbench/services/userDataProfile/common/extensionsResource'; -import { GlobalStateResource } from 'vs/workbench/services/userDataProfile/common/globalStateResource'; -import { IUserDataProfileTemplate, IUserDataProfileImportExportService, PROFILES_CATEGORY, IUserDataProfileManagementService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; -import { SettingsResource } from 'vs/workbench/services/userDataProfile/common/settingsResource'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; - -export class UserDataProfileImportExportService implements IUserDataProfileImportExportService { - - readonly _serviceBrand: undefined; - - private readonly settingsResourceProfile: SettingsResource; - private readonly globalStateProfile: GlobalStateResource; - private readonly extensionsProfile: ExtensionsResource; - - constructor( - @IInstantiationService instantiationService: IInstantiationService, - @IProgressService private readonly progressService: IProgressService, - @INotificationService private readonly notificationService: INotificationService, - @IUserDataProfileManagementService private readonly userDataProfileManagementService: IUserDataProfileManagementService, - @IQuickInputService private readonly quickInputService: IQuickInputService, - ) { - this.settingsResourceProfile = instantiationService.createInstance(SettingsResource); - this.globalStateProfile = instantiationService.createInstance(GlobalStateResource); - this.extensionsProfile = instantiationService.createInstance(ExtensionsResource); - } - - async exportProfile(options?: { skipComments: boolean }): Promise { - const settings = await this.settingsResourceProfile.getContent(options); - const globalState = await this.globalStateProfile.getContent(); - const extensions = await this.extensionsProfile.getContent(); - return { - settings, - globalState, - extensions - }; - } - - async importProfile(profileTemplate: IUserDataProfileTemplate): Promise { - const name = await this.quickInputService.input({ - placeHolder: localize('name', "Profile name"), - title: localize('save profile as', "Create from Current Profile..."), - }); - if (!name) { - return undefined; - } - - await this.progressService.withProgress({ - location: ProgressLocation.Notification, - title: localize('profiles.importing', "{0}: Importing...", PROFILES_CATEGORY.value), - }, async progress => { - await this.userDataProfileManagementService.createAndEnterProfile(name); - if (profileTemplate.settings) { - await this.settingsResourceProfile.apply(profileTemplate.settings); - } - if (profileTemplate.globalState) { - await this.globalStateProfile.apply(profileTemplate.globalState); - } - if (profileTemplate.extensions) { - await this.extensionsProfile.apply(profileTemplate.extensions); - } - }); - - this.notificationService.info(localize('imported profile', "{0}: Imported successfully.", PROFILES_CATEGORY.value)); - } - - async setProfile(profile: IUserDataProfileTemplate): Promise { - await this.progressService.withProgress({ - location: ProgressLocation.Notification, - title: localize('profiles.applying', "{0}: Applying...", PROFILES_CATEGORY.value), - }, async progress => { - if (profile.settings) { - await this.settingsResourceProfile.apply(profile.settings); - } - if (profile.globalState) { - await this.globalStateProfile.apply(profile.globalState); - } - if (profile.extensions) { - await this.extensionsProfile.apply(profile.extensions); - } - }); - this.notificationService.info(localize('applied profile', "{0}: Applied successfully.", PROFILES_CATEGORY.value)); - } - -} - -registerSingleton(IUserDataProfileImportExportService, UserDataProfileImportExportService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/userDataProfile/common/userDataProfileService.ts b/src/vs/workbench/services/userDataProfile/common/userDataProfileService.ts index 37727ac08a3..9966a1cda99 100644 --- a/src/vs/workbench/services/userDataProfile/common/userDataProfileService.ts +++ b/src/vs/workbench/services/userDataProfile/common/userDataProfileService.ts @@ -4,15 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { Promises } from 'vs/base/common/async'; -import { Codicon } from 'vs/base/common/codicons'; import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import { localize } from 'vs/nls'; -import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; -import { DidChangeUserDataProfileEvent, IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; - -const defaultUserDataProfileIcon = registerIcon('defaultProfile-icon', Codicon.settings, localize('defaultProfileIcon', 'Icon for Default Profile.')); +import { defaultUserDataProfileIcon, DidChangeUserDataProfileEvent, IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; export class UserDataProfileService extends Disposable implements IUserDataProfileService { diff --git a/src/vs/workbench/services/userDataProfile/common/userDataProfileStorageRegistry.ts b/src/vs/workbench/services/userDataProfile/common/userDataProfileStorageRegistry.ts deleted file mode 100644 index 807b20e259b..00000000000 --- a/src/vs/workbench/services/userDataProfile/common/userDataProfileStorageRegistry.ts +++ /dev/null @@ -1,62 +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 { Event, Emitter } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { Registry } from 'vs/platform/registry/common/platform'; - -export namespace Extensions { - export const ProfileStorageRegistry = 'workbench.registry.profile.storage'; -} - -export interface IProfileStorageKey { - readonly key: string; - readonly description?: string; -} - -/** - * A registry for storage keys used for profiles - */ -export interface IProfileStorageRegistry { - /** - * An event that is triggered when storage keys are registered. - */ - readonly onDidRegister: Event; - - /** - * All registered storage keys - */ - readonly all: IProfileStorageKey[]; - - /** - * Register profile storage keys - * - * @param keys keys to register - */ - registerKeys(keys: IProfileStorageKey[]): void; -} - -class ProfileStorageRegistryImpl extends Disposable implements IProfileStorageRegistry { - - private readonly _onDidRegister = this._register(new Emitter()); - readonly onDidRegister = this._onDidRegister.event; - - private readonly storageKeys = new Map(); - - get all(): IProfileStorageKey[] { - return [...this.storageKeys.values()].flat(); - } - - registerKeys(keys: IProfileStorageKey[]): void { - for (const key of keys) { - this.storageKeys.set(key.key, key); - } - this._onDidRegister.fire(keys); - } - -} - -Registry.add(Extensions.ProfileStorageRegistry, new ProfileStorageRegistryImpl()); - diff --git a/src/vs/workbench/services/views/browser/viewDescriptorService.ts b/src/vs/workbench/services/views/browser/viewDescriptorService.ts index 12750975fc0..19b35dc95f2 100644 --- a/src/vs/workbench/services/views/browser/viewDescriptorService.ts +++ b/src/vs/workbench/services/views/browser/viewDescriptorService.ts @@ -19,7 +19,6 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati import { getViewsStateStorageId, ViewContainerModel } from 'vs/workbench/services/views/common/viewContainerModel'; import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions'; import { localize } from 'vs/nls'; -import { Extensions, IProfileStorageRegistry } from 'vs/workbench/services/userDataProfile/common/userDataProfileStorageRegistry'; import { IStringDictionary } from 'vs/base/common/collections'; interface IViewsCustomizations { @@ -114,11 +113,6 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor this._register(this.extensionService.onDidRegisterExtensions(() => this.onDidRegisterExtensions())); - Registry.as(Extensions.ProfileStorageRegistry) - .registerKeys([{ - key: ViewDescriptorService.VIEWS_CUSTOMIZATIONS, - description: localize('views customizations', "Views Customizations"), - }]); } private migrateToViewsCustomizationsStorage(): void { diff --git a/src/vs/workbench/services/views/common/viewContainerModel.ts b/src/vs/workbench/services/views/common/viewContainerModel.ts index 82c767f520f..6b8593224a1 100644 --- a/src/vs/workbench/services/views/common/viewContainerModel.ts +++ b/src/vs/workbench/services/views/common/viewContainerModel.ts @@ -16,7 +16,6 @@ import { isUndefined, isUndefinedOrNull } from 'vs/base/common/types'; import { isEqual, joinPath } from 'vs/base/common/resources'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IStringDictionary } from 'vs/base/common/collections'; -import { Extensions, IProfileStorageRegistry } from 'vs/workbench/services/userDataProfile/common/userDataProfileStorageRegistry'; import { localize } from 'vs/nls'; import { ILogger, ILoggerService } from 'vs/platform/log/common/log'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -128,11 +127,6 @@ class ViewDescriptorsState extends Disposable { this.state = this.initialize(); - Registry.as(Extensions.ProfileStorageRegistry) - .registerKeys([{ - key: this.globalViewsStateStorageId, - description: localize('globalViewsStateStorageId', "Views visibility customizations in {0} view container", viewContainerName), - }]); } set(id: string, state: IViewDescriptorState): void { diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 45eeada71ea..b36112b6b94 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -1351,10 +1351,11 @@ export class RemoteFileSystemProvider implements IFileSystemProvider { } export class TestInMemoryFileSystemProvider extends InMemoryFileSystemProvider implements IFileSystemProviderWithFileReadStreamCapability { - override readonly capabilities: FileSystemProviderCapabilities = - FileSystemProviderCapabilities.FileReadWrite - | FileSystemProviderCapabilities.PathCaseSensitive - | FileSystemProviderCapabilities.FileReadStream; + override get capabilities(): FileSystemProviderCapabilities { + return FileSystemProviderCapabilities.FileReadWrite + | FileSystemProviderCapabilities.PathCaseSensitive + | FileSystemProviderCapabilities.FileReadStream; + } readFileStream(resource: URI): ReadableStreamEvents { const BUFFER_SIZE = 64 * 1024; diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 1d03361f7cb..0298a6c9ee6 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -84,7 +84,7 @@ import 'vs/workbench/services/extensionRecommendations/common/extensionIgnoredRe import 'vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig'; import 'vs/workbench/services/notification/common/notificationService'; import 'vs/workbench/services/userDataSync/common/userDataSyncUtil'; -import 'vs/workbench/services/userDataProfile/common/userDataProfileImportExportService'; +import 'vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService'; import 'vs/workbench/services/userDataProfile/browser/userDataProfileManagement'; import 'vs/workbench/services/remote/common/remoteExplorerService'; import 'vs/workbench/services/workingCopy/common/workingCopyService'; diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index 3876bc2ec98..d097639f885 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -85,6 +85,7 @@ import 'vs/workbench/services/search/electron-sandbox/searchService'; import 'vs/workbench/services/workingCopy/electron-sandbox/workingCopyHistoryService'; import 'vs/workbench/services/userDataSync/browser/userDataSyncEnablementService'; import 'vs/workbench/services/extensions/electron-sandbox/sandboxExtensionService'; +import 'vs/platform/userDataProfile/electron-sandbox/userDataProfileStorageService'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IUserDataInitializationService, UserDataInitializationService } from 'vs/workbench/services/userData/browser/userDataInit';