Initial share provider API and UI (#182999)

* Formalize share provider API

* i18n.resources.json

* Don't introduce a generic Success dialog severity
This commit is contained in:
Joyce Er 2023-05-22 15:32:43 -07:00 committed by GitHub
parent d470f53f49
commit bc1090cc10
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 529 additions and 4 deletions

View file

@ -510,6 +510,10 @@
"name": "vs/workbench/services/localization",
"project": "vscode-workbench"
},
{
"name": "vs/workbench/contrib/share",
"project": "vscode-workbench"
},
{
"name": "vs/workbench/contrib/accessibility",
"project": "vscode-workbench"

View file

@ -457,6 +457,7 @@
"--vscode-problemsErrorIcon-foreground",
"--vscode-problemsInfoIcon-foreground",
"--vscode-problemsWarningIcon-foreground",
"--vscode-problemsSuccessIcon-foreground",
"--vscode-profileBadge-background",
"--vscode-profileBadge-foreground",
"--vscode-progressBar-background",

View file

@ -28,7 +28,8 @@
"enabledApiProposals": [
"contribShareMenu",
"contribEditSessions",
"canonicalUriProvider"
"canonicalUriProvider",
"shareProvider"
],
"contributes": {
"commands": [

View file

@ -14,6 +14,7 @@ import { GitBaseExtension } from './typings/git-base';
import { GithubRemoteSourcePublisher } from './remoteSourcePublisher';
import { GithubBranchProtectionProviderManager } from './branchProtection';
import { GitHubCanonicalUriProvider } from './canonicalUriProvider';
import { VscodeDevShareProvider } from './shareProviders';
export function activate(context: ExtensionContext): void {
const disposables: Disposable[] = [];
@ -95,6 +96,7 @@ function initializeGitExtension(context: ExtensionContext, logger: LogOutputChan
disposables.add(gitAPI.registerPushErrorHandler(new GithubPushErrorHandler()));
disposables.add(gitAPI.registerRemoteSourcePublisher(new GithubRemoteSourcePublisher(gitAPI)));
disposables.add(new GitHubCanonicalUriProvider(gitAPI));
disposables.add(new VscodeDevShareProvider(gitAPI));
setGitHubContext(gitAPI, disposables);
commands.executeCommand('setContext', 'git-base.gitEnabled', true);

View file

@ -92,7 +92,7 @@ function getRangeOrSelection(lineNumber: number | undefined) {
: vscode.window.activeTextEditor?.selection;
}
function rangeString(range: vscode.Range | undefined) {
export function rangeString(range: vscode.Range | undefined) {
if (!range) {
return '';
}
@ -119,7 +119,7 @@ export function notebookCellRangeString(index: number | undefined, range: vscode
return hash;
}
function encodeURIComponentExceptSlashes(path: string) {
export function encodeURIComponentExceptSlashes(path: string) {
// There may be special characters like # and whitespace in the path.
// These characters are not escaped by encodeURI(), so it is not sufficient to
// feed the full URI to encodeURI().

View file

@ -0,0 +1,111 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { API } from './typings/git';
import { getRepositoryFromUrl, repositoryHasGitHubRemote } from './util';
import { encodeURIComponentExceptSlashes, getRepositoryForFile, notebookCellRangeString, rangeString } from './links';
export class VscodeDevShareProvider implements vscode.ShareProvider, vscode.Disposable {
readonly id: string = 'copyVscodeDevLink';
readonly label: string = vscode.l10n.t('Copy vscode.dev Link');
readonly priority: number = 10;
private _hasGitHubRepositories: boolean = false;
private set hasGitHubRepositories(value: boolean) {
vscode.commands.executeCommand('setContext', 'github.hasGitHubRepo', value);
this._hasGitHubRepositories = value;
this.ensureShareProviderRegistration();
}
private shareProviderRegistration: vscode.Disposable | undefined;
private disposables: vscode.Disposable[] = [];
constructor(private readonly gitAPI: API) {
this.initializeGitHubRepoContext();
}
dispose() {
this.disposables.forEach(d => d.dispose());
}
private initializeGitHubRepoContext() {
if (this.gitAPI.repositories.find(repo => repositoryHasGitHubRemote(repo))) {
this.hasGitHubRepositories = true;
vscode.commands.executeCommand('setContext', 'github.hasGitHubRepo', true);
} else {
this.disposables.push(this.gitAPI.onDidOpenRepository(async e => {
await e.status();
if (repositoryHasGitHubRemote(e)) {
vscode.commands.executeCommand('setContext', 'github.hasGitHubRepo', true);
this.hasGitHubRepositories = true;
}
}));
}
this.disposables.push(this.gitAPI.onDidCloseRepository(() => {
if (!this.gitAPI.repositories.find(repo => repositoryHasGitHubRemote(repo))) {
this.hasGitHubRepositories = false;
}
}));
}
private ensureShareProviderRegistration() {
if (vscode.env.appHost !== 'codespaces' && !this.shareProviderRegistration && this._hasGitHubRepositories) {
const shareProviderRegistration = vscode.window.registerShareProvider({ scheme: 'file' }, this);
this.shareProviderRegistration = shareProviderRegistration;
this.disposables.push(shareProviderRegistration);
} else if (this.shareProviderRegistration && !this._hasGitHubRepositories) {
this.shareProviderRegistration.dispose();
this.shareProviderRegistration = undefined;
}
}
provideShare(item: vscode.ShareableItem, _token: vscode.CancellationToken): vscode.ProviderResult<vscode.Uri> {
const repository = getRepositoryForFile(this.gitAPI, item.resourceUri);
if (!repository) {
return;
}
let repo: { owner: string; repo: string } | undefined;
repository.state.remotes.find(remote => {
if (remote.fetchUrl) {
const foundRepo = getRepositoryFromUrl(remote.fetchUrl);
if (foundRepo && (remote.name === repository.state.HEAD?.upstream?.remote)) {
repo = foundRepo;
return;
} else if (foundRepo && !repo) {
repo = foundRepo;
}
}
return;
});
if (!repo) {
return;
}
const blobSegment = repository?.state.HEAD?.name ? encodeURIComponentExceptSlashes(repository.state.HEAD?.name) : repository?.state.HEAD?.commit;
const filepathSegment = encodeURIComponentExceptSlashes(item.resourceUri.path.substring(repository?.rootUri.path.length));
const rangeSegment = getRangeSegment(item);
return vscode.Uri.parse(`${this.getVscodeDevHost()}/${repo.owner}/${repo.repo}/blob/${blobSegment}${filepathSegment}${rangeSegment}${rangeSegment}`);
}
private getVscodeDevHost(): string {
return `https://${vscode.env.appName.toLowerCase().includes('insiders') ? 'insiders.' : ''}vscode.dev/github`;
}
}
function getRangeSegment(item: vscode.ShareableItem) {
if (item.resourceUri.scheme === 'vscode-notebook-cell') {
const notebookEditor = vscode.window.visibleNotebookEditors.find(editor => editor.notebook.uri.fsPath === item.resourceUri.fsPath);
const cell = notebookEditor?.notebook.getCells().find(cell => cell.document.uri.fragment === item.resourceUri?.fragment);
const cellIndex = cell?.index ?? notebookEditor?.selection.start;
return notebookCellRangeString(cellIndex, item.selection);
}
return rangeString(item.selection);
}

View file

@ -0,0 +1,29 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// https://github.com/microsoft/vscode/issues/176316
declare module 'vscode' {
export interface TreeItem {
shareableItem?: ShareableItem;
}
export interface ShareableItem {
resourceUri: Uri;
selection?: Range;
}
export interface ShareProvider {
readonly id: string;
readonly label: string;
readonly priority: number;
provideShare(item: ShareableItem, token: CancellationToken): ProviderResult<Uri>;
}
export namespace window {
export function registerShareProvider(selector: DocumentSelector, provider: ShareProvider): Disposable;
}
}

View file

@ -98,9 +98,11 @@ export class MenuId {
static readonly MenubarViewMenu = new MenuId('MenubarViewMenu');
static readonly MenubarHomeMenu = new MenuId('MenubarHomeMenu');
static readonly OpenEditorsContext = new MenuId('OpenEditorsContext');
static readonly OpenEditorsContextShare = new MenuId('OpenEditorsContextShare');
static readonly ProblemsPanelContext = new MenuId('ProblemsPanelContext');
static readonly SCMChangeContext = new MenuId('SCMChangeContext');
static readonly SCMResourceContext = new MenuId('SCMResourceContext');
static readonly SCMResourceContextShare = new MenuId('SCMResourceContextShare');
static readonly SCMResourceFolderContext = new MenuId('SCMResourceFolderContext');
static readonly SCMResourceGroupContext = new MenuId('SCMResourceGroupContext');
static readonly SCMSourceControl = new MenuId('SCMSourceControl');

View file

@ -82,6 +82,7 @@ import './mainThreadAuthentication';
import './mainThreadTimeline';
import './mainThreadTesting';
import './mainThreadSecretState';
import './mainThreadShare';
import './mainThreadProfilContentHandlers';
import './mainThreadSemanticSimilarity';
import './mainThreadIssueReporter';

View file

@ -0,0 +1,55 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { ExtHostContext, ExtHostShareShape, IDocumentFilterDto, MainContext, MainThreadShareShape } from 'vs/workbench/api/common/extHost.protocol';
import { IShareProvider, IShareService, IShareableItem } from 'vs/workbench/contrib/share/common/share';
import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers';
@extHostNamedCustomer(MainContext.MainThreadShare)
export class MainThreadShare implements MainThreadShareShape {
private readonly proxy: ExtHostShareShape;
private providers = new Map<number, IShareProvider>();
private providerDisposables = new Map<number, IDisposable>();
constructor(
extHostContext: IExtHostContext,
@IShareService private readonly shareService: IShareService
) {
this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostShare);
}
$registerShareProvider(handle: number, selector: IDocumentFilterDto[], id: string, label: string): void {
const provider: IShareProvider = {
id,
label,
selector,
provideShare: async (item: IShareableItem) => {
return URI.revive(await this.proxy.$provideShare(handle, item, new CancellationTokenSource().token));
}
};
this.providers.set(handle, provider);
const disposable = this.shareService.registerShareProvider(provider);
this.providerDisposables.set(handle, disposable);
}
$unregisterShareProvider(handle: number): void {
if (this.providers.has(handle)) {
this.providers.delete(handle);
}
if (this.providerDisposables.has(handle)) {
this.providerDisposables.delete(handle);
}
}
dispose(): void {
this.providers.clear();
dispose(this.providerDisposables.values());
this.providerDisposables.clear();
}
}

View file

@ -103,6 +103,7 @@ import { ExtHostNotebookDocumentSaveParticipant } from 'vs/workbench/api/common/
import { ExtHostSemanticSimilarity } from 'vs/workbench/api/common/extHostSemanticSimilarity';
import { ExtHostIssueReporter } from 'vs/workbench/api/common/extHostIssueReporter';
import { IExtHostManagedSockets } from 'vs/workbench/api/common/extHostManagedSockets';
import { ExtHostShare } from 'vs/workbench/api/common/extHostShare';
export interface IExtensionRegistries {
mine: ExtensionDescriptionRegistry;
@ -186,6 +187,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
const extHostQuickOpen = rpcProtocol.set(ExtHostContext.ExtHostQuickOpen, createExtHostQuickOpen(rpcProtocol, extHostWorkspace, extHostCommands));
const extHostSCM = rpcProtocol.set(ExtHostContext.ExtHostSCM, new ExtHostSCM(rpcProtocol, extHostCommands, extHostDocuments, extHostLogService));
const extHostQuickDiff = rpcProtocol.set(ExtHostContext.ExtHostQuickDiff, new ExtHostQuickDiff(rpcProtocol, uriTransformer));
const extHostShare = rpcProtocol.set(ExtHostContext.ExtHostShare, new ExtHostShare(rpcProtocol, uriTransformer));
const extHostComment = rpcProtocol.set(ExtHostContext.ExtHostComments, createExtHostComments(rpcProtocol, extHostCommands, extHostDocuments));
const extHostProgress = rpcProtocol.set(ExtHostContext.ExtHostProgress, new ExtHostProgress(rpcProtocol.getProxy(MainContext.MainThreadProgress)));
const extHostLabelService = rpcProtocol.set(ExtHostContext.ExtHosLabelService, new ExtHostLabelService(rpcProtocol));
@ -842,6 +844,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
},
get tabGroups(): vscode.TabGroups {
return extHostEditorTabs.tabGroups;
},
registerShareProvider(selector: vscode.DocumentSelector, provider: vscode.ShareProvider): vscode.Disposable {
checkProposedApiEnabled(extension, 'shareProvider');
return extHostShare.registerShareProvider(checkSelector(selector), provider);
}
};

View file

@ -362,6 +362,11 @@ export interface IDocumentFilterDto {
isBuiltin?: boolean;
}
export interface IShareableItemDto {
resourceUri: UriComponents;
range?: IRange;
}
export interface ISignatureHelpProviderMetadataDto {
readonly triggerCharacters: readonly string[];
readonly retriggerCharacters: readonly string[];
@ -1247,6 +1252,11 @@ export interface MainThreadSearchShape extends IDisposable {
$handleTelemetry(eventName: string, data: any): void;
}
export interface MainThreadShareShape extends IDisposable {
$registerShareProvider(handle: number, selector: IDocumentFilterDto[], id: string, label: string): void;
$unregisterShareProvider(handle: number): void;
}
export interface MainThreadTaskShape extends IDisposable {
$createTaskId(task: tasks.ITaskDTO): Promise<string>;
$registerTaskProvider(handle: number, type: string): Promise<void>;
@ -2009,6 +2019,10 @@ export interface ExtHostQuickDiffShape {
$provideOriginalResource(sourceControlHandle: number, uri: UriComponents, token: CancellationToken): Promise<UriComponents | null>;
}
export interface ExtHostShareShape {
$provideShare(handle: number, shareableItem: IShareableItemDto, token: CancellationToken): Promise<UriComponents | undefined>;
}
export interface ExtHostTaskShape {
$provideTasks(handle: number, validTypes: { [key: string]: boolean }): Promise<tasks.ITaskSetDTO>;
$resolveTask(handle: number, taskDTO: tasks.ITaskDTO): Promise<tasks.ITaskDTO | undefined>;
@ -2525,6 +2539,7 @@ export const MainContext = {
MainThreadExtensionService: createProxyIdentifier<MainThreadExtensionServiceShape>('MainThreadExtensionService'),
MainThreadSCM: createProxyIdentifier<MainThreadSCMShape>('MainThreadSCM'),
MainThreadSearch: createProxyIdentifier<MainThreadSearchShape>('MainThreadSearch'),
MainThreadShare: createProxyIdentifier<MainThreadShareShape>('MainThreadShare'),
MainThreadTask: createProxyIdentifier<MainThreadTaskShape>('MainThreadTask'),
MainThreadWindow: createProxyIdentifier<MainThreadWindowShape>('MainThreadWindow'),
MainThreadLabelService: createProxyIdentifier<MainThreadLabelServiceShape>('MainThreadLabelService'),
@ -2565,6 +2580,7 @@ export const ExtHostContext = {
ExtHostLanguageFeatures: createProxyIdentifier<ExtHostLanguageFeaturesShape>('ExtHostLanguageFeatures'),
ExtHostQuickOpen: createProxyIdentifier<ExtHostQuickOpenShape>('ExtHostQuickOpen'),
ExtHostQuickDiff: createProxyIdentifier<ExtHostQuickDiffShape>('ExtHostQuickDiff'),
ExtHostShare: createProxyIdentifier<ExtHostShareShape>('ExtHostShare'),
ExtHostExtensionService: createProxyIdentifier<ExtHostExtensionServiceShape>('ExtHostExtensionService'),
ExtHostLogLevelServiceShape: createProxyIdentifier<ExtHostLogLevelServiceShape>('ExtHostLogLevelServiceShape'),
ExtHostTerminalService: createProxyIdentifier<ExtHostTerminalServiceShape>('ExtHostTerminalService'),

View file

@ -0,0 +1,43 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type * as vscode from 'vscode';
import { ExtHostShareShape, IMainContext, IShareableItemDto, MainContext, MainThreadShareShape } from 'vs/workbench/api/common/extHost.protocol';
import { DocumentSelector, Range } from 'vs/workbench/api/common/extHostTypeConverters';
import { IURITransformer } from 'vs/base/common/uriIpc';
import { CancellationToken } from 'vs/base/common/cancellation';
import { URI, UriComponents } from 'vs/base/common/uri';
export class ExtHostShare implements ExtHostShareShape {
private static handlePool: number = 0;
private proxy: MainThreadShareShape;
private providers: Map<number, vscode.ShareProvider> = new Map();
constructor(
mainContext: IMainContext,
private readonly uriTransformer: IURITransformer | undefined
) {
this.proxy = mainContext.getProxy(MainContext.MainThreadShare);
}
async $provideShare(handle: number, shareableItem: IShareableItemDto, token: CancellationToken): Promise<UriComponents | undefined> {
const provider = this.providers.get(handle);
const result = await provider?.provideShare({ selection: Range.to(shareableItem.range), resourceUri: URI.revive(shareableItem.resourceUri) }, token);
return result ?? undefined;
}
registerShareProvider(selector: vscode.DocumentSelector, provider: vscode.ShareProvider): vscode.Disposable {
const handle = ExtHostShare.handlePool++;
this.providers.set(handle, provider);
this.proxy.$registerShareProvider(handle, DocumentSelector.from(selector, this.uriTransformer), provider.id, provider.label);
return {
dispose: () => {
this.proxy.$unregisterShareProvider(handle);
this.providers.delete(handle);
}
};
}
}

View file

@ -71,6 +71,12 @@ MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, {
command: revealInOsCommand,
when: REVEAL_IN_OS_WHEN_CONTEXT
});
MenuRegistry.appendMenuItem(MenuId.OpenEditorsContextShare, {
title: nls.localize('miShare', "Share"),
submenu: MenuId.MenubarShare,
group: 'share',
order: 3,
});
// Menu registration - explorer

View file

@ -7,7 +7,7 @@ import 'vs/css!./media/scm';
import { Emitter } from 'vs/base/common/event';
import { IDisposable, DisposableStore, dispose } from 'vs/base/common/lifecycle';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions';
import { IMenuService, MenuId, IMenu, MenuRegistry } from 'vs/platform/actions/common/actions';
import { IAction } from 'vs/base/common/actions';
import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { ISCMResource, ISCMResourceGroup, ISCMProvider, ISCMRepository, ISCMService, ISCMMenus, ISCMRepositoryMenus } from 'vs/workbench/contrib/scm/common/scm';
@ -15,6 +15,7 @@ import { equals } from 'vs/base/common/arrays';
import { ISplice } from 'vs/base/common/sequence';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { localize } from 'vs/nls';
function actionEquals(a: IAction, b: IAction): boolean {
return a.id === b.id;
@ -266,3 +267,10 @@ export class SCMMenus implements ISCMMenus, IDisposable {
this.disposables.dispose();
}
}
MenuRegistry.appendMenuItem(MenuId.SCMResourceContext, {
title: localize('miShare', "Share"),
submenu: MenuId.SCMResourceContextShare,
group: '45_share',
order: 3,
});

View file

@ -0,0 +1,110 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { Codicon } from 'vs/base/common/codicons';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { localize } from 'vs/nls';
import { Action2, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { Severity } from 'vs/platform/notification/common/notification';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { Registry } from 'vs/platform/registry/common/platform';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { WorkspaceFolderCountContext } from 'vs/workbench/common/contextkeys';
import { Extensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions';
import { ShareProviderCountContext, ShareService } from 'vs/workbench/contrib/share/browser/shareService';
import { IShareService } from 'vs/workbench/contrib/share/common/share';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
const targetMenus = [
MenuId.EditorContextShare,
MenuId.SCMResourceContextShare,
MenuId.OpenEditorsContextShare,
MenuId.EditorTitleContextShare,
MenuId.MenubarShare,
// MenuId.EditorLineNumberContext, // todo@joyceerhl add share
MenuId.ExplorerContextShare
];
class ShareWorkbenchContribution {
private static SHARE_ENABLED_SETTING = 'workbench.experimental.share.enabled';
constructor(
@IShareService private readonly shareService: IShareService,
@IConfigurationService private readonly configurationService: IConfigurationService
) {
if (this.configurationService.getValue<boolean>(ShareWorkbenchContribution.SHARE_ENABLED_SETTING)) {
this.registerActions();
}
}
private registerActions() {
registerAction2(class ShareAction extends Action2 {
static readonly ID = 'workbench.action.share';
static readonly LABEL = localize('share', 'Share...');
constructor() {
super({
id: ShareAction.ID,
title: { value: ShareAction.LABEL, original: 'Share...' },
f1: true,
icon: Codicon.linkExternal,
precondition: ContextKeyExpr.and(ShareProviderCountContext.notEqualsTo(0), WorkspaceFolderCountContext.notEqualsTo(0)),
keybinding: {
weight: KeybindingWeight.WorkbenchContrib,
primary: KeyMod.Alt | KeyMod.CtrlCmd | KeyCode.KeyS,
},
menu: [
{ id: MenuId.CommandCenter, order: 1000 }
]
});
}
override async run(accessor: ServicesAccessor, ...args: any[]): Promise<void> {
const shareService = accessor.get(IShareService);
const resourceUri = accessor.get(IWorkspaceContextService).getWorkspace().folders[0].uri;
const clipboardService = accessor.get(IClipboardService);
const dialogService = accessor.get(IDialogService);
const urlService = accessor.get(IOpenerService);
const uri = await shareService.provideShare({ resourceUri }, new CancellationTokenSource().token);
if (uri) {
await clipboardService.writeText(uri.toString());
const result = await dialogService.input(
{
type: Severity.Info,
inputs: [{ type: 'text', value: uri.toString() }],
message: localize('shareSuccess', 'Copied link to clipboard!'),
custom: { icon: Codicon.check },
primaryButton: localize('open link', 'Open Link')
}
);
if (result.confirmed) {
urlService.open(uri, { openExternal: true });
}
}
}
});
const actions = this.shareService.getShareActions();
for (const menuId of targetMenus) {
for (const action of actions) {
// todo@joyceerhl avoid duplicates
MenuRegistry.appendMenuItem(menuId, action);
}
}
}
}
registerSingleton(IShareService, ShareService, InstantiationType.Delayed);
const workbenchContributionsRegistry = Registry.as<IWorkbenchContributionsRegistry>(Extensions.Workbench);
workbenchContributionsRegistry.registerWorkbenchContribution(ShareWorkbenchContribution, LifecyclePhase.Eventually);

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 { IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { ISubmenuItem } from 'vs/platform/actions/common/actions';
import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { ILabelService } from 'vs/platform/label/common/label';
import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
import { IShareProvider, IShareService, IShareableItem } from 'vs/workbench/contrib/share/common/share';
export const ShareProviderCountContext = new RawContextKey<number>('shareProviderCount', 0, localize('shareProviderCount', "The number of available share providers"));
export class ShareService implements IShareService {
readonly _serviceBrand: undefined;
readonly providerCount: IContextKey<number>;
private readonly _providers = new Set<IShareProvider>();
constructor(
@IContextKeyService private contextKeyService: IContextKeyService,
@ILabelService private readonly labelService: ILabelService,
@IQuickInputService private quickInputService: IQuickInputService
) {
this.providerCount = ShareProviderCountContext.bindTo(this.contextKeyService);
}
registerShareProvider(provider: IShareProvider): IDisposable {
this._providers.add(provider);
this.providerCount.set(this._providers.size);
return {
dispose: () => {
this._providers.delete(provider);
this.providerCount.set(this._providers.size);
}
};
}
getShareActions(): ISubmenuItem[] {
// todo@joyceerhl return share actions
return [];
}
async provideShare(item: IShareableItem, token: CancellationToken): Promise<URI | undefined> {
const providers = [...this._providers.values()];
if (providers.length === 0) {
return undefined;
}
if (providers.length === 1) {
return providers[0].provideShare(item, token);
}
const items: (IQuickPickItem & { provider: IShareProvider })[] = providers.map((p) => ({ label: p.label, provider: p }));
const selected = await this.quickInputService.pick(items, { canPickMany: false, placeHolder: localize('type to filter', 'Choose how to share {0}', this.labelService.getUriLabel(item.resourceUri)) }, token);
return selected?.provider.provideShare(item, token);
}
}

View file

@ -0,0 +1,34 @@
/*---------------------------------------------------------------------------------------------
* 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 { IDisposable } from 'vs/base/common/lifecycle';
import { IRange } from 'vs/base/common/range';
import { URI } from 'vs/base/common/uri';
import { LanguageSelector } from 'vs/editor/common/languageSelector';
import { ISubmenuItem } from 'vs/platform/actions/common/actions';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export interface IShareableItem {
resourceUri: URI;
location?: IRange;
}
export interface IShareProvider {
readonly id: string;
readonly label: string;
readonly selector: LanguageSelector;
prepareShare?(item: IShareableItem, token: CancellationToken): Thenable<boolean | undefined>;
provideShare(item: IShareableItem, token: CancellationToken): Thenable<URI | undefined>;
}
export const IShareService = createDecorator<IShareService>('shareService');
export interface IShareService {
_serviceBrand: undefined;
registerShareProvider(provider: IShareProvider): IDisposable;
getShareActions(): ISubmenuItem[];
provideShare(item: IShareableItem, token: CancellationToken): Thenable<URI | undefined>;
}

View file

@ -73,6 +73,7 @@ export const allApiProposals = Object.freeze({
scmTextDocument: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmTextDocument.d.ts',
scmValidation: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmValidation.d.ts',
semanticSimilarity: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.semanticSimilarity.d.ts',
shareProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.shareProvider.d.ts',
showLocal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.showLocal.d.ts',
tabInputTextMerge: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts',
taskPresentationGroup: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.taskPresentationGroup.d.ts',

View file

@ -368,4 +368,7 @@ import 'vs/workbench/contrib/bracketPairColorizer2Telemetry/browser/bracketPairC
// Accessibility
import 'vs/workbench/contrib/accessibility/browser/accessibility.contribution';
// Share
import 'vs/workbench/contrib/share/browser/share.contribution';
//#endregion

View file

@ -0,0 +1,29 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// https://github.com/microsoft/vscode/issues/176316
declare module 'vscode' {
export interface TreeItem {
shareableItem?: ShareableItem;
}
export interface ShareableItem {
resourceUri: Uri;
selection?: Range;
}
export interface ShareProvider {
readonly id: string;
readonly label: string;
readonly priority: number;
provideShare(item: ShareableItem, token: CancellationToken): ProviderResult<Uri>;
}
export namespace window {
export function registerShareProvider(selector: DocumentSelector, provider: ShareProvider): Disposable;
}
}