Skip to content

Commit e43bf31

Browse files
authored
Share profiles (microsoft#166898)
* Share profiles microsoft#159891 - Share profile in GitHub - Profile resource quick pick - Import profile from vscode link * remove duplicate code
1 parent ebb77a7 commit e43bf31

19 files changed

+650
-191
lines changed

extensions/github/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
"Other"
1414
],
1515
"activationEvents": [
16-
"*"
16+
"*",
17+
"onProfile",
18+
"onProfile:github"
1719
],
1820
"extensionDependencies": [
1921
"vscode.git-base"
@@ -27,7 +29,8 @@
2729
},
2830
"enabledApiProposals": [
2931
"contribShareMenu",
30-
"contribEditSessions"
32+
"contribEditSessions",
33+
"profileContentHandlers"
3134
],
3235
"contributes": {
3336
"commands": [

extensions/github/src/extension.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { DisposableStore, repositoryHasGitHubRemote } from './util';
1212
import { GithubPushErrorHandler } from './pushErrorHandler';
1313
import { GitBaseExtension } from './typings/git-base';
1414
import { GithubRemoteSourcePublisher } from './remoteSourcePublisher';
15+
import './importExportProfiles';
1516

1617
export function activate(context: ExtensionContext): void {
1718
context.subscriptions.push(initializeGitBaseExtension());
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { Octokit } from '@octokit/rest';
7+
import * as vscode from 'vscode';
8+
import { httpsOverHttp } from 'tunnel';
9+
import { Agent, globalAgent } from 'https';
10+
import { basename } from 'path';
11+
import { URL } from 'url';
12+
13+
class GitHubGistProfileContentHandler implements vscode.ProfileContentHandler {
14+
15+
readonly name = vscode.l10n.t('GitHub');
16+
17+
private _octokit: Promise<Octokit> | undefined;
18+
private getOctokit(): Promise<Octokit> {
19+
if (!this._octokit) {
20+
this._octokit = (async () => {
21+
const session = await vscode.authentication.getSession('github', ['gist', 'user:email'], { createIfNone: true });
22+
const token = session.accessToken;
23+
const agent = this.getAgent();
24+
25+
const { Octokit } = await import('@octokit/rest');
26+
27+
return new Octokit({
28+
request: { agent },
29+
userAgent: 'GitHub VSCode',
30+
auth: `token ${token}`
31+
});
32+
})();
33+
}
34+
return this._octokit;
35+
}
36+
37+
private getAgent(url: string | undefined = process.env.HTTPS_PROXY): Agent {
38+
if (!url) {
39+
return globalAgent;
40+
}
41+
try {
42+
const { hostname, port, username, password } = new URL(url);
43+
const auth = username && password && `${username}:${password}`;
44+
return httpsOverHttp({ proxy: { host: hostname, port, proxyAuth: auth } });
45+
} catch (e) {
46+
vscode.window.showErrorMessage(`HTTPS_PROXY environment variable ignored: ${e.message}`);
47+
return globalAgent;
48+
}
49+
}
50+
51+
async saveProfile(name: string, content: string): Promise<vscode.Uri | null> {
52+
const octokit = await this.getOctokit();
53+
const result = await octokit.gists.create({
54+
public: true,
55+
files: {
56+
[name]: {
57+
content
58+
}
59+
}
60+
});
61+
return result.data.html_url ? vscode.Uri.parse(result.data.html_url) : null;
62+
}
63+
64+
async readProfile(uri: vscode.Uri): Promise<string | null> {
65+
const gist_id = basename(uri.path);
66+
const octokit = await this.getOctokit();
67+
try {
68+
const gist = await octokit.gists.get({ gist_id });
69+
if (gist.data.files) {
70+
return gist.data.files[Object.keys(gist.data.files)[0]]?.content ?? null;
71+
}
72+
} catch (error) {
73+
// ignore
74+
}
75+
return null;
76+
}
77+
78+
}
79+
80+
vscode.window.registerProfileContentHandler('github', new GitHubGistProfileContentHandler());

extensions/github/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
},
1010
"include": [
1111
"src/**/*",
12-
"../../src/vscode-dts/vscode.d.ts"
12+
"../../src/vscode-dts/vscode.d.ts",
13+
"../../src/vscode-dts/vscode.proposed.profileContentHandlers.d.ts",
1314
]
1415
}

src/vs/workbench/api/browser/extensionHost.contribution.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ import './mainThreadAuthentication';
7575
import './mainThreadTimeline';
7676
import './mainThreadTesting';
7777
import './mainThreadSecretState';
78+
import './mainThreadProfilContentHandlers';
7879

7980
export class ExtensionPoints implements IWorkbenchContribution {
8081

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { CancellationToken } from 'vs/base/common/cancellation';
7+
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
8+
import { URI } from 'vs/base/common/uri';
9+
import { ExtHostContext, ExtHostProfileContentHandlersShape, MainContext, MainThreadProfileContentHandlersShape } from 'vs/workbench/api/common/extHost.protocol';
10+
import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers';
11+
import { IUserDataProfileImportExportService } from 'vs/workbench/services/userDataProfile/common/userDataProfile';
12+
13+
@extHostNamedCustomer(MainContext.MainThreadProfileContentHandlers)
14+
export class MainThreadProfileContentHandlers extends Disposable implements MainThreadProfileContentHandlersShape {
15+
16+
private readonly proxy: ExtHostProfileContentHandlersShape;
17+
18+
private readonly registeredHandlers = new Set<string>();
19+
20+
constructor(
21+
context: IExtHostContext,
22+
@IUserDataProfileImportExportService private readonly userDataProfileImportExportService: IUserDataProfileImportExportService,
23+
) {
24+
super();
25+
this.proxy = context.getProxy(ExtHostContext.ExtHostProfileContentHandlers);
26+
this._register(toDisposable(() => {
27+
for (const id of this.registeredHandlers) {
28+
this.userDataProfileImportExportService.unregisterProfileContentHandler(id);
29+
}
30+
this.registeredHandlers.clear();
31+
}));
32+
}
33+
34+
async $registerProfileContentHandler(id: string, name: string, extensionId: string): Promise<void> {
35+
this.userDataProfileImportExportService.registerProfileContentHandler(id, {
36+
name,
37+
extensionId,
38+
saveProfile: async (name: string, content: string, token: CancellationToken) => {
39+
const result = await this.proxy.$saveProfile(id, name, content, token);
40+
return result ? URI.revive(result) : null;
41+
},
42+
readProfile: async (uri: URI, token: CancellationToken) => {
43+
return this.proxy.$readProfile(id, uri, token);
44+
},
45+
});
46+
}
47+
48+
async $unregisterProfileContentHandler(id: string): Promise<void> {
49+
this.userDataProfileImportExportService.unregisterProfileContentHandler(id);
50+
}
51+
52+
}

src/vs/workbench/api/common/extHost.api.impl.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ import { checkProposedApiEnabled, ExtensionIdentifierSet, isProposedApiEnabled }
9595
import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/contrib/debug/common/debug';
9696
import { IExtHostLocalizationService } from 'vs/workbench/api/common/extHostLocalizationService';
9797
import { EditSessionIdentityMatch } from 'vs/platform/workspace/common/editSessions';
98+
import { ExtHostProfileContentHandlers } from 'vs/workbench/api/common/extHostProfileContentHandler';
9899

99100
export interface IExtensionRegistries {
100101
mine: ExtensionDescriptionRegistry;
@@ -186,6 +187,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
186187
const extHostWebviewViews = rpcProtocol.set(ExtHostContext.ExtHostWebviewViews, new ExtHostWebviewViews(rpcProtocol, extHostWebviews));
187188
const extHostTesting = rpcProtocol.set(ExtHostContext.ExtHostTesting, new ExtHostTesting(rpcProtocol, extHostCommands, extHostDocumentsAndEditors));
188189
const extHostUriOpeners = rpcProtocol.set(ExtHostContext.ExtHostUriOpeners, new ExtHostUriOpeners(rpcProtocol));
190+
const extHostProfileContentHandlers = rpcProtocol.set(ExtHostContext.ExtHostProfileContentHandlers, new ExtHostProfileContentHandlers(rpcProtocol));
189191
rpcProtocol.set(ExtHostContext.ExtHostInteractive, new ExtHostInteractive(rpcProtocol, extHostNotebook, extHostDocumentsAndEditors, extHostCommands, extHostLogService));
190192

191193
// Check that no named customers are missing
@@ -792,6 +794,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
792794
checkProposedApiEnabled(extension, 'externalUriOpener');
793795
return extHostUriOpeners.registerExternalUriOpener(extension.identifier, id, opener, metadata);
794796
},
797+
registerProfileContentHandler(id: string, handler: vscode.ProfileContentHandler) {
798+
checkProposedApiEnabled(extension, 'profileContentHandlers');
799+
return extHostProfileContentHandlers.registrProfileContentHandler(extension, id, handler);
800+
},
795801
get tabGroups(): vscode.TabGroups {
796802
return extHostEditorTabs.tabGroups;
797803
}

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,6 +1079,16 @@ export interface ExtHostUriOpenersShape {
10791079
$openUri(id: string, context: { resolvedUri: UriComponents; sourceUri: UriComponents }, token: CancellationToken): Promise<void>;
10801080
}
10811081

1082+
export interface MainThreadProfileContentHandlersShape {
1083+
$registerProfileContentHandler(id: string, name: string, extensionId: string): Promise<void>;
1084+
$unregisterProfileContentHandler(id: string): Promise<void>;
1085+
}
1086+
1087+
export interface ExtHostProfileContentHandlersShape {
1088+
$saveProfile(id: string, name: string, content: string, token: CancellationToken): Promise<UriComponents | null>;
1089+
$readProfile(id: string, uri: UriComponents, token: CancellationToken): Promise<string | null>;
1090+
}
1091+
10821092
export interface ITextSearchComplete {
10831093
limitHit?: boolean;
10841094
}
@@ -2326,6 +2336,7 @@ export const MainContext = {
23262336
MainThreadCustomEditors: createProxyIdentifier<MainThreadCustomEditorsShape>('MainThreadCustomEditors'),
23272337
MainThreadUrls: createProxyIdentifier<MainThreadUrlsShape>('MainThreadUrls'),
23282338
MainThreadUriOpeners: createProxyIdentifier<MainThreadUriOpenersShape>('MainThreadUriOpeners'),
2339+
MainThreadProfileContentHandlers: createProxyIdentifier<MainThreadProfileContentHandlersShape>('MainThreadProfileContentHandlers'),
23292340
MainThreadWorkspace: createProxyIdentifier<MainThreadWorkspaceShape>('MainThreadWorkspace'),
23302341
MainThreadFileSystem: createProxyIdentifier<MainThreadFileSystemShape>('MainThreadFileSystem'),
23312342
MainThreadExtensionService: createProxyIdentifier<MainThreadExtensionServiceShape>('MainThreadExtensionService'),
@@ -2385,6 +2396,7 @@ export const ExtHostContext = {
23852396
ExtHostStorage: createProxyIdentifier<ExtHostStorageShape>('ExtHostStorage'),
23862397
ExtHostUrls: createProxyIdentifier<ExtHostUrlsShape>('ExtHostUrls'),
23872398
ExtHostUriOpeners: createProxyIdentifier<ExtHostUriOpenersShape>('ExtHostUriOpeners'),
2399+
ExtHostProfileContentHandlers: createProxyIdentifier<ExtHostProfileContentHandlersShape>('ExtHostProfileContentHandlers'),
23882400
ExtHostOutputService: createProxyIdentifier<ExtHostOutputServiceShape>('ExtHostOutputService'),
23892401
ExtHosLabelService: createProxyIdentifier<ExtHostLabelServiceShape>('ExtHostLabelService'),
23902402
ExtHostNotebook: createProxyIdentifier<ExtHostNotebookShape>('ExtHostNotebook'),
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { CancellationToken } from 'vs/base/common/cancellation';
7+
import { toDisposable } from 'vs/base/common/lifecycle';
8+
import { URI, UriComponents } from 'vs/base/common/uri';
9+
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
10+
import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
11+
import type * as vscode from 'vscode';
12+
import { ExtHostProfileContentHandlersShape, IMainContext, MainContext, MainThreadProfileContentHandlersShape } from './extHost.protocol';
13+
14+
15+
export class ExtHostProfileContentHandlers implements ExtHostProfileContentHandlersShape {
16+
17+
private readonly proxy: MainThreadProfileContentHandlersShape;
18+
19+
private readonly handlers = new Map<string, vscode.ProfileContentHandler>();
20+
21+
constructor(
22+
mainContext: IMainContext,
23+
) {
24+
this.proxy = mainContext.getProxy(MainContext.MainThreadProfileContentHandlers);
25+
}
26+
27+
registrProfileContentHandler(
28+
extension: IExtensionDescription,
29+
id: string,
30+
handler: vscode.ProfileContentHandler,
31+
): vscode.Disposable {
32+
checkProposedApiEnabled(extension, 'profileContentHandlers');
33+
if (this.handlers.has(id)) {
34+
throw new Error(`Handler with id '${id}' already registered`);
35+
}
36+
37+
this.handlers.set(id, handler);
38+
this.proxy.$registerProfileContentHandler(id, handler.name, extension.identifier.value);
39+
40+
return toDisposable(() => {
41+
this.handlers.delete(id);
42+
this.proxy.$unregisterProfileContentHandler(id);
43+
});
44+
}
45+
46+
async $saveProfile(id: string, name: string, content: string, token: CancellationToken): Promise<UriComponents | null> {
47+
const handler = this.handlers.get(id);
48+
if (!handler) {
49+
throw new Error(`Unknown handler with id: ${id}`);
50+
}
51+
52+
return handler.saveProfile(name, content, token);
53+
}
54+
55+
async $readProfile(id: string, uri: UriComponents, token: CancellationToken): Promise<string | null> {
56+
const handler = this.handlers.get(id);
57+
if (!handler) {
58+
throw new Error(`Unknown handler with id: ${id}`);
59+
}
60+
61+
return handler.readProfile(URI.revive(uri), token);
62+
}
63+
}

src/vs/workbench/services/extensions/common/extensionsApiProposals.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export const allApiProposals = Object.freeze({
5050
notebookMessaging: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookMessaging.d.ts',
5151
notebookMime: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookMime.d.ts',
5252
portsAttributes: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.portsAttributes.d.ts',
53+
profileContentHandlers: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.profileContentHandlers.d.ts',
5354
quickPickSortByLabel: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickSortByLabel.d.ts',
5455
resolvers: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.resolvers.d.ts',
5556
scmActionButton: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmActionButton.d.ts',

0 commit comments

Comments
 (0)