Skip to content

Commit bc1090c

Browse files
authored
Initial share provider API and UI (microsoft#182999)
* Formalize share provider API * i18n.resources.json * Don't introduce a generic Success dialog severity
1 parent d470f53 commit bc1090c

File tree

21 files changed

+529
-4
lines changed

21 files changed

+529
-4
lines changed

build/lib/i18n.resources.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,10 @@
510510
"name": "vs/workbench/services/localization",
511511
"project": "vscode-workbench"
512512
},
513+
{
514+
"name": "vs/workbench/contrib/share",
515+
"project": "vscode-workbench"
516+
},
513517
{
514518
"name": "vs/workbench/contrib/accessibility",
515519
"project": "vscode-workbench"

build/lib/stylelint/vscode-known-variables.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,7 @@
457457
"--vscode-problemsErrorIcon-foreground",
458458
"--vscode-problemsInfoIcon-foreground",
459459
"--vscode-problemsWarningIcon-foreground",
460+
"--vscode-problemsSuccessIcon-foreground",
460461
"--vscode-profileBadge-background",
461462
"--vscode-profileBadge-foreground",
462463
"--vscode-progressBar-background",

extensions/github/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
"enabledApiProposals": [
2929
"contribShareMenu",
3030
"contribEditSessions",
31-
"canonicalUriProvider"
31+
"canonicalUriProvider",
32+
"shareProvider"
3233
],
3334
"contributes": {
3435
"commands": [

extensions/github/src/extension.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { GitBaseExtension } from './typings/git-base';
1414
import { GithubRemoteSourcePublisher } from './remoteSourcePublisher';
1515
import { GithubBranchProtectionProviderManager } from './branchProtection';
1616
import { GitHubCanonicalUriProvider } from './canonicalUriProvider';
17+
import { VscodeDevShareProvider } from './shareProviders';
1718

1819
export function activate(context: ExtensionContext): void {
1920
const disposables: Disposable[] = [];
@@ -95,6 +96,7 @@ function initializeGitExtension(context: ExtensionContext, logger: LogOutputChan
9596
disposables.add(gitAPI.registerPushErrorHandler(new GithubPushErrorHandler()));
9697
disposables.add(gitAPI.registerRemoteSourcePublisher(new GithubRemoteSourcePublisher(gitAPI)));
9798
disposables.add(new GitHubCanonicalUriProvider(gitAPI));
99+
disposables.add(new VscodeDevShareProvider(gitAPI));
98100
setGitHubContext(gitAPI, disposables);
99101

100102
commands.executeCommand('setContext', 'git-base.gitEnabled', true);

extensions/github/src/links.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ function getRangeOrSelection(lineNumber: number | undefined) {
9292
: vscode.window.activeTextEditor?.selection;
9393
}
9494

95-
function rangeString(range: vscode.Range | undefined) {
95+
export function rangeString(range: vscode.Range | undefined) {
9696
if (!range) {
9797
return '';
9898
}
@@ -119,7 +119,7 @@ export function notebookCellRangeString(index: number | undefined, range: vscode
119119
return hash;
120120
}
121121

122-
function encodeURIComponentExceptSlashes(path: string) {
122+
export function encodeURIComponentExceptSlashes(path: string) {
123123
// There may be special characters like # and whitespace in the path.
124124
// These characters are not escaped by encodeURI(), so it is not sufficient to
125125
// feed the full URI to encodeURI().
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
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 * as vscode from 'vscode';
7+
import { API } from './typings/git';
8+
import { getRepositoryFromUrl, repositoryHasGitHubRemote } from './util';
9+
import { encodeURIComponentExceptSlashes, getRepositoryForFile, notebookCellRangeString, rangeString } from './links';
10+
11+
export class VscodeDevShareProvider implements vscode.ShareProvider, vscode.Disposable {
12+
readonly id: string = 'copyVscodeDevLink';
13+
readonly label: string = vscode.l10n.t('Copy vscode.dev Link');
14+
readonly priority: number = 10;
15+
16+
17+
private _hasGitHubRepositories: boolean = false;
18+
private set hasGitHubRepositories(value: boolean) {
19+
vscode.commands.executeCommand('setContext', 'github.hasGitHubRepo', value);
20+
this._hasGitHubRepositories = value;
21+
this.ensureShareProviderRegistration();
22+
}
23+
24+
private shareProviderRegistration: vscode.Disposable | undefined;
25+
private disposables: vscode.Disposable[] = [];
26+
27+
constructor(private readonly gitAPI: API) {
28+
this.initializeGitHubRepoContext();
29+
}
30+
31+
dispose() {
32+
this.disposables.forEach(d => d.dispose());
33+
}
34+
35+
private initializeGitHubRepoContext() {
36+
if (this.gitAPI.repositories.find(repo => repositoryHasGitHubRemote(repo))) {
37+
this.hasGitHubRepositories = true;
38+
vscode.commands.executeCommand('setContext', 'github.hasGitHubRepo', true);
39+
} else {
40+
this.disposables.push(this.gitAPI.onDidOpenRepository(async e => {
41+
await e.status();
42+
if (repositoryHasGitHubRemote(e)) {
43+
vscode.commands.executeCommand('setContext', 'github.hasGitHubRepo', true);
44+
this.hasGitHubRepositories = true;
45+
}
46+
}));
47+
}
48+
this.disposables.push(this.gitAPI.onDidCloseRepository(() => {
49+
if (!this.gitAPI.repositories.find(repo => repositoryHasGitHubRemote(repo))) {
50+
this.hasGitHubRepositories = false;
51+
}
52+
}));
53+
}
54+
55+
private ensureShareProviderRegistration() {
56+
if (vscode.env.appHost !== 'codespaces' && !this.shareProviderRegistration && this._hasGitHubRepositories) {
57+
const shareProviderRegistration = vscode.window.registerShareProvider({ scheme: 'file' }, this);
58+
this.shareProviderRegistration = shareProviderRegistration;
59+
this.disposables.push(shareProviderRegistration);
60+
} else if (this.shareProviderRegistration && !this._hasGitHubRepositories) {
61+
this.shareProviderRegistration.dispose();
62+
this.shareProviderRegistration = undefined;
63+
}
64+
}
65+
66+
provideShare(item: vscode.ShareableItem, _token: vscode.CancellationToken): vscode.ProviderResult<vscode.Uri> {
67+
const repository = getRepositoryForFile(this.gitAPI, item.resourceUri);
68+
if (!repository) {
69+
return;
70+
}
71+
72+
let repo: { owner: string; repo: string } | undefined;
73+
repository.state.remotes.find(remote => {
74+
if (remote.fetchUrl) {
75+
const foundRepo = getRepositoryFromUrl(remote.fetchUrl);
76+
if (foundRepo && (remote.name === repository.state.HEAD?.upstream?.remote)) {
77+
repo = foundRepo;
78+
return;
79+
} else if (foundRepo && !repo) {
80+
repo = foundRepo;
81+
}
82+
}
83+
return;
84+
});
85+
86+
if (!repo) {
87+
return;
88+
}
89+
90+
const blobSegment = repository?.state.HEAD?.name ? encodeURIComponentExceptSlashes(repository.state.HEAD?.name) : repository?.state.HEAD?.commit;
91+
const filepathSegment = encodeURIComponentExceptSlashes(item.resourceUri.path.substring(repository?.rootUri.path.length));
92+
const rangeSegment = getRangeSegment(item);
93+
return vscode.Uri.parse(`${this.getVscodeDevHost()}/${repo.owner}/${repo.repo}/blob/${blobSegment}${filepathSegment}${rangeSegment}${rangeSegment}`);
94+
95+
}
96+
97+
private getVscodeDevHost(): string {
98+
return `https://${vscode.env.appName.toLowerCase().includes('insiders') ? 'insiders.' : ''}vscode.dev/github`;
99+
}
100+
}
101+
102+
function getRangeSegment(item: vscode.ShareableItem) {
103+
if (item.resourceUri.scheme === 'vscode-notebook-cell') {
104+
const notebookEditor = vscode.window.visibleNotebookEditors.find(editor => editor.notebook.uri.fsPath === item.resourceUri.fsPath);
105+
const cell = notebookEditor?.notebook.getCells().find(cell => cell.document.uri.fragment === item.resourceUri?.fragment);
106+
const cellIndex = cell?.index ?? notebookEditor?.selection.start;
107+
return notebookCellRangeString(cellIndex, item.selection);
108+
}
109+
110+
return rangeString(item.selection);
111+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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+
// https://github.com/microsoft/vscode/issues/176316
7+
8+
declare module 'vscode' {
9+
export interface TreeItem {
10+
shareableItem?: ShareableItem;
11+
}
12+
13+
export interface ShareableItem {
14+
resourceUri: Uri;
15+
selection?: Range;
16+
}
17+
18+
export interface ShareProvider {
19+
readonly id: string;
20+
readonly label: string;
21+
readonly priority: number;
22+
23+
provideShare(item: ShareableItem, token: CancellationToken): ProviderResult<Uri>;
24+
}
25+
26+
export namespace window {
27+
export function registerShareProvider(selector: DocumentSelector, provider: ShareProvider): Disposable;
28+
}
29+
}

src/vs/platform/actions/common/actions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,11 @@ export class MenuId {
9898
static readonly MenubarViewMenu = new MenuId('MenubarViewMenu');
9999
static readonly MenubarHomeMenu = new MenuId('MenubarHomeMenu');
100100
static readonly OpenEditorsContext = new MenuId('OpenEditorsContext');
101+
static readonly OpenEditorsContextShare = new MenuId('OpenEditorsContextShare');
101102
static readonly ProblemsPanelContext = new MenuId('ProblemsPanelContext');
102103
static readonly SCMChangeContext = new MenuId('SCMChangeContext');
103104
static readonly SCMResourceContext = new MenuId('SCMResourceContext');
105+
static readonly SCMResourceContextShare = new MenuId('SCMResourceContextShare');
104106
static readonly SCMResourceFolderContext = new MenuId('SCMResourceFolderContext');
105107
static readonly SCMResourceGroupContext = new MenuId('SCMResourceGroupContext');
106108
static readonly SCMSourceControl = new MenuId('SCMSourceControl');

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ import './mainThreadAuthentication';
8282
import './mainThreadTimeline';
8383
import './mainThreadTesting';
8484
import './mainThreadSecretState';
85+
import './mainThreadShare';
8586
import './mainThreadProfilContentHandlers';
8687
import './mainThreadSemanticSimilarity';
8788
import './mainThreadIssueReporter';
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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 { CancellationTokenSource } from 'vs/base/common/cancellation';
7+
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
8+
import { URI } from 'vs/base/common/uri';
9+
import { ExtHostContext, ExtHostShareShape, IDocumentFilterDto, MainContext, MainThreadShareShape } from 'vs/workbench/api/common/extHost.protocol';
10+
import { IShareProvider, IShareService, IShareableItem } from 'vs/workbench/contrib/share/common/share';
11+
import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers';
12+
13+
@extHostNamedCustomer(MainContext.MainThreadShare)
14+
export class MainThreadShare implements MainThreadShareShape {
15+
16+
private readonly proxy: ExtHostShareShape;
17+
private providers = new Map<number, IShareProvider>();
18+
private providerDisposables = new Map<number, IDisposable>();
19+
20+
constructor(
21+
extHostContext: IExtHostContext,
22+
@IShareService private readonly shareService: IShareService
23+
) {
24+
this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostShare);
25+
}
26+
27+
$registerShareProvider(handle: number, selector: IDocumentFilterDto[], id: string, label: string): void {
28+
const provider: IShareProvider = {
29+
id,
30+
label,
31+
selector,
32+
provideShare: async (item: IShareableItem) => {
33+
return URI.revive(await this.proxy.$provideShare(handle, item, new CancellationTokenSource().token));
34+
}
35+
};
36+
this.providers.set(handle, provider);
37+
const disposable = this.shareService.registerShareProvider(provider);
38+
this.providerDisposables.set(handle, disposable);
39+
}
40+
41+
$unregisterShareProvider(handle: number): void {
42+
if (this.providers.has(handle)) {
43+
this.providers.delete(handle);
44+
}
45+
if (this.providerDisposables.has(handle)) {
46+
this.providerDisposables.delete(handle);
47+
}
48+
}
49+
50+
dispose(): void {
51+
this.providers.clear();
52+
dispose(this.providerDisposables.values());
53+
this.providerDisposables.clear();
54+
}
55+
}

0 commit comments

Comments
 (0)