Share profiles (#166898)

* Share profiles #159891
- Share profile in GitHub
- Profile resource quick pick
- Import profile from vscode link

* remove duplicate code
This commit is contained in:
Sandeep Somavarapu 2022-11-21 21:03:02 +01:00 committed by GitHub
parent ebb77a7dfd
commit e43bf31ab1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 659 additions and 200 deletions

View file

@ -13,7 +13,9 @@
"Other"
],
"activationEvents": [
"*"
"*",
"onProfile",
"onProfile:github"
],
"extensionDependencies": [
"vscode.git-base"
@ -27,7 +29,8 @@
},
"enabledApiProposals": [
"contribShareMenu",
"contribEditSessions"
"contribEditSessions",
"profileContentHandlers"
],
"contributes": {
"commands": [

View file

@ -12,6 +12,7 @@ import { DisposableStore, repositoryHasGitHubRemote } from './util';
import { GithubPushErrorHandler } from './pushErrorHandler';
import { GitBaseExtension } from './typings/git-base';
import { GithubRemoteSourcePublisher } from './remoteSourcePublisher';
import './importExportProfiles';
export function activate(context: ExtensionContext): void {
context.subscriptions.push(initializeGitBaseExtension());

View file

@ -0,0 +1,80 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Octokit } from '@octokit/rest';
import * as vscode from 'vscode';
import { httpsOverHttp } from 'tunnel';
import { Agent, globalAgent } from 'https';
import { basename } from 'path';
import { URL } from 'url';
class GitHubGistProfileContentHandler implements vscode.ProfileContentHandler {
readonly name = vscode.l10n.t('GitHub');
private _octokit: Promise<Octokit> | undefined;
private getOctokit(): Promise<Octokit> {
if (!this._octokit) {
this._octokit = (async () => {
const session = await vscode.authentication.getSession('github', ['gist', 'user:email'], { createIfNone: true });
const token = session.accessToken;
const agent = this.getAgent();
const { Octokit } = await import('@octokit/rest');
return new Octokit({
request: { agent },
userAgent: 'GitHub VSCode',
auth: `token ${token}`
});
})();
}
return this._octokit;
}
private getAgent(url: string | undefined = process.env.HTTPS_PROXY): Agent {
if (!url) {
return globalAgent;
}
try {
const { hostname, port, username, password } = new URL(url);
const auth = username && password && `${username}:${password}`;
return httpsOverHttp({ proxy: { host: hostname, port, proxyAuth: auth } });
} catch (e) {
vscode.window.showErrorMessage(`HTTPS_PROXY environment variable ignored: ${e.message}`);
return globalAgent;
}
}
async saveProfile(name: string, content: string): Promise<vscode.Uri | null> {
const octokit = await this.getOctokit();
const result = await octokit.gists.create({
public: true,
files: {
[name]: {
content
}
}
});
return result.data.html_url ? vscode.Uri.parse(result.data.html_url) : null;
}
async readProfile(uri: vscode.Uri): Promise<string | null> {
const gist_id = basename(uri.path);
const octokit = await this.getOctokit();
try {
const gist = await octokit.gists.get({ gist_id });
if (gist.data.files) {
return gist.data.files[Object.keys(gist.data.files)[0]]?.content ?? null;
}
} catch (error) {
// ignore
}
return null;
}
}
vscode.window.registerProfileContentHandler('github', new GitHubGistProfileContentHandler());

View file

@ -9,6 +9,7 @@
},
"include": [
"src/**/*",
"../../src/vscode-dts/vscode.d.ts"
"../../src/vscode-dts/vscode.d.ts",
"../../src/vscode-dts/vscode.proposed.profileContentHandlers.d.ts",
]
}

View file

