From 9fed2f7eb336339038ae713b81bc5e1ef2238af1 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Fri, 22 Apr 2022 07:25:06 -0700 Subject: [PATCH] Enrich Git extension's remote source provider API (#147613) * Allow custom title in remote source picker * Include forks in query search results * Support `RemoteSource.detail` * Allow showing quickpicks in `getRemoteSources` * Allow custom placeholder and remote source icons * Add ability to customize placeholder * Register and show recently opened sources * Allow custom remote url labels * Add a separator label for remote sources * Update git-base typings * Make showing recent sources opt in * Add concept of recent remote source to `RemoteSourceProvider` concept * Recent sources should be sorted by timestamp * Pass current query to `getRemoteSources` * Fix applying query --- extensions/git-base/src/api/git-base.d.ts | 17 +++- extensions/git-base/src/remoteSource.ts | 81 +++++++++++++------ extensions/github/src/remoteSourceProvider.ts | 6 +- extensions/github/src/typings/git-base.d.ts | 17 +++- 4 files changed, 92 insertions(+), 29 deletions(-) diff --git a/extensions/git-base/src/api/git-base.d.ts b/extensions/git-base/src/api/git-base.d.ts index 70ac3b1b972..8510df6d043 100644 --- a/extensions/git-base/src/api/git-base.d.ts +++ b/extensions/git-base/src/api/git-base.d.ts @@ -31,9 +31,12 @@ export interface GitBaseExtension { export interface PickRemoteSourceOptions { readonly providerLabel?: (provider: RemoteSourceProvider) => string; - readonly urlLabel?: string; + readonly urlLabel?: string | ((url: string) => string); readonly providerName?: string; + readonly title?: string; + readonly placeholder?: string; readonly branch?: boolean; // then result is PickRemoteSourceResult + readonly showRecentSources?: boolean; } export interface PickRemoteSourceResult { @@ -44,17 +47,29 @@ export interface PickRemoteSourceResult { export interface RemoteSource { readonly name: string; readonly description?: string; + readonly detail?: string; + /** + * Codicon name + */ + readonly icon?: string; readonly url: string | string[]; } +export interface RecentRemoteSource extends RemoteSource { + readonly timestamp: number; +} + export interface RemoteSourceProvider { readonly name: string; /** * Codicon name */ readonly icon?: string; + readonly label?: string; + readonly placeholder?: string; readonly supportsQuery?: boolean; getBranches?(url: string): ProviderResult; + getRecentRemoteSources?(query?: string): ProviderResult; getRemoteSources(query?: string): ProviderResult; } diff --git a/extensions/git-base/src/remoteSource.ts b/extensions/git-base/src/remoteSource.ts index 50dec70863e..83e83ae1fa9 100644 --- a/extensions/git-base/src/remoteSource.ts +++ b/extensions/git-base/src/remoteSource.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { QuickPickItem, window, QuickPick } from 'vscode'; +import { QuickPickItem, window, QuickPick, QuickPickItemKind } from 'vscode'; import * as nls from 'vscode-nls'; import { RemoteSourceProvider, RemoteSource, PickRemoteSourceOptions, PickRemoteSourceResult } from './api/git-base'; import { Model } from './model'; @@ -24,17 +24,20 @@ async function getQuickPickResult(quickpick: QuickPick< class RemoteSourceProviderQuickPick { - private quickpick: QuickPick; + private quickpick: QuickPick | undefined; - constructor(private provider: RemoteSourceProvider) { - this.quickpick = window.createQuickPick(); - this.quickpick.ignoreFocusOut = true; + constructor(private provider: RemoteSourceProvider) { } - if (provider.supportsQuery) { - this.quickpick.placeholder = localize('type to search', "Repository name (type to search)"); - this.quickpick.onDidChangeValue(this.onDidChangeValue, this); - } else { - this.quickpick.placeholder = localize('type to filter', "Repository name"); + private ensureQuickPick() { + if (!this.quickpick) { + this.quickpick = window.createQuickPick(); + this.quickpick.ignoreFocusOut = true; + if (this.provider.supportsQuery) { + this.quickpick.placeholder = this.provider.placeholder ?? localize('type to search', "Repository name (type to search)"); + this.quickpick.onDidChangeValue(this.onDidChangeValue, this); + } else { + this.quickpick.placeholder = this.provider.placeholder ?? localize('type to filter', "Repository name"); + } } } @@ -45,35 +48,37 @@ class RemoteSourceProviderQuickPick { @throttle private async query(): Promise { - this.quickpick.busy = true; - try { - const remoteSources = await this.provider.getRemoteSources(this.quickpick.value) || []; + const remoteSources = await this.provider.getRemoteSources(this.quickpick?.value) || []; + + this.ensureQuickPick(); + this.quickpick!.show(); if (remoteSources.length === 0) { - this.quickpick.items = [{ + this.quickpick!.items = [{ label: localize('none found', "No remote repositories found."), alwaysShow: true }]; } else { - this.quickpick.items = remoteSources.map(remoteSource => ({ - label: remoteSource.name, + this.quickpick!.items = remoteSources.map(remoteSource => ({ + label: remoteSource.icon ? `$(${remoteSource.icon}) ${remoteSource.name}` : remoteSource.name, description: remoteSource.description || (typeof remoteSource.url === 'string' ? remoteSource.url : remoteSource.url[0]), + detail: remoteSource.detail, remoteSource, alwaysShow: true })); } } catch (err) { - this.quickpick.items = [{ label: localize('error', "$(error) Error: {0}", err.message), alwaysShow: true }]; + this.quickpick!.items = [{ label: localize('error', "$(error) Error: {0}", err.message), alwaysShow: true }]; console.error(err); } finally { - this.quickpick.busy = false; + this.quickpick!.busy = false; } } async pick(): Promise { - this.query(); - const result = await getQuickPickResult(this.quickpick); + await this.query(); + const result = await getQuickPickResult(this.quickpick!); return result?.remoteSource; } } @@ -83,6 +88,7 @@ export async function pickRemoteSource(model: Model, options: PickRemoteSourceOp export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions = {}): Promise { const quickpick = window.createQuickPick<(QuickPickItem & { provider?: RemoteSourceProvider; url?: string })>(); quickpick.ignoreFocusOut = true; + quickpick.title = options.title; if (options.providerName) { const provider = model.getRemoteProviders() @@ -93,24 +99,47 @@ export async function pickRemoteSource(model: Model, options: PickRemoteSourceOp } } - const providers = model.getRemoteProviders() + const remoteProviders = model.getRemoteProviders() .map(provider => ({ label: (provider.icon ? `$(${provider.icon}) ` : '') + (options.providerLabel ? options.providerLabel(provider) : provider.name), alwaysShow: true, provider })); - quickpick.placeholder = providers.length === 0 + const recentSources: (QuickPickItem & { url?: string; timestamp: number })[] = []; + if (options.showRecentSources) { + for (const { provider } of remoteProviders) { + const sources = (await provider.getRecentRemoteSources?.() ?? []).map((item) => { + return { + ...item, + label: (item.icon ? `$(${item.icon}) ` : '') + item.name, + url: typeof item.url === 'string' ? item.url : item.url[0], + }; + }); + recentSources.push(...sources); + } + } + + const items = [ + { kind: QuickPickItemKind.Separator, label: localize('remote sources', 'remote sources') }, + ...remoteProviders, + { kind: QuickPickItemKind.Separator, label: localize('recently opened', 'recently opened') }, + ...recentSources.sort((a, b) => b.timestamp - a.timestamp) + ]; + + quickpick.placeholder = options.placeholder ?? (remoteProviders.length === 0 ? localize('provide url', "Provide repository URL") - : localize('provide url or pick', "Provide repository URL or pick a repository source."); + : localize('provide url or pick', "Provide repository URL or pick a repository source.")); const updatePicks = (value?: string) => { if (value) { + const label = (typeof options.urlLabel === 'string' ? options.urlLabel : options.urlLabel?.(value)) ?? localize('url', "URL"); quickpick.items = [{ - label: options.urlLabel ?? localize('url', "URL"), + label: label, description: value, alwaysShow: true, url: value }, - ...providers]; + ...items + ]; } else { - quickpick.items = providers; + quickpick.items = items; } }; diff --git a/extensions/github/src/remoteSourceProvider.ts b/extensions/github/src/remoteSourceProvider.ts index 6e0b5fadbf7..c7e40884179 100644 --- a/extensions/github/src/remoteSourceProvider.ts +++ b/extensions/github/src/remoteSourceProvider.ts @@ -18,7 +18,9 @@ function asRemoteSource(raw: any): RemoteSource { const protocol = workspace.getConfiguration('github').get<'https' | 'ssh'>('gitProtocol'); return { name: `$(github) ${raw.full_name}`, - description: raw.description || undefined, + description: `${raw.stargazers_count > 0 ? `$(star-full) ${raw.stargazers_count}` : '' + }`, + detail: raw.description || undefined, url: protocol === 'https' ? raw.clone_url : raw.ssh_url }; } @@ -75,6 +77,8 @@ export class GithubRemoteSourceProvider implements RemoteSourceProvider { return []; } + query += ` fork:true`; + const raw = await octokit.search.repos({ q: query, sort: 'stars' }); return raw.data.items.map(asRemoteSource); } diff --git a/extensions/github/src/typings/git-base.d.ts b/extensions/github/src/typings/git-base.d.ts index 70ac3b1b972..8510df6d043 100644 --- a/extensions/github/src/typings/git-base.d.ts +++ b/extensions/github/src/typings/git-base.d.ts @@ -31,9 +31,12 @@ export interface GitBaseExtension { export interface PickRemoteSourceOptions { readonly providerLabel?: (provider: RemoteSourceProvider) => string; - readonly urlLabel?: string; + readonly urlLabel?: string | ((url: string) => string); readonly providerName?: string; + readonly title?: string; + readonly placeholder?: string; readonly branch?: boolean; // then result is PickRemoteSourceResult + readonly showRecentSources?: boolean; } export interface PickRemoteSourceResult { @@ -44,17 +47,29 @@ export interface PickRemoteSourceResult { export interface RemoteSource { readonly name: string; readonly description?: string; + readonly detail?: string; + /** + * Codicon name + */ + readonly icon?: string; readonly url: string | string[]; } +export interface RecentRemoteSource extends RemoteSource { + readonly timestamp: number; +} + export interface RemoteSourceProvider { readonly name: string; /** * Codicon name */ readonly icon?: string; + readonly label?: string; + readonly placeholder?: string; readonly supportsQuery?: boolean; getBranches?(url: string): ProviderResult; + getRecentRemoteSources?(query?: string): ProviderResult; getRemoteSources(query?: string): ProviderResult; }