Path: blob/main/extensions/git-base/src/remoteSource.ts
5223 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { QuickPickItem, window, QuickPick, QuickPickItemKind, l10n, Disposable } from 'vscode';6import { RemoteSourceProvider, RemoteSource, PickRemoteSourceOptions, PickRemoteSourceResult, RemoteSourceAction } from './api/git-base';7import { Model } from './model';8import { throttle, debounce } from './decorators';910async function getQuickPickResult<T extends QuickPickItem>(quickpick: QuickPick<T>): Promise<T | undefined> {11const listeners: Disposable[] = [];12const result = await new Promise<T | undefined>(c => {13listeners.push(14quickpick.onDidAccept(() => c(quickpick.selectedItems[0])),15quickpick.onDidHide(() => c(undefined)),16);17quickpick.show();18});1920quickpick.hide();21listeners.forEach(l => l.dispose());22return result;23}2425class RemoteSourceProviderQuickPick implements Disposable {2627private disposables: Disposable[] = [];28private isDisposed: boolean = false;2930private quickpick: QuickPick<QuickPickItem & { remoteSource?: RemoteSource }> | undefined;3132constructor(private provider: RemoteSourceProvider) { }3334dispose() {35this.disposables.forEach(d => d.dispose());36this.disposables = [];37this.quickpick = undefined;38this.isDisposed = true;39}4041private ensureQuickPick() {42if (!this.quickpick) {43this.quickpick = window.createQuickPick();44this.disposables.push(this.quickpick);45this.quickpick.ignoreFocusOut = true;46this.disposables.push(this.quickpick.onDidHide(() => this.dispose()));47if (this.provider.supportsQuery) {48this.quickpick.placeholder = this.provider.placeholder ?? l10n.t('Repository name (type to search)');49this.disposables.push(this.quickpick.onDidChangeValue(this.onDidChangeValue, this));50} else {51this.quickpick.placeholder = this.provider.placeholder ?? l10n.t('Repository name');52}53}54}5556@debounce(300)57private onDidChangeValue(): void {58this.query();59}6061@throttle62private async query(): Promise<void> {63try {64if (this.isDisposed) {65return;66}67this.ensureQuickPick();68this.quickpick!.busy = true;69this.quickpick!.show();7071const remoteSources = await this.provider.getRemoteSources(this.quickpick?.value) || [];72// The user may have cancelled the picker in the meantime73if (this.isDisposed) {74return;75}7677if (remoteSources.length === 0) {78this.quickpick!.items = [{79label: l10n.t('No remote repositories found.'),80alwaysShow: true81}];82} else {83this.quickpick!.items = remoteSources.map(remoteSource => ({84label: remoteSource.icon ? `$(${remoteSource.icon}) ${remoteSource.name}` : remoteSource.name,85description: remoteSource.description || (typeof remoteSource.url === 'string' ? remoteSource.url : remoteSource.url[0]),86detail: remoteSource.detail,87remoteSource,88alwaysShow: true89}));90}91} catch (err) {92this.quickpick!.items = [{ label: l10n.t('{0} Error: {1}', '$(error)', err.message), alwaysShow: true }];93console.error(err);94} finally {95if (!this.isDisposed) {96this.quickpick!.busy = false;97}98}99}100101async pick(): Promise<RemoteSource | undefined> {102await this.query();103if (this.isDisposed) {104return;105}106const result = await getQuickPickResult(this.quickpick!);107return result?.remoteSource;108}109}110111export async function getRemoteSourceActions(model: Model, url: string): Promise<RemoteSourceAction[]> {112const providers = model.getRemoteProviders();113114const remoteSourceActions = [];115for (const provider of providers) {116const providerActions = await provider.getRemoteSourceActions?.(url);117if (providerActions?.length) {118remoteSourceActions.push(...providerActions);119}120}121122return remoteSourceActions;123}124125export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions): Promise<string | PickRemoteSourceResult | undefined>;126export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions & { branch?: false | undefined }): Promise<string | undefined>;127export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions & { branch: true }): Promise<PickRemoteSourceResult | undefined>;128export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions = {}): Promise<string | PickRemoteSourceResult | undefined> {129const quickpick = window.createQuickPick<(QuickPickItem & { provider?: RemoteSourceProvider; url?: string })>();130quickpick.title = options.title;131132if (options.providerName) {133const provider = model.getRemoteProviders()134.filter(provider => provider.name === options.providerName)[0];135136if (provider) {137return await pickProviderSource(provider, options);138}139}140141const remoteProviders = model.getRemoteProviders()142.map(provider => ({ label: (provider.icon ? `$(${provider.icon}) ` : '') + (options.providerLabel ? options.providerLabel(provider) : provider.name), alwaysShow: true, provider }));143144const recentSources: (QuickPickItem & { url?: string; timestamp: number })[] = [];145if (options.showRecentSources) {146for (const { provider } of remoteProviders) {147const sources = (await provider.getRecentRemoteSources?.() ?? []).map((item) => {148return {149...item,150label: (item.icon ? `$(${item.icon}) ` : '') + item.name,151url: typeof item.url === 'string' ? item.url : item.url[0],152};153});154recentSources.push(...sources);155}156}157158const items = [159{ kind: QuickPickItemKind.Separator, label: l10n.t('remote sources') },160...remoteProviders,161{ kind: QuickPickItemKind.Separator, label: l10n.t('recently opened') },162...recentSources.sort((a, b) => b.timestamp - a.timestamp)163];164165quickpick.placeholder = options.placeholder ?? (remoteProviders.length === 0166? l10n.t('Provide repository URL')167: l10n.t('Provide repository URL or pick a repository source.'));168169const updatePicks = (value?: string) => {170if (value) {171const label = (typeof options.urlLabel === 'string' ? options.urlLabel : options.urlLabel?.(value)) ?? l10n.t('URL');172quickpick.items = [{173label: label,174description: value,175alwaysShow: true,176url: value177},178...items179];180} else {181quickpick.items = items;182}183};184185quickpick.onDidChangeValue(updatePicks);186updatePicks();187188const result = await getQuickPickResult(quickpick);189190if (result) {191if (result.url) {192return result.url;193} else if (result.provider) {194return await pickProviderSource(result.provider, options);195}196}197198return undefined;199}200201async function pickProviderSource(provider: RemoteSourceProvider, options: PickRemoteSourceOptions = {}): Promise<string | PickRemoteSourceResult | undefined> {202const quickpick = new RemoteSourceProviderQuickPick(provider);203const remote = await quickpick.pick();204quickpick.dispose();205206let url: string | undefined;207208if (remote) {209if (typeof remote.url === 'string') {210url = remote.url;211} else if (remote.url.length > 0) {212url = await window.showQuickPick(remote.url, { ignoreFocusOut: true, placeHolder: l10n.t('Choose a URL to clone from.') });213}214}215216if (!url || !options.branch) {217return url;218}219220if (!provider.getBranches) {221return { url };222}223224const branches = await provider.getBranches(url);225226if (!branches) {227return { url };228}229230const branch = await window.showQuickPick(branches, {231placeHolder: l10n.t('Branch name')232});233234if (!branch) {235return { url };236}237238return { url, branch };239}240241242