@ -75,6 +75,7 @@ import './mainThreadAuthentication';
import './mainThreadTimeline';
import './mainThreadTesting';
import './mainThreadSecretState';
import './mainThreadProfilContentHandlers';
export class ExtensionPoints implements IWorkbenchContribution {

View file

@ -0,0 +1,52 @@
/*---------------------------------------------------------------------------------------------
* 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 { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { ExtHostContext, ExtHostProfileContentHandlersShape, MainContext, MainThreadProfileContentHandlersShape } from 'vs/workbench/api/common/extHost.protocol';
import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers';
import { IUserDataProfileImportExportService } from 'vs/workbench/services/userDataProfile/common/userDataProfile';
@extHostNamedCustomer(MainContext.MainThreadProfileContentHandlers)
export class MainThreadProfileContentHandlers extends Disposable implements MainThreadProfileContentHandlersShape {
private readonly proxy: ExtHostProfileContentHandlersShape;
private readonly registeredHandlers = new Set<string>();
constructor(
context: IExtHostContext,
@IUserDataProfileImportExportService private readonly userDataProfileImportExportService: IUserDataProfileImportExportService,
) {
super();
this.proxy = context.getProxy(ExtHostContext.ExtHostProfileContentHandlers);
this._register(toDisposable(() => {
for (const id of this.registeredHandlers) {
this.userDataProfileImportExportService.unregisterProfileContentHandler(id);
}
this.registeredHandlers.clear();
}));
}
async $registerProfileContentHandler(id: string, name: string, extensionId: string): Promise<void> {
this.userDataProfileImportExportService.registerProfileContentHandler(id, {
name,
extensionId,
saveProfile: async (name: string, content: string, token: CancellationToken) => {
const result = await this.proxy.$saveProfile(id, name, content, token);
return result ? URI.revive(result) : null;
},
readProfile: async (uri: URI, token: CancellationToken) => {
return this.proxy.$readProfile(id, uri, token);
},
});
}
async $unregisterProfileContentHandler(id: string): Promise<void> {
this.userDataProfileImportExportService.unregisterProfileContentHandler(id);
}
}

View file

@ -95,6 +95,7 @@ import { checkProposedApiEnabled, ExtensionIdentifierSet, isProposedApiEnabled }
import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/contrib/debug/common/debug';
import { IExtHostLocalizationService } from 'vs/workbench/api/common/extHostLocalizationService';
import { EditSessionIdentityMatch } from 'vs/platform/workspace/common/editSessions';
import { ExtHostProfileContentHandlers } from 'vs/workbench/api/common/extHostProfileContentHandler';
export interface IExtensionRegistries {
mine: ExtensionDescriptionRegistry;
@ -186,6 +187,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
const extHostWebviewViews = rpcProtocol.set(ExtHostContext.ExtHostWebviewViews, new ExtHostWebviewViews(rpcProtocol, extHostWebviews));
const extHostTesting = rpcProtocol.set(ExtHostContext.ExtHostTesting, new ExtHostTesting(rpcProtocol, extHostCommands, extHostDocumentsAndEditors));
const extHostUriOpeners = rpcProtocol.set(ExtHostContext.ExtHostUriOpeners, new ExtHostUriOpeners(rpcProtocol));
const extHostProfileContentHandlers = rpcProtocol.set(ExtHostContext.ExtHostProfileContentHandlers, new ExtHostProfileContentHandlers(rpcProtocol));
rpcProtocol.set(ExtHostContext.ExtHostInteractive, new ExtHostInteractive(rpcProtocol, extHostNotebook, extHostDocumentsAndEditors, extHostCommands, extHostLogService));
// Check that no named customers are missing
@ -792,6 +794,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
checkProposedApiEnabled(extension, 'externalUriOpener');
return extHostUriOpeners.registerExternalUriOpener(extension.identifier, id, opener, metadata);
},
registerProfileContentHandler(id: string, handler: vscode.ProfileContentHandler) {
checkProposedApiEnabled(extension, 'profileContentHandlers');
return extHostProfileContentHandlers.registrProfileContentHandler(extension, id, handler);
},
get tabGroups(): vscode.TabGroups {
return extHostEditorTabs.tabGroups;
}

View file

@ -1079,6 +1079,16 @@ export interface ExtHostUriOpenersShape {
$openUri(id: string, context: { resolvedUri: UriComponents; sourceUri: UriComponents }, token: CancellationToken): Promise<void>;
}
export interface MainThreadProfileContentHandlersShape {
$registerProfileContentHandler(id: string, name: string, extensionId: string): Promise<void>;
$unregisterProfileContentHandler(id: string): Promise<void>;
}
export interface ExtHostProfileContentHandlersShape {
$saveProfile(id: string, name: string, content: string, token: CancellationToken): Promise<UriComponents | null>;
$readProfile(id: string, uri: UriComponents, token: CancellationToken): Promise<string | null>;
}
export interface ITextSearchComplete {
limitHit?: boolean;
}
@ -2326,6 +2336,7 @@ export const MainContext = {
MainThreadCustomEditors: createProxyIdentifier<MainThreadCustomEditorsShape>('MainThreadCustomEditors'),
MainThreadUrls: createProxyIdentifier<MainThreadUrlsShape>('MainThreadUrls'),
MainThreadUriOpeners: createProxyIdentifier<MainThreadUriOpenersShape>('MainThreadUriOpeners'),
MainThreadProfileContentHandlers: createProxyIdentifier<MainThreadProfileContentHandlersShape>('MainThreadProfileContentHandlers'),
MainThreadWorkspace: createProxyIdentifier<MainThreadWorkspaceShape>('MainThreadWorkspace'),
MainThreadFileSystem: createProxyIdentifier<MainThreadFileSystemShape>('MainThreadFileSystem'),
MainThreadExtensionService: createProxyIdentifier<MainThreadExtensionServiceShape>('MainThreadExtensionService'),
@ -2385,6 +2396,7 @@ export const ExtHostContext = {
ExtHostStorage: createProxyIdentifier<ExtHostStorageShape>('ExtHostStorage'),
ExtHostUrls: createProxyIdentifier<ExtHostUrlsShape>('ExtHostUrls'),
ExtHostUriOpeners: createProxyIdentifier<ExtHostUriOpenersShape>('ExtHostUriOpeners'),
ExtHostProfileContentHandlers: createProxyIdentifier<ExtHostProfileContentHandlersShape>('ExtHostProfileContentHandlers'),
ExtHostOutputService: createProxyIdentifier<ExtHostOutputServiceShape>('ExtHostOutputService'),
ExtHosLabelService: createProxyIdentifier<ExtHostLabelServiceShape>('ExtHostLabelService'),
ExtHostNotebook: createProxyIdentifier<ExtHostNotebookShape>('ExtHostNotebook'),

View file

@ -0,0 +1,63 @@
/*---------------------------------------------------------------------------------------------
* 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 { toDisposable } from 'vs/base/common/lifecycle';
import { URI, UriComponents } from 'vs/base/common/uri';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
import type * as vscode from 'vscode';
import { ExtHostProfileContentHandlersShape, IMainContext, MainContext, MainThreadProfileContentHandlersShape } from './extHost.protocol';
export class ExtHostProfileContentHandlers implements ExtHostProfileContentHandlersShape {
private readonly proxy: MainThreadProfileContentHandlersShape;
private readonly handlers = new Map<string, vscode.ProfileContentHandler>();
constructor(
mainContext: IMainContext,
) {
this.proxy = mainContext.getProxy(MainContext.MainThreadProfileContentHandlers);
}
registrProfileContentHandler(
extension: IExtensionDescription,
id: string,
handler: vscode.ProfileContentHandler,
): vscode.Disposable {
checkProposedApiEnabled(extension, 'profileContentHandlers');
if (this.handlers.has(id)) {
throw new Error(`Handler with id '${id}' already registered`);
}
this.handlers.set(id, handler);
this.proxy.$registerProfileContentHandler(id, handler.name, extension.identifier.value);
return toDisposable(() => {
this.handlers.delete(id);
this.proxy.$unregisterProfileContentHandler(id);
});
}
async $saveProfile(id: string, name: string, content: string, token: CancellationToken): Promise<UriComponents | null> {
const handler = this.handlers.get(id);
if (!handler) {
throw new Error(`Unknown handler with id: ${id}`);
}
return handler.saveProfile(name, content, token);
}
async $readProfile(id: string, uri: UriComponents, token: CancellationToken): Promise<string | null> {
const handler = this.handlers.get(id);
if (!handler) {
throw new Error(`Unknown handler with id: ${id}`);
}
return handler.readProfile(URI.revive(uri), token);
}
}

View file

@ -50,6 +50,7 @@ export const allApiProposals = Object.freeze({
notebookMessaging: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookMessaging.d.ts',
notebookMime: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookMime.d.ts',
portsAttributes: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.portsAttributes.d.ts',
profileContentHandlers: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.profileContentHandlers.d.ts',
quickPickSortByLabel: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickSortByLabel.d.ts',
resolvers: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.resolvers.d.ts',
scmActionButton: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmActionButton.d.ts',

View file

@ -17,7 +17,7 @@ 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';
import { IProfileResource, IProfileResourceChildTreeItem, IProfileResourceTreeItem, ProfileResourceType } from 'vs/workbench/services/userDataProfile/common/userDataProfile';
interface IProfileExtension {
identifier: IExtensionIdentifier;
@ -39,6 +39,10 @@ export class ExtensionsResource implements IProfileResource {
async getContent(profile: IUserDataProfile, exclude?: string[]): Promise<string> {
const extensions = await this.getLocalExtensions(profile);
return this.toContent(extensions, exclude);
}
toContent(extensions: IProfileExtension[], exclude?: string[]): string {
return JSON.stringify(exclude?.length ? extensions.filter(e => !exclude.includes(e.identifier.id.toLowerCase())) : extensions);
}
@ -140,22 +144,18 @@ export class ExtensionsResource implements IProfileResource {
}
}
export class ExtensionsResourceExportTreeItem implements IProfileResourceTreeItem {
abstract class ExtensionsResourceTreeItem implements IProfileResourceTreeItem {
readonly handle = this.profile.extensionsResource.toString();
readonly type = ProfileResourceType.Extensions;
readonly handle = ProfileResourceType.Extensions;
readonly label = { label: localize('extensions', "Extensions") };
readonly collapsibleState = TreeItemCollapsibleState.Expanded;
checkbox: ITreeItemCheckboxState = { isChecked: true };
private readonly excludedExtensions = new Set<string>();
constructor(
private readonly profile: IUserDataProfile,
@IInstantiationService private readonly instantiationService: IInstantiationService,
) { }
protected readonly excludedExtensions = new Set<string>();
async getChildren(): Promise<IProfileResourceChildTreeItem[]> {
const extensions = await this.instantiationService.createInstance(ExtensionsResource).getLocalExtensions(this.profile);
const extensions = await this.getExtensions();
const that = this;
return extensions.map<IProfileResourceChildTreeItem>(e => ({
handle: e.identifier.id.toLowerCase(),
@ -182,48 +182,51 @@ export class ExtensionsResourceExportTreeItem implements IProfileResourceTreeIte
}
async hasContent(): Promise<boolean> {
const extensions = await this.instantiationService.createInstance(ExtensionsResource).getLocalExtensions(this.profile);
const extensions = await this.getExtensions();
return extensions.length > 0;
}
abstract getContent(): Promise<string>;
protected abstract getExtensions(): Promise<IProfileExtension[]>;
}
export class ExtensionsResourceExportTreeItem extends ExtensionsResourceTreeItem {
constructor(
private readonly profile: IUserDataProfile,
@IInstantiationService private readonly instantiationService: IInstantiationService,
) {
super();
}
protected getExtensions(): Promise<IProfileExtension[]> {
return this.instantiationService.createInstance(ExtensionsResource).getLocalExtensions(this.profile);
}
async getContent(): Promise<string> {
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;
export class ExtensionsResourceImportTreeItem extends ExtensionsResourceTreeItem {
constructor(
private readonly content: string,
@IInstantiationService private readonly instantiationService: IInstantiationService,
) { }
async getChildren(): Promise<IProfileResourceChildTreeItem[]> {
const extensions = await this.instantiationService.createInstance(ExtensionsResource).getProfileExtensions(this.content);
return extensions.map<IProfileResourceChildTreeItem>(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, undefined, true]
}
}));
) {
super();
}
async hasContent(): Promise<boolean> {
const extensions = await this.instantiationService.createInstance(ExtensionsResource).getProfileExtensions(this.content);
return extensions.length > 0;
protected getExtensions(): Promise<IProfileExtension[]> {
return this.instantiationService.createInstance(ExtensionsResource).getProfileExtensions(this.content);
}
async getContent(): Promise<string> {
const extensionsResource = this.instantiationService.createInstance(ExtensionsResource);
const extensions = await extensionsResource.getProfileExtensions(this.content);
return extensionsResource.toContent(extensions, [...this.excludedExtensions.values()]);
}
}

View file

@ -13,7 +13,7 @@ import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataPro
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';
import { IProfileResource, IProfileResourceTreeItem, ProfileResourceType } from 'vs/workbench/services/userDataProfile/common/userDataProfile';
interface IGlobalState {
storage: IStringDictionary<string>;
@ -71,9 +71,10 @@ export class GlobalStateResource implements IProfileResource {
}
}
export class GlobalStateResourceExportTreeItem implements IProfileResourceTreeItem {
abstract class GlobalStateResourceTreeItem implements IProfileResourceTreeItem {
readonly handle = this.profile.globalStorageHome.toString();
readonly type = ProfileResourceType.GlobalState;
readonly handle = 'globalState';
readonly label = { label: localize('globalState', "UI State") };
readonly collapsibleState = TreeItemCollapsibleState.None;
checkbox: ITreeItemCheckboxState = { isChecked: true };
@ -83,14 +84,23 @@ export class GlobalStateResourceExportTreeItem implements IProfileResourceTreeIt
arguments: [this.resource, undefined, undefined]
};
constructor(
private readonly profile: IUserDataProfile,
private readonly resource: URI,
@IInstantiationService private readonly instantiationService: IInstantiationService
) { }
constructor(private readonly resource: URI) { }
async getChildren(): Promise<undefined> { return undefined; }
abstract getContent(): Promise<string>;
}
export class GlobalStateResourceExportTreeItem extends GlobalStateResourceTreeItem {
constructor(
private readonly profile: IUserDataProfile,
resource: URI,
@IInstantiationService private readonly instantiationService: IInstantiationService
) {
super(resource);
}
async hasContent(): Promise<boolean> {
const globalState = await this.instantiationService.createInstance(GlobalStateResource).getGlobalState(this.profile);
return Object.keys(globalState.storage).length > 0;
@ -102,21 +112,17 @@ export class GlobalStateResourceExportTreeItem implements IProfileResourceTreeIt
}
export class GlobalStateResourceImportTreeItem implements IProfileResourceTreeItem {
export class GlobalStateResourceImportTreeItem extends GlobalStateResourceTreeItem {
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 content: string,
resource: URI,
) {
super(resource);
}
constructor(private readonly resource: URI) { }
async getContent(): Promise<string> {
return this.content;
}
async getChildren(): Promise<undefined> { return undefined; }
}

View file

@ -6,7 +6,7 @@
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 { IProfileResource, IProfileResourceTreeItem, ProfileResourceType } 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';
@ -64,10 +64,11 @@ export class KeybindingsResource implements IProfileResource {
export class KeybindingsResourceTreeItem implements IProfileResourceTreeItem {
readonly type = ProfileResourceType.Keybindings;
readonly handle = this.profile.keybindingsResource.toString();
readonly label = { label: localize('keybindings', "Keyboard Shortcuts") };
readonly collapsibleState = TreeItemCollapsibleState.None;
checkbox: ITreeItemCheckboxState | undefined = { isChecked: true };
checkbox: ITreeItemCheckboxState = { isChecked: true };
readonly command = {
id: API_OPEN_EDITOR_COMMAND_ID,
title: '',

View file

@ -8,7 +8,7 @@ import { ConfigurationScope, Extensions, IConfigurationRegistry } from 'vs/platf
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 { IProfileResource, IProfileResourceTreeItem, ProfileResourceType } 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';
@ -83,10 +83,11 @@ export class SettingsResource implements IProfileResource {
export class SettingsResourceTreeItem implements IProfileResourceTreeItem {
readonly type = ProfileResourceType.Settings;
readonly handle = this.profile.settingsResource.toString();
readonly label = { label: localize('settings', "Settings") };
readonly collapsibleState = TreeItemCollapsibleState.None;
checkbox: ITreeItemCheckboxState | undefined = { isChecked: true };
checkbox: ITreeItemCheckboxState = { isChecked: true };
readonly command = {
id: API_OPEN_EDITOR_COMMAND_ID,
title: '',

View file

@ -14,7 +14,7 @@ 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';
import { IProfileResource, IProfileResourceChildTreeItem, IProfileResourceTreeItem, ProfileResourceType } from 'vs/workbench/services/userDataProfile/common/userDataProfile';
interface ISnippetsContent {
snippets: IStringDictionary<string>;
@ -80,10 +80,11 @@ export class SnippetsResource implements IProfileResource {
export class SnippetsResourceTreeItem implements IProfileResourceTreeItem {
readonly type = ProfileResourceType.Snippets;
readonly handle = this.profile.snippetsHome.toString();
readonly label = { label: localize('snippets', "Snippets") };
readonly collapsibleState = TreeItemCollapsibleState.Collapsed;
checkbox: ITreeItemCheckboxState | undefined = { isChecked: true };
checkbox: ITreeItemCheckboxState = { isChecked: true };
private readonly excludedSnippets = new ResourceSet();

View file

@ -11,7 +11,7 @@ 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';
import { IProfileResource, IProfileResourceTreeItem, ProfileResourceType } from 'vs/workbench/services/userDataProfile/common/userDataProfile';
interface ITasksResourceContent {
tasks: string | null;
@ -62,10 +62,11 @@ export class TasksResource implements IProfileResource {
export class TasksResourceTreeItem implements IProfileResourceTreeItem {
readonly type = ProfileResourceType.Tasks;
readonly handle = this.profile.tasksResource.toString();
readonly label = { label: localize('tasks', "User Tasks") };
readonly collapsibleState = TreeItemCollapsibleState.None;
checkbox: ITreeItemCheckboxState | undefined = { isChecked: true };
checkbox: ITreeItemCheckboxState = { isChecked: true };
readonly command = {
id: API_OPEN_EDITOR_COMMAND_ID,
title: '',

View file

@ -8,7 +8,7 @@ import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/
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 { IUserDataProfileImportExportService, PROFILE_FILTER, PROFILE_EXTENSION, IUserDataProfileContentHandler, IS_PROFILE_IMPORT_EXPORT_IN_PROGRESS_CONTEXT, PROFILES_TTILE, defaultUserDataProfileIcon, IUserDataProfileService, IProfileResourceTreeItem, IProfileResourceChildTreeItem, PROFILES_CATEGORY, isUserDataProfileTemplate, IUserDataProfileManagementService, ProfileResourceType } 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';
@ -49,6 +49,13 @@ import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { VSBuffer } from 'vs/base/common/buffer';
import { joinPath } from 'vs/base/common/resources';
import { escapeRegExpCharacters } from 'vs/base/common/strings';
import { Schemas } from 'vs/base/common/network';
import { CancellationToken } from 'vs/base/common/cancellation';
import Severity from 'vs/base/common/severity';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { IURLHandler, IURLService } from 'vs/platform/url/common/url';
import { asText, IRequestService } from 'vs/platform/request/common/request';
import { IProductService } from 'vs/platform/product/common/productService';
interface IUserDataProfileTemplate {
readonly name: string;
@ -61,7 +68,9 @@ interface IUserDataProfileTemplate {
readonly extensions?: string;
}
export class UserDataProfileImportExportService extends Disposable implements IUserDataProfileImportExportService {
export class UserDataProfileImportExportService extends Disposable implements IUserDataProfileImportExportService, IURLHandler {
private static readonly PROFILE_URL_AUTHORITY_PREFIX = 'profile-';
readonly _serviceBrand: undefined;
@ -85,10 +94,15 @@ export class UserDataProfileImportExportService extends Disposable implements IU
@INotificationService private readonly notificationService: INotificationService,
@IProgressService private readonly progressService: IProgressService,
@IDialogService private readonly dialogService: IDialogService,
@IClipboardService private readonly clipboardService: IClipboardService,
@IOpenerService private readonly openerService: IOpenerService,
@IRequestService private readonly requestService: IRequestService,
@IURLService urlService: IURLService,
@IProductService private readonly productService: IProductService,
@ILogService private readonly logService: ILogService,
) {
super();
this.registerProfileContentHandler(this.fileUserDataProfileContentHandler = instantiationService.createInstance(FileUserDataProfileContentHandler));
this.registerProfileContentHandler(Schemas.file, this.fileUserDataProfileContentHandler = instantiationService.createInstance(FileUserDataProfileContentHandler));
this.isProfileImportExportInProgressContextKey = IS_PROFILE_IMPORT_EXPORT_IN_PROGRESS_CONTEXT.bindTo(contextKeyService);
this.viewContainer = Registry.as<IViewContainersRegistry>(Extensions.ViewContainersRegistry).registerViewContainer(
@ -102,13 +116,31 @@ export class UserDataProfileImportExportService extends Disposable implements IU
icon: defaultUserDataProfileIcon,
hideIfEmpty: true,
}, ViewContainerLocation.Sidebar);
urlService.registerHandler(this);
}
registerProfileContentHandler(profileContentHandler: IUserDataProfileContentHandler): void {
if (this.profileContentHandlers.has(profileContentHandler.id)) {
throw new Error(`Profile content handler with id '${profileContentHandler.id}' already registered.`);
private isProfileURL(uri: URI): boolean {
return new RegExp(`^${UserDataProfileImportExportService.PROFILE_URL_AUTHORITY_PREFIX}`).test(uri.authority);
}
async handleURL(uri: URI): Promise<boolean> {
if (this.isProfileURL(uri)) {
await this.importProfile(uri);
return true;
}
this.profileContentHandlers.set(profileContentHandler.id, profileContentHandler);
return false;
}
registerProfileContentHandler(id: string, profileContentHandler: IUserDataProfileContentHandler): void {
if (this.profileContentHandlers.has(id)) {
throw new Error(`Profile content handler with id '${id}' already registered.`);
}
this.profileContentHandlers.set(id, profileContentHandler);
}
unregisterProfileContentHandler(id: string): void {
this.profileContentHandlers.delete(id);
}
async exportProfile(): Promise<void> {
@ -122,16 +154,54 @@ export class UserDataProfileImportExportService extends Disposable implements IU
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);
const userDataProfilesExportState = disposables.add(this.instantiationService.createInstance(UserDataProfileExportState, this.userDataProfileService.currentProfile));
const title = localize('export profile preview', "Export");
let exportProfile = await this.selectProfileResources(
userDataProfilesExportState,
localize('export title', "{0}: {1} ({2})", PROFILES_CATEGORY.value, title, this.userDataProfileService.currentProfile.name),
localize('export description', "Select data to export")
);
if (exportProfile === undefined) {
return;
}
if (!exportProfile) {
exportProfile = await this.showProfilePreviewView(`workbench.views.profiles.export.preview`, title, userDataProfilesExportState);
}
if (!exportProfile) {
return;
}
if (exportProfile) {
const profile = await userDataProfilesData.getProfileToExport();
const profile = await userDataProfilesExportState.getProfileToExport();
if (!profile) {
return;
}
const resource = await this.saveProfileContent(profile.name, JSON.stringify(profile));
if (resource) {
this.notificationService.info(localize('export success', "{0}: Exported successfully.", PROFILES_CATEGORY.value));
const saveResult = await this.saveProfileContent(profile.name, JSON.stringify(profile));
if (saveResult) {
const buttons = saveResult.id === Schemas.file ? undefined : [localize('copy', "Copy Link"), localize('open', "Open in {0}", this.profileContentHandlers.get(saveResult.id)?.name)];
const result = await this.dialogService.show(
Severity.Info,
localize('export success', "Profile '{0}' is exported successfully.", profile.name),
buttons
);
switch (result.choice) {
case 0:
await this.clipboardService.writeText(
URI.from({
scheme: this.productService.urlProtocol,
authority: `${UserDataProfileImportExportService.PROFILE_URL_AUTHORITY_PREFIX}${saveResult.id}`,
path: `/${saveResult.resource.toString()}`
}).toString());
break;
case 1:
await this.openerService.open(saveResult.resource.toString());
break;
}
}
}
} finally {
@ -141,7 +211,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU
async importProfile(uri: URI): Promise<void> {
if (this.isProfileImportExportInProgressContextKey.get()) {
this.logService.warn('Profile import/export already in progress.');
this.notificationService.warn('Profile import/export already in progress.');
return;
}
@ -154,16 +224,34 @@ export class UserDataProfileImportExportService extends Disposable implements IU
if (profileContent === null) {
return;
}
const profileTemplate: IUserDataProfileTemplate = JSON.parse(profileContent);
let 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);
const userDataProfileImportState = disposables.add(this.instantiationService.createInstance(UserDataProfileImportState, profileTemplate));
const title = localize('import profile preview', "Import");
let importProfile = await this.selectProfileResources(
userDataProfileImportState,
localize('import title', "{0}: {1} ({2})", PROFILES_CATEGORY.value, title, profileTemplate.name),
localize('import description', "Select data to import")
);
if (importProfile === undefined) {
return;
}
if (!importProfile) {
importProfile = await this.showProfilePreviewView(`workbench.views.profiles.import.preview`, title, userDataProfileImportState);
}
if (!importProfile) {
return;
}
profileTemplate = await userDataProfileImportState.getProfileTemplateToImport();
const profile = await this.getProfileToImport(profileTemplate);
if (!profile) {
return;
@ -199,44 +287,77 @@ export class UserDataProfileImportExportService extends Disposable implements IU
}
}
private async saveProfileContent(name: string, content: string): Promise<URI | null> {
const profileContentHandler = await this.pickProfileContentHandler();
private async saveProfileContent(name: string, content: string): Promise<{ resource: URI; id: string } | null> {
const id = await this.pickProfileContentHandler();
if (!id) {
return null;
}
const profileContentHandler = this.profileContentHandlers.get(id);
if (!profileContentHandler) {
return null;
}
const resource = await profileContentHandler.saveProfile(name, content);
return resource;
const resource = await profileContentHandler.saveProfile(name, content, CancellationToken.None);
return resource ? { resource, id } : null;
}
private async resolveProfileContent(resource: URI): Promise<string | null> {
if (await this.fileService.canHandleResource(resource)) {
return this.fileUserDataProfileContentHandler.readProfile(resource);
return this.fileUserDataProfileContentHandler.readProfile(resource, CancellationToken.None);
}
await this.extensionService.activateByEvent(`onProfile:import:${resource.authority}`);
const profileContentHandler = this.profileContentHandlers.get(resource.authority);
return profileContentHandler?.readProfile(resource) ?? null;
if (this.isProfileURL(resource)) {
const handlerId = resource.authority.substring(UserDataProfileImportExportService.PROFILE_URL_AUTHORITY_PREFIX.length);
await this.extensionService.activateByEvent(`onProfile:${handlerId}`);
const profileContentHandler = this.profileContentHandlers.get(handlerId);
if (profileContentHandler) {
return profileContentHandler.readProfile(URI.parse(resource.path.substring(1)), CancellationToken.None);
}
}
await this.extensionService.activateByEvent('onProfile');
for (const profileContentHandler of this.profileContentHandlers.values()) {
const content = await profileContentHandler.readProfile(resource, CancellationToken.None);
if (content !== null) {
return content;
}
}
const context = await this.requestService.request({ type: 'GET', url: resource.toString(true) }, CancellationToken.None);
if (context.res.statusCode === 200) {
return await asText(context);
} else {
const message = await asText(context);
this.logService.info(`Failed to get profile from URL: ${resource.toString()}. Status code: ${context.res.statusCode}. Message: ${message}`);
}
return null;
}
private async pickProfileContentHandler(): Promise<IUserDataProfileContentHandler | undefined> {
private async pickProfileContentHandler(): Promise<string | undefined> {
await this.extensionService.activateByEvent('onProfile');
if (this.profileContentHandlers.size === 1) {
return this.profileContentHandlers.values().next().value;
}
await this.extensionService.activateByEvent('onProfile:export');
return undefined;
const result = await this.quickInputService.pick([...this.profileContentHandlers.entries()].map(([id, handler]) => ({ label: handler.name, id })),
{ placeHolder: localize('select profile content handler', "Select the location where to export the profile") });
return result?.id;
}
private async getProfileToImport(profileTemplate: IUserDataProfileTemplate): Promise<IUserDataProfile | undefined> {
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 result = await this.dialogService.show(
Severity.Info,
localize('profile already exists', "Profile with name '{0}' already exists. Do you want to overwrite it?", profileTemplate.name),
[localize('overwrite', "Overwrite"), localize('create new', "Create New Profile"), localize('cancel', "Cancel")],
{ cancelId: 2 }
);
switch (result.choice) {
case 0: return profile;
case 2: return undefined;
}
// Create new profile
const nameRegEx = new RegExp(`${escapeRegExpCharacters(profileTemplate.name)}\\s(\\d+)`);
let nameIndex = 0;
for (const profile of this.userDataProfilesService.profiles) {
@ -264,7 +385,64 @@ export class UserDataProfileImportExportService extends Disposable implements IU
}
}
private async showProfilePreviewView(id: string, name: string, userDataProfilesData: UserDataProfileTreeViewData): Promise<boolean> {
private async selectProfileResources(profileImportExportState: UserDataProfileImportExportState, title: string, description: string): Promise<boolean | undefined> {
type ProfileResourceQuickItem = { item: IProfileResourceTreeItem; label: string };
const disposables: DisposableStore = new DisposableStore();
const quickPick = this.quickInputService.createQuickPick<ProfileResourceQuickItem>();
disposables.add(quickPick);
quickPick.title = title;
quickPick.ok = 'default';
quickPick.customButton = true;
quickPick.customLabel = localize('show contents', "Show Contents");
quickPick.description = description;
quickPick.canSelectMany = true;
quickPick.ignoreFocusOut = true;
quickPick.hideInput = true;
quickPick.hideCheckAll = true;
quickPick.busy = true;
let accepted: boolean = false;
let preview: boolean = false;
disposables.add(quickPick.onDidAccept(() => {
accepted = true;
quickPick.hide();
}));
disposables.add(quickPick.onDidCustom(() => {
preview = true;
quickPick.hide();
}));
const promise = new Promise<boolean | undefined>((c, e) => {
disposables.add(quickPick.onDidHide(() => {
try {
if (accepted || preview) {
for (const root of roots) {
root.checkbox.isChecked = quickPick.selectedItems.some(({ item }) => item === root);
}
c(accepted);
} else {
c(undefined);
}
} catch (error) {
e(error);
} finally {
disposables.dispose();
}
}));
});
quickPick.show();
const roots = await profileImportExportState.getRoots();
quickPick.busy = false;
const items = roots.map<ProfileResourceQuickItem>(item => ({ item, label: item.label?.label ?? item.type }));
quickPick.items = items;
quickPick.selectedItems = items.filter(({ item }) => item.checkbox?.isChecked);
return promise;
}
private async showProfilePreviewView(id: string, name: string, userDataProfilesData: UserDataProfileImportExportState): Promise<boolean> {
const disposables = new DisposableStore();
const viewsRegistry = Registry.as<IViewsRegistry>(Extensions.ViewsRegistry);
const treeView = disposables.add(this.instantiationService.createInstance(TreeView, id, name));
@ -324,8 +502,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU
class FileUserDataProfileContentHandler implements IUserDataProfileContentHandler {
readonly id = 'file';
readonly name = localize('file', "File");
readonly name = localize('file', "Local");
constructor(
@IFileDialogService private readonly fileDialogService: IFileDialogService,
@ -334,7 +511,7 @@ class FileUserDataProfileContentHandler implements IUserDataProfileContentHandle
@ITextFileService private readonly textFileService: ITextFileService,
) { }
async saveProfile(name: string, content: string): Promise<URI | null> {
async saveProfile(name: string, content: string, token: CancellationToken): Promise<URI | null> {
const profileLocation = await this.fileDialogService.showSaveDialog({
title: localize('export profile dialog', "Save Profile"),
filters: PROFILE_FILTER,
@ -347,8 +524,11 @@ class FileUserDataProfileContentHandler implements IUserDataProfileContentHandle
return profileLocation;
}
async readProfile(uri: URI): Promise<string> {
return (await this.fileService.readFile(uri)).value.toString();
async readProfile(uri: URI, token: CancellationToken): Promise<string | null> {
if (await this.fileService.canHandleResource(uri)) {
return (await this.fileService.readFile(uri, undefined, token)).value.toString();
}
return null;
}
async selectProfile(): Promise<URI | null> {
@ -374,7 +554,7 @@ class UserDataProfileExportViewPane extends TreeViewPane {
private totalTreeItemsCount: number = 0;
constructor(
private readonly userDataProfileData: UserDataProfileTreeViewData,
private readonly userDataProfileData: UserDataProfileImportExportState,
private readonly confirmLabel: string,
private readonly onConfirm: () => void,
private readonly onCancel: () => void,
@ -450,43 +630,10 @@ class UserDataProfileExportViewPane extends TreeViewPane {
const USER_DATA_PROFILE_IMPORT_EXPORT_SCHEME = 'userdataprofileimportexport';
const USER_DATA_PROFILE_IMPORT_EXPORT_PREVIEW_SCHEME = 'userdataprofileimportexportpreview';
abstract class UserDataProfileTreeViewData extends Disposable implements ITreeViewDataProvider {
async getChildren(element?: ITreeItem): Promise<ITreeItem[] | undefined> {
if (element) {
return (<IProfileResourceTreeItem>element).getChildren();
} else {
this.rootsPromise = undefined;
return this.getRoots();
}
}
private roots: IProfileResourceTreeItem[] = [];
private rootsPromise: Promise<IProfileResourceTreeItem[]> | undefined;
getRoots(): Promise<IProfileResourceTreeItem[]> {
if (!this.rootsPromise) {
this.rootsPromise = this.fetchRoots().then(roots => this.roots = roots);
}
return this.rootsPromise;
}
isEnabled(): boolean {
return this.roots.some(root => root.checkbox?.isChecked ?? true);
}
abstract onDidChangeCheckboxState(items: ITreeItem[]): ITreeItem[];
protected abstract fetchRoots(): Promise<IProfileResourceTreeItem[]>;
}
class UserDataProfileExportData extends UserDataProfileTreeViewData implements ITreeViewDataProvider {
private readonly disposables = this._register(new DisposableStore());
abstract class UserDataProfileImportExportState extends Disposable implements ITreeViewDataProvider {
constructor(
private readonly profile: IUserDataProfile,
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IFileService private readonly fileService: IFileService,
@IInstantiationService private readonly instantiationService: IInstantiationService
@IQuickInputService protected readonly quickInputService: IQuickInputService,
) {
super();
}
@ -512,6 +659,89 @@ class UserDataProfileExportData extends UserDataProfileTreeViewData implements I
return items;
}
async getChildren(element?: ITreeItem): Promise<ITreeItem[] | undefined> {
if (element) {
return (<IProfileResourceTreeItem>element).getChildren();
} else {
this.rootsPromise = undefined;
return this.getRoots();
}
}
private roots: IProfileResourceTreeItem[] = [];
private rootsPromise: Promise<IProfileResourceTreeItem[]> | undefined;
getRoots(): Promise<IProfileResourceTreeItem[]> {
if (!this.rootsPromise) {
this.rootsPromise = (async () => {
this.roots = await this.fetchRoots();
return this.roots;
})();
}
return this.rootsPromise;
}
isEnabled(resourceType?: ProfileResourceType): boolean {
if (resourceType !== undefined) {
return this.roots.some(root => root.type === resourceType && root.checkbox?.isChecked);
}
return this.roots.some(root => root.checkbox?.isChecked ?? true);
}
protected async getProfileTemplate(name: string, shortName: string | undefined): Promise<IUserDataProfileTemplate> {
const roots = await this.getRoots();
let settings: string | undefined;
let keybindings: string | undefined;
let tasks: string | undefined;
let snippets: string | undefined;
let extensions: string | undefined;
let globalState: string | undefined;
for (const root of roots) {
if (!root.checkbox?.isChecked) {
continue;
}
if (root instanceof SettingsResourceTreeItem) {
settings = await root.getContent();
} else if (root instanceof KeybindingsResourceTreeItem) {
keybindings = await root.getContent();
} else if (root instanceof TasksResourceTreeItem) {
tasks = await root.getContent();
} else if (root instanceof SnippetsResourceTreeItem) {
snippets = await root.getContent();
} else if (root instanceof ExtensionsResourceExportTreeItem) {
extensions = await root.getContent();
} else if (root instanceof GlobalStateResourceExportTreeItem) {
globalState = await root.getContent();
}
}
return {
name,
shortName,
settings,
keybindings,
tasks,
snippets,
extensions,
globalState
};
}
protected abstract fetchRoots(): Promise<IProfileResourceTreeItem[]>;
}
class UserDataProfileExportState extends UserDataProfileImportExportState {
private readonly disposables = this._register(new DisposableStore());
constructor(
private readonly profile: IUserDataProfile,
@IQuickInputService quickInputService: IQuickInputService,
@IFileService private readonly fileService: IFileService,
@IInstantiationService private readonly instantiationService: IInstantiationService
) {
super(quickInputService);
}
protected async fetchRoots(): Promise<IProfileResourceTreeItem[]> {
this.disposables.clear();
this.disposables.add(this.fileService.registerProvider(USER_DATA_PROFILE_IMPORT_EXPORT_SCHEME, this._register(new InMemoryFileSystemProvider())));
@ -600,60 +830,22 @@ class UserDataProfileExportData extends UserDataProfileTreeViewData implements I
}
}
const roots = await this.getRoots();
let settings: string | undefined;
let keybindings: string | undefined;
let tasks: string | undefined;
let snippets: string | undefined;
let extensions: string | undefined;
let globalState: string | undefined;
for (const root of roots) {
if (!root.checkbox?.isChecked) {
continue;
}
if (root instanceof SettingsResourceTreeItem) {
settings = await root.getContent();
} else if (root instanceof KeybindingsResourceTreeItem) {
keybindings = await root.getContent();
} else if (root instanceof TasksResourceTreeItem) {
tasks = await root.getContent();
} else if (root instanceof SnippetsResourceTreeItem) {
snippets = await root.getContent();
} else if (root instanceof ExtensionsResourceExportTreeItem) {
extensions = await root.getContent();
} else if (root instanceof GlobalStateResourceExportTreeItem) {
globalState = await root.getContent();
}
}
return {
name,
shortName: this.profile.shortName,
settings,
keybindings,
tasks,
snippets,
extensions,
globalState
};
return super.getProfileTemplate(name, this.profile.shortName);
}
}
class UserDataProfileImportData extends UserDataProfileTreeViewData implements ITreeViewDataProvider {
class UserDataProfileImportState extends UserDataProfileImportExportState {
private readonly disposables = this._register(new DisposableStore());
constructor(
private readonly profile: IUserDataProfileTemplate,
@IFileService private readonly fileService: IFileService,
@IQuickInputService quickInputService: IQuickInputService,
@IInstantiationService private readonly instantiationService: IInstantiationService
) {
super();
}
onDidChangeCheckboxState(items: ITreeItem[]): ITreeItem[] {
return items;
super(quickInputService);
}
protected async fetchRoots(): Promise<IProfileResourceTreeItem[]> {
@ -668,7 +860,6 @@ class UserDataProfileImportData extends UserDataProfileTreeViewData implements I
const settingsResource = this.instantiationService.createInstance(SettingsResource);
await settingsResource.apply(this.profile.settings, importPreviewProfle);
const settingsResourceTreeItem = this.instantiationService.createInstance(SettingsResourceTreeItem, importPreviewProfle);
settingsResourceTreeItem.checkbox = undefined;
if (await settingsResourceTreeItem.hasContent()) {
roots.push(settingsResourceTreeItem);
}
@ -678,7 +869,6 @@ class UserDataProfileImportData extends UserDataProfileTreeViewData implements I
const keybindingsResource = this.instantiationService.createInstance(KeybindingsResource);
await keybindingsResource.apply(this.profile.keybindings, importPreviewProfle);
const keybindingsResourceTreeItem = this.instantiationService.createInstance(KeybindingsResourceTreeItem, importPreviewProfle);
keybindingsResourceTreeItem.checkbox = undefined;
if (await keybindingsResourceTreeItem.hasContent()) {
roots.push(keybindingsResourceTreeItem);
}
@ -688,7 +878,6 @@ class UserDataProfileImportData extends UserDataProfileTreeViewData implements I
const tasksResource = this.instantiationService.createInstance(TasksResource);
await tasksResource.apply(this.profile.tasks, importPreviewProfle);
const tasksResourceTreeItem = this.instantiationService.createInstance(TasksResourceTreeItem, importPreviewProfle);
tasksResourceTreeItem.checkbox = undefined;
if (await tasksResourceTreeItem.hasContent()) {
roots.push(tasksResourceTreeItem);
}
@ -698,7 +887,6 @@ class UserDataProfileImportData extends UserDataProfileTreeViewData implements I
const snippetsResource = this.instantiationService.createInstance(SnippetsResource);
await snippetsResource.apply(this.profile.snippets, importPreviewProfle);
const snippetsResourceTreeItem = this.instantiationService.createInstance(SnippetsResourceTreeItem, importPreviewProfle);
snippetsResourceTreeItem.checkbox = undefined;
if (await snippetsResourceTreeItem.hasContent()) {
roots.push(snippetsResourceTreeItem);
}
@ -709,7 +897,7 @@ class UserDataProfileImportData extends UserDataProfileTreeViewData implements I
const content = VSBuffer.fromString(JSON.stringify(JSON.parse(this.profile.globalState), null, '\t'));
if (content) {
await this.fileService.writeFile(globalStateResource, content);
roots.push(this.instantiationService.createInstance(GlobalStateResourceImportTreeItem, globalStateResource));
roots.push(this.instantiationService.createInstance(GlobalStateResourceImportTreeItem, this.profile.globalState, globalStateResource));
}
}
@ -725,6 +913,10 @@ class UserDataProfileImportData extends UserDataProfileTreeViewData implements I
return roots;
}
async getProfileTemplateToImport(): Promise<IUserDataProfileTemplate> {
return this.getProfileTemplate(this.profile.name, this.profile.shortName);
}
}
registerSingleton(IUserDataProfileImportExportService, UserDataProfileImportExportService, InstantiationType.Delayed);

View file

@ -13,7 +13,8 @@ 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';
import { ITreeItem, ITreeItemCheckboxState, ITreeItemLabel } from 'vs/workbench/common/views';
import { CancellationToken } from 'vs/base/common/cancellation';
export interface DidChangeUserDataProfileEvent {
readonly preserveData: boolean;
@ -68,20 +69,34 @@ export const IUserDataProfileImportExportService = createDecorator<IUserDataProf
export interface IUserDataProfileImportExportService {
readonly _serviceBrand: undefined;
registerProfileContentHandler(profileContentHandler: IUserDataProfileContentHandler): void;
registerProfileContentHandler(id: string, profileContentHandler: IUserDataProfileContentHandler): void;
unregisterProfileContentHandler(id: string): void;
exportProfile(): Promise<void>;
importProfile(uri: URI): Promise<void>;
setProfile(profile: IUserDataProfileTemplate): Promise<void>;
}
export const enum ProfileResourceType {
Settings = 'settings',
Keybindings = 'keybindings',
Snippets = 'snippets',
Tasks = 'tasks',
Extensions = 'extensions',
GlobalState = 'globalState',
}
export interface IProfileResource {
getContent(profile: IUserDataProfile): Promise<string>;
apply(content: string, profile: IUserDataProfile): Promise<void>;
}
export interface IProfileResourceTreeItem extends ITreeItem {
readonly type: ProfileResourceType;
checkbox: ITreeItemCheckboxState;
readonly label: ITreeItemLabel;
getChildren(): Promise<IProfileResourceChildTreeItem[] | undefined>;
getContent(): Promise<string>;
}
export interface IProfileResourceChildTreeItem extends ITreeItem {
@ -89,11 +104,11 @@ export interface IProfileResourceChildTreeItem extends ITreeItem {
}
export interface IUserDataProfileContentHandler {
readonly id: string;
readonly name: string;
readonly description?: string;
saveProfile(name: string, content: string): Promise<URI | null>;
readProfile(uri: URI): Promise<string>;
readonly extensionId?: string;
saveProfile(name: string, content: string, token: CancellationToken): Promise<URI | null>;
readProfile(uri: URI, token: CancellationToken): Promise<string | null>;
}
export const defaultUserDataProfileIcon = registerIcon('defaultProfile-icon', Codicon.settings, localize('defaultProfileIcon', 'Icon for Default Profile.'));

View file

@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
declare module 'vscode' {
export interface ProfileContentHandler {
readonly name: string;
readonly description?: string;
saveProfile(name: string, content: string, token: CancellationToken): Thenable<Uri | null>;
readProfile(uri: Uri, token: CancellationToken): Thenable<string | null>;
}
export namespace window {
export function registerProfileContentHandler(id: string, profileContentHandler: ProfileContentHandler): Disposable;
}
}