diff --git a/extensions/git/package.json b/extensions/git/package.json index 1fee1f7ab85..e13864027b8 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -13,6 +13,7 @@ "diffCommand", "contribEditorContentMenu", "contribEditSessions", + "canonicalUriIdentityProvider", "contribViewsWelcome", "editSessionIdentityProvider", "quickDiffProvider", diff --git a/extensions/git/src/editSessionIdentityProvider.ts b/extensions/git/src/editSessionIdentityProvider.ts index 3212b80f37b..10ebbf349e4 100644 --- a/extensions/git/src/editSessionIdentityProvider.ts +++ b/extensions/git/src/editSessionIdentityProvider.ts @@ -24,7 +24,7 @@ export class GitEditSessionIdentityProvider implements vscode.EditSessionIdentit this.providerRegistration.dispose(); } - async provideEditSessionIdentity(workspaceFolder: vscode.WorkspaceFolder, _token: vscode.CancellationToken): Promise { + async provideEditSessionIdentity(workspaceFolder: vscode.WorkspaceFolder, token: vscode.CancellationToken): Promise { await this.model.openRepository(path.dirname(workspaceFolder.uri.fsPath)); const repository = this.model.getRepository(workspaceFolder.uri); @@ -34,8 +34,11 @@ export class GitEditSessionIdentityProvider implements vscode.EditSessionIdentit return undefined; } + const remoteUrl = repository.remotes.find((remote) => remote.name === repository.HEAD?.upstream?.remote)?.pushUrl?.replace(/^(git@[^\/:]+)(:)/i, 'ssh://$1/'); + const remote = remoteUrl ? await vscode.workspace.provideCanonicalUriIdentity(vscode.Uri.parse(remoteUrl), token) : null; + return JSON.stringify({ - remote: repository.remotes.find((remote) => remote.name === repository.HEAD?.upstream?.remote)?.pushUrl ?? null, + remote: remote?.toString() ?? remoteUrl, ref: repository.HEAD?.upstream?.name ?? null, sha: repository.HEAD?.commit ?? null, }); diff --git a/extensions/git/src/typings/vscode.proposed.canonicalUriIdentityProvider.d.ts b/extensions/git/src/typings/vscode.proposed.canonicalUriIdentityProvider.d.ts new file mode 100644 index 00000000000..3a61ca15798 --- /dev/null +++ b/extensions/git/src/typings/vscode.proposed.canonicalUriIdentityProvider.d.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * 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' { + + // https://github.com/microsoft/vscode/issues/180582 + + export namespace workspace { + /** + * + * @param scheme The URI scheme that this provider can provide canonical URI identities for. + * A canonical URI represents the conversion of a resource's alias into a source of truth URI. + * Multiple aliases may convert to the same source of truth URI. + * @param provider A provider which can convert URIs for workspace folders of scheme @param scheme to + * a canonical URI identifier which is stable across machines. + */ + export function registerCanonicalUriIdentityProvider(scheme: string, provider: CanonicalUriIdentityProvider): Disposable; + + /** + * + * @param uri The URI to provide a canonical URI identity for. + * @param token A cancellation token for the request. + */ + export function provideCanonicalUriIdentity(uri: Uri, token: CancellationToken): ProviderResult; + } + + export interface CanonicalUriIdentityProvider { + /** + * + * @param uri The URI to provide a canonical URI identity for. + * @param token A cancellation token for the request. + * @returns The canonical URI identity for the requested URI. + */ + provideCanonicalUriIdentity(uri: Uri, token: CancellationToken): ProviderResult; + } +} diff --git a/extensions/github/package.json b/extensions/github/package.json index c86b0495b43..3da6c53904c 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -27,7 +27,8 @@ }, "enabledApiProposals": [ "contribShareMenu", - "contribEditSessions" + "contribEditSessions", + "canonicalUriIdentityProvider" ], "contributes": { "commands": [ diff --git a/extensions/github/src/canonicalUriIdentityProvider.ts b/extensions/github/src/canonicalUriIdentityProvider.ts new file mode 100644 index 00000000000..89c9585c1f6 --- /dev/null +++ b/extensions/github/src/canonicalUriIdentityProvider.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken, CanonicalUriIdentityProvider, Disposable, Uri, workspace } from 'vscode'; + +const SUPPORTED_SCHEMES = ['ssh', 'https']; + +export class GitHubCanonicalUriIdentityProvider implements CanonicalUriIdentityProvider { + + private disposables: Disposable[] = []; + constructor() { + this.disposables.push(...SUPPORTED_SCHEMES.map((scheme) => workspace.registerCanonicalUriIdentityProvider(scheme, this))); + } + + dispose() { this.disposables.forEach((disposable) => disposable.dispose()); } + + async provideCanonicalUriIdentity(uri: Uri, _token: CancellationToken): Promise { + switch (uri.scheme) { + case 'ssh': + // if this is a git@github.com URI, return the HTTPS equivalent + if (uri.authority === 'git@github.com') { + const [owner, repo] = (uri.path.endsWith('.git') ? uri.path.slice(0, -4) : uri.path).split('/').filter((segment) => segment.length > 0); + return Uri.parse(`https://github.com/${owner}/${repo}`); + } + break; + case 'https': + if (uri.authority === 'github.com') { + return uri; + } + break; + } + + return undefined; + } +} diff --git a/extensions/github/src/extension.ts b/extensions/github/src/extension.ts index 4d183b8939e..26ed3bb39b7 100644 --- a/extensions/github/src/extension.ts +++ b/extensions/github/src/extension.ts @@ -13,6 +13,7 @@ import { GithubPushErrorHandler } from './pushErrorHandler'; import { GitBaseExtension } from './typings/git-base'; import { GithubRemoteSourcePublisher } from './remoteSourcePublisher'; import { GithubBranchProtectionProviderManager } from './branchProtection'; +import { GitHubCanonicalUriIdentityProvider } from './canonicalUriIdentityProvider'; export function activate(context: ExtensionContext): void { const disposables: Disposable[] = []; @@ -29,6 +30,7 @@ export function activate(context: ExtensionContext): void { disposables.push(initializeGitBaseExtension()); disposables.push(initializeGitExtension(context, logger)); + disposables.push(new GitHubCanonicalUriIdentityProvider()); } function initializeGitBaseExtension(): Disposable { diff --git a/extensions/github/src/typings/vscode.proposed.canonicalUriIdentityProvider.d.ts b/extensions/github/src/typings/vscode.proposed.canonicalUriIdentityProvider.d.ts new file mode 100644 index 00000000000..3a61ca15798 --- /dev/null +++ b/extensions/github/src/typings/vscode.proposed.canonicalUriIdentityProvider.d.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * 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' { + + // https://github.com/microsoft/vscode/issues/180582 + + export namespace workspace { + /** + * + * @param scheme The URI scheme that this provider can provide canonical URI identities for. + * A canonical URI represents the conversion of a resource's alias into a source of truth URI. + * Multiple aliases may convert to the same source of truth URI. + * @param provider A provider which can convert URIs for workspace folders of scheme @param scheme to + * a canonical URI identifier which is stable across machines. + */ + export function registerCanonicalUriIdentityProvider(scheme: string, provider: CanonicalUriIdentityProvider): Disposable; + + /** + * + * @param uri The URI to provide a canonical URI identity for. + * @param token A cancellation token for the request. + */ + export function provideCanonicalUriIdentity(uri: Uri, token: CancellationToken): ProviderResult; + } + + export interface CanonicalUriIdentityProvider { + /** + * + * @param uri The URI to provide a canonical URI identity for. + * @param token A cancellation token for the request. + * @returns The canonical URI identity for the requested URI. + */ + provideCanonicalUriIdentity(uri: Uri, token: CancellationToken): ProviderResult; + } +} diff --git a/src/vs/platform/workspace/common/canonicalUriIdentity.ts b/src/vs/platform/workspace/common/canonicalUriIdentity.ts new file mode 100644 index 00000000000..aa781962a7e --- /dev/null +++ b/src/vs/platform/workspace/common/canonicalUriIdentity.ts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * 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, UriComponents } from 'vs/base/common/uri'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export interface ICanonicalUriIdentityProvider { + readonly scheme: string; + provideCanonicalUriIdentity(uri: UriComponents, token: CancellationToken): Promise; +} + +export const ICanonicalUriIdentityService = createDecorator('canonicalUriIdentityService'); + +export interface ICanonicalUriIdentityService { + readonly _serviceBrand: undefined; + registerCanonicalUriIdentityProvider(provider: ICanonicalUriIdentityProvider): IDisposable; +} diff --git a/src/vs/workbench/api/browser/mainThreadWorkspace.ts b/src/vs/workbench/api/browser/mainThreadWorkspace.ts index 1932e5633b4..304141dcca9 100644 --- a/src/vs/workbench/api/browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/browser/mainThreadWorkspace.ts @@ -28,6 +28,7 @@ import { ExtHostContext, ExtHostWorkspaceShape, ITextSearchComplete, IWorkspaceD import { IEditSessionIdentityService } from 'vs/platform/workspace/common/editSessions'; import { EditorResourceAccessor, SaveReason, SideBySideEditor } from 'vs/workbench/common/editor'; import { coalesce, firstOrDefault } from 'vs/base/common/arrays'; +import { ICanonicalUriIdentityService } from 'vs/platform/workspace/common/canonicalUriIdentity'; @extHostNamedCustomer(MainContext.MainThreadWorkspace) export class MainThreadWorkspace implements MainThreadWorkspaceShape { @@ -42,6 +43,7 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { @ISearchService private readonly _searchService: ISearchService, @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, @IEditSessionIdentityService private readonly _editSessionIdentityService: IEditSessionIdentityService, + @ICanonicalUriIdentityService private readonly _canonicalUriIdentityService: ICanonicalUriIdentityService, @IEditorService private readonly _editorService: IEditorService, @IWorkspaceEditingService private readonly _workspaceEditingService: IWorkspaceEditingService, @INotificationService private readonly _notificationService: INotificationService, @@ -269,4 +271,29 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { disposable?.dispose(); this.registeredEditSessionProviders.delete(handle); } + + // --- canonical uri identities --- + private registeredCanonicalUriIdentityProviders = new Map(); + + $registerCanonicalUriIdentityProvider(handle: number, scheme: string) { + const disposable = this._canonicalUriIdentityService.registerCanonicalUriIdentityProvider({ + scheme: scheme, + provideCanonicalUriIdentity: async (uri: UriComponents, token: CancellationToken) => { + const result = await this._proxy.$provideCanonicalUriIdentity(uri, token); + if (result) { + return URI.revive(result); + } + return result; + } + }); + + this.registeredCanonicalUriIdentityProviders.set(handle, disposable); + this._toDispose.add(disposable); + } + + $unregisterCanonicalUriIdentityProvider(handle: number) { + const disposable = this.registeredCanonicalUriIdentityProviders.get(handle); + disposable?.dispose(); + this.registeredCanonicalUriIdentityProviders.delete(handle); + } } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index f4770a51dec..fb007d36098 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1097,6 +1097,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'editSessionIdentityProvider'); return extHostWorkspace.getOnWillCreateEditSessionIdentityEvent(extension)(listener, thisArgs, disposables); }, + registerCanonicalUriIdentityProvider: (scheme: string, provider: vscode.CanonicalUriIdentityProvider) => { + checkProposedApiEnabled(extension, 'canonicalUriIdentityProvider'); + return extHostWorkspace.registerCanonicalUriIdentityProvider(scheme, provider); + }, + provideCanonicalUriIdentity: (uri: vscode.Uri, token: vscode.CancellationToken) => { + checkProposedApiEnabled(extension, 'canonicalUriIdentityProvider'); + return extHostWorkspace.provideCanonicalUriIdentity(uri, token); + } }; // namespace: scm diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 5e20100ae03..c109335c636 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1203,6 +1203,8 @@ export interface MainThreadWorkspaceShape extends IDisposable { $requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Promise; $registerEditSessionIdentityProvider(handle: number, scheme: string): void; $unregisterEditSessionIdentityProvider(handle: number): void; + $registerCanonicalUriIdentityProvider(handle: number, scheme: string): void; + $unregisterCanonicalUriIdentityProvider(handle: number): void; } export interface IFileChangeDto { @@ -1549,6 +1551,7 @@ export interface ExtHostWorkspaceShape { $getEditSessionIdentifier(folder: UriComponents, token: CancellationToken): Promise; $provideEditSessionIdentityMatch(folder: UriComponents, identity1: string, identity2: string, token: CancellationToken): Promise; $onWillCreateEditSessionIdentity(folder: UriComponents, token: CancellationToken, timeout: number): Promise; + $provideCanonicalUriIdentity(uri: UriComponents, token: CancellationToken): Promise; } export interface ExtHostFileSystemInfoShape { diff --git a/src/vs/workbench/api/common/extHostWorkspace.ts b/src/vs/workbench/api/common/extHostWorkspace.ts index b691389a3b6..08271f0670d 100644 --- a/src/vs/workbench/api/common/extHostWorkspace.ts +++ b/src/vs/workbench/api/common/extHostWorkspace.ts @@ -696,6 +696,46 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac return undefined; } } + + // --- canonical uri identity --- + + private readonly _canonicalUriIdentityProviders = new Map(); + + // called by ext host + registerCanonicalUriIdentityProvider(scheme: string, provider: vscode.CanonicalUriIdentityProvider) { + if (this._canonicalUriIdentityProviders.has(scheme)) { + throw new Error(`A provider has already been registered for scheme ${scheme}`); + } + + this._canonicalUriIdentityProviders.set(scheme, provider); + const outgoingScheme = this._uriTransformerService.transformOutgoingScheme(scheme); + const handle = this._providerHandlePool++; + this._proxy.$registerCanonicalUriIdentityProvider(handle, outgoingScheme); + + return toDisposable(() => { + this._canonicalUriIdentityProviders.delete(scheme); + this._proxy.$unregisterCanonicalUriIdentityProvider(handle); + }); + } + + async provideCanonicalUriIdentity(uri: URI, cancellationToken: CancellationToken): Promise { + const provider = this._canonicalUriIdentityProviders.get(uri.scheme); + if (!provider) { + return undefined; + } + + const result = await provider.provideCanonicalUriIdentity?.(URI.revive(uri), cancellationToken); + if (!result) { + return undefined; + } + + return result; + } + + // called by main thread + async $provideCanonicalUriIdentity(uri: UriComponents, cancellationToken: CancellationToken): Promise { + return this.provideCanonicalUriIdentity(URI.revive(uri), cancellationToken); + } } export const IExtHostWorkspace = createDecorator('IExtHostWorkspace'); diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 033596491e8..f0958335506 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -8,6 +8,7 @@ export const allApiProposals = Object.freeze({ authGetSessions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authGetSessions.d.ts', authSession: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authSession.d.ts', + canonicalUriIdentityProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.canonicalUriIdentityProvider.d.ts', codiconDecoration: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.codiconDecoration.d.ts', commentsDraftState: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentsDraftState.d.ts', contribCommentEditorActionsMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentEditorActionsMenu.d.ts', diff --git a/src/vs/workbench/services/workspaces/common/canonicalUriIdentityService.ts b/src/vs/workbench/services/workspaces/common/canonicalUriIdentityService.ts new file mode 100644 index 00000000000..d5f47b09d94 --- /dev/null +++ b/src/vs/workbench/services/workspaces/common/canonicalUriIdentityService.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { ICanonicalUriIdentityService, ICanonicalUriIdentityProvider } from 'vs/platform/workspace/common/canonicalUriIdentity'; + +export class CanonicalUriIdentityService implements ICanonicalUriIdentityService { + declare readonly _serviceBrand: undefined; + + private readonly _providers = new Map(); + + registerCanonicalUriIdentityProvider(provider: ICanonicalUriIdentityProvider): IDisposable { + this._providers.set(provider.scheme, provider); + return { + dispose: () => this._providers.delete(provider.scheme) + }; + } + + async provideCanonicalUriIdentity(uri: URI, token: CancellationToken): Promise { + const provider = this._providers.get(uri.scheme); + if (provider) { + return provider.provideCanonicalUriIdentity(uri, token); + } + return undefined; + } +} + +registerSingleton(ICanonicalUriIdentityService, CanonicalUriIdentityService, InstantiationType.Delayed); diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 2aa3bf24396..29987fc9673 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -55,6 +55,7 @@ import 'vs/workbench/browser/parts/views/viewsService'; import 'vs/platform/actions/common/actions.contribution'; import 'vs/platform/undoRedo/common/undoRedoService'; import 'vs/workbench/services/workspaces/common/editSessionIdentityService'; +import 'vs/workbench/services/workspaces/common/canonicalUriIdentityService'; import 'vs/workbench/services/extensions/browser/extensionUrlHandler'; import 'vs/workbench/services/keybinding/common/keybindingEditing'; import 'vs/workbench/services/decorations/browser/decorationsService'; diff --git a/src/vscode-dts/vscode.proposed.canonicalUriIdentityProvider.d.ts b/src/vscode-dts/vscode.proposed.canonicalUriIdentityProvider.d.ts new file mode 100644 index 00000000000..3a61ca15798 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.canonicalUriIdentityProvider.d.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * 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' { + + // https://github.com/microsoft/vscode/issues/180582 + + export namespace workspace { + /** + * + * @param scheme The URI scheme that this provider can provide canonical URI identities for. + * A canonical URI represents the conversion of a resource's alias into a source of truth URI. + * Multiple aliases may convert to the same source of truth URI. + * @param provider A provider which can convert URIs for workspace folders of scheme @param scheme to + * a canonical URI identifier which is stable across machines. + */ + export function registerCanonicalUriIdentityProvider(scheme: string, provider: CanonicalUriIdentityProvider): Disposable; + + /** + * + * @param uri The URI to provide a canonical URI identity for. + * @param token A cancellation token for the request. + */ + export function provideCanonicalUriIdentity(uri: Uri, token: CancellationToken): ProviderResult; + } + + export interface CanonicalUriIdentityProvider { + /** + * + * @param uri The URI to provide a canonical URI identity for. + * @param token A cancellation token for the request. + * @returns The canonical URI identity for the requested URI. + */ + provideCanonicalUriIdentity(uri: Uri, token: CancellationToken): ProviderResult; + } +}